C_Primer第10章 数组和指针

本章介绍以下内容

  • 关键字:static
  • 运算符:&、*(一元)
  • 如何创建并初始化数组
  • 指针(在已学过的基础上)、指针和数组的关系
  • 编写处理数组的函数
  • 二维数组

10.1 数组

10.1.1初始化数组

int main(void)
{
    int powers[8]  = {1,2,4,6,8,16,32,64};/*从ANSI C开始支持这种初始化*/
    ....
}

如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可使用空格。

程序清单10.1 day_mon1.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]);
		
		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.

这个程序还不够完善,每4年打错一个月份的天数(即,2月份的天数)。该程序用初始化列表初始化day[],列表(用花括号括起来)中用逗号分隔各值。

注意该例使用了符号常量MONTHS表示数组大小,这是我们推荐且常用的做法。

注意,使用const声明数组

有时需要把数组设置为只读。这样,程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组。

如果初始化数组失败怎么办?程序清单10.2演示了这种情况。

程序清单10.2 no_data.c程序

/* 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]);
	
	return 0;
}

该程序的输出如下(系统不同,输出的结果可以不同)

 i    no_data[i]
 0       4200923
 1       4200832
 2       4194432
 3       2686756

使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值。

初始化列表中的项数与数组的大小一致。如果不一致会怎样?我位还是以上一个程序为例,但初始化列表中缺少两个元素,如程序清单10.3所示:

/* some_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]);
	
	return 0;
}

程序输出:

i  some_data[i]
0          1492
1          1066
2             0
3             0

 如上所示,编译器做得很好。当初始化列表中的值少于数组元素时,编译器会把剩余的元素都初始化为0.也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果部分初始化数组,剩余的元素就会被初始化为0。

省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数(见程序清单10.4)

程序清单10.4中

/* day_mon2.c --让编译器计算元素个数*/
#include <stdio.h>

int main(void)
{
	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 %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.

sizeof运算符给出它的运行对象的大小(以字节为单位)。所以sizeof days 是整个数组大小,sizeof day[0]是数组中一个元素的大小(以字节为单位)

自动计数的弊端:无法察觉初始化列表中的项数有误。

10.1.2 指定初始化器(C99)

C99 增加了一个新特性:指定初始化器(designated initializer)

int arr[6] = {0,0,0,0,0,0,212}; //传统语法

C99规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素:

int arr[6] = { [5] = 212};    //把arr[5]初始化为212

对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0。程序清单10.5中的初始化比较复杂。

程序清单10.5

//designate.c --使用指定初始化器
#include <stdio.h>
#define MONTHS 12
int main(void)
{
	int days[MONTHS] = { 31,28,[4] = 31,30,31,[1] = 29};
	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 29 days.
Month  3 has  0 days.
Month  4 has  0 days.
Month  5 has 31 days.
Month  6 has 30 days.
Month  7 has 31 days.
Month  8 has  0 days.
Month  9 has  0 days.
Month 10 has  0 days.
Month 11 has  0 days.
Month 12 has  0 days.

如果未指定元素大小会怎样?

int stuff0- = { 1,[6] =23}; //为7个元素

int staff() = {1,[6] = 4,9,10}//为9个元素

10.1.3 给数组元素赋值

声明数组后,可以借助数组下标(或索引)给数组元素赋值。例如,下面的程序段给数组的所有元素赋值

/*给数组赋值*/
#include <stdio.h>
#define SIZE 50
int main(void)
{
	int counter ,events[SIZE];
	for (counter = 0; counter < SIZE; counter++)
	{
		events[counter] = 2 * counter;
	}
	
	printf("events[49]:%d\n",events[49]); //打印数组最后一个元素的值
	return 0;
}

注意这段代码中使用循环给数组的元素依次赋值。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]都超出了两个数组末尾。 

10.1.4  数组边界

在使用数组时,要防止数组下标超出。也就是说,必须确保下标是有效的值。

考虑程序清单10.6 的问题。该程序创建了一个内含4个元素的数组,然后错误地使用了-1~6的下标。

程序清单10.6 bounds.c程序

//bounds.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;
}

程序输出

value1 = 44,value2 = 88
-1 -1
 0 1
 1 3
 2 5
 3 7
 4 9
 5 5
 6 0
value1 = 9,value2 = -1
address of arr[-1]:0028FF24
address of arr[4]:0028FF38
address of value1:0028FF38
address of value2:0028FF24

注意,该编译器似乎把value2储存在数组的前一个位置,把value1储存在数组的后一个位置(其他编译器在内存中数据顺序可能不同)。 在上面的输出中,arr[-1]与value2对应的内存地址相同,arr[4]和value1对应的内存地址相同。因此,使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中上。

C相信程序员能编写正确的代码,这样的程序运行速度更快。但并不是所有程序员都能做到这一点,所以就出现了下标越界的问题。

还要记住一点:数组元素的编号从0开始。最好是在声明数组时使用符号常量来表示数组的大小:

#define SIZE 4
int main(void)
{
    int arr[SIZE];
    for ()
    {
        int arr[SIZE];
        for (i = 0; i < SIZE; i++)
        .....
    }
}

这样能确保整个程序中的数组大小始终一致。 

10.1.5 指定数组的大小

本章前面的程序示例都使用整型常量来声明数组。

在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型常量。sizeof表达式被视为整型常量。另外,表达式的值 必须大于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之前不允许

变长数组(variable-length array)简称VLS(C11放弃了这一创新的举措,把VLA设定为可选)

10.2 多维数组

气象分析程序。程序目标,计算每年的总降水量、年平均降水量和月平均降水量。要计算年总降水量,必须对一行数据求和;要计算某月分平均降水量,必须对一列数据求和。二维数组很直观,实现这些操作也很容易。程序清单10.7 演示了这个程序。

程序清单10.7 rain.c程序。

/* 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("YESR     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 %.2f inches .\n\n",total / YEARS);
	printf("MONTHLY AVERAGES:\n\n");
	printf("Jan Feb  Mar Apr May Jun Jul Aug Sep Oct 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");
	return 0;
}

程序输出

YESR     RAINFALL  (inches)
 2010            32.4
 2011            37.9
 2012            49.8
 2013            44.0
 2014            32.9

The yearly average is 39.40 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 初始化二维数组

10.2.2  其它多维数组

int box[10][20][30]

可以把一维数组想象成一行数组,把二维数组想象成数据表,把三维数组想象成一叠数据表。例如,把上面声明的三维数组box想象成由10个二维数组(每个二维数组都是20行30列)堆叠起来的。

10.3 指针和数组

第9章介绍过指针 ,指针提供一种以符号形式使用地址的方法。因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。因此,使用用指针的程序更有效率。尤其是,指针能有效地处理数组。数组表示法其实是变相地使用指针。

我们举一个变相使用指针的例子:数组名是数组元素的地址。也就是说,如果flizny是一个数组,下现的语句成立:

flizny == &flizny[0];//数组名是该数组首元素的地址

flizny和&flizny[0]都表示数组首元素的内存地址(&地址运算符)。两者都是常量,在程序的运行过程中,不会改变。但是,可以把它们赋值给指针变量,其后可以修改指针变量的值,如程序清单10.8所示,注意指针加上一个数时,它的值发生了什么变化(转换说明%p通常以十六进制显示指针的值)。

//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);
	return 0;
	}

程序输出

                  short          double
pointers + 0:   0028FF2C   0028FF08
pointers + 1:   0028FF2E   0028FF10
pointers + 2:   0028FF30   0028FF18
pointers + 3:   0028FF32   0028FF20

我们的系统中,地址按字节编址,short类型占用2字节,double类型占用8字节。在C中,指针加1指的是增加一个存储单元。对数组而言,这意味着把加1后的地址是下一个元素的地址,而不是下一个字节的地址(见图10.3)。这是为什么必须声明指针指向对象类型的原因之一。只知道地址不够,因计算机要知道储存对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则*pt就无法正确地取回地址上的值)。

dates + 2 == &date[2]        //相同的地址
*(dates + 2) == dates[2]     //相同的值

以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C语言标准在描述数组表示法进确实借助了指针。也就是说,定义ar[n]的意思是*(ar +n)。可以认为*(ar +n)的意思是“到内存的ar位置,然后移动n个单元,检索储存在那里的值“。

顺带一提,不要混淆*(dates+2)和*dates+2。间接运算符(*)的优先级高于+,所以*dates+2相当于(*dates)+2"

*(dates + 2)        //dates第3个元素的值
*dates + 2         //dates第1个元素的值加2

明白了数组和指针的关系,便可在编写程序时适时使用数组表示法或指针表示法。运行程序清单10.9后输出的结果和程序清单10.2输出的结果相同。

/* day_mon1.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 %2d days.\n",index + 1,*(days + index)); //与days[index]相同
		
		return 0;
}

指针表示法和数组表示法是两种等效的方法。该例演示了可以用指针表示数组,反过来,也可以用数组表示指针。在使用数组为参数的函数时要注意这点。

10.4  函数、数组和指针

编写一个处理数组的函数,该函数返回数组中所有元素之和。待处理的是名为marbles的int类型数组。应该如何调用该函数?也许是下在这样:

total = sum(marbles);  //可能的函数调用

那么,该函数的原型是什么?记住,数组名是该数组首元素的地址,所以实际参数marbles是一个储存int类型值的地址,应把它赋给一个指针形式参数,即该形参是一个指向int的指针:

int sum(int *ar);//对应的函数原型

sum()从该参数获得了什么信息?它获得了该数组首元素的地址,知道要在该位置上找出一个整数。

注意,该参数并未包括数组元素个数信息。我们有两种方法让函数获得这一信息。第一种方法是,在函数代码中写上固定的数组大小:

int sum(int * ar) //相应的函数定义
{
	int i;
	int total = 0;
	
	for(i=0; i < 10; i++ )	//假设数组有10个元素
		total += ar[i];		//ar[i]与*(ar + i)相同
	return total;
} 

既然能使用指针表示数组名,也可以用数组名表示指针 。

该函数定义有限制,只能计算10个int类型的元素。另一个比较灵活的方法是把数组大小作为第2个参数:

int sum(int * ar,int n) //更通用的方法
{
	int i;
	int total = 0;
	
	for(i=0; i < n; i++ )	//使用n个元素
		total += ar[i];		//ar[i]与*(ar + i)相同
	return total;
} 

 这里,第1个形参告诉函数该数组的地址和数据类型,第2个形参告诉函数该数组中元素的个数。

关于函数的形参,还有一点要注意。只有在函数原型或函数定义头中,才可以用int ar[]代替int * ar;

int sum (int ar[],int n);

int *ar形式和int ar[] 形式都表示ar是一个指向int的指针。但是,int ar[]只能用于声明形式参数。第2种形式(int ar[])提醒读者指针ar指向的不仅仅是一个int类型值,还是一个int类型数组元素。

注意 声明数组形参

因为数组名是该数组首元素的地址、作为实际参数的数组名要求形式参数是一个与之匹配的指针,只有在这种情况下,C才会把int ar[] 和int *ar 解释成一样。与就是说,ar是指向int的指针,由于函数原型可以省略参数名,所以下面4种原型都是等价的:


/*
int sum(int *ar,int n);
int sum(int *,int n);
int sum(int ar[],int n);
int sum(int [],int n);
*/

但是,在函数定义中不能省略参数名,下面两种形式的函数定义等价:

/*
int sum(int *ar,int n)
{
	//其他代码已省略
}


int sum(int ar[],int n)
{
	//其他代码已省略
}

*/

 程序清单10.10演示了一个程序,使用sum()函数。该程序打印原始数组的大小和表示该数组的函数形参大小()

程序清单10.10 sum_arr1.c程序

//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 maribles is %ld bytes.\n",
	sizeof marbles);
	
	return 0;
}

int sum(int * ar,int n) //更通用的方法
{
	int i;
	int total = 0;
	
	for(i=0; i < n; i++ )	//使用n个元素
		total += ar[i];		//ar[i]与*(ar + i)相同
	printf("The size of ar is %ld bytes.\n",sizeof ar);
		
	return total;
} 

程序输出如下:

The size of ar is 4 bytes.
The total number of marbles is 190.
The size of maribles is 40 bytes.

注意,marbles的大小是40字节。这没问题,因为marbles内含10个int类型的值,每个值占4字节,所以整个marbles的大小是40字节。但是,ar才4字节。这是因为ar并不是数组本身,它是一个指向marbles数组元素的指针。我们的系统中用4字节储存地址,所以指针变量的大小是4字节(其他系统中地址的大小可能不是4字节)。简而言之,在程序清单10.10中,marbles是一个数组,ar是一个指向marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针ar。

10.4.1 使用指针形参

sum函数,第1 个指针指明数组的开始处,第2个指针指明数组的结束处。程序清单10.11演示了这种方法,同是该程序也表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。

程序清单10.11 sum_arr2.c程序

/*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);
	printf("The size of maribles is %ld bytes.\n",
	sizeof marbles);
	
	return 0;
}

/*使用指针算法 */
int sump(int * start,int * end)
{
	int total = 0;
	
	while (start < end)
	{
        /*
		total += *start; //把数组元素的值加起来
		start++;			//让指针指向下一个元素
		*/
		total += *start++;
	}
		
	return total;
} 

while (start < end)

因为while循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。

还可以把循环体压缩成一行代码:

total +=  * start++;

一元运行符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。也就是说,指针start先递增后指向。使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。如果使用*++start,顺序返过来,先递增指针,再使用指针指向位置上的值。如果使用(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然*start++的写法比较常用,但是*(start++)这样写更清楚。程序清单10.2的程序演示了这些优先级的情况。

程序清单10.12 order.c程序

/*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);
	return 0;
}

程序输出:

  *p1 = 100,   *p2= 100,    *p3 = 300
*p1++ = 100, *++p2= 200,(*p3)++ = 300
  *p1 = 200,   *p2= 200,    *p3 = 301

只有(*p3)++改变了数组元素的值,其他两个操作分别把p1和p2指向数组的下一个元素。

10.4.2 指针表示法和数组表示法

从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。如程序清单10.10所示,使用数组表示法,让函数是处理数组的这一意图更加明显。其他程序员可能更习惯使用指针表示法,觉得使用指针更自然,如程序清单10.11所示。

至于C语言,ar[i]和*(ar+i)这两个表达式者是等价的。无论ar 是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。

指针表示法(尤其与递增运符一起使用时)更接近机器语言,因此,一些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要目的是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。

10.5 指针操作

可以对指针进行哪些操作?C提供了一些基本的指针操作,下面的程序示例中演示了8种不同的操作。为了显示每种操作的结果,该程序打印了指针的值(该指针指向的地址)、储存在指针指向地址上的值,以及指针自己的地址。

程序清单10.13演示了指针变量的8种操作。

程序清单10.13 ptr_ops.c程序

//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);
	printf("ptr2 = %p,*ptr2 = %d,&ptr2 = %p\n",ptr2,*ptr2,&ptr2);
	
	//指针加法
	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 = %d\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;
}

程序输出

pointer value,dereferenced pointer,pointer address:
ptr1 = 0028FF28,*ptr1 = 100,&ptr1 = 0028FF24
ptr2 = 0028FF30,*ptr2 = 300,&ptr2 = 0028FF20

adding an int to a pointer:
ptr1 + 4  = 0028FF38,*(ptr1 + 4) = 500

values after ptr1++
ptr1 = 0028FF2C,*ptr1 = 200,&ptr1 = 0028FF24

values after ptr2--:
ptr2 = 0028FF2C,*ptr2 = 200,&ptr2 = 0028FF20

Pointers reset to original values:
ptr1 = 0028FF28,ptr2 = 0028FF30

subtracting one pointer from another:
ptr2 = 0028FF30,ptr1 = 0028FF28,ptr2 - ptr1 = 2

subtracting an int from a pointer:
ptr3 = 0028FF38,ptr3 -2 = 0028FF30

赋值:可以把地址赋给指针。例如,用数组名、带地址运行符(&)的变量名、另一个指针进行赋值。在该例中,把urn数组的首地址赋给了ptr1,该地址的编号是0028FF28。变量ptr2获得数组urn的第3个元素(urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。

解引用:*运行符给出指针指向地址上储存的值。因此,*ptr1的初值是100,该值储存在编号为0028FF28的地址上。

取址:和所有变量一样,指针变量也有自己地址和值。对指针而言,&运算符给出指针本身的地址。本例中,ptr1储存在内存编号为0028FF24的地址上,该存储单元储存的内容是0028FF28,即urn的地址。因此&ptr1是指向ptr1的指针。而ptr1是指向utn[0]的指针。

指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1 + 4  与&urn[4]等价。如果相加结果超出了初始指针指向的数组范围,计算结果是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递增指针:递增指向数组元素的指针可以让该指针移动至数组的一下个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节),ptr1指向urn[1] 。现在ptr1的值是0028FF2C(数组的下一个元素地址),*ptr的值为200(即urn[1]的值)。注意,ptr1本身的地址仍是0028FF24。毕竟,变量不会因为值发生变化就移动位置。

指针减去一个整数:

递减指针:

指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序注意10.13的输出中,ptr2-ptr1得2,意思是这两个所指向的两悠远互相隔两个int,而不是2字节。只要两个指针都指向相同的数组,C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。

比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

注意,这里的减法有两种。可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针。

使用指针时,不要解引用未初化的指针。

指针的第1 个基本用法是在函数间传递信息。第2个基本用法是用在处理数组的函数中。

10.6 保护数组中的数据

编写一个处理基本类型(如,int)的函数,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数组,只有程序需要在函数中改变数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理数组则效率更高。

传递地址会导致一些问题。C通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常要使用原始数据,因此这样的函数可以修改原数组。有时,这正是我们需要的。例如,下面的函数给数组的每个元素都加上一个相同的值。

int change_arr_price(double ar[],int n,double val)
{
	int i;
	for (i = 0;  i < n; i++)
		ar[i] += val;
	return 0;

调用该函数后,marbles数组中的每个元素的值都增加了2.5:

change_arr_price(marbles,SIZE,2.5);  //数组中每个元素增加2.5

该函数修改了数组中的数据,之所以可以这样做,是因为函数通过指针直接使用了原始数据。

然而,其他函数并不需要修改数据。例如,下面的函数计算数组中所有元素之各,它不用改变数组的数据。但是,由于ar实际上是一个指针,所以编程错误可能会破坏原始数据。例如,下面示例中的ar[i]++会导致数组中每个元素的值都加1:

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 %ld bytes.\n",sizeof ar);
		
	return total;
} 

total += *arr++或total+= *(arr++)//正确。

10.6.1 对形式参数使用const

在K&R C的年代,避免类似错误的唯一方法是提高警惕 。ANSI C提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。例如,sum()函数的原型和定义如下:

int sum(const int ar[],int n) /*函数原型*/
int sum(const int ar[],int n)	/*函数定义*/
{
	int i;
	int total = 0;
	for (i = 0; i < n; i++)
		total += ar[i];
	return total;
}

以上代码中const告诉编译器,该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似ar[i]++表达式,编译器会捕获这个错误,并生成一条错误信息。

程序清单10.14的程序中,一个函数显示数组的内容,另一个函数给数组每个元素都乘以一个给定值。因为第1个函数不用改变数组,所以在声明数组形参时使用了const;而第2个函数需要修改数组元素值,所以不使用const。

程序清单10.14 arf.c程序

/*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);
	
	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机制。

10.6.2 const的其他内容

保护数组

/*const_other.c 演示const数组、const指针和指向const的指针*/
#include <stdio.h>
#define MONTHS 12
int main()
{
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31}; //const保护数组。
days[9] =44;
return 0;  			/*编译错误*/
}

编译时输出:

const_other.c: In function 'main':
const_other.c:7:9: error: assignment of read-only location 'days[9]'
 days[9] =44;
         ^

const指针

/*const_other.c 演示const数组、const指针和指向const的指针*/
#include <stdio.h>
#define MONTHS 12
int main()
{
/*
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31}; //const保护数组。
days[9] =44;				/*编译错误*/
//return 0;


double rates[5] = {88.99,100.12,59.45,183.11,340.5};
const double *pd = rates;	//pd指向数组的首元素
//*pd = 29.89; 				//不允许
//pd[2] = 222.22;		//不允许
rates[0] = 99.99;	//允许,因为rates未被const限定
return 0; 
}

无论是使用指针表示法还是数组表示法,都不允许使用pd修改它所指向数据的值。但是要注意,因为rates并未被声明为const ,所以仍然可以通过rates修改元素的值。另外可以让pd指向别处:

pd++ /*让pd指向rates[1] -- 没问题*/

/*const_other.c 演示const数组、const指针和指向const的指针*/
#include <stdio.h>
#define MONTHS 12
void show_array(const double ar[],int n);
void mult_array(double ar[],int n,double mult);
int main()
{
/*
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31}; //const保护数组。
days[9] =44;				/*编译错误*/
//return 0;


double rates[5] = {88.99,100.12,59.45,183.11,340.5};
const double *pd = rates;	//pd指向数组的首元素
//*pd = 29.89; 				//不允许
//pd[2] = 222.22;		//不允许
//rates[0] = 99.99;	//允许,因为rates未被const限定
//pd++; /*让pd指向rates[1] -- 没问题*/
const double locked[4] ={0.08,0.075,0.0725,0.07};
const double *pc = rates;	//有效
pc = locked;	//有效
pc = &rates[3];	//有效

//然而,只能把非const数据的地址赋给普通指针
double *pnc = rates;	//有效
//pnc = locked;	//无效
pnc = &rates[3];
//这个规则非常合理。否则,通过指针就能改变const数组中的数据。

show_array(rates,5);  //有效
show_array(locked,4);	//有效

//因些,对函数的形参使用const不仅能保护数组,还能让函数处理const数组

mult_array(rates,5,1.2);	//有效
//mult_array(locked,4,1.2);//不要这样做


//C标准规定,使用非const标识符(如,mult_array()的形参ar)修改const数组(如,locked)导致的结果是未定义的。

show_array(rates,5); 
show_array(locked,4);


double * const pcc = rates;	//pcc指向数组的开始
//pcc = &rates[2];			//不允许,因为该指针不能指向别处
*pcc = 92.99;				//没问题--更改rates[0]的值

const double * const pccc = rates;
//pccc = &rates[2];  //不允许
//*pccc = 92.99;		//不允许

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;
}

10.7 指针和多维数组

指针和多维数组有什么关系?为什么要了解它们的关系?处理多维数组的函数要用到指针,所以在使用这种函数之前,先要更深入地学习指针。到于第1个问题,我们通过几个示例来回答。

程序清单10.15 zippol.c程序

/* zippol.c -- zipppo的相关信息*/
#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 = 0028FF20,   zippo + 1 = 0028FF28
zippo[0] = 0028FF20,zippo[0] + 1 = 0028FF24
   *zippo = 0028FF20, *zippo + 1 = 0028FF24
zippo[0][0] = 2
  *zippo[0] = 2
    **zippo = 2
      zippo[2][1] = 3
*(*(zippo + 2)+1) = 3

二维数组zippo的地址和一维数组zippo[0]的地址相同,它们的地址都是各自数组首元素的地址,因而与&zippo[0][0]的值也相同。

zippo[0]指向一个4字节的数据对象。zippo[0] 加1 ,其值加4(20+24得24)。数组名zippo是一个内含2个int类型值的数组地址,所以zippo指向一个8字节的数据对象。因此,zippo加1 ,它所指向的地址加8字节(20+8得28)。

zippo[0]和*zippo完全相同。对二维数组名解引用两次,得到储存在数组中的值。使用两个间接运算符(*)或者使用两个对方括号([])都能获得该值。

zippo[2][1]的等价法是*(*(zippo+2)+1)。

10.7.1  指向多维数组的指针

如声明一个指针变量pz指向一个二维数组(如,zippo)在编写处理类似zippo这样的二维数组时会用到这样的指针。把指针声明为指向int 的类型还不够。因为指向int只能与zippo[0]的类型匹配,说明该指针指向一个int类型的值。但是,zippo是它首元素的地址,该元素是一个内含两个int类型值的一维数组。因此,pz必须指向一个内含两个int类型值 的数组,而不是指向一个int类型值,其声明如下:

int (*pz)[2]    //pz指向一个内含两个int类型的数组

以上代码把pz声明为指向一个数组的指针,该数组内含两个int 类型的值 。

 程序清单10.16 zippo2.c程序

/* zippo1.c -- 通过指针获取zippo的信息*/
#include <stdio.h>
int main(void)
{
	int zippo[4][2] = {{2,4},{6,8},{1,3},{5,7}};
	int (*pz)[2];//*先与pz结合,pz是一个指向数组的(内含两个int类型的值)指针
	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("      zippo[2][1] = %d\n",zippo[2][1]);
	printf("*(*(pz + 2)+1)    = %d\n",*(*(pz + 2)+1));
	return 0;
}

程序输出:

   pz = 0028FF1C,   pz + 1 = 0028FF24
pz[0] = 0028FF1C,pz[0] + 1 = 0028FF20
  *pz = 0028FF1C,  *pz + 1 = 0028FF20
pz[0][0] = 2
  *pz[0] = 2
  **pz   = 2
      zippo[2][1] = 3
*(*(pz + 2)+1)    = 3

10.7.2 指针的兼容性

指针之间的赋值比数值类型之间的赋值要严格。例如,不同类型转换就可以把int类型的值赋给double类型的变量。但是两个类型的指针不能这样做。

/*ptr_compa.c 演示指针兼容性*/
#include <stdio.h>
int main()
{
		int n = 5;
		double x;
		int *pl = &n;
		double *pd = &x;
		x= n;			//隐式类型转换
//		pd = pl;		//编译时错误

	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;		//都是指向int的指针
	*p2 = ar2[0];//都是指向int的指针
	**p2 = ar2;	//无效
		return 0;
}

注意,以上无效的赋值表达式语句中涉及的两个指针都是指向不同的类型。例如,pt 指向一个int类型值,而ar1指向一个内含3个int类型元素的数组。类似地,pa指向一个内含3个int类型元素的数组,它与ar1的类型兼容,但是ar2指向一个内含2 个int类型的数组,所以 pa与ar2不兼容。

10.7.3 函数和多维数组

如果要编写处理二维数组的函数,首先要能正确地理解指针才能写出声明函数的形参。在函数体中,通常使用数组表示法进行相关操作。

下面,我们编写一个处理二维数组的函数。一种方法是,利用for循环处理一维数组的函数应用到二维数组的每一行。如下所示:

/*arrray2d1.c --处理二维数组,用for循环处理一维数组的函数应用到二维数组的每一行*/
#include <stdio.h>
int sump(int * start,int * end);
int main(void)
{
	int junk[3][4] = {{2,4,5,8},{3,5,6,9},{12,12,8,6}};
	int i,j;
	int total = 0;
	for (i = 0; i < 3; i ++)
		total += sump(junk[i],junk[i] + 4); //junk[i]是一维数组
	
	printf("The total number of 'junk[3][4] = {{2,4,5,8},{3,5,6,9},{12,12,8,6}}' is %ld.\n",total); 
	return 0;
}

/*使用指针算法 */
int sump(int * start,int * end)
{
	int total = 0;
	
	while (start < end)
	{
		/*
		total += *start; //把数组元素的值加起来
		start++;			//让指针指向下一个元素
		*/
		total += *start++;
	}
		
	return total;
} 

记住,如果junk是二维数组,junk[i]就是一维数组,可将其视为二维数组的一行。这里sum()函数计算二维数组的每行的总和,然后for循环再把每行的总和加起来。

函数要知道行和列的信息,可以通过声明正确类型的形参变量来完成,以便函数能正确地传递数组。在这种情况下,数组junk是一个内含3个数组的数组,每个元素是内含4个int类型值的数组(却junk是一个3行4列的二维数组)。通过前面的讨论可知,这表明junk是一个指向数组(内含4个int类型值)的指针。可以这样声明函数的形参:

void somefunction( int  (*pt)[4] );

另外,如果当且仅当pt是一个函数的形式参数时,可以这样声明:

void somefunction( int pt[][4]);

注意,第1个方括号是空的。空的方括号表明pt是一个指针。这样的变量稍后可以用作相同方法作为junk。下面的程序示例就是这样做的,如程序清单10.17所示。注意该程序演示了3种等价的原型语法。

程序清单10.17 array2d.c程序

//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}
	};
	sum_rows(junk,ROWS);
	sum_cols(junk,ROWS);
	printf("Sum of all elements = %d\n",sum2d(junk,ROWS));
	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

注意,ar和main()中的junk都使用数组表示法。因为ar和junk的类型相同,它们都是指向内含4个int类型值的数组的指针。

注意,下面的声明不正确:

int sum(int ar[][],int rows);//错误的声明

前面介绍过,编译器会把数组表示法转换成指针表示法。例如,编译器会把ar[10转换成ar+1。编译器对ar+1求值,要知道ar所指向的对象大小。下面的声明:

]nt sum(int ar[][4],int rows);//有效的声明

表示ar指向一个内含4个int类型值的数组(在我们的系统中,ar指向的对象占16字节),所以ar+1的意思是“该地址加上16字节”。如果第2个方括号是空的,编译器就不知道怎样处理。

一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号的值::

ins sum4d(int ar[][12][20][30],int rows);

因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。

下面的声明与该声明等价:

int sum4d(int (*ar)[12][20][30],int rows); //ar是一个指针

这里,ar指向一个12*20*30的int数组。

10.8 变长数组(VLA)

C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。如下所示:

int quarters = 4;

int regions = 5;

double sales[regions][quarters];//一个变长数组(VLA)

前面提到过,变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类型说明符(第12章介绍)。而且,不能在声明中初始化它们。最终,C11把变长数组作为一个可选特性,而不是必须强制实现的特性。

注意 变长数组不能改变大小

变长数组中的“变”不是指可以修改已创建数组的大小,一旦创建了变长数组,它的大小则保持不变,这里的“变”指的是“在创建数组时,可以使用变量指定数组的维度。

由于变长数组是C语言的新特性,目前完全支持这一特性的编译器不多。下面我位来看一个简单的例子:如何编写一个函数,计算int的二维数组所有元素之和。

首先,要声明一个带一维变长数组参数的函数,如下所示:

int sum2d(int rows,int cols,int ar[rows][cols]); //ar是一个变长数组(VLA)

程序清单10.18 vararr2d程序

//vararr2d.c --使用变长数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
int sum2d(int rows,int cols,int ar[rows][cols]);
void show_array(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];	//变长数组
	
	for(i = 0 ; i < rs; i++)
		for (j = 0; j < cs; j++)
			varr[i][j] = i*j +j;
		printf("3*4 array\n");
		printf("Sum of all elements = %d\n",sum2d(ROWS,COLS,junk));
		
		printf("2*6 array\n");
		printf("Sum of all elements = %d\n",sum2d(ROWS -1,COLS+2,morejunk));
		
		printf("3*10 array\n");
		printf("Sum of all elements = %d\n",sum2d(rs,cs,varr));
		show_array(rs,cs,varr);
			
}

//带变长数组形参的函数
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;
}
/*显示数组的内容*/
void show_array(int rows,int cols,int ar[rows][cols])
{
	int r;
	int c;
	for (r = 0; r < rows; r++)
		for(c = 0; c <cols; c++)
		printf("%8d",ar[r][c]);
	putchar('\n');
}

程序输出:

3*4 array
Sum of all elements = 80
2*6 array
Sum of all elements = 315
3*10 array
Sum of all elements = 270
       0       1       2       3       4       5       6       7       8       9
       0       2       4       6       8      10      12      14      16      18
       0       3       6       9      12      15      18      21      24      27

10.9  复合字面量

假设给带int类型形参的函数传递一个值,要传递int类型的变量,但是也可以传递int类型常量,如5。在C99标准以前,对于带数组形参的函数,情况不同,可以传递数组,但是没有等价的数组常量。C99新增了复合字面量(compound literal)。字面量是除符号常量外的常量。例如,5是int类型字面量,81.3是double类型的字面量,'Y'是char类型的字面量,"elephant"是字符串字面量。发布C99的委员会认为,如果有代表数组和结构内容的复合字面量,在编程时会更方便。

对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名。例如,下面是一个普通的数组声明:

int diva[2] = {10,20};

下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:

(int [2]){10,20}              //复合字面量

注意,去掉声明中的数组名,留下的int [2] 即是复合字面量的类型名。

初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:

(int []){50,20,90}               //内含3个元素的复合字面量

因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。也就是说,可以这样用:

int *pt1;

pt1 = (int [2]){10,20};

注意,该复合字面量的字面常量与上面创建的diva数组的字面常量完全相同。与有数组名的数组类似,复合字面量的类型名也代表首元素的地址。所以可以把它赋给指向int的指针。然后便可使用这个指针。例如,本例中*pt1 是10,pt1[1]是20。

还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:

int sum(const int ar[],int n);

...

int toal3;

total3 = sum((int []){4,4,4,5,5,5,},6);

这里,第1个实参是内含6个int类型值的数组,和数组名类似,这同是是该数组首元素的地址。

这种用法的好处是,把信息传入函数前不必先创建数组,这是复合字面量的典型用法。

可以氢这种用法应用于二维数组或多维数组。例如,下面的代码演示了如何创建二维int数组并储存其地址:

int (*pt2)[4];	//声明一个指向二维数组的指针,该数组内含2个数组元素,
				//每个元素是内含4个int类型值的数组
pt2 = (int [2][4]){{1,2,3,-9},{4,5,6,-8}};

如上所示,该复合字面量的类型是int[2][4],即一个2*4的int 数组。

程序清单10.19把上述例子放进一个完整的程序中。

//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];//声明一个指向二维数组的指针,该数组内含2个数组元素,
				//每个元素是内含4个int类型值的数组
	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

要支持C99的编译器才能正常运行该程序示例。

记住,复合字面量提供临时需要的值的一种手段。复合字面量具有块作用域(第12章将介绍相关内容),这意味着一旦离开定义复合字面量的块,程序将无法保证字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中。

猜你喜欢

转载自blog.csdn.net/tjjingpan/article/details/85089101
今日推荐