0. 前言
本文将深入浅出地讲述数组与指针之间的共性与关联,适合有一定 C/C++ 功底的同学进阶学习。文中的程序均在 64 位环境下运行,且程序运行的结果会以注释的方式呈现在代码中以便阅读。
全文学习约需 15 分钟。
1. 数组和指针的本质
数组类型和指针类型都是 C 的特殊数据类型,这里的“特殊”是相对于整型、浮点型等基本数据类型来说的。
一般地,数组类型变量统称为数组,指针类型变量统称为指针。
数组和指针均不能单独被定义,即不存在纯数组类型变量或纯指针类型变量。两者定义时除显式为数组或指针外,还必须显式一种基本数据类型。例如以下定义的数组和指针都属于整型类型:
int nums[] = {
0, 1, 2};
int* p = &nums;
数组和指针两者的本质如下:
- 数组为一组相同数据类型的元素的集合,数组大小等于元素大小乘以元素个数,可以使用运算符“[]”从下标 0 开始访问数组中的元素。
- 指针为存储某一地址的变量,指针大小为一个位宽内存大小(例如 32 位机上为 4 字节)。通过指针能够访问该内存地址,并能够按指针所属的数据类型对以该地址起始的内存进行解析,俗称指针指向内存。
请牢记数组和指针的本质定义,这是彻底理解数组与指针两者之间的共性与关联的关键。
2. 数组的基础语法
先回顾一下数组的基础语法。
现有以下一维数组:
int nums[2] = {
4, 5};
变量名称为 nums
,所属类型为整型数组即 int[2]
,这里的“2”指明了该数组中有两个整型元素;{4, 5}
为元素列表,表示使用列表中的元素初始化数组;使用“[]”从下标 0 开始访问数组所包含的元素,nums[0]
所指代的元素为“4”,nums[1]
所指代的元素为“5”。
定义数组时存在如下规则:
- 当数组的元素个数 N 大于元素列表中元素的个数 M 时,将会在元素列表的尾部补充 (N - M) 个“0”。例如
int nums[3] = {1};
等效于int nums[3] = {1, 0, 0};
。因此,在定义数组时常见情况是元素列表为{0}
,表示数组的所有元素都初始化为“0”。- 当数组的元素个数 N 小于元素列表中元素的个数 M 时,编译器(GCC)会发出警告,并只使用元素列表的前 N 个元素初始化数组。例如
int nums[2] = {1, 2, 3};
等效于int nums[2] = {1, 2};
。- 当省略数组的元素个数时,编译器会根据元素列表的元素个数自推导出数组的元素个数。例如
int nums[] = {1, 2};
,编译器将自推导为int nums[2] = {1, 2};
。- 元素列表可以使用“""”括起来,表示该数组为字符串,即数组的每个元素都为字符型,并会在字符串结尾隐式地添加字符
'\0'
作为字符串的结尾。例如char str[] = "123";
等效于char str[] = {'1', '2', '3', '\0'};
。
二维数组是在一维数组的基础上进行再集合,简单来说就是二维数组的每个元素都是一维数组,且这些子数组具备相同的数据类型、元素个数。
以整型二维数组为例,其采用矩阵方式呈现如下:
int nums[3][3] = {
{
1, 2, 3},
{
4, 5, 6},
{
7, 8, 9}
};
变量名称为 nums
,所属类型为整型二维数组即 int[3][3]
,第一个“3”指明该二维数组有 3 个数组元素即有 3 个一维数组,第二个“3”指明每个子数组的元素个数为 3 个。
使用“[][]”从下标 0 开始访问数组所包含的元素,第一个“[]”将决定访问第几个子数组,第二个“[]”将决定访问子数组的第几个元素。例如,nums[0][1]
所指代的元素为第一个子数组的第二个元素“2”,nums[1][0]
所指代的元素为第二个子数组的第一个元素“4”,依次类推。
在定义二维数组时,仍遵守以上定义数组的规则,但此时编译器不会根据子数组的元素列表的元素个数自推导出子数组的元素个数。简单来说,第一个“[]”能够不显式指明数组元素即子数组的个数,但第二个“[]”必须显式指明子数组中元素的个数。
还需要说明的是,定义数组时,元素列表中表示子数组的 {}
其实是非必须的,花括号只是提高程序阅读性的技巧。如以下定义二维数组仍是合法的:
int nums[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
对于三维数组、四维数组等多维数组,其原理与二维数组相同,均为对上一维数数组的再集合。在工程中一般至多使用二维数组,因为数组每增加一维其实际元素个数将增长数组元素个数的倍数,其所消耗的内存空间是非常巨大的。
3. 指针和数组的运算
在讲述数组与指针之间的共性前,需先了解关于数组和指针的运算。
指针存储着内存中的某个地址,当对任意数据类型的指针进行加减运算时,实际上都是对该地址值进行运算,俗称指针偏移。
那么,指针偏移的基本单位是多少呢?实践出真理:
int nums[] = {
1, 2, 3};
int* p = &nums[0];
printf("p = %p\n", p); // print: p = 0x7ffc3836cee0
printf("p + 1 = %p\n", p + 1); // print: p + 1 = 0x7ffc3836cee4
printf("*(p + 1) = %d\n", *(p + 1)); // print: *(p + 1) = 2
可见,p
与 p + 1
的差值为 4 字节,刚好一指针所属的整型类型的大小,p + 1
为返回 p
指向地址偏移 4 字节后的地址。
实际上,指针偏移的基本单位为指针所属的数据类型大小,指针的加减运算为指针所存储的地址加减所运算的 n 个基本单位的字节。简单来说,如上的 p + 1
等价于 (size_t)p + 1 * sizeof(int)
。这里强制转化为 size_t
表示为内存地址类型。
在程序中,当数组名称单独出现时,其值是数组的起始地址,该地址也是第一个元素的起始地址。例如:
int nums[] = {
1, 2, 3};
printf("nums = %p\n", nums); // print: nums = 0x7ffeedc61450
printf("&nums = %p\n", &nums); // print: &nums = 0x7ffeedc61450
printf("&nums[0] = %p\n", &nums[0]); // print: &nums[0] = 0x7ffeedc61450
那么,对数组直接进行加减运算,会得到什么呢?
实践是检验真理的唯一标准:
int nums[] =