第 10 章 数组和指针
在本章中你将学习下列内容:
· 关键字: static (静态)
· 运算符: & * (一元)
· 创建与初始化数组的方法。
· 指针(基于已学的基础知识)及指针和数组间的关系。
· 编写使用数组函数。
· 二维数组。
人们借助计算机来记录每月开支,日降水量,季度销售额,以及每周收支情况等。企业借助计算机来管理员工薪水,仓库存货清单,以及客户交易的记录等。程序员不可避免地需要处理大量的想到关联的数据。采用数组通常能够有效便捷地处理这类数据。第 6 章“C 控制语句:循环”中已经介绍了数组,本章将进一步讨论它。本章主要介绍如何编写处理数组的函数。处理数组的函数可以把模块化编程的优势应用于数组。同时,你还将看到数组和指针之间紧密的联系。
10.1 数组
回忆一下,数组(array)由一系列类型相同的元素构成。可以使用声明来告诉编译器你需要一个数组。数组声明(array declaration)中包括数组元素的数目和元素的类型。编译器根据这些信息创建合适的数组。数组元素可以具有同普通变量一样的类型。考虑下面数组声明的例子:
/* 一些数组声明的例子*/
int main (void)
{
float candy [365]; /* 365 个浮点数的数组 */
char code[12]; /* 12 个字符的数组 */
int states[50]; /* 50 个整数的数组 */
...
}
方括号([])表示 candy 和其他两个标识符均为数组,方括号内的数字指明了数组所包含的元素数目。
要访问数组中的元素,可以使用下标数字来表示单个元素。下标数字也称索引(index),是从 0 开始计数的。因此,candy[0] 是数组 candy 的首元素,candy[364]是第 365 个元素,也就是最后一个元素。这些我们已经比较熟悉了,以下将介绍一些新内容。
-------------------------------------------------------------------
10.1.1 初始化
程序中通常使用数组来存储数据。例如,含有 12 个元素的数组可以用来存储 12 个月份的天数。在这种情况下,程序开始时就初始化数组比较方便,下面介绍初始化方法。
你已经知道可以在单个数值变量(有时也称为标量)的声明中用表达式来初始化它,如下所示:
int fix = 1;
float flax = PI * 2;
此处,表达式中的 PI 已定义为宏。C 为数组的初始化引入了以下新语法:
int main (void)
{
int powers[8] ={1,2,4,6,8,16,32,64}; /* 只有 ANSI C 支持这种初始化的方式 */
...
}
从以上例子中可以看出,可以使用花括号括起来的一系列数值来初始化数组。数值之前用逗号隔开,在数值和逗号之前可以使用空格符。这样,首元素(powers[0])赋值为 1,依次类推(如果你的编译器不支持这种初始化,提示这是一个语法错误,那么你使用的是 ANSI 以前的编译器。在数组定义之前添加关键字 static 可解决此问题。第 12 章“存储类,链接和内存管理”将详细讨论这个关键字)。程序清单 10.1 的功能是打印出每个月的天数。
程序清单 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 %d 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 年仅打错一个月份的天数。程序使用括在花括号里的一系列数值对 days[]进行初始化,数值之间用逗号分开。
注意本例采用标识符常量 MONTHS 来代表数组的大小。这是一种常用的也是我们所推荐的做法。如果要采用每年 13 个月的历法,只用修改 #define 语句即可,无须查找并修改程序中每一个使用数组大小的地方。
----------------------------------------------------------------------
PS: 对数组使用 const 的方法
有时需要使用只读数组,也就是程序从数组中读取数值,但是程序不向数组中写数据。在这种情况下声明并初始化数组时,建设使用关键字 const。 我们对程序清单 10.1 的一部分进行优化,结果如下:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
这样,程序会把数组中每个元素当成常量来处理。和普通变量一样,需要在声明 const 数组时对其进行初始化,因为在声明之后,不能再对它赋值。明确了这一点,以后的例子中我们就可以对数组使用 const 了。
#include<stdio.h> int main(void) { ]={,,,,,,,}; //powers[1]=1; getchar(); ; }
---------------------------------------------------------------------------
使用末经初始化的数组会出现什么情况?程序是 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 ("%d %14d \n", i, no_data[i]);
system("PAUSE");
return 0;
}
下面是一个示例输出结果(你的运行结果可能有所不同)
i no_data[i]
0 144
1 2499312
2 36
3 2
与变通变量相似,在初始化之前数组元素的数值是不定的。编译器使用的数值是存储单元中已有的数值,因此上面的输出结果是不确定一的。
-----------------------------------------------------------------------
PS: 存储类解释
和其他变量相似,数组可以被定义为多种存储类(storage class),第 12 章将详述此主题。目前,只需要了解本章的数组属于自动存储类。也就是说,数组是在一个函数内声明的,并且声明时没有使用关键字 static 。到目前为止,本书所用的变量和数组都是自动类型的。
现在提起存储类的原因是:不同存储类有时具有不同的属性,因此不能把本章的知识推广到其他存储类。例如,如果没有进行初始化,一些存储类的变量和数组会把它们的存储单元设置为 0 。
---------------------------------------------------------------------------
初始化列表中的元素数目应该和数组大小一致。如果二者不一致,会出现什么情况? 我们仍然使用前面那个例子,如程序清单 10.3 所示,其中的初始化列表中缺少两个数组元素。
程序清单 10.3 somedata.c 程序
--------------------------------------------------------------
/* somedata.c --- 部分初始化的数组 */
#include <stdio.h>
#define SIZE 4
int main (void)
{
int some_data[SIZE] = {1492,1066};
int i;
printf ("%2s%12s \n","i","some_data[i]");
for (i = 0; i < SIZE; i++)
printf ("%2d%12d \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 程序
---------------------------------------------------------
/* 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]);
return 0;
}
程序清单 10.4 中有两点需要注意:
1·当使用空的方括号对数组进行初始化时,编译器会根据列表中的数值数目来确定数组大小。
2·注意 for 循环的控制语句。由于人工计算容易出错。因此可以让计算机来计算数组的大小。
运算符 sizeof 给出其后的对象或类型的大小(以字节为单位)。因此 sizeof days 是整个
数组的大小(以字节为单位),sizeof days[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
意外! 我们只向数组内放入了 10 个数值,但是我们的想法让程序自动找到数组的大小以免我们试图向数组填入进多的元素。这暴露出自动计数的弊端:初始化的元素个数有误时,我们可能意识不到。
另外,还有一个很简短的初始化数组的方法,这种方法仅限于字符串,将在下一章中详述。
-----------------------------------------------------------------------------
10.1.2 指定初始化项目 [C99]
C99 增加了一种新特性:指定初始化项目(designated initializer)。此特性允许选择对某些元素进行初始化。例如:要对数组的最后一个元素初始化。按照传统的 C 初始化语法,需要对每一个元素都初始化,才可以对最后的元素进行初始化:
int arr[6] = {0,1,2,3,4,5}; // 传统语法
而 C99 规定,在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素:
int arr[6] = {[5] = 5}; // 把 arr[5] 初始化为 5
对于通常的初始化,在初始化一个或多个元素后,末经初始化的元素都将被设置为 0 。程序清单 10.5中是一个较为复杂的例子。
程序清单 10.5 designate.c 程序
----------------------------------------------------------------------
/* 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]);
return 0;
}
如果编译器支持 C99 特性 则输出结果如下:
1 31
2 29
3 0
4 0
5 31
6 30
7 31
8 0
9 0
10 0
11 0
12 0
注:BCB 2010 VC2005 都不支持。。。
从输出结果可以看出指定初始化项目有两个重要特性。第一,如果在一个指定初始化项目后紧跟有不止一个值,例如在序列 [4]=31,30,31 中这样,则这些数值将用来对后续的数组元素初始化。也就是说,把 31 赋给 days[4]之后,接着把 30 和 31 分别赋给 days[5] 和 days[6]。第二,如果多次对一个元素进行初始化,则最后的一次有效。例如,在程序清单 10.5中,前面的 days[1]初始化为 28,而后面的指定初始化 [1]=29 覆盖了前面的数值,于是 days[1]的数值最终为 29 。
10.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] = oxec[SIZE]; // 不正确
yaks[SIZE] = {5,3,2,8}; // 不起作用
10.1.4 数组边界
使用数组的时候,需要注意数组索引不能超过数组的边界。也就是说,数组索引应该具有对于数组来说有效的值。例如,假定你有这样的声明:
int doofi[20];
那么你在数组索引的时候,要确保它的范围在 0 和 19 之间,因为编译器不会为你检查出这种错误。
考虑程序清单 10.6 中的程序。它创建了一个包含 4 个元素的数组,但却不小心使用了从 -1 到 6 的索引值
程序清单 10.6 bounds.c 程序
---------------------------------------------------------------
/* bounds.c ----- 超出数组的边界 */
#include <stdio.h>
#define SIZE 4
int main (void)
{
int valuel = 44;
int arr[SIZE];
int value2 = 88;
int i;
printf ("valuel = %d, value2 = %d \n",valuel,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 ("valuel = %d, value2 = %d \n",valuel,value2);
return 0;
}
编译器不检查索引的合法性。在标准 C 中,如果使用了错误的索引,程序执行结果是不可知的。也就是,程序也许能够运行,但是运行结果可能很奇怪,也可能会异常中断程序的执行。我们使用 BCB 2010 运行程序,其输出结果如下:
valuel = 44, value2 = 88
-1 -1
0 1
1 3
2 5
3 7
4 4
5 88
6 44
valuel = 44, value2 = 88
注意我们使用的编译器看起来是把 value2 正好存储在数组后面的那个存储单元中,把 value1 存储在数组前面的那个存储单元中(其他的编译器可能采取不同的顺序在内存中存储数据)。这样 arr[-1]就和 value1 对应同一个存储单元,arr[4] 和 value2 对应同一个存储单元。因此,使用超出数组边界的索引会改变其他变量的数值。对于不同的编译器,输出结果可能不同。
也许你会产生疑问,为什么 C 会允许这种事情发生。这仍然是出于 C 信任程序员的原则。不检查边界能够让 C 程序的运行速度更快。在程序运行之前,索引的值有可能尚未确定下来,所以编译器此时不能找出所有的索引错误。为了保证程序的正确性,编译器必须在运行时添加检查每个索引是否合法的代码,这会导致程序的运行速度减慢。因此,C 相信程序员的代码是正确的,从而可以得到速度更快的程序。但是并不是所有程序员都能够完美地做到这一点,因此问题就产生了。
一件需要记住的简单的事情就是,数组的计数是从 0 开始的。避免出现这个问题比较简单的方法是:在数组声明中使用符号常量,然后程序中需要使用数组大小的地方都直接引用符号常量:
#define SIZE 4
int main (void)
{
int arr[SIZE];
for (i = 0; i < SIZE; i++)
....
这样做的好处是保证整个程序中数组大小始终一致。
--------------------------------------------------------
10.1.5 指定数组大小
在前面提到的例子中,我们声明数组时使用的是整数常量:
#define SIZE 4
int main (void)
{
int arr[SIZE]; // 符号整数常量
double lots[144]; // 文字整数常量
还允许使用什么? 直到 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 类型指派为 int 类型
float a8[n]; // C99 之前不允许
float a9[m]; // C99 之前不允许
请参看上面的注释,遵循 C90 标准的 C 编译器不允许最后两个声明。而 C99 标准允许这两个声明,但这创建了一种新数组,称为变长数组 (variable-length array),简称 VLA。
C99 引入变长数组主要是为了使 C 更适于做数值计算。例如, VLA 的引入简化了将 FORTRAN 语言的数值运算例程库转换为 C 代码的过程。VLA 有某些限制;例如,声明时不能进行初始化。在充分了解古典 C 数组的局限性之后,我们将在本章的后面详细介绍 VLA。
10.2 多维数组
例如:气象分析员要分析 5 年中每月的降水量数据,首先需要解决的问题如何表示出这些数据。一种方法是用 60 个变量,每个变量代表一个数据项目(前面曾经提到过这种方法,和前面提到时的情况一样,这不是合适的方法)。使用一个 60 个元素数组的方法虽然可以采用,但是把各年度的数据单独放置会更好。也可以设置 5 个数组,每个数组包含 12 个元素。这是一种比较笨拙的方法,而且如果要处理的数据不是 5 年,而是 50 年,这种方法就很不合适。我们需要找到一种更好的方法。
更好的处理方法是使用一个数组的数组,即:主数组包含 5 个元素,每个元素代表一年。代表一年的元素是包含 12 个元素的数组。这种数组的数组,我们称之为二维数组。下面是这种数组的声明方法:
float rain[5][12]; // 5 个由 12 个浮点数组成的数组的数组
理解这个声明的一种方法是首先查看位于中间的那部分
float rain[5] [12] // rain 是一个包含 5 个元素的数组
这部分说明 rain 是一个包含 5 个元素的数组。至于每个元素的情况,需要查看声明的其余部分;
float rain[5] [12]; // 12个浮点数的数组
这说明每个元素的类型是 float[12];也就是说,rain 具有 5 个元素,并且每个元素都是包含 12 个 float 数值的数组。
按此推理,rain 的首元素 rain[0] 是一个包含 12 个 float 数值的数组。rain[1],rain[2]等等也是如此。rain[0]是数组,那么它的首元素是 rain[0][0],第二个元素是 rain[0][1],依此类推其他元素。简单地说,rain 是包含 5 个元素(每个元素又是包含 12 个 float 数的数组)的数组,rain[0]是包含 12 个 float 数的数组,rain[0][0]是一个 float 数。如果访问位于 2 行 3 列的元素,则用 rain[2][3](注意:数组中计数是从 0 开始的,因此 2 行实际指的是第 3 行)。
也可以把 rain 数组看作是一个二维数组,它包含有 5 行,每行 12 列,如图 10.1 所示。改变第二个下标,可以沿着一行移动,每移动一个单位代表一个月分。改变第一个下标,可以沿着一列垂直移动,每移动一个单位代表一个。
用二维视图表示数组便于我们直观地想象具有两个索引的数组。实际上,数组是顺序存储的,前 12 个元素之后,跟着就是第二个包含 12 个元素的数组,依次类推。
我们将在气象分析程序中采用这个二维数组。程序的目标是计算出年降水总量,年降水平均量,以及月降水平均量。要计算年降水总量,需要对某一行的数据求和。要计算某月的降水平均量,需要把对应于这个月份的列的所有数据求和。二维数组使这些计算变得直观有序,实现起来也比较文件。程序清单 10.7 展示了这个程序。
程序清单 10.7 rain.c 程序
------------------------------------------------------------------------------------------
/* rain.c 针对若干年的降水量数据,计算年降水总量,年降水平均量,以及月降水平均量 */
#include <stdio.h>
#define MONTHS 12
#define YEARS 5
int main (void)
{
// 把数组初始化为 2000 年到 2004 年降水量数据
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",2000+year,subtot);
total += subtot; // 所有年度的总降水量
}
printf ("\nThe yearly average is %.1f inches \n\n",total/YEARS);
printf ("MONTHLY ACERAGES; \n\n");
printf (" Jan Feb Mar Apr May Jun Jul Aug Sep Oct"); //为了显示每个月份
printf (" Nov Dec \n");
for (month = 0; month < MONTHS; month++)
{ // 对于每个月,各年该月份的总降水量
for (year = 0, subtot = 0; year < YEARS; year++)
subtot += rain[year][month];
printf ("%4.1f ",subtot/YEARS);
}
printf ("\n");
return 0;
}
输出如下:
YEAR RAINFALL (inches)
2000 32.4
2001 37.9
2002 49.8
2003 44.0
2004 32.9
The yearly average is 39.4 inches
MONTHLY ACERAGES;
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
研究此程序时,请注意数组的初始化方法和计算方案。而数组初始化是其中较复杂的部分,让我们先来研究相对简单的一部分(计算)。
要计算某年度的降水总量,则保持 year 为常量,让 month 遍历整个范围,这正是程序第一部分的内部 for 循环的作用。程序第一部分的外部循环的目的则是让变量 year 在值域(5年)内遍历。像这样的嵌套循环结构在处理二维数组时是比较方便的。利用一个循环处理第一个下标,利用另一个循环处理第二个下标。
程序第二部分的结构和第一部分相同,但 year 被改为内部循环,而 month 被改为外部循环。注意,自问循环每执行一次,内部循环完整遍历一次。因此,在月份改变之前,年度先遍历。先得到的是 5 年中一月份的降水平均量,然后依次类推。
10.2.1 初始化二维数组
对二维数组的初始化是建立在对一维数组的初始化之上的。首先,让我们回忆一下对一维的初始化方法,如下所示:
sometype arl[5] = {val1,val2,val3,val4,val5};
此处 val1,val2 等代表同 sometype 类型相应的数值。例如,如果 sometype 是 int,val1 可以是 7; 如果 sometype 是 double ,那么 val1 可以是 11.34 。而 rain 是包含 5 个元素的数组,每个元素又是包含 12 个 float 数的数组。因此,对于 rain ,val1 应该对一维 float 数组进行初始化。如下所示:
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6}} // 12个
也就是说,如果 sometype 是一个包含 12 个 double 数的数组,那么 val1 就是一个由 12 个double 数构成的数值列表。因此,可以采用以逗号分隔的 5 个这样的数值列表来初始化像 rain 这样的二维数组:
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}, // 第一个数组(内有12个元素)
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3}, // 第二个数组(内有12个元素)
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4}, // 第三个数组(内有12个元素)
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2}, // 第四个数组(内有12个元素)
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}, // 第五个数组(内有12个元素)
};
这个初始化使用了 5 个数值列表,每个数值列表都用花括号括起来。第一个列表被赋给数组的第一行,第二个列表被赋给数组的第二行,依此进行赋值。前面讨论的数据个数和数组大小的不匹配问题同样适用于此处的每一行。也就是说,如果第一个列表中有 10 个数值,则第一行只有 10 个元素得到赋值,最后 2 个元素被默认初始化为 0 。如果静静中的数值多于 12 个,则报告错误;而且这些数值不会影响到下一行的赋值。
初始化时候也可以省略内部的花括号,只保留最外面的一对花括号。只要保证数值的个数正确,初始化就是一样的。如果数值的个数不够,那么在数组初始化的时候,按照先后顺序来逐行赋值,因此前面的元素首先得到赋值,直到没有数值为止。后面没有赋值的元素被初始化为 0 。图 10.2 示意了这两种初始化的方法。
由于数组 rain 中存放不应该被修改的数据,因此在声明数组时使用了 const 修饰符。
------------------------------------------------------------
10.2.2 更多维数的数组
前面关于二维数组的讨论对于三维及至更多维数的数组同样适用。可以用如下方式声明三维数组:
int box[10][20][30];
可以这样直观地理解:一维数组是排成一行的数据,二维数组是放在一个平面上的数据,三维数组是把平面一层一层地垒起来。例如,可以把上面定义的数组 box 直观想象为数据构成的方块:由 10 个二维数组(每个二维数组都是 20行,30列)堆放起来构成的立方体。
另一种理解 box 的方法认为它是数组的数组的数组。即: box 是包含 10 个元素的数组,其中每个元素又是包含 20 个元素的数组,这 20 个元素中的每一个又是包含 30 个元素的数组。或者可以简单地按照所需的索引数目去理解数组。
通常处理三维数组时候需要 3 重嵌套循环,处理四维数组需要 4 重嵌套循环,对于其他多维数组,依此类推。在后面的章节中,我们只用二维数组来举例。
10.3 指针和数组
在第 9 章“函数”中提到过,指针提供了一种用来使用地址的符号方法。由于计算机的硬件指令很大程序上要依赖于地址,所以指针使你能够以类似于计算机底层的表达式来表达自己的意愿。这使得使用了指针的程序能够更高效地工作。特别地,指针能够很有效地处理数组。我们将看到,数组标记实际上是一种变相使用指针的形式。
我们举一个这种变相使用的例子:数组名同时也是该数组首元素的地址。也就是说,如果 flizny 是一个数组,下面的式子是正确的:
flizny == &flizny[0] //数组名是该数组首元素的地址
flizny 和 &flizny[0]都代表首元素的内存地址(回忆一下,&是地址运算符)。两者都是常量,因为在程序的动作过程中它们保持不变。然而可以把它们作为赋给指针变量的值,然后你可以修改指针变量的值,如程序清单 10.8 所示。请注意给指针加上一个数的时候,它的值会发生什么变化(回忆一下,指针说明符 %p 通常以十六进制形式显示值)。
程序清单 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 %10s \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: 0x0064fd20 0x0064fd28
pointers + 1: 0x0064fd22 0x0064fd30
pointers + 2: 0x0064fd24 0x0064fd38
pointers + 3: 0x0064fd26 0x0064fd40
第 2 行打印两个数组的起始地址,第 3 行是地址加 1 的结果,等等。请注意地址是十六进制的,因此 30 比 2f 大 1,比 28 大 8 。怎么回事?
0x0064fd20 + 1 等于 0x0064fd22?
0x0064fd30 + 1 等于 0x0064fd38?
真奇怪! 我们的系统是按字节编址的,但是 short 类型使用 2 个字节, double 类型使用 8 个字节。在 C 中,对一个指针加 1 的结果是对该指针增加 1 个存储单元 (storage unit)。对于数组而言,地址会增加到下一个元素的地址,而不是下一个字节(请参见图 10.3)。这就是为什么在声明指针时必须声明它所指向对象的类型。计算机需要知道存储对象所用的字节数,所以只有地址信息是不够的(即使指针是指向标题的,也需要声明指针类型;否则 *pt 操作不能正确返回数值)。
现在我们能够清楚地定义指向 int 的指针,指向 float 的指针,以及指向其他数据对象的指针:
· 指针的数值就是它所指向的对象的地址。地址的内部表示方式是由硬件来决定的。很多种计算机(
包括 PC 机和 Macintosh 机)都是以字节编址的,这意味着对每个内存字节顺序进行编号。对于
包含多个字节的数据类型,比如 double 类型的变量,对象的地址通常指的是其首字节的地址。
· 在指针前运用运算符 * 就可以得到该指针所指向的对象的数值。
· 对指针加 1 ,等价于对指针的值加上它指向的对象的字节大小。
下面的等式出了 C 的优点:
dates + 2 == &date[2]; // 相同的地址
*(dates + 2) == dates[2]; // 相同的值
这些关系总结了数组和指针间的密切关系:可以用指针标识数组的每个元素,并得到每个元素的数值。从本质上说,对同一个对象有两种不同的符号表示方法。C 语言标准在描述数组时,确实借助了指针的概念。例如,定义 ar[n]时,意思是 *(ar+n),即“寻址到内存中的 ar ,然后移动 n 个单位,再取出数值”。
顺便提一下,请注意区分 *(dates+2)和 *dates+2 。间接运算符(*)的优先级高于 + ,因此后者等价于 (*dates)+2.
*(dates +2 ) // dates 的第 3 个元素的值
*dates +2 // 将第 1 个元素的值和 2 相加
理解了数组和指针的关系,编程的时候就可以方便地选择两者中任意一种方法。例如,程序清单 10.9 和程序清单 10.1 编译后的运行输出结果一样。
程序清单 10.9 day_mon3.c 程序
------------------------------------------------------------------------
/* day_mon3.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 %d days \n",index+1,*(days+index)); // 与 days[index]相同
return 0;
}
此处,days 是数组首元素的地址;days+index 是元素 days[index]的地址;*(days+index)是这个元素的值,与 days[index]等价。每次循环会依次引用一个数组元素,并打印出该数组元素的内容。
这样编写程序有优势吗?不一定。程序清单 10.9 的例子只是用来表明指针和数组是两个等效的方法。这个例子表明可以使用指针来标记数组;反之亦然,也可以用数组方式来访问指针。当设计程序时,如果用到数组作为函数的参数,那么这一点就是很重要的。
10.4 函数,数组和指针
假设你要编写一个对数组进行操作的函数,目的是要此函数返回数组内所有元素的和,并假设 marbles 为这个 int 数组的名称。应该如何调用这个函数?一种合乎情理的猜测如下:
total = sum(marbles); // 可能的函数调用
那么原型应该是什么样的?数组名同时代表数组首元素的地址,因此实际参数 marbles 是一个 int 的地址,应把它赋给一个类型为指向 int 的指针的形式参量:
int sum (int *ar); // 相应的原型
函数 sum()从该参数可以得到什么信息呢?它得到数组首元素的地址,而且知道可以从此地址找到一个 int 。请注意它无从知道数组中元素的数量。于是在函数的定义中有两种选择,第一种是在函数代码中写上固定的数组大小,如下如示:
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;
}
上面的代码利用了这样的事实:正如可以在指针符号中使用数组名一样,也可以在数组符号中使用指针。同时,运算符 += 把其右边的操作数加到左边。因此, total 得到的是数组元素的和。
这种函数定义是有限制的,它仅在数组大小为 10 时可以工作。更函数的方法是把数组大小做为第二个参数传递给函数。
int sum (int *ar , int n) // 更适用的方法
{
int i;
int total = 0; //试验中 如果此处 total 不初始化 0 值的话,结果将是错误的
for (i = 0; i < n; i++) // 使用 n 表示元素的个数
total += ar[i]; // ar[i] 与 *(ar+i)相同
return total;
}
这里的第一个参数把数组地址和数组类型的信息传递给函数,第二个参数指导数组中的元素个数传递给函数。此外,关于函数参量还有一件需要说明的事情:在函数原型或函数定义头的场合中(并且也只有在这两种场合中),可以用 int *ar 代替 int ar[]:
int sum (int ar[], int n);
无论在任何情况下,形式 int *ar 都表示 ar 是指向 int 的指针。形式 int ar[] 也可以表示 ar 是指向 int 的指针,但只是在声明形式参量时才可以这样使用。使用第二种形式可以提醒读者 ar 不仅指向一个 int 数值,而且它指向的这个 int 是一个数组中的元素。
-------------------------------------------------------------------------
PS: 声明数组参量
由于数组名就是数组首元素的地址,所以如果实际参数是一个数组名,那么形式参量必须是与之相匹配的指针。在(而且仅在)这种场合中,C 对于 int ar[] 和 int *ar 作出同样解释,即 ar 是指向 int 的指针。由于原型允许省略名称,因此下面的 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);
{
// 代码
}
前面提到的 4 种原型是通用的,它们的函数定义可以采用上面两者之一。这些形式你都应该掌握。
-----------------------------------------------------------------------------
程序清单 10.10 是一个使用函数 sum()的程序。为了说明关于数组参数的一个有趣的事实,此程序同时打印出原数组的大小和代表数组的函数参量的大小(如果你的编译不支持用 %zd 说明符打印
sizeof 的返回值,请使用 %u 或者 %lu )。
程序清单 10.10 sum_arr1.c 程序
------------------------------------------------------
/* sum_arr1.c -- 对一个数组的所有元素求和 */
#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 %lu bytes \n",sizeof marbles);
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 %lu 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 字节。的确如此,因为 marbles 包含 10 个 int 类型的数,每个数占 4 个字节,因此总共占用 40 个字节。但是 ar 的大小只有 4个字节。这是因为 ar 本身并不是一个数组,它是一个指向 marbles 的首元素的指针。对于采用 4 字节地址的计算机系统,指针的大小为 4 个字节(其他系统中地址大小可能不是 4 个字节)。总之,在程序清单 10.10 中, marbles 是一个数组,而 ar 为一个指向 marbles 首元素的指针,C 中数组和指针之间的关系允许你在数组符号中使用指针 ar 。
-----------------------------------------------------------------------------
10.4.1 使用指针参数
使用数组的函数需要知道何时开始和何时结束数组。函数 sum()使用一个指针参量来确定数组的开始点,使用一个整数参量来指明数组的元素个数(指针参量同时确定了数组中数据的类型)。但是这并不是向函数传递数组信息的唯一方法。另一种方法是传递两个指针,第一个指针指明数组的起始地址(同前面的方法相同),第二个指针指明数组的结束地址。程序清单 10.11 中的示例程序示意了 这种方法。这个例子同时利用了指针参数是变量这一事实,也就是说,程序中没有使用索引来指示数组中的每个元素,而是直接修改指针本身,使指针依次指向各个数组元素。程序清单 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);
return 0;
}
/* 使用指针算术*/
int sump (int *start, int *end)
{
int total = 0;
while (start < end) // 这里的循环体可以精简为 total +=*start++;
{ //一元运算符 * 和 ++ 具有相等的优先级,但它在结合时是从右向左进行的
total +=*start; // 把值累加到 total 上
start++; // 把指针向前推进到下一个元素
}
return total;
}
----------------------------------------------------------------
由于指针 start 最初指向 marbles 的首元素,因此执行赋值表达式 total+= *start 时,把首元素的值(即 20) 加到 total 上。然后表达式 start++ 使指针变量 start增 1 ,从而指向数组的下一个元素。start 是指向 int 的指针,因此当 start 增 1 时它将增加 1 个 int 的大小。
请注意函数 sump() 和 sum() 结束加法循环的方式不一样。函数 sum()使用数组元素的个数做为第二个参数,循环利用这个值来控制循环次数:
for (i = 0; i < n; i++)
而函数 sump() 则使用第二个指针来控制循环次数
wihle (start < end )
因为这是一个对于不相等关系的判断,所以处理的最后一个元素将是 end 所指向的位置之前的元素。这就意味着 end 实际指向的位置是在数组最后一个元素之后。C 保证在为数组分配存储空间的时候,指向数组之后的第一个位置的指针也是合法的。这使上面例子中采用的结构是有效的,因为 start 在循环中最后得到的值是 end 。请注意使用这种“越界”指针可使函数调用的形式更整洁:
answer = sump (marbles, marbles + SIZE);
由于索引是从 0 开始的,因此 marbles + SIZE 指向数组结尾之处后的下一个元素。如果让 end 指向最后一个元素而不是指向数组结尾处之后下一个元素,就需要使用下面的代码:
answer = sump (marbles, marbles + SIZE - 1);
这种写法不仅仅看起来不整洁,而且也不容易被记住,因此比较容易导致编程错误。顺便说一句,尽管 C 保证指针 marbles + SIZE 是合法的,但对 marbled[SIZE](即该地址存储的内容)不作任何保证。
可以把上面的循环体精简为一行代码:
total += *start++;
一元运算符 * 和 ++ 具有相等的优先级,但它在结合时是从右向左进行的。这就意味着 ++ 应用于 start, 而不是应用于 *start。也就是说,是指针自增 1 ,而不是指针所指向的数据自增 1 。后缀形式(即 start++, 而不是 ++start)表示先把指针指向的数据加到 total上,然后指针再自增 1 。如果程序使用 *++start,则顺序就变为指针先自增 1 ,然后再使用其指向的值。然而如果程序使用 (*start)++,那么会使用 start 所指向的数据,然后再使该数据自增 1 ,而不是使指针自增 1 。这样,指针所指向的地址不变,但其中的元素却变成了一个新数据。 尽管 *start++ 比较常用,但为了清晰起见,应该使用 *(start++)。程序清单 10.12 中的程序示意了这些有关优先级的微妙之处。
程序清单 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 提供了 6 种基本的指针操作,下面的程序将具体演示这些操作。为了显示每一个操作结果,程序将打印出指针的值(即指针指向的地址),指针指向地址中存储的内容,以及指针本身的地址(如果你的编译器不支持 %p 说明符,那么要想打印出地址,就需要用 %u 或 %lu )。
程序清单 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 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 + 3 ) = %d\n",ptr1+4, *(ptr1 + 3));
/* 递增指针 */
ptr1++;
printf ("\nvalues after ptr1++ \n");
printf ("ptr1 = %p, *ptr1 = %d, &ptr1 = %p \n",ptr1,*ptr1,&ptr1);
/* 递减指针 */
--ptr2;
printf ("\n values 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 ("\n subtracting 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 address
ptr1 = 0012FF38, *ptr1 = 100 , &ptr1 = 0012FF34
adding an int to a pointer :
ptr1 + 4 = 0012FF48, *( ptr4 + 3 ) = 400
values after ptr1++
ptr1 = 0012FF3c, *ptr1 = 200, &ptr1 = 0012FF34
values after --ptr2
ptr2 = 0012FF38, *ptr2 = 200, &ptr2 = 0012FF30
Pointers reset to original values :
ptr1 = 0012FF38, ptr2 = 0012FF40
subtracting one pointer from another :
ptr2 = 0012FF40, ptr1 = 0012FF38, ptr2 - ptr1 = 2
subtracting an int from a pointer :
ptr3 = 0012FF48, ptr3 - 2 = 0012FF40
自解
-----------------------------------------------------
1. 指向
Pointer value, dereferenced pointer address
ptr1 = 0012FF38, *ptr1 = 100 , &ptr1 = 0012FF34
ptr1指向的地址 ptr1指向的值 ptr1本身的内存地址
------------------------------------------------------
2. 指针加法
adding an int to a pointer :
ptr1 + 4 = 0012FF48, *( ptr1 + 3 ) = 400
ptr1 +4 指向的地址 (+4 是加 四个指向数组的单元,这里是 int 类型 所以是 4 x 4 = 16
也恰好是 0012FF48 这个地址 0012FF48 - 0012FF38 = F(即16);
*(ptr1 + 3) 指向 urn[3] 的值 正好是 400
-------------------------------------------------------
3. 递增指针
values after ptr1++
ptr1 = 0012FF3c, *ptr1 = 200, &ptr1 = 0012FF34
ptr1++ (即+1)这里指针类型是 int (4个字节) ptr++ 也正好是 0012FF38 + 4 = 0012FF3C
*ptr1++ 即指向 urn[1] 这个元素 值也正好是 200
ptr1++ 后,它本身的处在的地址不变 即 0012FF34
------------------------------------------------------------
4. 递减指针
values after --ptr2
ptr2 = 0012FF38, *ptr2 = 200, &ptr2 = 0012FF30
ptr2-- 后 指向的地址是 0012FF38,
*ptr2 本身指向的是 urn[2]的值 即 300 -- 后 指向上一个数组元素 即 200
ptr2 本身的地址不变, ptr1 所在的地址是 0012FF34 ptr2 所在的地址是 0012FF30
--------------------------------------------------------------
5. 恢复为初始值
Pointers reset to original values :
ptr1 = 0012FF38, ptr2 = 0012FF40
ptr1 初始是 指向 urn[0] 即 0012FF38
ptr2 初始是 指向 urn[2] 即 0012FF40
---------------------------------------------------------------
6. 一个指针减去另一个指针
subtracting one pointer from another :
ptr2 = 0012FF40, ptr1 = 0012FF38, ptr2 - ptr1 = 2
0012FF40 - 0012FF38 = 8 (int 4字节) 即 = 2;
---------------------------------------------------------------
7. 一个指针减去另一个整数
subtracting an int from a pointer :
ptr3 = 0012FF48, ptr3 - 2 = 0012FF40
0012FF48 - 2 (int 4字节 实际上 减去 8 )所以 = 0012FF40
-----------------------------------------------------------------
下面的列表描述了可对指针变量执行的基本操作
1. 赋值(assignment)----可以把一个地址赋给指针。通常使用数组名或地址运算符 & 来进行地址赋值。本例中,把数组 urn 的起始地址赋给 ptr1,该地址是编号为 0012ff38 的内存单元。变量 ptr2 得到的是数组第 3 个的地址即 urn[2]。 注意:地址应该和指针类型兼容。也就是说,不能把一个 double 类型的地址赋给一个指向 int 的指针。 C99 允许类型指派这样做,但是我们不推荐使用这种方法。
---------------------------------------------------------------
2. 求值(value-finding)或取值(dereferenclng) --- 运算符 * 可以取出指针指向地址中存储的数值。因此,*ptr1 开始为 100,即 urn[0] 存储在地址 0012ff38 中的值。
------------------------------------------------------------------
3. 取指针地址 --- 指针变量同其他变量一样具有地址和数值,使用运算符 & 同样可以得到存储指针本身的地址。本例中,ptr1 被存储在内存地址 0012ff34中。 该内存单元的内容是 0012ff38,即 urn 的地址。
----------------------------------------------------------------------
4. 将一个整数加给指针 --- 可以使用 + 运算符来把一个整数加给一个指针,或者把一个指针加给一个正数。两种情况下,这个整数都会和指针所指类型的字节数相乘,然后所得的结果会加到初始地址上。于是, ptr+4 的结果等同于 &urn[4]。如果相加的结果了超出了初始指针所指出的数组的范围,那么这个结果是不确定的,除非超出数组最后一个元素的地址能够保证是有效的。
-----------------------------------------------------------------------
5. 增加指针的值 ---- 可以通过一般的加法或增量运算符来增加一个指针的值。对指向某数组元素的指针做增量运算,可以让指针指向该数组的下一个元素。因此, ptr1++ 运算把 ptr1 加上数值 4(我们的系统上的 int 为 4 个字节)使得 ptr1 指向 urn[1] (请参见图 10.4)。现在 ptr1 的值是
0012ff3c(下一个数组元素的地址), *ptr 的数值为 200 (urn[1]的值)。请注意 ptr1 本身的地址仍然是 0012ff34 。另忘了,变量不会因为它的值的变化而移动位置。
-------------------------------------------------------------------------
6. 从指针中减去一个整数 -- 可以使用 - 运算符来从一个指针中减去一个整数。指针必须是第一个操作数,或者是一个指向整数的指针。这个整数都会和指针所指类型的字节数相乘,然后所得的结果会从初始地址中减掉。于是,ptr3 - 2 的结果等同于 &urn[2], 因为 ptr3 是指向 &urn[4]的。如果相减的结果超出了初始指针所指向的数组的范围,那么这个结果是不确定的,除非超出数组最后一个元素的地址能够确保是有效的。
----------------------------------------------------------------------------
7. 减小指针的值 --- 指针当然也可以做减量运算。本例中,ptr2 自减 1 之后,它将不再指向第三个元素,而是指向第二个数组元素。请注意,你可以使用前缀和后缀形式的增量和减量运算符。对指针 ptr1 和 ptr2 都指向同一个元素 urn[1],直到它们被重置。
------------------------------------------------------------------------------
8. 求差值(Differencing)--- 可以求出两个指针间的差值。通常对分别指向同一个数组同两个元素的指针求差值,以求出元素之间的距离。差值的单位是相应类型的大小。例如在程序清单 10.13 的输出中, ptr2 - ptr1 的值是 2 ,表示指针所指向对象之间的距离为 2 个 int 数值大小,而不是 2 个字节。有效指针差值运算的前提是参加 运算的两个指针是指向同一个数组(或是其中之一指向数组后面的第一个地址)。指向两个不同数组的指针之间的差值运算可能会得到一个数值结果,但也可能会导致一个运行时错误。
-----------------------------------------------------------------------------------
9. 比较 -- 可以使用关系运算符来比较两个指针的值,前提是两个指针具有相同的类型。
--------------------------------------------------------------------------------------
注意,这里有两种形式的减法。可以用一个指针减掉另一个指针得到一个整数,也可以从一个指针中减去一个整数得到一个指针。
在进行指针的增量和减量运算时,需要牢记一些注意事项。计算机并不检查指针是否仍然指向某个数组元素。 C 保证指向数组元素的指针和指向数组后的第一个地址的指针是有效的。但是如果指针在进行了增量或减量运算后超出了这个范围,后果将是未知的。另外,可以对指向一个元素的指针进行取值运算。但不能对指向数组的第一个地址的指针进行取值运算,尽管这样的指针是合法的。
------------------------------------------------------------------------
PS: 对末初始化的指针取值
使用指针,有一个规则需要特别注意:不能对末初始化的指针取值。例如下面的例子:
int *pt; // 末初始化的指针
*pt = 5; // 一个可怕的错误
为什么这样的代码危害极大?这段程序的第二行表示把数值 5 存储在 pt 所指向的地址。但是由于 pt 没有被初始化,因此它的值是随机的,不知道 5 会被存储到什么位置。这个位置也许对系统危害不大,但也许会覆盖程序数据或代码,甚至导致程序的崩溃。切记:当创建一个指针时,系统只分配了用来存储指针本身的内存空间,并不分配用来存储数据的内存空间。因此在使用指针之前,必须给你赋予一个已分配的内存地址。比如,可以把一个已存在的变量地址赋给指针(当你使用一个指针参量的函数时,就属于这种情况)。或者使用函数 malloc()来首先分配内存,该函数将在第 12 章详细讨论。总之,使用指针时一定要注意,不能对末初始化的指针取值!
double *pd; // 末初始化的指针
*pd = 2.4; // 不能这样做
------------------------------------------------------------------------
给定下面的声明:
int urn[3];
int *ptr, *ptr2;
表 10.1 中是一些合法的和非法的语句:
表 10.1 一些合法和非法的语句
-----------------------------------------------------------
合法 非法
-----------------------------------------------------------
ptr1++; urn++;
-----------------------------------------------------------
ptr1 = ptr1 + 2; ptr2 = ptr2 + ptr1;
------------------------------------------------------------
ptr2 = urn + 1; ptr2 = urn *ptr1;
-----------------------------------------------------------
你可以将数组名理解为一个指针常量
这些操作带来很多可能性。C 程序员创建了指针数组,函数指针,指向指针的指针数组,指向函数的指针数组等等。但是不要紧张,我们后面的学习重点将放在已经学过的基本使用方式上。指针最基本的功能在于同函数交换信息。从前面学过的内容可知,如果需要让被调函数修改调用函数中的变量,就必须使用指针。指针的另一个基本功能是用在处理数组的函数中。下面我们再来看一个同时使用函数和数组的例子。
10.6 保护数组内容
在编写处理诸如 int 这样的基本类型函数时,可以向函数传递 int 数值,也可以传递指向 int 的指针。通常我们直接传递数值;只有需要在在函数中修改该值时,我们才传递指针。对于处理数组的函数,只能传递指针,原因是这样能使程序的效率更高。如果通过值向函数传递数组,那么函数中必须分配足够存入一个原数组的拷贝的存储空间,然后把原数组的所有数据复制到这个新数组中。如果简单地把数组的地址传递给函数,然后让函数直接读写原数组,程序的效率会更高。
这种技术也会带来一些问题。通常 C 传递数据的值,其原因是要保证原始数据的完整性。函数使用原始数据的一份拷贝,这样它就不会意外地修改原始数据。但是,由于处理数组的函数直接操作原始数据,所以它能够修改原数组。有时候这正是我们所需要的,例如下面这个函数的功能就是给数组中的每个元素加上同一个数值。
void add_to (dobule ar[], int n, double val)
{
int i;
for (i = 0; i < n; i++)
ar[i] += val;
}
因此,下面的函数调用将使数组 prices 里的每个元素增加 2.5:
add_to (prices,100,2.50);
该函数改变了数组的内容。之所以可以改变数组的内容,是因为函数使用了指针,从而能够直接使用原始数据。
然而也许其他的函数并不希望修改数据。例如下面这个函数的功能是计算数组中所有元素的和,所以它不应该改变数组的内容。然而由于 ar 实际上是一个指针,所以编程上的错误可以导致原始数据遇到破坏。例如,表达式 ar[i]++ 就会导致每个元素的值增加 1:
int sum (int ar[], int n) //错误的代码
{
int i;
int total = 0;
for (i = 0; i < n; i++);
total +=ar[i]++; // 错误地增加了每个元素的值
return total;
}
--------------------------------------------------------------------------
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;
}
这告知编译器:函数应当把 ar 所指向的数组作为包含常量数据的数组对待。这样,如果你意外地使用了诸如 ar[i]++ 之类的表达式,编译器将会发现这个错误并生成一条错误消息,通知你函数试图修改常量。
需要理解的是这样使用 const 并不要求原始数组是固定不变的;这只是说明函数在处理数组时,应把数组当作是固定不变的。使用 const 可以对数组提供保护,就像按值传递可以对基本类型提供保护一样;可阻止函数修改调用函数中的数据。总之,如果函数想修改数组,那么在声明数组参量时就不要使用 const ;如果函数不需要修改数组,那么在声明数组参量时最好使用 const 。
请参看程序清单 10.14 中的程序,其中一个函数显示数组,另一个函数对数组的每个元素乘上一个给定的数值。因为第一个函数不需要修改数组,所以使用 const ;因为第二个函数需要悠数组,所以不使用 const 。
程序清单 10.14 arf.c 程序
------------------------------------------------------------------
/* arf.c --- 处理数组的函数 */
#include <stdio.h>
#define SIZE 5
void show_array (const double ar[], int n);
void mult_arrar (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 arrar : \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_arrar (double ar[], int n, double mult) // 用同一乘数去乘每个数组元素
{
int i;
for (i = 0; i < n; i++)
ar[i] *= mult;
}
输出结果如下;
The original dip arrar :
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
请注意两个函数都是 vois 类型的。函数 mult_array 确实使数组 dip 得到了新值,但是通过指针实现的,而不是使用 return 机制实现的。
--------------------------------------------------------------------------
10.6.2 有关 const 的其他内容
前面我们讲过可使用 const 来创建符号常量:
const double PI = 3.14159;
以上也可以使用 #define 指令来实现。但使用 const 还可以创建数组常量,指针常量以及指向常量的指针。程序清单 10.4 说明了使用关键字 const 保护数组的方法:
#define MONTHS 12
const int dyays[MONTHE] = {31,28,31,30,31,30,31,31,30,31,30,31};
如果随后的程序代码试图改变数组,你将得到一个编译时的错误消息:
days[9] = 44; //编译错误
指向常量的指针不能用于修改数值,考虑下列代码:
double rates[5] = {88.99,100.12,59.45,183.11,340.5};
const double *pd = rates; // pd指向数组开始处
第二行代码把 pd 声明为指向 const double 的指针。这样,就不可以使用 pd 来修改它所指向的数值。
*pd = 29.89; // 不允许
pd[2] = 222.22; // 不允许
rates[0] = 99.99; // 允许,因为 raees 不是常量
无论是采用数组符号还是打针符号,都不能使用 pd 修改所指向数据的值。但请注意 因为 rates 并没有声明为常量,所以仍可以使用 rates 来修改其数值。另外要注意,还可以让 pd 指向其他地址:
pd++; // 让 pd 指向 rates[1] - 这是允许的
通常把指向常量的指针用作函数参量,以表明函数不会用这个指针来修改数据。例如,程序清单 10.14 中函数 show_array()的原型可以如下定义;
void show_array (const double *ar, int n);
关于指针赋值和 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 * pd = rates; //合法
pd = locked; //合法
pd = &rates[3]; //合法
这样的规则是合理的。否则,你就可以使用指针来修改被认为是常量的数据。
这些规则的实践结果是:像 show_array()这样的函数可以接受普通数组和常量数组的名称作为实际参数,因为两种参数都可以赋给指向常量的指针:
show_array (rates,5); //合法
show_array (locked,5); //合法
但是,像 mult_array()这样的函数不能接受常量数组的名称作为参数:
mult_array (rates,5,1.2); //合法
mult_array (locked,4,1.2); //不允许
因此,在函数参量定义中使用 const ,不仅可以保护数据,而且使函数可以使用声明为 const 的数组。
const 还有很多用法。例如:你可以使用关键字 const 来声明并初始化指针,以保证指针不会指向别处,关键在于 const 的位置:
double rates[5] = {88.99,100.12,59.45,183.11,340.5};
double * const pd = rates; // pd 指向数组的开始处
pd = &rates[2]; // 不允许
*pd = 92.99; // 可以,更改 rates[0] 的值
这样的指针仍然可用于修改数据,但它只能指向最初赋给它的地址。
最后,可以使用两个 const 来创建指针,这个指针既不可以更改所指向的地址,也不可以修改所指向的数据:
double rates[5] = {88.99,100.12,59.45,183.11,340.5};
const double * const pd = rates;
pd = &rates[2]; // 不允许
*pd = 92.99; // 不允许
10.7 指针和多维数组
指针和多级数组有什么关系?为什么我们需要知道它们之间的关系?函数是通过指针来处理多维数组的,因此在使用这样的函数之前,你需要更多地了解指针。对于第一个问题,让我们通过几个例子来找出答案。为了简化讨论,我们采用比较小的数组。假设有如下声明:
int zippo[4][2]; // 整数数组的数组
数组名 zippo 同时也是数组首元素的地址。在本例中,zippo 的首元素本身又是包含两个 int 的数组,zippo 也是包含两个 int 的数组的地址。下面从指针属性进一步分析:
· 因为 zippo 是数组首元素的地址,所以 zippo 的值和 &zippo[0]相同。另一方面,zippo[0]本身是包含两个整数的数组,因此 zippo[0]的值同其首元素(一个整数)地址 &zippo[0][0]相同。简单地说,zippo[0]是一个整数大小对象的地址,而 zippo 是两个整数大小对象的地址。因为整数和两个整数组成的数组开始于同一个地址,因此 zippo 和 zippo[0]具有相同的数值。
· 对一个指针(也即地址)加 1 ,会对原来的数值加上一个对应类型大小的数值。在这方面,zippo和 zippo[0]是不一样的,zippo 所指向对象的大小是两个 int ,而 zippo[0] 所指向对象的大小是一个 int。因此 zippo+1 和 zippo[0]+1 的结果不同。
· 对一个指针(也即地址)取值(使用运算符 * 或者带有索引的[]运算符)得到的是该指针所指向对象的数值,即一个 int 数值。同样,*zippo 代表其首元素 zippo[0]的值,但是 zippo[0]本身就是一个 int 数的地址,即 &zippo[0][0],因此 *zippo 是 zippo[0][0]。对这两个表达式同时应用取值运算符将得到 **zippo 等价于 *&zippo[0][0],后者简化后即为一个 int 数 zippo[0][0]。简言之,zippo 是地址的地址,需要两次取值才可以得到通常的数值。地址的地址或指针的指针是双重间接(double indirection)的典型例子。
显然,增加数组维数会增加指针的复杂度。现在,大多数 C 初学者都会认识到为什么指针被认为是该语言中最难掌握的部分。认真地学习了前面所讲的内容后,请阅读程序清单 10.15 中的实例,此例显示了一些地址值和数组内容。
程序清单 10.15 zippo1.c 程序
------------------------------------------------------------
/* 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)); // 二维数组名必须两次取值才可以取出数组中存储的数据
return 0;
}
在一个系统上,输出结果如下:
zippo = 0012FF34, zippo + 1 = 0012FF3C
zippo[0] = 0012FF34, zippo[0] + 1 = 0012FF38
*zippo = 0012FF34, *zippo + 1 = 0012FF38
zippo[0][0] = 2
*zippo[0] = 2
**zippo = 2
zippo[2][1] = 3
*(*(zippo+2)+1) = 3
其他系统的输出结果会与上面的不同,但输出结果之间的关系是和上面一样的。输出显示出二维数组 zippo 的地址和一维数组 zippo[0]的地址是相同的,均为相应的数组首元素的地址,它的值是和&zippo[0][0]相同的。
然而,差别也是有的。在我们的系统上, int 是 4 个字节长。前面我们讨论过,zippo[0]指向 4 字节长的数据对象。对 zippo[0]加 1 导致它的值增加 4 。数组名 zippo 是包含两个 int 数的数组的地址,因此它指向 8 字节长的数据对象,所以,对 zippo 加 1 导致它的值增加 8 。
程序显示 zippo[0] 和 *zippo 是相同的,这点是正确的。另一方面,二维数组名必须两次取值才可以取出数组中存储的数据。这可以两次使用间接运算符 (*)来实现,或两次使用方括号运算符([])(也可以采用一次 * 和一次 []来实现,但我们不讨论这么多的情况)。具体地:zippo[2][1]的等价指针符号表示为 *(*(zippo+2)+1)。你应尽力去分析理解。表 10.2 中分步建立了这个表达式;
表 10.2 分析 *(*(zippo+2)+1)
----------------------------------------------------------------------------
zippo 第 1 个大小为 2 个int 的元素的地址
-----------------------------------------------------------------------------
zippo+2 第 3 个大小为 2 个 int 的元素的地址
-----------------------------------------------------------------------------
*(zippo+2) 第 3 个元素,即包含 2 个 int 值的数组,因此也是其第 1 个元素(int值)的地址
-----------------------------------------------------------------------------------------
*(zippo+2)+1 包含 2 个 int 值的数组的第 2 个元素(int 值)的地址
------------------------------------------------------------------------------
*(*(zippo+2)+1) 数组第 3 行第 2 个 int (zippo[2][1])的值
-----------------------------------------------------------------------------
这里使用指针符号来显示数据的意图并不是为了说明可以用它替代更简单的 zippo[2][1]表达式,而是要说明当你正好有一个指向二维数组的指针并需要取值时,最好不要使用指针符号,而应当使用形式更简单的数组符号。
图 10.5 以另一种视图显示了数组地址,数组内容和指针之间的关系。
图 10.5 数组的数组
--------------------------------------------------------------------------
zippo zippo+1 zippo+2 zippo+3
| zippo[0] | | zippo[1] | | zippo[2] | | zippo[3] |
地址 |zippo |zippo | |zippo |zippo | |zippo |zippo | |zippo |zippo |
|[0][0]|[0][1]| |[1][0]|[1][1]| |[2][0]|[2][1]| |[3][0]|[3][1]|
----------------------------------------------------------------
OBF2 OBF4 OBF6 OBF8 OBFA OBFC OBFE 0C00
| | |
*zippo | |
*zippo+1 |
*zipoo+2
--------------------------------------------------------------------------
10.7.1 指向多维数组的指针
如何声明指向二维数组(如 zippo)的指针变量 pz?例如,在编写处理像 zippo 这样的数组的函数时,就会用到这类指针,指向 int 的指针可以胜任吗?不可以。这种指针只是和 zippo[0]兼容,因为它们都指向一个单个 int 值。但是 zippo 是其首元素的地址,而该首元素又包含了两个 int 值的数组。因此,pz 必须指向一个包含两个 int 值的数组,而不是指向一个单个 int 值。下面是正确的代码:
int (*pz)[2]; // pz 指向一个包含 2 个 int 值的数
该语句表明 pz 是指向包含两个 int 值的数组的指针。为什么使用圆括号?因为表达式中[]的优先级高于 * 。因此,如果我们这样声明:
int *pzx[2];
那么首先方括号和 pax 结合,表示 pax 是包含两个某种元素的数组。然后和 * 结合,表示 pax 是两个指针的数组。最后,用 int 来定义,表示 pax 是由两个指向 int 值的指针构成的数组。这种声明会创建两个 int 值的数组的指针。程序清单 10.16 显示了如何使用指向二维数组的指针。
程序清单 10.16 zippo2.c 程序
-------------------------------------------------------
// 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 = %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;
}
******************************************************************
自解: 个人认为 数组概念的难理解的一个原因是 数组元素是从 0 开始
而人的习惯性计数是从 1 开始,所以很容易出现偏差。
以后一接触数组的话,计数时 一定要从 0 开始。
********************************************************************
输出结果如下:
pz = 0012FF30, pz + 1 = 0012FF38
pz[0] = 0012FF30, pz[0] + 1 = 0012FF34
*pz = 0012FF30, *pz + 1 = 0012FF34
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) /* 这里的 + 是必须的,实际不是进行加法运算,而是指向 m 或 n
元素的地址或数值 */
#include <stdio.h> int main (void) { ,,,,,,,,,}; int *p; p=days; p[]=;//可见对于一元数组也适用 ; }
----------------------------------------------------------
10.7.2 指针兼容性
指针之间的赋值规则比数值类型的赋值更严格。例如,你可以不需要进行类型转换就直接把一个 int 数值赋给一个 double 类型的变量。但对于指针来说喧样的赋值是不允许的:
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; //都指向int[3]
pa = ar2; //非法 数组指针的长度应与内层数组的长度相同
p2 = &pt; //都指向 int *
*p2 = ar2[0]; //都指向 int
p2 = ar2; //非法
请注意,上面的非法赋值都包含着两个不指向同一类型的指针。例如,pt 指向一个 int 数值,但是 ar1 指向由 3 个 int 值构成的数组。同样,pa 指向由 3 个 int 值构成的数组,因此它与 ar1 的类型一致,但是和 ar2 的类型不一致,因为 ar2 指向由 2 个 int 值构成的数组。
后面的两个例子比较费解。变量 p2 是指向 int 的指针的指针,然而,ar2 是指向由 2 个 int 值构成的数组的指针(简单一些说,是指向 int[2]的指针)。因此 p2 和 ar2 类型不同,不能把 ar2 的值赋给 p2 。 但是 *p2 的类型为指向 int 的指针,所以它和 ar2[0]是兼容的。前面讲过,ar2[0]是指向其首元素 ar2[0][0]的指针,因此 ar2[0] 也是指向 int 的指针。
一般地,多重间接运算不容易理解。例如,考虑下面这段代码:
int *pl;
const int *p2;
const int **pp2;
p1 = p2 ; //非法, 把 const 指针赋给非 const 指针
p2 = p1; //合法, 把非 const 指针赋给 const 指针
pp2 = &p1;//非法, 把 const 指针赋给非 const 指针
正如前面所提到的,把 const 指针赋给非 const 指针是错误的,因为你可能会使用新指针来改变 const 数据。但是把非 const 指针赋给 const 指针是允许的,这样的赋值有一个前提:只进行一层间接运算:
这样理解:如果一个const指针被一个非const指针所指,那么它就可变了,所以不被允许
p2 = p1; //合法, 把非 const 指针赋给 const 指针
在进行两层间接运算时,这样的赋值不再安全。如果允许这样赋值,可能会产生如下的问题:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // 不允许,但我们假设允许
*pp2 = &n // 合法,二者都是 const ,但这同时会使 p1 指向 n
*p1 = 10; // 合法,但这将改变 const n 的值
-----------------------------------------------------------------------
10.7.3 函数和多维数组
如果要编写处理二维数组的函数,首先需要很好地理解指针以便正确声明函数的参数。在函数体内,通常可以使用数组符号来避免使用指针。
下面我们编写一个处理二维数组的函数。一种方法是把处理一维数组的函数应用到二维数组的每一行上,也就是如下所示这样处理:
int junk[3][4] = { {2,4,5,8},{3,5,6,9},{12,10,8,6} };
int i,j;
int total = 0;
for (i = 0; i < 3; i++)
total +=sum(junk[i],4); // junk[i] 是一维数组
如果 junk 是二维数组,那么 junk[i]就是一维数组,可以把它看做是二维数组的一行。函数 sum ()计算二维数组每行的和,然后由 for 循环把这些和加起来得到“总和”。
然而,使用这种方法得不到行列信息。在这个应该程序(求总和)中,行列的信息不重要,但是假设每行代表一年,每列代表一月,则可能一个函数来计算某个列的和。这种情况下,函数需要知道行列的信息。要具有行列信息,需要恰当地声明形参变量以便于函数能够正确地传递数组。在本例中,数组 junk 是 3 行 4 列的 int 数组。如果前面讨论中所指出的,这表明 junk 是指向由 4 个 int 值构成的数组的指针。声明此类函数参量的方法如下所示:
void somefunction (int (*pt)[4]);
当且仅当 pt 是函数的形式参量时,也可以作如下这样声明:
void somefunction (int pt[][4]);
注意到第一个方括号里是空的。这个空的方括号表示 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;
}
********************************************************************
晕死,编译的后 tot的值都不正确,找了二十分钟,却没发现问题,
用文件对比来看了下,结果又是 在一个 for 语句后面了一个分号
多分号和括号问题真的是郁闷啊,浪费了不少的时间
********************************************************************
输出结果如下:
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
程序清单 10.17 中的程序把数组名 junk(即指向首元素的指针,首元素是子数组)和符号常量 ROWS(代表行数,数值为 3)做为参数传递给函数。每个函数都把 ar 看做是指向包含 4 个 int 值的数组的指针。列数是在函数体内定义的,但是行数是靠函数传递得到的。这个函数可以工作在多种情况下。例如,如果把 12 做为行数传递给函数,则它可以处理 12 行 4 列的数组。这是因为 rows 是元素的数目;然而,每个元素都是一个数组,或者看做是一行,rows 也就可以看做是行数。
请注意 ar 的使用方式同 main()中 junk 的使用方式一样。这是因为 ar 和 junk 是同一类型,它们都是指向包含 4 个 int 值的数组的指针。
请注意下面的声明是不正确的:
int sum2 (int ar[][], int rows); // 错误的声明
回忆一下,编译器会把数组符号转换成指针符号。这就意味着,(例如) ar[1]会被转换成 ar+1 。编译器这样转换的时候需要知道 ar 所指向对象的数据大小。下面的声明:
int sum2 (int ar[][4], int rows); //合法的声明
就表示 ar 指向由 4 个 int 值构成的数组,也就是 16 个字节长(本系统上)的对象,所以 ar+1 表示“在这个地址上加 16 个字节大小”。如果是空括号,则编译器不能正确处理。
也可以如下这样在另一对方括号中填写大小,但编译器将忽略之:
int sum2 (int ar[3][4], int rows); // 合法声明,但 3 将被忽略
与使用 typedef 相比,这种形式要方便得多:
typedef int arr4[4]; // arr4 是 4 个 int 的数组
typedef arr4 arr3x4[3]; // arr3x4 是 3 个 arr4 的数组
int sum2 (arr3x4 ar, int rows); // 与下一声明相同
int sum2 (int ar[3][4], int rows); // 与下一声明相同
int sum2 (int ar[][4], int rows); // 标准形式
一般地,声明 N 难数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。
int sum4d (int ar[][12][20][3], int rows);
这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型。请参看下面的等效原型表示:
int sum4d (int (*ar)[12][20][30], int rows); // ar 是一个指针
此处 ar 指向一个 12x20x30 的 int 数组。
10.8 变长数组 (VLA)
处理二维数组的函数有一处可能不太容易理解:数组的行可以在函数调用时传递,但是数组的列却只能被预置在函数内部。例如下面这样的定义:
#define COLS 4
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;
}
现在,假设定义了如下的数组:
int array1[5][4];
int array2[100][4];
int array3[2][4];
可以使用下面的函数调用:
tot = sum2d (array1,5); // 对一个 5x4 的数组求和
tot = sum2d (array2,100); // 对一个 100x4 的数组求和
tot = sum2d (array3,2); // 对一个 2x4 的数组求和
这是因为行数可以传递给参量 rows,而 rows 是一个变量。但是如果要处理 6 行 5 列的数组,则需要创建另一个新的函数,其 COLS 定义为 5。 这是由于数组的维数必须是常量;因此不能用一个变量来代替 COLS 。
创建一个处理任意二维数组的函数,也是有可能的,但是比较繁琐(因为这样的函数需要把数组当作一维数组传递,然后由函数计算每行的起始地址)。而且,这种技巧和 FORTRAN 语言子程序不太一致, FORTRAN 语言允许在函数调用中指定二维的大小。虽然 FORTRAN 是很古老的编程语言,但多年以来,数值计算专家们研究出了很多有用的 FORTRAN 计算库。C 正逐渐代替 FORTRAN,因此如果能够简单地转换现有 FORTRAN 库将是很有益处的。
/* arr[0][0] row * col; 这里好像就能处理任意二维数组了吧 */
出于上面的原因,C99 标准引入了变长数组,它允许使用变量定义数组各维。例如你可以使用下面的声明:
int quarters = 4;
int regions = 5;
double sales [regions][quarters]; // 一个变长数组 (VLB)
正如前面提到的,变长数组有一些限制。变长数组必须是自动存储类的,这意味着它们必须在函数内部或作为函数参量声明,而且声明时不可以进行初始化。
VC6.0不支持
------------------------------------------------------------------------
PS: 变长数组的大小不会变化
变长数组中的“变”并不表示在创建数组后,你可修改其大小。变长数组的大小在创建后就是保持不变以。“变”的意思是说其维大小可以用变量来指定。
---------------------------------------------------------------------
因为变长数组是新增的特性,所以目前支持它的并不多。让我们来看一个简单的例子,该例阐明了如何编写一个计算任意二维 int 数组的和的函数。
首先,下面的代码示范了如何声明带有一个二维变长数组参数的函数:
int sum2d (int rows, int cols, int ar[rows][cols]);
// ar 是一个变长数组(VLA)
请注意前两个参量 (rows 和 cols)用作数组参量 ar 的维数。因为 ar 的声明中使用了 rows 和 cols ,所以在参量列表中,它们两个的声明需要早于 ar。因此,下面的原型是错误的:
int sum2d (int ar[rows][cols], int rows, int cols); // 顺序不正确
C99 标准规定,可以省略函数原型中的名称;但是如果省略名称,则需要用星号来代替省略的维数:
int sum2d (int,int, int ar[*][*]); //ar 是一个变长数组(VLA),其中省略了维数参量的名称
第二,函数的定义如下:
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;
}
除了新的函数头之外,这个函数区别于古典 C (请参见程序清单 10.17)的地方就是用变量 cols 代替常量 COLS 。这是因为在函数头中使用了变长数组。由于使用了代表行数和列数的两个变量,使得我们能够使用这个新的 sum2d()函数处理任意的二维 int 数组。从程序清单 10.18 中可以看出来这点,但是,前提是编译器必须能够支持变长数组这个新特性。该程序也说明基于变长数组的函数即可以处理古典 C 数组也可以处理变长数组。
程序清单 10.18 vararr2d.c 程序
------------------------------------------------------------------------------
/* 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]; // 变长数组
for (i = 0; i < rs; i++)
for (j = 0; j < cs; c++)
varr[i][j] = i * j + j;
printf (" 3 x 5 array \n");
printf (" Sum of all elements = %d \n",sum2d(ROWS,COLS,junk));
printf (" 2 X 6 array \n");
printf (" Sum of all elements = %d \n",sum2d(ROWS-1,COLS+2,morejunk));
printf (" 3 x 10 VLA \n");
printf (" Sum of all elements = %d \n",sum2d(rs,cs,varr));
}
// 带有一个 VLA 参数的函数
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;
}
输出结果如下:
3 x 5 array
Sum of all elements = 80
2 X 6 array
Sum of all elements = 315
3 x 10 VLA
Sum of all elements = 270
需要注意的一点是,函数定义参量列表中的变长数组声明实际上并没有创建数组。和老语法一样,变长数组名实际上是指针,也就是说具有变长数组参量的函数实际上直接使用原数组,因此它有能力修改做为参数传递进来的数组。下面程序段中指出了指针是何时声明的,以及实际数组是何时声明的。
int thing[10][6];
twoset (10,6,thile);
.....
}
void twosete (int n, int m, int ar[n][m]) // ar 是一个指针,它指向由 m 个 int 组成的数组
{
int temp[n][m]; // temp 是一个 nxm 的 int 数组
temp[0][0] = 2; // 把 temp 的一个元素设置为 2
ar[0][0] = 2; // 把 thing[0][0] 设置为 2
}
如程序所示,当调用 twoset()时,ar 成为指向 thing[0] 的指针,并创建 10x6 的数组 temp。由于 ar 和 thing 都是指向 thing[0] 的指针,因此 ar[0][0] 和 thing[0][0] 也是同一个数据。
变长数组允许动态分配存储单元。这表示可以在程序运行时指定数组的大小。常规的 C 数组是静态存储分配的,也就是说数组大小在编译时已经确定。这是因为数组大小是常量,所以编译器可以得到这些信息。第 12 章将详细介绍动态存储单元分配。
10.9 复合文字
假设需要向带有一个 int 参量的函数传递一个值,你可以传递一个 int 变量,也可以传递一个 int 常量,比如 5 ,在 C99 标准出现之前,数组参数的情况是不同的:可以传递数组,但没有所谓的数组常量可供传递。 C99 新增了复合文字(compound literal)。文字是非符号常量。例如:5 是 int类型的文字, 81.3 是 double 类型的文字,‘Y’是 char 类型的文字,"elephant" 是字符串文字。开发 C99 标准的委员会认为,如果有能够表示数组和结构的内容的复合文字,那么在编写程序时将更为方便。
对于数组来说,复合文字看起来像是在数组的初始化列表前面加上用圆括号括起来的类型名。例如,下面是普通数组的声明方法:
int diva[2] = {10,20};
下面是复合文字,创建了一个包含两个 int 值的无名称数组:
(int [2]) {10,20} // 复合文字
注意:类型名就是前面声明中去掉 diva 后剩余的部分,即 int[2]。
正如初始化一个命名数组时可以省略数组大小一样,初始化一个复合文字时也可以省略数组大小,编译器会自动计算元素的数目:
(int []) {50,20,90) // 有 3 个元素的复合文字
由于这些复合文字没有名称,因此不可能在一个语句中创建它们,然后在另一个语句中使用。而是必须在创建它们的同时通过某种方法来使用它们,一种方法是使用指针保存其位置。请参看下面的例子:
int *pt1;
pt1 = (int[2]){10,20};
请注意这个文字常量被标识为一个 int 数组。与数组名相同,这个常量同时代表首元素的地址,因此可以用它给一个指向 int 的指针赋值。随后就可以使用这个指针。例如,本例中 *pt1 是 10, pt1[1] 是 20.
另外,复合文字也可以做为实际参数被传递给带有类型与之匹配的形式参量的函数:
int sum (int ar[], int n);
...
int total3;
total = sum ((int[] {4,4,4,5,5,5},6);
上面的例子中,第一个参数是包含 6 个元素的 int 数组,同时也是首元素地址(同数组名一样)。这种给函数传递信息而不必先创建数组的做法,是复合常量的通常使用方法。
可以把这种技巧用在处理二维数组或多维数组的函数中。例如,下面的代码介绍如何创建一个二维 int 数组并保存在其地址:
int (*pt2)[4]; // 声明一个指向包含 4 个 int 数组的数组的指针
pt2 = (int [2][4]){1,2,3,-9}, {4,5,6,-8};
其中复合文字的类型是 int[2][4],即一个 2x4 的 int 数组。
程序清单 10.19 把上面这些例子包含到一个完整的程序内。
程序清单 10.19 flc.c 程序
-----------------------------------------------------------------------------
// flc.c --- 有趣的常量
#include <stdio.h>
#include <stdlib.h>
#define COLS 4
int sum2d (int ar[][COLS], int rows);
int sum (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 = sum(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);
system("PAUSE");
return 0;
}
int sum (int ar[], int n)
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total +=ar[i];
return total;
}
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;
}
这个示例需要支持 C99 的编译器
结果如下
total1 = 30
total2 = 4
total3 = 27
10.10 关键概念
当需要存储同种类型的许多元素时,数组是最佳的选择。C 把数组归于派生类型是因为它是建立在其他类型之上的。也就是说,你不是仅仅声明了一个数组,而是声明了一个 int 数组,float 数组或者其他类型的数组。所谓的其他类型本身就可以是一种数组类型,在这种情况下,可以得到数组的数组,或称为二维数组。
编写处理数组的函数常常是有利的,因为使特定的函数执行特定的功能有助于程序的模块化。使用数组名做为实际参数时,主要的一点是要知道并不是把整个数组传递给函数,而是传递它的地址;因此相应的形式参量是一个指针。处理数组时,函数必须知道数组的地址和元素的个数。数组地址直接传递给函数,而数组元素的个数信息需要内建于函数内部或被做为独立的参数传递给函数。后者更为通用,因为这种方法可以处理不同大小的数组。
数组和指针之间是紧密联系的,指针符号和数组的运算往往可以互换使用。正是由于这种紧密的联系,才允许处理数组的函数使用指针(而不是数组)作为形式参量,同时在函数中使用数组符号处理数组。
必须用一个常量表达式为传统的 C 数组指定数组的大小,因此在编译时数组大小已经确定。C99 标准提供了变长数组,这种数组的大小可以是一个变量。这就允许变长数组的大小可以在运行时才确定。
10.11 总结
数组是由同一种数据类型的元素系列构成的。数组元素按顺序存储于内存中,通过使用整数索引(或偏移量)来访问。在 C 中,首元素的索引值为 0 ,因此包含 n 个元素的数组的末元素索引为 n - 1 。程序员要能够正确地使用数组索引,因为编译器和程序运行时都不检查索引是否合法。
要声明一个简单的一维数组,可以采用下面的形式:
type name [size];
此处, type 是数组内每个元素的数据类型,name 是数组名, size 是元素的个数。传统上, C 要求 size 是一个常量整数表达式。而 C99 标准则允许使用非常量整数表达式,这种情况下,数组是变长数组。
C 把数组名解释为该数组首元素的地址。也就是说,数组名和指向首元素的指针是等价的。通常,数组和指针是紧密联系的。如果 ar 是数组,那么表达式 ar[i] 和 *(ar+i)是等价的。
C 不支持把整个数组作为函数参数进行传递,但是可以传递数组的地址。然后函数可以利用该地址来处理原始数组。如果函数功能不需要修改原始数组,那么在声明相应的形式参量,需要加上关键字 const 。 在被调函数中,你可以使用数组符号或指针符号。无论哪种形式,实际上使用的都是指针变量。
对指针加上一个整数或进行增量运算时,指针值的改变都是以所指向对象的字节大小为单位的。也就是说,如果 pd 指向数组内的一个 8 字节长的 double 数值,则对 pd 加 1 就相当于对它的值加上数值 8 。 这样,该指针会指向数组的下一个元素。
二维数组表示数组的数组。例如:
double sales[5][12]:
这个声明创建了包含 5 个元素的数组 sales,每个元素包含 12 double 数。这些一维数组的第 1 个可以写作 sales[0],第 2 个可以写作 sales[1],等等。每个都是包含 12 个 double 数的数组。
使用第二个索引可以访问这些数组的每一个元素。例如,sales[2][5]是 sales[2]的第 6 个元素,
sales[2] 是 sales 的第 3 个元素。
传统的 C 向函数传递多维数组的方法是把数组名(也就是地址)传递给相应类型的指针参量。指针的声明需要指定各维的大小(除了最左面的不需要明确指出大小);第一个参量的大小通常做为第二个参数来传递。例如,要处理前面提到的数组 sales , 函数原型和函数调用应该如下:
void display (double ar[][12], int rows);
...
display(sales,5);
变长数组则提供了另一种方法,两个维大小都可以做为参数被传递给函数。因此,函数原型和函数调用就可以如下这样写:
void display (int rows, int cols, double ar[rows][cols]);
...
display (5,12,sales);
本例使用了 int 数组和 double 数组,对于其他类型的数组,结论也都适用。然而,字符串有很多特殊的规则。这是由它的终止 null 字符决定的。有了这个终止字符,无须向函数传递字符串大小,函数就能够检测字符串的结束。在第 11 章“字符串和字符串函数”中我们将详细介绍字符串的特性。
10.12 复习题
----------------------------------------------------------------------------
1. 下面的程序将打印出什么 ?
#include "stdafx.h"
#include "stdlib.h"
int main (void)
{
int ref[] = {8,4,0,2};
int *ptr;
int index;
for (index = 0, ptr = ref; index < 4; index++,ptr++)
printf ("%d %d \n", ref[index],*ptr);
system("PAUSE");
return 0;
}
答:
8 8
4 4
0 0
2 2
-----------------------------------------------------------
2. 在第 1 题中,数组 ref 包含多少元素?
答: 数组 ref 有 4 个元素,因为在初始化列表中值的个数为 4 。
------------------------------------------------------------
3. 在第 1 题中,ref 是 哪些数据的地址?ref+1呢? ++ref 指向什么?
答: 数组名 ref 指向数组中的第一个元素(即首元素 整数 8)
表达式 ref+1 指向第二个元素 (即整数 4)
++ref 不是合法的 C 表达式,因为 ref 是常量而不是变量。
---------------------------------------------------------------
4. 下面每种情况中 *ptr 和 *(ptr+2) 的值分别是什么?
a.
int *ptr;
int torf[2][2] = {12,14,16};
ptr = torf[0];
答: 12 和 16 。 ptr 指向第一个元素,ptr+2 指向第三个元素,它是第二行的第一个元素。
b.
int *ptr;
int fotr[2][2] = { {12},{14,16} };
ptr = fort[0];
答: 12 和 14 (因为有花括号,所以只有 12 在第一行中)
-----------------------------------------------------------------------
5. 下面每种情况中 **ptr 和 **(ptr+1) 的值分别是什么?
a. int (*ptr)[2];
int torf[2][2] = {12,14,16);
ptr = torf;
答: ptr 指向第一行,ptr+1 指向第二行,*ptr 指向第一行中的第一个元素,而 *(ptr+1)指向第二行中的第一个元素。
12 和 16 注: 指针的指针指向的值和指针是一样的?
b.
int (*ptr)[2];
int fort[2][2] = { {12},{14,16} };
ptr = fotr;
答:12 和 14 ( 因为有花括号,所以只有 12 在第一行中)。
-------------------------------------------------------------------------------
6. 假设有如下定义:
int grid [30][10];
a. 用 1 种方法表示 grid[22][56] 的地址
b. 用 2 种方法表示 gtid[22][0] 的地址
c. 用 3 种方法表示 gtid[0][0] 的地址
答:
a. &grid[22][56];
b >id[22][0] 或 gtid[22]
后者是含有 10 个元素的一维数组名,所以它就是第一个元素,即元素 grid[22][0]的地址)
c. >id[0][0] 或 gtid[0] 或 (int *)gtid
这里 grid[0]是整数元素 grid[0][0]的地址,grid 是具有 10个元素的数组 grid[0]的地址。这两 个地址具有相同的数值但是类型不同,类型指派可以使它们的类型相同)。
-------------------------------------------------------------------------------
7. 用适当的方法声明下面每个变量:
a. digits : 一个包含 10 个 int 值的数组
b. rates : 一个包含 6 个 float 值的数组
c. mat : 一个包含 3 个元素的数组,其中每个元素是一个包含 5 个整数的数组
d. psa : 一个包含 20 个指向 char 的指针的数组
e. pstr: 一个指向数组的指针,其中数组由 20 个 char 值构成
答
a. int digits[10];
b. float rates[6];
c. int mat[3][5];
d. char *ptr[20];
/* 注意 []的优先级比 * 高,所以没有圆括号时首先应用数组描述符,然后才是指针描述符。因此这个声明与 char *(pas[20])相同。 */
e. char (*ptr)[20];
/* 和上题相反,应先定义指针再到数组,因为 []符优先级比 * 高 所以要圆括号。*/
/* char *pstr[20] 是不正确的,这会使 pstr 成为指针数组布不是指向数组的指针。具体地,pstr会指向一个单个 char (数组的第一个元素);pstr+1 会指向下一个字节。使用正确的声明, pstr就是一个变量而不是一个数组名,pstr+1 就指向起始字节后的第 20 个字节。*/
注: 定义时要分辨是指针的数组,还是数组的指针, 这样就明白了优先级应该那个在前
指针的数组 int *ptr[20];
数组的指针 int (*pte)[20];
--------------------------------------------------------------------------------------
8.
a. 定义一个包含 6 个 int 值的数组,并且用数值 1,2,4,8,16 和 32 进行初始化
b. 用数组符号表示 a 部分中数组的第3个元素(数值为 4 那个元素)
c. 假设系统支持 c99 规则,定义一个包含 100 个 int 值的数组并且初始化它,使它的末元素为 -1 ,其他元素的值不考虑。
答
a. int arr[6] = {1,2,4,8,16,32};
b. arr[2];
c. int arr[100] = { [99] = -1};
-----------------------------------------------------------------------------------
9. 包含 10 个元素的数组的索引是范围是什么?
从 0 到 9 。因为数组中的元素是从 0 开始计数的
---------------------------------------------------------------------------------
10. 假设有如下声明:
float rootbeer[10], things[10][5], *pf, value = 2.2;
int i = 3;
则下列语句中哪些是正确的,哪些是错误的?
a. rootbeer[2] = value;
/* 合法的 */
b. scanf ("%f",&rootheer);
/* 不合法, rootheer 是一个数组, 而不是一个 float 类型的变量 */
c. rootbeer = value;
/* 不合法, rootheer 是一个数组,而不是一个 float 类型的变量 */
d. printf ("%f", rootherr);
/* 不合法,rootherr 是一个数组,而不是一个 float 类型的变量 */
e. things[4][4] = rootbeer[3];
/* 合法 */
f. things[5] = rootberr;
/* 不合法 不能使用数组赋值 */
g. pf = value;
/* 不合法 pf value 是值而不是地址 应该是 pf = &value */
h pf = roothberr;
/* 合法 pf 指向数组 roothberr 的首地址 */
-----------------------------------------------------------------------
11. 声明一个 800 X 600 的 int 数组
int arr[800][600];
-------------------------------------------------------------------------
12. 以下是 3 个数组声明:
double trots[20];
short clops[10][30];
long shots[5][10][15];
a. 以传统的 void 函数方式,写出处理数组 trots 的函数原型和函数调用;然后以变长数组方式,写出处理数组 trots 的函数原型和函数调用。
void arrtrots (double ar[], int n);
arrtrots(trots,20);
void arrtrots (int n, double ar[n]);
arrtrots(20,trots);
b. 以传统的 void 函数方式,写出处理数组 clops 的函数原型和函数调用;然后以变长数组方式,写出处理数组 clops 的函数原型和函数调用。
void arrclops (short ar[30], int n);
arrclops(clops,10);
void arrclops (int i, int n, short ar[i][n]);
arrcops(10,30,clops);
c. 以传统 void 函数方式,写出处理数组 shots 的函数原型和函数调用;然后以变长数组方式,写出处理数组 shots 的函数原型和函数调用。
void arrshots (long ar[10][15], int c);
arrshots(shots,5);
void arrshots(int i, int n, int c, long ar[i][n][c]);
arrshots(5,10,15,shots);
--------------------------------------------------------------------------------
13. 下面是两个函数原型:
void show (double ar[], int n); // n 是元素数
woid show2 (double ar2[][3], int n); // n 是行数
a. 编写一个函数调用,把包含 8,3,9 和 2 的复合文字传递给函数 show()。
show((int[4]){8,3,9,2},4);
b. 编写一个函数调用,把包含 2 行 3 列数值的复合文字传递给函数 show2(),其中第一行为
8,3,9; 第二行为 5,4,1.
show2((int[][3]){8,3,9},{5,4,1},2);
-----------------------------------------------------------------------------
10.13 编程练习
1. 修改程序清单 10.7 中的程序 rain,使它不使用数组下标,而是使用指针进行计算(程序仍然需要声明初始化数组)。
解:
// etse.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "stdlib.h"
#define MONGHS 12
#define YEARS 5
int main (void)
{
const float rain [YEARS][MONGHS] ={
{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 < MONGHS; month++)
subtot += *(*(rain+year)+month); // 关键处
total += subtot;
}
printf ("\nThe yearly average is %.1f inches \n\n",total/YEARS);
printf ("MONTHLY ACERAGES; \n\n");
printf (" Jan Feb Mar Apr May Jun Jul Aug Sep Oct"); //为了显示每个月份
printf (" Nov Dec \n");
for (month = 0; month < MONGHS; month++)
{
for (year = 0, subtot = 0; year < YEARS; year++)
subtot += *(*(rain+year)+month); // 关键处
printf ("%4.1f ",subtot/YEARS);
}
printf ("\n");
system("PAUSE");
return 0;
}
------------------------------------------------------------------------------------------
2 . 编写一个程序,初始化一个 double 数组,然后把数组内容复制到另外两个数组(3 个数组都需要在主程序中声明)。制作第一份拷贝的函数使用数组符号。制作第二份拷贝的函数使用指针符号,并使用指针增量操作。把目标数组名和要复制的元素数目做为参数传递给函数。也就是说,如果下列声明,函数调用应该如下面所示:
解:
#include "stdafx.h"
#include "stdlib.h"
void copy_arr (double ar[], double arr[], int i); // 使用数组符号
void copy_ptr (double *ptr, double ar[], int i); // 使用指针符号
int main (void)
{
double source[5] = {1.1,2.2,3.3,4.4,5.5};
double target1[5];
double target2[5];
int n;
copy_arr (source,target1,5);
copy_ptr (source,target2,5);
printf ("target1 arr =");
for (n = 0; n < 5; n++)
printf ("%4.2f ",target1[n]);
putchar('\n');
printf ("target2 arr =");
for (n = 0; n < 5; n++)
printf ("%4.2f",target2[n]);
system("PAUSE");
return 0;
}
void copy_arr(double ar[], double arr[], int i)
{
int n;
for (n = 0; n < i; n++)
arr[n] = ar [n];
}
void copy_ptr(double *ptr, double ar[], int i)
{
int n;
for (n = 0; n < i; n++)
ar[n] = *ptr++;
}
-----------------------------------------------------------------------------
3. 编写一个函数,返回一个 int 数组中存储最大数值,并在一个简单的程序中测试这个函数。
解:
#include "stdafx.h"
#include "stdlib.h"
#define SIZE 10
int maxint (int ar[], int i);
int main (void)
{
int i,big;
int max[SIZE];
printf ("Please input 10 numbers : ");
for (i = 0; i < SIZE; i++)
scanf ("%d",&max[i]);
big = maxint(max,SIZE);
printf (" MAX numbers is %d" ,big);
system("PAUSE");
return 0;
}
int maxint(int ar[], int i)
{
int big = ar[0];
int n;
for (n = 0; n < SIZE; n++)
{
if (big < ar[n])
big = ar[n];
}
return big;
}
------------------------------------------------------------------------------------------
4. 编写一个函数,返回一个 double 数组中存储的最大数值的索引,并在一个简单程序中测试这个函数。
解:
#include "stdafx.h"
#include "stdlib.h"
#define SIZE 10
int maxint (double ar[], int n);
int main (void)
{
int i,big;
double max[SIZE];
printf ("Please input 10 numbers : ");
for (i = 0; i < SIZE; i++)
scanf("%lf", &max[i]); // 一定要注意用合适的数据类型的说明符
big = maxint (max,SIZE);
printf ("the max index in %d", big);
system("PAUSE");
return 0;
}
int maxint(double ar[], int n)
{
int i,big;
big = 0;
for (i = 0; i < SIZE; i++)
{
if (ar[big] < ar[i])
big = i;
}
return big;
}
注: 数据有偏差的时候,注意 分号 括号 和 是否用了合适的数据类型的说明符。
-------------------------------------------------------------------------------------
5. 编写一个函数,返回 double 数组中最大的和最小的数之间的差值,并在一个简单的程序是测试这个函数。
解:
#include "stdafx.h"
#define SIZE 10
double maxnum (double ar[], int n);
int main (void)
{
double max[SIZE];
int i;
double big;
printf ("Please input 10 numbers :");
for (i = 0; i < SIZE; i++)
scanf ("%lf",&max[i]);
big = maxnum(max,SIZE);
printf ("Max and Min is %lf \n",big);
return 0;
}
double maxnum(double ar[], int n)
{
int i;
double max = ar[0]; // 需要注意的是这里, max 实际上是指向 数组元素的指针
double min = ar[0]; // 如果这里表达为 min = 0; 那么将返回最大值而不是差值。
for (i = 0; i < SIZE; i++)
{
if(max < ar[i])
max = ar[i];
if (min > ar[i])
min = ar[i];
}
return max - min;
}
注: 本题要点实际上就是上面注释这里
----------------------------------------------------------------------------------
6. 编写一个程序,初始化一个二维 double 数组,并利用练习 2 的任一函数来把这个数组复制到另一个二维数组(因为二维数组是数组的数组,所以可以使用处理一维数组的函数来复制数组的每个子数组)
解:
#include "stdafx.h"
#define ROOW 2
#define LINE 5
void copy_arr(double ar[], double arr[], int i);
int main (void)
{
int i,n;
double arr1[ROOW][LINE] = { {1.1,2.2,3.3,4.4,5.5}, {0.1,0.2,0.3,0.4,0.5} };
double arr2[ROOW][LINE];
copy_arr(arr1[0],arr2[0],ROOW*LINE); // 关键是这句的用法
for (i = 0; i < ROOW; i++)
for (n = 0; n < LINE; n++)
printf ("arr2 is %4.2f \n",arr2[i][n]);
return 0;
}
void copy_arr (double ar[], double arr[], int i)
{
int n;
for (n = 0; n < i; n++)
arr[n] = ar[n];
}
注:
------------------------------------------------------------------------------
7. 利用练习 2 中的复制函数,把一个包含 7 个元素的数组内第 3 到 第 5 元素复制到一个包含 3 个元素的数组中。函数本身不需要修改,只需要选择合适的实际参数(实际参数不需要是数组名和数组大小,而只须是数组元素的地址和需要复制的元素数目。
解:
#include "stdafx.h"
#define MIN 3
#define MAX 7
void copy_arr (int arr[], int ar[], int i);
void show_arr(const int ar[], int n);
int main (void)
{
int arr1[7] = {1,2,3,4,5,6,7};
int arr2[3];
copy_arr(arr2,arr1+2, MIN);
show_arr(arr2,3);
return 0;
}
void copy_arr (int arr[], int ar[], int i)
{
int n;
for (n = 0; n < i; n++)
arr[n] = ar[n];
}
void show_arr(const int ar[], int n)
{
int i;
for (i = 0; i < n; i++)
printf("%d ", ar[i]);
putchar('\n');
}
------------------------------------------------------------------------------
8. 编写一个程序,初始化一个 3X5 的二维 double 数组,并利用一个基于变长数组的函数把该数组复制到另一个二维数组。还要编写一个基于变长数组的函数来显示两个数组的内容。这两个函数应该能够处理任意的 NxM 数组(如果没有可以支持变长数组的编译器,就使用传统 C 中处理 Nx5 数组的函数方法)。
解:
/* 传统 C 处理 N x5 数组的函数 处理方法 */
#include "stdafx.h"
#define ROOW 3
#define LINE 5
void copy_arr (double *pt, double *ptr, int i);
void show_arr (const double *ptr, int i);
int main (void)
{
double arr1[ROOW][LINE] = { {1.1,1.2,1.3,1.4,1.5},{2.1,2.2,2.3,2.4,2.5},{3.1,3.2,3.3,3.4,3.5} };
double arr2[ROOW][LINE];
show_arr(arr1[0],ROOW*LINE);
copy_arr(arr1[0],arr2[0],ROOW*LINE);
show_arr(arr2[0],ROOW*LINE);
return 0;
}
void show_arr (const double *ptr, int i)
{
int n;
printf ("The arr is : \n");
for (n = 0; n < i; n++)
printf("%4.2lf ", *(ptr+n));
putchar('\n');
}
void copy_arr (double *pt, double *ptr, int i)
{
int n;
for ( n = 0; n < i; n++)
*(ptr+n) = *(pt+n);
}
-----------------------------------------------------
/* 使用 变长数组的处理方法 */
#include <stdio.h>
#include <stdlib.h>
#define ROOW 3
#define LINE 5
void show_arr (int i, int n, double ar[i][n]);
void copy_arr (int i, int n, double ar[i][n], double ar1[i][n]);
int main (void)
{
int rs = ROOW;
int cs = LINE;
int i,n;
double arr1[rs][cs];
double arr2[ROOW][LINE] = { {1.1,1.2,1.3,1.4,1.5},{2.1,2.2,2.3,2.4,2.5},{3.1,3.2,3.3,3.4,3.5} };
show_arr(ROOW,LINE,arr2);
copy_arr(ROOW,LINE,arr1,arr2);
show_arr(ROOW,LINE,arr1);
system("PAUSE");
return 0;
}
void show_arr (int i, int n, double ar[i][n])
{
int c,j;
printf ("arr is : \n");
for ( c = 0; c < i; c++)
for ( j = 0; j < n; j++)
printf ("%1.2lf ", ar[c][j]);
putchar('\n');
}
void copy_arr (int i, int n, double ar[i][n], double ar1[i][n])
{
int c,j;
for ( c = 0; c < i; c++)
for (j = 0; j < n; j++)
ar[c][j] = ar1[c][j];
}
注: 奇怪 如果函数用 **ptr 来代替 ar[][] 的话 编译正常 但运行程序出错
-----------------------------------------------------------------------------------------
9. 编写一个程序,把两个彞内的相应元素相加,结果存储到第 3 个数组内。也就是说,如果数组 1 具有值 2,4,5,8 ,数组2 具有 1,0,4,6 ,则函数对数组 3 赋值为 3,4,9,14。 函数参数包括 3 个数组名和数组大小。并在一个简单的程序中测试这个函数。
解:
#include "stdafx.h"
#define SIZE 4
void show_arr (int ar[], int n);
void copy_arr (int ar[], int ar1[], int sum[], int n);
int main (void)
{
int arr[SIZE] = { 2,4,6,8};
int arr1[SIZE]= { 1,0,4,6};
int sumarr[SIZE];
show_arr(arr, SIZE);
show_arr(arr1, SIZE);
copy_arr (arr, arr1, sumarr, SIZE);
show_arr(sumarr, SIZE);
return 0;
}
void show_arr (int ar[], int n)
{
int i;
printf(" array is : \n");
for (i = 0; i < n; i++)
printf ("%4d",ar[i]);
putchar('\n');
}
void copy_arr( int ar[], int ar1[], int sum[], int n)
{
int i;
printf (" copy in ... \n");
for ( i = 0; i < n; i++)
sum[i] = ar[i] + ar1[i];
}
注: 这个题是正规的用法,没什么值的注意的,但不知道 copy 函数的参数能不能再少些 但题目要求也是用 四个参数
--------------------------------------------------------------------------------------
10. 编写一个程序,声明一个 3 x 5 的数组并初始化,具体数值可以随意。程序打印出数值,然后数值翻一番,接着再次打印出新值。编写一个函数来显示数组的内容,再编写另一个函数执行翻倍的功能。数组名和数组行数作为参数由程序传递给函数。 roow
解:
#include "stdafx.h"
#define ROOW 3
#define LINE 5
void show_arr (int ar[][LINE], int n);
void double_arr (int ar[][LINE],int n);
int main (void)
{
int arr[ROOW][LINE] = {{1,2,3,4,5}, {11,22,33,44,55,}, {111,222,333,444,555}};
show_arr(arr,ROOW);
double_arr(arr,ROOW);
show_arr(arr,ROOW);
return 0;
}
void show_arr (int ar[][LINE], int n)
{
int row,col;
printf( " array is : \n");
for (row = 0; row < n; row++)
for (col = 0; col < LINE; col++)
printf ("%4d ",ar[row][col]);
putchar('\n');
}
void double_arr(int ar[][LINE],int n)
{
int row,col;
for (row = 0; row < n; row++)
for (col = 0; col < LINE; col++)
ar[row][col] = 2 * ar[row][col];
}
注: 也是正规用法的一道题目
-------------------------------------------------------------------------
11. 重写程序清单 10.7 的程序 rain, main()中的主要功能改为由函数执行。
解:
#include "stdafx.h"
#define MONTHS 12
#define YEARS 5
float years (int y, int m, const float ar[YEARS][MONTHS]);
void months (int y, int m, const float ar[YEARS][MONTHS]);
int main (void)
{
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},
};
float total;
printf(" YEAR RAINFALL (inches) \n");
total = years (YEARS,MONTHS,rain); // 调用函数计算每年的降水量
printf ("\nThe yearly average is %.1f \n\n", total / YEARS); // 显示每年的平均降水量
printf ("MOHTHLY ACERAGES \n\n"); // 总年数的月降水量计算
printf (" Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\n\n ");
months (YEARS,MONTHS,rain);
putchar('\n');
}
float years (int y, int m, const float ar[YEARS][MONTHS])
{
float total,subtot;
int yi,mi;
for (yi = 0, total = 0; yi < y; yi++)
{
for (mi = 0, subtot = 0; mi < m; mi++)
subtot += ar[yi][mi];
printf ("%5d %15.1f \n" ,2000+yi,subtot);
total += subtot;
}
return total;
}
void months(int y, int m, const float ar[YEARS][MONTHS])
{
int yi,mi;
float total,subtot;
for (mi = 0; mi < m; mi++)
{
for (yi = 0, subtot = 0; yi < y; yi++)
subtot += ar[yi][mi];
printf ("%5.1f",subtot / YEARS);
}
}
注: 对算法还是有点迷糊,因为不是自己做的题目,只是修改程序清单的。
--------------------------------------------------------------------------------
12. 编写一个程序,提示用户输入 3 个数集,每个数集包括 5 个 double 值。程序应当实现下列功能:
a。 把输入信息存储到一个 3 x 5 的数组中
b。 计算出每个数集(包含 5 个数值)的平均值
c。 计算所有数组的平均数
d。 找出这 15 个数中的最大值
e。 打印出结果
每个任务需要用一个单独的函数来实现(使用传统 C 处理数组的方法)。对于任务 b ,需要编写计算并返回一维数组平均值的函数,循环 3 次 调用该函数来实现任务 b 。对于其他任务,函数应当把整个数组做为参数,并且完成任务 c 和 d 的函数应该向它的调用函数返回答案。
解:
#include "stdafx.h"
#define ROWS 3
#define COLS 5
void store (double ar[], int n);
double averag_row (double ar[], int ci);
double arverag_arr (double ar[ROWS][COLS]);
double maxarr (double ar[ROWS][COLS]);
void show_arr (double ar[][COLS], int ri);
int main (void)
{
double arr[ROWS][COLS];
int row;
/* 输入存储部分 */
for (row = 0; row < ROWS; row++)
{
printf ("Enter %d number for row %d \n",COLS,row+1);
store (arr[row],COLS);
}
show_arr(arr, ROWS); // 显示全部数组目录
/* 计算每个数集的平均值 */
for (row = 0; row < ROWS; row++)
printf("average value of row : %d = %g \n",row+1, averag_row(arr[row],COLS));
/* averag_row(arr[row],COLS) 作为 printf 的参数后,可以直接显示返回值 */
printf ("average value of all row : %g \n", arverag_arr(arr)); // 数组的平均值
printf("average max is : %g \n",maxarr(arr)); // 数值中的最大值
// 不用打印结果了。。。。
}
void store (double ar[], int n) // 存储函数
{
int i;
for (i = 0; i < n; i++)
{
printf ("Enter value #%d : ",i+1);
scanf ("%lf", &ar[i]);
}
}
void show_arr (double ar[][COLS], int ri) //显示数组
{
int r,c;
printf ("The array is : \n");
for (r = 0; r < ri; r++)
{
for (c = 0; c < COLS; c++)
printf ("%9.2f",ar[r][c]);
putchar('\n');
}
}
double averag_row(double ar[], int ci) // 数组 row 的平均值
{
int i;
double sum = 0.0;
for (i = 0; i < ci; i++)
sum += ar[i];
if (ci > 0)
return sum / ci;
else
return 0.0;
}
double arverag_arr(double ar[ROWS][COLS]) // 数组的平均值
{
int ri,ci;
double sum = 0.0;
for (ri = 0; ri < ROWS; ri++)
for (ci = 0; ci < COLS; ci++)
sum += ar[ri][ci];
if (sum > 0)
return sum / (ri * ci);
else
return 0.0;
}
double maxarr (double ar[ROWS][COLS]) // 数组中的最大值
{
int ri,ci;
double max = 0;
for (ri = 0; ri < ROWS; ri++)
for (ci = 0; ci < COLS; ci++)
{
if (max < ar[ri][ci])
max = ar[ri][ci];
}
if (max > 0)
return max;
else
return 0.0;
}
注: 对函数的参数 还是迷糊。。主要是不知道用几个参数和参数的形式是最适合的。
---------------------------------------------------------------------------------
13. 利用变长数组做为函数参量重做练习 12 。
解:
#include <stdio.h>
#include <stdlib.h>
#define ROWS 3
#define COLS 5
void store (double ar[], int n);
double averag_row (double ar[], int n);
double averag_arr (int rows, int cols, double ar[rows][cols]);
double maxarr (int rows, int cols, double ar[rows][cols]);
void show_arr (int rows, int cols, double ar[rows][cols]);
int main (void)
{
double arr[ROWS][COLS];
int row;
// 输入存储部分
for (row = 0; row < ROWS; row++)
{
printf ("Enter %d number for row %d \n",COLS,row+1);
store(arr[row],COLS);
}
show_arr(ROWS,COLS,arr); // 显示数组的全部元素
for (row = 0; row < ROWS; row++) // 计算每行数的平均值
printf ("average value of row %d : %g \n",row+1,averag_row(arr[row],COLS));
printf ("average value all of : %g \n",averag_arr(ROWS,COLS,arr)); // 数组的平均值
printf ("average max value is : %g \n",maxarr(ROWS,COLS,arr)); //数组元素的最大值
system("PAUSE");
return 0;
}
void store (double ar[], int n)
{
int row;
for (row = 0; row < n; row++)
{
printf ("Enter value #%d : ",row+1);
scanf ("%lf", &ar[row]);
}
}
double averag_row (double ar[], int n)
{
int col;
double sum = 0.0;
for (col = 0; col < n; col++)
sum += ar[col];
if (sum > 0)
return sum / col;
else
return 0.0;
}
double averag_arr (int rows, int cols, double ar[rows][cols])
{
int row,col;
double sum = 0.0;
for (row = 0; row < rows; row++)
for (col = 0; col < cols; col++)
sum += ar[row][col];
return sum / (rows * cols);
}
double maxarr (int rows, int cols, double ar[rows][cols])
{
int row,col;
double max = ar[0][0];
for (row = 0; row < rows; row++)
for (col = 0; col < cols; col++)
if (max < ar[row][col])
max = ar[row][col];
return max;
}
void show_arr (int rows, int cols, double ar[rows][cols])
{
int row,col;
printf ("array contents : \n");
for (row = 0; row < rows; row++)
{
for (col = 0; col < cols; col++)
printf ("%9.2f ", ar[row][col]);
putchar('\n');
}
putchar('\n');
}
注: 对数组还是糊糊的。。。看来要靠多练习来熟悉了
--------------------------------------------------------------------------------------------