← 返回文章列表

C 语言(七)|指针进阶:数组指针、函数指针与复杂声明

深入探讨C语言指针的高级用法,包括指针数组、数组指针、函数指针、指针与字符串、内存管理等核心知识点。

版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。

前言

指针是C语言的精髓,掌握指针意味着你真正理解了计算机的内存管理机制。本篇文章我们继续深入探讨指针的高级用法,包括指针数组、数组指针、函数指针、命令行参数、字符串与指针、动态内存管理等核心知识点。这些内容是C语言学习的重要里程碑,也是面试中频繁考察的重点。

一、指针数组与数组指针

1.1 指针数组

定义:指针数组是一个数组,其元素是指针。

语法格式:type *array_name[array_size];

#include <stdio.h>

int main() {
    int a = 10, b = 20, c = 30;
    int *arr[3] = {&a, &b, &c};  // 指针数组

    for (int i = 0; i < 3; i++) {
        printf("arr[%d] = %p, *arr[%d] = %d\n", i, arr[i], i, *arr[i]);
    }
    return 0;
}

1.2 数组指针

定义:数组指针是一个指针,指向一个数组。

语法格式:type (*pointer_name)[array_size];

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p)[5] = &arr;  // 数组指针,指向整个数组

    printf("arr = %p\n", arr);
    printf("&arr = %p\n", &arr);
    printf("p = %p\n", p);
    printf("*p = %p\n", *p);

    printf("(*p)[0] = %d\n", (*p)[0]);
    printf("(*p)[2] = %d\n", (*p)[2]);
    return 0;
}

1.3 区分指针数组与数组指针

记忆技巧:看*与数组名的优先级。

  • int arr[5]:[]优先级高于,所以是"指针的数组"—— 指针数组
  • int (p)[5]:括号优先级最高,先与p结合,所以是"指向数组的指针"—— 数组指针
#include <stdio.h>

int main() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int *p1[5] = {&a, &b, &c, &d, &e};  // 指针数组:5个int*组成的数组
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p2)[5] = &arr;                // 数组指针:指向int[5]的指针

    printf("指针数组大小:%zu\n", sizeof(p1));  // 5 * 8 = 40(64位系统)
    printf("数组指针大小:%zu\n", sizeof(p2));  // 8(64位系统)

    return 0;
}

二、二维数组与指针

2.1 二维数组名的理解

关键结论:

  • 二维数组名是指向"第一行数组"的指针类型(int(*)[列数])
  • arr[i]等价于*(arr+i),表示第i行的首地址
  • arr[i][j]等价于((arr+i)+j),表示第i行第j列的元素
#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    printf("arr = %p\n", arr);
    printf("arr[0] = %p\n", arr[0]);
    printf("&arr[0][0] = %p\n", &arr[0][0]);

    printf("*arr = %p\n", *arr);        // arr[0]的地址
    printf("**arr = %d\n", **arr);       // arr[0][0]的值

    printf("arr + 1 = %p\n", arr + 1);  // arr[1]的地址
    printf("*(arr + 1) = %p\n", *(arr + 1));  // arr[1]的地址
    printf("*(*(arr + 1) + 2) = %d\n", *(*(arr + 1) + 2));  // arr[1][2]的值
    return 0;
}

2.2 二维数组的遍历方式

#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 方式1:下标法
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%2d ", arr[i][j]);
        }
        printf("\n");
    }

    // 方式2:指针法
    printf("\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%2d ", *(*(arr + i) + j));
        }
        printf("\n");
    }

    // 方式3:行列指针
    printf("\n");
    for (int (*p)[4] = arr; p < arr + 3; p++) {
        for (int *q = *p; q < *p + 4; q++) {
            printf("%2d ", *q);
        }
        printf("\n");
    }
    return 0;
}

三、函数指针

3.1 理解函数指针

函数也有地址:函数是存储在代码区的代码块,函数名就是这段代码的首地址。

函数指针的定义:return_type (*pointer_name)(parameters);

#include <stdio.h>

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

int main() {
    int (*p)(int, int) = add;  // 函数指针,指向add函数

    printf("add的地址:%p\n", add);
    printf("p的值:%p\n", p);

    printf("(*p)(3, 4) = %d\n", (*p)(3, 4));  // 通过函数指针调用
    printf("p(3, 4) = %d\n", p(3, 4));         // 直接通过指针调用
    return 0;
}

3.2 函数指针的使用场景

场景1:作为参数传递(回调函数)

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }

int calc(int a, int b, int (*op)(int, int)) {
    return op(a, b);
}

int main() {
    printf("%d\n", calc(10, 5, add));  // 15
    printf("%d\n", calc(10, 5, sub));  // 5
    printf("%d\n", calc(10, 5, mul));  // 50
    printf("%d\n", calc(10, 5, div));  // 2
    return 0;
}

场景2:作为返回值

#include <stdio.h>

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

int (*select(char op))(int, int) {
    if (op == '+') return add;
    if (op == '-') return sub;
    return NULL;
}

int main() {
    int (*p)(int, int) = select('+');
    printf("%d\n", p(10, 5));  // 15
    return 0;
}

3.3 函数指针实战:简易计算器

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void Display() {
    printf("===== 简易计算器 =====\n");
    printf("1. 加法 (+)   2. 减法 (-)\n");
    printf("3. 乘法 (*)   4. 除法 (/)\n");
    printf("0. 退出程序\n");
    printf("=======================\n");
}

// 四则运算核心函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
    // 防除数为0异常
    if (b == 0) {
        printf("错误:除数不能为0!\n");
        return -1;
    }
    return a / b;
}

// 函数指针作为参数:统一调用逻辑
void calc(int (*p)(int, int)) {
    int a, b;
    printf("请输入两个操作数(空格分隔):");
    scanf("%d%d", &a, &b);
    // 单独处理除法异常
    if (p == div && b == 0) return;
    printf("运算结果:%d\n\n", p(a, b));
}

int main() {
    int input = 1;
    do {
        Display();
        printf("请选择操作(0-4):");
        scanf("%d", &input);
        switch (input) {
            case 1: calc(add); break;
            case 2: calc(sub); break;
            case 3: calc(mul); break;
            case 4: calc(div); break;
            case 0: printf("退出程序~\n"); break;
            default: printf("输入无效,请重新选择!\n\n"); break;
        }
    } while (input);
    return 0;
}

3.4 函数指针数组与转移表

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

// 四则运算函数(接口保持一致)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) {
    if (b == 0) { printf("错误:除数不能为0!\n"); return -1; }
    return a / b;
}
// 空函数:处理非法输入
int empty(int a, int b) { printf("输入无效!\n"); return 0; }

int main() {
    int a, b, op;
    // 函数指针数组(转移表):下标0-4对应空/加/减/乘/除
    int (*calc[5])(int, int) = {empty, add, sub, mul, div};

    while (1) {
        printf("\n===== 转移表计算器 =====\n");
        printf("1.加 2.减 3.乘 4.除 0.退出\n");
        printf("请选择操作:");
        scanf("%d", &op);
        if (op == 0) { printf("退出程序~\n"); break; }
        if (op < 0 || op > 4) { printf("输入错误!\n"); continue; }

        printf("请输入两个操作数:");
        scanf("%d%d", &a, &b);
        // 下标直接调用,无需switch/if判断
        if (op != 4 || b != 0) {
            printf("结果:%d\n", calc[op](a, b));
        }
    }
    return 0;
}

3.5 回调函数与qsort排序

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 学生结构体定义
struct student {
    char name[10];
    int age;
};

// 回调函数1:按姓名字典序升序
int cmp_student_name(const void* x, const void* y) {
    return strcmp(((const struct student*)x)->name, ((const struct student*)y)->name);
}

// 回调函数2:按年龄升序
int cmp_student_age(const void* x, const void* y) {
    return ((const struct student*)x)->age - ((const struct student*)y)->age;
}

// 打印学生信息
void print_student(struct student a[], int len) {
    for (int i = 0; i < len; i++) {
        printf("姓名:%s,年龄:%d\n", a[i].name, a[i].age);
    }
    printf("-------------------------\n");
}

int main() {
    // 初始化学生数据
    struct student a[] = { {"张三", 20}, {"李四", 19}, {"王五", 21} };
    int len = sizeof(a) / sizeof(a[0]);

    printf("原数据:\n");
    print_student(a, len);

    // 按姓名排序(传入姓名比较的回调函数)
    qsort(a, len, sizeof(struct student), cmp_student_name);
    printf("按姓名排序后:\n");
    print_student(a, len);

    // 按年龄排序(传入年龄比较的回调函数)
    qsort(a, len, sizeof(struct student), cmp_student_age);
    printf("按年龄排序后:\n");
    print_student(a, len);

    return 0;
}

3.6 qsort函数的模拟实现

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

// 通用交换函数:逐字节交换,支持任意类型数据
void Swap(char *buf1, char *buf2, size_t size) {
    for(size_t i = 0; i < size; i++) {
        char temp = *buf1;
        *buf1 = *buf2;
        *buf2 = temp;
        buf1++;
        buf2++;
    }
}

// 模拟qsort:基于冒泡排序的通用排序函数
void my_qsort(void* a, int count, int size, int(*point)(void* a, void* b)) {
    for (int i = 0; i < count - 1; i++) {
        for (int j = 0; j < count - 1 - i; j++) {
            if (point((char*)a + size * j, (char*)a + size * (j + 1)) > 0) {
                Swap((char*)a + j * size, (char*)a + (j + 1) * size, size);
            }
        }
    }
}

// 测试:复用学生排序的回调函数
struct student {
    char name[10];
    int age;
};

int cmp_student_age(const void* x, const void* y) {
    return ((const struct student*)x)->age - ((const struct student*)y)->age;
}

void print_student(struct student a[], int len) {
    for (int i = 0; i < len; i++) {
        printf("姓名:%s,年龄:%d\n", a[i].name, a[i].age);
    }
}

int main() {
    struct student a[] = { {"张三", 20}, {"李四", 19}, {"王五", 21} };
    int len = sizeof(a) / sizeof(a[0]);

    printf("自定义qsort按年龄排序后:\n");
    my_qsort(a, len, sizeof(struct student), cmp_student_age);
    print_student(a, len);

    return 0;
}

四、字符指针变量(面试高频核心考点)

4.1 指向常量字符串的核心特性

C 语言没有专门的字符串类型,字符串是以\0为结束标志的字符数组,其中常量字符串(如"abcdef")会被编译器存储在程序的只读数据区,内容不可修改,字符指针仅存储其首元素的地址。

#include <stdio.h>
int main()
{
    // p仅存储常量字符串首字符'a'的地址,非字符串整体赋值
    const char* p = "abcdef"; 
    printf("%c\n", *p);  // 解引用取首字符,输出'a'
    printf("%s\n", p);    // %s从首地址开始打印,直到遇到'\0'结束,输出"abcdef"

    // *p = 'g'; // 非法操作!编译可通过但运行直接崩溃,只读内存不可修改
    return 0;
}

核心规范:用const char*指向常量字符串,是规范的写法,能明确告知编译器与开发者 "此字符串内容不可修改",从语法层面规避误操作。

4.2 经典面试题:字符数组 vs 字符指针

这道题核心考察常量字符串的内存分配与编译器优化,是笔试必考题型:

#include <stdio.h>

int main() {
    // 字符数组——栈上分配两个独立内存空间,存储相同字符串
    char str1[] = "hello Luminous.";
    char str2[] = "hello Luminous.";

    // 字符指针——指向只读数据区的同一个常量字符串
    const char* str3 = "hello Luminous.";
    const char* str4 = "hello Luminous.";

    // 比较的是数组首元素的地址,两个独立内存地址不同
    if (str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n"); // 最终输出此行

    // 编译器做常量合并优化,两个指针指向同一块只读内存,地址相同
    if (str3 == str4)
        printf("str3 and str4 are same\n"); // 最终输出此行
    else
        printf("str3 and str4 are not same\n");

    return 0;
}

核心结论:需要修改字符串内容时,用字符数组(栈区内存可修改);仅读取字符串内容时,用字符指针(无需额外拷贝字符串,节省内存)。

4.3 指针数组模拟二维数组

指针数组的核心进阶用法是模拟二维数组,和真正的二维数组相比,它支持指向不同长度的一维数组,灵活性更高:

#include <stdio.h>
int main()
{
    // 定义3个独立的一维数组,作为模拟二维数组的"行"
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 2,3,4,5,6 };
    int arr3[] = { 3,4,5,6,7 };

    // 指针数组:每个元素都是int*,存储3个一维数组的首地址
    int* parr[3] = { arr1, arr2, arr3 };

    // 模拟二维数组的访问,parr[i][j]等价于*(*(parr + i) + j)
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

运行结果:

1 2 3 4 5 
2 3 4 5 6 
3 4 5 6 7 

核心区别:真正的二维数组内存是连续的,而指针数组模拟的二维数组,每个 "行" 是独立的一维数组,内存不连续,仅通过指针数组关联。

4.4 字符串数组

#include <stdio.h>

int main() {
    const char *names[] = {
        "Alice",
        "Bob",
        "Charlie",
        "David"
    };

    int len = sizeof(names) / sizeof(names[0]);
    for (int i = 0; i < len; i++) {
        printf("names[%d] = %s, 地址 = %p\n", i, names[i], names[i]);
    }
    return 0;
}

4.3 常见字符串操作函数(使用指针)

#include <stdio.h>

int my_strlen(const char *s) {
    int len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

char *my_strcpy(char *dest, const char *src) {
    int i = 0;
    while (src[i] != '\0') {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
    return dest;
}

char *my_strcat(char *dest, const char *src) {
    int i = 0, j = 0;
    while (dest[i] != '\0') i++;
    while (src[j] != '\0') {
        dest[i++] = src[j++];
    }
    dest[i] = '\0';
    return dest;
}

int my_strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2 && *s1 == *s2) {
        s1++;
        s2++;
    }
    return *s1 - *s2;
}

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

    printf("my_strlen(str1) = %d\n", my_strlen(str1));

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

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

    printf("my_strcmp(\"abc\", \"abd\") = %d\n", my_strcmp("abc", "abd"));
    return 0;
}

五、命令行参数

5.1 main函数的参数

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("参数个数:%d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

参数说明:

  • argc:命令行参数的个数(包括程序名)
  • argv:指针数组,每个元素指向一个参数字符串

5.2 使用示例

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("用法:%s <数字1> <数字2>\n", argv[0]);
        return 1;
    }

    int a = atoi(argv[1]);
    int b = atoi(argv[2]);

    printf("%d + %d = %d\n", a, b, a + b);
    return 0;
}

运行:program.exe 10 20

输出:10 + 20 = 30

六、动态内存管理

6.1 动态内存分配函数

函数说明
malloc分配指定字节的内存空间
calloc分配并初始化为0
realloc调整已分配内存的大小
free释放动态分配的内存

6.2 malloc与free

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

int main() {
    int n = 5;
    int *arr = (int *)malloc(n * sizeof(int));

    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
    }

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    arr = NULL;
    return 0;
}

6.3 calloc

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

int main() {
    int n = 5;
    int *arr = (int *)calloc(n, sizeof(int));

    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出0(已初始化为0)
    }
    printf("\n");

    free(arr);
    arr = NULL;
    return 0;
}

6.4 realloc

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

int main() {
    int *arr = (int *)malloc(5 * sizeof(int));

    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }

    arr = (int *)realloc(arr, 10 * sizeof(int));

    for (int i = 5; i < 10; i++) {
        arr[i] = (i + 1) * 10;
    }

    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    free(arr);
    arr = NULL;
    return 0;
}

6.5 常见的内存错误

错误1:内存泄漏

void leak() {
    int *p = (int *)malloc(sizeof(int));
    *p = 100;
    // 忘记free(p)
}

正确做法:配对使用malloc和free

错误2:野指针

int *p = (int *)malloc(sizeof(int));
free(p);
*p = 100;  // 错误:p已成为野指针

正确做法:释放后立即置为NULL

int *p = (int *)malloc(sizeof(int));
free(p);
p = NULL;

错误3:重复释放

int *p = (int *)malloc(sizeof(int));
free(p);
free(p);  // 错误:重复释放

七、void*指针

7.1 void*指针的特点

  • void*是一种"通用指针",可以指向任意类型
  • void*不能直接解引用,需要先强制转换
  • void*不能进行指针运算
#include <stdio.h>
#include <stdlib.h>

int main() {
    int a = 10;
    float b = 3.14;
    char c = 'A';

    void *p;

    p = &a;
    printf("%d\n", *(int *)p);

    p = &b;
    printf("%.2f\n", *(float *)p);

    p = &c;
    printf("%c\n", *(char *)p);

    return 0;
}

7.2 void*的使用场景

场景1:memcpy函数

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

int main() {
    int src[] = {1, 2, 3, 4, 5};
    int dest[5];

    memcpy(dest, src, sizeof(src));

    for (int i = 0; i < 5; i++) {
        printf("%d ", dest[i]);
    }
    printf("\n");
    return 0;
}

八、复杂指针声明

8.1 阅读技巧

从变量名开始,按优先级依次向外解读。

int *p[5];       // p是一个数组,元素是int*类型(指针数组)
int (*p)[5];     // p是一个指针,指向int[5]类型(数组指针)
int **p;        // p是一个指针,指向int*类型(指向指针的指针)
int *(*p)[5];   // p是一个指针,指向int*[5]类型

8.2 函数指针声明解读

int (*p)(int, int);  // p是指向函数的指针,参数为(int, int),返回int

int (*p[3])(int);    // p是数组,元素是指向int(int)的函数指针

int *(*p[3])(int);  // p是数组,元素是指向int*(int)的函数指针

九、指针与结构体

9.1 结构体指针的使用

#include <stdio.h>

struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student stu = {"Tom", 20, 95.5};
    struct Student *p = &stu;

    printf("姓名:%s\n", (*p).name);
    printf("姓名:%s\n", p->name);         // 两种方式等价
    printf("年龄:%d\n", p->age);
    printf("成绩:%.1f\n", p->score);

    p->age = 21;
    printf("修改后年龄:%d\n", stu.age);
    return 0;
}

9.2 结构体指针数组

#include <stdio.h>

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

int main() {
    struct Student students[] = {
        {"Alice", 20},
        {"Bob", 22},
        {"Charlie", 21}
    };

    struct Student *p = students;
    int len = sizeof(students) / sizeof(students[0]);

    for (int i = 0; i < len; i++) {
        printf("%s %d\n", (p + i)->name, (p + i)->age);
    }
    return 0;
}

十、常见面试题

10.1 题目:使用指针将字符串逆序

#include <stdio.h>

void reverse_string(char *str) {
    char *start = str;
    char *end = str;
    while (*end != '\0') {
        end++;
    }
    end--;

    while (start < end) {
        char temp = *start;
        *start = *end;
        *end = temp;
        start++;
        end--;
    }
}

int main() {
    char str[] = "Hello World";
    reverse_string(str);
    printf("%s\n", str);
    return 0;
}

10.2 题目:使用指针实现strstr函数

#include <stdio.h>

char *my_strstr(const char *haystack, const char *needle) {
    if (*needle == '\0') return (char *)haystack;

    while (*haystack != '\0') {
        const char *h = haystack;
        const char *n = needle;

        while (*h && *n && (*h == *n)) {
            h++;
            n++;
        }

        if (*n == '\0') {
            return (char *)haystack;
        }

        haystack++;
    }

    return NULL;
}

int main() {
    char str[] = "Hello World";
    char *result = my_strstr(str, "World");
    if (result) {
        printf("找到:%s\n", result);
    } else {
        printf("未找到\n");
    }
    return 0;
}

十一、sizeof 与 strlen 的核心差异(面试必考)

sizeof和strlen是 C 语言极易混淆的两个标识符,二者的核心差异是笔试面试的基础必考题。

11.1 核心差异对比表

特性sizeofstrlen
本质C 语言关键字 / 操作符C 语言标准库函数(需包含 <string.h>)
计算内容变量 / 类型占用的内存字节数字符串有效字符个数(不含结束符\0)
结束标志无需结束标志,直接计算内存总大小必须以\0为结束标志,遇到\0立即停止
操作对象任意类型(变量、数组、指针、类型名)仅能操作字符串(char*/ 字符数组)
计算时机编译期计算,结果为常量运行期计算,结果为变量
返回值类型size_t(无符号整数)size_t(无符号整数)

11.2 经典示例代码

#include <stdio.h>
#include <string.h>
int main() {
    char str[] = "abcdef";
    char *p = str;
    char str2[10] = "abc";

    printf("sizeof(str) = %zd\n", sizeof(str)); // 7(6个字符+1个'\0')
    printf("strlen(str) = %zd\n", strlen(str)); // 6(有效字符,不含'\0')
    printf("sizeof(p) = %zd\n", sizeof(p));     // 8(64位系统,指针大小固定)
    printf("sizeof(str2) = %zd\n", sizeof(str2)); // 10(数组总内存大小)
    printf("strlen(str2) = %zd\n", strlen(str2)); // 3(遇'\0'停止)

    return 0;
}

十二、经典指针笔试题解析

指针笔试题综合考察数组名的本质、指针运算规则、sizeof/strlen的使用,也是面试的压轴考点。

12.1 题型:找数组中唯一出现一次的两个数字

#include <stdio.h>

// 找出数组中只出现一次的两个数字(其余数字出现两次)
void findSingleNumbers(int* a, int n, int* res) {
    if (a == NULL || res == NULL || n < 2) {
        printf("输入参数无效!\n");
        return;
    }
    int sum = 0;
    int* p = a;
    for (int i = 0; i < n; i++, p++) {
        sum ^= *p;
    }
    int mask = 1;
    while ((sum & mask) == 0) {
        mask <<= 1;
    }
    int x = 0, y = 0;
    p = a;
    for (int i = 0; i < n; i++, p++) {
        if (*p & mask) {
            x ^= *p;
        }
        else {
            y ^= *p;
        }
    }
    *res = x;
    *(res + 1) = y;
}

int main() {
    int a[] = { 1, 2, 3, 4, 5, 1, 2, 3, 4, 6 };
    int n = sizeof(a) / sizeof(a[0]);
    int res[2];
    findSingleNumbers(a, n, res);
    printf("数组中只出现一次的两个数字是:%d 和 %d\n", res[0], res[1]);
    return 0;
}

运行结果:

数组中只出现一次的两个数字是:5 和 6

核心思路:

  1. 利用异或特性:相同数字异或结果为 0,0 异或任意数字结果为该数字本身,将数组中所有数字异或后,结果为两个目标数字的异或值;
  2. 找到异或结果中第一个为 1 的二进制位:该位表示两个目标数字在这一位上不同,以此为依据将数组分为两组;
  3. 分组异或:将两组数字分别异或,最终得到的两个结果就是数组中只出现一次的两个数字;
  4. 指针操作:通过指针遍历数组,p++的步长由指针类型决定(此处为 int*,步长 4 字节),精准访问每个数组元素。

十三、核心总结

  1. 复杂指针类型的区分:核心是优先级规则:() > [] > *,先结合的标识符决定其本质(先结合()是指针,先结合[]是数组);
  2. **void*是通用指针**:可接收任意类型地址,是实现通用编程的核心,需强转为char*后才能做地址运算;
  3. 回调函数是解耦关键:标准库qsort的通用性正是基于void*通用指针 + 用户自定义回调函数实现;
  4. 二维数组传参:列号不可省略,形参本质是数组指针,这是与一维数组传参的核心区别;
  5. sizeof与strlen:sizeof是编译期计算内存大小的操作符,strlen是运行期计算字符串长度的库函数,二者无关联,切勿混淆;
  6. 指针运算规则:仅支持 ± 整数、指针 - 指针、关系运算 ,指针 + 指针、乘除运算均为非法操作,指针 ± 整数的步长由指针类型决定。

十四、指针上下篇整体衔接

指针基础篇围绕指针的本质(内存地址)、基础操作、一维数组与指针的关联展开,是理解指针的基础;进阶篇则聚焦复杂指针类型、通用编程思想、面试高频考点,是对指针知识的综合应用和深化。二者结合构成了 C 语言指针的完整知识体系,而贯穿始终的核心底层逻辑不变:指针的本质是内存地址,指针的类型决定了解引用的权限和运算的步长。

所有示例代码均可直接复制到编译器运行,建议动手敲一遍、修改参数尝试不同的效果,只有通过实际操作,才能真正把指针的知识点吃透,应对考试和面试也会更得心应手。