【C++高阶】:智能指针的全面解析

article/2025/8/17 13:47:20

✨                                          落絮无声春堕泪,行云有影月含羞     🌏

📃个人主页:island1314

🔥个人专栏:C++学习

🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


目录

1.引言

 1.1 动态内存与智能指针

🌸1.1.1 动态内存

🌸1.1.2 智能指针

1.2 智能指针的重要性

1.3 像指针一样使用

1.4 支持智能指针对象拷贝

2. C++标准库的智能指针

2.1 auto_ptr

🌈2.1.1 基本概念

🌈2.1.2 基本用法

2.2 unique_ptr

✨2.2.1 基本概念

✨2.2.2 基本用法

2.3 shared_ptr

🎈2.3.1 基本概念

🎈2.3.2 基本用法

🎈2.3.3 注意事项

2.4 weak_ptr

🧩2.4.1 基本概念

🧩2.4.2 基本用法

🧩2.4.3 对循环引用的解决

🧩2.4.4 与shared_ptr的关系

3. 智能指针的实现

3.1 auto_ptr模拟实现

3.2 unique_ptr模拟实现

3.3 shared_ptr模拟实现

3.4 weak_ptr模拟实现

📖总结


1.引言

到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期:

  • 全局对象:程序启动时分配,在程序结束时销毁。
  • 局部对象:当我们进入其定义所在的程序块时被创建,在离开块时销毁。
  • 局部static对象:在第一次使用前分配,在程序结束时销毁。

我们的程序到目前为止只使用过静态内存或栈内存:

  • 静态内存:保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存:保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

  📒除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

 1.1 动态内存与智能指针

🌸1.1.1 动态内存

  📒除了局部和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。动态对象的正确释放是编程中极其容易出错的地方。为了更安全地使用动态对象,C++标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  • delete:在受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

但是动态内存的使用很容易出问题;

  • 内存泄漏:有时我们会忘记释放内存,在这种情况下就会产生内存泄漏
  • 非法指针:有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针

  📒C++11为了更容易(同时也更安全)地使用动态内存,新的标准库提供了智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。具体说明如下:

🌸1.1.2 智能指针

        从比较简单的层面来看,智能指针是 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 机制对普通指针进行的一层封装,利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。

  • 智能指针本身: 智能指针是一个类模板的实例,通常作为局部变量存在于栈区(Stack)。当函数返回或者局部变量超出其作用域时,栈区的内存会被自动释放。
  • 智能指针管理的对象: 智能指针通常用来管理在堆区(Heap)上分配的内存。这是通过调用如new操作符来完成的。堆区的内存会一直存在,直到显式地释放它(使用delete操作符)或者当程序结束时才会被系统回收

其实质如下:

  •  由于C++中没有垃圾回收机制,必须自己释放掉分配的内存,否则就会造成内存泄露。比如对于普通的new或者malloc分配空间,需要及时的释放空间,否则容易造成内存泄漏,导致堆区空间不足。但是,随着代码量的增多,很容易忘记释放空间,解决这个问题最有效的方法是使用智能指针(smart pointer),其会自动释放。省去手动管理内存
  • 智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数每使用它一次,内部引用计数加1,计数器的大小等于指向对象的智能指针的数量,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存

注意:智能指针的头文件为<memory>

1.2 智能指针的重要性

  📒在C++编程中,内存管理一直是一个不可或缺的话题。传统的C++程序员依赖newdelete(新建和删除)来手动管理内存,但是由于new和delete不能自动管理资源不支持自定义删除器,导致使用该方式容易导致内存泄漏或是双重释放等问题。这就是智能指针(Smart Pointers)登场的原因。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效。

在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

  📒智能指针不仅仅是一个指针,它是一个对象,拥有生命周期(Lifetime)。当智能指针的生命周期结束时,它会自动释放所拥有的资源。这种自动管理机制极大地减少了程序员的负担,也降低了出错的可能性。

“The best code is no code at all.” - Jeff Atwood

这句话在这里非常合适。【越少的代码用于管理内存,越少的地方会出错。】智能指针就是这样一种工具,让你能更专注于业务逻辑而非内存管理。

1.3 像指针一样使用

下面将用一个自定义智能指针SmartPtr为例:

​// 模板
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr) //当智能指针未初始化时,赋予nullptr缺省值: _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }
private:T* _ptr;
};
int main()
{SmartPtr<int> p(new int(1));cout << *p << endl;return 0;
}

其中重载了*->运算符,使得使用这个类就像使用指针一样。智能指针是一个模板类,以能够管理任何类型的指针引用的内存,如果模板参数是一个有公有成员的类,那么还能使用->访问其成员。

1.4 支持智能指针对象拷贝

上面实现的智能指针SmartPtr是极不完善的,如果想实现拷贝构造和拷贝赋值:

int main(){	SmartPtr<int> ap1(new int(1));SmartPtr<int> ap2(ap1); //拷贝构造SmartPtr<int> ap3(new int(2));SmartPtr<int> ap4(new int(2));ap4 = ap3; // 拷贝赋值return 0;
}

错误(Clion):

malloc: *** error for object 0x600003c84030: pointer being freed was not allocated

造成程序崩溃的原因:是在这个类中没有实现拷贝构造函数和拷贝赋值函数,而编译器默认生成的全都是对内置类型的浅拷贝(值拷贝):相当于ap1和ap2、ap3和ap4共同管理同一块空间。当出了ap1的作用域后,调用析构函数,释放空间,ap2再次调用析构函数时导致这块已经被释放的空间再次被释放。ap3和ap4同理。 

要解决浅拷贝造成的二次析构问题,就必须要去实现深拷贝的拷贝构造函数和拷贝赋值函数吗?

答案是不用的,智能指针的功能需求是模拟指针的使用,本质是帮指针托管资源,那么指针的拷贝或赋值操作就相当于两个指针指向同一块内存空间。资源管理权转移,通过不负责任的拷贝,会导致被拷贝对象悬空。虽然资源能得到释放,但是会造成垂悬指针。智能指针将内存资源的管理和对象的生命周期绑定在一起,如果只是像上面一样简单地满足RAII,那么一定会发生二次析构的问题,因为创建的智能指针对象一定会调用析构函数,且不论程序是否正常结束。

  • 程序正常结束:对象出了作用域调用析构函数;

  • 程序不正常结束:例如抛异常,跳转到catch块相当于跳转到另一个函数的栈帧中,也相当于出了作用域,依然调用析构函数。

后面以标准库中(C++98)智能指针auto_ptr为例。

2. C++标准库的智能指针

在c++中,智能指针一共定义了4种:
auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。

📝其中,auto_ptr 在 C++11已被摒弃,在C++17中已经移除不可用。

📙虽然auto_ptr有很多问题,但是也没有取消,因为可能有人在用,因此后面弄了一个

Boost库,属于C++扩展库(第三方库),不属于C++标准库(C++标准库是写了头文件就能用)

注意:本文主要讲的就是后面的 三种智能指针。

智能指针类型
unique_ptr独占的智能指针,该指针独占对象的所有权,每个对象智能有一个该指针
shared_ptr共享的智能指针,多个共享指针可以指向同一个对象
weak_ptr弱引用的智能指针,该指针是对象的一个非拥有性引用,不共享指针,不能操作资源,用来指向一个shared_ptr,主要用来避免shared_ptr循环引用导致的内存泄露

为了方便我们对下面智能之类的理解,我们定义如下的类

// Date类
struct Date{int _year;int _month;int _day;Date (int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){}~Date() {cout << "~Date()" << endl;}
};
//指定删除器模板
template<class T>
class DeleteArray{
public:void operator()(T* ptr){delete[] ptr;}
};
//文件删除的模板
class Fclose{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};
//Test类
class Test
{
public:Test() : _num(0){cout << "Test() 构造成功..." << endl;}Test(int x) : _num(0){cout << "Test(int x) 构造成功, x = " << x << endl;}Test(string str) : _num(0){cout << "Test(string str) 构造成功, str = " << str << endl;}~Test(){cout << "~Test() 析构成功..." << endl;}void setValue(int v){this->_num = v;}void print(){cout << "_num: " << this->_num << endl;}
private:int _num;
};

2.1 auto_ptr

🌈2.1.1 基本概念

为了解决上面1.4 里面的问题,由于出现多次析构问题的本质是同一块内存空间被多个对象通过管理,因此如果将资源的管理权只交给一个对象,就不会出现多次析构问题。

🌈2.1.2 基本用法
int main(){	auto_ptr<Date> ap1(new Date);//拷贝时,管理权限转移,被拷贝悬空auto_ptr<Date> ap2(ap1);//下面这样会使程序会崩,因为auto_ptr的拷贝是管理权转移,这样就会导致ap1空了ap1->_year++;return 0;
}

然而,将一个对象对资源的管理权转移后,就意味着这个对象再对资源访问是一个非法操作,程序会因此崩溃。如果让不熟悉auto_ptr原理的人使用,因为拷贝操作而造成非法指针或内存泄漏是有可能的(拷贝悬空),而这也是致命的错误,因此许多公司明文规定禁止auto_ptr的使用进而由C++11的unique_ptr和shared_ptr取代


2.2 unique_ptr

✨2.2.1 基本概念

出现多次析构问题的本质是同一块内存空间被多个对象通过管理,如果将资源的管理权只交给一个对象,就不会出现多次析构问题。

C++98的auto_ptr因为拷贝和赋值操作而造成内存泄漏和悬垂指针的问题而饱受诟病,C++11引入的unique_ptr则粗暴地砍掉了它的拷贝和赋值功能。这通过C++11引入的关键字delete的新功能实现。

在C++11之前,可以通过将构造函数和拷贝赋值函数私有声明实现。

🌞它实现对对象的独占所有权语义。这意味着一个unique_ptr在任何时候都指向一个对象,而且这个对象只能由一个unique_ptr拥有。当unique_ptr被销毁(例如离开其作用域)时,它所指向的对象也会被自动删除。

✨2.2.2 基本用法

样例1:(初概)

int main() {unique_ptr<Date> up1(new Date);//不支持拷贝//unique_ptr<Date> up2(up1);//unique_ptr<Date> up2(new Date[6]); //error,因为类型不匹配,由于我们是用new []构造的,那么就要用delete[]去销毁//定制删除器解决上述问题unique_ptr<Date, DeleteArray<Date>> up2(new Date[6]); //用定制的删除器//unique_ptr<Date[]> up2(new Date[5]);unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r"));return 0;
}

样例2:(详细)

1. 初始化

  • std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。

 2. 指定删除器

  •  unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器
int main()
{/*--------------------------  一,初始化智能指针unique_ptr  ------------------------------*/// 1.通过构造函数初始化unique_ptr<int> up1(new int(3));//unique_ptr<int> up = up1;  // 编译错误,不能复制// 2.通过移动函数初始化unique_ptr<int> up2 = move(up1); // 现在up2独占资源,up1变为空// 3.通过reset,释放资源并将指针置为空,然后再初始化up1.reset(new int(7));//对比reset,release会释放资源的所有权但不删除,返回原始指针up1.release();/*--------------------------  二,unique_ptr的使用  ------------------------------*///1.方法一unique_ptr<Test> up3(new Test(666));Test* pt = up3.get();pt->setValue(6);pt->print();//2.方法二up3->setValue(777);up3->print();/*------------------------------------  三,指定删除器  -----------------------------------*/1.函数指针类型//using ptrFunc = void(*)(Test*);//unique_ptr<Test, ptrFunc> up4(new Test("hello"), [](Test* t) {//    cout << "-----------------------" << endl;//    delete t;//    });//2.仿函数类型(利用可调用对象包装器)unique_ptr<Test, function<void(Test*)>> up4(new Test("hello"), [](Test* t) {cout << "-----------------------" << endl;delete t;});/*---------- 四,独占(共享)的智能指针可以管理数组类型的地址,能够自动释放 ---------*/unique_ptr<Test[]> up5(new Test[3]);//在c++11中shared_ptr不支持下面的写法,c++11以后才支持的shared_ptr<Test[]> up6(new Test[3]);return 0;
}


2.3 shared_ptr

🎈2.3.1 基本概念

🌙shared_ptr是C++11的智能指针,通过引用计数的方式解决智能指针的拷贝问题。

🌙每个被管理的资源有有一个对应的引用计数,这个引用计数记录当前有多少对象在管理这块资源。

🌙每新增加一个对象管理这块资源则对该资源的引用计数++,一个对象不在管理这块资源或对象析构时那么该资源对应的引用计数 – –,当一个资源的引用计数为0时那么就说明已经没有对象在管理这块资源了,这时候就可以进行释放了。

引用计数的方式能够支持多个对象一起管理一个资源,也就支持智能指针的拷贝,只有当资源的引用计数减为0时才会释放,保证了同一个资源不会被多次释放:

🌙实现了对象的共享所有权语义。多个shared_ptr可以指向同一个对象,并且每个shared_ptr持有一个引用计数。当最后一个指向某个对象的shared_ptr被销毁或重置时,该对

象才会被删除。

🎈2.3.2 基本用法

样例1:(初概)

int main() {//vector<shared_ptr<Date>> v;shared_ptr<Date> sp1(new Date);shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl; //查看其引用计数//定制删除器shared_ptr <Date[]> sp4(new Date[5]);//shared_ptr<FILE, Fclose> up5(fopen("test.cpp", "r")); //不支持传这个模板参数shared_ptr<FILE> up5(fopen("test.cpp", "r"), Fclose());shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);shared_ptr<int> sp7((int*)malloc(40), [](int* ptr){cout << "free:" << ptr << endl;free(ptr);});return 0;
}

1. 初始化

  • 共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count

shared_ptr通过一个指针保持对一个对象的共享所有权。多个shared_ptr对象可以拥有同一个对象。当以下情况之一发生时,对象被销毁并释放其内存:

  • 拥有该对象的最后一个shared_ptr被销毁;
  • 通过reset()函数将shared_ptr赋值为另一个指针。

2. 获取原始指针

  • 对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的get()方法得到原始地址

 3. 指定删除器

  •   当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

4. 引用计数

  • auto_ptr转移资源后造成内存泄漏和悬垂指针的主要原因就是每个auto_ptr智能指针对象管理的资源是各自独立的,非此即彼。shared_ptr共享同一个资源,内存资源只在最后一个智能指针解除引用时释放,这样就不会造成资源被单方面地接管造成的问题。
  • 引用计数使得一个空间可以被多个对象管理,当引用计数为0时,说明已经没有智能指针管理这块内存空间了,此时才能释放资源,弥补了auto_ptr的缺陷。要知道引用计数的值,只需要调用shared_ptr的成员函数use_count()即可。

样例2:(详细)

int main()
{/*----------------------------  一,初始化智能指针shared_ptr  ------------------------------*/// 1.通过构造函数初始化shared_ptr<int> sp1(new int(3));cout << "sp1管理的内存引用计数:" << sp1.use_count() << endl;// 2.通过移动和拷贝构造函数初始化shared_ptr<int> sp2 = move(sp1);cout << "sp1管理的内存引用计数:" << sp1.use_count() << endl;cout << "sp2管理的内存引用计数:" << sp2.use_count() << endl;shared_ptr<int> sp3 = sp2; //赋值cout << "sp2管理的内存引用计数: " << sp2.use_count() << endl;cout << "sp3管理的内存引用计数: " << sp3.use_count() << endl;// 3.通过 std::make_shared初始化shared_ptr<int> sp4 = make_shared<int>(8);shared_ptr<Test> sp5 = make_shared<Test>(7);shared_ptr<Test> sp6 = make_shared<Test>("Love Life");// 4.通过reset初始化sp6.reset();//重置sp6, ps6的引用基数为0cout << "sp6管理的内存引用计数: " << sp6.use_count() << endl;sp5.reset(new Test("hello")); //重置了指针的指向对象,原来的对象已经释放cout << "sp5管理的内存引用计数: " << sp5.use_count() << endl;cout << endl << endl;/*-----------------------------  二,共享智能指针shared_ptr的使用  ------------------------------*/// 1.方法一Test* t = sp5.get();t->setValue(1000);t->print();// 2.方法二sp5->setValue(7777);sp5->print();cout << endl << endl;///*------------------------------  三,指定删除器  -----------------------------------*/// 1.简单举例shared_ptr<Test> ppp(new Test(100), [](Test* t) {//释放内存cout << "Test对象的内存被释放了......." << endl;delete t;});printf("----------------------------------------------------------------------\n");2.如果是数组类型的地址,就需要自己写指定删除器,否则内存无法全部释放//	//shared_ptr<Test> p1(new Test[5], [](Test* t) {//	//    delete[]t;//	//    });3.也可以使用c++给我们提供的 默认删除器函数(函数模板)shared_ptr<Test> p2(new Test[3], default_delete<Test[]>());4.c++11以后可以这样写 也可以自动释放内存shared_ptr<Test[]> p3(new Test[3]);return 0;
}

 此外我们还可以封装一个函数模板make_shared_array方法来让shared_ptr支持数组:

template <typename T>
shared_ptr<T> make_share_array(size_t size)
{//返回匿名对象return shared_ptr<T>(new T[size], default_delete<T[]>());
}int main()
{shared_ptr<int> ptr1 = make_share_array<int>(10);cout << ptr1.use_count() << endl;shared_ptr<string> ptr2 = make_share_array<string>(7);cout << ptr2.use_count() << endl;
}
🎈2.3.3 注意事项

        shared_ptr中的引用计数是存放在堆区,因为这样可以让所有指向同一个对象的shared_ptr。如果引用计数在栈区,那么当一个shared_ptr改变指向或者离开作用域时,就无法通知其他shared_ptr更新引用计数了。因此,引用计数也不能是静态成员,每个类型实例化的智能指针对象时共用静态成员,这会导致管理相同资源的对象和管理不同资源的对象共用同一个引用计数。

由于在堆区的引用计数和同一类型的智能指针是绑定在一起的,当智能指针释放资源时,也需要释放引用计数占用的内存。


2.4 weak_ptr

🧩2.4.1 基本概念

⭐弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手,是对shared_ptr所管理对象的一个非拥有性引用,它不管理shared_ptr内部的指针。std::weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数析构也不会减少引用计数。最后一个指向对象的shared_ptr被销毁时,无论是否还有weak_ptr指向该对象,对象都会被删除。它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在,解决shared_ptr可能导致的循环引用问题

在这部分利用率局部变量的特性,C++的局部变量存在栈中,当变量的生命周期结束后,那栈会自动释放空间。而智能指针同样为局部变量,存在栈中。

🧩2.4.2 基本用法

样例1:(初概)

// ———————— weadk_ptr ——————————
int main()
{// weak_ptr不支持管理资源,不支持RAII//std::weak_ptr<Date> wp1(new Date);weak_ptr<Date> wp;shared_ptr<Date> sp; //保证不过期{shared_ptr<Date> n1(new Date);wp = n1;cout << wp.expired() << endl;n1->_day++;//sp = wp.lock();}// 出了作用域就失效了cout << wp.expired() << endl;}

样例2:(详细)

std::weak_ptr底层与std::shared_ptr共享相同的引用计数机制,但不会增加计数。


int main()
{/*----------------------------  一,初始化智能指针weak_ptr  ------------------------------*/// weak_ptr不支持管理资源,不支持RAIIshared_ptr<int> sp(new int(2));weak_ptr<int> wp1;; //构造了一个空weak_ptr对象weak_ptr<int> wp2(wp1); //通过一个空weak_ptr对象构造了另一个空weak_ptr对象weak_ptr<int> wp3(sp); //通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象weak_ptr<int> wp4; wp4 = sp; //通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象(这是一个隐式类型转换)weak_ptr<int> wp5;wp5 = wp3; //通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象/*----------------------------  二,weak_ptr常用函数  ------------------------------*/// 1.通过调用weak_ptr类提供的use_count()方法可以获得当前共享该资源的shared_ptr数量cout << "wp1管理的shared_ptr内存引用计数:" << wp1.use_count() << endl;cout << "wp2管理的shared_ptr内存引用计数:" << wp2.use_count() << endl;cout << "wp3管理的shared_ptr内存引用计数:" << wp3.use_count() << endl;cout << "wp4管理的shared_ptr内存引用计数:" << wp4.use_count() << endl;cout << "wp5管理的shared_ptr内存引用计数:" << wp5.use_count() << endl;cout << endl << endl;// 2.通过调用std::weak_ptr类提供的expired()方法来判断//判断指针所指的内存空间是否被释放掉 / 指针是否为空 / 是否还有shared_ptr指针指向weak_ptr指向的内存空间shared_ptr<int> sp2 = make_shared<int>(10);weak_ptr<int> wp6(sp2); // shared_ptr初始化weak_ptr  sp2.reset(new int); // 此时,已没有一个shared_ptr指针指向weak_ptr指向的内存区域  cout << "是否已没有shared_ptr指针指向该内存区域:" << wp6.expired() << endl;shared_ptr<int> sp22 = nullptr;weak_ptr<int> wp7(sp22);cout << "weak_ptr指针是否为空:" << wp7.expired() << endl;// 3.通过调用weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象,返回一个shared_ptr,增加引用计数shared_ptr<int> sp3 = wp3.lock(); // 利用返回的shared_ptr初始化  cout << "share_ptr的初始化为:" <<*sp3 << endl;cout << "wp3管理的shared_ptr内存引用计数:" << wp3.use_count() << endl;return 0;
}
🧩2.4.3 对循环引用的解决

假如存在这样的一个结构体

struct ListNode{int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;//用weak_ptr就可以避免内存泄露//weak_ptr<ListNode> _next;//weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};

 情况一:当我们在shaed_ptr的正常情况下未出现内存泄露

int main()
{// 下面这就是循环引用 --> 内存泄露std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);n1->_next = n2;//n2->_prev = n1; //互相指向,就会出现内存泄露cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

情况二:当我们增加一句n2->_prev = n1; 到函数中,此时就出现了互相指向,导致了内存泄露

情况三:造成问题的本质是引用计数永不为0,那么只要将其中一个智能指针改为weak_ptr即可:

🧩2.4.4 与shared_ptr的关系

🌸由上面可知:weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期。也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

🌸它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

也就是说,weak_ptr是为了弥补shared_ptr循环引用而生的,它没有RAII的特性,不直接管理资源,只是shared_ptr的跟班,这也是weak_ptr支持使用shared_ptr构造的原因。

在使用上,weak_ptr支持指针所有的操作。它不是一个功能型的智能指针,而是辅助型,它的使命是解决shared_ptr造成的循环引用问题。

🌸与 shared_ptr 不同,weak_ptr 不能直接访问所指向的对象。要访问对象,需要先调用 lock() 方法将其转换为 shared_ptr。如果所指向的对象已经被销毁,则 lock() 方法返回空指针。

3. 智能指针的实现

📚看完前面其智能指针的实现,相信大家对智能指针也有个大概的了解了,下面我们来实现部分的智能指针吧,如unique_ptr、shared_ptr、weak_ptr.

智能指针的实现,必须解决下面三个问题:

  • RAII,将资源交给对象的生命周期管理,即构造对象时开始获取(管理)资源,析构对象时释放资源;
  • 像真正的指针一样使用;
  • 支持智能指针对象的拷贝。

其中最容易实现的是像指针一样使用,只要重载使用指针的运算符即可。其次是实现RAII,最后是智能指针对象的拷贝。

3.1 auto_ptr模拟实现

auto_ptr的实现原理:管理权转移的思想

  • 析构函数:需要对它管理的指针判空,只有指针非空时才能对其进行释放资源操作,释放资源以后对其置空。

  • 拷贝构造函数:用传入对象管理的内存资源来构造当前对象,并将传入对象管理资源的指针置空。

  • 拷贝赋值函数:先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。

namespace qian {template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};

3.2 unique_ptr模拟实现

模拟实现的过程:去除auto_ptr中拷贝和赋值的函数。

📚注意:C++98中,delete的意思是不让编译器自动生成默认函数,而C++11为了实现这个智能指针,赋予delete一个新功能:不允许调用,因此我们在下面的拷贝和赋值中用到了delete

namespace qian
{template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}~unique_ptr(){if (_ptr != nullptr){cout << "delete:" << _ptr << endl;delete _ptr;_ptr = nullptr;}}unique_ptr(auto_ptr<T>& ap) = delete;unique_ptr& operator=(auto_ptr<T>& ap) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
int main()
{qian::unique_ptr<int> up1(new int);qian::unique_ptr<int> up2(new int);up2 = up1; // errorreturn 0;
}

3.3 shared_ptr模拟实现

  • 增加count成员变量,表示引用计数;
  • 构造函数:当获取到资源则设置count=1,表示当前只有一个智能指针对象管理此资源;
  • 拷贝构造函数:将传入的智能指针对象中的count++,表示新增了一个管理者;
  • 拷贝赋值函数:将本智能指针的count--,表示解除对当前资源的引用,然后再将传入的智能
  • 指针对象中的count++,表示管理新的资源;
  • 析构函数:count--,表示解除对当前管理资源的引用,如果count=0则释放资源;
  • 重载*和->运算符,使shared_ptr对象具有指针一样的行为。

其中,operator = 的重载需要注意两个问题:

  • 内存泄漏:赋值时要把自己的引用计数给对方,赋值代表对方要共同接管自己管理的资源,所以对方的引用计数也要 - 1;
  • 自我赋值:本质也会造成内存泄漏,自我赋值后资源的管理权并未发生变化,但是引用计数却 + 1了,到真正最后一个对象时,引用计数仍不为0(如果自我赋值1次,那就是1),造成资源不能释放,内存泄漏。
namespace qian 
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){if (_ptr != nullptr){cout << "delete:" << _ptr << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;}}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(shared_ptr<T>& sp){if (_ptr != sp._ptr)       // 管理同一资源的智能指针赋值无意义{if (--(*_pcount) == 0) // 将管理的资源的引用计数-1{cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}_ptr = sp._ptr;       // 与传入的智能指针共享资源_pcount = sp._pcount; // 将自己的引用计数和传入的智能指针同步(*_pcount)++;         // 引用计数+1,表示自己是新增的管理者}return *this;}// 获取引用计数int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount; // 引用计数};
}int main(){qian::shared_ptr<int> sp1(new int(1));cout << sp1.use_count() << endl;qian::shared_ptr<int> sp2(sp1);cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;qian::shared_ptr<int> sp3(new int(0));cout << sp3.use_count() << endl;qian::shared_ptr<int> sp4(new int(2));sp3 = sp4;cout << sp3.use_count() << endl;cout << sp4.use_count() << endl;return 0;
}

3.4 weak_ptr模拟实现

  • 构造函数:无参构造
  • 拷贝构造:支持参数是shared_ptr类型和本身类型构造,同时接管shared_ptr管理的资源,但不增加引用计数
  • 拷贝赋值:同上。智能指针一般有get()接口,所以返回指针时可以调用
  • 像指针一样
namespace qian
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()) //sp的get接口在share_pr中已实现,如上{}weak_ptr& operator=(const shared_ptr<T>& sp){//_ptr = sp._shared_ptr;_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}


📖总结

以上就是智能指针的全部内容啦,后面我会单独出一篇关于自定义删除器的博客,敬请期待咯!!!

💞 💞 💞那么本篇到此就结束,希望我的这篇博客可以给你提供有益的参考和启示,感谢大家支持!!!祝大家天天开心。


http://www.hkcw.cn/article/uWgXdJKdZA.shtml

相关文章

【C++对于C语言的扩充】函数重载、引用以及内联函数

文章目录 &#x1f680;前言&#x1f680;函数重载注意&#xff1a;✈️为什么C可以实现函数重载&#xff0c;而C语言却不行呢&#xff1f; &#x1f680;引用✈️引用的特性✈️C中为什么要引入引用✈️引用与指针的区别 &#x1f680;内联函数✈️内联函数特性 &#x1f680;…

【C++指南】STL容器的安全革命:如何封装Vector杜绝越界访问与迭代器失效?

&#x1f31f; 各位看官好&#xff0c;我是egoist2023&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 使用STL的三个境界&#xff1a;能用&#xff0c;明理&#xff0c;能扩展 &#x1f44d; 如果觉得这篇文章有帮助&#…

C++ 异常处理机制与自定义异常体系

目录 1.C语言传统的处理错误的方式 &#x1f60a; 1. 终止程序 2. 返回错误码 3.实际使用中的情况 2. C异常概念&#x1f33c; 2.1 C异常的基本概念 2.2异常的抛出和匹配原则 2.3 异常的重新抛出 2.4 异常安全 2.5 异常规范 3. 自定义异常体系 &#x1f495;&#x…

C++入门看这一篇就够了——超详细讲解(120000多字详细讲解,涵盖C++大量知识)

目录 一、面向对象的思想 二、类的使用 1.类的构成 2.类的设计 三、对象的基本使用 四、类的构造函数 1.构造函数的作用 2.构造函数的特点 3.默认构造函数 3.1.合成的默认构造函数 3.2.手动定义的默认构造函数 四、自定义的重载构造函数 五、拷贝构造函数 1.手动…

【第53节】Windows编程必学之使用C++写exe压缩加密壳

目录 一、实现背景 1.1 前言 1.2 前置知识 1.3 达到目标 二、壳的实现要点 2.1 写壳怎么做 2.2 写壳的困难点 2.3 如何写壳代码 2.4 API函数的调用问题 2.5 重定位问题 2.6 信息交互问题 2.7 调试问题 2.8 关于目标程序的随机基址 2.9 关于目标程序的导入表 2.1…

C++离线查询

前言 C算法与数据结构 打开打包代码的方法兼述单元测试 概念及原理 离线算法( offline algorithms)&#xff0c;离线计算就是在计算开始前已知所有输入数据&#xff0c;输入数据不会产生变化&#xff0c;且在解决一个问题后就要立即得出结果的前提下进行的计算。 通俗的说&a…

金价又涨了!金饰克价涨至1018元,一夜涨14元

美东时间5月23日,国际贵金属期货普遍收涨,COMEX黄金期货涨1.90%,报3357.70美元/盎司,本周累计上涨4.75%。5月24日,国内金饰价格跟涨。周生生足金饰品标价1018元/克,较前一日1004元/克的价格上涨14元/克。责任编辑:zx0002

日本人准备开始吃饲料了?

日本农业水产大臣小泉进次郎十分骄傲地宣布政府将要拿出2021年所产陈米以每5公斤1800日元的价格进行售卖(合人民币差不多1斤大米9块钱)。当地专家吹捧此举将有效缓解日本米荒,并放话越是陈米吃着越香,这下日本人有口福了结果评论区直接翻车了,有网友直接贴出往年饲料米价格…

国际乒联发声明回应选举争议 谴责扰乱行为并重启会议

当地时间29日,国际乒联发布了关于2025年度代表大会期间选举事宜的声明。5月27日,在卡塔尔多哈举行的国际乒联年度股东大会上,因主席选举争议引发混乱,会议最终宣布临时暂停。声明中提到,主席选举结束后,一些既不是会员协会代表也不是执行委员会、理事会、委员会成员或受邀…

胖东来红内裤案宣判:“段某”赔偿40万元 名誉权获法院支持

2025年5月28日,许昌市魏都区人民法院公开审理了许昌市胖东来商贸集团有限公司与段某之间的名誉权纠纷案。法院判决段某在其个人抖音账号“两个小段(小)”发布书面道歉信的视频,并赔偿胖东来公司40万元经济损失。部分人大代表、政协委员、媒体记者、律师代表和企业代表旁听了…

市监总局就毕井泉被查表态 再度引发市场关注

六年多前,毕井泉因长春长生疫苗案从原国家食品药品监督管理总局局长位置引咎辞职的消息震惊了市场;六年多后,他被查的消息再次引发市场的强烈关注。据中央纪委国家监委网站5月29日消息,十四届全国政协常委、经济委员会副主任毕井泉涉嫌严重违纪违法,目前正接受中央纪委国家…

高芙评职业生涯最经典三胜 荣耀时刻回顾

近日,美国网球运动员高芙在法网接受记者采访时,回顾了自己职业生涯中的三场经典胜利。这三场比赛分别是2024年终总决赛争冠战对阵郑钦文、2019年温网第一轮对阵大威廉姆斯以及2023年美网决赛对阵萨巴伦卡。她还特别提到了此前罗马半决赛与郑钦文的那场长达三个半小时的大战,…

女子露营归来脖子惨遭“毁容” 提醒:夏季蚊虫活跃,如遇皮肤瘙痒红肿不能拖

近日,浙江30岁女子小妍露营归来后,颈部便出现刺痛和瘙痒,起初她并未在意。两天后,症状急剧加重——皮肤红肿成片,冒出红色丘疹和水疱,还伴随灼热疼痛。无独有偶,小学生骏骏在户外骑车后,小腿处的皮肤上也出现了个大包。两人来浙江省皮肤病医院就医后,均被确诊为“虫咬…

男子乘火车旅行刷新吉尼斯纪录:24小时内乘火车旅行5887.76公里

近日,吉尼斯世界纪录官网公布了一项纪录——中国男子王冬成功以24小时内5887.76公里的火车旅行距离,刷新了“24小时内乘坐火车旅行最远距离”的吉尼斯世界纪录。▲王冬刷新吉尼斯世界纪录今年39岁的王冬是四川德阳人,12年前在上海求学时的他,就曾因换乘8趟列车回家而走红网…

从外卖APP到网络协议:深入解析UDP及应用层协议

目录 1. 应用层和传输层1.1 开发中常见的自定义协议格式 2. UDP2.1 源端口号及目的端口号2.2 UDP报文长度2.3 UDP校验和(checksum) 3. 基于UDP的应用层协议 关注我&#xff0c;学习更多企业开发和面试内容~ 1. 应用层和传输层 应用层和程序员接触最密切&#xff0c;应用程序&a…

【JavaWeb】基本概念、web服务器、Tomcat、HTTP协议

目录 1. 基本概念1.1 基本概念1.2 web应用程序1.3 静态web1.4 动态web 2. web服务器3. tomcat详解3.1 安装3.2 启动3.3 配置3.3.1 配置启动的端口号3.3.2 配置主机的名称3.3.3 其他常用配置项日志配置数据源配置安全配置 3.4 发布一个网站 4. Http协议4.1 什么是http4.2 http的…

自扶正救生艇,乘风破浪,守护生命

在复杂水域救援中存在显著缺陷。遇巨浪或急流漩涡易倾覆且无法自主复位&#xff0c;使救援人员与被困者陷入二次危险。统计显示&#xff0c;激流救援中近三成五的救援人员伤亡源于船只倾覆后被困。更严重的是&#xff0c;传统救生艇倾覆后常需外部救援力量才能恢复&#xff0c;…

法国7月1日起实施最严户外禁烟令,范围包括这些

法国卫生与家庭部长卡特琳沃特兰表示,法国将在所有儿童可能出入的户外场所禁止吸烟。该禁令将于7月1日生效,范围包括海滩、公园、花园、学校外、公交车站和体育场馆等。责任编辑:zx0002

规定明令禁止自热类食品坐火车既不能带也不能托运

自热米饭发热包因高温爆炸风险被高铁禁止,单独食材包携带需密封完好且车上禁加热。各地规定不一,建议优先选择高铁餐食或非加热速食,出行前确认车站要求。一、发热包为何被禁?安全风险是主因自热米饭的发热包主要成分为镁铝粉、生石灰等,遇水后快速放热,温度可达100℃以上…

贵妇扎堆的“月子中心”IPO 圣贝拉月子中心28天收费13.88万起

核心提示:1.尽管“圣贝拉”定位高端月子服务,28天收费13.88万元起,但一度陷亏损泥潭,2022年经调整净亏损4463万元,高成本结构侵蚀利润,租赁与人力成本合计占销售成本近70%。2.这家“最贵”月子中心的合规风险密集暴露:位于北京的月子中心2021-2022年两次因无证行医被处罚…