你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

数组和指针有什么区别?

[复制链接]
gaosmile 发布时间:2020-8-11 20:18
一、能力错觉

当书本(或谷歌)摆在眼前时,大脑会产生错觉,以为学习材料也同样存入了大脑,阅读毕竟比回想简单多了。

以为反复的阅读资料就是自己已经掌握知识,这就是能力错觉

解决能力错觉的方法:

  • 积极回想——让大脑提取关键概念,而非通过重复阅读被动地获取知识,这样才能高效地学习。

现在网络上盛行各种it类的视频教程,我不否认不少视频教程是高质量的,但是所有视频类资料都有一个问题:

  • 可以让人不用阅读书籍,减少思考,只要被动地听老师们讲课就能舒舒服服地获取到知识,这很容易会让某些初级的软件开发人员形成能力错觉,仿佛视频里写过的代码,解决过的 bug 都是自己已经学到的知识似的。

有效的解决办法是

  • 公开学习笔记和练习代码。公开学习笔记的目的是借助外部压力,高效回想,进而提高自己的学习标准。

  • 另外,公开写作则会给你的写作增加很多维度的外部压力,你会想如何让别人更好地理解我要表达的意思;如何传递更多价值,让别人读完有所收获;如何让更多人看到;如何让别人读得下去;如何排版让大家看得更舒服;



二、数组和指针有什么区别?

正文目录:

1. 用于声明时两者有重大区别
2. 你真的理解声明和定义吗?
3. 数组和指针的底层是如何访问数据的?
4. 哪些场景可以用指针代替数组?
5. 为什么C语言要把数组形参退化为指针?
6. 如何使用指针访问多维数组?
7. 相关面试题

写作目的:

  • 正确看待数组和指针。

测试环境:

  • Ubuntu 16.04
  • Gcc 5.4.0
微信图片_20200811201726.gif
1. 用于声明时两者有重大区别

1) 误导新手的说法:

由于数组和指针的所谓等价性非常接近,不少程序员有时忽视了二者之间的其他重要区别 ,最误导新手的说法之一就是 “数组和指针是相同的",这是一种非常危险的说法。

看下面这个例子:

extern int *x;
extern int x[];

  • 第一条语句声明 x 是个 int 型的指针;

  • 第二条语句声明 x 是个 int 型数组,长度尚未确定,即存储长度在别处定义。


2) 为什么有些人会误以为指针和数组总是可以互换?

最主要原因是
对数组的引用 ( x ) 总是可以写成对指针的引用 ( *(x+i) )

  • 即确实存在一种指针和数组的定义完全相同的上下文环境。不幸的是,这只是数组的一种极为普通的用法,并非所有情况下都是如此。

2. 你真的理解声明和定义吗?

想要要真正理解为什么 extern int *x 不等于 extern int x[],我们首先需要搞清楚什么是声明,什么是定义。

1) 链接器的视角:

  • C 语言中的对象必须有且只有一个定义,但它可以有多个 extern 声明。这里所说的对象跟 C++ 中的对象并无关系,这里说的对象是 从链接器的视角来看的,链接器将各个函数、变量都视为对象

2) 定义和声明的联系与区别:

  • 定义是一种特殊的声明,它创建了一个对象;声明简单地说明了在其他地方创建的对象的名字。

  • 定义只能出现在一个地方,它指定了对象的类型并分配内存以创建新的对象。声明可以多次出现 以描述对象的类型,用于指代其他地方定义的对象,它不为对象分配内存。

  • extern 对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行。由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息 (多维数组例外)。


3) 总结成一句话:

  • 定义 = 声明 + 分配内存 (创建对象)

4) 回过头来看这个例子:

extern int *x;
extern int x[];

前者声明了一个指针,后者声明了一个数组,那么它们对应的指针和数组的定义(最重要的是内存分配) 能相等吗?


3. 数组和指针的底层是如何访问数据的?

现在我们来看看指针和数组的定义与使用。

1) "地址 X (Address)" 和 "地址 X 的内容(Contents of Address)" 之间的区别:

对于"地址 X" 和 "地址 X 的内容",在 C 语言中是用同一个符号来表示这两样东西,由编译器根据上下文环境判断它的具体含义。

2) 看下面这个例子:

X = Y
  • 符号 X 的含义是 X 所代表的地址,它是左值,编译时可知;

  • 符号 Y 的含义是 Y 所代表的地址上的内容,它是右值,运行时才知;

  • 左值包括可修改的左值和不可修改的左值,C 语言中,一般的数据类型都是都可作为可修改的左值,只有数组是不可修改的左值;

  • 数组的地址在编译时可知,编译器有了这个地址 (即数组首地址),就可以直接进行读写操作。而指针必须在运行时取得它的当前值,然后才能对它进行解除引用操作,才能进行读写操作。


3) 数组和指针的访问方式是不同的:

char a[9] = "abcedefgh";
微信图片_20200811201730.png
访问数组
  • 上面这个例子中,a 是一个数组。

  • 在编译器符号表里有一个符号 a ,它的地址为9980;

  • 数组内的字符都可以从这个地址 + 偏移量找到,编译器甚至并不需要知道数组的总长度;


char c = 'F';
char *p = &c;

微信图片_20200811201732.png
使用指针访问字符
  • 上面这个例子中,p 是一个指针。

  • 在编译器符号表中有一个符号 p, 它的地址为 4624;

  • p 指向的对象是一个字符。为了取得这个字符,必须得到地址 p 的内容 (5081),把它作为字符的地址并从这个地址中取得这个字符 ('F')。


4) 当定义为指针 (char *p),并以数组方式 (p) 引用时会发生什么?

char *p = ”abcdefgh”
printf("%c\n", p[3]);

char *a = ”abcdefgh”
printf("%c\n", a[3]);

  • p[3] 和 a[3] 都能成功访问到字符 'd';

  • a 表示 "从 a 的地址开始,前进 i 步,每步都是一个字符(数组类型的长度)”;

  • p 表示 "从 p 所指的地址开始,前进 i 步,每步都是一个字符(即指针所指类型的长度)”;

  • 所以,当你用 extern char *p 来声明 char p[10]时,编译器会把 p 当成一个指针(Address),然后去获取 *(p) (即 Content of Addrss),这时最好的结果是程序立马崩溃,你能快点发现问题。最糟糕的情况是,程序崩溃在将来的某个时刻,你则 debug 到怀疑人生



4. 哪些场景可以用指针代替数组?

数组和指针容易混淆使用的 2 大类场景:

  • 声明

  • 在表达式中使用;


1) 声明:
声明的场景包括 3 种:

  • 1> 不可以的场景:定义也是一种声明,定义数组时不能用指针的形式;

  • 2> 不可以的场景:extern 数组时不能改写成指针的形式, 例如:


int char[10];    // define
extern char a[]; // ok
extern char *a;  // error

  • 3> 可以的场景:函数的形参,用数组形式还是指针形式,随你自己的喜好。

2) 在表达式中使用:

  • 在表达式中,指针形式和数组形式等效。

3) 几条重要的规则:

  • 规则1:"表达式中的数组名" 就是指针;

  • 规则2:把数组下标可当作指针的偏移量;

  • 规则3: "作为函数参数的数组名" 等同于指针;



5. 为什么 C 语言要把数组形参退化为指针?1) 出于效率的考虑:
  • 在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参作一份拷贝并传递给调用的函数,函数不能修改作为实参的实际变量的值)。

  • 如果要拷贝整个数组,无论在时间上还是在内存空间上的开销都可能是非常大的


2) 出于简化编译器的考虑:

  • 在 C 语言中,所有的数组在作为参数传递时都转换为指向数组起始地址的指针,而其他的参数均采用传值调用。

  • 允许程序员把形参声明为数组 (程序员打算传递给函数的东西) 或者指针 (函数实际所接收到的东西)。在函数内部,编译器始终把它当作一个指向数组第一个元素的指针


3) 看下面这个例子:

static int array[10], array2[10];

static void func1(int *ptr)
{
    ptr[1] = 3;
    *ptr = 3;
    ptr = array2;
}

static void func2(int array[])
{
    array[1] = 3;
    *array = 3;
    array = array2;   // OK, because array is a pointer
    printf("*array=%d\n", *array);
}

int main(void)
{
    func1(array);
    func2(array);
   
    array[1] = 3;
    *array = 3;
    array = array2; // ERROR
    return 0;
}

编译运行:

// main 中调用 array = array2时:
11: error: assignment to expression with array type

// 去掉 main / array = array2时:
$ ./point_array_arg
*array=0


6. 如何使用指针访问多维数组?

1) C 语言的多维数组:

  • 采用最右的下标先变化原则,其最大的用途是存储多个字符串;

  • 单个元素的存储和引用实际上是以线性形式排列在内存中;

  • 不能把一个数组赋值给另一个数组,因为数组作为一个整体不能成为赋值的对象;

  • 可以把数组名赋值给一个指针,是因为在表达式中的数组名被编译器当作一个指针;

  • 指针下标引用的规则告诉我们 pea[j] 被编译器解释为 ((pea + i) + j);

  • 可以通过声明一个一维指针数组 ( (char *)pea[4],下标方括号的优先级比指针的星号高),其中每个指针指向一个字符串,来取得类似二维字符数组的效果;


微信图片_20200811201735.png
指针数组
7. 相关面试题

1) 找错:计算字符串长度

下面这段程序是为了把字符串转换为大写:

#include <stdio.h>

void UpperCase(char str[])
{
    int test = sizeof(str);
    int test2 = sizeof(str[0]);

    for(size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i) {
        if('a'<=str && str<='z')
            str -= ('a'-'A');
    }
}



int main(void)
{

    char str[] = "aBcDeefGHijKL";
    printf("The length of str is %d\n", sizeof(str)/sizeof(str[0]));

    UpperCase(str);
    printf("result: %s\n", str);
    return 0;
}

运行结果:

$ ./sizeof_array
The length of str is 14
result: ABCDEEFGHijKL

问题出在 UpperCase() 里的 sizeof(str),这里的 str 是一个指针而不是数组

正确的写法有2种

  • 给UpperCase 传递长度参数,这是最稳妥的方式。
  • 在UpperCase 内使用strlen 获取字符串长度,这种方法仅适用于以 ‘\0’ 结尾的字符数组;

收藏 评论0 发布时间:2020-8-11 20:18

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版