C++ 智能指针 - 全部用法详解

article/2025/9/19 19:37:27

为什么要学习智能指针?
咳咳,这个问题不是问大家的,是询问我自己的!
我依稀记得刚离校出来找实习工作那会儿(2020年7月),去面试一份工作,面试官问了我许多问题,其中有一个问题就是问:什么是智能指针?
卧槽?当时我就懵逼了,智能指针是什么?我压根就没有听说过…
最后,面试的这份工作理所应当的“黄了”。
一年后,现在趁着有闲余时间,学习一下智能指针相关知识,丰富一下自己;也给有这方面需求的朋友提供鄙人的一些见解!


目录

  • 一、为什么要使用智能指针
  • 二、auto_ptr
  • 三、unique_ptr
    • auto_ptr 与 unique_ptr智能指针的内存管理陷阱
  • 四、shared_ptr
    • shared_ptr使用陷阱
  • 五、weak_ptr
    • expired函数的用法
  • 六、智能指针的使用陷阱
  • 七、总结


一、为什么要使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

如下例子就是内存泄露的例子:

#include <iostream>
#include <string>
#include <memory>using namespace std;// 动态分配内存,没有释放就return
void memoryLeak1() {string *str = new string("动态分配内存!");return;
}// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {string *str = new string("内存泄露!");// ...此处省略一万行代码// 发生某些异常,需要结束函数if (1) {return -1;}/// 另外,使用try、catch结束函数,也会造成内存泄漏!/delete str;	// 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放return 1;
}int main(void) {memoryLeak1();memoryLeak2();return 0;
} 

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!

  1. C++98 提供了 auto_ptr 模板的解决方案
  2. C++11 增加unique_ptr、shared_ptr 和weak_ptr

二、auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

用法:
头文件: #include < memory >
用 法: auto_ptr<类型> 变量名(new 类型)

例 如:
auto_ptr< string > str(new string(“我要成为大牛~ 变得很牛逼!”));
auto_ptr<vector< int >> av(new vector< int >());
auto_ptr< int > array(new int[10]);


我们先定义一个类,类的构造函数和析构函数都输出一个字符串用作提示!
定义一个私有成员变量,赋值20.
再定义一个私有成员方法用于返回这个私有成员变量。

class Test {
public:Test() { cout << "Test的构造函数..." << endl; }~Test() { cout << "Test的析构函数..." << endl; }int getDebug() { return this->debug; }private:int debug = 20;
};

当我们直接new这个类的对象,却没有释放时。。。

int main(void) {Test *test = new Test;return 0;
} 

在这里插入图片描述

可以看到,只是打印了构造函数这个字符串,而析构函数的字符却没有被打印,说明并没有调用析构函数!这就导致了内存泄露!
解决内存泄露的办法,要么手动delete,要么使用智能指针!

使用智能指针

// 定义智能指针
auto_ptr<Test> test(new Test);

智能指针可以像普通指针那样使用:

cout << "test->debug:" << test->getDebug() << endl;
cout << "(*test).debug:" << (*test).getDebug() << endl;

这时再试试:

int main(void) {//Test *test = new Test;auto_ptr<Test> test(new Test);cout << "test->debug:" << test->getDebug() << endl;cout << "(*test).debug:" << (*test).getDebug() << endl;return 0;
} 

在这里插入图片描述
自动调用了析构函数。
为什么智能指针可以像普通指针那样使用???
因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象。
在这里插入图片描述
具体原因不用深究,只需知道他为什么可以这样操作就像!
函数中返回的是调用get()方法返回的值,那么这个get()是什么呢?

智能指针的三个常用函数:

  1. get() 获取智能指针托管的指针地址

    // 定义智能指针
    auto_ptr<Test> test(new Test);Test *tmp = test.get();		// 获取指针返回
    cout << "tmp->debug:" << tmp->getDebug() << endl;
    

    但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
    函数原型

    _NODISCARD _Ty * get() const noexcept
    {	// return wrapped pointerreturn (_Myptr);
    }
    
  2. release() 取消智能指针对动态内存的托管

    // 定义智能指针
    auto_ptr<Test> test(new Test);Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
    delete tmp2;	// 之前分配的内存需要自己手动释放
    

    也就是智能指针不再对该指针进行管理,改由管理员进行管理!
    函数原型

    _Ty * release() noexcept
    {	// return wrapped pointer and give up ownership_Ty * _Tmp = _Myptr;_Myptr = nullptr;return (_Tmp);
    }
    
  3. reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

    // 定义智能指针
    auto_ptr<Test> test(new Test);test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULLtest.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之
    

    reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
    函数原型:

    void reset(_Ty * _Ptr = nullptr)
    {	// destroy designated object and store new pointerif (_Ptr != _Myptr)delete _Myptr;_Myptr = _Ptr;
    }
    

使用建议

  1. 尽可能不要将auto_ptr 变量定义为全局变量或指针;

    // 没有意义,全局变量也是一样
    auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);	
    
  2. 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个 智能指针;

    auto_ptr<Test> t1(new Test);
    auto_ptr<Test> t2(new Test);
    t1 = t2;	// 不要这样操作...
    
  3. C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。

  4. auto_ptr 被C++11抛弃的主要原因

    1). 复制或者赋值都会改变资源的所有权

    // auto_ptr 被C++11抛弃的主要原因
    auto_ptr<string> p1(new string("I'm Li Ming!"));
    auto_ptr<string> p2(new string("I'm age 22."));cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;// p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
    // 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
    p1 = p2;	
    cout << "p1 = p2 赋值后:" << endl;
    cout << "p1:" << p1.get() << endl;
    cout << "p2:" << p2.get() << endl;
    

    在这里插入图片描述

    2). 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值

    vector<auto_ptr<string>> vec;
    auto_ptr<string> p3(new string("I'm P3"));
    auto_ptr<string> p4(new string("I'm P4"));// 必须使用std::move修饰成右值,才可以进行插入容器中
    vec.push_back(std::move(p3));
    vec.push_back(std::move(p4));cout << "vec.at(0):" <<  *vec.at(0) << endl;
    cout << "vec[1]:" <<  *vec[1] << endl;// 风险来了:
    vec[0] = vec[1];	// 如果进行赋值,问题又回到了上面一个问题中。
    cout << "vec.at(0):" << *vec.at(0) << endl;
    cout << "vec[1]:" << *vec[1] << endl;
    

    访问越界了!
    在这里插入图片描述

    3). 不支持对象数组的内存管理

    auto_ptr<int[]> array(new int[5]);	// 不能这样定义
    

    在这里插入图片描述

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

测试代码

#include <iostream>
#include <string>
#include <memory>
#include <vector>using namespace std;class Test {
public:Test() { cout << "Test的构造函数..." << endl; }~Test() { cout << "Test的析构函数..." << endl; }int getDebug() { return this->debug; }private:int debug = 20;
};// 不要定义为全局变量,没有意义
//auto_ptr<Test> test(new Test);void memoryLeak1() {//Test *test = new Test;// 定义智能指针auto_ptr<Test> test(new Test);cout << "test->debug:" << test->getDebug() << endl;cout << "(*test).debug:" << (*test).getDebug() << endl;// get方法Test *tmp = test.get();		// 获取指针返回cout << "tmp->debug:" << tmp->getDebug() << endl;// release方法Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管delete tmp2;	// 之前分配的内存需要自己手动释放// reset方法:重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULLtest.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之// 忠告:不要将智能指针定义为指针//auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);// 忠告:不要定义指向智能指针对象的指针变量//auto_ptr<Test> t1(new Test);//auto_ptr<Test> t2(new Test);//t1 = t2;return;
}int memoryLeak2() {//Test *test = new Test();// 定义智能指针auto_ptr<Test> test(new Test);// ...此处省略一万行代码// 发生某些异常,需要结束函数if (1) {return -1;}//delete test;return 1;
}int main1(void) {//memoryLeak1();//memoryLeak2();//Test *test = new Test;//auto_ptr<Test> test(new Test);//cout << "test->debug:" << test->getDebug() << endl;//cout << "(*test).debug:" << (*test).getDebug() << endl; auto_ptr 被C++11抛弃的主要原因//auto_ptr<string> p1(new string("I'm Li Ming!"));//auto_ptr<string> p2(new string("I'm age 22."));////cout << "p1:" << p1.get() << endl;//cout << "p2:" << p2.get() << endl;//p1 = p2;//cout << "p1 = p2 赋值后:" << endl;//cout << "p1:" << p1.get() << endl;//cout << "p2:" << p2.get() << endl;// 弊端2.在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制vector<auto_ptr<string>> vec;auto_ptr<string> p3(new string("I'm P3"));auto_ptr<string> p4(new string("I'm P4"));vec.push_back(std::move(p3));vec.push_back(std::move(p4));cout << "vec.at(0):" <<  *vec.at(0) << endl;cout << "vec[1]:" <<  *vec[1] << endl;// 风险来了:vec[0] = vec[1];cout << "vec.at(0):" << *vec.at(0) << endl;cout << "vec[1]:" << *vec[1] << endl;// 弊端3.不支持对象数组的内存管理//auto_ptr<int[]> array(new int[5]);	// 不能这样定义return 0;
} 

三、unique_ptr

auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有三大问题:

  1. 复制和赋值会改变资源的所有权,不符合人的直觉。
  2. 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
  3. 不支持对象数组的操作

以上问题已经在上面体现出来了,下面将使用unique_ptr解决这些问题。

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。

unique_ptr特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的

A. 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

unique_ptr<string> p1(new string("I'm Li Ming!"));
unique_ptr<string> p2(new string("I'm age 22."));cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;p1 = p2;					// 禁止左值赋值
unique_ptr<string> p3(p2);	// 禁止左值赋值构造unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2);	// 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

在这里插入图片描述

运行截图:
在这里插入图片描述

B. 在 STL 容器中使用unique_ptr,不允许直接赋值

vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));vec.push_back(std::move(p3));
vec.push_back(std::move(p4));cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;vec[0] = vec[1];	/* 不允许直接赋值 */
vec[0] = std::move(vec[1]);		// 需要使用move修饰,使得程序员知道后果cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

在这里插入图片描述

当然,运行后是直接报错的,因为vec[1]已经是NULL了,再继续访问就越界了。

C. 支持对象数组的内存管理

// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]);	// 支持这样定义

除了上面ABC三项外,unique_ptr的其余用法都与auto_ptr用法一致。

  1. 构造

    class Test {
    public:Test() { cout << "Test的构造函数..." << endl; }~Test() { cout << "Test的析构函数..." << endl; }void doSomething() { cout << "do something......" << endl; }
    };// 自定义一个内存释放其
    class DestructTest {public:void operator()(Test *pt) {pt->doSomething();delete pt;}
    };// unique_ptr<T> up; 空的unique_ptr,可以指向类型为T的对象
    unique_ptr<Test> t1;// unique_ptr<T> up1(new T());	定义unique_ptr,同时指向类型为T的对象
    unique_ptr<Test> t2(new Test);// unique_ptr<T[]> up;	空的unique_ptr,可以指向类型为T[的数组对象
    unique_ptr<int[]> t3;// unique_ptr<T[]> up1(new T[]);	定义unique_ptr,同时指向类型为T的数组对象
    unique_ptr<int[]> t4(new int[5]);// unique_ptr<T, D> up();	空的unique_ptr,接受一个D类型的删除器D,使用D释放内存
    unique_ptr<Test, DestructTest> t5;// unique_ptr<T, D> up(new T());	定义unique_ptr,同时指向类型为T的对象,接受一个D类型的删除器D,使用删除器D来释放内存
    unique_ptr<Test, DestructTest> t6(new Test);
    
  2. 赋值

    unique_ptr<Test> t7(new Test);
    unique_ptr<Test> t8(new Test);
    t7 = std::move(t8);	// 必须使用移动语义,结果,t7的内存释放,t8的内存交给t7管理
    t7->doSomething();
    
  3. 主动释放对象

    unique_ptr<Test> t9(new Test);
    t9 = NULL;
    t9 = nullptr;
    t9.reset();
    
  4. 放弃对象的控制权

    Test *t10 = t9.release();
    
  5. 重置

    t9.reset(new Test);
    

auto_ptr 与 unique_ptr智能指针的内存管理陷阱

auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str);	// p1托管str指针
{auto_ptr<string> p2;p2.reset(str);	// p2接管str指针时,会先取消p1的托管,然后再对str的托管
}// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;

在这里插入图片描述
这是由于auto_ptr 与 unique_ptr的排他性所导致的!
为了解决这样的问题,我们可以使用shared_ptr指针指针!


四、shared_ptr

熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?

如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!

在这里插入图片描述

class Person {
public:Person(int v) {this->no = v;cout << "构造函数 \t no = " << this->no << endl;}~Person() {cout << "析构函数 \t no = " << this->no << endl;}private:int no;
};// 仿函数,内存删除
class DestructPerson {
public:void operator() (Person *pt) {cout << "DestructPerson..." << endl;delete pt;}
};
  1. 引用计数的使用

    调用use_count函数可以获得当前托管指针的引用计数。

    shared_ptr<Person> sp1;shared_ptr<Person> sp2(new Person(2));// 获取智能指针管控的共享指针的数量	use_count():引用计数
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;// 共享
    sp1 = sp2;cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;shared_ptr<Person> sp3(sp1);
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl;
    cout << "sp2	use_count() = " << sp3.use_count() << endl << endl;
    

    如上代码,sp1 = sp2; 和 shared_ptr< Person > sp3(sp1);就是在使用引用计数了。

    sp1 = sp2; --> sp1和sp2共同托管同一个指针,所以他们的引用计数为2;
    shared_ptr< Person > sp3(sp1); --> sp1和sp2和sp3共同托管同一个指针,所以他们的引用计数为3;
    在这里插入图片描述

  2. 构造

    1). shared_ptr< T > sp1; 空的shared_ptr,可以指向类型为T的对象

    shared_ptr<Person> sp1;
    Person *person1 = new Person(1);
    sp1.reset(person1);	// 托管person1
    

    2). shared_ptr< T > sp2(new T()); 定义shared_ptr,同时指向类型为T的对象

    shared_ptr<Person> sp2(new Person(2));
    shared_ptr<Person> sp3(sp1);
    

    3). shared_ptr<T[]> sp4; 空的shared_ptr,可以指向类型为T[]的数组对象 C++17后支持

    shared_ptr<Person[]> sp4;
    

    4). shared_ptr<T[]> sp5(new T[] { … }); 指向类型为T的数组对象 C++17后支持

    shared_ptr<Person[]> sp5(new Person[5] { 3, 4, 5, 6, 7 });
    

    5). shared_ptr< T > sp6(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D释放内存

    shared_ptr<Person> sp6(NULL, DestructPerson());
    

    6). shared_ptr< T > sp7(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存

    shared_ptr<Person> sp7(new Person(8), DestructPerson());
    
  3. 初始化

    1). 方式一:构造函数

    shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(up1);  // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
    

    2). 方式二:使用make_shared 初始化对象,分配内存效率更高(推荐使用)
    make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:
    make_shared<类型>(构造类型对象需要的参数列表);

    shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
    shared_ptr<string> up4 = make_shared<string>("字符串");
    shared_ptr<Person> up5 = make_shared<Person>(9);
    
  4. 赋值

    shared_ptrr<int> up1(new int(10));  // int(10) 的引用计数为1
    shared_ptr<int> up2(new int(11));   // int(11) 的引用计数为1
    up1 = up2;	// int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
    
  5. 主动释放对象

    shared_ptrr<int> up1(new int(10));
    up1 = nullptr ;	// int(10) 的引用计数减1,计数归零内存释放 
    // 或
    up1 = NULL; // 作用同上 
    
  6. 重置
    p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
    p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
    p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
    p1是一个指针!

  7. 交换
    p1 和 p2 是智能指针

    std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
    p1.swap(p2);    // 交换p1 和p2 管理的对象,原对象的引用计数不变
    

shared_ptr使用陷阱

shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

如下代码:
Boy类中有Girl的智能指针;
Girl类中有Boy的智能指针;
当他们交叉互相持有对方的管理对象时…

#include <iostream>
#include <string>
#include <memory>using namespace std;class Girl;class Boy {
public:Boy() {cout << "Boy 构造函数" << endl;}~Boy() {cout << "~Boy 析构函数" << endl;}void setGirlFriend(shared_ptr<Girl> _girlFriend) {this->girlFriend = _girlFriend;}private:shared_ptr<Girl> girlFriend;
};class Girl {
public:Girl() {cout << "Girl 构造函数" << endl;}~Girl() {cout << "~Girl 析构函数" << endl;}void setBoyFriend(shared_ptr<Boy> _boyFriend) {this->boyFriend = _boyFriend;}private:shared_ptr<Boy> boyFriend;
};void useTrap() {shared_ptr<Boy> spBoy(new Boy());shared_ptr<Girl> spGirl(new Girl());// 陷阱用法spBoy->setGirlFriend(spGirl);spGirl->setBoyFriend(spBoy);// 此时boy和girl的引用计数都是2
}int main(void) {useTrap();system("pause");return 0;
}

运行截图:

在这里插入图片描述

可以看出,程序结束了,但是并没有释放内存,这是为什么呢???

如下图:
当我们执行useTrap函数时,注意,是没有结束此函数,boy和girl指针其实是被两个智能指针托管的,所以他们的引用计数是2
在这里插入图片描述

useTrap函数结束后,函数中定义的智能指针被清掉,boy和girl指针的引用计数减1,还剩下1,对象中的智能指针还是托管他们的,所以函数结束后没有将boy和gilr指针释放的原因就是于此。
在这里插入图片描述

所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!

当然,这也是有办法解决的,那就是使用weak_ptr弱指针。

针对上面的情况,还讲一下另一种情况。如果是单方获得管理对方的共享指针,那么这样着是可以正常释放掉的!
例如:

void useTrap() {shared_ptr<Boy> spBoy(new Boy());shared_ptr<Girl> spGirl(new Girl());// 单方获得管理//spBoy->setGirlFriend(spGirl);spGirl->setBoyFriend(spBoy);	
}

在这里插入图片描述
反过来也是一样的!

这是什么原理呢?

  1. 首先释放spBoy,但是因为girl对象里面的智能指针还托管着boy,boy的引用计数为2,所以释放spBoy时,引用计数减1,boy的引用计数为1;
  2. 在释放spGirl,girl的引用计数减1,为零,开始释放girl的内存,因为girl里面还包含有托管boy的智能指针对象,所以也会进行boyFriend的内存释放,boy的引用计数减1,为零,接着开始释放boy的内存。最终所有的内存都释放了。

五、weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

  1. 弱指针的使用;
    weak_ptr wpGirl_1; // 定义空的弱指针
    weak_ptr wpGirl_2(spGirl); // 使用共享指针构造
    wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针

  2. 弱指针也可以获得引用计数;
    wpGirl_1.use_count()

  3. 弱指针不支持 * 和 -> 对指针的访问;
    在这里插入图片描述

  4. 在必要的使用可以转换成共享指针 lock();

    shared_ptr<Girl> sp_girl;
    sp_girl = wpGirl_1.lock();// 使用完之后,再将共享指针置NULL即可
    sp_girl = NULL;
    

使用代码:

shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());// 弱指针的使用
weak_ptr<Girl> wpGirl_1;			// 定义空的弱指针
weak_ptr<Girl> wpGirl_2(spGirl);	// 使用共享指针构造
wpGirl_1 = spGirl;					// 允许共享指针赋值给弱指针cout << "spGirl \t use_count = " << spGirl.use_count() << endl;
cout << "wpGirl_1 \t use_count = " << wpGirl_1.use_count() << endl;// 弱指针不支持 * 和 -> 对指针的访问
/*wpGirl_1->setBoyFriend(spBoy);
(*wpGirl_1).setBoyFriend(spBoy);*/// 在必要的使用可以转换成共享指针
shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;

当然这只是一些使用上的小例子,具体用法如下:

请看Boy类

#include <iostream>
#include <string>
#include <memory>using namespace std;class Girl;class Boy {
public:Boy() {cout << "Boy 构造函数" << endl;}~Boy() {cout << "~Boy 析构函数" << endl;}void setGirlFriend(shared_ptr<Girl> _girlFriend) {this->girlFriend = _girlFriend;// 在必要的使用可以转换成共享指针shared_ptr<Girl> sp_girl;sp_girl = this->girlFriend.lock();cout << sp_girl.use_count() << endl;// 使用完之后,再将共享指针置NULL即可sp_girl = NULL;}private:weak_ptr<Girl> girlFriend;
};class Girl {
public:Girl() {cout << "Girl 构造函数" << endl;}~Girl() {cout << "~Girl 析构函数" << endl;}void setBoyFriend(shared_ptr<Boy> _boyFriend) {this->boyFriend = _boyFriend;}private:shared_ptr<Boy> boyFriend;
};void useTrap() {shared_ptr<Boy> spBoy(new Boy());shared_ptr<Girl> spGirl(new Girl());spBoy->setGirlFriend(spGirl);spGirl->setBoyFriend(spBoy);
}int main(void) {useTrap();system("pause");return 0;
}

在这里插入图片描述

在类中使用弱指针接管共享指针,在需要使用时就转换成共享指针去使用即可!

自此问题完美解决!

expired函数的用法

应评论区某位朋友的要求,现在加上weak_ptr指针的expired函数的用法!

expired:判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true

如果返回true,等价于 use_count() == 0,即已经没有托管的对象了;当然,可能还有析构函数进行释放内存,但此对象的析构已经临近(或可能已发生)。

示例
演示如何用 expired 检查指针的合法性。
在网上找了一段代码,加上自己的注释理解

#include <iostream>
#include <memory>std::weak_ptr<int> gw;void f() {// expired:判断当前智能指针是否还有托管的对象,有则返回false,无则返回trueif (!gw.expired()) {std::cout << "gw is valid\n";	// 有效的,还有托管的指针} else {std::cout << "gw is expired\n";	// 过期的,没有托管的指针}
}int main() {{auto sp = std::make_shared<int>(42);gw = sp;f();}// 当{ }体中的指针生命周期结束后,再来判断其是否还有托管的指针f();return 0;
}

在这里插入图片描述

在 { } 中,sp的生命周期还在,gw还在托管着make_shared赋值的指针(sp),所以调用f()函数时打印"gw is valid\n";
当执行完 { } 后,sp的生命周期已经结束,已经调用析构函数释放make_shared指针内存(sp),gw已经没有在托管任何指针了,调用expired()函数返回true,所以打印"gw is expired\n";


六、智能指针的使用陷阱

  1. 不要把一个原生指针给多个智能指针管理;

    int *x = new int(10);
    unique_ptr< int > up1(x);
    unique_ptr< int > up2(x);
    // 警告! 以上代码使up1 up2指向同一个内存,非常危险
    或以下形式:
    up1.reset(x);
    up2.reset(x);

  2. 记得使用u.release()的返回值;
    在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.

  3. 禁止delete 智能指针get 函数返回的指针;
    如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

  4. 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
    shared_ptr< int > sp1(new int(10));
    // 一个典型的错误用法 shared_ptr< int > sp4(sp1.get());


七、总结

智能指针虽然使用起来很方便,但是要注意使用智能指针的一些陷阱,否则会造成严重的内存报错或者内存泄露等问题!


http://chatgpt.dhexx.cn/article/LNNkxHSm.shtml

相关文章

AndroidStudio编译时 CXX1405 错误解决

AndroidStudio 编译带C的native库项目时报错&#xff0c; [CXX1405] error when building with cmake using D:\WorkSpace\AndroidXXXX\app\src\main\cpp\CMakeLists.txt: Build command failed.解决方法&#xff1a; cmd 到 Android SDK的cmake路径下&#xff0c;执行 cmake …

硬件速攻-AT24CXX存储器

AT24C02是什么&#xff1f; AT24CXX是存储芯片&#xff0c;驱动方式为IIC协议 实物图&#xff1f; 引脚介绍&#xff1f; A0 地址设置角 可连接高电平或低电平 A1 地址设置角 可连接高电平或低电平 A2 地址设置角 可连接高电平或低电平 1010是设备前四位固定地址 &#xf…

【dbus-cxx】libsigc++ 和 dbus-cxx 在 Ubuntu 中的编译和配置

文章目录 参考资料配置环境cmakelibsigcdbus-cxx 例程server.cppclient.cppMakefile运行结果 此帖作为自己学习记录使用&#xff0c;尚未使用交叉编译&#xff0c;仅在本地使用 参考资料 需先了解DBUS基础知识&#xff0c;建议看DBUS官方文档 DBUS-CXX也是建议看官方文档&…

正点原子IIC例程讲解笔记(三)——24cxx.c中函数理解

目录 一、24C02 简介 二、在 AT24CXX 指定地址写入一个数据&#xff1a; 三、在ATC24XX指定地址读出一个数据 四、检查AT24CXX是否正常&#xff1a;u8 AT24CXX_Check(void) 五、在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据 六、在 AT24CXX 里面的指定地址开始读出…

windows用VS2019下编译log4cxx日志库

一、下载相关库文件 获取log4cxx源码包&#xff1a;http://logging.apache.org/log4cxx/index.html 获取依赖库apr和apr-util源码包:http://archive.apache.org/dist/apr/apr-1.2.11-win32-src.zip http://archive.apache.org/dist/apr/apr-util-1.2.10-win32-src.zip 编译apr…

log4cxx编译

本人进行过win7 64位操作系统和win10家庭版的log4cxx编译&#xff0c;使用的是vs2015&#xff0c;下面是详情。 1.sed下载 sed-4.2.1-bin.zip、sed-4.2.1-dep.zip下载地址&#xff1a;http://gnuwin32.sourceforge.net/packages/sed.htm 下载后&#xff0c;将sed的两个压缩包解…

【RT-Thread Master】at24cxx软件包使用笔记

硬件介绍 RT-Thread版本&#xff1a;V4.1.0软件包名称&#xff1a;at24cxxMCU型号&#xff1a;AT32F407VET7EEPROM型号&#xff1a;AT24C16 使用说明 1、使用menuconfig将软件包添加进入工程&#xff0c;路径如下所示。 2、把IIC总线打开&#xff0c;这里使用软件IIC&#…

linux下编译和安装log4cxx,ubuntu下log4cxx安装使用

需要安装log4cxx&#xff0c;安装的过程中可是充满了坎坷。。。最大的问题是在make log4cxx时&#xff0c;总是报undefined XML什么什么的错误&#xff0c;查了一下也没解决了&#xff0c;然后把apr-utils删了重新装了一下就好了。。 log4cxx现在是apache的一个项目&#xff0c…

linux下编译和安装log4cxx,RedHat如何安装log4cxx日志库

log4cxx日志库是一种动态库&#xff0c;用于记录c的日志&#xff0c;那么RedHat系统下要如何安装log4cxx日志库呢&#xff1f;下面小编就给大家介绍下RedHat安装log4cxx日志库的步骤&#xff0c;感兴趣的朋友不妨来了解下吧。 首先&#xff0c;我得到信息&#xff0c;安装这个库…

AT24Cxx读写全面理解

AT24Cxx - 电可擦可写E2PROM 芯片介绍 基础介绍\引脚介绍 AT24Cxx系列EEPROM是由美国Mcrochip公司出品&#xff0c;1-512K位的支持I2C总线数据传送协议的串行CMOS E2PROM&#xff0c;可用电擦除&#xff0c;可编程自定时写周期&#xff08;包括自动擦除时间不超过10ms&#…

mongodb-cxx-driver使用

mongocxx driver 是构建在 MongoDB C driver 之上的 1.首先需要安装mongo-c-driver wget https://github.com/mongodb/mongo-c-driver/releases/download/ 1.23.1/mongo-c-driver-1.23.1.tar.gz tar xzf mongo-c-driver-1.23.1.tar.gz cd mongo-c-driver-1.23.1 mkdir cmak…

老胡的周刊(第095期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 tabby[2] 自托管的 AI 编码助手&#xff0c;…

程序员养生指北

吴小胖第八次推送 阅读时间预计3分钟~ 熬夜篇 互联网人熬夜是不能避免的&#xff0c;原因却各不相同。 不加班的时候&#xff0c;总会对自己说&#xff0c;今天一定早睡&#xff0c;然鹅... 午休篇 熬夜的程序员总想依靠午休补觉&#xff0c;然鹅... 更不幸的是&#xff0c;互联…

老杨说运维 | 中国IT运维市场的现状与趋势

文章内容来源《第一新声》 对擎创科技CEO杨辰(老杨)的专访 前言&#xff1a; 中国目前正面临百年未有之大变局&#xff0c;在这个变局中&#xff0c;不稳定性和不确定性正在增强。疫情持续反复、国际形势变化多端&#xff0c;导致国内多个行业出现发展增速下降、产供销节奏打…

老杨说运维 | 非常重要,事关转型

《荀子》有云&#xff1a;“水能载舟&#xff0c;亦能覆舟。”在公司日常运营过程中&#xff0c;数据指标就像是水&#xff0c;孕育着生命&#xff0c;承载着万物。科学的数据指标能指引公司在正确的道路上不断前进&#xff0c;使平淡无常的业务焕发新生&#xff0c;而不合理的…

学习springcloud的一些心得体会——老卫的天气预报系统

1&#xff1a;建立天气预报springboot系统 首先先建立一个天气预报的springboot系统&#xff0c;具体流程如下&#xff1a; &#xff08;1&#xff09;从cityList.xml中获取城市信息&#xff0c; &#xff08;2&#xff09;然后根据下面的链接获取各个城市的天气预报信息&am…

学习springboot项目的一些心得-----老卫的博客系统

去年年底接触了springboot框架&#xff0c;这两天复习了一遍&#xff0c;主要是跟着老卫博客系统这个课程学习的。 springboot介绍&#xff1a;总的感觉springboot就是基于spring开发的一套框架&#xff0c;好处就是不用配置复杂的依赖包&#xff0c;xml的一些文件&#xff0c…

thymeleaf和spring boot的集成踩坑总结(练习项目是老卫的博客项目)

bug1: 2019-08-08 16:44:38.102 ERROR 19810 — [nio-8080-exec-8] org.thymeleaf.TemplateEngine : [THYMELEAF][http-nio-8080-exec-8] Exception processing template “users/list”: Error resolving template “users/list”, template might not exist or might not be …

《跟老卫学 HarmonyOS 开发》:以父之名・码力全开!写段 HarmonyOS 祝父亲节

#父亲节祝福语# 爸爸在我心中就像旗帜 他教会我做人与处事的方向 在父亲节这个特别的日子里 我想对爸爸说长大以后我就要成为您 使用ArkUI开发“父亲节的祝福” 使用ArkUI开发“父亲节的祝福”&#xff0c;效果如下&#xff1a; 使用DevEco Studio整体开发HarmonyOS整体时…

【老卫搞机】090期:键盘?主机?全功能键盘主机!

这是一台我个人DIY的第二代键盘主机&#xff08;第一代见&#xff1a;【老卫搞机】074期&#xff1a;键盘&#xff1f;主机&#xff1f;键盘主机&#xff01;_哔哩哔哩_bilibili&#xff09;。相比与第一代的作品&#xff0c;第二代产品除了USB 3接口、USB 2接口、mini HDMI接口…