指针(上):从概念到实战,我带你一步步吃透

指针,是C++里绕不开的一道坎。很多初学者一听到指针就头疼,觉得它又抽象又难懂。其实说白了,指针就是一个存放地址的变量。你想想看,我们平时用的普通变量,存的是值;指针变量呢,存的是另一个变量的门牌号。今天这一讲,我们就从最基础的概念开始,把指针的方方面面掰开揉碎了讲清楚。

一、指针到底是什么?

我个人习惯把指针比作遥控器。你手里的遥控器本身不发光不发热,但它能控制电视、空调。指针也一样,它本身不存数据,但它知道数据在内存的哪个位置,通过它就能操作那个数据。

举个例子:

int a = 10;      // 普通变量,存的是10
int* p = &a;     // 指针变量,存的是a的地址

这里 p 就是指针,它里面装的是 a 在内存中的地址。通过 *p 就能拿到 a 的值,也就是10。

小提示: 初学者最容易搞混的是 &*& 是取地址,* 是解引用(取地址里的值)。记住一句话:& 是找门牌号,* 是进门拿东西。

二、指针的定义与初始化

定义指针的语法很简单:

类型* 指针名;

比如:

int* p;          // 定义一个指向int的指针
double* dp;      // 指向double的指针
char* cp;        // 指向char的指针

但这里有个坑——未初始化的指针。我刚开始学的时候,就犯过这个错:

int* p;          // 只定义,没初始化
*p = 100;        // 危险!p指向哪里?不知道!

这种未初始化的指针,我们叫它野指针。它指向的内存地址是随机的,往里面写数据,轻则程序崩溃,重则破坏系统数据。

警告: 定义指针时一定要初始化!如果暂时不知道指向哪里,就初始化为 nullptr

正确的做法:

int a = 10;
int* p = &a;      // 初始化指向a
int* q = nullptr; // 暂时不指向任何对象

三、空指针 nullptr

C++11 引入了 nullptr,专门表示空指针。在它之前,大家用 NULL0,但这两个有歧义。比如:

void func(int);
void func(int*);

func(NULL);   // 到底调用哪个?有歧义!
func(nullptr); // 明确调用指针版本

所以,我个人建议:永远用 nullptr,别再用 NULL 了。

使用空指针时要注意:

int* p = nullptr;
if (p == nullptr) {
    // 安全,p不指向任何对象
}
// *p = 10;  // 错误!解引用空指针是未定义行为
核心原则: 使用指针前,一定要检查它是不是 nullptr。这是避免程序崩溃的第一道防线。

四、野指针——我踩过的坑

野指针,就是指向无效内存的指针。产生野指针的常见场景:

  1. 未初始化:定义后直接使用
  2. 指向已释放的内存:比如 delete 后没置空
  3. 指向栈上已销毁的变量:函数返回后,局部变量的地址就失效了

我曾经在项目里遇到过这样一个bug:

int* getPtr() {
    int x = 100;
    return &x;  // 危险!x是局部变量,函数结束就销毁了
}

int* p = getPtr();
cout << *p;  // 未定义行为!可能输出100,也可能崩溃

这个bug让我排查了整整一个下午。后来我养成了一个习惯:函数返回指针时,一定要确保指针指向的生命周期足够长。要么用 new 在堆上分配,要么传引用参数。

避坑指南: 我曾经因为野指针导致线上服务偶发崩溃,查了三天才发现是某个 delete 后的指针没置 nullptr。从那以后,我每次 delete 完都立刻写 ptr = nullptr;

五、指针运算

指针也能做加减运算,但和普通数字的加减不一样。指针加减的是指向类型的大小

int arr[] = {10, 20, 30, 40};
int* p = arr;      // 指向arr[0]

cout << *p;       // 输出10
p++;               // 移动一个int大小(通常是4字节)
cout << *p;       // 输出20
p += 2;            // 移动两个int大小
cout << *p;       // 输出40

你想想看,为什么 p++ 不是加1个字节,而是加4个字节?因为指针知道它指向的是 int,所以移动一个单位就是移动一个 int 的大小。这就是指针的类型安全

指针还可以相减:

int* p1 = &arr[0];
int* p2 = &arr[3];
ptrdiff_t diff = p2 - p1;  // 结果是3,表示相差3个元素
注意: 指针运算只能在同一个数组内进行。跨数组的指针运算,结果是未定义的。我见过有人把两个不同数组的指针相减,结果完全不可控。

六、const 与指针

const 和指针的组合,是很多人的噩梦。其实只要记住一句话:const 修饰谁,谁就不能变

1. 指向常量的指针(pointer to const)

const int a = 10;
const int* p = &a;  // p可以改指向,但不能通过p修改a的值

int b = 20;
p = &b;             // 可以,p改指向b
// *p = 30;         // 错误!不能通过p修改b的值

这种指针,说白了就是只读视角。你可以换着看不同的对象,但不能通过这个指针去修改对象。

2. 常量指针(const pointer)

int a = 10;
int* const p = &a;  // p的指向不能变,但可以通过p修改a的值

*p = 20;            // 可以,a变成20
// p = &b;         // 错误!p是常量,不能改指向

这种指针,就是固定视角。你只能盯着一个对象看,但可以随意修改它。

3. 指向常量的常量指针

const int a = 10;
const int* const p = &a;  // 既不能改指向,也不能修改值

这个组合最严格,啥都不能动。

记忆口诀:const* 的哪边。
const int* — const 在 * 左边,修饰的是指向的对象(对象不可变)
int* const — const 在 * 右边,修饰的是指针本身(指针不可变)

七、知识体系总览

下面这张图,把今天讲的内容串了起来。你可以把它当作一个思维导图来用:

指针(上) 指针概念 存放地址的变量 通过地址间接访问 定义与初始化 类型* 指针名 必须初始化 空指针 nullptr C++11 引入 比 NULL 更安全 野指针 未初始化 指向已释放内存 指向局部变量 指针运算 加减移动元素 相减得元素个数 只能在数组内 const 与指针 指向常量的指针 常量指针 指向常量的常量指针 核心:理解地址、类型、const 三者的关系

八、总结

指针这一块,说白了就是三个核心:

  • 地址:指针存的是地址,不是值
  • 类型:指针知道它指向什么类型,所以加减运算按类型大小移动
  • const:看 const 在 * 的哪边,决定谁不可变

嗯,今天的内容就到这里。指针的下半部分,我们会聊指针和数组的关系、指针作为函数参数、以及更高级的指针用法。但今天这些基础,你一定要亲手敲代码练一练。光看是学不会指针的,真的。