第 11 章:指针与数组
指针和数组是 C 语言中最紧密相关的核心概念,二者相辅相成。数组名在大多数场景下会被编译器解析为指向首元素的指针,而指针通过算术运算可像数组一样访问内存。这种关联性带来了极高的灵活性,让批量数据处理更高效,但也容易因概念混淆引发错误 ------ 理解二者的本质差异与共性,是掌握 C 语言的关键一步。
11.1 数组名的本质
数组名的核心特性:在大多数表达式中,数组名会自动转换为指向数组首元素的常量指针 (即&数组名[0]),但数组名并非普通指针,它是 "不可修改的指针常量"。
11.1.1 数组名作为指针常量
数组名的 "常量属性" 是与普通指针变量的核心区别:
- 数组名指向固定(首元素地址),不能作为左值修改指向(如
arr = NULL编译错误); - 普通指针变量是变量,可随时修改指向(如
ptr = NULL合法); - 数组名可直接赋值给指针变量(本质是传递首元素地址)。
示例代码:
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 数组名与首元素地址完全等价
printf("数组名arr: %p\n", arr);
printf("首元素地址&arr[0]: %p\n", &arr[0]);
printf("arr == &arr[0] ? %s\n", arr == &arr[0] ? "是" : "否");
// 数组名是指针常量,不能修改指向(编译错误)
// arr = NULL; // 错误:数组名不能作为左值
// 普通指针变量可修改指向
int *ptr = arr; // 正确:ptr接收arr的首元素地址
ptr = NULL; // 正确:指针变量可修改指向
return 0;
}
运行结果:
bash
数组名arr: 0061FF08
首元素地址&arr[0]: 0061FF08
arr == &arr[0] ? 是
--------------------------------
Process exited after 0.05777 seconds with return value 0
请按任意键继续. . .
11.1.2 sizeof 运算符的关键差异(数组名 vs 指针)
sizeof 是区分数组名和指针的 "试金石",核心差异源于二者的本质不同:
- 数组名用 sizeof:计算整个数组的总字节数(元素个数 × 单个元素字节数);
- 指针变量用 sizeof:计算指针变量本身的字节数(与指向类型无关,通常为 4 字节(32 位)或 8 字节(64 位))。
示例代码:
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指针指向数组首元素
printf("数组arr的大小: %zu 字节\n", sizeof(arr)); // 5×4=20字节(int占4字节)
printf("指针ptr的大小: %zu 字节\n", sizeof(ptr)); // 8字节(64位系统)或4字节(32位)
printf("int的大小: %zu 字节\n", sizeof(int)); // 4字节(单个元素大小)
// 正确计算数组元素个数:总大小 ÷ 单个元素大小
printf("数组元素个数: %zu\n", sizeof(arr) / sizeof(arr[0]));
// 错误:指针sizeof无法计算数组元素个数(ptr是指针变量,不是数组)
// printf("指针元素个数: %zu\n", sizeof(ptr) / sizeof(ptr[0]));
return 0;
}
运行结果(64 位系统):
bash
数组arr的大小: 20 字节
指针ptr的大小: 8 字节
int的大小: 4 字节
数组元素个数: 5
--------------------------------
Process exited after 0.0503 seconds with return value 0
请按任意键继续. . .
11.2 指针运算与数组访问
指针算术运算的核心特性:运算步长由指针指向的类型决定 (如int*步长 4 字节,char*步长 1 字节),这使得指针能像数组一样高效访问元素,甚至更灵活。
11.2.1 指针算术运算(+、-、++、--)
指针支持的算术运算及规则:
- 指针 + 整数:指针指向的地址向后移动 "整数 × 步长" 字节(如
int* p+2移动 8 字节); - 指针 - 整数:指针指向的地址向前移动 "整数 × 步长" 字节;
- 指针 ++/--:指针自增 / 自减 1(步长由类型决定);
- 同类型指针相减:结果为两个指针之间的 "元素个数"(而非字节数),用于计算数组元素间隔。
示例代码:
c
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers;
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("初始指针位置: %p -> %d\n", ptr, *ptr); // 指向首元素10
// 指针+2:移动2个int(8字节),指向30
ptr = ptr + 2;
printf("+2后指针位置: %p -> %d\n", ptr, *ptr);
// 指针-1:移动1个int(4字节),指向20
ptr = ptr - 1;
printf("-1后指针位置: %p -> %d\n", ptr, *ptr);
// 指针++:自增1,指向30
ptr++;
printf("递增后位置: %p -> %d\n", ptr, *ptr);
// 指针--:自减1,指向20
ptr--;
printf("递减后位置: %p -> %d\n", ptr, *ptr);
// 指针相减:计算元素个数
int *start = numbers;
int *end = numbers + size;
printf("指针距离: %td 个元素\n", end - start); // 输出5(间隔5个元素)
return 0;
}
11.2.2 数组索引与指针运算的等价性
核心结论:对于数组arr和指针ptr=arr,arr[i]与\*(arr+i)、ptr[i]与\*(ptr+i)完全等价。
等价性原理:数组索引本质是 "指针算术运算 + 解引用"------arr[i]表示 "从数组首地址移动 i 个元素,再解引用访问值",底层与指针运算逻辑一致。
示例代码:
c
#include <stdio.h>
int main() {
int arr[5] = {100, 200, 300, 400, 500};
int *ptr = arr;
printf("各种访问方式的等价性:\n");
// 数组名的等价访问(arr是常量指针)
printf("arr[2] = %d\n", arr[2]); // 传统索引:300
printf("*(arr + 2) = %d\n", *(arr + 2));// 指针运算:300
printf("*(2 + arr) = %d\n", *(2 + arr));// 加法交换律:300
printf("2[arr] = %d\n", 2[arr]); // 语法允许(不推荐):300
// 指针变量的等价访问
printf("ptr[2] = %d\n", ptr[2]); // 指针用索引:300
printf("*(ptr + 2) = %d\n", *(ptr + 2));// 指针运算:300
return 0;
}
运行结果:
bash
各种访问方式的等价性:
arr[2] = 300
*(arr + 2) = 300
*(2 + arr) = 300
2[arr] = 300
ptr[2] = 300
*(ptr + 2) = 300
--------------------------------
Process exited after 0.0512 seconds with return value 0
请按任意键继续. . .
注意:2[arr]语法合法但不推荐,可读性极差,实际开发中应使用传统索引或指针运算。
11.3 指针与多维数组
多维数组(如二维数组)的本质是 "数组的数组",其内存是连续存储的。指针访问多维数组的核心是理解 "行指针" 和 "元素指针" 的区别。
11.3.1 二维数组的内存布局
关键特性:二维数组在内存中按 "行优先" 顺序连续存储,无额外间隔。例如int matrix[3][4]的内存顺序为:matrix[0][0]→matrix[0][1]→...→matrix[0][3]→matrix[1][0]→...→matrix[2][3]。
地址差异核心:
matrix:指向第一行的 "行指针"(类型为int (*)[4]),matrix+i表示指向第 i 行,地址跳4×4=16字节(一行 4 个 int);matrix[0]:第一行的数组名,退化为指向首元素的 "元素指针"(类型为int*),matrix[0]+j表示指向第 0 行第 j 个元素,地址跳 4 字节。
示例代码:
c
#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printf("二维数组的内存布局分析:\n");
printf("整个数组大小: %zu 字节\n", sizeof(matrix)); // 3×4×4=48字节
printf("一行大小: %zu 字节\n", sizeof(matrix[0])); // 4×4=16字节
printf("一个元素大小: %zu 字节\n", sizeof(matrix[0][0])); // 4字节
printf("\n地址分析:\n");
printf("matrix: %p\n", matrix); // 行指针:指向第0行
printf("matrix[0]: %p\n", matrix[0]); // 元素指针:指向第0行第0列
printf("&matrix[0][0]: %p\n", &matrix[0][0]); // 元素地址:与matrix[0]相同
printf("matrix + 1: %p\n", matrix + 1); // 跳一行(16字节)
printf("matrix[0] + 1: %p\n", matrix[0] + 1); // 跳一个元素(4字节)
return 0;
}
11.3.2 遍历二维数组的 3 种方法
基于内存布局和指针特性,提供 3 种遍历方式,适用于不同场景:
c
#include <stdio.h>
#define ROWS 3
#define COLS 4
int main() {
int matrix[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方法1:传统双重循环(数组索引,最直观)
printf("方法1: 传统双重循环\n");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf("%2d ", matrix[i][j]);
}
printf("\n");
}
// 方法2:使用行指针(指向一行的指针)
printf("\n方法2: 使用行指针\n");
int (*row_ptr)[COLS] = matrix; // 行指针,指向包含COLS个int的数组
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf("%2d ", row_ptr[i][j]); // 等价于 (*(row_ptr+i))[j]
}
printf("\n");
}
// 方法3:使用元素指针(逐个访问所有元素,利用连续内存)
printf("\n方法3: 使用元素指针\n");
int *elem_ptr = &matrix[0][0]; // 元素指针,指向首元素
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf("%2d ", *(elem_ptr + i * COLS + j)); // 计算偏移量
}
printf("\n");
}
return 0;
}
运行结果:
bash
方法1: 传统双重循环
1 2 3 4
5 6 7 8
9 10 11 12
方法2: 使用行指针
1 2 3 4
5 6 7 8
9 10 11 12
方法3: 使用元素指针
1 2 3 4
5 6 7 8
9 10 11 12
--------------------------------
Process exited after 0.0523 seconds with return value 0
请按任意键继续. . .
11.4 指针数组 vs 数组指针(核心区分)
二者是 C 语言高频混淆点,核心差异在于 "优先级" 和 "本质":指针数组是 "数组",数组指针是 "指针",可通过声明语法和用途快速区分。
11.4.1 指针数组(数组里装指针)
声明语法:数据类型 *数组名[数组大小](如char *names[4])
本质:一个数组,每个元素都是指针变量(如names[0]是char*类型)。
核心特点:数组大小由元素个数决定,每个元素可指向不同的对象(如不同字符串)。
示例代码(字符串指针数组):
c
#include <stdio.h>
#include <string.h>
int main() {
// 指针数组:每个元素是char*(指向字符串的指针)
char *names[] = {"Alice", "Bob", "Charlie", "David"};
int count = sizeof(names) / sizeof(names[0]); // 数组大小4(4个指针)
printf("指针数组示例:\n");
for (int i = 0; i < count; i++) {
printf("names[%d] = %s (指针地址: %p, 字符串地址: %p)\n",
i, names[i], &names[i], names[i]);
}
// 修改指针数组的元素(切换指针指向,不修改字符串本身)
char new_name[] = "Eve";
names[1] = new_name; // names[1]现在指向new_name的地址
printf("\n修改后:\n");
for (int i = 0; i < count; i++) {
printf("names[%d] = %s\n", i, names[i]);
}
return 0;
}
运行结果:
bash
指针数组示例:
names[0] = Alice (指针地址: 0061FF00, 字符串地址: 00404000)
names[1] = Bob (指针地址: 0061FF08, 字符串地址: 00404006)
names[2] = Charlie (指针地址: 0061FF10, 字符串地址: 0040400A)
names[3] = David (指针地址: 0061FF18, 字符串地址: 00404012)
修改后:
names[0] = Alice
names[1] = Eve
names[2] = Charlie
names[3] = David
--------------------------------
Process exited after 0.0534 seconds with return value 0
请按任意键继续. . .
11.4.2 数组指针(指针指向数组)
声明语法:数据类型 (*指针名)[数组大小](如int (*arr_ptr)[5])
本质:一个指针,专门指向 "包含指定个数元素的数组"(如arr_ptr指向包含 5 个 int 的数组)。
核心特点:指针运算步长为整个数组的大小(如arr_ptr+1跳5×4=20字节)。
示例代码:
c
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
// 数组指针:指向包含5个int的数组(括号改变优先级)
int (*arr_ptr)[5] = &arr; // 必须取数组地址(&arr),不能直接写arr(退化为元素指针)
printf("数组指针示例:\n");
printf("arr_ptr(指针值): %p\n", arr_ptr);
printf("*arr_ptr(解引用→数组首元素地址): %p\n", *arr_ptr);
printf("arr(数组名→首元素地址): %p\n", arr);
// 通过数组指针访问元素(两种等价方式)
printf("\n通过数组指针访问元素:\n");
printf("(*arr_ptr)[0] = %d\n", (*arr_ptr)[0]); // 解引用指针→数组,再索引
printf("(*arr_ptr)[2] = %d\n", (*arr_ptr)[2]); // 访问第3个元素30
// 数组指针的运算(步长=数组大小)
printf("\n指针运算(步长=5×4=20字节):\n");
printf("arr_ptr: %p\n", arr_ptr);
printf("arr_ptr + 1: %p\n", arr_ptr + 1); // 地址+20字节
return 0;
}
运行结果:
bash
数组指针示例:
arr_ptr(指针值): 0061FF04
*arr_ptr(解引用→数组首元素地址): 0061FF04
arr(数组名→首元素地址): 0061FF04
通过数组指针访问元素:
(*arr_ptr)[0] = 10
(*arr_ptr)[2] = 30
指针运算(步长=5×4=20字节):
arr_ptr: 0061FF04
arr_ptr + 1: 0061FF18
--------------------------------
Process exited after 0.0517 seconds with return value 0
请按任意键继续. . .
核心区分表
| 特性 | 指针数组(如int *ptr_arr[5]) |
数组指针(如int (*arr_ptr)[5]) |
|---|---|---|
| 本质 | 数组(元素是指针) | 指针(指向数组) |
| 优先级 | [] > *,先识别数组 |
() > *,先识别指针 |
| 运算步长 | 指针类型步长(如int*→4 字节) |
数组大小步长(如 5×4=20 字节) |
| 常见用途 | 存储多个字符串、多组数据地址 | 遍历多维数组、传递二维数组 |
11.5 字符串与指针
C 语言中字符串本质是 "'\0'结尾的字符数组",字符指针是处理字符串的灵活方式,比字符数组更便于切换指向。
11.5.1 字符串常量与字符数组的指针差异
c
#include <stdio.h>
int main() {
// 字符串常量:存储在只读内存区,指针指向常量地址(不可修改内容)
char *str1 = "Hello, World!";
// 字符数组:存储在栈区/全局区,可修改内容
char str2[] = "Hello, World!";
printf("str1: %s (指针地址: %p, 字符串地址: %p)\n", str1, &str1, str1);
printf("str2: %s (数组地址: %p, 首元素地址: %p)\n", str2, str2, &str2[0]);
// 字符数组可修改内容
str2[0] = 'h';
printf("修改后str2: %s\n", str2); // 输出"hello, World!"
// 字符串常量不可修改(运行时错误,只读内存区保护)
// str1[0] = 'h';
// 指针可切换指向不同字符串常量(合法,修改的是指针指向,不是字符串本身)
str1 = "New string";
printf("切换指向后str1: %s\n", str1);
return 0;
}
11.5.2 字符串操作的底层是指针移动
标准库字符串函数(如strlen、strcpy)的底层的都是通过指针移动实现的,自定义实现可帮助理解核心逻辑:
c
#include <stdio.h>
#include <string.h>
// 自定义strlen:统计'\0'前的字符数(指针移动计数)
size_t my_strlen(const char *str) {
const char *ptr = str; // 临时指针,不修改原指针
while (*ptr != '\0') { // 指针移动直到遇到结束符
ptr++;
}
return ptr - str; // 指针相减=元素个数(字符数)
}
// 自定义strcpy:将src复制到dest(指针移动赋值)
char* my_strcpy(char *dest, const char *src) {
char *original_dest = dest; // 保存dest初始地址(需返回)
while ((*dest++ = *src++) != '\0'); // 逐字符复制,直到src的'\0'
return original_dest;
}
int main() {
char source[] = "Source string";
char destination[50];
// 测试自定义函数
printf("自定义字符串函数测试:\n");
printf("source长度: %zu\n", my_strlen(source)); // 输出13
my_strcpy(destination, source);
printf("复制结果: %s\n", destination); // 输出"Source string"
// 标准库函数示例
char str1[20] = "Hello";
char str2[20] = "World";
strcat(str1, " "); // 连接空格
strcat(str1, str2); // 连接str2
printf("\n标准库strcat结果: %s\n", str1); // 输出"Hello World"
return 0;
}
11.6 指针与数组作为函数参数
数组作为函数参数时,会自动退化为指向首元素的指针(即 "传地址"),因此函数内修改数组会影响原始数组;指针作为参数则直接传递地址,效果一致。
11.6.1 一维数组作为函数参数(3 种等价声明)
核心结论:一维数组作为参数时,以下 3 种声明完全等价,均退化为int*:
void func(int arr[])void func(int *arr)void func(int arr[5])(数组大小 5 被忽略,无意义)
注意:必须显式传递数组大小,函数内无法通过sizeof(arr)计算(arr 已退化为指针)。
示例代码:
c
#include <stdio.h>
// 3种等价声明(任选一种即可)
void print_array1(int arr[], int size);
void print_array2(int *arr, int size);
void print_array3(int arr[5], int size);
// 实现(统一用一种即可)
void print_array1(int arr[], int size) {
printf("数组元素: ");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]); // 等价于*(arr+i)
}
printf("\n");
}
// 修改数组(传地址,影响原始数组)
void modify_array(int *arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // 每个元素×2
}
}
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("原始数组: ");
print_array1(numbers, size);
modify_array(numbers, size); // 传数组名(退化为指针)
printf("修改后数组: ");
print_array2(numbers, size); // 用等价声明调用
return 0;
}
11.6.2 二维数组作为函数参数(必须指定列数)
二维数组退化为 "行指针"(如int (*)[COLS]),因此函数参数必须指定列数(编译器需计算行的大小),行数可省略。
3 种常见声明方式:
c
#include <stdio.h>
#define COLS 4 // 必须指定列数
// 方式1:数组表示法(最直观)
void print_matrix1(int matrix[][COLS], int rows) {
printf("方法1 - 数组表示法:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < COLS; j++) {
printf("%2d ", matrix[i][j]);
}
printf("\n");
}
}
// 方式2:行指针表示法(等价于方式1)
void print_matrix2(int (*matrix)[COLS], int rows) {
printf("方法2 - 指针表示法:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < COLS; j++) {
printf("%2d ", *(*(matrix + i) + j)); // 行指针+i→第i行,解引用→元素指针+j
}
printf("\n");
}
}
// 方式3:元素指针表示法(视为一维数组,需传行列数)
void print_matrix3(int *matrix, int rows, int cols) {
printf("方法3 - 一维数组处理:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%2d ", matrix[i * cols + j]); // 计算全局偏移量
}
printf("\n");
}
}
int main() {
int matrix[3][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print_matrix1(matrix, 3);
printf("\n");
print_matrix2(matrix, 3);
printf("\n");
print_matrix3(&matrix[0][0], 3, COLS); // 传首元素地址
return 0;
}