指针与C++:引用与指针的区别、this 指针、虚函数表与指针、智能指针的原理

说实话,很多C语言老手刚转到C++时,最困惑的就是指针和引用到底有什么区别。我当年也一样,看着&符号,心想这不就是取地址吗?怎么又冒出来个引用?今天咱们就把这些概念彻底理清楚。

一、引用与指针:看似相似,实则不同

引用和指针都能间接访问对象,但本质上有区别。指针是一个变量,存储的是地址;引用是别名,是某个已存在对象的另一个名字。

核心区别:

  • 指针可以为空(nullptr),引用必须初始化且不能为空
  • 指针可以重新赋值,引用一旦绑定就不能改变
  • 指针有自身的地址和大小,引用没有独立的内存空间
  • 指针支持算术运算,引用不支持
int a = 10;
int* p = &a;   // 指针,存储a的地址
int& r = a;    // 引用,a的别名

p = nullptr;    // 合法,指针可以为空
// int& r2;     // 错误!引用必须初始化

int b = 20;
p = &b;         // 合法,指针可以指向别处
// r = b;        // 不是重新绑定!这是把b的值赋给a

cout << sizeof(p);  // 8(64位系统),指针本身的大小
cout << sizeof(r);  // 4,引用的大小就是对象的大小

我在项目中遇到过这样一个坑:有个同事用引用作为函数返回值,结果返回了一个局部变量的引用。程序跑起来时好时坏,调试了半天才发现是悬空引用的问题。嗯,这里要注意——引用虽然用起来像值类型,但底层实现其实就是指针。所以返回局部变量的引用,本质上和返回局部变量的指针一样危险。

我的建议:

  • 函数参数传递:能用引用就用引用,避免指针的null检查
  • 需要重新绑定或允许为空:用指针
  • 实现容器或迭代器:用指针,因为需要算术运算

二、this 指针:每个对象都知道自己是谁

你想想看,当你在成员函数里访问成员变量时,编译器怎么知道是哪个对象的变量?答案就是 this 指针。

this 指针是每个非静态成员函数内部隐含的一个指针,指向调用该函数的对象。它本质上就是 ClassName* const this——一个常量指针,你不能改变它指向的对象。

class Student {
private:
    string name;
    int age;
public:
    Student(const string& name, int age) {
        // 当参数名和成员变量名冲突时,必须用this
        this->name = name;
        this->age = age;
    }
    
    Student& setAge(int age) {
        this->age = age;
        return *this;  // 返回对象本身,支持链式调用
    }
    
    void print() const {
        // 在const成员函数中,this的类型是 const Student*
        cout << this->name << ": " << this->age << endl;
    }
};

我曾经在写一个链表类时,利用 this 指针实现了链式调用:list.insert(1).insert(2).insert(3)。这种写法在配置类或建造者模式中特别常见,代码读起来很流畅。

注意:

  • 静态成员函数没有 this 指针,因为它们不属于某个对象
  • 不要在成员函数中保存 this 指针供后续使用,除非你清楚对象的生命周期
  • this 指针本身不能为 null,但如果对象是通过空指针调用的成员函数,this 就是空指针

三、虚函数表与指针:多态的底层实现

C++ 的多态是怎么实现的?说白了就是通过虚函数表(vtable)和虚函数指针(vptr)。每个包含虚函数的类都有一个虚函数表,每个对象都有一个指向该表的虚函数指针。

class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func3() { cout << "Base::func3" << endl; }  // 非虚函数
};

class Derived : public Base {
public:
    void func1() override { cout << "Derived::func1" << endl; }
    virtual void func4() { cout << "Derived::func4" << endl; }
};

这个结构可以用一张图来表示:

虚函数表与虚函数指针结构 Base 对象 vptr → Base虚函数表 其他成员变量... Base 虚函数表 Base::func1() 地址 Base::func2() 地址 Derived 对象 vptr → Derived虚函数表 其他成员变量... Derived 虚函数表 Derived::func1() 地址 Base::func2() 地址 通过基类指针调用 func1() 时,实际调用的是 Derived::func1() 这就是动态绑定——运行时根据 vptr 找到正确的函数

为什么会有这样的设计?因为编译器在编译时不知道基类指针到底指向哪个派生类对象,所以只能在运行时通过 vptr 去查虚函数表,找到实际要调用的函数。这就是动态绑定的本质。

避坑指南:

  • 构造函数中调用虚函数不会触发多态,因为此时 vptr 可能还没初始化完成
  • 不要在析构函数中调用虚函数,因为派生类的部分已经被销毁了
  • 虚函数表是类级别的,所有对象共享同一个表,但每个对象有自己的 vptr

四、智能指针:告别手动 delete

手动管理内存是 C 和 C++ 最容易出问题的地方。我早期做嵌入式开发时,就因为在中断处理函数里忘记释放内存,导致系统跑几天就崩了。智能指针就是为了解决这个问题而生的。

4.1 unique_ptr:独占所有权

unique_ptr 是独占所有权的智能指针。同一时刻只能有一个 unique_ptr 指向某个对象,不能拷贝,只能移动。这就像一把钥匙,只有一个人能持有。

#include <memory>

std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1;  // 错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1);  // 可以移动,p1变为空

// 推荐使用 make_unique(C++14)
auto p3 = std::make_unique<int>(100);

// 自定义删除器
auto deleter = [](FILE* f) { 
    if (f) fclose(f); 
    cout << "文件已关闭" << endl; 
};
std::unique_ptr<FILE, decltype(deleter)> filePtr(fopen("test.txt", "r"), deleter);

我在项目中用 unique_ptr 管理硬件资源特别顺手。比如打开一个串口设备,用 unique_ptr 包装起来,函数退出时自动关闭,再也不用担心忘记释放了。

4.2 shared_ptr:共享所有权

shared_ptr 使用引用计数来管理共享所有权。多个 shared_ptr 可以指向同一个对象,当最后一个 shared_ptr 被销毁时,对象才被释放。

std::shared_ptr<int> sp1 = std::make_shared<int>(42);
std::shared_ptr<int> sp2 = sp1;  // 引用计数变为2

cout << sp1.use_count();  // 输出2

sp1.reset();  // 引用计数减1
sp2.reset();  // 引用计数变为0,对象被销毁

// 循环引用问题
struct Node {
    std::shared_ptr<Node> next;
    // std::weak_ptr<Node> next;  // 用weak_ptr打破循环
};

auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->next = n1;  // 循环引用!内存泄漏

我曾经踩过的坑:循环引用导致的内存泄漏。两个 shared_ptr 互相引用,引用计数永远到不了0。解决办法是用 weak_ptr 打破循环。weak_ptr 不增加引用计数,只是观察者,需要使用时通过 lock() 获取 shared_ptr。

3.3 智能指针对比

特性 unique_ptr shared_ptr weak_ptr
所有权 独占 共享 不拥有
引用计数 有(不增加计数)
拷贝 禁止 允许 允许
移动 允许 允许 允许
性能开销 几乎为零 有(引用计数) 较小
适用场景 资源独占 资源共享 观察者、打破循环引用

说实话,智能指针是现代 C++ 的基石之一。我建议你在新项目中尽量使用智能指针,而不是裸指针。但也要注意,智能指针不是万能的——比如在嵌入式系统中,内存受限且实时性要求高,智能指针的引用计数操作可能带来不确定的开销。这时候,手动管理内存反而更可控。

总结一下:

  • 引用是别名,指针是变量——用引用传参,用指针实现多态
  • this 指针是成员函数的隐含参数,指向当前对象
  • 虚函数表是多态的底层实现,vptr 指向 vtable,运行时动态绑定
  • unique_ptr 独占所有权,shared_ptr 共享所有权,weak_ptr 辅助观察

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