前言
老C/C++开发,用惯了裸指针的方式,之前发布过一篇文章,专门分享结构体通过函数指针成员的方式是实现类成员函数的效果,本篇风格也一样,从C/C++的裸指针,再延伸到C++智能指针。
概述
裸指针:只保存地址,不表达所有权,也不保证释放。在动态内存场景下,如果不主动释放内存,或者改变保存的地址,都会导致内存泄漏。
智能指针:通过 RAII(资源获取即初始化) 机制,①在智能指针对象创建 (构造函数调用)时,接管动态分配的内存或其他资源 ;②当智能指针对象离开作用域 (例如函数结束、代码块退出或发生异常栈展开)时,编译器会自动调用其析构函数 ,从而自动释放所管理的资源。
纯理论会感觉比较生硬,以下从语法编译&&系统&&资源方向拆解出易懂的概念。
语法编译&&系统&&资源
我的一位大学计算机教授老师(杨耀)在给我们上代码怎么变成可执行程序的课程时,详细的讲解了gcc/g++编译器的执行过程,其中最浅显的理解如下:
LIFO 嵌套结构规则
代码中的'{'和'}'之间是一个作用域,他在语法和资源分配上都符合LIFO 嵌套结构(典型数据结构栈--"后进先出")的规则。(需要注意的是这个LIFO嵌套结构不是系统中的物理栈)
以下是一个常见的代码案例,展示入栈/出栈节点。
//-------------------------------案例1--------------------------------------
int g; // 栈底(第一个入栈):全局作用域
void f() { // 压入:函数体作用域
int a;
{ // 压入:内层块作用域
int b;
// 符号查找:沿栈向上搜索 b → a → g(最近声明优先,即"遮蔽")
} // 弹出:b 离开可见性,符号表恢复
} // 弹出:a 离开可见性
//-------------------------------案例2--------------------------------------
void c() { /* ... */ }
void b() { c(); }
void a() { b(); }
int main() { a(); }
// 调用序列压栈:main → a → b → c
// 返回序列弹栈:c → b → a → main (严格后进先出)
注意,'{'和'}'不是维持绝对的作用域资源释放规则,C++语法中也有特殊规则,比如在局部作用域中用static 定义的变量,这个静态变量存储在静态存储区 (.data/.bss),全局变量也存储在静态存储区中,所以在main()函数结束时,才会释放这个特殊的资源。
void f() {
static int counter = 0; // 存储在静态数据区 (.data/.bss)--与全局变量一个区域
counter++;
} // counter 活到程序结束,下次调用值还在
生成/释放资源
LIFO 模型展示了主要生命周期,生命周期中最重要的就是资源的管理(生成/释放),应用程序运行时,内存中主要提供了栈、堆、静态存储区的资源分配。
内存的知识点可以查看我的另外一篇博文,本篇不展开
【QT入门到晋级】内存_qt new 结构体-CSDN博客文章浏览阅读803次,点赞15次,收藏16次。本文面向QT后台开发者,系统讲解C/C++在Linux环境下的内存布局与应用实践,涵盖栈、静态存储区、堆及虚拟地址空间(mmap)四大内存区域的特性、适用场景与性能对比。重点分析各类内存的生命周期、大小限制、效率差异及典型使用案例,如大数组处理、批量数据库操作、高清视频共享内存设计等,为QT开发者提供内存选型与优化依据。https://blog.csdn.net/liangyuna8787/article/details/150637450?spm=1001.2014.3001.5501 在语法层次上,LIFO 模型管理的是"变量",如果其中一个变量是裸指针,当裸指针所在作用域结束时,裸指针也会被直接释放掉,如果在裸指针被释放之前,开发人员通过new申请了堆资源(动态内存),但没有用delete释放堆资源(动态内存),那么这个堆资源就会一直存在,这就是内存泄漏常见的场景。
RAII 优化 LIFO 模型
LIFO 模型抽象本身只保证"自动变量的存储随作用域失效",它并不天然管理堆内存、文件句柄这些非栈资源,这也是在使用裸指针申请堆内存时,必须显示释放堆内存,才能规避内存泄漏的风险(出现异常退出时,不能完全规避这种风险)。
但如果引入RAII机制,以类替代裸指针 入栈,释放堆内存交由类的析构函数来完成。这样的好处就是①不需要显示释放堆内存;②异常退出情况下,也不会出现内存泄漏的风险(出栈时,必定会调用这个类的析构函数释放资源)。
以上描述**"以类替代裸指针入栈"** 的特征就是智能指针 的特性。智能指针本身就是一个类 ,按LIFO 模型资源出栈(作用域结束 / 异常展开 / 函数返回)时,必定会通过智能指针的析构函数释放堆内容,以下是流程图:
栈上对象(unique_ptr<T>,8字节)
│ 出栈(作用域结束 / 异常展开 / 函数返回)
▼
析构函数 ~unique_ptr()
│
▼
delete ptr_ ──────► 堆上的 T 对象被销毁
智能指针的本质
一个单成员(裸指针)的类模板,把"堆内存的释放"这一行代码搬进了析构函数,从而把一个不受作用域管辖的资源,绑到一个严格受 LIFO 管辖的对象上。unique_ptr 是它的最简形式(零开销,delete 直挂),shared_ptr 是它的多路复用形式(释放动作变成"原子计数归零判定",且自己管理着第二层资源------控制块)。
以下是GCC libstdc++(bits/unique_ptr.h)的真实代码
template<typename _Tp, typename _Dp = default_delete<_Tp> >
class unique_ptr {
// 数据成员不是裸的 pair,而是一个 tuple ------ 暗藏 EBO 优化,见下文
tuple<pointer, deleter_type> _M_t;
public:
/// Destructor, the only important part
~unique_ptr() noexcept
{
auto& __ptr = _M_t._M_ptr();
if (__ptr != nullptr)
get_deleter()(std::move(__ptr)); // 通过删除器释放,而非直接 delete
__ptr = pointer(); // 置空
}
// reset 的逻辑与析构共享同一实现(do_reset / 直接内联)
pointer reset(pointer __p = pointer()) noexcept {
pointer __old = get();
_M_t._M_ptr() = __p;
if (__old != nullptr)
get_deleter()(std::move(__old));
return __old;
}
// ...
};
从上面的源码中,可以看到有一个独享智能指针unique_ptr的模板类,这个模板类包含两个参数:①要创建的对象;②自定义的删除函数。
智能指针删除器
默认删除器
不提供第二个参数(自定义的删除函数)时,就调用默认的析构函数,下个给出裸指针和同等效果的智能指针的对比代码
#include <iostream>
#include <vector>
#include <windows.h>
#include <memory>
using namespace std;
// 自定义测试类,打印所有构造/析构的调用信息
class MyClass {
private:
int val;
public:
// 普通构造函数
MyClass(int v = 0) : val(v) {
cout << "[调用构造函数] val = " << val << endl;
}
void printMessage(const char* msg){
std::cout << "Message: " << msg << std::endl;
}
// 析构函数
~MyClass() {
cout << "[调用析构函数] val = " << val << endl;
}
};
void old_process(int v = 0) {
MyClass* obj = new MyClass(v); // 手动分配
obj->printMessage("hello nomal");
delete obj; // 必须手动释放
}
void uq_process(int v = 1) {
// 用 make_unique 创建,异常安全,避免 new 裸调用
std::unique_ptr obj = std::make_unique<MyClass>(v);
obj->printMessage("hello unique");
// 离开作用域时,unique_ptr 自动析构 MyClass
}
int main() {
// 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲
SetConsoleOutputCP(CP_UTF8);
ios::sync_with_stdio(true);
cout << "=== main函数正式开始执行 ===" << endl;
old_process();
uq_process();
return 0;
}
/*******打印信息********
=== main函数正式开始执行 ===
[调用构造函数] val = 0
Message: hello nomal
[调用析构函数] val = 0
[调用构造函数] val = 1
Message: hello unique
[调用析构函数] val = 1
***************/
自定义删除器
如果释放资源不仅仅是delete释放堆,还需要进行业务逻辑操作时(比如某个业务类退出时,必须同步通知关联的类时),可以自定义删除器,代码如下
#include <iostream>
#include <vector>
#include <windows.h>
#include <memory>
class MyClass {
public:
MyClass();
~MyClass();
void printMessage(const char* msg) { // ① 原代码多了一个 {,括号不配对
std::cout << "Message: " << msg << std::endl;
}
};
#ifdef __cplusplus
extern "C" {
#endif
MyClass* createMyClass() {
return new MyClass();
}
void destroyMyClass(MyClass* obj) {
//-----这里进行其他业务操作
delete obj;
}
#ifdef __cplusplus
}
#endif
#include <memory>
#include "MyClass.h"
// 删除器类型直接写函数指针类型:
//第二个参数void(*)(MyClass*)是void destroyMyClass(MyClass* obj) 的函数指针
using MyClassPtr = std::unique_ptr<MyClass, void(*)(MyClass*)>;
void demo1() {
// 构造时必须同时传【对象指针】和【删除器函数】
MyClassPtr p(createMyClass(), &destroyMyClass);
//std::unique_ptr<MyClass, void(*)(MyClass*)> p(
// createMyClass(),
// &destroyMyClass // 自定义删除器,函数指针方式
//);
p->printMessage("hello");
} // 析构 → get_deleter()(ptr) → 通过函数指针调用 destroyMyClass(ptr) → delete obj
智能指针类型
| 类型 | 适用场景 | 注意事项 |
|---|---|---|
unique_ptr |
独占资源、成员变量、工厂返回 | 不能拷贝,只能 move |
shared_ptr |
多所有者共享 | 有控制块和原子计数开销 |
weak_ptr |
观察、缓存、打破循环引用 | 使用前必须 lock() |
atomic<shared_ptr> |
多线程原子切换指针 | 保护指针本身,不保护对象内容 |
out_ptr/inout_ptr |
对接 C API | C++23 起可用 |
unique_ptr:独占所有权
同一时刻只能有一个 unique_ptr 拥有对象,禁止拷贝,只允许移动 。是之前的裸指针升级替代版,大部分情况下,都是用 unique_ptr,下面是定义方法:
| 类别 | 写法 | 资源来源 |
|---|---|---|
| 一、裸指针接管 | unique_ptr<T> p{new T}; |
new 表达式 |
| 一、裸指针接管 | unique_ptr<T> p(new T); |
new 表达式 |
| 一、裸指针接管 | unique_ptr<T[]> p(new T[n]); |
new[] |
| 一、裸指针接管 | unique_ptr<T, D> p(cApiCreate(), deleter); |
C 工厂函数 |
| 二、make 工厂 | auto p = make_unique<T>(args); |
函数内 new |
| 二、make 工厂 | auto p = make_unique<T[]>(n); |
函数内 new[] |
| 二、make 工厂 | auto p = make_unique_for_overwrite<T[]>(n); |
跳过清零 |
| 三、移动/转换 | unique_ptr<T> p2(std::move(p1)); |
另一个 unique_ptr |
| 三、移动/转换 | unique_ptr<T> p2 = std::move(p1); |
同上 |
| 三、移动/转换 | unique_ptr<Base> p = make_unique<Derived>(); |
派生类指针 |
| 三、移动/转换 | auto p = factory(); |
工厂函数返回值 |
| 四、后期绑定 | p.reset(new T(...)); |
运行时决定 |
一、裸指针接管
前三项案例如下,第四项的C工厂函数参考前面的自定义删除器的案例。
class Widget {
public:
explicit Widget(std::string name) : name_(std::move(name)) {
std::cout << " [" << name_ << "] 构造\n";
}
~Widget() {
std::cout << " [" << name_ << "] 析构\n";
}
void hello() const { std::cout << " [" << name_ << "] hello\n"; }
private:
std::string name_;
};
void demo_from_new() {
// 直接初始化
std::unique_ptr<Widget> p(new Widget("甲"));
// 花括号括号初始化
std::unique_ptr<Widget> q{new Widget("乙")};
p->hello();
q->hello();
} // 输出析构:乙、甲(后构造先析构)
void demo_from_new_array() {
// 注意模板参数是 T[],删除器自动变成 delete[]
std::unique_ptr<Widget[]> arr(new Widget[3]{
Widget("one"), Widget("two"), Widget("three")
});
arr[0].hello(); // 数组版用 operator[] 访问
} // 一次性 delete[] 三个
int main() {
// 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲
SetConsoleOutputCP(CP_UTF8);
ios::sync_with_stdio(true);
cout << "=== main函数正式开始执行 ===" << endl;
demo_from_new();
demo_from_new_array();
return 0;
}
/********打印输出*********
=== main函数正式开始执行 ===
[甲] 构造
[乙] 构造
[甲] hello
[乙] hello
[乙] 析构
[甲] 析构
[one] 构造
[two] 构造
[three] 构造
[one] hello
[three] 析构
[two] 析构
[one] 析构
*****************/
二、make 工厂
前两项是最常用的用法,std::make_unique的优点在于:①异常安全;②代码里不出现裸 new;③ 与make_shared 风格统一,案例代码如下:
class Widget {
public:
explicit Widget(std::string name) : name_(std::move(name)) {
std::cout << " [" << name_ << "] 构造\n";
}
~Widget() {
std::cout << " [" << name_ << "] 析构\n";
}
void hello() const { std::cout << " [" << name_ << "] hello\n"; }
private:
std::string name_;
};
void demo_make() {
// C++14 起可用。auto 推导出 unique_ptr<Widget>
auto p = std::make_unique<Widget>("旺财");
// 显式类型也行(不推荐,多打字还容易写错)
std::unique_ptr<Widget> q = std::make_unique<Widget>("小黑");
p->hello();
}
void demo_make_array() {
auto arr = std::make_unique<Widget[]>(3); // 3 个元素,值初始化
arr[0].hello();
// 注意:这里只能默认构造(调用 Widget()),
// 要带参数构造数组没有 make 版本,得退回 new Widget[3]{...}
}
int main() {
// 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲
SetConsoleOutputCP(CP_UTF8);
ios::sync_with_stdio(true);
cout << "=== main函数正式开始执行 ===" << endl;
demo_make();
demo_make_array();
return 0;
}
/********打印输出*********
=== main函数正式开始执行 ===
[甲] 构造
[乙] 构造
[甲] hello
[乙] hello
[乙] 析构
[甲] 析构
[one] 构造
[two] 构造
[three] 构造
[one] hello
[three] 析构
[two] 析构
[one] 析构
*****************/
第三项std::make_unique_for_overwrite用途是跳过memset清零过程,后续直接用 read/recv/memcpy 把数据写进去,因为马上会被覆盖,所以没必要先清零。如果写成 make_unique<char[]>(4096),它会先清零,然后 read() 又覆盖掉,清零就浪费了。
使用要点
- 只适合 马上会整体覆盖写入 的场景,比如 IO 缓冲区、网络接收缓冲、文件读取缓冲。
- 不能在写入之前读取缓冲区内容,否则是未定义行为。
- 对于有构造函数的类类型,
make_unique_for_overwrite仍然会调用默认构造函数;它主要省的是 POD/数组的初始化开销。 - 需要 C++20 支持。
可以把它理解成:"我知道这块内存马上会被写满,所以别帮我清零。"
三、移动/转换
前两种是常用的移动构造的方法,一种是圆括号,一种是等号,参见函数demo_move:
#include <iostream>
#include <vector>
#include <windows.h>
#include <memory>
using namespace std;
class Widget {
public:
Widget() : name_("默认") { std::cout << " [默认] 构造\n"; }
explicit Widget(std::string name) : name_(std::move(name)) {
std::cout << " [" << name_ << "] 构造\n";
}
~Widget() {
std::cout << " [" << name_ << "] 析构\n";
}
void hello() const { std::cout << " [" << name_ << "] hello\n"; }
private:
std::string name_;
};
void demo_move() {
auto p1 = std::make_unique<Widget>("原主");
std::cout << "移动前 p1 是否为空: " << (p1 == nullptr) << "\n"; // false
// 两种等价写法:
std::unique_ptr<Widget> p2(std::move(p1)); // ① 圆括号
// std::unique_ptr<Widget> p2 = std::move(p1);// ② 等号(拷贝初始化,走移动构造)
std::cout << "移动后 p1 是否为空: " << (p1 == nullptr) << "\n"; // true!
p2->hello(); // 只有 p2 能用
// p1->hello(); // ❌ 空指针解引用,崩溃
}
int main() {
// 新增这两行,强制控制台使用UTF-8编码,同时刷新输出缓冲
SetConsoleOutputCP(CP_UTF8);
ios::sync_with_stdio(true);
cout << "=== main函数正式开始执行 ===" << endl;
demo_move();
return 0;
}
关键理解 :std::move 之后,原指针立刻变空。它不是"复制了一份",而是"把钥匙交出去了"。
第三种make_unique用途是转换移动:派生类 → 基类(多态工厂的根基)
struct Shape { virtual ~Shape() = default; virtual void draw() const = 0; };
struct Circle : Shape { void draw() const override { std::cout << "○\n"; } };
struct Square : Shape { void draw() const override { std::cout << "□\n"; } };
void demo_convert() {
// make_unique<Circle> 返回 unique_ptr<Circle>,
// 直接初始化 unique_ptr<Shape> ------ 转换移动构造自动完成
std::unique_ptr<Shape> s = std::make_unique<Circle>();
s->draw(); // ○
s = std::make_unique<Square>(); // 移动赋值:先 delete 旧的 ○,再接管 □
s->draw(); // □
} // 只析构 □(○ 早在赋值时就被释放了)
这是"工厂返回 unique_ptr<Base>"能成立的原因。前提 :Shape 必须有虚析构函数,否则 delete 走基类指针是 UB。
还有另外一种从工厂函数返回值接收的方式:
std::unique_ptr<Widget> createWidget(std::string name) {
return std::make_unique<Widget>(std::move(name));
// 注意:这里【不要】写 return std::move(local)!
// 返回局部变量直接 return,编译器做 RVO/移动,写 move 反而阻止优化
}
void demo_factory() {
// std::unique_ptr<Widget> p = createWidget("工厂货"); // 接收返回值,不需要显式 move
p->hello();
}
四、后期绑定(定义和使用分离)
先进行空定义,再进行reset重置更新,reset时,会调用之前类的析构函数。
void demo_reset() {
std::unique_ptr<Widget> p; // 先空定义(第一类)
std::cout << "--- 开始绑定 ---\n";
p.reset(new Widget("绑定一号")); // 接管一个裸指针
p->hello();
std::cout << "--- 换绑 ---\n";
p.reset(new Widget("绑定二号")); // 立即析构一号,接管二号
p->hello();
std::cout << "--- 清空 ---\n";
p.reset(); // 立即析构二号,p 变空
// p = nullptr; // 完全等价的另一种写法
std::cout << "--- 结束 ---\n";
}
/* 输出:
--- 开始绑定 ---
[绑定一号] 构造
[绑定一号] hello
--- 换绑 ---
[绑定二号] 构造
[绑定一号] 析构 ← reset(new...) 时,旧对象立即释放
[绑定二号] hello
--- 清空 ---
[绑定二号] 析构 ← reset() 时,立即释放
--- 结束 ---
*/