目录

这是一道涉及指针与数组声明的常见面试题。混淆指针数组与数组指针,是较易出现的理解偏差。

结论

就声明本身而言,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 则随作用域结束自动回收。

常见误区

  1. 误认为已分配 10 块字符串存储:指针数组存放的是指针,而非指针所指向的内容。声明仅预留指针本身的存储。

  2. 未初始化即解引用:局部情形下的 p[i] 取值不确定,直接以 printf(“%s”, p[i]) 使用会触发未定义行为。

  3. 未分配即按二维字符串访问:在通过 p[i][j] 访问前,须确保 p[i] 已指向合法内存。

  4. 遗漏 free:每个 malloc 应对应一次 free,并注意释放顺序与避免重复释放。

  5. 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 二级指针。