引言:为什么需要理解数组与指针的差异?
在C语言中,数组和指针是最基础且容易混淆的概念。尤其是*p[]
(指针数组)和(*p)[]
(数组的数组)的语法差异,涉及类型优先级、内存布局和操作方式的本质区别。本文通过具体代码示例,结合fruits1
(二维数组)和fruits2
(指针数组)的对比,深入解析两者的核心差异,并探讨实际开发中的应用场景。
核心概念:*p[]
与(*p)[]
的类型优先级
C语言中,运算符优先级决定了表达式的解析顺序。其中,[]
(下标运算符)的优先级高于*
(解引用运算符)。因此:
*p[]
会被解析为*(p[])
,即数组的指针(指针数组):数组的每个元素是指针;
(*p)[]
会被解析为(*p)[]
,即数组的数组(二维数组):数组的每个元素是另一个数组。
用户代码中的fruits1
和fruits2
正是这两种类型的典型代表:
1 2
| char fruits1[][10] = { "apple", "banana", "cherry" }; // 二维数组(数组的数组) char *fruits2[] = { "apple","banana","cherry" }; // 指针数组(数组的指针)
|
代码逐行解析:从定义到操作的完整流程
1. 数据定义:二维数组 vs 指针数组
fruits1
:二维数组(数组的数组)
1
| char fruits1[][10] = { "apple", "banana", "cherry" };
|
- 类型:
char [3][10]
(3个元素,每个元素是char [10]
的数组);
- 内存布局:所有字符串连续存储在内存中,形成一个3×10的二维数组(实际存储为
'a','p','p','l','e','\0',...
);
- 特点:数组名
fruits1
是常量指针,指向第一个子数组的起始地址(&fruits1[0]
);fruits1[i]
是第i
个子数组的起始地址(&fruits1[i][0]
)。
fruits2
:指针数组(数组的指针)
1
| char *fruits2[] = { "apple","banana","cherry" };
|
- 类型:
char *[3]
(3个元素,每个元素是char *
指针);
- 内存布局:数组本身存储3个指针(每个指针指向一个字符串字面量的地址);
- 特点:数组名
fruits2
是常量指针,指向第一个指针的起始地址(&fruits2[0]
);fruits2[i]
是第i
个指针的地址(存储字符串字面量的首地址)。
2. Num_arr
函数:打印数组内容与长度
函数通过sizeof
计算数组长度,并遍历打印每个字符串及其长度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void Num_arr() { // 打印fruits1(二维数组) printf("Fruits1: "); for (int i = 0; i < (sizeof(fruits1) / sizeof(fruits1[0])); i++) { printf("第%d个字符串是:%s, 字符串长度是:%zu\n", (i + 1), fruits1[i], strlen(fruits1[i])); }
// 打印fruits2(指针数组) printf("\nFruits2:\n"); for (int i = 0; i < sizeof(fruits2) / sizeof(fruits2[0]); ++i) { printf("第%d个字符串是:%s, 字符串长度是:%zu\n", (i + 1), fruits2[i], strlen(fruits2[i])); } }
|
关键细节:
sizeof(fruits1) / sizeof(fruits1[0])
:计算二维数组的行数(3
),因为sizeof(fruits1)
是整个二维数组的大小(3×10=30
字节),sizeof(fruits1[0])
是单个子数组的大小(10
字节);
sizeof(fruits2) / sizeof(fruits2[0])
:计算指针数组的元素个数(3
),因为sizeof(fruits2)
是指针数组的大小(3×8=24
字节,假设64位系统),sizeof(fruits2[0])
是单个指针的大小(8
字节)。
3. Change
函数:修改数组元素的差异
函数演示了对两种数组的修改操作:
1 2 3 4 5
| void Change() { // fruits1[0] = "orange"; 错误:数组名是常量指针,不可重新赋值 fruits2[0] = "orange"; // 正确:指针数组的元素是指针,可重新指向新字符串 strcpy(fruits1[0], "orange"); // 正确:修改二维数组的内容(非数组名) }
|
核心区别:
fruits1[0]
:是二维数组的子数组名(char [10]
类型),本质是常量指针(指向子数组的起始地址),无法通过=
重新赋值;
fruits2[0]
:是指针数组的元素(char *
类型),是普通指针变量,可以通过=
重新指向其他字符串;
strcpy(fruits1[0], "orange")
:通过strcpy
修改二维数组的内容(覆盖原字符串),这是允许的,因为数组名指向的内存区域是可写的。
4. Chang_banana
函数:修改字符的细节
函数演示了对字符串中单个字符的修改:
1 2 3 4
| void Chang_banana() { fruits1[1][0] = 'B'; // 正确:修改二维数组的字符(非字符串字面量) fruits2[1] = "Banana"; // 正确:修改指针数组的指向(原字符串未被修改) }
|
注意事项:
fruits1[1][0] = 'B'
:fruits1
的子数组存储的是字符串"banana"
(可写内存),因此可以直接修改第一个字符为'B'
(结果为"Banana"
);
fruits2[1] = "Banana"
:fruits2[1]
原指向字符串字面量"banana"
(只读内存),但通过指针重新指向"Banana"
(新的可写内存),原"banana"
未被修改(若尝试修改"banana"
的内容会导致未定义行为)。
5. 主函数:指针数组的灵活操作
主函数定义了另一个指针数组fruits3
,并演示了对其的修改:
1 2 3 4 5 6 7 8 9 10 11 12
| int main(void) { char apple[] = "apple"; // 栈上的字符数组(可写) char banana[] = "banana"; // 栈上的字符数组(可写) char cherry[] = "cherry"; // 栈上的字符数组(可写) char *fruits3[] = { apple, banana, cherry }; // 指针数组指向栈上的数组
fruits3[0] = "orange"; // 正确:指针重新指向新的字符串(堆或只读区) fruits3[1][0] = 'B'; // 正确:修改栈上数组的字符(`banana`变为"Banana")
Chang_banana(); // 调用函数修改`fruits1`和`fruits2` return 0; }
|
关键场景:
fruits3
是指针数组,元素指向栈上的字符数组(apple
、banana
、cherry
);
fruits3[0] = "orange"
:指针重新指向字符串字面量"orange"
(通常存储在只读区);
fruits3[1][0] = 'B'
:修改栈上banana
数组的第一个字符("banana"
变为"Banana"
)。
内存布局对比:二维数组 vs 指针数组
特性 |
二维数组(fruits1 ) |
指针数组(fruits2 ) |
类型 |
char [3][10] (数组的数组) |
char *[3] (数组的指针) |
内存存储 |
连续存储(所有字符在一个连续内存块中) |
非连续存储(数组存储指针,指针指向分散的内存) |
修改数组名 |
不允许(数组名是常量指针) |
允许(数组名是常量指针,但元素是指针变量) |
修改元素内容 |
允许(通过下标修改字符) |
允许(通过指针修改指向的内容或重新指向) |
字符串字面量存储 |
存储在数组内存中(可写) |
存储在只读区(不可直接修改内容) |
潜在问题与最佳实践
问题1:指针数组指向无效内存
1 2
| char *fruits2[] = { "apple", "banana", NULL }; // 最后一个元素为NULL fruits2[2][0] = 'C'; // 崩溃!NULL指针无指向的内存
|
原因:指针数组的元素可能指向NULL
或其他无效地址,直接解引用会导致崩溃。
解决方案:操作指针数组前,需检查指针是否为NULL
。
问题2:二维数组越界访问
1
| printf("%s", fruits1[3]); // 越界访问!`fruits1`只有3个元素(索引0-2)
|
原因:二维数组的索引范围是0
到行数-1
,越界访问会导致未定义行为。
解决方案:访问前检查索引是否在有效范围内(0 ≤ i < 行数
)。
总结:数组与指针的核心差异
通过本文的解析,我们掌握了:
\*p[]
(指针数组):数组的元素是指针,存储的是内存地址,可灵活指向不同的内存区域;
(\*p)[]
(二维数组):数组的元素是另一个数组,内存连续存储,适合处理固定大小的字符串集合;
- 操作差异:指针数组可重新指向新内存,二维数组可直接修改内容(需确保内存可写);
- 内存布局:指针数组非连续存储,二维数组连续存储,各有适用场景(如动态扩展用指针数组,固定数据用二维数组)。
这些知识是C语言进阶的核心,熟练掌握后可更高效地处理字符串操作、内存管理和复杂数据结构(如链表、哈希表)。
完整源代码:C语言数组与指针深度解析(*p[]
vs (*p)[]
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h>
/* * 核心目标:对比二维数组((*p)[])与指针数组(*p[])的类型差异与操作限制 * 关键结论: * - []优先级高于*,因此*p[]是数组的指针(指针数组),(*p)[]是数组的数组(二维数组) * - 二维数组名是常量指针(不可重新赋值),但可通过下标修改元素内容(需内存可写) * - 指针数组的元素是指针变量(可重新赋值指向新内存),但指向的内容是否可写取决于目标内存 */
// 二维数组(数组的数组):连续内存存储,每个子数组固定大小 char fruits1[][10] = { "apple", "banana", "cherry" }; // 3个元素,每个元素是char[10]
// 指针数组(数组的指针):存储指针的数组,指针指向独立内存 char *fruits2[] = { "apple", "banana", "cherry" }; // 3个元素,每个元素是char*
// 指针数组指向栈上的字符数组(可写内存) char apple_stack[] = "apple"; // 栈上的字符数组(可写) char banana_stack[] = "banana"; // 栈上的字符数组(可写) char cherry_stack[] = "cherry"; // 栈上的字符数组(可写) char *fruits3[] = { apple_stack, banana_stack, cherry_stack }; // 指针数组指向栈数组
// 函数1:打印数组内容与长度(验证内存布局与可访问性) void print_arrays() { printf("===== 打印二维数组 fruits1 =====\n"); for (int i = 0; i < sizeof(fruits1) / sizeof(fruits1[0]); i++) { printf("fruits1[%d]: %s(长度:%zu,内存地址:%p)\n", i, fruits1[i], strlen(fruits1[i]), (void*)fruits1[i]); }
printf("\n===== 打印指针数组 fruits2 =====\n"); for (int i = 0; i < sizeof(fruits2) / sizeof(fruits2[0]); i++) { printf("fruits2[%d]: %s(长度:%zu,指针地址:%p,指向内存:%p)\n", i, fruits2[i], strlen(fruits2[i]), (void*)&fruits2[i], (void*)fruits2[i]); }
printf("\n===== 打印指针数组 fruits3(指向栈数组) =====\n"); for (int i = 0; i < sizeof(fruits3) / sizeof(fruits3[0]); i++) { printf("fruits3[%d]: %s(长度:%zu,指针地址:%p,指向内存:%p)\n", i, fruits3[i], strlen(fruits3[i]), (void*)&fruits3[i], (void*)fruits3[i]); } }
// 函数2:修改数组元素(验证操作限制) void modify_arrays() { // 1. 尝试修改二维数组的"数组名"(非法操作) // fruits1 = fruits2; // 编译错误:数组名是常量指针,不可重新赋值
// 2. 修改二维数组的内容(通过下标,合法) strcpy(fruits1[0], "orange"); // 正确:覆盖二维数组的内存内容 fruits1[1][0] = 'B'; // 正确:修改二维数组的字符(原"banana"→"Banana")
// 3. 修改指针数组的"数组名"(非法操作) // fruits2 = fruits3; // 错误:数组名是常量指针,不可重新赋值 // 修正:指针数组的元素是指针,应逐个修改元素指向 fruits2[0] = "orange"; // 正确:修改指针数组的第0个元素指向新字符串 fruits2[1] = "Banana"; // 正确:修改指针数组的第1个元素指向新字符串(原"banana"未被修改)
// 4. 修改指针数组指向的内容(取决于目标内存是否可写) fruits3[1][0] = 'b'; // 正确:修改栈上的banana_stack数组(可写内存) // fruits2[1][0] = 'b'; // 未定义行为!fruits2[1]指向字符串字面量(只读内存) }
// 函数3:验证指针数组与二维数组的本质区别 void validate_differences() { // 二维数组的内存是连续的(所有字符在一个块中) printf("\n===== 验证二维数组内存连续性 =====\n"); printf("fruits1[0]地址:%p,fruits1[0][0]地址:%p(偏移0)\n", (void*)fruits1, (void*)&fruits1[0][0]); printf("fruits1[0]地址:%p,fruits1[0][9]地址:%p(偏移9)\n", (void*)fruits1, (void*)&fruits1[0][9]); printf("fruits1[1]地址:%p(偏移10,与fruits1[0][9]+1一致)\n", (void*)fruits1[1]);
// 指针数组的内存是非连续的(存储指针,指针指向分散内存) printf("\n===== 验证指针数组内存非连续性 =====\n"); printf("fruits2[0]指针值:%p(指向字符串字面量)\n", (void*)fruits2[0]); printf("fruits2[1]指针值:%p(指向字符串字面量)\n", (void*)fruits2[1]); printf("fruits2[2]指针值:%p(指向字符串字面量)\n", (void*)fruits2[2]); printf("fruits2数组本身的地址:%p,与指针值无关\n", (void*)fruits2); }
int main(void) { // 初始化后直接打印 print_arrays();
// 修改数组元素并再次打印 modify_arrays(); printf("\n===== 修改后的数组状态 =====\n"); print_arrays();
// 验证内存布局差异 validate_differences();
return 0; }s
|
代码说明与关键注释
1. 数据定义部分
fruits1
(二维数组):char [3][10]
类型,3个元素,每个元素是固定大小(10字节)的字符数组。内存连续存储所有字符串,可直接通过下标修改内容(需确保内存可写)。
fruits2
(指针数组):char *[3]
类型,3个元素是指针变量,初始指向字符串字面量(通常存储在只读区)。指针变量本身可重新赋值,但指向的内容是否可写取决于目标内存。
fruits3
(指针数组指向栈数组):指针数组的元素指向栈上的字符数组(apple_stack
等),这些栈数组是可写的,因此可通过指针修改其内容。
2. print_arrays
函数
- 打印二维数组的每个子数组内容、长度和内存地址;
- 打印指针数组的每个元素(指针值)、指向内容的长度、指针自身地址和指向的内存地址;
- 打印指针数组指向栈数组的特殊情况(验证栈内存的可写性)。
3. modify_arrays
函数
- 二维数组修改:通过
strcpy
覆盖fruits1[0]
的内容(合法,因二维数组内存可写);通过下标修改fruits1[1][0]
的字符(合法,因内存可写)。
- 指针数组修改:直接修改指针数组元素的指向(如
fruits2[0] = "orange"
),但不可直接修改数组名(fruits2 = ...
是错误的,因数组名是常量指针)。
- 字符串字面量保护:尝试修改
fruits2[1][0]
会触发未定义行为(字符串字面量存储在只读区)。
4. validate_differences
函数
- 二维数组内存连续性:验证二维数组的所有字符存储在连续内存中(
fruits1[1]
的地址等于fruits1[0]
的地址+10字节)。
- 指针数组内存非连续性:验证指针数组存储的是指针变量(地址连续),但指针指向的内存是分散的(字符串字面量存储在不同位置)。
编译与运行结果示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| ===== 打印二维数组 fruits1 ===== fruits1[0]: apple(长度:5,内存地址:0x7ffd...) fruits1[1]: banana(长度:6,内存地址:0x7ffd...) fruits1[2]: cherry(长度:6,内存地址:0x7ffd...)
===== 打印指针数组 fruits2 ===== fruits2[0]: apple(长度:5,指针地址:0x7ffd..., 指向内存:0x55f5...) fruits2[1]: banana(长度:6,指针地址:0x7ffd..., 指向内存:0x55f5...) fruits2[2]: cherry(长度:6,指针地址:0x7ffd..., 指向内存:0x55f5...)
===== 打印指针数组 fruits3(指向栈数组) ===== fruits3[0]: apple(长度:5,指针地址:0x7ffd..., 指向内存:0x7ffd...) fruits3[1]: banana(长度:6,指针地址:0x7ffd..., 指向内存:0x7ffd...) fruits3[2]: cherry(长度:6,指针地址:0x7ffd..., 指向内存:0x7ffd...)
===== 修改后的数组状态 ===== ===== 打印二维数组 fruits1 ===== fruits1[0]: orange(长度:6,内存地址:0x7ffd...) fruits1[1]: Banana(长度:6,内存地址:0x7ffd...) fruits1[2]: cherry(长度:6,内存地址:0x7ffd...)
===== 打印指针数组 fruits2 ===== fruits2[0]: orange(长度:6,指针地址:0x7ffd..., 指向内存:0x55f5...) fruits2[1]: Banana(长度:6,指针地址:0x7ffd..., 指向内存:0x7ffd...) fruits2[2]: cherry(长度:6,指针地址:0x7ffd..., 指向内存:0x55f5...)
===== 打印指针数组 fruits3(指向栈数组) ===== fruits3[0]: apple(长度:5,指针地址:0x7ffd..., 指向内存:0x7ffd...) fruits3[1]: banana(长度:6,指针地址:0x7ffd..., 指向内存:0x7ffd...) fruits3[2]: cherry(长度:6,指针地址:0x7ffd..., 指向内存:0x7ffd...)
===== 验证二维数组内存连续性 ===== fruits1[0]地址:0x7ffd..., fruits1[0][0]地址:0x7ffd...(偏移0) fruits1[0]地址:0x7ffd..., fruits1[0][9]地址:0x7ffd...(偏移9) fruits1[1]地址:0x7ffd...(偏移10,与fruits1[0][9]+1一致)
===== 验证指针数组内存非连续性 ===== fruits2[0]指针值:0x55f5...(指向字符串字面量) fruits2[1]指针值:0x55f5...(指向字符串字面量) fruits2[2]指针值:0x55f5...(指向字符串字面量) fruits2数组本身的地址:0x7ffd..., 与指针值无关
|
核心结论总结
特性 |
二维数组(fruits1 ) |
指针数组(fruits2 ) |
类型 |
char [3][10] (数组的数组) |
char *[3] (数组的指针) |
内存布局 |
连续存储(所有字符在一个连续内存块中) |
非连续存储(数组存储指针,指针指向分散的内存) |
修改数组名 |
不允许(数组名是常量指针) |
允许修改元素指向(数组名是常量指针,但元素是指针变量) |
修改元素内容 |
允许(通过下标修改字符,需内存可写) |
允许(通过指针修改指向的内容或重新指向) |
字符串字面量存储 |
存储在数组内存中(可写) |
存储在只读区(不可直接修改内容) |
通过运行和分析此代码,可直观理解*p[]
(指针数组)与(*p)[]
(二维数组)的本质差异,以及它们在实际开发中的应用场景(如动态扩展用指针数组,固定数据用二维数组)。