C Primer Plus学习笔记(九)- 数组和指针
数组
数组由数据类型相同的同一系列元素组成
需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型
普通变量可以使用的类型,数组元素都可以用
float candy[365]; // 内含 365 个 float 类型元素的数组 char code[12]; // 内含 12 个 char 类型元素的数组 int states[50]; // 内含 50 个 int 类型元素的数组
方括号([])表明 candy、code 和 states 都是数组,方括号中的数字表明数组中的元素个数
要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素
数组元素的编号从 0 开始,所以 candy[0] 表示 candy 数组的第 1 个元素,candy[364] 表示第 365 个元素,也就是最后一个元素
初始化数组
只储存单个值的变量有时也称为标量变量(scalar variable)
int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};
用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔,逗号和值之间可以使用空格
根据上面的初始化,把 1 赋给数组的首元素(nums[0]),以此类推
不支持 ANSI 的编辑器会把这种形式的初始化识别为语法错误,在数组声明前加上关键字 static 可以解决此问题
使用 const 声明数组
用 const 声明和初始化数组的话,这个数组就是只读数组,程序在运行过程中不能修改数组中的内容
const int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};
和普通变量一样,应该使用声明来初始化 const 数据
如果初始化数组失败的话
#include <stdio.h> #define SIZE 4 int main(void) { int no_data[SIZE]; // 未初始化数组 int i; printf("%2s%14s\n", "i", "no_data[i]"); for (i = 0; i < SIZE; i++) printf("%2d%14d\n", i, no_data[i]); return 0; }
运行结果
使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值
编译器使用的值是内存相应位置上的现有值,系统不同,输出的结果可能不同
初始化列表中的项数应与数组的大小一致
#include <stdio.h> #define SIZE 4 int main(void) { int some_data[SIZE] = {2, 4}; int i; printf("%2s%14s\n", "i", "some_data[i]"); for (i = 0; i < SIZE; i++) printf("%2d%14d\n", i, some_data[i]); return 0; }
运行结果
当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0
如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;如果部分初始化数组,剩余的元素就会被初始化为 0
可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数
#include <stdio.h> int main(void) { const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31}; int i; for (int i = 0; i < sizeof days / sizeof days[0]; i++) printf("Month %2d has %d days.\n", i + 1, days[i]); return 0; }
运行结果
sizeof days 是整个数组的大小(以字节为单位),sizeof days[0] 是数组中一个元素的大小(以字节为单位)
整个数组的大小除以单个元素的大小就是数组元素的个数
指定初始化器(C99)
C99 增加了一个新特性:指定初始化器(designated initializer),利用该特性可以初始化指定的数组元素
C99 规定,可以在初始化列表中使用带方括号的小标指明待初始化的元素
int arr[6] = {[5] = 212}; // 把 arr[5] 初始化为 212
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为 0
#include <stdio.h> #define MONTHS 12 int main() { int days[MONTHS] = {31, 28, [4] = 31, 30, 31, [1] = 29}; int i; for (i = 0; i < MONTHS; i++) printf("%2d %d\n", i + 1, days[i]); return 0; }
运行结果
如果指定初始化器后面有更多的值,那么后面的这些值将被用于初始化指定元素后面的元素
[4] = 31, 30, 31,在 days[4] 被初始化为 31 后,days[5] 和 days[6] 将分别被初始化为 30 和 31
如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
如果没有指定元素的大小
int num[] = {1, [6] = 23}; int days[] = {1, [6] = 4, 9, 10};
编译器会把数组的大小设置为足够装得下初始化的值
num 数组有 7 个元素,编号为 0~6;days 数组有 9 个元素
给数组元素赋值
声明数组后,可以借助数组下标(或索引)给数组元素赋值
C 不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值
#define SIZE 5 int main(void) { int oxen[SIZE] = {5, 3, 2, 8}; int yaks[SIZE]; yaks = oxen; // 不允许 yaks[SIZE] = oxen[SIZE]; // 数组下标越界 yaks[SIZE] = {5, 3, 2, 8}; // 不起作用 }
oxen 数组的最后一个元素是 oxen[SIZE-1],所以 oxen[SIZE] 和 yaks [SIZE] 都超出了两个数组的末尾
数组边界
在使用数组时,要防止数组下标超出边界,必须确保下标是有效的值
编译器不会检查数组下标是否使用得当
在 C 标准中,使用越界下标的结果是未定义的
#include <stdio.h> #define SIZE 4 int main(void) { int value1 = 44; int arr[SIZE]; int value2 = 88; int i; printf("value1 = %d, value2 = %d\n", value1, value2); for (i = -1;i <= SIZE; i++) arr[i] = 2 * i + 1; for (i = -1; i < 7; i++) printf("%2d %d\n", i, arr[i]); printf("value1 = %d, value2 = %d\n", value1, value2); printf("address of arr[-1]: %p\n", &arr[-1]); printf("address of arr[4]: %p\n", &arr[4]); printf("address of value1: %p\n", &value1); printf("address of value2: %p\n", &value2); return 0; }
运行结果
使用越界的数组下标会导致程序改变其他变量的值,不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止
数组元素的编号从 0 开始,最好是在声明数组时使用符号常量来表示数组的大小
指定数组的大小
在 C99 标准之前,声明数组时只能在方括号中使用整型常量表达式
整型常量表达式,是由整型常量构成的表达式
sizeof 表达式被视为整型常量,但是 const 值不是(与 C++ 不同)
表达式的值必须大于 0
int n = 5; int m = 8; float a1[5]; // 可以 float a2[5*2 + 1]; // 可以 float a3[sizeof(int) + 1]; // 可以 float a4[-4]; // 不可以,数组大小必须大于 0 float a5[0]; // 不可以,数组大小必须大于 0 float a6[2.5]; // 不可以,数组大小必须是整数 float a7[(int)2.5]; // 可以,已被强制转换为整型常量 float a8[n]; // C99 之前不允许 float a9[m]; // C99 之前不允许
C99 标准允许后两种的声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA(C11 把 VLA 设定为可选,而不是语言必备的特性)
声明 VLA 时不能进行初始化
多维数组
初始化二维数组
初始化二维数组是建立在初始化一维数组的基础上
const int dates[YEARS][MONTHS] = { {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }
初始化二维数组 dates
这个初始化使用了 5 个数值列表,每个数值列表都用花括号括起来
如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是并不会影响其他行的初始化
初始化时可以省略内部的花括号,只保留最外面的一堆花括号,要保证初始化的数值个数正确
如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值,后面没有值初始化的元素被统一初始化为 0
其他多维数组
声明一个三维数组:
int nums[10][20][30];
nums 内含 10 个元素,每个元素是内含 20 个元素的数组,这 20 个数组元素中的每个元素是内含 30 个元素的数组
处理三维数组要使用 3 重嵌套循环,处理四维数组要使用 4 重嵌套循环。对于其他多维数组,以此类推
查找地址:& 运算符
指针(pointer)是 C 语言最重要的也是最复杂的概念之一,用于储存变量的地址
如果主调函数不使用 return 返回的值,则必须通过地址才能修改主调函数中的值
一元 & 运算符给出变量的存储地址
如果 pooh 是变量名,那么 &pooh 是变量的地址
可以把地址看作是变量在内存中的位置
int pooh = 24; printf("%d %p\n", pooh, &pooh);
输出的内容为
24 是变量 pooh 的值,0061FF2C 是变量 pooh 的存储地址
PC 地址通常用十六进制形式表示,每个十六进制数对应 4 位
指针简介
指针是一个值为内存地址的变量(或数据对象)
char 类型变量的值是字符,int 类型变量的值是整数,指针变量的值是地址
假设一个指针变量名是 ptr
ptr = &pooh; // 把 pooh 的地址赋给 ptr
对于这条语句,我们说 ptr “指向” pooh
ptr 和 &pooh 的区别是 ptr 是变量,而 &pooh 是常量
要创建指针变量,先要声明指针变量的类型
间接运算符:*
假设已知 ptr 指向 bah
ptr = &bah;
然后使用间接运算符 *(indirection operator)找出储存在 bah 中的值,该运算符有时也称为解引用运算符(dereferencing operator)
int pooh = 24; int * ptr; // 声明一个指针变量 ptr int val; ptr = &pooh; // 把 pooh 的地址赋给 ptr val = * ptr; // 把 ptr 指向的值赋给变量 val printf("%d\n", val);
输出结果
语句 ptr = &pooh; 和 val = * ptr; 放在一起相当于语句:val = pooh
最终结果就是把 24 赋给变量 val
声明指针
声明指针必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小
程序必须知道储存在指定地址上的数据类型。long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭
int * pi; // pi 是指向 int 类型变量的指针 char * pc; // pc 是指向 char 类型变量的指针 float * pf, * pg; // pf、pg 都是指向 float 类型变量的指针
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针
int * pi;
声明的意思是 pi 是一个指针,*pi 是 int 类型
* 和指针名之间的空格可有可无。通常,在声明时使用空格,在解引用变量时省略空格
指针实际上是一个新类型,不是整数类型
指针和数组
数组名是数组首元素的地址
如果 days 是一个数组,下面的语句成立:
days == &days[0]; // 数组名是该数组首元素的地址
days 和 &days[0] 都表示数组首元素的内存地址(& 是地址运算符),两者都是常量,在程序的运行过程中,不会改变
可以把它们赋值给指针变量,然后可以修改指针变量的值
#include <stdio.h> #define SIZE 4 int main(void) { short dates[SIZE]; short * pti; short index; double bills[SIZE]; double * ptf; pti = dates; ptf = bills; printf("%23s %10s\n", "short", "double"); for (index = 0; index < SIZE; index++) printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index); return 0; }
运行结果
地址按字节编址,short 类型占用 2 字节,double 类型占用 8 字节
short:0061FF1C + 2 为 0061FF1E
double:0061FEF8 + 8 为 0061FF00
在 C 中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址
指针指向的是标量变量,也要知道变量的类型,否则 *pti 就无法正确地取回地址上的值
指针的值是它所指向对象的地址。地址的表达方式依赖于计算机内部的硬件。许多计算机都是按字节编址,意思是内存中的每个字节都按顺序编号。一个较大对象的地址通常是该对象第一个字节的地址
在指针前面使用 * 运算符可以得到该指针所指向对象的值
指针加 1,指针的值递增它所指向类型的大小(以字节为单位)
假定 days 是一个一维数组
days + 2 == &days[2]; // 相同的地址 *(days + 2) == dates[2] // 相同的值
C 语言标准在描述数组表示法时借助了指针
定义 ar[n] 的意思是 *(ar + n),*(ar + n) 的意思是“到内存的 ar 位置,然后移动 n 个单元”
间接运算符(*)的优先级高于 +,所以 *days+2 相当于 (*days) + 2,*(days + 2) 是 days 第 3 个元素的值,*days + 2 是 days 第 1 个元素的值加 2
#include <stdio.h> #define MONTHS 12 int main() { int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int index; for (index = 0; index < MONTHS; index++) printf("Month %2d has %d days.\n", index + 1, *(days + index)); // *(days + index) 与 days[index]相同 return 0; }
运行结果
days 是数组首元素的地址,days + index 是元素 days[index] 的地址,而 *(days + index) 则是该元素的值,相当于 days[index]
函数、数组和指针
只有在函数原型或函数定义头中,才可以使用 int ar[] 代替 int * ar
int sum(int ar[], int n);
int *ar 形式和 int ar[] 形式都表示 ar 是一个指向 int 的指针。但是,int ar[] 只能用于声明形式参数
int ar[] ,指针 ar 指向的不仅仅一个 int 类型值,还是一个 int 类型数组的元素
因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C 才会把 int ar[] 和 int * ar 解释成一样的
由于函数原型可以省略参数名,所以下面 4 种原型都是等价的:
int sum(int *ar, int n); int sum(int *, int); int sum(int ar[], int n); int sum(int [], int);
但是,在函数定义中不能省略参数名
int sum(int *ar, int n) { }
相当于
int sum(int ar[], int n) { }
如果系统中用 8 字节储存地址,指针变量的大小为 8 字节
使用指针形参
函数要处理数组必须知道何时开始,何时结束
给函数传递两个指针,第 1 个指针指明数组的开始处,第 2 个指针指明数组的结束处
指针操作
指针变量的 8 种基本操作
#include <stdio.h> int main(void) { int urn[5] = {100, 200, 300, 400, 500}; int *ptr1, *ptr2, *ptr3; ptr1 = urn; // 把一个地址赋给一个指针 ptr2 = &urn[2]; // 把一个地址赋给一个指针 printf("pointer value, dereferenced pointer, pointer address:\n"); printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1); // 指针加法 ptr3 = ptr1 + 4; printf("\nadding an int to a pointer:\n"); printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4)); ptr1++; // 递增指针 printf("\nvalues after ptr1++:\n"); printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1); ptr2--; // 递减指针 printf("\nvalues after ptr2--:\n"); printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2); --ptr1; // 恢复为初始值 ++ptr2; // 恢复为初始值 printf("\nPointers reset to original values:\n"); printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2); // 一个指针减去另一个指针 printf("\nsubtracting one pointer from another:\n"); printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %ld\n", ptr2, ptr1, ptr2 - ptr1); // 一个指针减去一个整数 printf("\nsubtracting an int from a pointer:\n"); printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2); return 0; }
运行结果
指针变量的基本操作:
赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。地址和指针的类型要兼容,不能把 double 类型的地址赋给指向 int 的指针
解引用:* 运算符给出指针指向地址上的储存的值
取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址
指针与整数相加:可以使用 + 运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效
递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。变量不会因为值发生变化就移动位置
指针减去一个整数:可以使用 - 运算符从一个指针中减去一个整数。指针必须是第 1 个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效
递减指针:除了递增指针还可以递减指针。前缀和后缀的递增和递减运算符都可以使用
指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。ptr2 - ptr1 得 2,意思是这两个指针所指向的两个元素相隔两个 int,而不是 2 字节。只有两个指针都指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误
比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象
注意:这里的减法有两种。可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针
在递增或递减指针时还要注意一些问题:
编译器不会检查指针是否仍指向数组元素
C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效
如果递增或递减一个指针后超出了这个范围,则是未定义的
可以解引用指向数组任意元素的指针。即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针
千万不要解引用未初始化的指针
int *pt; // 未初始化的指针 *pt = 5; // 严重的错误
第 2 行的意思是把 5 储存在 pt 指向的位置。但是 pt 未被初始化,其值是一个随机值,所以不知道 5 将储存在何处。
这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃
创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。
因此,在使用指针之前,必须先用已分配的地址初始化它。
可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况),也可以用 malloc() 函数先分配内存
保护数组中的数据
只有程序需要在函数中改变传递的数值时,才会传递指针
对于数组别无选择,必须传递指针
如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝到新的数组中
如果把数组的地址传递给函数,让函数直接处理原数组则效率要高
处理数组的函数通常都需要使用原始数据
对形式参数使用 const
如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字 const
如果编写的函数需要修改数组,在声明数组形参时则不使用 const;如果编写的函数不用修改数组,在声明数组形参时最好使用 const
指针和多维数组
int days[4][2]; // 内含 int 数组的数组
数组名 days 是该数组首元素的地址,days 的首元素是一个内含两个 int 值的数组,所以 days 是这个内容两个 int 值的数组的地址
days[0] 本身是一个内含两个整数的数组,所以 days[0] 的值和它首元素(一个整数)的地址(即 &days[0][0] 的值)相同
days[0] 是一个占用一个 int 大小对象的地址,而 days 是一个占用两个 int 大小对象的地址
由于这个整数和内含两个整数的数组都开始与同一个地址,所以 days 和 days[0] 的值相同
给指针或地址加 1,其值会增加对应类型大小的数值
days + 1 和 days[0] + 1 的值不同,因为 days 指向的对象占用了两个 int 大小,而 days[0] 指向的对象只占用了一个 int 大小
解引用一个指针(在指针前使用 * 运算符)或在数组名后使用带下标的 [] 运算符,得到引用对象代表的值
days[0] 是该数组首元素(days[0][0])的地址,所以 *(days[0]) 表示储存在 days[0][0] 上的值(即一个 int 类型的值)
*days 代表该数组首元素(days[0]) 的值,但是 days[0] 本身是一个 int 类型值的地址。该值的地址是 &days[0][0],所以 *days 就是 &days[0][0]
**days 与 *& days[0][0] 等价,相当于 days[0][0],即一个 int 类型的值
days 是地址的地址,必须解引用两次才能获得原始值
如果 days[0] 指向一个 int 类型(4 字节)的数据对象,days[0] 加 1,其值加 4(十六进制中,38 + 4 得 3c)。数组名 days 是一个内含 2 个 int 类型值的数组的地址,所以 days 指向一个 8 字节的数据对象。因此,days 加 1,它所指向的地址加 8 字节(十六进制中,38 + 3 得 40)
指向多维数组的指针
pz 必须指向一个内含两个 int 类型值的数组,而不是指向一个 int 类型值,声明如下
int (*pz)[2]; // pz 指向一个内含两个 int 类型值的数组
pz 被声明为指向一个数组的指针,该数组内含两个 int 类型值
因为 [] 的优先级高于 *,所以需要有括号
int *pax[2]; // pax 是一个内含两个指针元素的数组,每个元素都指向 int 的指针
由于 [] 优先级高,先与 pax 结合,所以 pax 成为一个内含两个元素的数组,然后 * 表示 pax 数组内含两个指针
有括号的声明的是一个指向数组(内含两个 int 类型的值)的指针,没有括号的声明了两个指向 int 的指针
虽然 pz 是一个指针,不是数组名,但是也可以使用 pz[2][1] 这样的写法
可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
days[m][n] == *(*(days + m) + n) pz[m][n] == *(*(pz + m) + n)
指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把 int 类型的值赋给 double 类型的变量,但是两个类型的指针不能这么做
int x = 20; const int y = 23; int * p1 = &x; const int * p2 = &y; const int ** pp2; p1 = p2; // 不安全 -- 把 const 指针赋给非 const 指针 p2 = p1; // 有效 -- 把非 const 指针赋给 const 指针 pp2 = &p1; // 不安全 -- 嵌套指针类型赋值
把 const 指针赋给非 const 指针不安全,因为这样可以使用新的指针改变 const 指针指向的数据。编译器在编译代码时,可能会给出警告,执行这样的代码是未定义的。但是把非 const 指针赋给 const 指针没问题
进行两级解引用时,这样的赋值也不安全
const int **pp2; int *p1; const int n = 13; pp2 = &p1; // 允许,但是这导致 const 限定符失效(根据第 1 行代码,不能通过 *pp2 修改它所指向的内容) *pp2 = &n; // 有效,两者都声明为 const,但是这将导致 p1 指向 n(*pp2 已被修改) *p1 = 10; // 有效,但是这将改变 n 的值(但是根据第 3 行代码,不能修改 n 的值)
在 Terminal 中使用 gcc 编译包含以上代码的小程序,导致 n 最终的值是 13,但是在相同的系统下使用 clang 来编译,n 最终的值是 10,两个编译器都给出指针类型不兼容的警告
函数和多维数组
可以这样声明函数的形参:
void func(int (*pt)[4]);
当且仅当 pt 是一个函数的形式参数时,可以这样声明:
void func(int pt[][4]);
第 1 个方括号是空的,空的方括号表明 pt 是一个指针
下面声明不正确:
int sum(int ar[][]); // 错误的声明
编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1] 转换成 ar+1。编译器对 ar+1 求值,要知道 ar 所指向的对象大小
int sum(int ar[][4]); // 有效声明
表示 ar 指向一个内含 4 个 int 类型值的数组,所以 ar+1 的意思是“该地址加上 16 字节”
如果第 2 对方括号是空的,编译器就不知道该怎样处理
也可以在第 1 对方括号中写上大小,但是编译器会忽略该值
int sum(int ar[3][4]); // 有效声明,但是 3 将被忽略
与使用 typedef 相比,这种形式方便得多
typedef int arr4[4]; // arr4 是一个内含 4 个 int 的数组 typedef arr4 arr3x4[3]; // arr3x4 是一个内含 3 个 arr4 的数组 int sum(arr3x4 ar); // 与下面的声明相同 int sum(int ar[3][4]); // 与下面的声明相同 int sum(int ar[][4]); // 标准形式
一般而言,声明一个指向 N 维数组的指针时,只能省略最左边方括号中的值
int sum(int ar[][12][20][30]);
第 1 对方括号只用于表明这是一个指针,而其他方括号则用于描述指针所指向数据对象的类型
上面的声明相当于:
int sum(int (*ar)[12][20][30]); // ar 是一个指针
ar 指向一个 12x20x30 的 int 数组
变长数组(VLA)
变长数组有一些限制,变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用 static 或 extern 存储类别说明符。而且,不能在声明中初始化它们
C99/C11 标准允许在声明变长数组时使用 const 变量,所以该数组的定义必须是声明在块中的自动存储类别数组
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小
普通 C 数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度
声明一个带二维变长数组参数的函数
int sum(int rows, int cols, int ar[rows][cols]); // ar 是一个变长数组(VLA)
在形参列表中必须在声明 ar 之前先声明 rows 和 cols 这两个形参
int sum(int ar[rows][cols], int rows, int cols); // 无效顺序
C99/C11 标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度
int sum(int, int, int ar[*][*]); // ar 是一个变长数组(VLA),省略了维度形参名
在函数定义的形参列表中声明的变长数组并未实际创建数组
变长数组名实际上是一个指针,这说明带变长数组形参的函数实际上是在原始数组中处理数组,因此可以修改传入的数组
复合字面量
在 C99 标准以前,对于带数组形参的函数,情况不同,可以传递数组,但是没有等价的数组常量
C99 新增了复合字面量(compound literal),字面量是除符号常量外的常量
例如,5 是 int 类型字面量,81.3 是 double 类型的字面量,'Y' 是 char 类型的字面量,"day" 是字符串字面量
对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名
一个普通的数组声明:
int days[2] = {10, 20};
下面的复合字面量创建了一个和 days 数组相同的匿名数组,也有两个 int 类型的值:
(int [2]){10, 20}; // 复合字面量
去掉声明中的数组名,留下的 int [2] 即是复合字面量的类型名
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:
(int []){10, 20, 30} // 内含 3 个元素的复合字面量
因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它
使用指针记录地址就是一种用法
int * pt1; pt1 = (int [2]){10, 20};
该复合字面量与上面创建的 days 数组的字面常量完全相同
复合字面量的类型名也代表首元素的地址,*pt1 是 10,pt1[1] 是 20
还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:
int sum(const int ar[], int n); ... int total; total = sum((int []){4, 4, 4, 5, 5, 5}, 6);
第 1 个实参是内含 6 个 int 类型值的数组,和数组名类似,这同时也是该数组首元素的地址。这种用法的好处是,把信息传入函数前不必先创建数组,这是复合字面量的典型用法
可以把这种用法用于二维数组或多维数组
int (*pt2)[4]; // 声明一个指向二维数组的指针,该数组内含 2 个数组元素,每个元素是内含 4 个 int 类型值的数组 pt2 = (int [2][4]){{1, 2, 3, -9}, {4, 5, 6, -8}};
该字面量的类型是 int [2][4],即一个 2x4 的 int 数组
复合字面量是提供只临时需要的值的一种手段
复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中