《C Primer Plus》第十章-数组和指针(深入讲解数组与指针的关系,复合字面量,指定初始化器)

本章不熟悉的知识较多,不再在此列举。

见加粗部分。几乎都是指针的知识。

数组和指针

本章内容

本章介绍以下内容:

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

数组

  • 数组由数据类型相同的一系列元素组成。
  • 需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。普通变量可以使用的类型,数组元素都可以用。
/* 一些数组声明*/
int main(void)
{
   float candy[365];   /* 内含365个float类型元素的数组 */
   char code[12];     /*内含12个char类型元素的数组*/
   int states[50];    /*内含50个int类型元素的数组 */
   ...
}
  • 要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。数组元素的编号从0开始,所以candy[0]表示candy数组的第1个元素,candy[364]表示第365个元素,也就是最后一个元素。

初始化数组

  • 只存储单个值的变量有时也称为标量变量
int main(void)
{
   int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初始化 */
   ...
}
  • 如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。
  • 不支持ANSI的编译器会把这种形式的初始化识别为语法错误,在数组声明前加上关键字static可解决此问题。第12章将详细讨论这个关键字
/* 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;
}
  • 注意该例使用了符号常量MONTHS表示数组大小,这是我们推荐且常用的做法。

  • 注意 使用const声明数组
    有时需要把数组设置为只读。这样,程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用const声明和初始化数组。因此,程序清单10.1中初始化数组应改成:

    const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

  • 这样修改后,程序在运行过程中就不能修改该数组中的内容。和普通变量一样,应该使用声明来初始化const数据,因为一旦声明为const,便不能再给它赋值。明确了这一点,就可以在后面的例子中使用const了。

/* 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 0

    1 4204937

    2 4219854

    3 2147348480

  • 使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值。编译器使用的值是内存相应位置上的现有值,因此,读者运行该程序后的输出会与该示例不同。

注意 存储类别警告

数组和其他变量类似,可以把数组创建成不同的存储类别(storage class)。第12章将介绍存储类别的相关内容,现在只需记住:本章描述的数组属于自动存储类别,意思是这些数组在函数内部声明,且声明时未使用关键字static。到目前为止,本书所用的变量和数组都是自动存储类别。在这里提到存储类别的原因是,不同的存储类别有不同的属性,所以不能把本章的内容推广到其他存储类别。对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。

/* 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。
  • 如果初始化列表的项数多于数组元素个数,编译器可没那么仁慈,它会毫不留情地将其视为错误。但是,没必要因此嘲笑编译器。其实,可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数
/* 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;
}
  • ·如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。
    ·注意for循环中的测试条件。由于人工计算容易出错,所以让计算机来计算数组的大小。sizeof运算符给出它的运算对象的大小(以字节为单位)。所以sizeof days是整个数组的大小(以字节为单位),sizeof days[0]是数组中一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。

指定初始化器(C99)

  • C99增加了一个新特性:指定初始化器
  • int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
// 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

  • 以上输出揭示了指定初始化器的两个重要特性。第一,如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段:[4] = 31,30,31,那么后面这些值将被用于初始化指定元素后面的元素。也就是说,在days[4]被初始化为31后,days[5]和days[6]将分别被初始化为30和31。第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。
  • 如果未指定元素大小会怎样?

int stuff[] = {1, [6] = 23}; //会发生什么?

int staff[] = {1, [6] = 4, 9, 10}; //会发生什么?

  • 编译器会把数组的大小设置为足够装得下初始化的值。所以,stuff数组有7个元素,编号为0~6;而staff数组的元素比stuff数组多两个(即有9个元素)。

给数组元素赋值

/* 给数组的元素赋值 */
#include <stdio.h>
#define SIZE 50
int main(void)
{
   int counter, evens[SIZE];
   for (counter = 0; counter < SIZE; counter++)
     evens[counter] = 2 * counter;
   ...
}
  • C不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。下面的代码段演示了一些错误的赋值形式:

/* 一些无效的数组赋值 */

#define SIZE 5

int main(void)

{

int oxen[SIZE] = {5,3,2,8}; /* 初始化没问题 */

int yaks[SIZE];

yaks = oxen; /* 不允许 */

yaks[SIZE] = oxen[SIZE]; /* 数组下标越界 */

yaks[SIZE] = {5,3,2,8}; /* 不起作用 */

  • oxen数组的最后一个元素是oxen[SIZE-1],所以oxen[SIZE]和yaks[SIZE]都超出了两个数组的末尾。

数组边界

  • int doofi[20]; 那么在使用该数组时,要确保程序中使用的数组下标在0~19的范围内,因为编译器不会检查出这种错误
// 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 1624678494

6 32767

value1 = 9, value2 = -1

address of arr[-1]: 0x7fff5fbff8cc

address of arr[4]: 0x7fff5fbff8e0

address of value1: 0x7fff5fbff8e0

address of value2: 0x7fff5fbff8cc

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

#define SIZE 4

int main(void)

{

int arr[SIZE];

for (i = 0; i < SIZE; i++)

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

指定数组的大小

int* n = 5;

int* m = 8;

float* a1[5]; // 可以

float* a2[5*2 + 1]; //可以

float* a3[sizeof(int) + 1]; //可以

float* a4[-4]; // 不可以,数组大小必须大于0

float* a5[0]; // 不可以,数组大小必须大于0

float* a6[2.5]; // 不可以,数组大小必须是整数

float* a7[(int)2.5]; // 可以,已被强制转换为整型常量

float* a8[n]; // C99之前不允许

float* a9[m]; // C99之前不允许

  • 上面的注释表明,以前支持C90标准的编译器不允许后两种声明方式。而C99标准允许这样声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称VLA(C11放弃了这一创新的举措,把VLA设定为可选,而不是语言必备的特性)。

多维数组

float rain[5][12]; // 内含5个数组元素的数组,每个数组元素内含12个float类型的元素

  • rain[0]是一个数组,那么它的首元素就是rain[0][0],第2个元素是rain[0][1],以此类推。简而言之,数组rain有5个元素,每个元素都是内含12个float类型元素的数组,rain[0]是内含12个float值的数组,rain[0][0]是一个float类型的值。假设要访问位于2行3列的值,则使用rain[1][2]。
/* rain.c -- 计算每年的总降水量、年平均降水量和5年中每月的平均降水量 */
#include <stdio.h>
#define MONTHS 12    // 一年的月份数
#define YEARS  5    // 年数
int main(void)
{
   // 用2010~2014年的降水量数据初始化数组
   const float rain[YEARS][MONTHS] =
   {
     { 4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6 },
     { 8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3 },
     { 9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4 },
     { 7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2 },
     { 7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2 }
   };
   int year, month;
   float subtot, total;
   printf(" YEAR  RAINFALL (inches)\n");
   for (year = 0, total = 0; year < YEARS; year++)
   {          // 每一年,各月的降水量总和
     for (month = 0, subtot = 0; month < MONTHS; month++)
        subtot += rain[year][month];
     printf("%5d %15.1f\n", 2010 + year, subtot);
     total += subtot;  // 5年的总降水量
   }
   printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
   printf("MONTHLY AVERAGES:\n\n");
   printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");
   printf(" Nov Dec\n");
   for (month = 0; month < MONTHS; month++)
   {          // 每个月,5年的总降水量
     for (year = 0, subtot = 0; year < YEARS; year++)
        subtot += rain[year][month];
     printf("%4.1f ", subtot / YEARS);
   }
   printf("\n");
   return 0;
}
  • 下面是该程序的输出:

YEAR RAINFALL (inches)

2010 32.4

2011 37.9

2012 49.8

2013 44.0

2014 32.9

The yearly average is 39.4 inches.

MONTHLY AVERAGES:

Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

7.3 7.3 4.9 3.0 2.3 0.6 1.2 0.3 0.5 1.7 3.6 6.7

  • 程序使用了两个嵌套for循环。第1个嵌套for循环的内层循环,在year不变的情况下,遍历month计算某年的总降水量;而外层循环,改变year的值,重复遍历month,计算5年的总降水量。这种嵌套循环结构常用于处理二维数组,一个循环处理数组的第1个下标,另一个循环处理数组的第2个下标:
  • 第2个嵌套for循环和第1个的结构相同,但是内层循环遍历year,外层循环遍历month。记住,每执行一次外层循环,就完整遍历一次内层循环。因此,在改变月份之前,先遍历完年,得到某月5年间的平均降水量,以此类推

初始化二维数组

  • 初始化二维数组是建立在初始化一维数组的基础上。首先,初始化一维数组如下:

sometype ar1[5] = {val1, val2, val3, val4, val5};

  • 这里,val1、val2等表示sometype类型的值。例如,如果sometype是int,那么val1可能是7;如果sometype是double,那么val1可能是11.34,诸如此类。但是rain是一个内含5个元素的数组,每个元素又是内含12个float类型元素的数组。所以,对rain而言,val1应该包含12个值,用于初始化内含12个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}

  • 也就是说,如果arl每个元素是一个内含12个double类型元素的数组,那么val1就是一个由12个double类型值构成的数值列表。因此,为了初始化二维数组rain,要用逗号分隔5个这样的数值列表:

    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}

    };

  • 如果第1个列表中只有10个数,则只会初始化数组第1行的前10个元素,而最后两个元素将被默认初始化为0。如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是这并不会影响其他行的初始化。

  • 初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果与上面相同。但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为0。

其他多维数组

  • 可以这样声明一个三维数组:

int box[10][20][30];

  • 可以把一维数组想象成一行数据,把二维数组想象成数据表,把三维数组想象成一叠数据表。例如,把上面声明的三维数组box想象成由10个二维数组(每个二维数组都是20行30列)堆叠起来。
  • 还有一种理解box的方法是,把box看作数组的数组。也就是说,box内含10个元素,每个元素是内含20个元素的数组,这20个数组元素中的每个元素是内含30个元素的数组。或者,可以简单地根据所需的下标值去理解数组。
  • 通常,处理三维数组要使用3重嵌套循环,处理四维数组要使用4重嵌套循环。对于其他多维数组,以此类推。在后面的程序示例中,我们只使用二维数组。

指针和数组

  • 数组名是数组首元素的地址。也就是说,如果flizny是一个数组,下面的语句成立:

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

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

// 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: 0x7fff5fbff8dc 0x7fff5fbff8a0

    pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8

    pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0

    pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8

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

  • ·指针的值是它所指向对象的地址。地址的表示方式依赖于计算机内部的硬件。许多计算机(包括PC和Macintosh)都是按字节编址,意思是内存中的每个字节都按顺序编号。这里,一个较大对象的地址(如double类型的变量)通常是该对象第一个字节的地址。

  • ·在指针前面使用*运算符可以得到该指针所指向对象的值。

  • ·指针加1,指针的值递增它所指向类型的大小(以字节为单位)。

  • dates + 2 == &dates[2] // 相同的地址
  • *(dates + 2) == dates[2] // 相同的值
  • *(dates + 2) // dates第3个元素的值
  • *dates + 2 // dates第1个元素的值加2
/* day_mon3.c -- uses pointer notation */
#include <stdio.h>
#define MONTHS 12
int main(void)
{
   int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
   int index;
   for (index = 0; index < MONTHS; index++)
     printf("Month %2d has %d days.\n", index + 1, 
             *(days + index));  //与 days[index]相同
   return 0;
}
  • 这里,days是数组首元素的地址,days + index是元素days[index]的地址,而*(days + index)则是该元素的值,相当于days[index]。for循环依次引用数组中的每个元素,并打印各元素的内容。

函数、数组和指针

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

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

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

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

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

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;
}
  • 另一个比较灵活的方法是把数组大小作为第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;
}
  • 关于函数的形参,还有一点要注意。只有在函数原型或函数定义头中,才可以用int ar[]代替int * ar:
    int sum (int ar[], int n);
    *int ar形式和int ar[]形式都表示ar是一个指向int的指针。但是,int ar[]只能用于声明形式参数。第2种形式(int ar[])提醒读者指针ar指向的不仅仅是一个int类型值,还是一个int类型数组的元素。
// sum_arr1.c -- 数组元素之和
// 如果编译器不支持 %zd,用 %u 或 %lu 替换它
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
   int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };
   long answer;
   answer = sum(marbles, SIZE);
   printf("The total number of marbles is %ld.\n", answer);
   printf("The size of marbles is %zd bytes.\n",
     sizeof marbles);
   return 0;
}
int sum(int ar[], int n)   // 这个数组的大小是?
{
   int i;
   int total = 0;
   for (i = 0; i < n; i++)
     total += ar[i];
   printf("The size of ar is %zd bytes.\n", sizeof ar);
   return total;
}

The size of ar is 8 bytes.

The total number of marbles is 190.

The size of marbles is 40 bytes.

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

使用指针形参

  • 还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。
/* sum_arr2.c -- 数组元素之和 */
#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
   int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };
   long answer;
   answer = sump(marbles, marbles + SIZE);
   printf("The total number of marbles is %ld.\n", answer);
   return 0;
}
/* 使用指针算法 */
int sump(int * start, int * end)
{
   int total = 0;
   while (start < end)
   {
     total += *start;  // 把数组元素的值加起来
     start++;      // 让指针指向下一个元素
   }
   return total;
}
  • 因为while循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。这使得while循环的测试条件是有效的,因为start在循环中最后的值是end。注意,使用这种“越界”指针的函数调用更为简洁:

    answer = sump(marbles, marbles + SIZE);

  • 虽然C保证了marbles + SIZE有效,但是对marbles[SIZE](即存储在该位置上的值)未作任何保证,所以程序不能访问该位置。

  • 还可以把循环体压缩成一行代码:
    total += *start++;

  • 一元运算符*和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。也就是说,指针start先递增后指向。使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。

  • 虽然*start++的写法比较常用,但是(start++)这样写更清楚。*

/* order.c -- 指针运算中的优先级 */
#include <stdio.h>
int data[2] = { 100, 200 };
int moredata[2] = { 300, 400 };
int main(void)
{
   int * p1, *p2, *p3;
   p1 = p2 = data;
   p3 = moredata;
   printf(" *p1 = %d,  *p2 = %d,   *p3 = %d\n",*p1, *p2, *p3);
   printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++, *++p2, (*p3)++);
   printf(" *p1 = %d,  *p2 = %d,   *p3 = %d\n",*p1, *p2, *p3);
   return 0;
}
  • 下面是该程序的输出:

    *p1 = 100, *p2 = 100, *p3 = 300

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

    *p1 = 200, *p2 = 200, *p3 = 301

指针操作

  • 用%td转换说明打印地址的差值
// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{
   int urn[5] = { 100, 200, 300, 400, 500 };
   int * ptr1, *ptr2, *ptr3;
   ptr1 = urn;      // 把一个地址赋给指针
   ptr2 = &urn[2];    // 把一个地址赋给指针
              // 解引用指针,以及获得指针的地址
   printf("pointer value, dereferenced pointer, pointer address:\n");
   printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
   // 指针加法
   ptr3 = ptr1 + 4;
   printf("\nadding an int to a pointer:\n");
   printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
   ptr1++;        // 递增指针
   printf("\nvalues after ptr1++:\n");
   printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
   ptr2--;        // 递减指针
   printf("\nvalues after --ptr2:\n");
   printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
   --ptr1;        // 恢复为初始值
   ++ptr2;        // 恢复为初始值
   printf("\nPointers reset to original values:\n");
   printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
   // 一个指针减去另一个指针
   printf("\nsubtracting one pointer from another:\n");
   printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
   // 一个指针减去一个整数
   printf("\nsubtracting an int from a pointer:\n");
   printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
   return 0;
}

pointer value, dereferenced pointer, pointer address:

ptr1 = 0x7fff5fbff8d0, *ptr1 =100, &ptr1 = 0x7fff5fbff8c8

adding an int to a pointer:

ptr1 + 4 = 0x7fff5fbff8e0, *(ptr1 + 4) = 500

values after ptr1++:

ptr1 = 0x7fff5fbff8d4, *ptr1 =200, &ptr1 = 0x7fff5fbff8c8

values after --ptr2:

ptr2 = 0x7fff5fbff8d4, *ptr2 = 200, &ptr2 = 0x7fff5fbff8c0

Pointers reset to original values:

ptr1 = 0x7fff5fbff8d0, ptr2 = 0x7fff5fbff8d8

subtracting one pointer from another:

ptr2 = 0x7fff5fbff8d8, ptr1 = 0x7fff5fbff8d0, ptr2 - ptr1 = 2

subtracting an int from a pointer:

ptr3 = 0x7fff5fbff8e0, ptr3 - 2 = 0x7fff5fbff8d8

  • ·赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。在该例中,把urn数组的首地址赋给了ptr1,该地址的编号恰好是0x7fff5fbff8d0。变量ptr2获得数组urn的第3个元素(urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。
  • ·解引用:*运算符给出指针指向地址上存储的值。因此,*ptr1的初值是100,该值存储在编号为0x7fff5fbff8d0的地址上
  • ·取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。本例中,ptr1存储在内存编号为0x7fff5fbff8c8的地址上,该存储单元存储的内容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向utn[0]的指针。
  • ·指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1+4与&urn[4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
  • ·递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节),ptr1指向urn[1](见图10.4,该图中使用了简化的地址)。现在ptr1的值是0x7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[1]的值)。注意,ptr1本身的地址仍是0x7fff5fbff8c8。毕竟,变量不会因为值发生变化就移动位置。
  • ·指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与&urn[2]等价,因为ptr3指向的是&urn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
    ·递减指针:当然,除了递增指针还可以递减指针。在本例中,递减ptr2使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn[1]。
  • ·指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序清单10.13的输出中,ptr2 - ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。
  • ·比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

解引用未初始化的指针

说到注意事项,一定要牢记一点:千万不要解引用未初始化的指针。例如,考虑下面的例子:

int * pt; // 未初始化的指针

*pt = 5; // 严重的错误

为何不行?第2行的意思是把5存储在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将存储在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。

保护数组中的数据

  • 传递地址会导致一些问题。C通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。有时,这正是我们需要的。
  • 然而,其他函数并不需要修改数据。

对形式参数使用const

  • ANSI C提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。
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并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。
  • 一个函数显示数组的内容,另一个函数给数组每个元素都乘以一个给定值。因为第1个函数不用改变数组,所以在声明数组形参时使用了const;而第2个函数需要修改数组元素的值,所以不使用const。
/* arf.c -- 处理数组的函数 */
#include <stdio.h>
#define SIZE 5
void show_array(const double ar[], int n);
void mult_array(double ar[], int n, double mult);
int main(void)
{
   double dip[SIZE] = { 20.0, 17.66, 8.2, 15.3, 22.22 };
   printf("The original dip array:\n");
   show_array(dip, SIZE);
   mult_array(dip, SIZE, 2.5);
   printf("The dip array after calling mult_array():\n");
   show_array(dip, SIZE);
   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

const的其他内容

  • 可以创建const数组、const指针和指向const的指针。

  • 使用const关键字保护数组:

    #define MONTHS 12

    const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

  • 指向const的指针不能用于改变值。考虑下面的代码:

    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

    const double * pd = rates; // pd指向数组的首元素

  • 第2行代码把pd指向的double类型的值声明为const,这表明不能使用pd来更改它所指向的值:

    *pd = 29.89; // 不允许

    pd[2] = 222.22; //不允许

    rates[0] = 99.99; // 允许,因为rates未被const限定

  • 可以让pd指向别处:

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

  • 指向const的指针通常用于函数形参中,表明该函数不会使用指针改变数据。

  • 关于指针赋值和const需要注意一些规则。首先,把const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的:

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};*

const double locked[4] = {0.08, 0.075, 0.0725, 0.07};*

const double * pc = rates; // 有效*

pc = locked; //有效

pc = &rates[3]; //有效 然而,只能把非const数据的地址赋给普通指针:

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};*

const double locked[4] = {0.08, 0.075, 0.0725, 0.07};

double * pnc = rates; // 有效

pnc = locked; // 无效

pnc = &rates[3]; // 有效

这个规则非常合理。否则,通过指针就能改变const数组中的数据。

  • const还有其他的用法。例如,可以声明并初始化一个不能指向别处的指针,关键是const的位置:

    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

    double * const pc = rates; // pc指向数组的开始

    pc = &rates[2]; // 不允许,因为该指针不能指向别处

    *pc = 92.99; // 没问题 – 更改rates[0]的值

    可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。

  • 最后,在创建指针时还可以使用const两次,该指针既不能更改它所指向的地址,也不能修改指向地址上的值:

double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};

const double * const pc = rates;

pc = &rates[2]; //不允许

*pc = 92.99; //不允许

指针和多维数组

int zippo[4][2]; /* 内含int数组的数组 */

  • 然后数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。下面,我们从指针的属性进一步分析。
  • ·因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同。
  • ·给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。因此,zippo + 1和zippo[0] + 1的值不同。
  • ·解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。因为zippo[0]是该数组首元素(zippo[0][0])的地址,所以*(zippo[0])表示存储在zippo[0][0]上的值(即一个int类型的值)。与此类似,*zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。对两个表达式应用解引用运算符表明,**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indirection)的例子。
- /* 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 = 0x0064fd38,    zippo + 1 = 0x0064fd40
 zippo[0]= 0x0064fd38,  zippo[0] + 1 = 0x0064fd3c
  *zippo = 0x0064fd38,   *zippo + 1 = 0x0064fd3c
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(十六进制中,38+4得3c)。数组名zippo是一个内含2个int类型值的数组的地址,所以zippo指向一个8字节的数据对象。因此,zippo加1,它所指向的地址加8字节(十六进制中,38+8得40)。
  • 该程序演示了zippo[0]和*zippo完全相同,实际上确实如此。然后,对二维数组名解引用两次,得到存储在数组中的值。使用两个间接运算符(*)或者使用两对方括号([])都能获得该值(还可以使用一个*和一对[],但是我们暂不讨论这么多情况)。
  • 要特别注意,与zippo[2][1]等价的指针表示法是*(*(zippo+2) + 1)。看上去比较复杂,应最好能理解。
zippo       ←二维数组首元素的地址(每个元素都是内含两个int类型元素的一维数组)
zippo+2      ←二维数组的第3个元素(即一维数组)的地址
*(zippo+2)    ←二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)地址
*(zippo+2) + 1  ←二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)地址
*(*(zippo+2) + 1) ←二维数组的第3个一维数组元素的第2int类型元素的值,即数组的第3行第2
          列的值(zippo[2][1]

指向多维数组的指针

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

  • 以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*。考虑下面的声明:

    int * pax[2]; // pax是一个内含两个指针元素的数组,每个元素都指向int的指针

/* zippo2.c -- 通过指针获取zippo的信息 */
#include <stdio.h>
int main(void)
{
   int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };
   int(*pz)[2];
   pz = zippo;
   printf("  pz = %p,  pz + 1 = %p\n",  pz, pz + 1);
   printf("pz[0] = %p, pz[0] + 1 = %p\n",  pz[0], pz[0] + 1);
   printf(" *pz = %p,  *pz + 1 = %p\n",  *pz, *pz + 1);
   printf("pz[0][0] = %d\n", pz[0][0]);
   printf(" *pz[0] = %d\n", *pz[0]);
   printf("  **pz = %d\n", **pz);
   printf("   pz[2][1] = %d\n", pz[2][1]);
   printf("*(*(pz+2) + 1) = %d\n", *(*(pz + 2) + 1));
   return 0;
}

pz = 0x0064fd38, pz + 1 = 0x0064fd40

pz[0] = 0x0064fd38, pz[0] + 1 = 0x0064fd3c

*pz = 0x0064fd38,000 *pz + 1 = 0x0064fd3c

pz[0][0] = 2

*pz[0] = 2

**pz = 2

pz[2][1] = 3

((pz+2) + 1) = 3

zippo[m][n] == ((zippo + m) + n)

pz[m][n] == ((pz + m) + n)

指针的兼容性

  • 例如,不用类型转换就可以把int类型的值赋给double类型的变量,但是两个类型的指针不能这样做。
int n = 5;
double x;
int * p1 = &n;
double * pd = &x;
x = n;       // 隐式类型转换
pd = p1;      // 编译时错误
// 更复杂的类型也是如此。假设有如下声明:
int * pt;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2;  // 一个指向指针的指针
// 有如下的语句:
pt = &ar1[0][0];   // 都是指向int的指针
pt = ar1[0];     // 都是指向int的指针
pt = ar1;      // 无效
pa = ar1;      // 都是指向内含3个int类型元素数组的指针
pa = ar2;      // 无效
p2 = &pt;      // 都是指向int *的指针
*p2 = ar2[0];    // 都是指向int的指针
p2 = ar2;      // 无效
  • 上面的最后两个例子有些棘手。变量p2是指向指针的指针,它指向的指针指向int,而ar2是指向数组的指针,该数组内含2个int类型的元素。所以,p2和ar2的类型不同,不能把ar2赋给p2。但是,*p2是指向int的指针,与ar2[0]兼容。因为ar2[0]是指向该数组首元素(ar2[0][0])的指针,所以ar2[0]也是指向int的指针。
int x = 20;
const int y = 23;
int * p1 = &x;
const int * p2 = &y;
const int ** pp2;
p1 = p2;    // 不安全 -- 把const指针赋给非const指针
p2 = p1;    // 有效 -- 把非const指针赋给const指针
pp2 = &p1;   // 不安全 –- 嵌套指针类型赋值
  • 但是进行两级解引用时,这样的赋值也不安全,例如,考虑下面的代码:

    const int **pp2;
    int *p1;
    const int n = 13;
    pp2 = &p1;  // 允许,但是这导致const限定符失效(根据第1行代码,不能通过**pp2修改它所指向的内容)
    *pp2 = &n;  // 有效,两者都声明为const,但是这将导致p1指向n(*pp2已被修改)
    *p1 = 10;  //有效,但是这将改变n的值(但是根据第3行代码,不能修改n的值)
    

C const和C++ const

C和C++中const的用法很相似,但是并不完全相同。区别之一是,C++允许在声明数组大小时使用const整数,而C却不允许。区别之二是,C++的指针赋值检查更严格:

const int y;

const int * p2 = &y;

int * p1;

p1 = p2; // C++中不允许这样做,但是C可能只给出警告

C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果通过p1更改y,其行为是未定义的。

函数和多维数组

// array2d.c -- 处理二维数组的函数
#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int);    // 省略形参名,没问题
int sum2d(int(*ar)[COLS], int rows);   // 另一种语法
int main(void)
{
   int junk[ROWS][COLS] = {
        { 2, 4, 6, 8 },
        { 3, 5, 7, 9 },
        { 12, 10, 8, 6 }
   };
   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

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

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

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

    int sum2(int ar[][4], int rows); // 有效声明

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

    int sum2(int ar[3][4], int rows); // 有效声明,但是3将被忽略

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

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

  • 因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。下面的声明与该声明等价:

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

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

变长数组(VLA)

  • C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。如下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters];  // 一个变长数组(VLA)
  • 前面提到过,变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符(第12章介绍)。而且,不能在声明中初始化它们。最终,C11把变长数组作为一个可选特性,而不是必须强制实现的特性。

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

首先,要声明一个带二维变长数组参数的函数,如下所示:
int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一个变长数组(VLA)
注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。因此,下面的原型是错误的:
int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名

  • 是否可以在声明数组时使用const变量?

    const int SZ = 80;

    double ar[SZ]; // 是否允许?

  • C90标准不允许(也可能允许)。数组的大小必须是给定的整型常量表达式,可以是整型常量组合,如20、sizeof表达式或其他不是const的内容。

  • C99/C11标准允许在声明变长数组时使用const变量。所以该数组的定义必须是声明在块中的自动存储类别数组。

复合字面量

  • C99新增了复合字面量(compound literal)。字面量是除符号常量外的常量。例如,5是int类型字面量,81.3是double类型的字面量,'Y’是char类型的字面量,"elephant"是字符串字面量。发布C99标准的委员会认为,如果有代表数组和结构内容的复合字面量,在编程时会更方便。

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

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

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

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

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

    int * pt1;

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

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

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

    int total3;

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

// flc.c -- 有趣的常量
#include <stdio.h>
#define COLS 4
int sum2d(const int ar[][COLS], int rows);
int sum(const int ar[], int n);
int main(void)
{
   int total1, total2, total3;
   int * pt1;
   int(*pt2)[COLS];
   pt1 = (int[2]) { 10, 20 };
   pt2 = (int[2][COLS]) { {1, 2, 3, -9}, { 4, 5, 6, -8 } };
   total1 = sum(pt1, 2);
   total2 = sum2d(pt2, 2);
   total3 = sum((int []){ 4, 4, 4, 5, 5, 5 }, 6);
   printf("total1 = %d\n", total1);
   printf("total2 = %d\n", total2);
   printf("total3 = %d\n", total3);
   return 0;
}
int sum(const int ar [], int n)
{
   int i;
   int total = 0;
   for (i = 0; i < n; i++)
     total += ar[i];
   return total;
}
int sum2d(const int ar [][COLS], int rows)
{
   int r;
   int c;
   int tot = 0;
   for (r = 0; r < rows; r++)
     for (c = 0; c < COLS; c++)
        tot += ar[r][c];
   return tot;
}
  • 要支持C99的编译器才能正常运行该程序示例(目前并不是所有的编译器都支持),其输出如下:

    total1 = 30

    total2 = 4

    total3 = 27

复习题

  1. 下面的程序将打印什么内容?

    #include <stdio.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);
       return 0;
    }
    
  2. 在复习题1中,ref有多少个元素?

  3. 在复习题1中,ref的地址是什么?ref + 1是什么意思?++ref指向什么?

  4. 在下面的代码中,ptr和(ptr + 2)的值分别是什么?
    a.
    int *ptr;
    int torf[2][2] = {12, 14, 16};
    ptr = torf[0];
    b.
    int * ptr;
    int fort[2][2] = { {12}, {14,16} };
    ptr = fort[0];

  5. 在下面的代码中,ptr和(ptr + 1)的值分别是什么?
    a.
    int (*ptr)[2];
    int torf[2][2] = {12, 14, 16};
    ptr = torf;
    b.
    int (*ptr)[2];
    int fort[2][2] = { {12}, {14,16} };
    ptr = fort;

  6. 假设有下面的声明:
    int grid[30][100];
    a.用1种写法表示grid[22][56]的地址
    b.用2种写法表示grid[22][0] 的地址
    c.用3种写法表示grid[0][0] 的地址

  7. 正确声明以下各变量:
    a.digits是一个内含10个int类型值的数组
    b.rates是一个内含6个float类型值的数组
    c.mat是一个内含3个元素的数组,每个元素都是内含5个整数的数组
    d.psa是一个内含20个元素的数组,每个元素都是指向char的指针
    e.pstr是一个指向数组的指针,该数组内含20个char类型的值

  8. a.声明一个内含6个int类型值的数组,并初始化各元素为1、2、4、8、16、32
    b.用数组表示法表示a声明的数组的第3个元素(其值为4)
    c.假设编译器支持C99/C11标准,声明一个内含100个int类型值的数组,并初始化最后一个元素为-1,其他元素不考虑
    d.假设编译器支持C99/C11标准,声明一个内含100个int类型值的数组,并初始化下标为5、10、11、12、13的元素为101,其他元素不考虑

  9. 内含10个元素的数组下标范围是什么?

  10. 假设有下面的声明:
    float rootbeer[10], things[10][5], *pf, value = 2.2;
    int i = 3;
    判断以下各项是否有效:
    a.rootbeer[2] = value;
    b.scanf("%f", &rootbeer );
    c.rootbeer = value;
    d.printf("%f", rootbeer);
    e.things[4][4] = rootbeer[3];
    f.things[5] = rootbeer;
    g.pf = value;
    h.pf = rootbeer;

  11. 声明一个800×600的int类型数组。

  12. 下面声明了3个数组:
    double trots[20];
    short clops[10][30];
    long shots[5][10][15];
    a.分别以传统方式和以变长数组为参数的方式编写处理trots数组的void函数原型和函数调用
    b.分别以传统方式和以变长数组为参数的方式编写处理clops数组的void函数原型和函数调用
    c.分别以传统方式和以变长数组为参数的方式编写处理shots数组的void函数原型和函数调用

  13. 下面有两个函数原型:
    void show(const double ar[], int n); // n是数组元素的个数
    void show2(const double ar2[][3], int n); // n是二维数组的行数
    a.编写一个函数调用,把一个内含8、3、9和2的复合字面量传递给show()函数。
    b.编写一个函数调用,把一个2行3列的复合字面量(8、3、9作为第1行,5、4、1作为第2行)传递给show2()函数。


  1. 打印的内容如下:
    8 8
    4 4
    0 0
    2 2
  2. 数组ref有4个元素,因为初始化列表中的值是4个。
  3. 数组名ref指向该数组的首元素(整数8)。表达式ref + 1指向该数组的第2个元素(整数4)。++ref不是有效的表达式,因为ref是一个常量,不是变量。
  4. ptr指向第1个元素,ptr + 2指向第3个元素(即第2行的第1个元素)。
    a.12和16。
    b.12和14(初始化列表中,用花括号把12括起来,把14和16括起来,所以12初始化第1行的第1个元素,而14初始化第2行的第1个元素)。
  5. ptr指向第1行,ptr + 1指向第2行。ptr指向第1行的第1个元素,而(ptr + 1)指向第2行的第1个元素。
    a.12和16。
    b.12和14(同第4题,12初始化第1行的第1个元素,而14初始化第2行的第1个元素)。
  6. a.&grid[22][56]
    b.&grid[22][0]或grid[22]
    (grid[22]是一个内含100个元素的一维数组,因此它就是首元素grid[22][0]的地址。)
    c.&grid[0][0]或grid[0]或(int *) grid
    (grid[0]是int类型元素grid[0][0]的地址,grid是内含100个元素的grid[0]数组的地址。这两个地址的数值相同,但是类型不同,可以用强制类型转换把它们转换成相同的类型。)
  7. a.int digits[10];
    b.float rates[6];
    c.int mat[3][5];
    d.char * psa[20] ;
    注意,[]比*的优先级高,所以在没有圆括号的情况下,psa先与[20]结合,然后再与*结合。因此该声明与char *(psa[20]);相同。
    e.char (*pstr)[20];
    注意
    对第e小题而言,char *pstr[20];不正确。这会让pstr成为一个指针数组,而不是一个指向数组的指针。具体地说,如果使用该声明,pstr就指向一个char类型的值(即数组的第1个成员),而pstr + 1则指向下一个字节。使用正确的声明,pstr是一个变量,而不是一个数组名。而且pstr + 1指向起始字节后面的第20个字节。
  8. a.int sextet[6] = {1, 2, 4, 8, 16, 32};
    b.sextet[2]
    c.int lots[100] = { [99] = -1};
    d.int pots[100] = { [5] = 101, [10] = 101,101, 101, [3]=101};
  9. 0~9
  10. a.rootbeer[2] = value;有效。
    b.scanf("%f", &rootbeer );无效,rootbeer不是float类型。
    c.rootbeer = value;无效,rootbeer不是float类型。
    d.printf("%f", rootbeer);无效,rootbeer不是float类型。
    e.things[4][4] = rootbeer[3];有效。
    f.things[5] = rootbeer;无效,不能用数组赋值。
    g.pf = value;无效,value不是地址。
    h.pf = rootbeer;有效。
  11. int screen[800][600] ;
  12. a.
    void process(double ar[], int n);
    void processvla(int n, double ar[n]);
    process(trots, 20);
    processvla(20, trots);
    b.
    void process2(short ar2[30], int n);
    void process2vla(int n, int m, short ar2[n][m]);
    process2(clops, 10);
    process2vla(10, 30, clops);
    c.
    void process3(long ar3[10][15], int n);
    void process3vla(int n, int m,int k, long ar3[n][m][k]);
    process3(shots, 5);
    process3vla(5, 10, 15, shots);
  13. a.
    show( (int [4]) {8,3,9,2}, 4);
    b.
    show2( (int [][3]){{8,3,9}, {5,4,1}}, 2);

编程练习

  1. 修改程序清单10.7的rain.c程序,用指针进行计算(仍然要声明并初始化数组)。
  2. 编写一个程序,初始化一个double类型的数组,然后把该数组的内容拷贝至3个其他数组中(在main()中声明这4个数组)。使用带数组表示法的函数进行第1份拷贝。使用带指针表示法和指针递增的函数进行第2份拷贝。把目标数组名、源数组名和待拷贝的元素个数作为前两个函数的参数。第3个函数以目标数组名、源数组名和指向源数组最后一个元素后面的元素的指针。也就是说,给定以下声明,则函数调用如下所示:
    double source[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
    double target1[5];
    double target2[5];
    double target3[5];
    copy_arr(target1, source, 5);
    copy_ptr(target2, source, 5);
    copy_ptrs(target3, source, source + 5)
  3. 编写一个函数,返回存储在int类型数组中的最大值,并在一个简单的程序中测试该函数。
  4. 编写一个函数,返回存储在double类型数组中最大值的下标,并在一个简单的程序中测试该函数。
  5. 编写一个函数,返回存储在double类型数组中最大值和最小值的差值,并在一个简单的程序中测试该函数。
  6. 编写一个函数,把double类型数组中的数据倒序排列,并在一个简单的程序中测试该函数。
  7. 编写一个程序,初始化一个double类型的二维数组,使用编程练习2中的一个拷贝函数把该数组中的数据拷贝至另一个二维数组中(因为二维数组是数组的数组,所以可以使用处理一维数组的拷贝函数来处理数组中的每个子数组)。
  8. 使用编程练习2中的拷贝函数,把一个内含7个元素的数组中第3~第5个元素拷贝至内含3个元素的数组中。该函数本身不需要修改,只需要选择合适的实际参数(实际参数不需要是数组名和数组大小,只需要是数组元素的地址和待处理元素的个数)。
  9. 编写一个程序,初始化一个double类型的3×5二维数组,使用一个处理变长数组的函数将其拷贝至另一个二维数组中。还要编写一个以变长数组为形参的函数以显示两个数组的内容。这两个函数应该能处理任意N×M数组(如果编译器不支持变长数组,就使用传统C函数处理N×5的数组)。
  10. 编写一个函数,把两个数组中相对应的元素相加,然后把结果存储到第3个数组中。也就是说,如果数组1中包含的值是2、4、5、8,数组2中包含的值是1、0、4、6,那么该函数把3、4、9、14赋给第3个数组。函数接受3个数组名和一个数组大小。在一个简单的程序中测试该函数。
  11. 编写一个程序,声明一个int类型的3×5二维数组,并用合适的值初始化它。该程序打印数组中的值,然后各值翻倍(即是原值的2倍),并显示出各元素的新值。编写一个函数显示数组的内容,再编写一个函数把各元素的值翻倍。这两个函数都以函数名和行数作为参数。
  12. 重写程序清单10.7的rain.c程序,把main()中的主要任务都改成用函数来完成。
  13. 编写一个程序,提示用户输入3组数,每组数包含5个double类型的数(假设用户都正确地响应,不会输入非数值数据)。该程序应完成下列任务。
    a.把用户输入的数据存储在3×5的数组中
    b.计算每组(5个)数据的平均值
    c.计算所有数据的平均值
    d.找出这15个数据中的最大值
    e.打印结果
    每个任务都要用单独的函数来完成(使用传统C处理数组的方式)。完成任务b,要编写一个计算并返回一维数组平均值的函数,利用循环调用该函数3次。对于处理其他任务的函数,应该把整个数组作为参数,完成任务c和d的函数应把结果返回主调函数。
  14. 以变长数组作为函数形参,完成编程练习13。

  1. #include <stdio.h>
    #define MONTHS 12 // 一年的月份数
    #define YEARS 5   // 年数
    int main(void)
    {
        // 用2010~2014年的降水量数据初始化数组
        const float rain[YEARS][MONTHS] =
            {
                {4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
                {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
                {9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
                {7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
                {7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}};
        int year, month;
        float subtot, total;
        printf(" YEAR  RAINFALL (inches)\n");
        for (year = 0, total = 0; year < YEARS; year++)
        { // 每一年,各月的降水量总和
            for (month = 0, subtot = 0; month < MONTHS; month++)
                // subtot += rain[year][month];
                subtot += *(*(rain + year) + month);
            printf("%5d %15.1f\n", 2010 + year, subtot);
            total += subtot; // 5年的总降水量
        }
        printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
        printf("MONTHLY AVERAGES:\n\n");
        printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");
        printf(" Nov Dec\n");
        for (month = 0; month < MONTHS; month++)
        { // 每个月,5年的总降水量
            for (year = 0, subtot = 0; year < YEARS; year++)
                // subtot += rain[year][month];
                subtot += *(*(rain + year) + month);
            printf("%4.1f ", subtot / YEARS);
        }
        printf("\n");
        return 0;
    }
    
  2. #include <stdio.h>
    void copy_arr(double target[], double source[], int size);
    void copy_ptr(double target[], double source[], int size);
    void copy_ptrs(double target[], double *start, double *end);
    void display(double arr[], int size); // 展示数组元素,测试用
    int main(void)
    {
        double source[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
        double target1[5];
        double target2[5];
        double target3[5];
        copy_arr(target1, source, 5);
        copy_ptr(target2, source, 5);
        copy_ptrs(target3, source, source + 5);
        display(target1, 5);
        display(target2, 5);
        display(target3, 5);
        return 0;
    }
    void copy_arr(double target[], double source[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            target[i] = source[i];
        }
    }
    void copy_ptr(double target[], double source[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            *(target + i) = *(source + i);
        }
    }
    void copy_ptrs(double target[], double *start, double *end)
    {
        // int *i = start;
        int i = 0;
        while (start < end)
        {
            *(target + i++) = *start++;
        }
    }
    void display(double arr[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            printf("%g ", arr[i]);
        }
        printf("\n");
    }
    
  3. #include <stdio.h>
    int i_max(int arr[], int size);
    int main(void)
    {
        int arr[] = {1, 55, 3535, 5, 5466754, 1, 74574}; // 随便写的数组
        int max = i_max(arr, sizeof arr / sizeof arr[0]);
        printf("max = %d\n", max);
        return 0;
    }
    int i_max(int arr[], int size)
    {
        int max = arr[0];
        for (int i = 1; i < size; i++)
        {
            if (arr[i] > max)
            {
                max = arr[i];
            }
        }
        return max;
    }
    
  4. #include <stdio.h>
    int max_index(double arr[], int size);
    int main(void)
    {
        double arr[] = {1.1, 2.2, 5.5, 3.3, 4.4};
        int index = max_index(arr, sizeof arr / sizeof arr[0]);
        printf("%d\n", index);
        return 0;
    }
    int max_index(double arr[], int size)
    {
        double max = arr[0];
        int index;
        for (int i = 1; i < size; i++)
        {
            if (arr[i] > max)
                max = arr[i];
        }
        for (int i = 0; i < size; i++)
        {
            if (arr[i] == max)
            {
                index = i;
                break;
            }
        }
        return index;
    }
    
  5. #include <stdio.h>
    double sub_max_min(double arr[], int size);
    int main(void)
    {
        double arr[] = {1.1, 2.2, 3.3, 4.4, 5.5};
        double result = sub_max_min(arr, sizeof arr / sizeof arr[0]);
        printf("%g\n", result);
        return 0;
    }
    double sub_max_min(double arr[], int size)
    {
        double max = arr[0], min = arr[0];
        for (int i = 1; i < size; i++)
        {
            if (arr[i] > max)
                max = arr[i];
            if (arr[i] < min)
                min = arr[i];
        }
        return max - min;
    }
    
  6. #include <stdio.h>
    void reverse(double arr[], int size);
    void display(double arr[], int size);
    int main(void)
    {
        // double arr[] = {1.1, 2.2, 3.3, 4.4, 5.5};
        double arr[] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6};
        // 长度为奇数和偶数分别测试
        reverse(arr, sizeof arr / sizeof arr[0]);
        display(arr, sizeof arr / sizeof arr[0]);
        return 0;
    }
    void reverse(double arr[], int size)
    {
        for (int i = 0; i < size / 2; i++)
        {
            double temp = arr[i];
            arr[i] = arr[size - i - 1];
            arr[size - i - 1] = temp;
        }
    }
    void display(double arr[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            printf("%g ", arr[i]);
        }
        printf("\n");
    }
    
  7. #include <stdio.h>
    void copy_arr(double target[], double source[], int size);
    void display(double arr[], int size);
    int main(void)
    {
        double arr[3][5] = {{1.1, 2.2, 3.3, 4.4, 5.5},
                            {2.2, 3.3, 4.4, 5.5, 1.1},
                            {1.1, 2.2, 3.3, 4.4, 6.6}};
        double target[3][5];
        for (int i = 0; i < 3; i++)
        {
            copy_arr(target[i], arr[i], 5);
        }
        for (int i = 0; i < 3; i++)
        {
            display(target[i], 5);
        }
        return 0;
    }
    // 拷贝编程题2
    void copy_arr(double target[], double source[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            target[i] = source[i];
        }
    }
    void display(double arr[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            printf("%g ", arr[i]);
        }
        printf("\n");
    }
    
#include <stdio.h>
   void copy_ptrs(double target[], double *start, double *end);
void display(double arr[], int size);
   int main(void)
   {
       double arr[7] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0};
       double target[3];
       // 需要传结束元素后面一个位置的指针
       copy_ptrs(target, arr + 2, arr + 5);
       display(target, 3);
       return 0;
   }
   // 复制题2
   void copy_ptrs(double target[], double *start, double *end)
   {
       // int *i = start;
       int i = 0;
       while (start < end)
       {
           *(target + i++) = *start++;
       }
   }
   void display(double arr[], int size)
   {
       for (int i = 0; i < size; i++)
       {
           printf("%g ", arr[i]);
       }
       printf("\n");
   }
  1. #include <stdio.h>
    void copy_arr(int row, int col, double source[][col], double target[][col]);
    void display(int row, int col, double arr[][col]);
    int main(void)
    {
        int row = 3, col = 5;
        double arr[3][5] = {
            {1.0, 2.0, 3.0, 4.0, 5.0},
            {2.0, 2.0, 3.0, 4.0, 5.0},
            {3.0, 2.0, 3.0, 4.0, 5.0},
        };
        double target[row][col];
        copy_arr(row, col, arr, target);
        display(row, col, arr);
        display(row, col, target);
        return 0;
    }
    void copy_arr(int row, int col, double source[][col], double target[][col])
    {
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < col; j++)
            {
                target[i][j] = source[i][j];
            }
        }
    }
    void display(int row, int col, double arr[][col])
    {
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < col; j++)
            {
                printf("%g ", arr[i][j]);
            }
            printf("\n");
        }
        printf("\n");
    }
    
  2. #include <stdio.h>
    void add_arr(double arr1[], double arr2[], double result[], int size);
    void display(double arr[], int size);
    int main(void)
    {
        double arr1[] = {2.0, 4.0, 5.0, 8.0};
        double arr2[] = {1.0, 0.0, 4.0, 6.0};
        double result[] = {0, 0, 0, 0};
        add_arr(arr1, arr2, result, sizeof arr1 / sizeof arr1[0]);
        display(result, sizeof arr1 / sizeof arr1[0]);
        return 0;
    }
    void display(double arr[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            printf("%g ", arr[i]);
        }
        printf("\n");
    }
    void add_arr(double arr1[], double arr2[], double result[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            result[i] = arr1[i] + arr2[i];
        }
    }
    
  3. #include <stdio.h>
    void display(int row, int arr[][5]);
    void doubling(int row, int arr[][5]);
    int main(void)
    {
        int arr[3][5] = {
            {1, 2, 3, 4, 5},
            {1, 2, 3, 4, 5},
            {1, 2, 3, 4, 5}};
        display(3, arr);
        doubling(3, arr);
        display(3, arr);
        return 0;
    }
    void display(int row, int arr[][5])
    {
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < 5; j++)
            {
                printf("%d ", arr[i][j]);
            }
            printf("\n");
        }
        printf("\n");
    }
    void doubling(int row, int arr[][5])
    {
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < 5; j++)
            {
                arr[i][j] *= 2;
            }
        }
    }
    
  4. #include <stdio.h>
    #define MONTHS 12 // 一年的月份数
    #define YEARS 5   // 年数
    float get_total(const float arr[][MONTHS], int row);
    void ave_months(const float arr[][MONTHS], int row);
    int main(void)
    {
        // 用2010~2014年的降水量数据初始化数组
        const float rain[YEARS][MONTHS] =
            {
                {4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
                {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
                {9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
                {7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
                {7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}};
        int year, month;
        float subtot, total;
        printf(" YEAR  RAINFALL (inches)\n");
        total = get_total(rain, YEARS);
        printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
        printf("MONTHLY AVERAGES:\n\n");
        printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");
        printf(" Nov Dec\n");
        ave_months(rain,YEARS);
        return 0;
    }
    float get_total(const float arr[][MONTHS], int row)
    {
        float total, subtot;
        int year,month;
        for (year = 0, total = 0; year < row; year++)
        { // 每一年,各月的降水量总和
            for (month = 0, subtot = 0; month < MONTHS; month++)
                subtot += arr[year][month];
            printf("%5d %15.1f\n", 2010 + year, subtot);
            total += subtot; // 5年的总降水量
        }
        return total;
    }
    void ave_months(const float arr[][MONTHS], int row)
    {
        float subtot;
        int month,year;
        for (month = 0; month < MONTHS; month++)
        { // 每个月,5年的总降水量
            for (year = 0, subtot = 0; year < YEARS; year++)
                subtot += arr[year][month];
            printf("%4.1f ", subtot / YEARS);
        }
        printf("\n");
    }
    
  5. #include <stdio.h>
    void input(double arr[][5], int row);
    double sub_ave(double arr[], int size);
    double average(double arr[][5], int row);
    double max(double arr[][5], int row);
    int main(void)
    {
        double arr[3][5];
        input(arr, 3);
        for (int i = 0; i < 3; i++)
        {
            double result = sub_ave(arr[i], 5);
            printf("%g ", result);
        }
        printf("\n");
        double result = average(arr, 3);
        printf("%g\n", result);
        result = max(arr, 3);
        printf("%g\n", result);
        return 0;
    }
    void input(double arr[][5], int row)
    {
        printf("enter five groups of numbers and every group has five numbers.\n");
        for (int i = 0; i < row; i++)
        {
            printf("enter 第 %d group:\n", i + 1);
            for (int j = 0; j < 5; j++)
            {
                scanf("%lf", &arr[i][j]);
            }
        }
    }
    double sub_ave(double arr[], int size)
    {
        double sum = 0.0;
        for (int i = 0; i < size; i++)
        {
            sum += arr[i];
        }
        return sum / size;
    }
    double average(double arr[][5], int row)
    {
        double sum = 0.0;
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < 5; j++)
                sum += arr[i][j];
        }
        return sum / 5 / row;
    }
    double max(double arr[][5], int row)
    {
        double max = arr[0][0];
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < 5; j++)
                if (arr[i][j] > max)
                    max = arr[i][j];
        }
        return max;
    }
    
  6. #include <stdio.h>
    void input(int row, int col, double arr[row][col]);
    double sub_ave(double arr[], int size);
    double average(int row, int col, double arr[row][col]);
    double max(int row, int col, double arr[row][col]);
    int main(void)
    {
        double arr[3][5];
        input(3, 5, arr);
        for (int i = 0; i < 3; i++)
        {
            double result = sub_ave(arr[i], 5);
            printf("%g ", result);
        }
        printf("\n");
        double result = average(3, 5, arr);
        printf("%g\n", result);
        result = max(3, 5, arr);
        printf("%g\n", result);
        return 0;
    }
    void input(int row, int col, double arr[row][col])
    {
        printf("enter five groups of numbers and every group has five numbers.\n");
        for (int i = 0; i < row; i++)
        {
            printf("enter 第 %d group:\n", i + 1);
            for (int j = 0; j < col; j++)
            {
                scanf("%lf", &arr[i][j]);
            }
        }
    }
    double sub_ave(double arr[], int size)
    {
        double sum = 0.0;
        for (int i = 0; i < size; i++)
        {
            sum += arr[i];
        }
        return sum / size;
    }
    double average(int row, int col, double arr[row][col])
    {
        double sum = 0.0;
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < col; j++)
                sum += arr[i][j];
        }
        return sum / 5 / row;
    }
    double max(int row, int col, double arr[row][col])
    {
        double max = arr[0][0];
        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < col; j++)
                if (arr[i][j] > max)
                    max = arr[i][j];
        }
        return max;
    }
    
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jian圣楠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值