C++基础笔记(9):智能指针
在 C++ 中,内存管理一直是一个核心且容易出错的问题。传统的原始指针(raw pointer)需要程序员手动管理其生命周期,一旦处理不当,就容易导致内存泄漏、悬空指针、重复释放等问题。为了解决这些问题,C++ 引入了更加安全和现代化的工具——智能指针(Smart Pointer)。
1. 基本概念
1.1. 智能指针的基本概念
智能指针是一种抽象的数据类型,它通常通过类模板实现,从而具备良好的泛型能力,可以用于管理任意类型的动态资源。与普通指针不同,智能指针并不仅仅是一个地址的持有者,它本质上是一个“对象”,内部封装了原始指针,并通过类的析构函数自动释放指针所指向的内存或对象。将“资源释放”这一容易出错的操作,从程序员手动控制转移到了对象生命周期管理中,从而显著降低了内存泄漏的风险。
例如下面这段代码,智能指针通过“对象析构自动释放资源”的机制,将资源管理问题转化为对象生命周期问题:
#include <iostream>
#include <memory>
using namespace std;
void test_raw() {
int* p = new int(10);
// 如果这里发生异常或提前 return,就会内存泄漏
delete p;
}
void test_smart() {
std::unique_ptr<int> p(new int(10));
// 无需手动 delete,离开作用域自动释放
}
可以看到,智能指针通过“对象析构自动释放资源”的机制,将资源管理问题转化为对象生命周期问题,这正是现代 C++ 推荐的编程方式。
1.2.智能指针的调用
智能指针定义在标准库的 <memory> 头文件中,位于 std 命名空间下,它们是 RAII(Resource Acquisition Is Initialization,资源获取即初始化)编程思想的重要体现。RAII 的核心思想是:资源在对象构造时获取,在对象析构时释放。也就是说,任何需要手动管理的资源(如堆内存、文件句柄、锁等),都应该交由一个栈对象来管理,这个对象在生命周期结束时自动执行清理逻辑,从而保证资源一定会被释放。例如:
#include <memory>
void func() {
std::unique_ptr<int> p(new int(20));
// 在函数结束时,无论是正常返回还是异常退出,p都会被析构,从而释放资源
}
相比手动
new/delete,更加可靠和规范,也是现代 C++(尤其是 C++11 之后)推荐的资源管理范式。
1.3. 现阶段智能指针的使用
在实际开发中,原始指针的使用范围已经被大大限制。通常情况下,当一个指针需要表示“资源所有权”时,应优先使用智能指针;而原始指针更多只用于不涉及所有权的场景,例如函数参数传递、临时访问、或者性能敏感且生命周期清晰的局部逻辑中。
换句话说,现代 C++ 的设计理念是:用智能指针表达所有权,用原始指针表达观察关系。标准库中提供了多种智能指针类型,包括 auto_ptr(已弃用)、unique_ptr、shared_ptr 和 weak_ptr,它们分别针对不同的资源管理场景进行设计,在后续内容中我们会逐一展开分析它们的实现机制与使用方式。
2. 标准库提供的四类智能指针
C++ 标准库中提供了四类典型的智能指针,分别是 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。它们虽然都用于管理动态申请的资源,但设计目标并不相同:有的强调独占所有权,有的强调共享所有权,有的则专门用于解决循环引用问题。
2.1.auto_ptr(已弃用)
auto_ptr 是 C++98 时代提供的早期智能指针,可以在对象析构时自动释放所管理的内存。它采用的是一种“所有权模式”,当 auto_ptr 对象过期时,会通过析构函数自动调用 delete 释放资源。 但是它最大的缺陷在于:拷贝或赋值时会发生所有权转移,原对象会被置空。这意味着表面上看似普通的赋值操作,实际上会偷偷改变资源归属,代码行为不直观,也极易埋下错误隐患。除此之外,auto_ptr 也不支持数组管理,因为它内部使用的是 delete 而不是 delete[]。正因为这些设计问题,auto_ptr 在 C++11 中被弃用,并最终在后续标准中移除。
这里仅做了解即可,所以不展出示例代码以免误导。
2.2 unique_ptr
unique_ptr 是C++ 11中最常用的智能指针之一。它实现的是独占式拥有的概念,也就是说,同一时刻只能有一个 unique_ptr 指向某个对象。 这种设计可以非常明确地表达资源归属,避免多个对象同时“以为自己拥有资源”而导致重复释放问题。也正因为它强调独占所有权,所以 unique_ptr 不允许拷贝,也不允许普通赋值,如果确实要转移所有权,就必须显式使用 std::move。
它的基本调用方式如下:
std::unique_ptr<int> p(new int(10));
或者更推荐的是这样写:
auto p = std::make_unique<int>(10);
下面给出3个最基础的调用实例:
(1)所有权转移
#include <iostream>
#include <memory>
using namespace std;
int main() {
auto u1 = std::make_unique<int>(10);
// auto u2 = u1; // 编译错误,不允许拷贝
auto u3 = std::move(u1);
if (!u1) {
cout << "u1 is nullptr" << endl;
}
cout << *u3 << endl;
return 0;
}
这段代码中,
u1原本独占那块动态申请的内存,执行std::move(u1)后,资源所有权转移给了u3,此时u1变为空,u3成为新的唯一拥有者。这里的“不能拷贝、只能移动”正是unique_ptr最重要的行为特征。
(2)管理数组
#include <iostream>
#include <memory>
using namespace std;
int main() {
auto arr = std::make_unique<int[]>(3);
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
for (int i = 0; i < 3; ++i) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
unique_ptr支持管理数组,并且会自动调用delete[]来释放资源,这一点比auto_ptr更安全。
(3)作为函数返回值
#include <iostream>
#include <memory>
using namespace std;
std::unique_ptr<int> createNumber() {
return std::make_unique<int>(20);
}
int main() {
auto ptr = createNumber();
cout << *ptr << endl;
return 0;
}
作为函数返回值来转移资源所有权,在这个例子中,函数内部创建资源,返回给调用者后,由调用者继续接管该资源。整个过程不需要手动
delete,同时又能明确表达“这份资源只有一个所有者”。
2.3 shared_ptr
如果说 unique_ptr 强调“一个资源只能有一个主人”,那么 shared_ptr 则适用于“一个资源可以被多个对象共同拥有”的场景。根据你的笔记,shared_ptr 实现的是共享式拥有的概念,它通过引用计数机制来管理对象生命周期:每多一个 shared_ptr 指向该对象,计数加一;每少一个,计数减一;当计数减为 0 时,对象才会被自动释放。
基本调用方式如下:
std::shared_ptr<int> p(new int(10));
推荐写为现在这个形式:
auto p = std::make_shared<int>(10);
shared_ptr 很适合一些“共享资源”的轻量级实战场景,例如多个对象共同使用同一份配置数据:
#include <iostream>
#include <memory>
using namespace std;
class Config {
public:
Config() { cout << "Config created" << endl; }
~Config() { cout << "Config destroyed" << endl; }
void show() {
cout << "using shared config" << endl;
}
};
int main() {
std::shared_ptr<Config> c1 = std::make_shared<Config>();
std::shared_ptr<Config> c2 = c1;
c1->show();
c2->show();
cout << "count = " << c1.use_count() << endl;
return 0;
}
这个例子中,c1 和 c2 共享同一个 Config 对象,直到它们都离开作用域时,析构函数才会执行。这种模式在缓存、资源池、共享配置、图结构节点等场景下非常常见。
2.4. weak_ptr
weak_ptr 可以理解为一种“弱观察者”。它本身并不拥有对象,也不会增加引用计数,只是用来观察一个由 shared_ptr 管理的对象是否仍然存在。weak_ptr 的主要作用有两个:
- 协助
shared_ptr解决循环引用问题, - 在不影响对象生命周期的前提下观察对象状态。
它不能单独使用,必须依附于 shared_ptr:
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp;
由于 weak_ptr 不拥有对象,因此它不能直接解引用,必须先通过 lock() 尝试提升为一个有效的 shared_ptr,然后再访问对象。先看一个最基础的调用示例:
#include <iostream>
#include <memory>
using namespace std;
int main() {
std::shared_ptr<int> s1(new int(10));
std::weak_ptr<int> w1 = s1;
if (std::shared_ptr<int> s2 = w1.lock()) {
cout << *s2 << endl;
} else {
cout << "object has been deleted" << endl;
}
s1.reset();
if (std::shared_ptr<int> s3 = w1.lock()) {
cout << *s3 << endl;
} else {
cout << "object has been deleted" << endl;
}
return 0;
}
- 这段代码中,第一次
lock()时对象仍然存在,所以可以成功拿到一个新的shared_ptr并访问数据;当s1.reset()后,原对象被销毁,再次lock()就会返回空指针,从而避免访问悬空对象。weak_ptr最典型的实战用途,是解决shared_ptr的循环引用问题。比如给出的A和B相互引用的例子非常典型:如果双方都用shared_ptr,那么即使函数结束,引用计数也无法降到 0,对象就永远不会被释放。
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
std::weak_ptr<B> pb; // 改成 weak_ptr,避免循环引用
~A() { cout << "A destroyed" << endl; }
};
class B {
public:
std::shared_ptr<A> pa;
~B() { cout << "B destroyed" << endl; }
};
void test() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->pb = b;
b->pa = a;
}
int main() {
test();
return 0;
}
在这个例子中,B 仍然通过 shared_ptr 拥有 A,但 A 只通过 weak_ptr 观察 B,因此不会增加 B 的引用计数,最终就能成功打破循环引用。函数结束后,对象会正常析构。
weak_ptr 不负责拥有资源,只负责观察资源,它最重要的价值在于避免 shared_ptr 之间形成循环引用。此外,在一些缓存、观察者模式、事件回调等场景中,weak_ptr 也很常用,因为它能安全判断对象是否还存在,而不会延长对象寿命。
3. 代码实战
#include <iostream>
#include <memory> // 智能指针头文件
#include <string>
using namespace std;
// ======================================================
// 第一部分:定义一个简单的角色类,用来演示 unique_ptr 和 shared_ptr
// ======================================================
class Player {
private:
string name; // 玩家名字
public:
// 构造函数:创建对象时输出提示,方便观察对象何时被创建
Player(const string& n) : name(n) {
cout << "Player " << name << " created." << endl;
}
// 析构函数:对象销毁时输出提示,方便观察对象何时被释放
~Player() {
cout << "Player " << name << " destroyed." << endl;
}
// 普通成员函数:输出玩家信息
void show() const {
cout << "Player name: " << name << endl;
}
};
// ======================================================
// 第二部分:演示 unique_ptr 作为函数返回值
// 说明:unique_ptr 支持所有权转移,适合作为函数返回值
// ======================================================
unique_ptr<Player> createPlayer(const string& name) {
// 使用 make_unique 创建对象,推荐写法,避免直接裸写 new
return make_unique<Player>(name);
}
// ======================================================
// 第三部分:定义两个类,演示 weak_ptr 解决循环引用
// 场景:Teacher 和 Student 彼此关联
// 如果双方都用 shared_ptr,就可能产生循环引用
// ======================================================
class Student; // 前向声明,告诉编译器 Student 类后面会定义
class Teacher {
public:
string name; // 教师姓名
weak_ptr<Student> student; // 用 weak_ptr 指向学生,避免循环引用
Teacher(const string& n) : name(n) {
cout << "Teacher " << name << " created." << endl;
}
~Teacher() {
cout << "Teacher " << name << " destroyed." << endl;
}
};
class Student {
public:
string name; // 学生姓名
shared_ptr<Teacher> teacher; // 学生持有教师,表示共享关系
Student(const string& n) : name(n) {
cout << "Student " << name << " created." << endl;
}
~Student() {
cout << "Student " << name << " destroyed." << endl;
}
};
// ======================================================
// 主函数:统一演示三类现代智能指针
// ======================================================
int main() {
cout << "================ unique_ptr 演示 ================" << endl;
// --------------------------------------------------
// 1. unique_ptr:独占所有权
// 一个对象同一时刻只能被一个 unique_ptr 管理
// --------------------------------------------------
auto p1 = make_unique<Player>("Knight");
// 调用对象成员函数
p1->show();
// unique_ptr 不允许拷贝,下面这句如果取消注释会编译报错
// auto p2 = p1;
// 只能通过 move 转移所有权
auto p2 = move(p1);
// 转移后,p1 变为空
if (!p1) {
cout << "p1 is now nullptr after move." << endl;
}
// 现在资源由 p2 独占
p2->show();
cout << endl;
cout << "================ unique_ptr 作为返回值 ================" << endl;
// --------------------------------------------------
// 2. unique_ptr 作为函数返回值
// createPlayer 返回一个 unique_ptr,所有权安全转移给 hero
// --------------------------------------------------
auto hero = createPlayer("Archer");
hero->show();
cout << endl;
cout << "================ shared_ptr 演示 ================" << endl;
// --------------------------------------------------
// 3. shared_ptr:共享所有权
// 多个 shared_ptr 可以共同管理同一个对象
// --------------------------------------------------
auto sp1 = make_shared<Player>("Mage");
// 让 sp2 也指向同一个对象
auto sp2 = sp1;
// use_count() 用于查看当前引用计数
cout << "sp1 use_count: " << sp1.use_count() << endl;
cout << "sp2 use_count: " << sp2.use_count() << endl;
// 通过其中任意一个智能指针都可以访问对象
sp1->show();
sp2->show();
// reset() 表示当前 shared_ptr 放弃管理权
sp1.reset();
// 此时对象还不会被销毁,因为 sp2 仍然指向它
cout << "After sp1.reset(), sp2 use_count: " << sp2.use_count() << endl;
sp2->show();
cout << endl;
cout << "================ weak_ptr 演示 ================" << endl;
// --------------------------------------------------
// 4. weak_ptr:弱引用,不增加引用计数
// 用来解决 shared_ptr 循环引用问题
// --------------------------------------------------
{
// 创建学生对象和教师对象
auto stu = make_shared<Student>("Alice");
auto tea = make_shared<Teacher>("Mr.Wang");
// 学生持有教师:shared_ptr
stu->teacher = tea;
// 教师只观察学生:weak_ptr
tea->student = stu;
// weak_ptr 不能直接访问对象,必须先 lock()
if (auto lockedStudent = tea->student.lock()) {
cout << "Teacher can see student: " << lockedStudent->name << endl;
} else {
cout << "Student object has been destroyed." << endl;
}
// 这里代码块结束后:
// stu 和 tea 都会离开作用域
// 因为 Teacher 持有的是 weak_ptr,不会形成循环引用
// 所以 Student 和 Teacher 都能正常析构
}
cout << endl;
cout << "================ 程序结束 ================" << endl;
return 0;
}
1. 第一段演示的是
unique_ptr。这里重点体现了它的独占所有权:不能拷贝,只能通过move转移;并且统一采用了推荐写法make_unique,和前文讲解保持一致。2. 第二段演示的是
unique_ptr作为函数返回值。这正好对应提到的“可以高效地将资源所有权转移给调用者”这一点。3. 第三段演示的是
shared_ptr。通过两个智能指针共同管理同一个Player对象,展示了引用计数的变化过程,这也是shared_ptr最核心的机制。4. 第四段演示的是
weak_ptr。用Teacher和Student的相互关联来说明:如果双方都使用shared_ptr,就会形成循环引用;而改成一方使用weak_ptr后,就可以正常释放资源。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)