目录
一、数组
定义:数组由数据类型相同的一系列元素组成
声明数组:告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。
数组类型:普通变量可以使用的类型,数组元素都可以用。
/* 一些数组声明*/
int main(void)
{
float candy[365]; /* 内含365个float类型元素的数组 */
char code[12]; /*内含12个char类型元素的数组*/
int states[50]; /*内含50个int类型元素的数组 */
...
}/* 访问数组中的元素*/
candy[0]; //表示candy数组的第1个元素
1.1 初始化数组
只储存单个值的变量有时也称为标量变量(scalar variable),如下:
int fix = 1;
float flax = PI * 2;
C使用新的语法来初始化数组:
int main(void)
{
int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初
始化 */
...
}
/* day_mon1.c -- 打印每个月的天数 */
#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]);
getchar();
return 0;
}
//这个程序还不够完善,每4年打错一个月份的天数(即,2月份的天数)
运行结果:
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声明和初始化数组,可以创建只读数组。一旦声明为const,便不能再给它赋值。
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
初始化失败的例子:
/* no_data.c -- 为初始化数组 */
#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]);
getchar();
return 0;
}
运行结果:
i no_data[i]
0 -858993460
1 -858993460
2 -858993460
3 -858993460
在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值。
注意:可以把数组创建成不同的存储类别(storage class)本章描述的数组属于自动存储类别,意思是这些数组在函数内部声明,且声明时未使用关键字static。对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。
初始化列表中的项数应与数组的大小一致。一个不一致的例子:
/* no_data.c -- 为初始化数组 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
int some_data[SIZE] = { 1492, 1066 }; /* 缺少元素 */
int i;
printf("%2s%14s\n", "i", "some_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, some_data[i]);
getchar();
return 0;
}
运行结果:
i some_data[i]
0 1492
1 1066
2 0
3 0
如果部分初始化数组,剩余的元素就会被初始化为0。
如果初始化列表的项数多于数组元素个数,编译器视为错误。
可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数,如下:
//程序清单10.4 day_mon2.c程序
/* day_mon2.c -- 让编译器计算元素个数 */
#include <stdio.h>
int main(void)
{
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };
int index;
for (index = 0; index < sizeof days / sizeof days[0]; index++)
printf("Month %2d has %d days.\n", index + 1, days[index]);
getchar();
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.
- 如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。
- sizeof运算符给出它的运算对象的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。
- 自动计数的弊端:无法察觉初始化列表中的项数有误。
1.2 指定初始化器(C99)
C99 增加新特性:指定初始化器(designated initializer)。可以初始化指定的数组元素。对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0。
//designate.c -- 使用指定初始化器
#include <stdio.h>
#define months 12
int main(void)
{
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]);
getchar();
return 0;
}
运行结果:
1 31
2 29
3 0
4 0
5 31
6 30
7 31
8 0
9 0
10 0
11 0
12 0
指定初始化器的两个重要特性。第一,如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。
如果未指定数组大小,编译器会把数组的大小设置为足够装得下初始化的值。
//designate.c -- 不指定数组大小
#include <stdio.h>
int main(void)
{
int days[] = { 31, 28,[4] = 31, 30, 31, [1] = 29 };
int i;
for (i = 0; i < sizeof days/sizeof days[0]; i++)
printf("%2d %d\n", i + 1, days[i]);
getchar();
return 0;
}
运行结果:
1 31
2 29
3 0
4 0
5 31
6 30
7 31
1.3 给数组元素赋值
声明数组后,可以借助数组下标(或索引)给数组元素赋值。
/* 给数组的元素赋值 */
#include <stdio.h>
#define SIZE 50
int main(void)
{
int counter, evens[SIZE];
for (counter = 0; counter < SIZE; counter++)
evens[counter] = 2 * counter;
...
}
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}; /* 不起作用 */
}
1.4 数组边界
在使用数组时,要防止数组下标超出边界。因为编译器不会检查出这种错误(但是,一些编译器发出警告,然后继续编译程序)。
// bounds.c -- 数组下标越界
#include <stdio.h>
#define SIZE 4
int main(void)
{
int value1 = 44;
int arr[SIZE];
int value2 = 88;
int i;
int j;
printf("value1 = %d, value2 = %d\n", value1, value2);
for (i = -1; i <= SIZE; i++)
arr[i] = 2 * i + 1;
for (j = -1; j < 7; j++)
printf("%2d %d\n", j, arr[j]);
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);
getchar();
return 0;
}
//本程序使用gcc编译,循环打印没有显示-1
运行结果:
value1 = 44, value2 = 88
0 1
1 3
2 5
3 7
4 9
5 5
6 5
value1 = 9, value2 = -1
address of arr[-1]: 0061FF00
address of arr[4]: 0061FF14
address of value1: 0061FF14
address of value2: 0061FF00
在C标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。编译器不对此报错,因为不检查边界,C 程序可以运行更快。
数组元素的编号从0开始。最好是在声明数组时使用符号常量来表示数组的大小,这样做能确保整个程序中的数组大小始终一致。
1.5 指定数组的大小
在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型常量,但是(与C++不同)const值不是。另外,表达式的值必须大于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之前不允许
C90标准的编译器不允许后两种声明方式。而C99标准允许这样声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA(C11 放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性)。
二、多维数组
为了方便表示数据,可以使用数组的数组。以二维数组为例:
/* rain.c -- 计算每年的总降水量、年平均降水量和5年中每月的平
均降水量 */
#include <stdio.h>
#define months 12 // 一年的月份数
#define years 5 // 年数
int main(void) {
// 用2010~2014年的降水量数据初始化数组
const float 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 year, month;
float subtot, total;
printf(" year rainfall (inches)\n");
for (year = 0, total = 0; year < years; year++)
{ // 每一年,各月的降水量总和
for (month = 0, subtot = 0; month < months;
month++)
subtot += rain[year][month];
printf("%5d %15.1f\n", 2010 + year, subtot);
total += subtot; // 5年的总降水量
}
printf("\nthe yearly average is %.1f inches.\n\n",
total / years);
printf("monthly averages:\n\n");
printf(" jan feb mar apr may jun jul aug\
sep oct");
printf(" nov dec\n");
for (month = 0; month < months; month++)
{ // 每个月,5年的总降水量
for (year = 0, subtot = 0; year < years;
year++)
subtot += rain[year][month];
printf("%4.1f ", subtot / years);
}
printf("\n");
getchar();
return 0;
}
运行结果:
year rainfall (inches)
2010 32.4
2011 37.9
2012 49.8
2013 44.0
2014 32.9
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
2.1 初始化二维数组
const float 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}
};
如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是这并不会影响其他行的初始化。初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果与上面相同。但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为0。
2.2 其他多维数组
处理三维数组要使用3重嵌套循环,处理四维数组要使用4重嵌套循环。对于其他多维数组,以此类推。
三、指针和数组
数组与指针的关系:数组表示法其实是在变相地使用指针。例如,数组名是数组首元素的地址。
flizny == &flizny[0]; // 数组名是该数组首元素的地址
//程序清单10.8 pnt_add.c程序
// pnt_add.c -- 指针地址
#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 %15s\n", "short", "double");
for (index = 0; index < SIZE; index++)
printf("pointers + %d: %10p %10p\n", index, pti +
index, ptf + index);
getchar();
return 0;
}
运行结果:
short double
pointers + 0: 010FF780 010FF740
pointers + 1: 010FF782 010FF748
pointers + 2: 010FF784 010FF750
pointers + 3: 010FF786 010FF758
我们的系统中,地址按字节编址,short类型占用2字节,double类型占用8字节。在C中,指针加1指的是增加一个存储单元。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址不够,因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则*pt 就无法正确地取回地址上的值)。
指针的值是它所指向对象的地址。
在指针前面使用*运算符可以得到该指针所指向对象的值。
指针加1,指针的值递增它所指向类型的大小(以字节为单位)。
C 语言标准在描述数组表示法时确实借助了指针。也就是说,定义ar[n]的意思是*(ar + n)。
dates + 2 == &date[2] // 相同的地址
*(dates + 2) == dates[2] // 相同的值
不要混淆 *(dates+2)和*dates+2。间接运算符(*)的优先级高于+
*(dates + 2) // dates第3个元素的值
*dates + 2 // dates第1个元素的值加2
指针表示法和数组表示法是两种等效的方法。适时使用数组表示法和指针表示法:
//适时使用数组表示法和指针表示法
/* day_mon3.c -- uses pointer notation */
#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 %d days.\n", index + 1,
*(days + index)); //与 days[index]相同
getchar();
return 0;
}
四、函数、数组和指针
使用数组作为形参的函数,其形参的类型为指针,有两种表示方法:用指针表示数组名;用数组名表示指针。
int sum(int * ar, int n)
int sum(int ar[], int n)
注:只有在函数原型或函数定义头中,才可以用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)
{
//其他代码已省略
}
// sum_arr1.c -- 数组元素之和
// 如果编译器不支持 %zd,用 %u 或 %lu 替换它
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19,
26, 31, 20 };
long answer;
answer = sum(marbles, SIZE);
printf("The total number of marbles is %ld.\n", answer);
printf("The size of marbles is %zd bytes.\n",
sizeof marbles);
getchar();
return 0;
}
int sum(int ar[], int n) // 这个数组的大小是?
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
printf("The size of ar is %zd bytes.\n", sizeof ar);
return total;
}
运行结果:
The size of ar is 4 bytes.
The total number of marbles is 190.
The size of marbles is 40 bytes.
整个marbles的大小是40字节。但是,ar才4字节。这是因为ar并不是数组本身,它是一个指向 marbles 数组首元素的指针。我们的系统中用4 字节储存地址。marbles是一个数组, ar是一个指向marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针ar。
4.1 使用指针形参
函数要处理数组必须知道何时开始、何时结束。现有两种方法:1、指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。2、传递两个指针,第1个指针指明数组的开始处,第2个指针指明数组的结束处。
指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。
/* sum_arr2.c -- 数组元素之和 */
#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19,
26, 31, 20 };
long answer;
answer = sump(marbles, marbles + SIZE);
printf("The total number of marbles is %ld.\n", answer);
getchar();
return 0;
}
/* 使用指针算法 */
int sump(int * start, int * end)
{
int total = 0;
while (start < end)
{
total += *start; // 把数组元素的值加起来
start++; // 让指针指向下一个元素
}
return total;
}
运行结果:
The total number of marbles is 190.
start是指向int的指针,start递增1相当于其值递增int类型的大小。
还可以把循环体压缩成一行代码:
total += *start++;
或者total += *(start++);//更清楚
一元运算符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。
/* order.c -- 指针运算中的优先级 */
#include <stdio.h>
int data[2] = { 100, 200 };
int moredata[2] = { 300, 400 };
int main(void)
{
int * p1, *p2, *p3;
p1 = p2 = data;
p3 = moredata;
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);
getchar();
return 0;
}
运行结果:
*p1 = 100, *p2 = 100, *p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
*p1 = 200, *p2 = 200, *p3 = 301
4.2 指针表示法和数组表示法
ar[i]和*(ar+1)这两个表达式都是等价的。无论ar是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。
指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。
五、指针操作
// ptr_ops.c -- 指针操作
#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 =\
%td\n", ptr2, ptr1, ptr2 - ptr1);
// 一个指针减去一个整数
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 -
2);
getchar();
return 0;
}
运行结果:
pointer value, dereferenced pointer, pointer address:
ptr1 = 0097F7A0, *ptr1 =100, &ptr1 = 0097F794
adding an int to a pointer:
ptr1 + 4 = 0097F7B0, *(ptr1 + 4) = 500
values after ptr1++:
ptr1 = 0097F7A4, *ptr1 =200, &ptr1 = 0097F794
values after --ptr2:
ptr2 = 0097F7A4, *ptr2 = 200, &ptr2 = 0097F788
Pointers reset to original values:
ptr1 = 0097F7A0, ptr2 = 0097F7A8
subtracting one pointer from another:
ptr2 = 0097F7A8, ptr1 = 0097F7A0, ptr2 - ptr1 =2
subtracting an int from a pointer:
ptr3 = 0097F7B0, ptr3 - 2 = 0097F7A8
上面程序演示了指针变量的 8种基本操作。除了这些操作,还可以使用关系运算符来比较指针。
赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。注意,地址应该和指针类型兼容。
解引用:*运算符给出指针指向地址上储存的值。
取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
递减指针:除了递增指针还可以递减指针。
指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。
比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
注意:
编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。
千万不要解引用未初始化的指针。
int * pt;// 未初始化的指针
*pt = 5; // 严重的错误
第2行的意思是把5储存在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将储存在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。
六、保护数组中的数据
编写函数时,通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。
6.1 对形式参数使用const
如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */
使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。
一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用const。
/* arf.c -- 处理数组的函数 */
#include <stdio.h>
#define SIZE 5
void show_array(const double ar[], int n);
void mult_array(double ar[], int n, double mult);
int main(void)
{
double dip[SIZE] = { 20.0, 17.66, 8.2, 15.3, 22.22 };
printf("The original dip array:\n");
show_array(dip, SIZE);
mult_array(dip, SIZE, 2.5);
printf("The dip array after calling mult_array():\n");
show_array(dip, SIZE);
getchar();
return 0;
}
/* 显示数组的内容 */
void show_array(const double ar[], int n)
{
int i;
for (i = 0; i < n; i++)
printf("%8.3f ", ar[i]);
putchar('\n');
}
/* 把数组的每个元素都乘以相同的值 */
void mult_array(double ar[], int n, double mult)
{
int i;
for (i = 0; i < n; i++)
ar[i] *= mult;
}
运行结果:
The original dip array:
20.000 17.660 8.200 15.300 22.220
The dip array after calling mult_array():
50.000 44.150 20.500 38.250 55.550
注意该程序中两个函数的返回类型都是void。虽然mult_array()函数更新了dip数组的值,但是并未使用return机制。
6.2 const的其他内容
把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
const double * pc = rates; // 有效
pc = locked; //有效
pc = &rates[3]; //有效
只能把非const数据的地址赋给普通指针:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
double * pnc = rates; // 有效
pnc = locked; // 无效
pnc = &rates[3]; // 有效
这个规则非常合理。否则,通过指针就能改变const数组中的数据。
对函数的形参使用const不仅能保护数据,还能让函数处理const数组。另外,不应该把const数组名作为实参传递给形参为非const的函数:
C标准规定,使用非const标识符修改const数据导致的结果是未定义的。
const还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,关键是const的位置:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
double * const pc = rates; // pc指向数组的开始
pc = &rates[2]; // 不允许,因为该指针不能指向别处
*pc = 92.99; // 没问题 -- 更改rates[0]的值
可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * const pc = rates;
pc = &rates[2]; //不允许
*pc = 92.99; //不允许
在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值。
七、指针和多维数组
int zippo[4][2]; /* 内含int数组的数组 */
zippo和zippo[0]的值相同 ,但zippo + 1和zippo[0] + 1的值不同。因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。
解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。
*zippo就是&zippo[0][0]。
**zippo等价于*&zippo[0][0],相当于zippo[0][0]。
简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(doubleindirection)的例子。
/* zippo1.c -- zippo的相关信息 */
#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));
getchar();
return 0;
}
运行结果:
zippo = 010FFE98, zippo + 1 = 010FFEA0
zippo[0] = 010FFE98, zippo[0] + 1 =010FFE9C
*zippo = 010FFE98, *zippo + 1 = 010FFE9C
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(*(zippo+2) + 1) = 3
如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。
7.1 指向多维数组的指针
声明一个指针变量pz指向一个二维数组
int (* pz)[2]; // pz指向一个内含两个int类型值的数组
以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。在声明中使用圆括号是因为[]的优先级高于*。
int * pax[2]; // pax是一个内含两个指针元素的数组,每个元素都指向int的指针
这行代码声明了两个指向int的指针。
/* zippo2.c -- 通过指针获取zippo的信息 */
#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[1] = %p, pz[1] + 1 = %p\n", pz[1],
pz[1] + 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));
getchar();
return 0;
}
运行结果:
pz = 010FF7B0, pz + 1 = 010FF7B8
pz[0] = 010FF7B0, pz[0] + 1 = 010FF7B4
pz[1] = 010FF7B8, pz[1] + 1 = 010FF7BC
*pz = 010FF7B0, *pz + 1 = 010FF7B4
pz[0][0] = 2
*pz[0] = 2
**pz = 2
pz[2][1] = 3
*(*(pz+2) + 1) = 3
虽然pz是一个指针,不是数组名,但是也可以使用 pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] == *(*(pz + m) + n)
7.2 指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。
int n = 5;
double x;
int * p1 = &n;
double * pd = &x;
x = n; // 隐式类型转换
pd = p1; // 编译时错误
int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2; // 一个指向指针的指针
有如下的语句:
pt = &ar1[0][0]; // 都是指向int的指针
pt = ar1[0]; // 都是指向int的指针
pt = ar1; // 无效
pa = ar1; // 都是指向内含3个int类型元素数组的指针
pa = ar2; // 无效
p2 = &pt; // both pointer-to-int *
*p2 = ar2[0]; // 都是指向int的指针
p2 = ar2; // 无效
把const指针赋给非const指针不安全,因为这样可以使用新的指针改变const指针指向的数据。但是把非const指针赋给const指针没问题,前提是只进行一级解引用:
p2 = p1; // 有效 -- 把非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的值)
标准规定了通过非const指针更改const数据是未定义的。
C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果通过p1更改y,其行为是未定义的。
// array2d.c -- 处理二维数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int[][COLS], int); // 省略形参名,没问题
int sum2d(int(*ar)[COLS], int rows); // 另一种语法
int main(void)
{
int junk[ROWS][COLS] = { { 2, 4, 6, 8 },
{ 3, 5, 7, 9 },
{ 12, 10, 8, 6 }
};
int i, k;
sum_rows(junk, ROWS);
sum_cols(junk, ROWS);
printf("Sum of all elements = %d\n", sum2d(junk,
ROWS));
getchar();
return 0;
}
void sum_rows(int ar[][COLS], int rows)
{
int r;
int c;
int tot;
for (r = 0; r < rows; r++)
{
tot = 0;
for (c = 0; c < COLS; c++)
tot += ar[r][c];
printf("row %d: sum = %d\n", r, tot);
}
}
void sum_cols(int ar[][COLS], int rows)
{
int r;
int c; int tot;
for (c = 0; c < COLS; c++)
{
tot = 0;
for (r = 0; r < rows; r++)
tot += ar[r][c];
printf("col %d: sum = %d\n", c, tot);
}
}
int sum2d(int ar[][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
tot += ar[r][c];
return tot;
}
运行结果:
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维数组的指针时,只能省略最左边方括号中的值,也可以在第1对方括号中写上大小,但是编译器会忽略该值:
int sum4d(int ar[][12][20][30], int rows);
int sum2(arr3x4 ar, int rows); // 与下面的声明相同
int sum2(int ar[3][4], int rows); // 与下面的声明相同
int sum2(int ar[][4], int rows); // 标准形式
typedef int arr4[4]; // arr4是一个内含 4 个int的数组
typedef arr4 arr3x4[3]; // arr3x4 是一个内含3个 arr4的数组
注意:下面的声明不正确
int sum2(int ar[][], int rows); // 错误的声明
编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1]转换成 ar+1。编译器对ar+1求值,要知道ar所指向的对象大小。
八、变长数组(VLA)
C规定,数组的维数必须是常量,不能用变量。C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。
变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。而且,不能在声明中初始化它们。
注意:变长数组不能改变大小
一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。
声明一个带二维变长数组参数的函数:
int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一个变长数组(VLA)
int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序
int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名
ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度。
//vararr2d.c -- 使用变长数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
int sum2d(int rows, int cols, int ar[rows][cols]);
int main(void)
{
int i, j;
int rs = 3;
int cs = 10;
int junk[ROWS][COLS] = {
{ 2, 4, 6, 8 },{ 3, 5, 7, 9 },
{ 12, 10, 8, 6 }
};
int morejunk[ROWS - 1][COLS + 2] = {
{ 20, 30, 40, 50, 60, 70 },
{ 5, 6, 7, 8, 9, 10 }
};
int varr[rs][cs]; // 变长数组(VLA)
for (i = 0; i < rs; i++)
for (j = 0; j < cs; j++)
varr[i][j] = i * j + j;
printf("3x5 array\n");
printf("Sum of all elements = %d\n", sum2d(ROWS,
COLS, junk));
printf("2x6 array\n");
printf("Sum of all elements = %d\n", sum2d(ROWS -
1, COLS + 2, morejunk));
printf("3x10 VLA\n");
printf("Sum of all elements = %d\n", sum2d(rs, cs,
varr));
getchar();
return 0;
}
// 带变长数组形参的函数
int sum2d(int rows, int cols, int ar[rows][cols])
{
int r;
int c; int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < cols; c++)
tot += ar[r][c];
return tot;
}
运行结果:
3x5 array
Sum of all elements = 80
2x6 array
Sum of all elements = 315
3x10 VLA
Sum of all elements = 270
和传统的语法类似,变长数组名实际上是一个指针。
注:const和数组大小
C90标准不允许(也可能允许)在声明数组时使用const变量。数组的大小必须是给定的整型常量表达式,可以是整型常量组合。C99/C11 标准允许在声明变长数组时使用 const 变量。所以该数组的定义必须是声明在块中的自动存储类别数组。变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小。普通 C数组都是静态内存分配,即在编译时确定数组的大小。
九、复合字面量
字面量是除符号常量外的常量。例如,5是int类型字面量, 81.3是double类型的字面量,'Y'是char类型的字面量,"elephant"是字符串字面量。
C99新增了复合字面量(compound literal)。
int diva[2] = {10, 20};//普通的数组声明
(int [2]){10, 20} // 复合字面量
(int []){50, 20, 90} // 内含3个元素的复合字面量
去掉声明中的数组名,留下的int [2]即是复合字面量的类型名。
初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数。
因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。
int * pt1;
pt1 = (int [2]) {10, 20};
还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数。
int sum(const int ar[], int n);
...
int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);
把信息传入函数前不必先创建数组,这是复合字面量的典型用法。
// flc.c -- 有趣的常量
#include <stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS], int rows);
int sum(const int ar[], int n);
int main(void)
{
int total1, total2, total3;
int * pt1;
int(*pt2)[COLS];
pt1 = (int[2]) { 10, 20 };
pt2 = (int[2][COLS]) { {1, 2, 3, -9}, { 4, 5, 6, -8 } };
total1 = sum(pt1, 2);
total2 = sum2d(pt2, 2);
total3 = sum((int []){ 4, 4, 4, 5, 5, 5 }, 6);
printf("total1 = %d\n", total1);
printf("total2 = %d\n", total2);
printf("total3 = %d\n", total3);
return 0;
}
int sum(const int ar [], int n)
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
return total;
}
int sum2d(const int ar [][COLS], int rows)
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < COLS; c++)
tot += ar[r][c];
return tot;
}
运行结果:
total1 = 30
total2 = 4
total3 = 27
复合字面量是提供只临时需要的值的一种手段。复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中。