一次小bug引发的一篇思考:数组指针与指针数组的深度解析& 大厂面试相关题全攻略 上中下三篇连续更新中

1 文章来源:

今天下午搞这个题目的时候,竟然再数组指针和指针数组这里碰到了一个问题,数组指针和指针数组的核心区别!!!!!非常重要!!!!

这是非常核心的一个认知错误!!!

在 C 语言的浩瀚世界里,指针无疑是其最强大也最令人望而却步的特性之一。而当指针与数组结合时,便诞生了两个常常让初学者乃至有经验的开发者都感到困惑的概念:数组指针(Pointer to Array)和指针数组(Array of Pointers)。它们在语法上仅有一字之差,但在语义和用途上却有着天壤之别。

1. 概念辨析:形似神异

在理解数组指针和指针数组之前,我们必须牢记 C 语言中一个核心的规则:“近绑定”原则。这意味着在声明中,一个变量会优先与其最近的修饰符结合。

1.1. 数组指针 (Pointer to Array)

定义: 数组指针,本质上是一个指针。它指向的是一个完整的数组

声明形式: Type (*pointer_name)[size];

  • Type:数组中元素的数据类型。

  • *pointer_name:括号 () 的存在使得 pointer_name 首先与 * 结合,表明 pointer_name 是一个指针。

  • [size]:接在 (*pointer_name) 之后,表示这个指针指向的是一个包含 sizeType 类型元素的数组。

理解要点:

  1. “先是一个指针”(*pointer_name) 决定了它的本质是指针

  2. “再指向一个数组”[size] 决定了它指向的目标是一个具有特定大小的数组。

  3. 类型匹配:数组指针的类型必须与其所指向的数组的类型和大小完全匹配。

示例:

int arr[5]; // 声明一个包含5个整型元素的数组
int (*p)[5]; // 声明一个数组指针 p,它指向一个包含5个整型元素的数组
p = &arr; // 将数组 arr 的地址赋值给 p

在这里,p 的类型是 int (*)[5]。这意味着 p 存储的是 arr 整个数组的地址。当你对 p 进行 p + 1 运算时,它会跳过整个 arr 数组的大小(5 * sizeof(int) 字节)。

1.2. 指针数组 (Array of Pointers)

定义: 指针数组,本质上是一个数组。它的每个元素都是一个指针

声明形式: Type *array_name[size];

  • Type:数组中每个指针所指向的数据类型。

  • array_name[size]array_name 首先与 [size] 结合,表明 array_name 是一个包含 size 个元素的数组。

  • *:接在 Typearray_name[size] 之间,表示数组的每个元素都是 Type 类型的指针。

理解要点:

  1. “先是一个数组”array_name[size] 决定了它的本质是数组

  2. “再是每个元素都是指针”* 决定了数组的每个元素都是指针类型。

  3. 元素独立:数组中的每个指针元素可以指向不同的内存位置,甚至可以是不同的类型(尽管通常为了类型安全,会保持一致)。

示例:

int *arr_ptr[3]; // 声明一个指针数组 arr_ptr,它包含3个指向整型的指针
int a = 10, b = 20, c = 30;
arr_ptr[0] = &a; // arr_ptr 的第一个元素指向变量 a
arr_ptr[1] = &b; // arr_ptr 的第二个元素指向变量 b
arr_ptr[2] = &c; // arr_ptr 的第三个元素指向变量 c

在这里,arr_ptr 的类型是 int *[3]。这意味着 arr_ptr 是一个数组,它里面存放了三个 int* 类型的指针。每个 arr_ptr[i] 都是一个独立的指针变量。

2. 内存布局与操作差异

理解了定义,接下来我们将通过内存布局和实际操作来进一步区分两者。

2.1. 数组指针的内存布局与操作

一个数组指针变量本身只占用固定的内存空间(通常是 4 或 8 字节,取决于系统位数),用于存储它所指向的整个数组的起始地址。

内存布局(示意图):

内存地址 -> |  ...  |
            | ------- |
            |  p (地址) |  <-- 数组指针 p 存储的地址
            | ------- |
            |  ...  |
            | ------- |
            | arr[0] |  <-- arr 数组的起始地址
            | arr[1] |
            | arr[2] |
            | arr[3] |
            | arr[4] |
            | ------- |
            |  ...  |

操作:

  1. 赋值: 只能通过 &数组名 获取整个数组的地址来赋值给数组指针。

    int arr[5];
    int (*p)[5] = &arr; // 正确
    // int (*p)[5] = arr; // 错误!arr 是首元素地址,类型是 int*,与 int (*)[5] 不匹配
    
    
  2. 解引用:

    • *p:解引用数组指针 p 得到的是它所指向的整个数组。*p 的类型相当于 int[5]

    • (*p)[i]:通过解引用得到的数组再通过下标访问元素。

    • p[0][i]:另一种等价的访问方式,编译器会将 p[0] 解释为 *p

    • p[i]:当 i 不为 0 时,p[i] 会被解释为 *(p + i),即跳过 i 个完整数组的地址,然后解引用。这在实际中很少用于单个数组指针,更多用于数组指针的数组(即指向多个数组的数组指针)。

  3. 指针算术:

    • p + 1:移动到下一个“完整数组”的起始地址。如果 p 指向 arr,那么 p + 1 将指向 arr 后面紧接着的内存区域(如果存在的话,或者更准确地说是跳过 sizeof(*p)sizeof(arr) 字节)。

    • p - 1:移动到前一个“完整数组”的起始地址。

示例代码:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int (*p)[5]; // 声明一个数组指针,指向一个包含5个整型元素的数组

    p = &arr; // 将数组 arr 的地址赋值给 p

    printf("arr 的地址: %p\n", (void*)arr);
    printf("&arr 的地址: %p\n", (void*)&arr);
    printf("p 的值 (arr 的地址): %p\n", (void*)p);
    printf("p + 1 的值: %p (跳过 sizeof(arr) 字节)\n", (void*)(p + 1));
    printf("----------------------------------------\n");

    // 访问数组元素
    printf("通过数组指针访问元素:\n");
    for (int i = 0; i < 5; i++) {
        printf("(*p)[%d] = %d\n", i, (*p)[i]);
    }
    printf("----------------------------------------\n");

    // 另一种访问方式:p[0] 等价于 *p
    printf("通过 p[0] 访问元素:\n");
    for (int i = 0; i < 5; i++) {
        printf("p[0][%d] = %d\n", i, p[0][i]);
    }
    printf("----------------------------------------\n");

    return 0;
}

运行结果(示例):

arr 的地址: 000000AA62DFFB20
&arr 的地址: 000000AA62DFFB20
p 的值 (arr 的地址): 000000AA62DFFB20
p + 1 的值: 000000AA62DFFB34 (跳过 sizeof(arr) 字节)  // 0x34 - 0x20 = 0x14 = 20 字节 (5 * 4 字节)
----------------------------------------
通过数组指针访问元素:
(*p)[0] = 10
(*p)[1] = 20
(*p)[2] = 30
(*p)[3] = 40
(*p)[4] = 50
----------------------------------------
通过 p[0] 访问元素:
p[0][0] = 10
p[0][1] = 20
p[0][2] = 30
p[0][3] = 40
p[0][4] = 50
----------------------------------------

2.2. 指针数组的内存布局与操作

一个指针数组变量本身占用 size * sizeof(Type*) 字节的内存空间,因为它需要为每个指针元素分配独立的存储空间。

内存布局(示意图):

内存地址 -> |  ...  |
            | ------- |
            | arr_ptr[0] (存储的是 &a 的地址) | <-- 指针数组 arr_ptr 的起始地址
            | ------- |
            | arr_ptr[1] (存储的是 &b 的地址) |
            | ------- |
            | arr_ptr[2] (存储的是 &c 的地址) |
            | ------- |
            |  ...  |
            | ------- |
            | a (值 10) |  <-- 变量 a 的地址
            | ------- |
            | b (值 20) |  <-- 变量 b 的地址
            | ------- |
            | c (值 30) |  <-- 变量 c 的地址
            | ------- |
            |  ...  |

操作:

  1. 赋值: 数组的每个元素都可以独立地被赋值为某个变量的地址,或者另一个指针的值。

    int *arr_ptr[3];
    int a = 10, b = 20, c = 30;
    arr_ptr[0] = &a; // 正确
    arr_ptr[1] = &b; // 正确
    arr_ptr[2] = &c; // 正确
    // arr_ptr[0] = a; // 错误!类型不匹配
    
    
  2. 解引用:

    • arr_ptr[i]:获取数组中第 i 个指针元素的值(即它所指向的地址)。

    • *arr_ptr[i]:解引用数组中第 i 个指针元素,得到它所指向的值。

  3. 指针算术:

    • arr_ptr + 1:移动到下一个“指针元素”的起始地址。

    • arr_ptr[0] + 1:如果 arr_ptr[0] 指向一个数组的起始,则 arr_ptr[0] + 1 会移动到该数组的下一个元素。这通常用于 char *arr[] 存储字符串数组的场景。

示例代码:

#include <stdio.h>

int main() {
    int num1 = 100, num2 = 200, num3 = 300;
    int *arr_ptr[3]; // 声明一个指针数组,包含3个指向整型的指针

    arr_ptr[0] = &num1;
    arr_ptr[1] = &num2;
    arr_ptr[2] = &num3;

    printf("num1 的地址: %p, 值: %d\n", (void*)&num1, num1);
    printf("num2 的地址: %p, 值: %d\n", (void*)&num2, num2);
    printf("num3 的地址: %p, 值: %d\n", (void*)&num3, num3);
    printf("----------------------------------------\n");

    printf("arr_ptr 的地址: %p\n", (void*)arr_ptr); // 指针数组的起始地址
    printf("arr_ptr + 1 的地址: %p (跳过 sizeof(int*) 字节)\n", (void*)(arr_ptr + 1));
    printf("----------------------------------------\n");

    printf("通过指针数组访问元素:\n");
    for (int i = 0; i < 3; i++) {
        printf("arr_ptr[%d] 的值 (地址): %p, *arr_ptr[%d] 的值: %d\n",
               i, (void*)arr_ptr[i], i, *arr_ptr[i]);
    }
    printf("----------------------------------------\n");

    // 字符串数组是典型的指针数组应用
    char *names[3] = {"Alice", "Bob", "Charlie"};
    printf("字符串数组示例:\n");
    for (int i = 0; i < 3; i++) {
        printf("names[%d]: %s\n", i, names[i]);
    }
    printf("----------------------------------------\n");

    return 0;
}

运行结果(示例):

num1 的地址: 000000965F2FF874, 值: 100
num2 的地址: 000000965F2FF878, 值: 200
num3 的地址: 000000965F2FF87C, 值: 300
----------------------------------------
arr_ptr 的地址: 000000965F2FF850
arr_ptr + 1 的地址: 000000965F2FF858 (跳过 sizeof(int*) 字节) // 0x58 - 0x50 = 0x8 = 8 字节 (在64位系统上)
----------------------------------------
通过指针数组访问元素:
arr_ptr[0] 的值 (地址): 000000965F2FF874, *arr_ptr[0] 的值: 100
arr_ptr[1] 的值 (地址): 000000965F2FF878, *arr_ptr[1] 的值: 200
arr_ptr[2] 的值 (地址): 000000965F2FF87C, *arr_ptr[2] 的值: 300
----------------------------------------
字符串数组示例:
names[0]: Alice
names[1]: Bob
names[2]: Charlie
----------------------------------------

3. 大厂面试高频考点与深入分析

在大型科技公司的 C 语言面试中,数组指针和指针数组是考察面试者对 C 语言底层理解程度的利器。这些题目往往不只是简单地让你区分概念,而是要求你深入理解它们在内存中的表现、类型转换、函数传参时的行为,以及如何避免常见的陷阱。

3.1. 考点一:sizeofstrlen 的行为

这是一个经典的考察点,它要求你理解 sizeofstrlen 的作用以及它们处理数组名和指针时的区别。

  • sizeof:计算类型或变量所占的字节数

    • 当作用于数组名时,sizeof(arr) 返回整个数组的总字节数。

    • 当作用于指针时,sizeof(ptr) 返回指针变量本身所占的字节数(在 32 位系统上是 4 字节,64 位系统上是 8 字节),而不是它指向的数据的大小。

  • strlen:计算字符串的长度,直到遇到第一个 \0 字符为止。它只适用于以 \0 结尾的字符数组或字符串。

深入分析: 对于数组指针 int (*p)[5];sizeof(p) 返回指针变量 p 的大小(4 或 8 字节),而 sizeof(*p) 返回 p 所指向的整个数组的大小(5 * sizeof(int) 字节)。 对于指针数组 int *arr_ptr[3];sizeof(arr_ptr) 返回整个指针数组的大小(3 * sizeof(int*) 字节),而 sizeof(arr_ptr[0]) 返回数组中第一个指针元素的大小(即 int* 的大小,4 或 8 字节)。

3.2. 考点二:函数传参时的“降维”问题

C 语言中,数组作为函数参数传递时,会发生降维(decay)。这意味着:

  • 一维数组作为参数传递时,会降维为指向其首元素的指针

    • 例如,void func(int arr[])void func(int *arr) 都表示 func 接收一个 int* 类型的参数。

  • 多维数组作为参数传递时,会降维为指向其第一维元素的指针

    • 例如,void func(int arr[][5])void func(int (*arr)[5]) 都表示 func 接收一个指向包含 5 个整型元素的数组的指针(即一个数组指针)。

深入分析: 面试官会通过让你编写或分析涉及数组作为参数的函数来考察你是否理解这种降维行为。关键在于,函数内部无法直接得知原始数组的完整大小,只能得知其降维后的指针类型的大小。

3.3. 考点三:复杂指针声明的解读

这是最常见的面试题之一,要求你能够准确地解析复杂的指针声明。掌握“近绑定”原则和从内到外(或从右到左,再结合括号)的解析顺序是关键。

解析方法:

  1. 找到变量名:确定哪个是变量名。

  2. 看最接近的修饰符:如果变量名最近的是 [],则它是一个数组;如果最近的是 *,则它是一个指针。

  3. 看括号:括号可以改变优先级。(*p) 表示 p 是一个指针。

  4. 逐步解析:根据修饰符的类型和顺序,逐步确定变量的完整含义。

示例:

  • int *p[10];

    • p 首先与 [10] 结合,表明 p 是一个大小为 10 的数组。

    • * 再与 int 结合,表明数组的每个元素都是 int* 类型。

    • 结论: p 是一个包含 10 个指向整型的指针的数组(指针数组)。

  • int (*p)[10];

    • p 首先与 * 结合,因为有括号 (),表明 p 是一个指针。

    • [10] 再与 int 结合,表明这个指针指向一个包含 10 个 int 元素的数组。

    • 结论: p 是一个指向包含 10 个整型元素的数组的指针(数组指针)。

  • int **p;

    • p 首先与最近的 * 结合,表明 p 是一个指针。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值