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 数组

复合字面量是提供只临时需要的值的一种手段

复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中

posted @ 2018-06-05 23:43  Sch01aR#  阅读(464)  评论(0编辑  收藏  举报