一、内存泄漏与智能指针的诞生背景
1. 什么是内存泄漏?
内存泄漏是指程序中已分配的堆内存,由于未释放或无法释放,导致内存无法被重复使用,最终引发程序变慢、系统资源耗尽甚至崩溃。
| 类型 | 说明 |
|---|---|
| 堆内存泄漏 | 堆上申请的内存,使用结束后未归还操作系统 |
| 资源泄漏 | 系统资源(如 socket、文件句柄)被创建后未归还 |
2. new 在函数中的执行步骤
| 步骤 | 说明 |
|---|---|
| 1 | 计算类型大小 |
| 2 | 在堆区申请类型大小的空间 |
| 3 | 如果是自定义类型 ,在空间中调用构造函数构造对象;如果是内置类型,不做此步 |
| 4 | 将分配的空间地址给 p |
cpp
// 自定义类型:会调用构造函数
Int* p1 = new Int(10); // 步骤1-4全部执行
// 内置类型:不调用构造函数
int* p2 = new int(10); // 只执行步骤1、2、4
3. 裸指针存在的五大问题
| 问题 | 说明 |
|---|---|
| 无法区分单个对象还是数组 | 不知道该用 delete 还是 delete[] |
| 无法判断是否"拥有"对象 | 不知道是否该销毁它 |
| 无法确定销毁方式 | 是 delete,还是调用特定销毁函数 |
| 难以保证只销毁一次 | 在所有代码路径(分支、异常)中都很难保证 |
| 无法判断指针是否悬挂 | 指向已释放的内存 |
4. 智能指针的核心作用
C++ 没有 GC 垃圾回收机制,手动管理内存极易出错。
智能指针在离开作用域时自动释放内存,解决忘记释放、重复释放、异常泄漏等问题。
智能指针可管理堆内存,也可管理系统资源(文件、socket)。裸指针和智能指针不要混用。
二、RAII 核心思想(智能指针的基石)
1. 什么是 RAII?
RAII(Resource Acquisition Is Initialization,资源获取即初始化)的核心:用局部栈对象管理资源,利用析构函数自动释放。
cpp
class WriteFile {
FILE* _fp;
public:
WriteFile(const string& name) { _fp = fopen(name.c_str(), "w"); }
~WriteFile() { fclose(_fp); } // 自动释放
};
RAII 的三大作用:
| 作用 | 说明 |
|---|---|
| 自动释放资源 | 不用手动写 delete / fclose / unlock,对象出作用域 → 调用析构 → 释放资源 |
| 保证异常安全 | 即使抛异常、提前退出,栈上对象依然被销毁 → 资源照样释放 |
| 代码更安全简洁 | 不用到处记着释放资源,从根源解决内存泄漏、文件忘记关闭等问题 |
关键 :函数无论正常 return 还是抛异常,局部对象都会自动析构,资源一定被释放。
建议:智能指针定义为局部变量,全局变量失去 RAII 意义。
2. RAII 四大步骤
| 步骤 | 说明 |
|---|---|
| 1 | 设计一个类来封装资源 |
| 2 | 在构造函数中获取资源 |
| 3 | 在析构函数中释放资源 |
| 4 | 使用时定义局部对象,由生命周期自动管理 |
3. 完整示例:管理文件
cpp
class WriteFile
{
private:
FILE* _fp;
public:
WriteFile(const std::string& name)
{
errno_t tag = fopen_s(&_fp, name.c_str(), "w");
if (tag)
{
cout << "file open fail" << endl;
exit(1);
}
}
~WriteFile()
{
fclose(_fp);
_fp = nullptr;
}
};
就算发生异常提前退出,WriteFile 对象也会被销毁,文件一定会被关闭。
4. 为什么释放后要置空?
释放指针后,它变成了"野指针"------还指向原来的地址,但那块内存已经归还系统了。
重复释放会导致两种后果:
| 情况 | 说明 |
|---|---|
| 第一种:内存被重新分配给其他人 | 你释放 p 后,其他代码申请内存恰好得到同一块地址(比如 s 指向它)。这时如果你再 delete p,就会把 s 正在用的内存释放掉 → 程序崩溃 |
| 第二种:系统直接终止程序 | 如果这块内存已经空闲,系统检测到你在重复释放已释放的内存,会直接终止程序(不给你任何机会) |
cpp
delete p;
p = nullptr; // ✅ 最佳实践:释放后立即置空
注意 :一旦释放空间,立即将指针置为 nullptr,这样重复 delete 也不会有问题(delete nullptr 是安全的)。
三、auto_ptr 剖析(C++98 智能指针)
1. auto_ptr 源码结构
cpp
namespace MySmartPtr
{
template<class _Ty>
class My_auto_ptr
{
public:
typedef _Ty element_type;
private:
_Ty* _M_ptr;
public:
explicit My_auto_ptr(_Ty* p = nullptr) : _M_ptr(p) {}
~My_auto_ptr()
{
delete _M_ptr;
}
_Ty* get() const { return _M_ptr; }
_Ty& operator*() const { return *_M_ptr; }
_Ty* operator->() const { return _M_ptr; }
void reset(_Ty* p = nullptr)
{
delete _M_ptr;
_M_ptr = p;
}
_Ty* release()
{
_Ty* _tmp = _M_ptr;
_M_ptr = nullptr;
return _tmp;
}
void Swap(My_auto_ptr& other)
{
std::swap(this->_M_ptr, other._M_ptr);
}
// C++98 的拷贝构造:所有权转移(相当于 C++11 的移动构造)
My_auto_ptr(My_auto_ptr& _other)
{
_M_ptr = _other._M_ptr;
_other._M_ptr = nullptr;
}
My_auto_ptr& operator=(My_auto_ptr& _other)
{
if (this != &_other)
{
delete _M_ptr;
_M_ptr = _other._M_ptr;
_other._M_ptr = nullptr;
}
return *this;
}
};
}
2. 核心辅助函数详解
| 函数 | 作用 |
|---|---|
get() |
返回被管理的裸指针,不改变所有权 |
operator->() |
重载箭头运算符,让智能指针可以像裸指针一样访问成员 |
operator*() |
重载解引用运算符,让智能指针可以像裸指针一样取值 |
reset(p) |
释放当前对象,接管新对象 p |
release() |
交出控制权(不释放内存),返回裸指针,内部指针置空 |
Swap() |
交换两个智能指针的内容 |
3. release() vs reset() 的区别
| 函数 | 作用 | 是否释放内存 |
|---|---|---|
release() |
交出控制权,返回裸指针 | ❌ 不释放 |
reset() |
释放旧内存并指向新内存 | ✅ 释放 |
注意 :值相同 ≠ 对象相同,reset 销毁旧对象、创建新对象,即使新旧值一样,也是不同的对象。
cpp
sp.reset(new Int(20)); // 释放旧内存,接管新内存
Int* p = sp.release(); // sp 置空,p 指向原内存,需手动 delete
delete p;
4. Swap() 交换示例
cpp
MySmartPtr::My_auto_ptr<Int> sp(new Int(10));
MySmartPtr::My_auto_ptr<Int> p2; // _M_ptr = nullptr
p2.Swap(sp); // 交换后,sp 变空,p2 接管原内存
// sp->PrintInt(); // error:sp 已空
p2->PrintInt(); // 输出 10
说明 :Swap() 交换两个智能指针的 _M_ptr 指针,不涉及内存的释放和重新分配,只是交换所有权。常用于实现移动语义或避免临时对象拷贝。
5. 智能指针的访问方式
cpp
Int* ip = new Int(10);
// 裸指针对对象的访问
ip->PrintInt();
(*ip).Value() = 10; // . 的优先级高于 *,所以要加 ()
// 智能指针对对象的访问
MySmartPtr::My_auto_ptr<Int> sp(new Int(10)); // sp 是一个对象
// sp. 是智能指针的方法,sp-> 是所指值的方法
sp->PrintInt();
(*sp).Value() = 100;
sp.operator->()->PrintInt(); // 与 sp->PrintInt(); 没有区别
sp.operator*().PrintInt();
说明 :sp->PrintInt() 实际调用的是 sp.operator->()->PrintInt(),operator->() 返回裸指针,再调用该指针的成员函数。operator*() 返回对象引用,因此 (*sp).Value() 可以修改对象的值。
6. auto_ptr 的三大致命缺陷
缺陷一:所有权转移语义不符合直觉
拷贝构造/赋值时,原对象会失去对资源的所有权,变为空指针,后续使用会导致崩溃。
cpp
int main()
{
MySmartPtr::My_auto_ptr<int> ap(new int(10));
MySmartPtr::My_auto_ptr<int> bp = ap; // ap 的所有权转移给 bp
cout << *ap << endl; // 崩溃:ap 已为空
}
缺陷二:无法管理数组对象
析构函数使用 delete 而非 delete[],无法正确释放数组对象。
cpp
void func()
{
MySmartPtr::My_auto_ptr<Int> ip(new Int[10]); // 调用十次构造函数
return; // 析构函数只释放一个,error
}
特殊情况 :如果 Int 类型没有析构函数,程序仍能正常运行。原因:Int 类型没有析构函数,new 时不会添加对象个数,仍然当做内置类型看待,只是释放空间,不调用析构函数。
缺陷三:无法在 STL 容器中使用
cpp
void func()
{
std::list<MySmartPtr::My_auto_ptr<Int>> mylist;
mylist.push_back(MySmartPtr::My_auto_ptr<Int>(new Int(10)));
// list 的拷贝构造或复制要带有 const 的左值引用,auto_ptr 不满足条件
}
STL 容器的元素必须支持可复制、可赋值,而 auto_ptr 的拷贝会改变所有权,导致容器内部操作时出现悬空指针。
7. C++98 auto_ptr 的关键背景
C++98 标准中没有右值引用,也没有 std::move/std::forward 语义,这导致 auto_ptr 的拷贝构造和赋值只能采用"所有权转移"的方式实现,这也是它后续被弃用的根本原因。
四、值语义 vs 对象语义
1. 值语义(Value Semantics)
拷贝后,两个对象完全独立,修改一个不影响另一个。
cpp
int main()
{
Int a(10);
Int b(a); // 拷贝后,a 和 b 独立
a.Value() = 100;
b.PrintInt(); // 输出 10,不受影响
}
适用场景 :内置类型、可独立复制的自定义类型(如 Int、Point)。
2. 对象语义(Object Semantics)
对象不可拷贝 ,拷贝会导致多个对象共享同一资源,析构时重复释放。正确做法是禁止拷贝。
cpp
class WriteFile
{
FILE* _fp;
public:
WriteFile(const std::string& name) { /* 打开文件 */ }
~WriteFile() { fclose(_fp); }
// 禁止拷贝(对象语义)
WriteFile(const WriteFile&) = delete;
WriteFile& operator=(const WriteFile&) = delete;
};
适用场景:文件句柄、智能指针、数据库连接等管理独占资源的类。
什么样的类型不允许拷贝?
面向对象意义下的对象(如文件、网络连接、智能指针),拷贝会导致两个对象指向同一份资源,析构时重复释放。这类类型应该禁止拷贝,或者使用移动语义转移所有权。
示例 :int 具有值语义,但放在 auto_ptr 中不具有值语义。
cpp
int main()
{
MySmartPtr::My_auto_ptr<int> ip(new int(100));
MySmartPtr::My_auto_ptr<int> ip2(ip);
cout << *ip2 << endl;
cout << *ip << endl; // error:ip 已为空
}
五、const 成员函数与指针的 const 特性
1. 类中自动给 this 加的 const
cpp
class MyClass {
void func();
};
// 编译器实际处理:void func(MyClass* const this);
解释 :编译器自动给成员函数添加 this 指针,类型是 MyClass* const this。这个 const 修饰的是 this 指针本身,表示不能改变 this 的指向 (即不能让它指向别的对象),但可以修改 this 指向的成员变量。
2. 函数后面的 const
cpp
int getValue() const;
// 等价于:编译器将 this 处理成 const ClassName* const this
解释 :函数后面加 const,会把 this 指针变成 const ClassName* const this:
-
第一个
const(在*左边):封锁this指向的内容,即不能修改成员变量 -
第二个
const(在*右边):封锁this指针本身,即不能改变指向
所以 const 成员函数只能读取成员变量,不能修改。
3. 函数返回类型前面的 const
cpp
const int& getValue() const
解释 :const 放在返回值类型前面,修饰的是返回值本身,防止调用者通过返回值修改原对象。
-
const int&:返回常引用,调用者不能通过这个引用来修改原对象的value -
函数后面的
const:该函数是只读的,不能修改成员变量
cpp
const int& getValue() const { return value; }
int main() {
MyClass obj;
int x = obj.getValue(); // ✅ 可以,拷贝
// obj.getValue() = 10; // ❌ 错误,返回值是 const 引用,不能赋值
}
对比 :如果不加 const,返回 int&,调用者就可以修改原对象:
cpp
int& getValue() { return value; } // 不加 const,可以修改
obj.getValue() = 10; // ✅ 可以修改
总结 :返回类型前的 const,在返回引用时才有实际意义------防止外部修改原对象。
4. get() 函数的 const 分析
cpp
_Ty* get() const { return _M_ptr; }
解释 :get() 后面的 const 让 this 变成 const My_auto_ptr* const this,即 this 的指向和 this 指向的内容都不能改。但返回值 _Ty* 前面没有 const,所以调用者拿到裸指针后可以修改指针指向的内容。
什么时候需要加 const?
-
如果希望
_M_ptr指向的内容不被修改,返回值前面要加const:const _Ty* get() const -
如果想允许修改,就不加:
_Ty* get() const -
如果返回引用且希望引用本身不变,用
_Ty* const& get() const
六、RAII 与异常安全
1. 异常路径中的资源释放
下面的代码演示了即使抛出异常,智能指针依然会自动释放资源。
cpp
int func(int i)
{
MySmartPtr::My_auto_ptr<Int> sp(new Int(10));
if (i < 0)
{
throw std::out_of_range("i < 0");
}
return i;
}
int main()
{
try
{
func(-1);
}
catch (const std::out_of_range& e)
{
cout << e.what() << endl;
}
return 0;
}
为什么?
当函数执行到 throw 时,程序会立即展开栈帧,逐层销毁当前函数中的所有局部对象。sp 是局部对象,它的析构函数会被自动调用,从而释放所管理的堆内存。无论正常 return 还是抛异常,局部对象都会被销毁,这是 C++ 的确定性行为。
关键:无论正常返回还是抛异常,局部对象都会自动析构,资源一定被释放。这是 RAII 的核心优势。
2. 智能指针与裸指针不要混用
下面的代码演示了 release() 交出控制权后手动释放,以及混用可能带来的问题。
cpp
int main()
{
MySmartPtr::My_auto_ptr<Int> sp(new Int(10));
sp->PrintInt();
sp.reset(new Int(20));
sp->PrintInt();
Int* p = sp.release(); // sp 置空,交出控制权
delete p; // 手动释放
// 不要混用:既用智能指针又用裸指针,容易混乱
return 0;
}
说明 :release() 交出控制权后,原内存由调用者负责释放。智能指针和裸指针混用容易导致所有权混乱,引发重复释放或内存泄漏。
七、总结
| 知识点 | 核心要点 |
|---|---|
| 内存泄漏 | 堆内存或系统资源使用后未释放,导致资源耗尽 |
| new 执行步骤 | 计算大小 → 申请空间 →(自定义类型)调用构造 → 返回地址 |
| 智能指针三个作用 | 自动释放资源、保证异常安全、代码更安全简洁 |
| RAII | 用局部栈对象管理资源,构造获取、析构释放 |
| auto_ptr 缺陷 | 所有权转移、无法管理数组、无法用于 STL 容器 |
| release() vs reset() | release() 交出控制权不释放;reset() 释放旧内存并指向新内存 |
| 值语义 | 拷贝后独立,互不影响,允许拷贝 |
| 对象语义 | 资源独占,拷贝无意义,应禁止拷贝(= delete) |
| const 成员函数 | 修饰 this 为 const ClassName* const,不能修改成员变量 |
| 异常安全 | RAII 确保异常时局部对象析构,资源自动释放 |
| 建议 | 裸指针和智能指针不要混用;释放后立即置空 |