也许你认为,C语言中的数组非常好理解,就是把一组相同类型的元素存储在同一块空间里。但是你可能并没有真正理解数组的本质,不信的话请回答一下下面的几个小问题,如果你能非常清晰的回答这些问题,那么你对C语言中的数组的理解就入门了。
- 一维数组和二维数组在定义时,哪些大小可以省略,哪些不可以省略?如果可以省略,在什么时候是可以省略的呢?
- 一维数组和二维数组在内存中是如何存储的?
- 一维数组和二维数组的数组名分别表示什么意思?
- sizeof(数组名) 和 &数组名 分别表示什么?
- 一维数组和二维数组传参时,形参应该如何写?
- 数组和指针有什么联系?有什么区别?
- 如何使用指针来遍历一维数组和二维数组?
- 如何理解数组指针和指针数组?
- 什么是柔性数组?
如果你想了解这些内容,那就花点时间阅读下本篇博客吧,希望你能有所收获。
什么是数组
数组,指的是相同类型的元素的集合,比如,想要存储10个整数,除了定义10个int类型的变量,也可以定义一个数组来存储。
数组分为一维数组和二维数组,三维以上的数组就不常见了,所以本篇博客重点讨论一维数组和二维数组。
所谓一维数组,就是很正常的把一些元素放到一起。比如存储1~10,就这么存储:[1 2 3 4 5 6 7 8 9 10]。
而二维数组是有行和列的,比如,用一个3行5列的二维数组来存储1~15,可以这么理解:
数组的定义和初始化
数组的定义和初始化是要按照指定的个数来的。
对于一维数组,只需要表示出数组名、数组容量和数组元素类型即可。比如,一个能存储10个int类型的数组就应该这么定义:int arr[10];
这样的数组如果是一个局部的数组,也就是在大括号内定义的话,默认会被初始化为随机值。如果你想要手动初始化,比如把1~10放进去,可以这么写:
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
如果手动进行初始化,可以省略数组的元素个数,编译器会根据你初始化的元素个数来给数组开辟空间。
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
以上代码中,虽然我把表示数组大小的方括号里的10给省略了,编译器也会开辟一个能够存储10个int数据的数组,因为我手动给这个数组初始化了。
当然,我也可以只给数组的部分元素初始化,比如:
int arr[10] = {1, 2, 3};
如果像上面这么写,这个数组就只会初始化前3个元素为1~3,剩下的7个元素会被默认初始化成0,所以数组实际存储的是:[1 2 3 0 0 0 0 0 0 0]。
一个很常见的写法是,把整个数组都初始化成全0。
int arr[10] = {0};
根据我们对一位数组不完全初始化的理解,如果像上面这么写,就只把第一个元素初始化为0,其余9个元素会被默认初始化为0,其实效果就是把整个数组的所有元素都初始化为0。
讲完了一维数组,那二维数组呢?
二维数组的定义需要指定行数和列数,比如一个3行5列的数组应该这么定义:int arr[3][5];
如果要对其进行初始化,比如初始化为:第一行1~5,第二行6~10,第三行11~15,就这么写:
int arr[3][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
效果可以理解成:
也可以把每一行也用大括号括起来,这样更加清晰:
int arr[3][5] = { {1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}, {11, 12, 13, 14, 15} };
建议再换个行:
int arr[3][5] = { {1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15} };
那能不能像一维数组那样省略大小呢?答案是:行可以省略,列不能省略!也就是说,可以省略行,写成这样:
int arr[][5] = { {1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15} };
编译器看到后,就会理解成,由于你放了3行,所以行数就是3。
同样,也可以进行不完全初始化,比如:
int arr[3][5] = { {1, 2, 3, 4, 5},
{6, 7, 8, 9, 10} };
由于只放了2行,第三行就会被默认初始化为0,也就是这样的布局:
当然每一行也可以不完全初始化,比如:
int arr[3][5] = { {1, 2, 3},
{6, 7} };
效果就是,第一行前3个元素初始化为1~3,后2个元素初始化为0,第二行的前2个元素初始化为6和7,后3个元素初始化为0,第三行全部初始化为0。
当然,如果内层的大括号省略掉,也有类似的效果,不过会一行一行放,放满一行再放下一行,比如:
int arr[3][5] = {1, 2, 3, 4, 5, 6, 7};
会先把第一行放满,第一行就是1~5,第二行前2个元素就是6和7,后3个元素初始化为0,第三行所有元素都是0。
和一维数组一样,如果想把所有元素都初始化为0,就这么写:
int arr[3][5] = {0};
这样就只放了一个0,其余元素都被初始化为0,就相当于把整个数组的所有元素都初始化为0。
数组在内存中的存储
数组在内存中的存储满足以下2点:
- 数组在内存中是连续存储的。
- 随着数组下标的增长,地址是由低到高变化的。
先举一个一维数组的例子。写一个程序,把一维数组的每一项的地址都打印出来:
#include <stdio.h>
int main()
{
int arr[5];
for (int i = 0; i < 5; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
输出结果如下:
观察到,数组的相邻2项之间的地址都相差了4个字节。为啥是4个字节呢?因为每个int是4个字节,由于数组在内存中是连续存储的,相邻两项的地址自然隔了4个字节。除此之外,随着数组下标从0增长到4,地址也是从低到高变化的。
再举一个二维数组的例子:
#include <stdio.h>
int main()
{
int arr[3][5];
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 5; ++j)
{
printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
}
return 0;
}
输出结果如下:
相邻2项的地址仍然间隔4个字节,且随着下标的增长,地址也在增长,这说明数组的内存分布规律同样适用于二维数组。
神奇的数组名
先说结论。在C语言中,数组名表示数组首元素地址,但是有2个例外:
- sizeof(数组名),数组名直接放在sizeof()内部,此时数组名不表示首元素地址,而是表示整个数组,求的是整个数组的大小,单位是字节。
- &数组名,在&符号后面的数组名,此时数组名不表示首元素地址,而是表示整个数组,取出的是整个数组的地址。详见后面讲解的“数组指针”。
关于sizeof,大家应该很熟悉了,可以计算类型的大小。如果想要计算数组的大小,就sizeof(数组名)即可。比如:
int arr[10];
printf("%d\n", sizeof(arr)); // 40
以上代码中,由于数组有10个元素,每个元素是int,数组总大小是40个字节,所以会输出40。
下面来对比一下3个代码:
int arr[10];
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
其实3个玩意打印出来结果是一样的。但是我们要理解它们的本质:
- arr是数组名,数组名表示数组首元素地址,也就是arr[0]的地址。
- &arr[0],就是数组首元素的地址。
- &arr,数组名表示整个数组,取出的是整个数组的地址。
“整个数组的地址”和“数组首元素地址”在值上是一样的。但是如果+1,或者解引用,效果是不一样的,这就要牵扯对指针的理解了。指针类型决定了指针+1时跳过的步长。比如一个int*的指针+1会跳过4个字节,也就是跳过一个int,而一个char*的指针+1会跳过1个字节,也就是跳过了一个char。
在上面的例子中,arr和&arr[0]都表示数组首元素的地址,也就是一个int的地址,类型是int*,+1后会跳过一个int,也就是跳过4个字节。而&arr表示整个数组的地址,如果+1,会跳过整个数组。观察一下以下程序:
#include <stdio.h>
int main()
{
int arr[10];
printf(" arr = %p\n", arr);
printf(" arr + 1 = %p\n", arr + 1);
printf(" &arr[0] = %p\n", &arr[0]);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
printf(" &arr = %p\n", &arr);
printf(" &arr + 1 = %p\n", &arr + 1);
return 0;
}
输出结果如下:
观察到确实是这样的。arr+1和&arr[0]+1都跳过4个字节,而&arr+1跳过了16进制的0x28,也就是10进制的40个字节。
那二维数组的数组名表示什么呢?实际上,二维数组的数组名也表示首元素的地址,而二维数组的首元素是第一行,也就是说,二维数组的数组名表示第一行的地址。比如一个二维数组是int arr[3][5];
,数组名arr表示的是第一行的地址,也就是一个容量是5个int的一维数组的地址,类型就是int (*)[5]。
数组和指针的区别和联系
数组和指针有什么区别呢?这个问题其实很奇怪,因为这2个玩意完全就不是一个东西。数组是一组相同类型的元素的集合,而指针是用来存储地址的,它们八竿子打不着。但是确实存在着一个关联,那就是前面讲解到的:数组名表示数组首元素的地址。根据这一点,就可以拿到一个指向数组首元素的指针。又由于数组在内存中是连续存放的,就可以通过这个指针来遍历这个数组。比如:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
while (p < arr + 10)
{
printf("%d ", *p++);
}
return 0;
}
输出结果如下:
数组传参
有些时候,我们要把数组作为实参,传递给函数,函数的形参应该如何写呢?
先看一维数组传参。假设有这样的场景:
int arr[10];
test(arr);
此时函数test的形参应该如何写呢?
- 我们传过去了一个数组,自然可以用一个数组来接收:
void test(int arr[10]);
- 数组的大小可以省略,而且建议省略:
void test(int arr[]);
- 由于数组名表示首元素地址,我们本质上是传过去了一个地址,所以应该用一个指针来接收:
void test(int* arr);
- 既然传过去的是地址,那前2种写法中,数组的大小重要吗?一点都不重要,所以你哪怕乱写其实也不会出问题,比如:
void test(int arr[1000]);
接着看二维数组传参:
int arr[3][5];
test(arr);
此时test的形参应该如何写呢?
- 我们传过去了一个数组,自然可以用一个数组来接收:
void test(int arr[3][5]);
- 注意:二维数组的行可以省略,但是列不能省略!比如:
void test(int arr[][5]);
- 数组名表示首元素地址,二维数组的数组名表示第一行的地址,也就是一个容量为5个int的数组的地址,类型是int (*)[5],形参可以这么写:
void test(int (*arr)[5]);
- 既然传过去的是地址,那前2种写法中,数组的大小重要吗?一点都不重要,所以你哪怕乱写其实也不会出问题,但是在二维数组中,列是很重要的,必须写对,但是行随便。比如:
void test(int arr[1000][5]);
指针数组和数组指针
指针数组,本质是数组,只不过存放的是指针。比如:int* arr[10];
就是一个指针数组,能存储10个int*类型的指针。
数组指针,本质是指针,只不过是指向数组的指针,存放的是数组的地址。详见我之前写过的一篇博客,戳这里跳转。
柔性数组
柔性数组是结构体内的大小可以变化的数组,这个知识点和动态内存管理也有联系,这里展开讲解就太长了。还好我之前写了一篇博客来讲解这个知识点,戳这里跳转。
总结
现在我们可以回答开头的问题了。
- 一维数组和二维数组在定义时,哪些大小可以省略,哪些不可以省略?如果可以省略,在什么时候是可以省略的呢?一维数组如果初始化,可以不指定大小,编译器会根据初始化的情况来给数组开辟空间。二维数组如果初始化,行可以省略,但是列不能省略,编译器会根据初始化的情况来决定有几行。
- 一维数组和二维数组在内存中是如何存储的?数组在内存中是连续存放的,随着数组下标的增长,地址是由低到高变化的,这点对一维数组和二维数组都适用。
- 一维数组和二维数组的数组名分别表示什么意思?数组名表示首元素地址,但是有2个例外:sizeof(数组名),&数组名,数组名都表示整个数组。除此之外,数组名都表示首元素地址,其中二维数组的“首元素”指的是第一行。
- sizeof(数组名) 和 &数组名 分别表示什么?sizeof(数组名)求的是整个数组的大小,&数组名取出来的是整个数组的地址。
- 一维数组和二维数组传参时,形参应该如何写?可以用数组接收,一维数组的大小可以省略,二维数组的行可以省略,列不能省略。对于省略的大小,其实乱写也是符合语法的,但是不建议。除此之外,根据“数组名表示数组首元素地址”这个知识点,也可以用指针接收。
- 数组和指针有什么联系?有什么区别?数组是一组相同类型元素的集合,指针是存储地址的。它们之间被“数组名”这个桥梁关联起来,因为数组名表示数组首元素地址。
- 如何使用指针来遍历一维数组和二维数组?根据数组在内存中是连续存储的,只要拿到首元素地址,就能遍历整个数组。
- 如何理解数组指针和指针数组?数组指针是一个指针,存储的是数组的地址。指针数组是一个数组,存储的元素类型是指针。
- 什么是柔性数组?柔性数组是结构体内的最后一个成员数组,且大小可以变化(大小不确定)的。管理柔性数组,要使用动态内存管理的方式。
感谢大家的阅读!