第十三章 指针(二):指针与字符串、指针数组、多级指针、void指针

指针这东西,学完基础用法后,真正的挑战才刚刚开始。我个人觉得,指针的进阶用法才是C语言的灵魂所在。今天咱们聊聊指针与字符串的纠缠、指针数组的妙用、多级指针的层层递进,还有那个神秘的void指针。

13.1 指针与字符串:字符指针的真相

字符串在C语言里,本质上就是字符数组。但用指针操作字符串,效率会高很多。我刚开始学的时候,总觉得字符数组和字符指针差不多,后来踩了坑才明白——它们有本质区别。

// 方式一:字符数组
char str1[] = "Hello";
// 方式二:字符指针
char *str2 = "Hello";

这两种写法,看起来都是存了个"Hello"。但实际内存布局完全不同。

关键区别:

  • str1[] 是在栈上分配了6个字节(含'\0'),内容可以修改
  • str2 指向的是字符串常量区,内容不能修改

我曾经在项目中写过这样的代码:char *p = "hello"; p[0] = 'H'; 结果程序直接崩溃。后来排查了半天,才发现是试图修改字符串常量。嗯,这个教训挺深刻的。

为什么不能修改?因为字符串常量存储在只读数据段。你想想看,如果多个指针指向同一个字符串常量,其中一个改了,其他的全受影响——这显然不合理。

13.2 字符串操作:指针的拿手好戏

用指针遍历字符串,代码可以写得很优雅。我习惯用这种方式:

char *p = str;
while (*p) {
    // 处理 *p
    p++;
}

这个循环,说白了就是:只要当前字符不是'\0',就继续。简洁、高效、不易出错。

写一个自己的字符串拷贝函数,你就能体会到指针的威力:

void my_strcpy(char *dest, const char *src) {
    while (*dest++ = *src++)
        ;
}

一行代码搞定。赋值表达式的值就是被赋的字符,当遇到'\0'时,值为0,循环结束。我个人觉得,这种写法是C语言指针魅力的集中体现。

小技巧:参数用const char *src,表明源字符串不会被修改。这是好习惯,能避免很多意外。

13.3 指针数组:数组里存的是指针

指针数组,就是数组的每个元素都是一个指针。最常见的应用场景是处理多个字符串。

const char *weekdays[] = {
    "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday", "Sunday"
};

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

你看,每个元素都是指向字符串常量的指针。这样做的好处是:

  • 节省内存:每个字符串不必连续存储
  • 方便交换:交换两个指针比交换整个字符串快得多
  • 灵活管理:可以动态改变指向

我在项目中遇到过需要按字母序排序星期几的情况。如果用二维字符数组,交换两个字符串得用strcpy,效率低。用指针数组,直接交换指针就行——快得不是一星半点。

13.4 多级指针:指针的指针

多级指针,说白了就是指向指针的指针。二级指针最常见,三级以上就很少见了。

int value = 42;
int *ptr = &value;
int **pptr = &ptr;

内存关系是这样的:

  • pptr 指向 ptr
  • ptr 指向 value
  • value 的值是42

二级指针最常见的用途是:在函数内部修改指针本身的值。你想想看,如果要在函数里给指针分配内存,只传一级指针是不够的。

void allocate_memory(int **p, int size) {
    *p = (int *)malloc(size * sizeof(int));
    if (*p == NULL) {
        // 处理错误
    }
}

int main() {
    int *arr = NULL;
    allocate_memory(&arr, 10);
    // 现在 arr 指向了分配的内存
    free(arr);
    return 0;
}

为什么必须传二级指针?因为C语言是值传递。如果传一级指针,函数内部修改的是指针的副本,外面的指针还是NULL。传二级指针,才能修改外面那个指针的值。

我曾经在写链表操作时,忘了用二级指针来修改头指针,结果链表永远只有第一个节点。排查了半小时才发现问题——嗯,这种错误犯过一次就不会再犯了。

13.5 void指针:万能指针

void * 是一种特殊指针,它可以指向任何类型的数据。说白了,就是"无类型指针"。

int a = 10;
double b = 3.14;
char c = 'X';

void *vp;
vp = &a;   // 可以
vp = &b;   // 也可以
vp = &c;   // 都可以

但void指针有两个限制:

  • 不能直接解引用:*vp 是错的,因为不知道类型
  • 不能做指针运算:vp++ 是错的,因为步长未知

使用前必须强制类型转换:

int *ip = (int *)vp;
printf("%d\n", *ip);

void指针最常见的用途是实现通用函数。qsortmemsetmemcpy 这些库函数,都用void指针来接受任意类型的数据。

void *memcpy(void *dest, const void *src, size_t n);

这个函数不管你要拷贝的是int数组、double数组还是结构体数组,都能处理。因为void指针不在乎类型,它只关心字节数。

我的习惯:写通用接口时用void指针,但内部使用前一定做类型转换。不要偷懒,类型安全很重要。

13.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以对照着梳理一下思路。

指针进阶 指针与字符串 字符数组 vs 字符指针 字符串常量不可修改 指针遍历字符串 指针数组 数组元素是指针 处理多个字符串 交换效率高 多级指针 二级指针最常见 函数内修改指针 动态内存分配 void指针 万能指针 需强制类型转换 通用函数接口 核心:理解指针的本质——地址 指针类型决定了解释内存的方式 多级指针就是层层指向,每一层都存着下一层的地址 void指针放弃类型信息,换取灵活性

13.7 避坑指南

指针用多了,踩坑是难免的。我把自己踩过的坑整理一下,你遇到了可以少走弯路。

常见错误 错误示例 正确做法
修改字符串常量 char *p = "abc"; p[0]='x'; 用字符数组 char p[] = "abc";
忘记二级指针 void func(int *p) { p=malloc(...); } 传指针的地址 void func(int **p)
void指针直接运算 void *p; p++; 先转成具体类型再运算
指针数组越界 char *arr[3]; arr[3] = ...; 注意数组下标从0开始

核心总结:指针就是地址,类型决定了怎么解释这个地址里的内容。多级指针就是地址的地址,void指针就是放弃类型信息。理解了这个,指针就不再神秘。

好了,这一章的内容就到这儿。指针的进阶用法需要多练,光看是学不会的。打开你的编译器,把上面的代码跑一遍,改一改,看看会发生什么——这才是学习指针最好的方式。


公众号:蓝海资料掘金营,微信deep3321