1、数组的概念
有人说,数组有什么可讲的,不就是一堆相同数据类型元素类型的集合吗,确实是这样,在我学习学校C语言的时候,我清晰的记得我们老师很笼统的讲了一下基本知识,并没有深剖,如果你认为数组简单,但它确实有你值得研究的地方。就好比现在很多大学生学完C语言居然分不清数组名和取地址数组名的区别。
数组简单的使用并遍历打印:
#define N 10 int main() { int arr[N] = { 1,2,3,4,5,6,7,8,9,10 }; int i = 0; for (i = 0; i < N; i++) { printf("%d ", arr[i]); } return 0; }
这里我们要说几个点:
- 在C99语法之前,[ ] 中必须是常量值,不能支持变量,在C99标准才支持了变长数组的概念
- 数组在定义时如果没有完全初始化,其他默认初始化为 0
- 如果定义数组你实在不知道初始化成什么,建议初始化为 0 即可,不然会成随机值
- 如果没有指定数组元素个数,则会根据初始化内容来确定元素个数
2、数组的内存布局
在讨论数组内存布局之前,我们首先要了解栈区的空间是如何使用的,通过一串代码我们来看一下:
这里我们发现,先定义的变量,地址是比较大的,后续变量一次减小,这是为什么呢?
首先a,b,c 变量都是在 main 函数中定义的,也就是在栈上开辟的临时变量,而 a 先定义则意味着需要先被开辟空间,那么 a 就先入栈,也就是我们之前所说到,栈是整体向下的,先使用高地址后使用低地址。
紧随着我们来看看数组在内存中的布局,如果按照我们上面的理解,arr[0] 的地址肯定是大于后面元素的地址,那么到底是不是这样呢?
这里我们发现,先定义的 a 的地址确实比后定义的数组所有地址都要大,这个符合我们的预期,但是为什么 arr[0] 的地址却小于后面下标的元素呢?我们先来画图看看他们的内存布局:
所以这里我们能得出:数组是线性且连续增长的,也就是数组下标随着地址的增长而增长,如果我们站在开辟空间的角度,不应该把数组认为一个个独立的元素,应该整体开辟空间!
这里有的小伙伴可能会想,这打印出来的明明不是连续的啊,从arr[0]地址处00F3FE64 到arr[1]地址处00F3FE68这中间明明隔了四个字节啊,这是因为我们是整型数组,而且 '&' 取地址取出的是元素第一个字节的地址,也就是我们上期说的取出的是最小的地址。如果你换成 char 类型数组的话,那么就很显而易见了,感兴趣的下来自己尝试一番。
3、数组名到底是什么?
其实对于这个概念,可能有很多小伙伴还模糊不清,特别是对于刚接触C语言的来说,要弄明白数组到底表示什么,其实很简单,仔细的往后看:
通过这个例子,我们可以明显看出,数组名代表首元素地址,同时 arr+1 也是跳过一个整型,也即 对应着 arr[0] 的地址,所以我们可以简单的得出一个暂定结论:数组名代表首元素地址。
这里我要告诉大家,数组名确实是表示首元素地址,但是有两个例外:
- &数组名:取出的是整个数组的地址(后面有例子)
- sizeof(数组名):一定是sizeof() 里面单独放数组名,计算的是整个数组的大小
除了以上两个例外,其他任何地方你见到的数组名都是代表数组首元素地址。
以上的例子只是证明数组名代表首元素地址的例子,我们下面就来看如上所述两个例外的例子:
如上例子确实证明了我们的结论:当 sizeof() 括号内部单独放数组名时,计算的是整个数组的大小,而 &数组名 取出的是整个数组的地址,如何看呢?打印地址打印的是 16进制,我们用 &arr + 1 的结果减去 &arr 的结果,得到的是 28,转换成对应的 10进制 刚好是 40,也就是说,我们 &arr + 1 跳过的是整个数组的大小,同理他也代表整个数组的地址。
这里有的小伙伴可能又有疑问了,指针+1 不是表示跳过对应类型大小的字节吗?那 arr 数组每个元素的类型是 int 啊,怎么不是跳过 4 个字节呢?
这里我们要明白,arr 数组每个元素的类型确实是 int,但是这个数组的类型是 int [10] ,如何理解呢,就好比我们定义一个指针变量,int* p,可以说 p 的类型是 int*,也就是整型指针类型,同理,我们 int arr[10],那么他的类型是不是可以这么理解:这个数组有 10个 int 类型的元素,所以他的类型也就可以理解成 int[10]。也就是 int[10] 的类型大小是 40 字节,但是我们是 &arr,它表示的是整个数组的地址,所以就可以理解成,取地址取出的指针,他指向的是整个数组,那个数组有十个元素,也就是一个指向数组的指针,即 int(*arr)[10] (下期会讲),他的类型就是 int(*)[10] ,那对应的 指针+1 也应该跳过整个数组,所以&arr+1,相较于 &arr 的差值是 40,指针就是地址,我取出数组的地址,也就是一个指针罢了。
4、数组名做左值和右值的区别
对于刚初识C语言的小伙伴,可能对数组还是马马虎虎的,会认为数组名可以当成变量一样去使用,去给他赋值,其实这种认识是错误的,我们看一个例子就知道了:
int main() { int arr[10] = { 0 }; //赋值成 1~10 arr = { 1,2,3,4,5,6,7,8,9,10 };//err return 0; }
这样直接拿起数组名赋值显然是错误的,为什么呢?前面我们就讲到,数组名大部分情况表示的都是数组首元素地址,而地址是个常量,是无法被修改的,在C语言的语法层面上就已经不允许这样操作了。
上面是数组名做左值的例子,显然这个例子是错误的,那么当数组名做右值又是什么样的含义呢?
int main() { int arr[10] = { 0 }; int* p = arr; return 0; }
还是那句话,数组名代表首元素地址,但是有两个例外,但是如上代码的例子显然不是例外当中的,所以他代表的就是 arr[0] 的地址,这样一番操作,就让我们指针变量 p,有权限去访问数组的元素也可以做修改。
总结:数组名做左值是错误的,地址是常量并不能被修改或赋值,数组名做右值,本质等价于 &arr[0]
5、以指针的形式访问和以数组的形式访问
既然上面我们提到数组名表示首元素地址,而地址就是指针,所以我们是不是也可以通过解引用地址的形式来访问数组的元素呢? 好比 arr 代表首元素的地址,那我们 *(arr) 是不是就可以访问数组的第一个元素呢?是的!如果我们 *(arr + 1),也就是代表着访问数组的第二个元素,那我们设定一个变量 i,i 每次递增1,这样下去就可以达到遍历数组的效果!(为了方便下个例子,我们本例采用字符串数组演示)
显而易见,我们确实可以通过指针的形式去访问数组,同时数组也可以用 [ ] 引用下标的方式来访问,而且数组名代表着首元素地址,地址就是指针,那我们能不能将指针变量和 [ ] 引用下标的方式结合起来呢? 【指针变量里面放的就是地址(指针)】
看到这,我相信你对数组和指针又有了更深的理解,这里我们要说一点,上述的测试用例 arr 和 str 其实是有区别的,arr 的整个数组都是在栈上保存,可以被修改,虽然 str 指针变量也是在栈上保存的,但是他存放的是一个常量字符串的首地址,常量字符串放在字符常量区,不可以被修改。
这里有个问题,指针和数组的语法访问方式居然那么相似,那他们之间有什么关系呢?
他们没有任何关系!指针就是指针,指针变量在32位系统下永远占四个字节,他存放的值是某一个内存的地址,而数组就是数组,其大小与元素的类型和个数有关。
从语法层面看,他们的访问方式可以一样,但是从底层来看他们的寻址方案是完全不同的!
总结: 指针和数组指向或表示同一块空间的时候,访问方式具有相似性,你可以用指针解引用的方式,也可以用 [ ] 下标引用方案。但是我们也要明白,他们是指针和数组是两个东西,不能混为一谈。
6、为什么C语言要把指针和数组元素访问互通呢?
void PrintfArr_1(int arr[], int n) { int i = 0; for (i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); } void PrintfArr_2(int* ptr, int n) { int i = 0; for (i = 0; i < n; i++) { printf("%d ", *(ptr + i)); } printf("\n"); } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; PrintfArr_1(arr, 10); PrintfArr_2(arr, 10); return 0; }
上面的代码并没有什么问题,不过,大家可以发现,如果没有将指针和数组元素打通的话,那么在C语言中,如果有大量的函数调用而且有大量的传参,会要求程序员进行各种访问习惯的变化,只要是要求人做的,那么就有提升代码的出错率和调试的难度。
所以干脆,C语言将指针和数组的访问方式打通,让程序员在函数内,也好像可以使用数组那样进行元素访问,本质减少了编程难度。