Note
数组(array)是同类型元素的有序集合,指针(pointer)是存储地址的变量。本章揭示两者的”亲密关系”:数组名就是首元素地址,ar[i] 本质是 *(ar+i) 的伪装。掌握指针与数组的配合,是写出高效 C 程序的关键一步。
10.1 数组
数组(array) 由一系列相同数据类型的元素组成,声明告诉编译器元素个数与类型:
float candy[365]; /* array of 365 floats */
char code[12]; /* array of 12 chars */
int states[50]; /* array of 50 ints */访问元素用下标(subscript,索引 index),编号从 0 开始:candy[0] 是第一个元素,candy[364] 是最后一个。
10.2 数组初始化
用花括号括起、逗号分隔的值列表即可初始化:
int powers[8] = {1,2,4,6,8,16,32,64}; /* ANSI only */清单 10.1(day_mon1.c)用它打印每月天数:
#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;
}用符号常量表示数组大小是推荐做法:将来改尺寸只需改一处。只读不写的数组应加 const:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};不初始化会怎样? 清单 10.2(no_data.c)打印未初始化的数组,得到的全是垃圾值(如 4204937)——编译器直接使用内存位置上碰巧已有的内容。部分初始化(清单 10.3,int some_data[SIZE] = {1492, 1066};)则让后两个元素自动为 0:列表不够时剩余元素补 0,一个不初始化才是垃圾值,列表超长则是编译错误。
Warning
未初始化的自动存储类数组,元素值是不确定的垃圾值,读取它们等于读随机数据。本章讨论的是函数内声明且未加 static 的自动数组;其他存储类未初始化时元素会被置 0(第 12 章)。
让编译器数元素:省略大小,编译器按列表项数定长(清单 10.4):
const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31};
for (index = 0; index < sizeof days / sizeof days[0]; index++)sizeof days 是整个数组的字节数,除以单个元素的字节数即元素个数。指定初始化器(C99)则在列表中用方括号索引指定初始化哪个元素(清单 10.5):
int arr[6] = { [5] = 212}; // 只初始化 arr[5]
int days[MONTHS] = {31,28, [4] = 31,30,31, [1] = 29};两条规则:指定项后的值继续初始化后续元素(days[5]=30、days[6]=31);同一元素多次初始化时最后一次生效。未初始化的元素一律为 0。
给数组赋值:声明之后只能逐元素赋值(通常用循环)。C 不允许整体赋值,花括号列表只在初始化时可用:
yaks = oxen; /* not allowed */
yaks[SIZE] = oxen[SIZE]; /* invalid */10.3 数组边界(Array Bounds)
下标必须落在 0 到 n-1 之间——编译器不检查越界。清单 10.6(bounds.c)故意对 4 元素数组使用 -1 到 6 的下标,某编译器输出显示 value1 从 44 变成 -1、value2 从 88 变成 9:这套实现把 value1、value2 恰好放在数组紧前、紧后,arr[-1] 和 arr[4] 便”踩”到了它们。越界的后果是未定义的(undefined):可能正常、可能怪异、可能中止。
Warning
C 信任程序员:不做边界检查换来更快的程序,风险全由你承担。两个习惯:牢记下标从 0 开始、最大到 n-1;在声明和循环里统一使用符号常量(
for (i = 0; i < SIZE; i++))。
10.4 指定数组大小
C99 之前,方括号里必须是大于 0 的整型常量表达式(sizeof 表达式算常量,const 值不算——与 C++ 不同):
float a1[5]; // yes
float a3[sizeof(int) + 1]; // yes(sizeof 表达式算常量)
float a4[-4]; // no, a5[0]: size must be > 0
float a6[2.5]; // no, size must be an integer
float a8[n]; // not allowed before C99C99 起允许最后这种写法,即变长数组(VLA,variable-length array)——为数值计算和移植 FORTRAN 库而生,VLA 不能在声明时初始化,后文详述。
10.5 多维数组(Multidimensional Arrays)
存 5 年的月降雨量,最好用”数组的数组”:
float rain[5][12]; // 5 个元素,每个元素是含 12 个 float 的数组从内向外读声明:rain 是含 5 个元素的数组,每个元素类型是 float[12]。rain[0]、rain[1]……各是 12 个 float 的数组,rain[0][0] 才是单个 float。也可看作 5 行 12 列的表格:第二个下标沿行移动(逐月),第一个下标沿列移动(逐年)。内部存储是按行顺序排布的。

清单 10.7(rain.c)是二维数组的经典应用——求各年总量、年均值和各月均值:
/* rain.c -- finds yearly totals, yearly average, and monthly average for several years of rainfall data */
#include <stdio.h>
#define MONTHS 12 // number of months in a year
#define YEARS 5 // number of years of data
int main(void)
{
// initializing rainfall data for 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 each year, sum rainfall for each month
for (month = 0, subtot = 0; month < MONTHS; month++)
subtot += rain[year][month];
printf("%5d %15.1f\n", 2000 + year, subtot);
total += subtot; // total for all 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");
for (month = 0; month < MONTHS; month++)
{ // for each month, sum rainfall over years
for (year = 0, subtot = 0; year < YEARS; year++)
subtot += rain[year][month];
printf("%4.1f ", subtot/YEARS);
}
printf("\n");
return 0;
}嵌套循环是二维数组的天然搭档:一个循环管第一个下标,另一个管第二个;交换内外循环的位置,就从”按年统计”变成”按月统计”。二维初始化:五个内层花括号列表再套一层外层花括号,每个内层列表对应一行。某行列表太短则该行剩余元素补 0;太长是错误,不会挤到下一行。可省略内层花括号(数据按行顺序填充),但保留内层括号可读性更好。

三维照此类推:int box[10][20][30]; 像 10 张 20×30 表格叠放,处理时用三层嵌套循环。
10.6 指针与数组(Pointers and Arrays)
数组记法是指针记法的伪装:数组名就是首元素的地址,flizny == &flizny[0]。两者都是常量,但可赋给指针变量。对指针加 1 会怎样?清单 10.8(pnt_add.c)分别对 short 和 double 数组的指针做加法,输出显示:
- short 指针 +1,地址增加 2(short 占 2 字节)
- double 指针 +1,地址增加 8(double 占 8 字节)
给指针加 1,C 加的是一个存储单元(元素)的大小,不是 1 个字节——这正是声明指针必须指明指向类型的原因。

数组与指针的核心等式:
dates + 2 == &dates[2] /* 同一个地址 */
*(dates + 2) == dates[2] /* 同一个值 */C 标准就是把 ar[n] 定义为 (ar + n) 的。注意区分 *(dates+2)(第 3 个元素的值)与 *dates+2(首元素的值加 2)—— 的优先级高于 +。因此清单 10.9 用 *(days + index) 代替 days[index],输出与清单 10.1 完全相同。
10.7 函数、数组和指针
求数组和的函数怎么写?数组名是首元素地址,形参应是指向 int 的指针;光有地址不知道元素个数,把大小作为第二个参数传入:```c int sum(int * ar, int n) // 清单 10.10 的 sum() { int i; int total = 0; for (i = 0; i < n; i++) // use n elements total += ar[i]; // ar[i] the same as *(ar + i) return total; }
**声明数组形参**:只有在函数原型或函数头中(且仅限此场合),`int ar[]` 与 `int * ar` 同义——ar 是指向 int 的指针。以下四个原型完全等价:
```txt
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);
清单 10.10(sum_arr1.c)暴露了一个关键事实:sizeof marbles 是 40 字节(10 个 4 字节 int),而函数内 sizeof ar 只有 4 字节——ar 是首元素的指针,不是数组本身。这也解释了 C 为什么不按值传数组:拷贝太浪费,传地址又快又省,代价是函数能改动原数据。
另一种写法:传首尾两个指针(清单 10.11,sum_arr2.c):
/* use pointer arithmetic */
int sump(int * start, int * end)
{
int total = 0;
while (start < end)
{
total += *start; /* add value to total */
start++; /* advance pointer to next element */
}
return total;
}调用为 sump(marbles, marbles + SIZE):end 指向末元素之后的位置(one-past-the-end)。C 保证该位置是合法指针(但不保证可解引用),while (start < end) 处理完末元素即停。若 end 指向末元素本身,调用就得写成 marbles + SIZE - 1,难看又易错。指针参数是变量、可自增,这是它比数组名(常量)灵活之处。
total += *start++; 一行浓缩了优先级规则:* 与 ++ 优先级相同、从右向左结合,++ 作用于指针而非值;后缀形式保证先取值加进 total、指针再前进。对比:*++start 先移动再取值;(*start)++ 则取值后把值加 1、指针不动(清单 10.12 order.c 验证:只有 (*p3)++ 改了数组元素的值,另两个只是移动指针)。ar[i] 与 *(ar+i) 对数组名和指针变量都有效,但 ar++ 只对指针变量合法;指针记法可能生成更高效代码,但正确性和清晰度优先,优化交给编译器。
10.8 指针运算(Pointer Operations)
清单 10.13(ptr_ops.c)演示了指针的八种基本操作,核心片段:
int urn[5] = {100, 200, 300, 400, 500};
int *ptr1, *ptr2, *ptr3;
ptr1 = urn; // 赋值:数组首地址
ptr2 = &urn[2]; // 赋值:第三个元素地址
ptr3 = ptr1 + 4; // 指针 + 整数
ptr1++; // 递增:移到下一元素
ptr2--; // 递减:移回上一元素
ptr2 - ptr1; // 求差:相距 2 个 int(不是 2 字节)
ptr3 - 2; // 指针 - 整数
ptr1 < ptr2; // 比较:同类型指针可用关系运算符归纳:
- 赋值:地址须与指针类型兼容,不能把 double 的地址赋给 int *(C99 强制)。
- 解引用:* 取出指向位置的值。
- 取指针自身的地址:指针也是变量,&ptr1 可得它自己的地址。
- 加/减整数:整数乘以所指类型字节数再加到地址上;结果越出原数组则未定义——唯一例外是”末元素之后”的位置。
- 递增/递减:移动到下/上一个元素;指针自己的地址不变。
- 求差:两个指向同一数组的指针相减,得到相距的元素个数;跨数组相减无意义。
- 比较:同类型指针可用 ==、< 等。

数组名(urn)是常量不能自增,指针不能相加、相乘——ptr1++、ptr2 = ptr1 + 2、ptr2 = urn + 1 合法,urn++、ptr2 + ptr1、urn * ptr1 非法。
Warning
计算机不跟踪指针是否仍指向数组元素。指向元素或”末元素之后”是安全的,越过这条线则行为未定义;指向”末元素之后”的指针合法,但不保证可以解引用。
解引用未初始化的指针是灾难:
int * pt; // an uninitialized pointer
*pt = 5; // a terrible errorpt 的值是随机的,5 会被写到不可预知的位置——可能无害、可能覆盖数据或代码、可能崩溃。创建指针只分配了存放指针本身的内存,并未为它指向的数据分配内存。用指针前必须先让它指向已分配的位置(如已有变量的地址),或先用 malloc() 分配(第 12 章)。
Warning
铁律:绝不对未初始化的指针解引用。
double * pd; *pd = 2.4;同样危险。
10.9 保护数组内容(Protecting Array Contents)
处理基本类型可选传值或传指针;数组只能传指针(为效率),函数因此获得了修改原数据的能力。有时这正是想要的:
void add_to(double ar[], int n, double val)
{
int i;
for (i = 0; i < n; i++)
ar[i] += val;
}add_to(prices, 100, 2.50); 让 prices 每个元素加 2.5。但对只读函数来说,一个笔误(如 total += ar[i]++;)就可能毁掉原数据。对策:在原型和函数头中给形参加 const,即 int sum(const int ar[], int n);——此后 ar[i]++ 触发编译错误。注意:const 形参并不要求实参真是 const 数组,它只是约束这个函数别改数据,相当于给数组提供了按值传递式的保护。清单 10.14(arf.c)中 show_array() 只读数组故用 const,mult_array() 要改数组故不用——两者都是 void,mult_array() 靠指针而非 return 把新值送回 dip。
Success
原则一句话:函数要改数组,形参别加 const;函数不改数组,形参务必加 const。既防笔误破坏数据,也让函数能同时接受普通数组和 const 数组作实参。
关于 const 的更多内容
const 的位置不同,含义不同:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates; // 指向 const 的指针
*pd = 29.89; // 不允许:不能用 pd 改值
pd[2] = 222.22; // 不允许
rates[0] = 99.99; // 允许:rates 本身不是 const
pd++; // 允许:可以改指向赋值规则:const 或非 const 数据的地址都可赋给”指向 const 的指针”;但 const 数据的地址不能赋给普通指针——否则能借指针篡改 const 数据。推论:show_array()(const 形参)两种数组都能收;mult_array()(非 const 形参)不能收 const 数组。const 放在 * 之后(double * const pc = rates;)则锁死指向、仍可改值;两个 const 都上(const double * const pc)则指向与值都锁死。
10.10 指针与多维数组
以 int zippo[4][2]; 为例(4 个元素,每个是含 2 个 int 的数组):
- zippo 是首元素的地址,而首元素是”两 int 数组”,所以 zippo 指向一个数组型对象;zippo[0] 则指向单个 int(&zippo[0][0])。两者数值相同、类型不同。
- zippo + 1 地址加 8(2 个 int),zippo[0] + 1 地址只加 4(1 个 int)。
- zippo 是”地址的地址”——双重间接(double indirection),要解引用两次才得到 int 值:**zippo == zippo[0][0]。
清单 10.15(zippo1.c)打印验证上述关系(zippo、zippo[0]、*zippo 地址相同但步长不同;zippo[2][1] 与 ((zippo+2)+1) 相等)。逐步拆解等价表达式:
zippo → 首个"两 int 元素"的地址
zippo+2 → 第三个"两 int 元素"的地址
*(zippo+2) → 第三个元素(两 int 数组),即其首 int 的地址
*(zippo+2)+1 → 该数组中第二个 int 的地址
*(*(zippo+2)+1) → 第三行第二个 int 的值,即 zippo[2][1]
指向多维数组的指针:pointer-to-int 不行——那兼容 zippo[0],而 zippo 指向”两 int 数组”。正确写法是 int (* pz)[2];。括号必不可少:[] 优先级高于 *,int * pax[2]; 声明的是”两个 int 指针的数组”,加括号才是”指向两 int 数组的一个指针”。清单 10.16(zippo2.c)证明指针 pz 可完全像数组名一样使用:
zippo[m][n] == *(*(zippo + m) + n)
pz[m][n] == *(*(pz + m) + n)指针兼容性:指针间赋值比数值类型严格——x = n;(int→double)可隐式转换,pd = p1;(int*→double*)直接编译错误。规律:非法赋值都是两指针指向的类型不一致。以 int ar1[2][3]; int ar2[3][2]; int (*pa)[3]; int **p2; 为例:pt = ar1;(pt 是 int *)无效,pa = ar1; 有效,pa = ar2; 无效。const 的两层间接也不安全:pp2 = &p1;(const int ** 接 int **)被禁止——否则可经 *pp2 = &n;、*p1 = 10; 一路改掉真正的 const 数据。单层间接下”非 const 赋给 const”(p2 = p1;)则合法。
10.11 函数与多维数组
二维数组形参应声明为”指向行的指针”。以 int junk[3][4] 为例,junk 指向”含 4 个 int 的数组”,形参写法:
void somefunction( int (* pt)[4] ); // pt 是指针
void somefunction( int pt[][4] ); // 形参专用等价写法清单 10.17(array2d.c)的三个函数分别按行求和、按列求和、求总和,原型展示了三种等价语法:
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int[][COLS], int); // ok to omit names
int sum2d(int (*ar)[COLS], int rows); // another syntax列数内建于函数,行数经参数传入——同一函数可用于任何”4 列”数组。但 int sum2(int ar[][], int rows); 是错误声明:编译器要把 ar[1] 转换为 ar+1,必须知道 ar 指向的对象多大,空括号无从算起。第一对括号可写数字但会被忽略(方便 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); // 合法,3 被忽略
int sum2(int ar[][4], int rows); // 标准形式一般规律:N 维数组形参必须给出除最左一维外的所有维度——最左括号标识指针,其余括号描述所指对象,如 int sum4d(int ar[][12][20][30], int rows); 等价于 int sum4d(int (*ar)[12][20][30], int rows);。
10.12 变长数组(VLAs)
经典 C 的痛点:行数能通过参数传入,列数却写死在函数里,想算 6×5 数组就得再写一个函数。C99 的**变长数组(VLA)**允许用变量作维度:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // a VLA限制:必须是自动存储类(函数内或作形参);不能在声明时初始化。注意 VLA 的”变长”指创建时尺寸可由变量指定,不是创建后还能改大小——VLA 一旦创建尺寸就固定;C99 刚发布时各家编译器支持并不齐全。
带 VLA 参数的函数(清单 10.18,vararr2d.c 的核心):
int sum2d(int rows, int cols, int ar[rows][cols]); // ar a 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;
}rows、cols 必须在 ar 之前声明(ar 的声明用到了它们),顺序颠倒就错;原型省略参数名时用星号代替维度:int sum2d(int, int, int ar[*][*]);。这个 sum2d() 通吃传统数组和 VLA、任意行列组合。注意:参数表中的 VLA 声明并不创建数组——ar 仍是指针,函数操作的仍是原数组;函数体内 int temp[n][m]; 才是真正的 VLA。VLA 还意味着动态内存分配(运行期定大小),传统数组则是编译期定死的静态分配,第 12 章展开。
Success
VLA 形参让”一个函数处理任意尺寸的二维数组”从别扭的技巧变成一行声明,这是 C99 为接替 FORTRAN 数值计算库而设计的关键特性。
10.13 复合字面量(Compound Literals)
给 int 形参可传常量 5;C99 之前数组却没有对应的”数组常量”。复合字面量(compound literal) 补上了:类型名 + 括号包裹的初始化列表。
int diva[2] = {10, 20}; // 普通数组声明
(int [2]){10, 20} // 复合字面量:匿名的两 int 数组
(int []){50, 20, 90} // 省略大小,编译器数出 3 个复合字面量匿名,必须创建时即用。两种典型用法(清单 10.19,flc.c):
int * pt1;
int (*pt2)[COLS];
pt1 = (int [2]) {10, 20}; // 用指针记住位置,*pt1 是 10
pt2 = (int [2][COLS]) { {1,2,3,-9}, {4,5,6,-8} };
total1 = sum(pt1, 2); // 30
total2 = sum2d(pt2, 2); // 4
total3 = sum((int []) {4,4,4,5,5,5}, 6); // 27
复合字面量像数组名一样代表首元素地址,能赋给匹配类型的指针、直接传给匹配的函数——**不建数组就把一批值传给函数**,这是它最典型的用途。
## 要点回顾
- 数组下标从 0 到 n-1;越界与否全靠自律,编译器不检查。部分初始化补 0、全不初始化是垃圾值、列表超长是错误;`sizeof days / sizeof days[0]` 自动求元素个数。
- 数组名 = 首元素地址,`ar[i] ≡ *(ar + i)`;指针 +1 跨过一个元素的字节数;数组传参传地址,形参 `int ar[]` 与 `int * ar` 在函数头中同义。
- 不改数组的函数,形参加 const——保护数据还兼容 const 实参;const 位置不同,锁"值"还是锁"指向"不同。
- 二维数组形参必须写明除第一维外的所有维度;`int (*pz)[2]` 是指向"两 int 数组"的指针,别与 `int *pax[2]`(指针数组)混淆。
- C99 新特性:指定初始化器 `[5] = 212`、VLA 让维度可用变量、复合字面量让匿名数组直接当实参。
> [!question]- 自测题
>
> 1. `int ref[] = {8, 4, 0, 2};` 配合 `for (index = 0, ptr = ref; index < 4; index++, ptr++) printf("%d %d\n", ref[index], *ptr);` 打印什么?ref 有几个元素?
> 2. ref 是什么的地址?ref + 1 呢?
> 3. `float a5[0];` 为什么非法?`float a8[n];`(n 是 int 变量)在 C99 前后各是什么情况?
> 4. 写出下列变量的声明:a) digits,含 10 个 int 的数组;b) mat,含 3 个"含 5 个 int 的数组"的数组;c) psa,含 20 个 char 指针的数组;d) pstr,指向"含 20 个 char 的数组"的指针。
> 5. 为什么 `int sum2(int ar[][], int rows);` 是错误的声明?该怎么改?
>
> **答案解析**
> 1. 每行打印两个相同的数:8 8、4 4、0 0、2 2。ref 由初始化列表自动定长,共 4 个元素。
> 2. ref 是首元素 ref[0](类型 int)的地址;ref + 1 是 ref[1] 的地址(地址值增加一个 int 的字节数)。
> 3. 数组大小必须大于 0,0 元素数组非法;a8[n] 在 C99 前非法,C99 起合法,属于变长数组 VLA(不能在声明时初始化)。
> 4. a) `int digits[10];`;b) `int mat[3][5];`;c) `char * psa[20];`;d) `char (*pstr)[20];`(c 与 d 的区别:括号决定 * 先结合还是 [] 先结合)。
> 5. 编译器处理 ar[r][c] 时要按 ar + r 定位第 r 行,但第二维为空意味着它不知道每行含多少个 int,无法计算偏移。应补上列数,如 `int sum2(int ar[][4], int rows);` 或 `int sum2(int (*ar)[4], int rows);`。