1. 为什么指针是C++的“劝退之王”?

很多初学者在学完基础语法后,一碰到指针就开始怀疑人生:指针到底是什么?它和变量有什么区别?为什么用不好就会段错误、内存泄漏、悬空指针?

其实指针并没有想象中那么可怕。它本质上就是一个保存内存地址的变量。你之所以觉得难,是因为它把“值”和“地址”两个概念叠加在了一起,再加上内存管理、引用、智能指针等一堆衍生概念,很容易让人绕晕。

这篇文章会从最底层的“内存地址”讲起,一步步带你理解指针的本质、常见用法、经典陷阱,以及现代C++中如何用智能指针优雅地替代裸指针。读完你会发现:指针不是玄学,而是C++最强大的工具之一。

2. 先搞懂内存地址:指针的地基

在理解指针之前,必须先理解一个核心概念:程序运行时,变量是存放在内存中的,而内存中的每一个字节都有一个编号,这个编号就是内存地址。

你可以把内存想象成一排带编号的储物柜。每个柜子能放一定大小的数据,柜子的编号就是地址。当你声明一个变量时,编译器会为你分配一个或多个柜子,并把变量名和这些柜子的编号关联起来。

2.1 取地址运算符 & 与解引用运算符 *

在C++中,有两个运算符是理解指针的关键:

  • &(取地址运算符):获取变量在内存中的起始地址。
  • *(解引用运算符):根据地址访问该地址上存储的数据。

2.2 第一个指针程序

#include <iostream>

int main() {
    int age = 25;          // 声明一个普通变量
    int* ptr = &age;       // 声明一个指针,保存 age 的地址

    std::cout << "age 的值:" << age << std::endl;
    std::cout << "age 的地址:" << &age << std::endl;
    std::cout << "ptr 保存的地址:" << ptr << std::endl;
    std::cout << "通过 ptr 解引用得到:" << *ptr << std::endl;

    *ptr = 30;             // 通过指针修改 age 的值
    std::cout << "修改后 age 的值:" << age << std::endl;

    return 0;
}

运行这段代码,你会发现 ptr 保存的地址和 &age 完全一致,而 *ptr 取出的值就是 age 的值。这就是指针最核心的机制:通过地址间接访问变量。

2.3 指针的声明语法

指针的声明语法是 类型* 变量名,其中类型表示“这个指针指向什么类型的数据”。例如:

  • int* p:指向 int 的指针
  • double* p:指向 double 的指针
  • char* p:指向 char 的指针
  • void* p:无类型指针,可以指向任意类型,但不能直接解引用

3. 指针的核心操作:赋值、解引用与算术

掌握了基础语法后,接下来要理解指针的三种核心操作:赋值、解引用和指针算术。

3.1 指针的赋值

指针只能保存“同类型变量的地址”,否则编译器会报错(除非使用 void* 或强制类型转换)。例如:

int a = 10;
double b = 3.14;

int* p1 = &a;    // 正确:p1 指向 int
// int* p2 = &b; // 错误:&b 是 double*,不能赋给 int*

void* p3 = &b;   // 正确:void* 可以保存任意地址
// *p3 = 1.0;    // 错误:void* 不能直接解引用

3.2 解引用与空指针

解引用一个指针前,必须确保它指向有效的内存。如果指针是 nullptr(空指针),解引用会导致程序崩溃(段错误)。

int* p = nullptr;
// *p = 42;  // 危险!解引用空指针会导致崩溃

if (p != nullptr) {
    *p = 42;
} else {
    std::cout << "指针为空,不能解引用" << std::endl;
}

3.3 指针算术:数组与指针的亲密关系

指针支持加减运算,但加减的单位是“指向类型的大小”,而不是1个字节。这一点在遍历数组时非常有用:

#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* p = arr;   // 数组名会退化为指向首元素的指针

    for (int i = 0; i < 5; ++i) {
        std::cout << "arr[" << i << "] = " << *(p + i) << std::endl;
    }

    // 指针也可以自增遍历
    int* q = arr;
    while (q != arr + 5) {
        std::cout << *q << " ";
        ++q;
    }
    std::cout << std::endl;

    return 0;
}

注意:p + 1 并不是地址加1,而是加 sizeof(int) 个字节(通常是4字节),这样 *(p + 1) 才能正确访问下一个 int 元素。

4. 指针与函数:传值、传址与引用

指针最常见的用途之一,就是作为函数参数,实现“在函数内部修改外部变量”的效果。

4.1 传值 vs 传址

#include <iostream>

void swap_by_value(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
    // 这里修改的是副本,不影响外部变量
}

void swap_by_pointer(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
    // 通过地址修改外部变量
}

void swap_by_reference(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
    // 引用是别名,直接修改外部变量
}

int main() {
    int x = 1, y = 2;

    swap_by_value(x, y);
    std::cout << "传值后:x=" << x << ", y=" << y << std::endl;  // 1, 2

    swap_by_pointer(&x, &y);
    std::cout << "传址后:x=" << x << ", y=" << y << std::endl;  // 2, 1

    swap_by_reference(x, y);
    std::cout << "传引用后:x=" << x << ", y=" << y << std::endl;  // 1, 2

    return 0;
}

4.2 指针与引用的区别

对比项指针引用
是否可以为空可以,nullptr不可以,必须初始化
是否可以重新指向可以,随时改变指向不可以,绑定后不能改变
是否需要解引用需要 *不需要,直接使用
内存占用占用一个指针大小的空间通常不额外占用空间(编译器优化)
适用场景动态内存、数组遍历、可选参数函数参数、运算符重载、避免拷贝

4.3 函数指针:把函数当作参数传递

函数指针是指向函数的指针,可以用来实现回调机制:

#include <iostream>

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

// 函数指针作为参数
int compute(int x, int y, int (*func)(int, int)) {
    return func(x, y);
}

int main() {
    int (*fp)(int, int) = add;   // 声明并初始化函数指针
    std::cout << "add(3, 4) = " << fp(3, 4) << std::endl;

    std::cout << "compute(3, 4, add) = " << compute(3, 4, add) << std::endl;
    std::cout << "compute(3, 4, multiply) = " << compute(3, 4, multiply) << std::endl;

    return 0;
}

5. 动态内存:new 与 delete 的正确姿势

指针最强大的能力,是配合 newdelete 在堆上动态分配内存。堆内存的生命周期由程序员控制,这也是内存泄漏和悬空指针的“重灾区”。

5.1 基本用法

#include <iostream>

int main() {
    // 分配单个变量
    int* p = new int(42);
    std::cout << "*p = " << *p << std::endl;
    delete p;   // 释放内存
    p = nullptr; // 置空,避免悬空指针

    // 分配数组
    int* arr = new int[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * i;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    delete[] arr;  // 释放数组要用 delete[]

    return 0;
}

5.2 经典陷阱:内存泄漏与悬空指针

  • 内存泄漏new 了却没有 delete,导致内存无法回收。程序长期运行会耗尽内存。
  • 悬空指针delete 之后没有把指针置空,指针仍然保存着已释放的地址。再次解引用是未定义行为。
  • 重复释放:对同一个指针 delete 两次,会导致程序崩溃。
int* p = new int(10);
delete p;
// p 现在是悬空指针
// delete p;  // 错误!重复释放

p = nullptr;  // 正确做法:置空
delete p;     // 对 nullptr 执行 delete 是安全的

5.3 用智能指针告别手动管理

现代C++强烈推荐使用智能指针替代裸指针,让内存管理自动化:

#include <iostream>
#include <memory>

int main() {
    // unique_ptr:独占所有权,不能拷贝
    std::unique_ptr<int> uptr = std::make_unique<int>(100);
    std::cout << "*uptr = " << *uptr << std::endl;

    // shared_ptr:共享所有权,引用计数
    std::shared_ptr<int> sptr1 = std::make_shared<int>(200);
    std::shared_ptr<int> sptr2 = sptr1;
    std::cout << "use_count = " << sptr1.use_count() << std::endl;

    // weak_ptr:弱引用,不增加引用计数,用于打破循环引用
    std::weak_ptr<int> wptr = sptr1;
    if (auto sp = wptr.lock()) {
        std::cout << "weak_ptr 有效,*sp = " << *sp << std::endl;
    }

    return 0;
    // 离开作用域时,智能指针自动释放内存
}

6. 指针进阶:多级指针、const 与 void*

掌握了基础之后,再来看几个容易让人“崩溃”的进阶概念。

6.1 多级指针:指向指针的指针

二级指针保存的是“一级指针的地址”,常用于在函数中修改指针本身:

#include <iostream>

void allocate(int** pp) {
    *pp = new int(99);  // 修改外部指针的指向
}

int main() {
    int* p = nullptr;
    allocate(&p);       // 传入指针的地址
    std::cout << "*p = " << *p << std::endl;
    delete p;
    return 0;
}

6.2 const 与指针的三种组合

写法含义能否修改指向能否修改值
const int* p指向 const int 的指针可以不可以
int* const pconst 指针,指向 int不可以可以
const int* const pconst 指针,指向 const int不可以不可以
int a = 10, b = 20;

const int* p1 = &a;   // 指向常量的指针
// *p1 = 30;          // 错误:不能通过 p1 修改值
p1 = &b;             // 正确:可以改变指向

int* const p2 = &a;   // 常量指针
*p2 = 30;            // 正确:可以修改值
// p2 = &b;          // 错误:不能改变指向

const int* const p3 = &a;  // 双重 const
// *p3 = 30;          // 错误
// p3 = &b;          // 错误

6.3 void* 与类型转换

void* 可以保存任意类型的地址,但使用前必须转换回具体类型:

int a = 42;
void* vp = &a;

// 必须转换回 int* 才能解引用
int* ip
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐