这是一道涉及指针与数组声明的常见面试题。混淆指针数组与数组指针,是较易出现的理解偏差。
结论
就声明本身而言,char *p[10] 仅分配一块内存。
该声明定义的是一个包含 10 个 char* 元素的数组,即”指针数组”(array of pointers)。编译器仅为这 10 个指针本身预留一块连续内存:作为局部变量时位于栈上;作为全局或静态变量时位于 .bss 段(零初始化)。
在 64 位平台下,每个指针占 8 字节,合计 10 × 8 = 80 字节;32 位平台下为 10 × 4 = 40 字节。需要强调的是:这 10 个指针在声明时尚未指向任何字符存储(局部变量情形下其值为不确定值)。字符串内容需经后续 malloc 或赋值操作才存在。
因此,答案并非”10 块”,而是”1 块(数组本体)“。所谓字符串存储,需由后续分配或赋值引入。
声明拆解:优先级决定语义
在 C 语言的声明符中,下标运算符 [] 的绑定优先级高于解引用运算符 *。解析声明时应从内层向外、依据结合顺序判定:
char *p[10];
└─┬─┘
先与 [] 结合 → p 是一个数组
元素类型 → 去除 p[10] 后剩余 char * → 元素为指向 char 的指针
由此可得:p 是数组,其元素为指针,即”指针数组”(array of pointers)。
判定规则:当 [] 先于 * 结合时,声明的是一个数组;数组元素的类型由声明符去除数组名与下标后剩余的部分决定。
内存布局
声明 char *p[10];(64 位、局部变量)时,栈上仅存在如下一块连续区域;10 个指针槽的初始值处于不确定状态(indeterminate value),并不指向任何字符串:
栈(stack)/ .bss(全局或静态情形为零初始化)
┌──────────┬──────────┬──────┬──────────┐
│ p[0] │ p[1] │ ... │ p[9] │
│ char* │ char* │ │ char* │
│ 8B 不确定 │ 8B 不确定 │ │ 8B 不确定 │
└──────────┴──────────┴──────┴──────────┘
│ │ │
▼ ▼ ▼
(未指向任何字符串) ← 声明本身不分配字符串存储
在将 p[i] 用作字符串之前,其尚未指向合法内存,此时解引用属于未定义行为(undefined behavior),可能表现为输出乱码或段错误。
四类易混淆声明对比
面试官通常关注候选人能否区分下列四种声明的语义差异:
| 声明 | 本质 | 声明阶段分配内存 | 64 位大小 |
|---|---|---|---|
| char *p[10] | 指针数组 | 1 块(数组本体) | 80 B |
| char (*p)[10] | 指向含 10 个 char 的数组的指针 | 1 块(仅指针本身) | 8 B |
| char p[10] | 字符数组 | 1 块(数组本体) | 10 B |
| char **p | 二级指针 | 1 块(仅指针本身) | 8 B |
char (*p)[10]:括号使 * 优先结合,p 为指针,指向一个含 10 个 char 的数组。声明仅分配指针(8 字节);该 10 字节数组并不实际存在,除非执行 p = malloc(…)。
char p[10]:最基础的字符数组,10 个 char 连续存放,占 1 块 10 字节。
char 二级指针:单个指针,占 8 字节,未指向任何对象。
不同平台下的尺寸
指针宽度由目标平台的 ABI(数据模型,如 LP64、LLP64、ILP32)决定,与 char 类型本身的大小无关:
32 位平台(典型数据模型 ILP32):sizeof(char*) 为 4,char *p[10] 占 40 字节。
64 位平台(Linux/macOS 采用 LP64,Windows 采用 LLP64):sizeof(char*) 为 8,占 80 字节。
以下为 64 位平台下的 sizeof 验证结果:sizeof(p) 为 80(整个数组,10 × 8);sizeof(p[0]) 为 8(一个 char* 指针);sizeof(*p[0]) 为 1(char 本身)。
补充说明数组到指针的退化(array decay)规则:p 在多数表达式上下文中(例如作为函数实参)会退化为指向首元素的指针,类型为指向 char* 的指针(即 char 二级指针);而 &p 的类型为指向 char* 数组的指针,二者不可混用。
为每个指针分配堆内存的情形
1 块仅描述声明阶段。若为每个元素分别分配堆内存,内存块数量随之变化:
char *p[10];
for (int i = 0; i < 10; i++) {
// 再分配 10 块堆内存(省略返回值检查,完整示例见后)
p[i] = malloc(64);
strcpy(p[i], "item");
}
此时内存块数 = 1(数组本体)+ 10(堆上字符串)= 11 块。每块堆内存都应配对调用 free(p[i]),否则将产生内存泄漏;数组本体 p 则随作用域结束自动回收。
常见误区
-
误认为已分配 10 块字符串存储:指针数组存放的是指针,而非指针所指向的内容。声明仅预留指针本身的存储。
-
未初始化即解引用:局部情形下的 p[i] 取值不确定,直接以 printf(“%s”, p[i]) 使用会触发未定义行为。
-
未分配即按二维字符串访问:在通过 p[i][j] 访问前,须确保 p[i] 已指向合法内存。
-
遗漏 free:每个 malloc 应对应一次 free,并注意释放顺序与避免重复释放。
-
char *p = “abc” 的语义陷阱:字符串字面量通常存放在只读段(.rodata),p 仅是一个指向它的 8 字节指针;尝试以 p[0] = ‘x’ 修改其内容,在多数实现上会触发段错误。
进一步考察点
能够阐述以下任一方面,通常表明对内存模型有较深入的理解,而非仅记忆结论:
- 区分内存区域:数组本体位于栈(或 .bss),malloc 所得位于堆,字面量位于 .rodata。
- 说明生命周期与对齐:栈上数组随作用域自动回收;指针通常按 8 字节对齐。
- 主动对比 char (*p)[10] 与 char 二级指针,说明二者类型不同,且 sizeof 与传参退化行为存在差异。
- 指出”几块内存”取决于是否继续分配:声明阶段为 1 块,全部 malloc 后为 1+N 块。
可验证代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
// 仅分配 1 块:10 个 char* 的数组(栈上)
char *p[10];
// 80 (64 位)
printf("sizeof(p) = %zu\n", sizeof(p));
// 8
printf("sizeof(p[0]) = %zu\n", sizeof(p[0]));
// p[0] 尚未初始化,值不确定,切勿解引用
printf("p[0] 未初始化(值不确定,勿解引用)\n");
// 额外分配 10 块堆内存
for (int i = 0; i < 10; i++) {
p[i] = malloc(64);
if (!p[i]) { perror("malloc"); exit(EXIT_FAILURE); }
strcpy(p[i], "item");
}
// ... 使用 p[i] ...
// 及时释放,避免泄漏
for (int i = 0; i < 10; i++) free(p[i]);
return 0;
}
区分要点
[] 优先结合,先确定数组;数组元素的类型由剩余声明部分决定。
指针数组仅提供指针的存储槽位,并不提供指针所指向的内容:char *p[10] 在声明阶段占 1 块 80 字节(64 位)。
字符串存储需另行分配:malloc/strdup 每调用一次增加一块堆内存,使用后应 free。
四类声明速记:*p[10] 指针数组、(*p)[10] 数组指针、p[10] 字符数组、char 二级指针。