C 语言数组指针与指针数组深度剖析:一道 VIP 笔试题引发的思考 随笔#2

发布于:2025-06-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

首先我这篇文章我是为什么想写呢出于什么机缘巧合呢就是上面这张练习这五大大厂面试题的时候绕来绕去给哥们绕晕了!
相信很多朋友在学习 C 语言时,都曾对 sizeof 运算符和指针运算感到有些迷茫。别担心,本文将由浅入深,结合具体的代码示例,带你彻底弄懂这些概念,让你在面对类似的面试题时能够游刃有余。

一、温故知新:数组指针与指针数组

在深入分析题目之前,我们先来简单回顾一下数组指针和指针数组的区别,这对于理解后续的内容至关重要。

1. 数组指针(Pointer to Array)

数组指针是一个指针,它指向的是一个数组。其定义形式如下:

C

数据类型 (*指针变量名)[数组大小];

例如,int (*ptr)[4] 表示 ptr 是一个指向包含 4 个 int 类型元素的数组的指针。

2. 指针数组(Array of Pointers)

指针数组是一个数组,它的每个元素都是一个指针。其定义形式如下:

C

数据类型 *数组名[数组大小];

例如,int *arr[3] 表示 arr 是一个包含 3 个指向 int 类型元素的指针的数组。

核心区别在于:前者是一个指针,指向整个数组;后者是一个数组,其元素是指针。

二、VIP 笔试题解析

现在,让我们来看一下这道引发我们思考的“VIP”笔试题:

C

#include <stdio.h>

int main() {
    int b12[3][4] = {0};
    printf("%ld \n", sizeof(b12[0]));
    printf("%ld \n", sizeof(*b12));
    printf("%ld \n", sizeof(*(b12 + 1)));
    printf("%ld \n", sizeof(*(&b12[0] + 1)));
    //#self  !!!vip  错了!应该就是a[1]的大小

    printf("%ld \n", sizeof(b12 + 1));
    // #self !!!vip b12+1就是行指针往后移动一个int[4] 的数量
    printf("%ld \n", sizeof(&b12[0] + 1));
    //a[0]就是*a,&(*a) = a ,就等于sizeof(a)
    printf("%ld \n", sizeof(b12[0] + 1));
    //!!!vip #self  相当于往后移动一个元素,非常重要,直接是一个元素
    printf("%ld \n", sizeof(*(b12[0] + 1)));
    //一个整数 !!!vip#self

    printf("\n第二大题: \n");
    int b13[]  = {12,2,3,4};
    printf("%ld \n ",sizeof(&b13));
    printf("%ld \n " , sizeof(*&b13));
    printf("%ld \n",sizeof(&b13+1));

    printf("\n第三大题: \n");
    int b14[] = {1,2,3,4,5};
    int* ptr = (int* )(&b14+1);
    printf("%d , %d \n\n\n",*(b14+1),*(ptr-1));
    //b14 类型: int [5]
    // !!! 错题:& b14类型: int (*)[5] 指向数组的一个指针

    printf("\n第四大题: \n");
    int b15[2][5] = {1,2,3,4,5,6,7,8,9,10};
    int* ptr1 = (int*)(&b15+1);
    int* ptr2 = (int*)(*(b15+1));
    printf("%d , %d ",*(ptr1-1),*(ptr2-1));

    printf("\n第五大题: \n");
    int b16[5][5];
    int(*p16 )[4];
    p16 = b16 ;
    printf("%d \n",&p16[2][1]-&b16[2][1]);

    return 0;
}

接下来,我们将逐个分析每个 printf 语句的输出结果,并结合用户在代码中的注释进行深入探讨。

2.1 第一大题分析

printf("%ld \n", sizeof(b12[0]));
  • b12 的类型: int [3][4],表示一个包含 3 个元素的一维数组,每个元素又是一个包含 4 个 int 类型元素的数组。
  • b12[0] 的类型: int [4],表示二维数组 b12 的第一个元素,即一个包含 4 个 int 类型元素的一维数组。
  • sizeof(b12[0]): 计算一个包含 4 个 int 类型元素的数组的大小。假设 int 类型占用 4 个字节,那么 sizeof(b12[0]) 的结果就是 4 * 4 = 16 字节。
printf("%ld \n", sizeof(*b12));
  • b12 在表达式中会退化为指向其第一个元素的指针,类型是 int (*)[4],即一个指向包含 4 个 int 类型元素的数组的指针。
  • *b12:b12 进行解引用,得到的是 b12 指向的第一个元素,也就是 b12[0],其类型是 int [4]
  • sizeof(*b12): 计算 b12[0] 的大小,结果同样是 16 字节。
printf("%ld \n", sizeof(*(b12 + 1)));
  • b12 + 1: b12 是指向 int [4] 的指针,加 1 会移动一个 int [4] 的大小,指向 b12 的第二个元素,即 b12[1]
  • *(b12 + 1):b12 + 1 进行解引用,得到的是 b12[1],其类型是 int [4]
  • sizeof(*(b12 + 1)): 计算 b12[1] 的大小,结果仍然是 16 字节。
  • 用户的注释 // #self !!!vip 错了!应该就是a[1]的大小 是正确的! 这里的 a 指的是 b12,所以 *(b12 + 1) 的大小就是 b12 的第二个元素 b12[1] 的大小。
printf("%ld \n", sizeof(*(&b12[0] + 1)));
  • b12[0]: 类型是 int [4]
  • &b12[0]:b12[0] 的地址,得到的是一个指向包含 4 个 int 类型元素的数组的指针,类型是 int (*)[4]
  • &b12[0] + 1: 指针加 1,移动一个 int [4] 的大小,指向 b12 的第二个元素,也就是 &b12[1]
  • *(&b12[0] + 1):&b12[0] + 1 进行解引用,得到的是 b12[1],其类型是 int [4]
  • sizeof(*(&b12[0] + 1)): 计算 b12[1] 的大小,结果是 16 字节。
printf("%ld \n", sizeof(b12 + 1));
  • b12: 类型为 int [3][4],在表达式中退化为指向其第一个元素的指针,类型是 int (*)[4]
  • b12 + 1: 指针加 1,移动一个 int [4] 的大小,指向 b12 的第二个元素,b12[1]
  • sizeof(b12 + 1): 计算指针 b12 + 1 本身的大小。在 32 位系统中,指针大小通常为 4 字节;在 64 位系统中,指针大小通常为 8 字节。
printf("%ld \n", sizeof(&b12[0] + 1));
  • b12[0]: 类型是 int [4]
  • &b12[0]:b12[0] 的地址,得到的是一个指向 int [4] 的指针,类型是 int (*)[4]
  • &b12[0] + 1: 指针加 1,移动一个 int [4] 的大小,指向 b12[1]
  • sizeof(&b12[0] + 1): 计算指针 &b12[0] + 1 本身的大小,结果同样是指针的大小(4 或 8 字节)。
  • 用户的注释 //a[0]就是*a,&(*a) = a ,就等于sizeof(a) 描述了数组名和指向数组首元素的指针之间的关系,但在 sizeof 运算符中,数组名不会退化为指针,sizeof(a) 会计算整个数组的大小。然而,在这里我们讨论的是 &b12[0] + 1,它是一个明确的指针。
printf("%ld \n", sizeof(b12[0] + 1));
  • b12[0]: 类型是 int [4],在表达式中会退化为指向其第一个元素的指针,类型是 int *
  • b12[0] + 1: 指针加 1,移动一个 int 的大小,指向 b12[0] 的第二个元素。
  • sizeof(b12[0] + 1): 计算指针 b12[0] + 1 本身的大小,结果是指针的大小(4 或 8 字节)。
  • 用户的注释 //!!!vip #self 相当于往后移动一个元素,非常重要,直接是一个元素 是基本正确的。 这里移动的是 b12[0] 这个一维数组中的一个元素。
printf("%ld \n", sizeof(*(b12[0] + 1)));
  • b12[0] + 1: 是一个指向 b12[0] 的第二个元素的 int * 类型指针。
  • *(b12[0] + 1): 对该指针进行解引用,得到的是一个 int 类型的元素。
  • sizeof(*(b12[0] + 1)): 计算一个 int 类型元素的大小,结果是 4 字节(假设 int 占用 4 字节)。
  • 用户的注释 //一个整数 !!!vip#self 是正确的。

2.2 第二大题分析

printf("%ld \n ",sizeof(&b13));
  • b13 的类型: int [4],表示一个包含 4 个 int 类型元素的一维数组。
  • &b13: 取数组 b13 的地址,得到的是一个指向包含 4 个 int 类型元素的数组的指针,类型是 int (*)[4]
  • sizeof(&b13): 计算指针 &b13 本身的大小,结果是指针的大小(4 或 8 字节)。
printf("%ld \n " , sizeof(*&b13));
  • &b13: 类型是 int (*)[4]
  • *&b13:&b13 进行解引用,得到的是 b13 本身,其类型是 int [4]
  • sizeof(*&b13): 计算数组 b13 的大小,结果是 4 * 4 = 16 字节。
printf("%ld \n",sizeof(&b13+1));
  • &b13: 类型是 int (*)[4]
  • &b13 + 1: 指针加 1,移动一个 int [4] 的大小,指向数组 b13 之后紧邻的内存位置。
  • sizeof(&b13 + 1): 计算指针 &b13 + 1 本身的大小,结果是指针的大小(4 或 8 字节)。

2.3 第三大题分析

int* ptr = (int* )(&b14+1);
  • b14 的类型: int [5]
  • &b14 的类型: int (*)[5],指向包含 5 个 int 类型元素的数组的指针。用户的注释是正确的!
  • &b14 + 1: 指针加 1,移动一个 int [5] 的大小,指向数组 b14 之后紧邻的内存位置。
  • (int )(&b14 + 1):* 将 int (*)[5] 类型的指针强制转换为 int * 类型的指针。现在 ptr 指向数组 b14 最后一个元素之后的一个 int 大小的内存位置。
printf("%d , %d \n\n\n",*(b14+1),*(ptr-1));
  • b14 + 1: b14 在表达式中退化为指向其第一个元素的指针 int *。加 1 后指向 b14 的第二个元素 b14[1]
  • *(b14 + 1): 解引用,得到 b14[1] 的值,即 2
  • ptr - 1: ptr 当前指向 b14 之后的位置,减 1 后指向 b14 的最后一个元素 b14[4]
  • *(ptr - 1): 解引用,得到 b14[4] 的值,即 5

因此,该 printf 语句的输出结果是 2 , 5

2.4 第四大题分析

int* ptr1 = (int*)(&b15+1);
  • b15 的类型: int [2][5]
  • &b15 的类型: int (*)[2][5],指向包含 2 个元素的一维数组,每个元素是包含 5 个 int 的数组的指针。
  • &b15 + 1: 指针加 1,移动一个 int [2][5] 的大小,指向数组 b15 之后紧邻的内存位置。
  • (int )(&b15 + 1):* 将指针强制转换为 int * 类型。ptr1 指向整个二维数组 b15 之后的一个 int 大小的内存位置。
int* ptr2 = (int*)(*(b15+1));
  • b15 在表达式中退化为指向其第一个元素的指针,类型是 int (*)[5],指向二维数组的第一行 b15[0]
  • b15 + 1: 指针加 1,移动一个 int [5] 的大小,指向二维数组的第二行 b15[1]
  • *(b15 + 1): 解引用,得到 b15[1],其类型是 int [5]。在表达式中,b15[1] 会再次退化为指向其第一个元素的指针,类型是 int *,指向 b15[1][0]
  • (int)(*(b15 + 1)):* 将 int * 类型的指针强制转换为 int * 类型(实际上没有类型转换)。ptr2 指向 b15[1][0]
printf("%d , %d ",*(ptr1-1),*(ptr2-1));
  • ptr1 - 1: ptr1 指向 b15 之后的位置,减 1 后指向 b15 的最后一个元素 b15[1][4],其值为 10
  • *(ptr1 - 1): 解引用,得到 10
  • ptr2 - 1: ptr2 指向 b15[1][0],减 1 后指向 b15[0] 的最后一个元素 b15[0][4],其值为 5
  • *(ptr2 - 1): 解引用,得到 5

因此,该 printf 语句的输出结果是 10 , 5

2.5 第五大题分析

int(*p16 )[4];
  • 定义了一个数组指针 p16,它指向一个包含 4 个 int 类型元素的数组。
p16 = b16 ;
  • b16 的类型: int [5][5]
  • b16 在赋值给 p16 时会发生类型不兼容b16 退化为指向其第一个元素的指针,类型是 int (*)[5],而 p16 的类型是 int (*)[4]。尽管如此,编译器通常会允许这种赋值,但会导致一些潜在的问题,因为 p16 会按照每行 4 个 int 的步长进行运算,而实际每行有 5 个 int
printf("%d \n",&p16[2][1]-&b16[2][1]);
  • p16[2]: 由于 p16 指向每行 4 个元素的数组,p16[2] 实际上访问的是 b16 中偏移量为 2 * 4 行的起始位置(将 b16 看作一维数组)。
  • p16[2][1]: 访问该“行”的第二个元素。在内存中,这相当于访问 b16 中第 (2 * 4) + 1 = 9int 元素(从 0 开始计数)。
  • &p16[2][1]: 取该元素的地址。
  • b16[2]: 访问 b16 的第三行。
  • b16[2][1]: 访问 b16 第三行的第二个元素。
  • &b16[2][1]: 取该元素的地址.

现在,我们来计算这两个地址之间的差值。假设 int 占用 4 个字节。

  • &b16[2][1] 的内存偏移量是 2 * 5 + 1 = 11 (从 0 开始计数)。
  • &p16[2][1] 按照 p16 的步长计算,其访问的实际是 b16 中索引为 (2 * 4) + 1 = 9 的元素。

地址差值是 (11 - 9) = 2int 类型的大小。因此,输出结果应该是 2

需要特别注意的是 p16 = b16; 这种赋值方式是不安全的,因为它涉及了不兼容的指针类型。在实际编程中应该避免这种操作。

三、面试重点与常见误区

通过这道“VIP”笔试题的分析,我们可以总结出以下几个在面试中经常被考察的知识点和常见的误区:

  1. sizeof 运算符: 能够正确计算不同类型(包括数组和指针)的大小。记住,sizeof(数组名) 返回整个数组的大小,而数组名在表达式中通常会退化为指向第一个元素的指针。
  2. 指针运算: 理解指针加减整数时,移动的步长是所指向数据类型的大小。对于指向数组的指针,步长是整个数组的大小。
  3. 数组名与指向数组首元素的指针: 区分数组名本身和指向数组首元素的指针之间的区别。在 sizeof 和取地址运算符 & 后,数组名不会退化为指针。
  4. 多维数组的内存布局: 理解多维数组在内存中是线性存储的,按行优先(C 语言中)。
  5. 指向数组的指针(数组指针): 掌握其定义和使用方法,特别是在函数参数传递中。
  6. 类型强制转换: 了解类型强制转换可能带来的风险,以及在什么情况下应该谨慎使用。
  7. 指针与 const: (虽然本题没有直接考察,但也是高频考点) 理解 const 修饰指针的不同方式及其含义。

常见误区:

  • 混淆数组指针和指针数组的定义和含义。
  • 认为 sizeof(数组名) 返回的是指针的大小。
  • 对指向多维数组的指针运算的步长理解不准确。
  • 不清楚数组名在不同语境下(如作为函数参数)的行为。

四、总结

这道“VIP”笔试题虽然看似简单,但却涵盖了 C 语言中数组和指针的诸多核心概念。通过对每一行代码的深入分析,我们不仅理解了 sizeof 运算符和指针运算的规则,更重要的是加深了对数组在内存中布局以及数组指针和指针数组之间差异的认识。

希望本文的分析能够帮助你更好地理解这些重要的 C 语言知识点,在未来的学习和面试中更加自信。如果你觉得这篇文章对你有帮助,请点赞和分享,让更多的人受益!后续我们还会带来更多关于 C/C++ 的技术分享,敬请期待!

------------------------------------------------------------------------------------------------------------------------2025年更新 6月4号 

第一部分,我们详细剖析了一道“VIP”笔试题,并回顾了数组指针与指针数组的基础概念。为了帮助大家更好地应对像腾讯、阿里、字节跳动等顶级大厂的 C 语言面试,本部分将继续深入探讨与数组和指针相关的高级主题,并结合实际面试中常见的题型进行扩展分析,提供更全面的知识覆盖和实战指导。

五、动态内存分配与数组、指针

动态内存分配是 C 语言中一项至关重要的技术,它允许程序在运行时根据需要申请和释放内存。这与静态分配(在编译时确定内存大小)形成对比。在面试中,动态内存分配经常与数组和指针结合考察。

5.1 malloc, calloc, realloc, free

首先,我们回顾一下动态内存分配的四个核心函数:

  • malloc(size_t size): 分配 size 字节的内存块,不对内存进行初始化。
  • calloc(size_t num, size_t size): 分配 num * size 字节的内存块,并将所有字节初始化为 0。
  • realloc(void *ptr, size_t size): 尝试重新调整 ptr 指向的内存块的大小为 size 字节。如果可以原地扩展,则返回原指针;否则,会分配新的内存块,将原有数据复制过来,并释放旧的内存块,返回新内存块的指针。如果 ptrNULL,则等同于 malloc(size)
  • free(void *ptr): 释放 ptr 指向的动态分配的内存块。ptr 必须是之前通过 malloc, calloc, 或 realloc 返回的指针。释放 NULL 指针是安全的。

5.2 动态分配的数组

5.2.1 动态分配一维数组

C

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n;
    printf("请输入数组大小: ");
    scanf("%d", &n);

    // 使用 malloc 分配动态数组
    int *arr_malloc = (int*)malloc(n * sizeof(int));
    if (arr_malloc == NULL) {
        perror("malloc failed");
        return 1;
    }

    printf("使用 malloc 分配的数组 (未初始化):\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr_malloc[i]); // 内容是未知的
    }
    printf("\n");

    // 使用 calloc 分配动态数组并初始化为 0
    int *arr_calloc = (int*)calloc(n, sizeof(int));
    if (arr_calloc == NULL) {
        perror("calloc failed");
        free(arr_malloc);
        return 1;
    }

    printf("使用 calloc 分配的数组 (已初始化为 0):\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr_calloc[i]);
    }
    printf("\n");

    // 使用 realloc 调整数组大小
    int new_n = n * 2;
    int *arr_realloc = (int*)realloc(arr_malloc, new_n * sizeof(int));
    if (arr_realloc == NULL) {
        perror("realloc failed");
        free(arr_calloc);
        return 1;
    }
    printf("使用 realloc 调整大小后的数组 (原有数据可能保留):\n");
    for (int i = 0; i < new_n; i++) {
        printf("%d ", arr_realloc[i]);
    }
    printf("\n");

    free(arr_realloc); // 注意:这里释放的是 realloc 返回的指针
    free(arr_calloc);

    return 0;
}
5.2.2 动态分配二维数组

动态分配二维数组有几种常见方法:

方法一:使用指针数组

C

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) {
        perror("malloc failed for rows");
        return 1;
    }

    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            perror("malloc failed for columns");
            // 释放之前已分配的内存
            for (int j = 0; j < i; j++) {
                free(matrix[j]);
            }
            free(matrix);
            return 1;
        }
    }

    // 初始化矩阵
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }

    // 打印矩阵
    printf("动态分配的二维数组 (使用指针数组):\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);

    return 0;
}

方法二:使用连续的一块内存

C

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int *matrix = (int*)malloc(rows * cols * sizeof(int));
    if (matrix == NULL) {
        perror("malloc failed for matrix");
        return 1;
    }

    // 初始化矩阵
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i * cols + j] = i * cols + j;
        }
    }

    // 打印矩阵
    printf("动态分配的二维数组 (使用连续内存):\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i * cols + j]);
        }
        printf("\n");
    }

    free(matrix);
    return 0;
}

方法三:使用数组指针

C

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int (*matrix)[cols] = (int(*)[cols])malloc(rows * cols * sizeof(int));
    if (matrix == NULL) {
        perror("malloc failed for matrix");
        return 1;
    }

    // 初始化矩阵
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }

    // 打印矩阵
    printf("动态分配的二维数组 (使用数组指针):\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    free(matrix);
    return 0;
}

5.3 指针运算与动态内存

对动态分配的内存进行指针运算与静态分配的数组类似,但需要格外注意边界,避免访问越界。

5.4 内存泄漏

内存泄漏是指程序在使用完动态分配的内存后,没有及时使用 free 函数释放,导致这部分内存无法被其他程序使用。长时间运行的程序如果发生内存泄漏,可能会耗尽系统内存,导致程序崩溃。

避免内存泄漏是面试中的重要考察点。常见的预防措施包括:

  • 每次使用 malloc, calloc, 或 realloc 后,确保最终都有对应的 free 调用。
  • 在函数中分配的内存,确保在函数退出前被释放,或者将其所有权明确地传递给调用方。
  • 对于复杂的程序,可以使用内存泄漏检测工具(如 Valgrind)进行检测。

面试题 7:请指出以下代码可能存在的内存泄漏问题:

C

#include <stdio.h>
#include <stdlib.h>

char* allocateString(int size) {
    char *str = (char*)malloc(size * sizeof(char));
    return str;
}

int main() {
    char *message = allocateString(50);
    sprintf(message, "Hello, World!");
    printf("%s\n", message);
    // free(message); // 忘记释放内存
    return 0;
}

答案与分析:

allocateString 函数中通过 malloc 分配了内存,并将指向该内存的指针返回给 main 函数的 message 变量。但是,在 main 函数中,并没有使用 free(message) 来释放这块内存,导致了内存泄漏。

5.5 动态内存分配相关面试题

  • 请解释 malloc, calloc, realloc 的区别。
  • 动态分配的内存在哪个内存区域?(堆)
  • 如何判断 malloc 是否分配成功?(检查返回值是否为 NULL
  • 什么是内存泄漏?如何避免?
  • free 一个已经被 free 过的指针会发生什么?(未定义行为,通常会导致程序崩溃)
  • 可以使用 free 释放静态分配的内存吗?(不可以,会导致错误)
  • 编写一个函数,动态创建一个 mn 列的二维整型数组,并初始化所有元素为 0。

六、函数指针与指针数组

函数指针是指向函数的指针,它存储了函数在内存中的地址。指针数组的元素可以是任何类型的指针,包括函数指针。

6.1 函数指针的定义和使用

C

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    int (*funcPtr)(int, int); // 定义一个函数指针,指向接受两个 int 参数并返回 int 的函数

    funcPtr = add;
    printf("Addition: %d\n", funcPtr(5, 3)); // 通过函数指针调用 add 函数

    funcPtr = subtract;
    printf("Subtraction: %d\n", funcPtr(5, 3)); // 通过函数指针调用 subtract 函数

    return 0;
}

6.2 函数指针数组

函数指针数组是一个数组,其每个元素都是一个函数指针。这在需要根据条件执行不同函数时非常有用,例如实现命令分发器或状态机。

C

#include <stdio.h>
#include <stdlib.h>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) {
    if (b == 0) {
        fprintf(stderr, "Error: Division by zero\n");
        exit(EXIT_FAILURE);
    }
    return a / b;
}

int main() {
    int (*operations[4])(int, int) = {add, subtract, multiply, divide};
    int choice, num1, num2;

    printf("Enter two numbers: ");
    scanf("%d %d", &num1, &num2);

    printf("Choose operation (0: add, 1: subtract, 2: multiply, 3: divide): ");
    scanf("%d", &choice);

    if (choice >= 0 && choice < 4) {
        printf("Result: %d\n", operations[choice](num1, num2));
    } else {
        printf("Invalid choice\n");
    }

    return 0;
}

6.3 回调函数

函数指针的一个重要应用是实现回调函数。回调函数是指将一个函数的指针作为参数传递给另一个函数,使得被调用的函数可以在适当的时候“回调”执行传递进来的函数。

C

#include <stdio.h>
#include <stdlib.h>

// 回调函数类型定义
typedef void (*operation_callback)(int);

// 接受回调函数的函数
void processArray(int *arr, int size, operation_callback callback) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]); // 调用回调函数处理每个元素
    }
}

// 具体的回调函数
void printElement(int element) {
    printf("%d ", element);
}

void squareElement(int element) {
    printf("%d ", element * element);
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);

    printf("Printing elements: ");
    processArray(numbers, size, printElement);
    printf("\n");

    printf("Squaring elements: ");
    processArray(numbers, size, squareElement);
    printf("\n");

    return 0;
}

6.4 函数指针相关面试题

  • 什么是函数指针?如何定义和使用?
  • 函数名和 &函数名 的区别是什么?(通常情况下,它们的值是相同的,都表示函数的地址)
  • 什么是函数指针数组?有什么应用场景?
  • 什么是回调函数?在哪些场景下会使用回调函数?
  • 请编写一个程序,使用函数指针实现一个简单的计算器,可以进行加减乘除运算。

七、指针与结构体 (Structs)

结构体是 C 语言中组合不同类型数据的重要方式。指针可以指向结构体,也可以作为结构体的成员。

7.1 指向结构体的指针

C

#include <stdio.h>
#include <string.h>

struct Person {
    char name[20];
    int age;
};

int main() {
    struct Person person1 = {"Alice", 30};
    struct Person *ptrPerson;

    ptrPerson = &person1;

    printf("Name: %s, Age: %d\n", person1.name, person1.age);
    printf("Name (via pointer): %s, Age (via pointer): %d\n", ptrPerson->name, ptrPerson->age);

    // 使用指针修改结构体成员
    strcpy(ptrPerson->name, "Bob");
    ptrPerson->age = 25;

    printf("Updated Name: %s, Updated Age: %d\n", person1.name, person1.age);

    return 0;
}

7.2 结构体数组与指向结构体的指针

C

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point points[3] = {{1, 2}, {3, 4}, {5, 6}};
    struct Point *ptrPoints;

    ptrPoints = points; // 指向结构体数组的第一个元素

    for (int i = 0; i < 3; i++) {
        printf("Point %d: (%d, %d)\n", i, ptrPoints[i].x, ptrPoints[i].y);
        printf("Point %d (via pointer): (%d, %d)\n", i, (ptrPoints + i)->x, (ptrPoints + i)->y);
    }

    return 0;
}

7.3 结构体包含指针成员

结构体可以包含指向其他数据类型的指针,这在实现链表、树等动态数据结构时非常常见。

C

#include <stdio.h>
#include <stdlib.h>

struct Node {
    int data;
    struct Node *next;
};

int main() {
    struct Node *head = (struct Node*)malloc(sizeof(struct Node));
    if (head == NULL) {
        perror("malloc failed");
        return 1;
    }
    head->data = 10;
    head->next = NULL;

    struct Node *second = (struct Node*)malloc(sizeof(struct Node));
    if (second == NULL) {
        perror("malloc failed");
        free(head);
        return 1;
    }
    second->data = 20;
    second->next = NULL;
    head->next = second;

    printf("Linked List: %d -> %d -> NULL\n", head->data, head->next->data);

    free(head->next);
    free(head);

    return 0;
}

7.4 结构体相关面试题

  • 如何定义一个指向结构体的指针?如何通过指针访问结构体的成员?
  • 结构体数组在内存中是如何存储的?
  • 结构体中包含指针成员有什么作用?在什么场景下会使用?
  • 什么是自引用结构体?如何定义?(用于实现链表等数据结构)
  • 请设计一个表示学生的结构体,包含姓名、年龄和指向下一个学生的指针,并创建一个包含三个学生的简单链表。

八、指针与字符串

字符串在 C 语言中本质上是字符数组,而指针在处理字符串时非常灵活和高效.

8.1 字符串操作

C

#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "Hello";
    char *str2 = "World";

    printf("str1: %s\n", str1);
    printf("str2: %s\n", str2);

    // 使用指针遍历字符串
    printf("str1 (using pointer): ");
    for (char *p = str1; *p != '\0'; p++) {
        printf("%c", *p);
    }
    printf("\n");

    // 字符串拷贝
    char str3[20];
    strcpy(str3, str2);
    printf("str3 (after strcpy): %s\n", str3);

    // 字符串比较
    if (strcmp(str1, str2) == 0) {
        printf("str1 and str2 are equal\n");
    } else {
        printf("str1 and str2 are not equal\n");
    }

    return 0;
}

8.2 const char* vs. char[]

  • char str[] = "Hello";: 在栈上分配一个足够存储 "Hello" (包括 null 终止符) 的字符数组,字符串内容可以修改。
  • const char* str = "Hello";: 指针 str 指向存储在只读内存区域(常量区)的字符串字面量 "Hello",字符串内容通常不允许修改。尝试修改可能会导致程序崩溃。

8.3 字符串数组

C

#include <stdio.h>

int main() {
    // 使用 char 指针数组存储字符串
    char *names1[] = {"Alice", "Bob", "Charlie"};
    int size1 = sizeof(names1) / sizeof(names1[0]);
    for (int i = 0; i < size1; i++) {
        printf("Name %d: %s\n", i, names1[i]);
    }

    // 使用二维 char 数组存储字符串 (每个字符串长度有限制)
    char names2[3][10] = {"David", "Eve", "Frank"};
    for (int i = 0; i < 3; i++) {
        printf("Name %d: %s\n", i, names2[i]);
    }

    return 0;
}

8.4 字符串相关面试题

  • 字符串在 C 语言中是如何表示的?
  • char str[] = "abc";char *str = "abc"; 有什么区别?
  • 如何使用指针遍历一个字符串?
  • 请编写一个函数,计算一个字符串的长度(不使用 strlen)。
  • 请编写一个函数,将一个字符串反转(使用指针实现)。
  • 解释 const char* 的含义。

九、更高级的指针运算与数组索引

在面试中,有时会遇到更复杂的指针运算和数组索引的题目,旨在考察对底层内存操作的理解。

练习题 8:分析以下代码的输出:

C

#include <stdio.h>

int main() {
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int *p = &arr[0][0];

    printf("%d\n", *(p + 4));
    printf("%d\n", *(*(arr + 1) + 2));
    printf("%d\n", arr[2][0]);
    printf("%d\n", (*(arr + 2))[1]);

    return 0;
}

答案与分析:

  • int *p = &arr[0][0];: p 指向二维数组 arr 的第一个元素 arr[0][0]
  • *(p + 4): pint* 类型,p + 4 向后移动了 4 个 int 的位置,指向 arr[1][1],其值为 5
  • *(*(arr + 1) + 2):
    • arr 退化为指向第一行的指针 int (*)[3]
    • arr + 1 指向第二行 arr[1]
    • *(arr + 1) 得到第二行 arr[1], 类型为 int [3], 再次退化为指向 arr[1][0] 的指针 int*
    • *(arr + 1) + 2 指向 arr[1][2]
    • *(*(arr + 1) + 2) 得到 arr[1][2] 的值,即 6
  • arr[2][0]: 直接访问第三行第一个元素,值为 7
  • (*(arr + 2))[1]:
    • arr + 2 指向第三行 arr[2]
    • *(arr + 2) 得到第三行 arr[2], 类型为 int [3], 再次退化为指向 arr[2][0] 的指针 int*
    • (*(arr + 2))[1] 访问该指针指向的数组的第二个元素,即 arr[2][1],其值为 8

因此,输出结果是:

5
6
7
8

十、总结 (Part 2)

本部分我们继续深入探讨了 C 语言中数组和指针的高级应用,涵盖了动态内存分配、函数指针、结构体与指针、字符串与指针以及更复杂的指针运算。这些都是顶级大厂面试中非常重要的考察点。通过掌握这些知识,并进行大量的练习,你将能够更自信地应对各种笔试和面试题目。

记住,熟练掌握指针和数组的关键在于多写代码,多思考内存的布局和变化。希望本文能为你提供更全面的学习指导。在下一部分,我们将继续探讨其他 C 语言面试中的常见主题,例如位运算、文件操作、预处理器等,敬请期待。