C++ 智能指针:auto_ptr

一、内存泄漏与智能指针的诞生背景

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,不受影响
}

适用场景 :内置类型、可独立复制的自定义类型(如 IntPoint)。

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() 后面的 constthis 变成 const My_auto_ptr* const this,即 this 的指向和 this 指向的内容都不能改。但返回值 _Ty* 前面没有 const,所以调用者拿到裸指针后可以修改指针指向的内容。

什么时候需要加 const

  • 如果希望 _M_ptr 指向的内容不被修改,返回值前面要加 constconst _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 成员函数 修饰 thisconst ClassName* const,不能修改成员变量
异常安全 RAII 确保异常时局部对象析构,资源自动释放
建议 裸指针和智能指针不要混用;释放后立即置空
相关推荐
wuminyu1 小时前
专家视角看Lambda表达式的原理解析
java·linux·c语言·jvm·c++
ximu_polaris1 小时前
设计模式(C++)-行为型模式-命令模式
c++·设计模式·命令模式
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 189. 轮转数组 | C++ 三次反转经典魔法 (O(1) 空间)
c++·算法·leetcode
淀粉肠kk2 小时前
【C++11】智能指针详解
开发语言·c++
不想写代码的星星2 小时前
COW(Copy-on-Write):开抄开抄,哎嘿,我装的
开发语言·c++
Sylvia-girl2 小时前
C++内存如何管理?
java·jvm·c++
无敌秋2 小时前
C++ 单例模式
c++·单例模式
Brilliantwxx2 小时前
【C++】认识标准库STL(2)
开发语言·c++
故事还在继续吗2 小时前
STL 容器算法手册
开发语言·c++·算法