C程序中的数组与指针共生关系
在C语言的编程世界中,处理大量同类型的数据是程序员不可避免的任务。无论是统计日降雨量、管理库存,还是记录客户交易,我们都需要一种高效的方式来组织这些相关数据。数组,作为相同数据类型元素的集合,正是为此而生。然而,要真正掌握C语言的精髓,就必须深入理解数组背后更本质的概念------指针。这篇文章将深度剖析数组与指针之间密不可分、甚至可以说是一体两面的共生关系,揭示C语言高效和强大的根源。
数组:数据的有序集合
创建数组时,我们必须明确告知编译器两件事:元素的类型和数量。编译器会据此在内存中开辟一块连续的空间。访问数组中的元素,我们通过下标(或称索引)来实现,这个编号从0开始。
初始化与声明
初始化数组最直接的方式是提供一个值的列表,用花括号 {}
包围。
c
/* day_mon1.c -- 打印每个月的天数 */
#include <stdio.h>
#define MONTHS 12
int main(void)
{
// 使用花括号和逗号分隔的列表来初始化数组
const 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;
}
代码描述 :此程序定义了一个包含12个整数的常量数组 days
,用于存储每个月的天数。const
关键字确保这个数组在程序运行期间不会被意外修改,这是一种良好的编程实践,用于保护关键数据。for
循环遍历数组,通过下标 days[index]
访问每个元素并打印出来。
C语言在初始化方面提供了相当的灵活性。如果初始化列表中的项数少于数组的元素个数,编译器会自动将剩余的元素初始化为0。这是一个非常重要的特性,可以避免未初始化数组中存在的"垃圾值"。
c
/* some_data.c -- 部分初始化数组 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
int some_data[SIZE] = { 1492, 1066 }; // 只提供了2个初始值
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;
}
代码描述 :some_data
数组的大小为4,但只初始化了前两个元素。运行此程序,你会发现 some_data[2]
和 some_data[3]
的值都是0,这是编译器自动完成的。
C99标准引入了"指定初始化器",允许我们初始化指定的数组元素,而不必按顺序进行。
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("%d %d\n", i + 1, days[i]);
return 0;
}
代码描述:这段代码展示了指定初始化器的两个重要特性:
- 顺序填充 :
[4] = 31
初始化了第5个元素(下标为4)后,紧随其后的30
和31
会被用来初始化后续的元素,即days[5]
和days[6]
。 - 最终赋值有效 :
days[1]
(下标为1的元素)最初被初始化为28
,但随后又被[1] = 29
重新赋值,因此最终它的值是29
。未被显式初始化的元素,如days[2]
和days[3]
,会被自动初始化为0。
指针:访问数据的另一种途径
指针提供了一种以符号形式使用内存地址的方法。它与数组的关系,是C语言强大与高效的根源之一。最核心的概念是:数组名是该数组首元素的地址。
这意味着,如果 flizny
是一个数组,那么 flizny
和 &flizny[0]
在值上是完全等价的。这个地址是一个常量,不能被修改。但我们可以将它赋值给一个指针变量,然后通过操作指针来访问数组。
c
// pnt_add.c -- 指针地址
#include <stdio.h>
#define SIZE 4
int main(void)
{
short dates[SIZE];
double bills[SIZE];
short * pti;
double * ptf;
pti = dates; // 把数组地址赋给指针
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (int index = 0; index < SIZE; index++)
printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);
return 0;
}
代码描述 :该程序声明了一个 short
型指针 pti
和一个 double
型指针 ptf
,并分别将 dates
和 bills
数组的地址赋给它们。循环打印 指针 + 整数
的结果。你会发现 pti
每加1,其地址值增加2(sizeof(short)
);而 ptf
每加1,地址值增加8(sizeof(double)
)。这揭示了指针运算的本质:在C中,指针加1,指的是增加一个存储单元。对数组而言,这意味着地址会移动到下一个元素的地址,而不是下一个字节的地址。
这种关系使得数组表示法和指针表示法可以互换。C语言标准明确定义 ar[n]
的意思就是 *(ar + n)
。
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++)
// *(days + index) 与 days[index] 相同
printf("Month %2d has %d days.\n", index + 1, *(days + index));
return 0;
}
代码描述 :这个程序使用 *(days + index)
来获取数组元素的值,其中 days + index
计算出第 index
个元素的地址,*
运算符则解引用该地址以获取存储的值。其功能和输出与使用 days[index]
的版本完全一致,有力地证明了二者的等效性。
协同工作:函数中的数组与指针
将数组传递给函数时,这种指针与数组的等价性变得至关重要。你无法将整个数组作为参数传递给函数,因为这涉及到庞大的数据拷贝,效率极低。实际上传递的,仅仅是数组的地址------一个指向其首元素的指针。
因此,一个接收数组的函数,其形参实际上是一个指针。int *ar
和 int ar[]
这两种写法在作为函数形参时是完全等价的。
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 };
printf("The size of marbles is %zd bytes.\n", sizeof marbles);
sum(marbles, SIZE);
return 0;
}
int sum(int ar[], int n)
{
printf("The size of ar is %zd bytes.\n", sizeof ar);
// ... sum calculation ...
}
代码描述 :此代码的关键在于 sizeof
运算符的结果。在 main
函数中,sizeof marbles
会返回 10 * sizeof(int)
,即40字节(假设 int
为4字节)。然而,在 sum
函数内部,sizeof ar
返回的是指针的大小(在64位系统上通常是8字节),而不是整个数组的大小。这清晰地证明了传递给函数的是地址,而非数组实体。
为了保护传入函数的数据不被意外修改,我们可以使用 const
关键字。
c
// arf.c -- 处理数组的函数
void show_array(const double ar[], int n);
void mult_array(double ar[], int n, double mult);
代码描述 :在 show_array
函数中,ar
被声明为 const
,这意味着函数内部任何试图修改 ar
指向的数据(如 ar[i] = 0;
)的操作都会导致编译错误。而 mult_array
函数需要修改数组内容,因此其参数 ar
没有 const
限定。
深入多维数组
对于二维数组,例如 int zippo[4][2]
,我们可以将其理解为"数组的数组"。这里的指针关系变得更加微妙:zippo
是指向"包含2个int的数组"的指针,而 zippo[0]
是指向 int
的指针。
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 = %d\n", **zippo);
printf(" zippo[2][1] = %d\n", zippo[2][1]);
printf("*(*(zippo+2)+1) = %d\n", *(*(zippo + 2) + 1));
return 0;
}
代码描述:这段代码是理解多维数组与指针关系的关键。
zippo
和zippo[0]
的地址值相同,但zippo + 1
的地址偏移了8字节(一行的大小),而zippo[0] + 1
只偏移了4字节(一个元素的大小)。*zippo
解引用了行指针,其结果是第一行的地址,所以*zippo
和zippo[0]
的值相同。**zippo
是对行指针解引用再对元素指针解引用,最终得到第一个元素zippo[0][0]
的值。*(*(zippo+2)+1)
通过指针运算精确地定位到了zippo[2][1]
。
要声明一个能指向 zippo
这种二维数组的指针,必须使用 int (* pz)[2];
语法。在将二维数组传递给函数时,这个概念尤为重要,函数的形参必须能匹配传入的实参类型,且必须指明除第一维之外的所有维度的大小。
c
// array2d.c -- 处理二维数组的函数
#define ROWS 3
#define COLS 4
void sum_cols(int ar[][COLS], int rows); // COLS是必须的
int main(void) {
int junk[ROWS][COLS] = { ... };
sum_cols(junk, ROWS);
}
代码描述 :sum_cols
函数的声明 int ar[][COLS]
告诉编译器 ar
是一个指针,它指向一个包含 COLS
(即4)个 int
元素的数组。编译器需要 COLS
这个信息来正确计算 ar[r][c]
的内存地址。
C99/C11 的现代特性
变长数组 (VLA)
C99标准引入了变长数组,允许使用变量来定义数组的维度,极大地增强了代码的通用性。
c
// vararr2d.c -- 使用变长数组的函数
#include <stdio.h>
int sum2d(int rows, int cols, int ar[rows][cols]); // VLA作为函数形参
int main(void)
{
int junk[3][4] = { ... };
int varr[3][10]; // VLA声明
// ...
sum2d(3, 4, junk);
sum2d(3, 10, varr);
return 0;
}
代码描述 :通过VLA,sum2d
函数现在可以处理任意行列的二维 int
数组。注意,在函数原型和定义中,维度变量 rows
和 cols
必须在数组 ar
之前声明。
复合字面量
C99的另一个创新是复合字面量,它允许创建匿名的、临时的数组常量,常用于函数调用。
c
// flc.c -- 有趣的常量
#include <stdio.h>
int sum(const int ar[], int n);
int sum2d(const int ar[][4], int rows);
int main(void)
{
int total1, total2, total3;
int *pt1 = (int[2]){10, 20}; // 创建匿名数组并存储地址
total1 = sum(pt1, 2);
total2 = sum2d((int[2][4]){{1,2,3,-9},{4,5,6,-8}}, 2); // 直接传递匿名二维数组
total3 = sum((int[]){4,4,4,5,5,5}, 6); // 省略大小的匿名数组
// ...
return 0;
}
代码描述 :该代码展示了复合字面量的三种用法。pt1
指向一个在代码中直接定义的匿名数组。sum2d
和 sum
的调用则直接将复合字面量作为实参传递,避免了预先声明变量的需要,使代码更加紧凑。
附录:精选代码解读
1. ptr_ops.c
--- 指针操作的权威展示
这个程序是理解指针所有基本操作的绝佳范例。让我们逐一分析。
c
// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{
int urn[5] = {100, 200, 300, 400, 500};
int * ptr1, *ptr2, *ptr3;
ptr1 = urn; // 操作1: 赋值
ptr2 = &urn[2]; // 也是赋值
// 打印初始状态
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
ptr3 = ptr1 + 4; // 操作2: 指针加法
printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
ptr1++; // 操作3: 递增指针
printf("ptr1 after ptr1++: %p, *ptr1 = %d\n", ptr1, *ptr1);
ptr2--; // 操作4: 递减指针
printf("ptr2 after ptr2--: %p, *ptr2 = %d\n", ptr2, *ptr2);
// 恢复ptr1和ptr2
--ptr1;
++ptr2;
printf("ptr2 - ptr1 = %td\n", ptr2 - ptr1); // 操作5: 指针求差
printf("ptr3 - 2 = %p\n", ptr3 - 2); // 操作6: 指针减整数
return 0;
}
深度解读:
- 赋值 :
ptr1 = urn;
将数组urn
的首地址赋给ptr1
。ptr2 = &urn[2];
将第3个元素的地址赋给ptr2
。 - 解引用与取址 :
*ptr1
得到ptr1
指向地址的值(100)。&ptr1
得到指针变量ptr1
自身的内存地址,它与urn
的地址是完全不同的。 - 指针加法 :
ptr1 + 4
计算的是ptr1
的地址加上4 * sizeof(int)
。结果是指向urn[4]
的地址。*(ptr1 + 4)
自然就是urn[4]
的值(500)。 - 递增/递减 :
ptr1++
是一个副作用操作,它修改了ptr1
自身的值,使其指向下一个元素urn[1]
。ptr2--
同理,使其从指向urn[2]
变为指向urn[1]
。 - 指针求差 :
ptr2 - ptr1
计算的是两个地址之间相隔多少个元素 。在恢复后,ptr1
指向urn[0]
,ptr2
指向urn[2]
,它们之间相隔2个int
元素,所以结果是2,而不是地址的字节差。 - 指针减整数 :
ptr3 - 2
与指针加法类似,计算结果是一个新的地址,即ptr3
的地址减去2 * sizeof(int)
。因为ptr3
指向urn[4]
,所以结果是urn[2]
的地址。
2. zippo
数组 --- *(*(zippo+2)+1)
的逐步解析
这个表达式是理解多维数组指针表示法的试金石。让我们以 int zippo[4][2]
为例,一步步拆解它如何等价于 zippo[2][1]
。
-
zippo
:- 含义: 二维数组的名称。
- 类型 :
int (*)[2]
,即一个指向"包含2个int
的数组"的指针。 - 值 : 整个数组的起始地址,数值上等于
&zippo[0]
。
-
zippo + 2
:- 含义: 对指向行的指针进行算术运算。
- 计算 :
zippo
的地址值加上2 * sizeof(int[2])
。sizeof(int[2])
是8字节(假设int
为4字节),所以地址增加了16字节。 - 结果 :
&zippo[2]
,即第3行的起始地址。
-
*(zippo + 2)
:- 含义: 解引用行指针。
- 计算 : 获取
&zippo[2]
地址处存储的值。由于zippo + 2
是一个指向数组的指针,解引用它得到的是那个数组本身。在C中,数组名即其首元素地址。 - 结果 :
zippo[2]
,它是一个一维数组。其值等于&zippo[2][0]
,即第3行第1个元素的地址。 - 类型 :
int *
,即一个指向int
的指针。
-
*(zippo + 2) + 1
:- 含义: 对指向元素的指针进行算术运算。
- 计算 :
&zippo[2][0]
的地址值加上1 * sizeof(int)
,即4字节。 - 结果 :
&zippo[2][1]
,即第3行第2个元素的地址。
-
*(*(zippo + 2) + 1)
:- 含义: 解引用最终的元素指针。
- 计算 : 获取
&zippo[2][1]
地址处存储的值。 - 结果 :
zippo[2][1]
的值。