对标题和序号稍加修改。
10.1 数组
数组(array)由数据类型相同的一系列元素组成。声明形式:DataType Name[Size];
/*一些数组声明*/
float candy[365];
char code[12];
int states[50];
[]
表示声明一个名为XXX的数组。[]
中的数字表示数组大小,即元素个数。- 数组元素编号从0开始。
- 使用下标(索引)访问数组元素,形如code[0]。
10.1.1 初始化数组
使用初始化列表初始化数组,各值之间用,
分隔,逗号和值之间可以使用空格。示例程序:
/*打印每个月的天数*/
#include<stdio.h>
#define MONTHS 12 /*明示常量,月份数*/
int main(void)
{
/*声明并初始化数组*/
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 %2d days.\n",index+1,days[index]);
return 0;
}
输出结果:
Month 1 has 31 days.
Month 2 has 28 days.
Month 3 has 31 days.
Month 4 has 30 days.
Month 5 has 31 days.
Month 6 has 30 days.
Month 7 has 31 days.
Month 8 has 31 days.
Month 9 has 30 days.
Month 10 has 31 days.
Month 11 has 30 days.
Month 12 has 31 days.
注意:
- 使用数组前必须初始化。
- 初始化列表的项数与数组大小一致。
- 声明只读const数组只能检索值不能修改值。
数组与变量相似,编译器使用的是内存相应位置上的现有值,为了读取正确的值,任何变量都要在使用前声明和初始化。
10.1.2 错误的初始化
初始化列表中的项数小于数组大小时,编译器会把未初始化的元素值设为0。示例程序:
/*不完整的初始化列表*/
#include<stdio.h>
int main(void)
{
int values[5]={2019,2020,2021};
printf("index value\n");
for (int index = 0; index < 5; index++)
printf("%5d %5d\n",index,values[index]);
return 0;
}
输出结果:
index value
0 2019
1 2020
2 2021
3 0
4 0
当初始化列表项数多于数组大小时,编译器会视为错误。
10.1.3 自动匹配数组大小
/*自动匹配数组大小和初始化列表值*/
#include<stdio.h>
int main(void)
{
int values[]={2019,2020,2021};
printf("index value\n");
/*用sizeof运算符求数组元素个数*/
for (int index = 0; index < sizeof(values)/sizeof(values[0]); index++)
printf("%5d %5d\n",index,values[index]);
return 0;
}
输出结果:
index value
0 2019
1 2020
2 2021
10.1.4 指定初始化器(C99)
指定初始化器(designated initializer)可以在初始化列表中用[index]
初始化指定的元素。例如int arr[6]={arr[5]=212};
只初始化最后一个元素。示例程序:
/*使用指定初始化器*/
#include<stdio.h>
int main(void)
{
int arr[6]={[2]=5,[4]=9};
printf("index value\n");
for (int index = 0; index < 6; index++)
printf("%5d %5d\n",index,arr[index]);
return 0;
}
输出结果:
index value
0 0
1 0
2 5
3 0
4 9
5 0
错误形式:int arr[15]={[5]=[10]=[11]=[12]=101,[3]=101};
指定初始化器的重要特性:
- 指定初始化器后的值用于初始化指定元素后的元素。
- 再次为指定元素赋值会覆盖初始化器的值。
- 不能用多个初始化器和赋值符号组合起来赋值。
10.1.5给数组元素赋值
使用索引为数组元素赋值,示例程序:
#include<stdio.h>
#define SIZE 5
int main(void)
{
int arr[SIZE]={1,2,3,4,5};
printf("index original current\n");
for (int index = 0; index < SIZE; index++)
/*使用索引访问元素值进行s乘法运算以改变元素值*/
printf("%5d %8d %7d\n",index,arr[index],arr[index]*2);
return 0;
}
输出结果:
index original current
0 1 2
1 2 4
2 3 6
3 4 8
4 5 10
10.1.6 数组边界
使用数组时防止索引出界,要保证索引值是有效的,编译器不会检查此种错误,使用越界索引的结果是未定义的。示例程序:
#include<stdio.h>
#define SIZE 5
int main(void)
{
int arr[SIZE]={1,2,3,4,5};
printf("index value\n");
/*使用越界索引5和6*/
for (int index = 0; index < 7; index++)
printf("%5d %5d\n",index,arr[index]);
return 0;
}
输出结果:
index value
0 1
1 2
2 3
3 4
4 5
5 0
6 16
10.1.7 指定数组大小
C99之前,声明数组时[]
中只能用整型常量表达式,但是const值不可以,且值必须大于0。示例:
int arr1[SIZE]; //明示常量
int arr2[5]; //字面常量
int arr3[5*2+1]; //整型常量表达式
int arr4[sizeof(int)+1];//整型常量表达式
float arr5[-4]; //不行,值必须大于0
float arr6[0]; //不行,值必须大于0
float arr7[2.5]; //不行,值必须是整数
int arr8[(int)2.5]; //强制转换成整形常量
int arr8[m]; //C99之前不允许
int arr9[n]; //C99之前不允许
arr8、arr9两个声明创建了变长数组(variable-length array,VLA),声明VLA时不能进行初始化,后面详述。
10.2 二维数组
二维数组是多维数组,也称为数组的数组,主数组的每个元素都是一个子数组。可以把二维数组看成一个行列表,主元素个数代表行数,子元素个数代表列数。例如arr[4][5]
可以用下表表示。
主\子数组元素 | 第1个 | 第2个 | 第3个 | 第4个 | 第5个 |
---|---|---|---|---|---|
第1个 | arr [0] [0] | arr [0] [1] | arr [0] [2] | arr [0] [3] | arr [0] [4] |
第2个 | arr [1] [0] | arr [1] [1] | arr [0] [1] | arr [1] [3] | arr [1] [4] |
第3个 | arr [2] [0] | arr [2] [1] | arr [2] [2] | arr [2] [3] | arr [2] [4] |
第4个 | arr [3] [0] | arr [3] [1] | arr [3] [2] | arr [3] [3] | arr [3] [4] |
示例程序:
/*二维数组:降水量问题*/
#include<stdio.h>
#define MONTHS 12 /*明示常量,一年月份数*/
#define YEARS 5 /*明示常量,调查的年数*/
int main(void)
{
const double rain[YEARS][MONTHS]=
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2},
}; /*使用初始化列表进行初始化*/
int month,year; /*控制变量*/
double one_year,total; /*1年的总降水量,总降水量*/
/*计算并输出各年总降水量、5年总降水量以及5年平均值*/
printf("\nYEAR RAINFALL (inches)\n");
for ( year = 0, total=0; year < YEARS; year++) /*控制年份*/
{
for ( month = 0, one_year=0; month < MONTHS; month++) /*控制月份*/
{
one_year+=rain[year][month]; /*求一年各月份总降水量*/
}
printf("%d %.1f\n",2010+year,one_year);
total+=one_year; /*求5年的总降水量*/
}
printf("\nTotal precipitation in 5 years is %.1f inches.\n",total);
printf("The yearly average is %.1f inches.\n",total/YEARS);
/*计算并输出各月5年总降水量的平均值*/
printf("\nMONTHLY AVERAGES:\n");
printf("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\n");
for ( month = 0; month < MONTHS; month++) /*控制月份*/
{
for ( year = 0, total = 0; year < YEARS; year++) /*控制年份*/
{
total+=rain[year][month]; /*求每个月5年的总降水量*/
}
printf("%.1f ",total/YEARS);
}
return 0;
}
输出结果:
YEAR RAINFALL (inches)
2010 32.4
2011 37.9
2012 49.8
2013 44.0
2014 32.9
Total precipitation in 5 years is 197.0 inches.
The yearly average is 39.4 inches.
MONTHLY AVERAGES:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
7.3 7.3 4.9 3.0 2.3 0.6 1.2 0.3 0.5 1.7 3.6 6.7
10.2.1 初始化二维数组
初始化二维数组是建立在初始化一维数组的基础上,示例:
const double rain[YEARS][MONTHS]=
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2},
};
初始化列表中含5个子列表,分别用于初始化子数组。如果某列表中的数值个数超出了元素个数会出错但是不影响其他初始化列表。也可以省略每个子列表的{}
,保留最外面的{}
,只要项数正确仍保证能正确初始化,但是项数不够时,按先后顺序逐行初始化,未初始化的元素设值为0。
10.3 指针
10.3.1 地址运算符:&
指针(pointer)变量用于储存变量的地址,地址可视为变量在计算机内部的名称,一元运算符**&**返回变量的地址,运算对象是变量名,%p是地址的转换说明(不支持就用%u或%lu),示例程序:
#include<stdio.h>
int main(void)
{
int num=5;
printf("num's adress is %p.\n",&num);
return 0;
}
输出结果:
num's adress is 000000000061FE1C.
大多数系统以十六进制显示地址,每个十六进制数对应4位。
10.3.2 解引用运算符:*
指针变量储存的是变量在内存中的位置,如果有指针变量ptr,那么ptr=#
解释为指针pyr指向num。解引用运算符 * (dereferencing operator)也称为间接运算符(indirection operator)用于返回指针指向某位置上的值,运算对象是指针变量或地址。示例int val=*ptr;
将ptr指向位置上的值赋值给变量val。示例程序:
#include<stdio.h>
int main(void)
{
int num=5;
printf("num's value is %d.\n",*(&num));
return 0;
}
输出结果:
num's value is 5
普通变量将值作为基本量,吧地址作为派生量,指针变量把地址作为基本量,把解引用的值作为派生量。
10.3.3 声明指针变量
声明指针变量时必须指定指针所指向变量的类型,示例:
int * ptr_i;
char * ptr_c;
float * ptr_f;
类型说明符表明指针变量指向对象的类型,*
表明声明的是一个指针变量,通常在声明时*
与指针名间有空格,解引用时没有。大多数系统中,地址用无符号整型表示,%p是指针的转换说明。
10.3.4 使用指针在函数间通信
/*交换变量值*/
#include<stdio.h>
void swap(int * ptr_x,int * ptr_y);/*函数原型*/
int main(void)
{
int x=5,y=10;
printf("Originally x=%d, y=%d.\n",x,y);
swap(&x,&y);/*函数调用,传递两个变量地址a'c*/
printf("Now x=%d, y=%d.\n",x,y);
return 0;
}
/*函数定义,形参列表声明两个指针变量*/
void swap(int * ptr_x,int * ptr_y)
{
int temp;/*临时变量,存储待交换的值*/
/*解引用,交换值*/
temp=*ptr_y;
*ptr_y=*ptr_x;
*ptr_x=temp;
}
输出结果:
Originally x=5, y=10.
Now x=10, y=5.
10.3.5 解引用未初始化的指针
千万不要解引用未初始化的指针,因为在声明指针时,指分配了储存指针本身地址的内存,因此在使用指针前要初始化指针!
10.4 指针和数组
数组名是数组首元素的地址。如果有数组arr,那么就有arr==&arr[0];
,且两者都是常量。示例程序:
/*显示数组元素的地址*/
#include<stdio.h>
#define SIZE 5 /*明示常量,数组大小*/
int main(void)
{
/*声明数组*/
short arr_s[SIZE];
double arr_d[SIZE];
/*声明指针变量*/
short * ptr_s;
double * ptr_d;
/*把数组地址赋给指针*/
ptr_s=arr_s;
ptr_d=arr_d;
/*输出信息*/
printf(" operation arr_s's address arr_d's address\n");
for (int index = 0; index < SIZE; index++)
printf("pointers+%d %p %p\n",index,ptr_s+index,ptr_d+index);
return 0;
}
输出结果:
operation arr_s's address arr_d's address
pointers+0 000000000061FDFE 000000000061FDD0
pointers+1 000000000061FE00 000000000061FDD8
pointers+2 000000000061FE02 000000000061FDE0
pointers+3 000000000061FE04 000000000061FDE8
pointers+4 000000000061FE06 000000000061FDF0
- 大多数计算机按字节编址,一个字节对应一个地址编号。
- 指针加1,指针值就递增它所指向类型的大小(以字节为单位)。
- 数组名是地址常量,所以不能用递增递减运算符。
- 编译器会将数组表示法转换成指针表示法。
因此可以用指针标识数组元素和获取元素值,示例:
arr+2==&arr[2];
*(arr+2)==arr[2];
/*解引用运算符优先级高于算数运算符*/
*arr+2==arr[0]+2;
示例程序:
/*使用指针打印每个月的天数*/
#include<stdio.h>
#define MONTHS 12 /*明示常量,月份数*/
int main(void)
{
/*声明并初始化数组*/
int days[MONTHS]={31,28,31,30,31,30,31,31,30,31,30,31};
/*声明指针并初始化*/
int * ptr=days;
/*比较指针值大小并输出*/
for (int index=0; ptr<=days+(MONTHS-1); ptr++,index++)
printf("Month %2d has %2d days.\n",index+1,*ptr);
return 0;
}
输出结果:
Month 1 has 31 days.
Month 2 has 28 days.
Month 3 has 31 days.
Month 4 has 30 days.
Month 5 has 31 days.
Month 6 has 30 days.
Month 7 has 31 days.
Month 8 has 31 days.
Month 9 has 30 days.
Month 10 has 31 days.
Month 11 has 30 days.
Month 12 has 31 days.
10.4.1 数组形参
数组名是数组首元素的地址,因此可以用数组名表示指针,但是数组名作形参名仅限于函数原型和定义中。示例程序:
/*用数组形参求数组元素之和*/
#include<stdio.h>
#define SIZE 10 /*明示常量,月份数*/
long sum(int arr[],int n);/*原型,使用数组形参*/
int main(void)
{
/*声明并初始化数组*/
int array[SIZE]={20,10,5,39,4,16,19,26,31,20};
/*存储求和结果*/
long result;
/*函数调用,传递数组首元素地址和大小*/
result=sum(array,SIZE);
printf("In main(), the size of array is %zd bytes.\n",sizeof array);
printf("The total number of arr is %ld.\n",result);
return 0;
}
/*函数定义,使用数组形参*/
long sum(int arr[],int n)
{
long total=0;
/*求和*/
for (int i = 0; i < n; i++)
total+=arr[i];
printf("In sum(), the size of arr is %zd bytes.\n",sizeof arr);
return total;
}
输出结果:
In sum(), the size of arr is 8 bytes.
In main(), the size of array is 40 bytes.
The total number of arr is 190.
程序源代码中,[]
表示声明指针arr,此形式只用于声明形式参数,所以数组名形式只能用于函数定义和原型的形参声明中,指向多维数组的指针亦是如此(稍后详述)。可以用* arr
代替,此形式还可以用于一般声明中。程序输出中对两个数组名用sizeof求值结果不同,这是因为array是有10个int元素的数组,一共占用40字节,而arr只是一个指针,指向一个int数组,存储的是一个占用8字节地址。
因为数组名是数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针,只有在这种情况下,C才会把
*arr
和arr[]
解释成一样的。
10.4.2 指针形参的特殊用法
10.4.1中的示例运用首元素地址和数组大小对数组元素求和,除此以外,还可以传递另一个指针形参作为数组的结束处。C保证在为数组分配空间时,指向数组后面的第一个位置的指针仍然是有效指针,但是对该位置上的值未作任何保证,所以不能访问。对10.4.1示例程序修改后:
#include<stdio.h>
#define SIZE 10 /*明示常量,月份数*/
long sum(int * start,int * end);/*原型,使用数组形参*/
int main(void)
{
/*声明并初始化数组*/
int arr[SIZE]={20,10,5,39,4,16,19,26,31,20};
/*存储求和结果*/
long result;
/*函数调用,传递两个指针*/
result=sum(arr,arr+SIZE);
printf("The total number of arr is %ld.\n",result);
return 0;
}
/*函数定义,使用数组形参*/
long sum(int * start,int * end)
{
long total=0;
/*求和*/
while (start<end)
{
total+=*start; //将该位置上的值加起来
start++; //让指针指向下一个元素
}
return total;
}
10.4.3 指针和运算符的优先级
对于10.4.2中的程序还可以将while循环体中的两行代码压缩成total+=*(start++);
,因为_和+优先级相同,但是结合律是从右往左,++结合律是从左往右,所以是先对递增表达式求值后解引用在递增。以下为*_和+运算符优先级的演示:
#include<stdio.h>
int arr1[2]={100,200};
int arr2[2]={300,400};
int main(void)
{
int * p1, * p2, * p3;
p1=p2=arr1;
p3=arr2;
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",*p1,*p2,*p3);
printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++,*++p2,(*p3)++);
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",*p1,*p2,*p3);
return 0;
}
输出结果:
*p1 = 100, *p2 = 100, *p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
*p1 = 200, *p2 = 200, *p3 = 301
10.5 指针操作
- **赋值:**可以把地址赋给指针,例如数组名、带
&
运算符的变量名、另一个指针。注意,地址和指针类型要兼容。 - 解引用:
*
运算符给出指针指向地址上的值。 - **取址:**用
&
运算符给出指针本身的地址。 - **指针与整数相加:**将整数与指针所指向类型的大小(以字节为单位)相乘再与地址相加。
- **递增指针:**递增指针指向数组的指针可以让该指针指向下一个元素。
- **指针与整数相减:**与加法运算不同,指针只能是第一个运算对象,然后将整数与指针所指向类型的大小(以字节为单位)相乘再让地址去减。
- **递减指针:**递减指针指向数组的指针可以让该指针指向前一个元素。
- **指针求差:**当两个指针指向同一数组的不同元素时,两个指针相减可以求出两个元素之间的距离,差值的单位与数组类型的单位相同,且结果是元素个数并非字节数。
- **比较:**可以用关系运算符比较两个指向相同类型的指针。
示例程序:
#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, dereference pointer, pointer address:\n");
printf("ptr1=%p, ptr2=%p, ptr3=%p\n",ptr1,*ptr1,&ptr1);
ptr3=ptr1+4; /*指针变量与整数相加*/
printf("\nadding an int to a pointer:\n");
printf("ptr1+4=%p,*(ptr1+4)=%d\n",ptr3,*(ptr1+4));
ptr1++; /*递增指针*/
printf("\nafter ptr1++:\n");
printf("ptr1=%p,*ptr1=%d,&ptr1=%p\n",ptr1,*ptr1,&ptr1);
ptr2--; /*递减指针*/
printf("\nafter ptr2--:\n");
printf("ptr2=%p,*ptr2=%d,&ptr2=%p\n",ptr2,*ptr2,&ptr2);
--ptr1; /*递增指针,恢复初始值*/
++ptr2; /*递减指针,恢复初始值*/
printf("\npointers reset to origin values:\n");
printf("ptr1=%p,*ptr1=%d\nptr2=%p,*ptr2=%d\n",ptr1,*ptr1,ptr2,*ptr2);
/*指针求差*/
printf("\nsubtrcting one pointer from another:\n");
printf("ptr1=%p,ptr2=%p,ptr2-ptr1=%lld\n",ptr1,ptr2,ptr2-ptr1);
/*指针减去一个整数*/
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3=%p,ptr3-2=%p,*(ptr3-2)=%d\n",ptr3,ptr3-2,*(ptr3-2));
return 0;
}
输出结果:
pointer value, dereference pointer, pointer address:
ptr1=000000000061FE00, ptr2=0000000000000064, ptr3=000000000061FDF8
adding an int to a pointer:
ptr1+4=000000000061FE10,*(ptr1+4)=500
after ptr1++:
ptr1=000000000061FE04,*ptr1=200,&ptr1=000000000061FDF8
after ptr2--:
ptr2=000000000061FE04,*ptr2=200,&ptr2=000000000061FDF0
pointers reset to origin values:
ptr1=000000000061FE00,*ptr1=100
ptr2=000000000061FE08,*ptr2=300
subtrcting one pointer from another:
ptr1=000000000061FE00,ptr2=000000000061FE08,ptr2-ptr1=2
subtracting an int from a pointer:
ptr3=000000000061FE10,ptr3-2=000000000061FE08,*(ptr3-2)=300
10.6 保护数组中的数据
10.6.1 const修饰数组形参
int sum(const int ar[], int n); /*函数原型*/
int sum(const int ar[], int n) /*函数定义*/
{...}
以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内容,如果在函数中不小心修改了元素值。编译器会捕获该错误并生成错误信息。此处用const修饰形参表示在处理该数组时将其视为不可更改的常量来达到保护数组内容的目的。
10.6.2 const数组
const int arr[5]={100,200,300,400,500};
以上声明并初始化了一个const数组,只能用来访问元素值不能修改元素值。
10.6.3 指向const的指针
int arr[5]={100,200,300,400,500};
const int * ptr = arr;
以上声明并初始化了一个指向const的指针,这表明不能用来更改它所指向的值:
*ptr=600; /*不允许*/
ptr[2]=600; /*不允许*/
arr[2]=600; /*允许,因为arr不是const数组*/
ptr++; /*允许,指向arr[1]*/
ptr=&arr[2];/*允许,指向arr[2]*/
指向const的指针常用于函数形参中,表明不会使用该指针改变数据。示例:
void show_array(const int * ptr, int n)
{...}
其次,可以把const数据或非const数据赋给指向const的指针,但是只能把非const数据赋给普通指针。用普通指针修改指向const数据的结果是未定义的。
10.6.4 const指针
int arr[5]={100,200,300,400,500};
int const * ptr = arr;
以上声明并初始化了一个const指针,不能指向其他位置:
ptr=&arr[2];/*不允许*/
*ptr=600; /*允许,更改arr[0]的值*/
10.6.5 指向const的const指针
int arr[5]={100,200,300,400,500};
const int const * ptr = arr;
以上声明并初始化了一个指向const的const指针,既不能修改指向位置的值也不能指向其他位置。
10.7 指针和多维数组
int zippo[4][2];/*声明int二位数组*/
- 二维数组名zippo是数组首元素的地址,所以
zippo==&zippo[0]
。 zippo[0]
是一个内含两个int值的数组,所以zippo[0]
的值与其首元素的地址相同,即zippo[0]==&zippo[0][0]
。zippo[0]
是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址,又由于二维数组和一维数组开始于同一地址,所以zippo
与zippo[0]
数值相同,但是类型不同,zippo
是一个数组的地址,zippo[0]
是一个int类型对象的值,所以zippo[0]==(int *)zippo
。- 在解引用中,
*zippo==zippo[0]
,因为zippo[0]
本身的值就是&zippo[0][0]
的值,所以有**zippo==zippo[0][0]
,这就是双重间接(double indirection)。 - 指针运算中,二维数组指向占用两个int大小的对象,一维数组指向占用一个int大小的对象,所以
zippo+1
与zippo[0]+1
不同。
以下程序演示二维数组中地址间的关系:
#include<stdio.h>
int main(void)
{
int zippo[4][2]={
{2,4},{6,8},
{1,3},{5,7}
};/*声明并初始化二维数组*/
printf("zippo=%p,zippo+1=%p\n",zippo,zippo+1);
printf("zippo[0]=%p,zippo[0]+1=%p\n",zippo[0],zippo[0]+1);
printf("*zippo=%p,*zippo+1=%p\n",*zippo,*zippo+1);
printf("zippo[0][0]=%d\n",zippo[0][0]);
printf("*zippo[0]=%d\n",*zippo[0]);
printf("**zippo=%d\n",**zippo);
printf("zippo[2][1]=%d\n",zippo[2][1]);
printf("*(*(zippo+2)+1)=%d\n",*(*(zippo+2)+1));
return 0;
}
输出结果:
zippo=000000000061FE00,zippo+1=000000000061FE08
zippo[0]=000000000061FE00,zippo[0]+1=000000000061FE04
*zippo=000000000061FE00,*zippo+1=000000000061FE04
zippo[0][0]=2
*zippo[0]=2
**zippo=2
zippo[2][1]=3
*(*(zippo+2)+1)=3
程序中使用双重间接访问了数组储存的值,其中*(*(zippo+2)+1)
实际为zippo[2][1]
的值:
zippo /*二维数组首元素的地址,即&zippo[0]*/
zippo+2 /*二维数组的第3个元素(一维数组)的地址,即&zippo[2]*/
*(zippo+2) /*二维数组的第3个元素(一维数组)首元素的地址,即&zippo[2][0]*/
*(zippo+2)+1/*二维数组的第3个元素(一维数组)的第2个元素的地址,即&zippo[2][1]*/
*(*(zippo+2)+1)/*二维数组的第3个元素(一维数组)的第2个元素的值,即zippo[2][1]*/
10.7.1 指向多维数组的指针
在处理类似zippo数组时使用普通的指针只能与一维数组的类型匹配,但是声明一个指向多维数组的指针就可以较好的处理二维数组:
int (* pz)[2];
以上声明了一个指向一个内含2个int类型值的数组的指针pz([]
优先级高,所以不加()
就声明了一个指针数组),此形式适用于一般声明、函数定义和原型的形参声明中,NAME[][SIZE]
形式只能用于函数定义和原型的形参声明。以下程序演示如何使用指向二维数组的指针:
#include<stdio.h>
int main(void)
{
int zippo[4][2]={
{2,4},{6,8},
{1,3},{5,7}
};/*声明并初始化二维数组*/
int (* pz)[2];/*声明指向二维数组的指针*/
pz=zippo;/*初始化指针*/
printf("pz=%p,pz+1=%p\n",pz,pz+1);
printf("pz[0]=%p,pz[0]+1=%p\n",pz[0],pz[0]+1);
printf("*pz=%p,*pz+1=%p\n",*pz,*pz+1);
printf("pz[0][0]=%d\n",pz[0][0]);
printf("*pz[0]=%d\n",*pz[0]);
printf("**pz=%d\n",**pz);
printf("pz[2][1]=%d\n",pz[2][1]);
printf("*(*(pz+2)+1)=%d\n",pz[2][1]);
return 0;
}
输出结果:
pz=000000000061FDF0,pz+1=000000000061FDF8
pz[0]=000000000061FDF0,pz[0]+1=000000000061FDF4
*pz=000000000061FDF0,*pz+1=000000000061FDF4
pz[0][0]=2
*pz[0]=2
**pz=2
pz[2][1]=3
*(*(pz+2)+1)=3
10.7.2 指针的兼容性
为指针赋值要在两个对象类型一致的情况下进行。示例:
int n=5;
double x=5.5;
int * pi=&n; //有效
double * pd=&x; //有效
x=n; //隐式类型转换
pd=pi; //无效
更复杂的类型亦是如此:
int * p1; //普通的指向一个int的指针
int (*pa)[3]; //指向内含3个int类型元素数组的指针
int arr1[2][3];
int arr2[3][2];
int **p2; //指向指针的指针
p1=&arr1[0][0]; //有效,都是指向int的指针
p1=arr1[0]; //有效,都是指向int的指针
p1=arr1; //无效,arr1是指向内含3个int类型元素数组的指针
pa=arr1; //有效,都是指向内含3个int类型元素数组的指针
pa=arr2; //无效,pa指向内含3个int类型元素的数组,arr2指向内含2个int的数组
p2=&p1; //有效,都是指向指针的指针
*p2=arr2[0]; //有效,都是指向int的指针
p2=arr2; //无效
10.7.3 多重解引用的弊端
示例1:
int x=20; //非const变量
const int y =23;//const变量
int * p1=&x; //非const指针指向非const变量x
const int * p2=&y;//指向const的指针,指向const变量
const int ** pp2;
p1=p2; //不安全,将const指针赋给非const指针
p2=p1; //有效,将非const指针赋给指向const的指针
pp2=&p1; //不安全,嵌套指针类型赋值
将非const指针赋给const指针没问题,前提是只进行一级解引用。进行两级解引用时,这样的赋值也是不安全的,示例2:
const int **pp2;//不能通过*pp2修改指向的内容
int *p1;
const int n=13;//不能修改n的值
pp2=&p1; //允许,但是导致const限定符失效
*pp2=&n; //一级解引用,导致p1指向n,*pp2被修改
*p1=10; //二级解引用,导致n的值发生改变
这种行为是未定义的,使用不同的编译器编译,得出n的结果可能是不同的,因此要注意指针赋值的兼容性,同时不要做未定义的解引用行为。
10.7.4 函数和多维数组
当函数处理二维数组的时,使用指向多维数组的指针是很有效的,而且还能有效得记录行列信息,示例程序:
/*处理二维数组的函数*/
#include<stdio.h>
#define ROWS 3 /*明示常量,行数*/
#define COLS 4 /*明示常量,列数*/
void sum_rows(int ar[][COLS],int rows); /*函数原型,[]表明ar是一个指针*/
void sum_cols(int [][COLS],int rows); /*省略形参名的函数原型*/
int sum(int (*ar)[COLS],int rows); /*函数原型*/
int main(void)
{
int junk[ROWS][COLS]={
{2,4,6,8},
{3,5,7,9},
{12,10,8,6}
};/*声明并初始化二维数组*/
/*传递指向数组的指针及行数*/
sum_rows(junk,ROWS);
sum_cols(junk,ROWS);
printf("Sum of all elements=%d\n",sum(junk,ROWS));
return 0;
}
/*函数定义*/
/*计算每行元素之和*/
void sum_rows(int ar[][COLS],int rows)
{
int r,total;
for (r = 0; r < rows; r++)
{
total=0;
for (int c = 0; c < COLS; c++)
total+=ar[r][c];
printf("row %d: sum=%d\n",r,total);
}
}
/*计算每列元素之和*/
void sum_cols(int ar[][COLS],int rows)
{
int c,total;
for (c = 0; c < COLS; c++)
{
total=0;
for (int r = 0; r < rows; r++)
total+=ar[r][c];
printf("col %d: sum=%d\n",c,total);
}
}
/*计算所有元素之和*/
int sum(int (*ar)[COLS],int rows)
{
int r,c;
int total=0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
total+=ar[r][c];
return total;
}
输出结果:
row 0: sum=20
row 1: sum=24
row 2: sum=36
col 0: sum=17
col 1: sum=19
col 2: sum=21
col 3: sum=23
Sum of all elements=80
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值,也可以不省略,编译器会自动忽略。而且只有在函数定义和原型中才能用ar[]
替代(*ar)
。
10.8 变长数组(VLA)
C规定,数组的维数必须是常量。C99新增变长数组(Variable-Length Array,VLA)后,允许使用变量表示数组的维度和大小(不能用长常量表达式)。此处的“变”是指在创建数组时可以使用变量指定数组的维度和大小,一旦创建了变长数组就不能改变大小。但是变长数组有一些限制,变长数组必须是自动存储类别(storage class),意味着无论在函数中还是作为形参声明,都不能用static或extern存储类别说明符(第12章详解),且不能在声明时初始化。声明示例:
int rows=3; /*行数*/
int cols=5; /*列数*/
int arr[rows][cols];/*声明变长数组,用变量指定行列数*/
由于变长数组用变量指定行列数,所以处理变长数组的函数也可以处理传统的数组。在使用变长数组做形参的函数定义和原型中必须把行数和列数形参放在数组形参前面,因为变长数组的声明要使用这两个变量:
/*计算数组元素之和的函数原型*/
int sum(int rows, int cols, int arr[rows][cols]);/*正确的*/
int sum(int arr[rows][cols], int rows, int cols);/*错误的*/
int sum(int, int, int arr[*][*]);/*根据C99/11,省略参数名时用*代替省略的维度*/
/*计算数组元素之和的函数定义*/
int sum(int rows, int cols, int arr[rows][cols])
{
int r,c;
int total=0;
for (r=0;r<rows;r++)
for(c=0;c<cols;c++)
total+=arr[r][c];
return total;
}
处理变长数组的示例程序:
#include<stdio.h>
#define ROWS 3 /*明示常量,行数*/
#define COLS 4 /*明示常量,列数*/
int sum(int rows, int cols, int arr[rows][cols]);/*函数原型*/
int main(void)
{
int rows=3,cols=10;/*变长数组的行列数*/
int arr1[ROWS][COLS]={
{2,4,6,8},
{3,5,7,9},
{12,10,8,6}
};/*普通二维数组1*/
int arr2[ROWS-1][COLS+2]={
{20,30,40,50,60,70},
{5,6,7,8,9,10}
};/*普通二维数组2*/
int varr[rows][cols];/*声明变长数组*/
/*初始化变长数组*/
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
varr[i][j]=i*j+j;
printf("3x5 array\n");
printf("Sum of all elements = %d\n",sum(ROWS,COLS,arr1));
printf("2x6 array\n");
printf("Sum of all elements = %d\n",sum(ROWS-1,COLS+2,arr2));
printf("3x10 VLA\n");
printf("Sum of all elements = %d\n",sum(rows,cols,varr));
return 0;
}
/*函数定义*/
int sum(int rows, int cols, int arr[rows][cols])
{
int r,c;
int total=0;
for (r=0;r<rows;r++)
for(c=0;c<cols;c++)
total+=arr[r][c];
return total;
}
输出结果:
3x5 array
Sum of all elements = 80
2x6 array
Sum of all elements = 315
3x10 VLA
Sum of all elements = 270
处理变长数组的函数原型中的数组形参并未真正创建一个数组,而且数组名实际上也是一个指针。变长数组还允许动态内存分配,即可以在程序运行时指定数组大小,普通C数组时静态内存分配,即在编译时确定数组大小。
10.9 复合字面量
字面量就是除符号常量外的常量。C99新增复合字面量(compound literal)。复合字面量类似数组的初始化列表,类型名用前面的()
括起来,声明示例:
(int [2]){10,20};//相较于普通数组,只是去掉了声明时的数组名和赋值运算符
(int []){10,20};//也可以省略大小
声明中type [size]
就是复合字面量的类型名,是匿名的,所以必须在创建时使用,其类型名也是首元素的地址,所以可以赋给指针:
int * ptr;
ptr=(int [2]){10,20};
还可以作为实参传递给带有匹配形式的函数:
int sum(const int ar[], int n);
...
int total;
total=sum((int []){4,4,4,5,5,5},6);
对于二维或多维数组,只需要声明类型匹配的指针就可以使用:
int (*ptr)[4]; //声明指向二维数组的指针,该数组含有2个数组元素
//每个元素是含有4个int类型值的数组
ptr=(int [2][4]){{1,2,3,-9},{4,5,6,-8}};
使用复合字面量的示例程序:
/*处理变长数组的函数*/
#include<stdio.h>
#define COLS 4 /*明示常量,列数*/
int sum(const int arr[], int n);/*函数原型*/
int sum2d(const int arr[][COLS], int rows);/*函数原型*/
int main(void)
{
int total1,total2,total3;
int * ptr1; /*普通指针,指向一个int类型对象*/
int (*ptr2)[COLS]; /*指向二维数组的指针*/
ptr1=(int [2]){10,20}; /*复合字面量,一维数组*/
ptr2=(int [2][COLS]){{1,2,3,-9},{4,5,6,-8}};/*复合字面量,二维数组*/
total1=sum(ptr1,2); /*传递参数:指针和元素个数*/
total2=sum((int []){4,4,4,5,5,5,},6);/*传递参数:指针和元素个数*/
total3=sum2d(ptr2,2); /*传递参数:指针和行数*/
printf("total1 = %d\n",total1);
printf("total2 = %d\n",total2);
printf("total3 = %d\n",total3);
return 0;
}
/*函数定义*/
int sum(const int arr[], int n)
{/*arr是指针形参,n是元素个数*/
int total=0;
for (int i = 0; i < n; i++)
total+=arr[i];
return total;
}
int sum2d(const int arr[][COLS], int rows)
{/*arr是指针形参,rows是行数*/
int total=0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < COLS; c++)
total+=arr[r][c];
return total;
}
输出结果:
total1 = 30
total2 = 27
total3 = 4
注意,复合字面量具有块作用域(第12章详解),意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在,复合字面量的定义在最内层的{}
中。