指针与基本类型:从整型到多级指针

大家好,我是你们的老朋友。今天我们来聊聊指针与基本类型的关系。说实话,指针这东西,刚学的时候觉得挺玄乎的,用久了就会发现——其实它就是地址的化身。你想想看,计算机里所有的数据都存在内存里,而内存就像一排带编号的柜子。指针,就是那个写着柜子编号的小纸条。

指向整型的指针

先来个最基础的。整型指针,说白了就是指向存放整数的那个柜子。

int a = 42;
int *p = &a;   // p 里存的是 a 的地址
*p = 100;      // 通过 p 修改 a 的值

我个人习惯在定义指针时,把星号紧贴着变量名写:int *p。这样读起来就是「p 是一个指针,指向 int」。当然也有人喜欢写成 int* p,语法上没问题,但容易让人误解成 int* 是一种类型。其实 C 语言里,int* p, q 这种写法会坑到你——q 只是个 int,不是指针。

注意: 我曾经在代码 review 时见过一个同事,把 int* p, q 当成两个指针用,结果调试了一下午。所以我的建议是:要么一行只定义一个变量,要么星号紧贴变量名。

指向字符型的指针

字符指针就更有意思了。它既可以指向单个字符,也可以指向字符串常量。

char ch = 'A';
char *cp = &ch;

char *str = "Hello, world!";  // 指向字符串常量
printf("%c\n", str[0]);       // 输出 H

这里有个坑,我当年就踩过。字符串常量存在只读区,你试图通过 str[0] = 'h' 去修改它?程序直接崩溃。所以如果你需要修改字符串,请用字符数组:

char buf[] = "Hello";
buf[0] = 'h';  // 没问题

指向浮点型的指针

浮点指针和整型指针在用法上几乎一样,但有个细节要注意:指针的步长取决于它指向的类型。

float f = 3.14f;
float *fp = &f;
printf("%f\n", *fp);  // 3.140000

// 指针加 1,地址实际增加 sizeof(float) = 4 字节
printf("%p\n", fp);   // 假设是 0x1000
printf("%p\n", fp+1); // 0x1004

嗯,这里要记住:指针的加减运算,是以它指向的类型大小为单位的。不是简单的地址加 1。

void* 泛型指针

void* 是个特殊的存在。它就像一个「万能容器」,可以存放任何类型的地址。但你不能直接解引用它,因为编译器不知道它指向的是什么类型。

int a = 10;
float b = 2.5f;
void *vp;

vp = &a;   // 可以
vp = &b;   // 也可以

// printf("%d\n", *vp);  // 错误!不能直接解引用
printf("%d\n", *(int*)vp);  // 先转成 int*,再解引用
实用技巧: 我在写通用数据结构(比如链表、队列)时,经常用 void* 来存储数据。这样一套代码可以处理 int、float、甚至结构体。但代价是类型安全没了,需要你自己保证转换的正确性。

const 与指针

const 和指针的组合,是面试题里的常客,也是实际项目中容易搞混的地方。其实就两种情况:

写法 含义 能否修改指向 能否修改值
const int *p 常量指针(指向常量的指针) 可以 不可以
int *const p 指针常量(指针本身是常量) 不可以 可以
const int *const p 两者都不可变 不可以 不可以

怎么记?我有个土办法:看 const 和星号的位置。const 在星号左边,修饰的是「指向的内容」;const 在星号右边,修饰的是「指针本身」。

int a = 10, b = 20;

const int *p1 = &a;
p1 = &b;      // 可以
// *p1 = 30;  // 不行

int *const p2 = &a;
// p2 = &b;   // 不行
*p2 = 30;     // 可以
避坑指南: 我曾经在嵌入式项目中,把一个 const 指针误传给了某个函数,结果那个函数偷偷改了数据,导致整个系统行为异常。所以我的建议是:函数参数里,如果只读不写,一律加 const。这既是文档,也是保护。

多级指针(指向指针的指针)

多级指针,说白了就是「指向指针的指针」。最常见的场景是:你想在一个函数里修改传入的指针本身。

void alloc_memory(int **pp, int size) {
    *pp = (int*)malloc(size * sizeof(int));
    // 这里修改了 *pp,也就是调用者传入的指针变量
}

int main() {
    int *p = NULL;
    alloc_memory(&p, 10);  // 传入指针的地址
    // 现在 p 指向了一块堆内存
    free(p);
    return 0;
}

你看,如果只传 int *p,函数里修改 p 的值不会影响到外面的 p。但传了 int **pp,就能通过 *pp 修改外面的 p。这就是多级指针的核心用途。

再比如二维数组的动态分配:

int **matrix = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
    matrix[i] = (int*)malloc(cols * sizeof(int));
}

这里 matrix 就是一个二级指针,它指向一个指针数组,每个指针又指向一行数据。

我的经验: 多级指针不要超过三级。三级以上,代码的可读性会急剧下降。如果你发现需要四级指针,多半是设计有问题,考虑用结构体封装一下。

知识体系总览

下面这张图,把本章的核心知识点串了起来。你可以把它当作一个快速索引。

指针与基本类型 基本类型指针 int* / char* / float* 步长 = sizeof(类型) void* 泛型指针 万能容器,需强制转换 常用于通用数据结构 const 与指针 常量指针 vs 指针常量 const 位置决定权限 多级指针 int** / char*** 修改指针本身 / 二维数组

好了,这一章的内容就到这里。指针这东西,光看是学不会的,一定要动手写代码。我建议你打开编辑器,把上面的例子都跑一遍,然后试着改一改,看看会发生什么。遇到段错误别慌——那是你进步最快的时候。


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