创建数组
数组是一组相同类型的值,按照顺序储存在一起。数组通过变量名后加方括号表示,方括号里面是数组的成员数量。
int arr[100];
上面示例声明了一个数组arr
,里面包含100个成员,每个成员都是int
类型。注意,声明数组时,必须给出数组的大小。
数组的成员从0
开始编号,所以数组arr[100]
就是从第0号成员一直到第99号成员,最后一个成员的编号会比数组长度小1
。
数组名后面使用方括号指定编号,就可以引用该成员。也可以通过该方式,对该位置进行赋值。
arr[0] = 13;
arr[99] = 42;
上面示例对数组arr
的第一个位置和最后一个位置,进行了赋值。
注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,可能读取或写入预期外内存,非常危险,必须非常小心。
int arr[100];
arr[100] = 51; // 数组越界
上面示例中,数组arr
只有100个成员,因此arr[100]
这个位置是不存在的。但是,引用这个位置并不会报错,会正常运行,使得紧跟在arr
后面的那块内存区域被赋值,而那实际上是其他变量的区域,因此不知不觉就更改了其他变量的值。这很容易引发错误,而且难以发现。
数组也可以在声明时,使用大括号初始化,对每一个成员赋值, 注意使用大括号初始化时,必须在数组声明时赋值,否则编译时会报错。
int arr[5] = {22, 37, 3490, 18, 95};
下面代码中,数组arr
声明之后再进行大括号赋值,导致报错。
int arr[5];
arr = {22, 37, 3490, 18, 95}; // 报错
报错的原因是,C 语言规定,数组变量一旦声明,就不得修改变量指向的地址,具体会在后文解释。由于同样的原因,数组赋值之后,再用大括号修改值,也是不允许的。
int arr[5] = {1, 2, 3, 4, 5};
arr = {22, 37, 3490, 18, 95}; // err
使用大括号初始化,大括号里面的值不能多于数组的长度,否则编译时会报错。如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为0
。
// 两者相等
int a[5] = {22, 37, 3490}; // 剩余元素自动填充0
int a[5] = {22, 37, 3490, 0, 0};
如果要将整个数组的每一个成员都设置为零,最简单的写法就是下面这样,char数组比较特殊,会自动填充'\000'
int a[100] = {0};
数组初始化时,可以指定为哪些位置的成员赋值。
int a[15] = {[2] = 29, [9] = 7, [14] = 48};
上面示例中,数组的2号、9号、14号位置被赋值,其他位置的值都自动设为0。
指定位置的赋值可以不按照顺序,下面的写法与上面的例子是等价的。
int a[15] = {[9] = 7, [14] = 48, [2] = 29};
指定位置的赋值与顺序赋值,可以结合使用。
int a[15] = {1, [5] = 10, 11, [10] = 20, 21}
上面示例中,0号、5号、6号、10号、11号被赋值。
C 语言允许省略方括号里面的数组成员数量,这时将根据大括号里面的值的数量,自动确定数组的长度。
// 两者相等
int a[] = {22, 37, 3490};
int a[3] = {22, 37, 3490};
上面示例中,数组a
的长度,将根据大括号里面的值的数量,确定为3
。
省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。下面面示例中,数组a
的最大下标是9
,所以数组的长度是10。
int a[] = {[2] = 6, [9] = 12};
数组长度
sizeof
运算符计算对象占用内存字节数,传入宿主会返回整个数组内存长度。
int a[] = {22, 37, 3490};
int arrLen = sizeof(a); // 12
上面示例中,sizeof
返回数组字节长度是12
。
由于数组成员都是同一个类型,每个成员的字节长度都是一样的,所以数组整体的字节长度除以某个数组成员的字节长度,就可以得到数组的成员数量。
sizeof(a) / sizeof(a[0])
上面示例中,sizeof(a)
是整个数组的字节长度,sizeof(a[0])
是数组成员的字节长度,相除就是数组的成员数量。
注意,sizeof
返回值的数据类型是size_t
,所以sizeof(a) / sizeof(a[0])
的数据类型也是size_t
。在printf()
里面的占位符,要用%zd
或%zu
。
int x[12];
printf("%zu\n", sizeof(x)); // 48
printf("%zu\n", sizeof(int)); // 4
printf("%zu\n", sizeof(x) / sizeof(int)); // 12
上面示例中,sizeof(x) / sizeof(int)
就可以得到数组成员数量12
。
数组指针无法获取数组长度,这是数组指针和数组名重要的区别之一
int x[12];
int *p = &x;
printf("%zu\n", sizeof(x)); // 48
printf("%zu\n", sizeof(p)); // 8
数组名称
数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。请看下面的例子。
int arr[5] = {11, 22, 33, 44, 55};
int* p = &arr[0];
printf("%d\n", *p); // Prints "11"
上面示例中,&a[0]
就是数组a
的首个成员11
的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p
),可以获得首个成员的值11
。
由于数组的起始地址是常用操作,&array[0]
的写法有点麻烦,C 语言规定数组名等同于起始地址,也就是说数组名就是指向第一个成员array[0]
的指针常量。
int *p_arr = &arr[0];
*p_arr == &arr[0]; // true
arr[n] == *(arr + n); // true
上面示例中等式总是成立
把数组名当指针使用,也可以取出变量值。看起来数组名和指针有相同的特性,其实不然,两者是有本质区别的,只有部分场景可以混用,其它章节会有说明
// 取出第一个元素
printf("%d\n", *a); // 11
// 移动指针到下一个元素
printf("%d\n", *(a+1)); // 22
如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。函数接受数组作为参数,函数原型可以写成下面这样。
// 写法一
int sum(int arr[], int len);
// 写法二
int sum(int *arr, int len);
上面示例中,传入整数数组,与传入整数指针两者等价
示例通过数组指针对成员求和,函数形参是指针类型,通过指针获取数组的每个成员。
int sum(int *arr, int len) {
int i;
int total = 0;
// 通过下标遍历数组
for (i = 0; i < len; i++) {
total += arr[i];
}
return total;
}
上面案例可以看出,指针也支持下标方式访问元素
数组名也支持运算,可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址
int arr[5] = {11, 22, 33, 44, 55};
for (int i = 0; i < 5; i++) {
printf("%d\n", *(arr + i));
}
数组名还有个重要特性,指向的地址是不能更改。声明数组时,编译器自动为数组分配了内存地址,这个地址与数组名是绑定的,不可更改,下面的代码会报错。
int ints[100];
ints = NULL; // 报错
*
和&
运算符也可以用于多维数组。
int arr[4][2];
*(arr[0]); // 取出 a[0][0] 的值
**a // 等同于
上面示例中,由于a[0]
本身是一个指针,指向第二维数组的第一个成员a[0][0]
。所以,*(a[0])
取出的是a[0][0]
的值。至于**a
,就是对a
进行两次*
运算,第一次取出的是a[0]
,第二次取出的是a[0][0]
。同理,二维数组的&a[0][0]
等同于*a
。
数组指针
数组是一段连续的内存地址,数组名是保存第一个元素内存地址的常量,指针也是指向内存起始地址,所以以下等式总是成立
int arr[] = {1, 2, 3};
int *p_int = arr; // 普通指针
p_int == &arr[0] // true
数组名的这个特性导致一些有趣特性,数组指针是指向数组起始地址,普通指针也是指向元素的起始地址,两者虽然是不同类型,有些场景可以混用和替换,因为都指向 int 类型的内存地址。
普通指针也可以访问数组,三种方式都可以,如下案例
p_int[0];
*p_int
*(p_int+0);
注意,普通指针也支持下标方式读取元素
数组指针,专门用于指向数组的指针,与普通指针有所区别。如下申明指针数组,指向数组首地址
int arr[3] = {1,2,3} // 数组
int (*p_arr)[3] = &arr; // 数组指针,指向数组首地址
申明语句int* p_arr[3]
,中括号[]
优先级更高先和p_arr
结合,表示p_arr
是长度为3的整型数组,再结合*
结合,表示长度为3个整型指针数组。需要使用小括号()
改变优先级,使*
和p_arr
先结合,表示p_arr
是个指针, 再结合[]
,表明是个指向长度为3的数组指针
注意:这里有两个概念容易混淆
- 数组指针:类型是指针,指向数组的指针;
- 指针数组:类型是数组,存储的每个元素都是指针
使用数组指针访问元素,看起来麻烦一些.
(*p_arr)[0];
*p_arr[0];
*(*p_arr+0)
普通指针 和 数组指针都指向数组首地址,如下等式总是成立,它们有部分特性相似,比如都可以读取数组内容,但有本质区别。
p_int == p_arr; // true
在元素访问上区别,申明方式不同,编译器解析方式有区别对待,数组指针维护有数组长度信息,如下案例
sizeof(*p_int); // 4, 获取目标类型是int,32位系统下长度是4;
sizeof(*p_arr) // 12, 获取目标类型是长度3的int数组,长度是 4 * 3 = 12;
还有重要区别是指针运算,步长是指向数据类型的长度,所以数组指针的步长是整个数组的长度。
p_int+1; // p = p+4
p_arr+1; // p2 = p2+12
p_int
表示int的长度,p_arr
表示整个数组的长度。日常更多的使用方式是普通指针,通过指针预算可以更细粒度控制
遍历数组也有区别
int arr[] = {1, 2, 3};
int *p_int = arr; // 普通指针
int (*p_arr)[3] = &arr; // 数组指针
for(i=0; i<3; i++) {
printf("%d\n", *(p_int+i)); // 普通指针, 等价printf("%d\n", p_int[i]);
printf("%d\n", *p_arr)[0]); // 数组指针
}
另外,遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现。
int sum(int* start, int* end) {
int total = 0;
while (start < end) {
total += *start;
start++;
}
return total;
}
int arr[5] = {20, 10, 5, 39, 4};
printf("%i\n", sum(arr, arr + 5));
上面示例中,arr
是数组的起始地址,arr + 5
是结束地址。只要起始地址小于结束地址,就表示还没有到达数组尾部。
反过来,通过数组的减法,可以知道两个地址之间有多少个数组成员,请看下面的例子,自己实现一个计算数组长度的函数。
int arr[5] = {20, 10, 5, 39, 88};
int* p = arr;
while (*p != 88)
p++;
printf("%i\n", p - arr); // 4
上面示例中,将某个数组成员的地址,减去数组起始地址,就可以知道,当前成员与起始地址之间有多少个成员。
同一个数组的两个成员的指针相减时,返回它们之间的距离。
int* p = &a[5];
int* q = &a[1];
printf("%d\n", p - q); // 4
printf("%d\n", q - p); // -4
上面示例中,变量p
和q
分别是数组5号位置和1号位置的指针,它们相减等于4或-4。
对于多维数组,数组指针的加减法对于不同维度,含义是不一样的。
int arr[4][2];
// 指针指向 arr[1]
arr + 1;
// 指针指向 arr[0][1]
arr[0] + 1
上面示例中,arr
是一个二维数组,arr + 1
是将指针移动到第一维数组的下一个成员,即arr[1]
。由于每个第一维的成员,本身都包含另一个数组,即arr[0]
是一个指向第二维数组的指针,所以arr[0] + 1
的含义是将指针移动到第二维数组的下一个成员,即arr[0][1]
。
数组名、普通指针、数组指针的特性
类型 | 申明方式 | sizeof 长度&运算步长 | 指向地址 | 只读 |
---|---|---|---|---|
数组 | int arr[3] |
12 | 首地址 | 是 |
普通指针 | int *p_int = arr |
4 | 首地址 | 否 |
数组指针 | int (*p_arr)[3] = arr |
12 | 首地址 | 否 |
数组复制
由于数组名是指针,所以复制数组不能简单地复制数组名
int b[3] = {1, 2, 3};
int a[3] = b; // err
int *p_int = b; // 指针
复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制。
for (i = 0; i < N; i++) {
a[i] = b[i];
}
上面示例中,通过将数组b
的成员逐个复制给数组a
,从而实现数组的赋值。
另一种方法是使用memcpy()
函数(定义在头文件string.h
),直接把数组所在的那一段内存,再复制一份。
memcpy(a, b, sizeof(b));
上面示例中,将数组b
所在的那段内存,复制给数组a
。这种方法要比循环复制数组成员要快。
变长数组
数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)。c99 标准中,新增了可变长度数组;C11 中 VLA 变为可选项,不是语言必备的特性。
变长数组中的 “变” 不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小则保持不变。这里的 “变” 指的是在创建数组时,可以使用变量指定数组的长度。(普通数组只能用常量或常量表达式指定数组的长度)
- 变长数组 VLA 只能是局部变量数组
- 变长数组 VLA 不能在定义的时候进行初始化
- 变长数组 VLA 必须是自动存储类别,即不能使用 extern 或 static 存储类别说明符
- 变长数组 VLA 不等于动态数组,本质还是静态数组,也就是说,数组的长度在变量的整个生命周期中是不可变的
- 由于变长数组只能是局部变量,且必须是自动存储类别,因此变长数组分配在栈上
- 可变长数组对于多维数组也适用(如 array[a][b] )
int n = x + y;
int arr[n];
上面示例中,数组arr
就是变长数组,因为它的长度取决于变量n
的值,编译器没法事先确定,只有运行时才能知道n
是多少。
变长数组的根本特征,就是数组长度只有运行时才能确定。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。任何长度需要运行时才能确定的数组,都是变长数组。
int i = 10;
int a1[i];
int a2[i + 5];
int a3[i + k];
上面示例中,三个数组的长度都需要运行代码才能知道,编译器并不知道它们的长度,所以它们都是变长数组。
变长数组也可以用于多维数组,下面示例中c[m][n]
就是二维变长数组。
int m = 4;
int n = 5;
int c[m][n];
使用 **malloc **也实现编程数组,详见内存管理章节
char数组
C语言中没有字符串类型,使用char数组表示字符串,为了兼容部分场景,所以char数组比较特殊,有一些特有属性,可能会自动初始化,但并不可靠,要看操作系统分配内存机制
// 所有元素初始化 \0 (ASCII码第一个字符, 十六进制0x00, 不是数值0)
char chars[10];
// 建议显示初始化
char chars[10] = {0};
其他类型数组是随机值
使用 \0
表示字符串结尾,并会自动添加
char chars[] = "hello";
sizeof(chars); // 自动尾部添加0x00, 所以字符串长度是6 (hello + \0 = 6)
printf
打印字符串时候,逐个读取字符,遇到\0
表示到达尾部,停止读取,打印输出
如下只能打印部分内容
char chs[5] = {[0]='a', [1]='b', [3]='c'};
printf("%s\n", chs); // 输出ab
因为chs[2] 初始化为\0
长度不够,无法自动添加\0
char chars[5] = "hello";
printf("%s\n", chars); // 打印会乱码
添加\0
失败,缺少标志位,printf打印异常
其他情况
char str1[] = {'a', 'b'}; // 乱码, 缺少尾部\0
char str2[] = {"hello"}; // 正常
char str3[] = "hello"; // 正常
字符指针可以使用字面量初始化,这种写法其他类型不允许
char* str = "hello";
printf("%s\n", str); // 自动添加\0, 正常打印
多维数组
C 语言允许声明多个维度的数组,有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号。
int board[10][10];
上面示例声明了一个二维数组,第一个维度有10个成员,第二个维度也有10个成员。多维数组可以理解成,上层维度的每个成员本身就是一个数组。比如上例中,第一个维度的每个成员本身就是一个有10个成员的数组,因此整个二维数组共有100个成员(10 x 10 = 100)。
三维数组就使用三个方括号声明,以此类推。
int c[4][5][6];
引用二维数组的每个成员时,需要使用两个方括号,同时指定两个维度。
board[0][0] = 13;
board[9][9] = 13;
注意,board[0][0]
不能写成board[0, 0]
,因为0, 0
是一个逗号表达式,返回第二个值,所以board[0, 0]
等同于board[0]
。跟一维数组一样,多维数组每个维度的第一个成员也是从0
开始编号。
多维数组也可以使用大括号,一次性对所有成员赋值。
int a[2][5] = {
{0, 1, 2, 3, 4},
{5, 6, 7, 8, 9}
};
上面示例中,a
是一个二维数组,这种赋值写法相当于将第一维的每个成员写成一个数组。这种写法不用为每个成员都赋值,缺少的成员会自动设置为0
。
多维数组也可以指定位置,进行初始化赋值。
int a[2][2] = {[0][0] = 1, [1][1] = 2};
上面示例中,指定了[0][0]
和[1][1]
位置的值,其他位置就自动设为0
。
不管数组有多少维度,在内存里面都是线性存储,a[0][0]
的后面是a[0][1]
,a[0][1]
的后面是a[1][0]
,以此类推。因此,多维数组也可以使用单层大括号赋值,下面的语句与上面的赋值语句是完全等同的。
int a[2][2] = {1, 0, 0, 2};
作为函数的参数
声明参数数组
数组作为函数的参数,一般会同时传入数组名和数组长度。
int sum_array(int a[], int n) {
// ...
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(a, 4);
上面示例中,函数sum_array()
的第一个参数是数组本身,也就是数组名,第二个参数是数组长度。由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入。
如果函数的参数是多维数组,那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。
int sum_array(int a[][4], int n) {
// ...
}
int a[2][4] = {
{1, 2, 3, 4},
{8, 9, 10, 11}
};
int sum = sum_array(a, 2);
上面示例中,函数sum_array()
的参数是一个二维数组。第一个参数是数组本身(a[][4]
),这时可以不写第一维的长度,因为它作为第二个参数,会传入函数,但是一定要写第二维的长度4
。
这是因为函数内部拿到的,只是数组的起始地址a
,以及第一维的成员数量2
。如果要正确计算数组的结束地址,还必须知道第一维每个成员的字节长度。写成int a[][4]
,编译器就知道了,第一维每个成员本身也是一个数组,里面包含了4个整数,所以每个成员的字节长度就是4 * sizeof(int)
。
变长数组作为参数
变长数组作为函数参数时,写法略有不同。数组a[n]
是一个变长数组,它的长度取决于变量n
的值,只有运行时才能知道。所以变量n
作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]
的长度,否则就会报错。实参a
其实是一个指针,不同于数组名无法使用sizeof(a)获取长度。
int sum_array(int n, int a[n]) {
// ...
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(4, a);
因为函数原型可以省略参数名,所以变长数组的原型中,可以使用*
代替变量名,也可以省略变量名,下面两种变长函数的原型写法,都是合法的。
int sum_array(int, int [*]);
int sum_array(int, int []);
变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。
// 原来的写法
int sum_array(int a[][4], int n);
// 变长数组的写法
int sum_array(int n, int m, int a[n][m]);
上面示例中,函数sum_array()
的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。
数组字面量作为参数
C 语言允许将数组字面量作为参数,传入函数。
// 数组变量作为参数
int a[] = {2, 3, 4, 5};
int sum = sum_array(a, 4);
// 数组字面量作为参数
int sum = sum_array((int []){2, 3, 4, 5}, 4);
上面示例中,两种写法是等价的。第二种写法省掉了数组变量的声明,直接将数组字面量传入函数。{2, 3, 4, 5}
是数组值的字面量,(int [])
类似于强制的类型转换,告诉编译器怎么理解这组值。
参考
C语言的变长数组
详解C语言变长数组
C语言中数组名和指针的区别