移动语义和智能指针

2024-09-04 15:28
文章标签 指针 智能 移动 语义

本文主要是介绍移动语义和智能指针,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第十章 移动语义与智能指针

移动语义

为什么要用移动语义?

我们回顾一下之前模拟的String.cc

class String
{
public:String(): _str(new char[1]()) {}String(const char* pstr):_str(new char[strlen(pstr) + 1]()) {strcpy(_str, pstr);}String(const String& rhs):_str(new char[strlen(rhs.c_str()) + 1]()){strcpy(_str, rhs.c_str());}String &operator=(const String &rhs){if (this != &rhs){delete [] _str;_str = new char[strlen(rhs.c_str()) + 1];strcpy(_str, rhs.c_str());}return *this;}~String(){if (_str){delete [] _str;_str = nullptr;}}private:char* _str;
};void test0(){// 构造函数String s1("hello");// 拷贝构造String s2 = s1;// 先构造,再拷贝构造// 利用"hello"这个字符串创建了一个临时对象,并复制给了s3// 这一步实际上new了两次String s3 = "hello";
}

创建 s3 的过程中实际创建了一个临时对象,也会在堆空间上申请一片空间,然后把字符串内容复制给 s3 的pstr,这一行结束时临时对象的生命周期结束,它申请的那片空间被回收。这片空间申请了,又马上被回收,实际上可以视作一种不必要的开销。我们希望能够少new一次,可以直接将 s3 能够复用临时对象申请的空间。

左值与右值

左值和右值是针对表达式而言的,左值是指表达式执行结束后依然存在的持久对象右值是指表达式执行结束后就不再存在的临时对象

那如何进行区分呢?其实也简单,能对表达式取地址的,称为左值;不能取地址的,称为右值

在实际使用过程中,字面值常量匿名对象(临时对象)匿名变量(临时变量),都称为右值。右值又被称为即将被销毁的对象。

字面值常量:也就是10, 20这样的数字,属于右值,不能取地址。

字符串常量:“world",是属于左值的,位于内存中的文字常量区

试试看下面这些取址操作和引用绑定操作是否可行:

void test1() {int a = 1, b = 2;&a;&b;// &(a + b); // error 临时变量(匿名变量) 右值// &10; // 右值&String("hello"); // 匿名对象String("hello").print(); // ok 声明周期在当前行String str("hello"); // 有名对象&str; // ok// 非const引用尝试绑定int & r1 = a;int & r2 = 1; // error 非const引用不能绑定右值// const引用尝试绑定// const引用可以绑定右值const int & r3 = 1;const int & r4 = a;String s1("hello");String s2("wangdao");&s1;&s2;&(s1 + s2); // 右值
}

如上定义的int & r1 const int & r3 叫作左值引用const左值引用

非const左值引用只能绑定到**左值**,不能绑定到右值,也就是非const左值引用只能识别出左值。

const左值引用既可以绑定到左值,也可以绑定到**右值**,也就是表明const左值引用不能区分是左值还是右值。

——希望能够区分出右值,并且还要进行绑定

就是为了实现String s3 = "hello"的空间复用需求。

右值引用 &&

C++11提出了新特性右值引用

右值引用不能绑定到左值,但是可以绑定到右值,也就是右值引用可以识别出右值

// 非const引用:不能绑定右值
int & r1 = a;
int & r2 = 1; // error// const引用:既可以绑定左值,又可以绑定右值
const int & r3 = 1;
const int & r4 = a;// 右值引用:只能绑定右值
int && r_ref = 10;
int && r_ref2 = a; // error

右值引用本身是左值还是右值?

—— 对r_ref取地址是可行的,r_ref本身是一个左值。但这并不代表右值引用本身一定是左值

实际上,右值引用既可以是左值(比如:作为函数的参数、有名字的变量),也可以是右值(函数的返回类型)

这个问题,我们留到1.1.6章节再做讨论。

移动构造函数(重要) T(T && rhs)

有了右值引用后,实际上再接收临时对象作为参数时就可以分辨出来。

之前 String str1 = String("hello"); 这种操作调用的是拷贝构造函数,形参为 const String & 类型,既能绑定右值又能绑定左值。为了确保进行左值的复制时不出错,一律采用重新开辟空间的方式。

有了能够分辨出右值的右值引用之后,我们就可以定义一个新的构造函数了 —— 移动构造函数 String(String && rhs)

image-20240322100627895

给String类加上移动构造函数,在初始化列表中完成:

  1. 浅拷贝,使s3的pstr指向临时对象的pstr所指向的空间(复用)
  2. 将临时对象(右操作数)的pstr设为空指针,因为这个临时对象会马上销毁(要避免临时对象调用析构函数回收掉这片堆空间)
String(String && rhs) // 传进来临时对象作为参数
: _pstr(rhs._pstr)	  // 浅拷贝
{cout << "String(String&&)" << endl;rhs._pstr = nullptr; // 要避免临时对象调用析构函数回收掉这片堆空间
}

再运行代码,加上编译器的去优化参数 -fno-elide-constructors,发现:

String s3 = "hello";没有再调用拷贝构造函数,而是调用了移动构造函数

String str1 = String("hello");这种操作调用的是拷贝构造函数

移动构造函数的特点:

  1. 移动构造函数优于拷贝构造函数执行(实际上绑定左值也会经历这个过程,但是移动构造函数中的右值引用不能绑定左值,所以采用了拷贝构造函数)。

  2. 移动构造函数如果不显式写出,编译器不会自动生成。(错误)

移动赋值函数(重要) T & operator=(T && rhs)

有了移动构造函数的成功经验,很容易想到原本的赋值运算符函数。

比如,我们进行如下操作时:

String s3("hello");
s3 = String("wangdao");

原本赋值运算符函数的做法:

image-20231107100059563

我们希望复用临时对象申请的空间,那么也同样需要赋值运算符函数能够分辨出接收的参数是左值还是右值,同样可以利用右值引用。

image-20231107100306135

再写出移动赋值函数(移动赋值运算符函数),优先级也是高于赋值运算符函数。

注意:自复制判断自复制判断自复制判断自复制判断自复制判断自复制判断

String & operator=(String && rhs){if(this != &rhs){ // 自复制判断delete [] _pstr; //为什么要做自复制判断?因为需要先delete掉s3对象的堆空间,如果是自复制,下面在进行赋值就会报错// 浅拷贝_pstr = rhs._pstr;rhs._pstr = nullptr;cout << "String& operator=(String&&)" << endl;}return *this;
}

总结:

将**拷贝构造函数赋值运算符函数称为具有复制控制语义的函数**

将**移动构造函数移动赋值函数称为具有移动语义的函数**

具有移动语义的函数优于具有复制控制语义的函数先执行;

具有移动语义的函数如果不显式写出,编译器不会自动生成,必须手写。(错误)

C++11中新增的移动构造函数和移动赋值函数的生成条件如下:

  • 移动构造函数的生成条件:没有自己实现移动构造函数,并且没有自己实现**析构函数**、拷贝构造函数和**拷贝赋值函数**。
  • 移动赋值重载函数的生成条件:没有自己实现移动赋值重载函数,并且没有自己实现**析构函数**、拷贝构造函数和**拷贝赋值函数**。

也就是说,移动构造和移动赋值的生成条件与之前六个默认成员函数不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成。

思考:移动赋值函数中的自复制判断是否还有必要?

String s1("hello");
// 右值复制给左值,肯定不是同一个对象
s1 = String("world");
// 创建了两个内容相同的临时对象,也不是同一对象
String("wangdao") = String("wangdao");

似乎去掉自复制判断不会造成问题,但是c++11提出了一种方式,将左值转为右值,就是std::move()函数

补充:

在C++11之前,一个类中有如下六个默认成员函数:

  • 构造函数。
  • 析构函数。
  • 拷贝构造函数。
  • 拷贝赋值函数。
  • 取地址重载函数。
  • const取地址重载函数。

std::move()函数

在一些使用移动语义的场景下,有时需要将左值转为右值。std::move() 函数的作用是**显式的将一个左值转换为右值其实现本质上就是一个强制转换。当将一个左值显式转换为右值后,原来的左值对象就无法正常工作了,必须要重新赋值**才可以继续使用。

void test() {int a = 1;&(std::move(a)); // error,左值转成了右值String s1("hello");cout << "s1:" << s1 << endl;String s2 = std::move(s1);cout << "s1:" << s1 << endl;cout << "s2:" << s2 << endl;
}
  • 验证:如果将移动赋值函数的自复制判断去除,如下情况依然会调用移动赋值函数,但是s1的pstr所指向的空间被回收,且被设为了空指针,会出错。
String str1("hello");
s1 = std::move(s1);
s1.print();
  • 验证:将移动赋值函数中的浅拷贝去除,让左操作数s1 的 _pstr重新指向一片空间,后面对右操作数的 _pstr 设为空指针,但是依然造成了程序的中断,所以说明对 std::move(s1) 的内容进行修改,会导致s1的内容也被修改。

std::move() 的本质是在底层做了强制转换(并不是像名字表面的意思一样做了移动)

String & operator=(String && rhs){delete [] _pstr;_pstr = new char[1]();rhs._pstr = nullptr;cout << "String& operator=(String&&)" << endl;return *this;
}

—— 所以**移动赋值函数的自复制判断不应该省略**。

void test4(){String s1("hello"); 	// 两次构造函数s1 = String("world"); 	// 一次构造函数 一次移动赋值函数s1.print(); // worlds1 = std::move(s1); 	// 一次移动赋值函数s1.print(); // world
}

右值引用本身的性质

我们来定义一个返回值是右值引用的函数:

int && func(){return 10;
}void test1(){// &func();  // 无法取址,说明返回的右值引用本身也是一个右值int && ref = func();&ref;  // 可以取址,此时ref是一个右值引用,其本身是左值
}
// 探讨右值引用
String func2(){String str1("wangdao");str1.print();return str1;
}void test5(){// func2();// &func2(); // error,编译时出错,函数的返回值是右值String && ref = func2(); // ok&ref;  // 右值引用本身为左值
}

右值引用本身是左值还是右值,取决于是否有名字,有名字就是左值,没名字就是右值。

String func2(){String str1("wangdao");str1.print();return str1;
}void test2(){func2(); // 调用移动构造函数// &func2(); // error,右值String && ref = func2();&ref;  // 右值引用本身为左值
}

拷贝构造函数 和 移动构造函数 的调用时机

这里func2的调用按以前的理解会调用拷贝构造函数,但是发现结果是调用了移动构造函数。

调用时机:

  • 返回的对象其生命周期即将结束,就不再调用拷贝构造函数,而是调用移动构造函数。

  • 如果返回的对象声明周期大于func3()函数,执行return语句时还是调用拷贝构造函数

String s10("beijing");String func3(){s10.print();return s10;
}void test3(){func3(); // 调用拷贝构造函数
}

总结:当类中同时定义移动构造函数和拷贝构造函数,需要对以前的规则进行补充,调用哪个函数还需要取决于返回的对象的生命周期

资源管理

C语言在进行资源管理的时候,比如文件指针,由于分支较多,或者由于写代码的人与维护的人不一致,导致分支没有写的那么完善,从而导致文件指针没有释放。

void UseFile(char const* fn) {FILE* f = fopen(fn, “r”); // 1. 获取资源// …… // 2.使用资源// 回收资源有很多分支if (!g()) { fclose(f); return; }// ...if (!h()) { fclose(f); return; }// ...fclose(f); // 释放资源
}

根据之前单例对象自动释放的经验,我们可以想到利用对象的生命周期去管理资源。那么就可以尝试实现一个安全回收文件的程序了。

class SafeFile
{
public:// 在构造函数中初始化资源(托管资源)SafeFile(FILE * fp): _fp(fp){cout << "SafeFile(FILE*) " << endl;}// 提供方法访问资源void write(const string & msg){fwrite(msg.c_str(),1,msg.size(),_fp);}// 利用析构函数释放资源~SafeFile(){cout << "~SafeFile()" << endl;if(_fp){/* fclose(_fp); */cout << "fclose(_fp)" << endl;}}
private:FILE * _fp;
};void test0(){string msg = "hello,world";SafeFile sf(fopen("wd.txt","a+"));sf.write(msg);
}

RAII技术

以上例子其实已经用到了RAII的技术。所谓RAII,是C++提出的资源管理的技术,全称为Resource Acquisition Is Initialization,由C++之父Bjarne Stroustrup提出。其本质是**利用对象的生命周期来管理资源**(内存资源、文件描述符、文件、锁等),因为当对象的生命周期结束时,会自动调用析构函数。

RAII类的常见特征

RAII技术,具备以下基本特征:

  • 在构造函数中初始化资源,或托管资源。

  • 在析构函数中释放资源

  • 一般不允许进行复制或者赋值(对象语义

  • 提供若干访问资源的方法(如:读写文件)。

对象语义与值语义:

  • 值语义:可以进行复制或赋值(两个变量的值可以相同)
int a = 10; int b = a; int c = 20;
c = a; // 赋值
int d = c; // 复制
  • 对象语义:不允许复制或者赋值

(全世界不会有两个完全一样的人,程序世界中也不会有两个完全一样的对象)

常用手段:

  1. 拷贝构造函数赋值运算符函数设置为**私有**的
  2. 拷贝构造函数赋值运算符函数=delete
  3. 使用继承的思想,将基类的拷贝构造函数赋值运算符函数删除(或设为私有,让派生类继承基类
RAII类的模拟

我们可以实现以下的一个类,模拟RAII的思想

template <class T>
class RAII
{
public:// 1.在构造函数中初始化资源(托管资源)RAII(T * data): _data(data){cout << "RAII(T*)" << endl;}// 2.在析构函数中释放资源~RAII(){cout << "~RAII()" << endl;if(_data){delete _data;_data = nullptr;}}// ===============================3.提供访问资源的方法==============================T * operator->(){   // 返回类型T的指针return _data;}T & operator*(){    // T& 函数的返回结果是一个左值return *_data;  // 返回_data指针指向的内容,_data是一个T*类型的指针}// 通过对象获取到裸指针T * get() const{return _data;   // 返回T*类型的_data,_data是存储一个该类型对象的地址}// 重新接管新的资源void set(T * data){ // 接受一个T类型的指针作为参数if(_data){delete _data;_data = nullptr;}_data = data;   // 将新的资源指针 data 赋值给 _data}// 4.不允许复制或赋值RAII(const RAII & rhs) = delete;RAII& operator=(const RAII & rhs) = delete;private:T * _data;
};

如下,pt 不是一个指针,而是一个对象,但是它的使用已经和指针完全一致了。这个对象可以托管堆上的Point对象,而且不用考虑delete。

void test0() {Point * pt = new Point(1, 2);// 智能指针的雏形RAII<Point> raii(pt);raii->print();(*raii).print();
}
void test0(){RAII<int> raii(new int(10)); // RAII(T*)// Point * p2 = new Point(1,2);// p2->print();// (*p2).print();// delete p2;RAII<Point> raii2(new Point(3,8)); // Point(int=0,int=0) RAII(T*)raii2->print(); // (3,8)(*raii2.get()).print(); // (3,8)raii2.set(new Point(4,5));(*raii2).print(); // (4,5)int * p = new int(20);  // 堆对象cout << *p << endl; // 20delete p; // 如果不delete漏4B
}

RAII技术的本质:利用栈对象的生命周期管理资源,因为栈对象在离开作用域时候,会执行析构函数

智能指针

c++11提供了以下几种智能指针,位于**头文件<memory>**,它们都是类模板。

std::auto_ptr		c++0x
std::unique_ptr		c++11
std::shared_ptr		c++11
std::weak_ptr		c++11
auto_ptr的使用

auto_ptr 是最简单的智能指针,使用上存在缺陷,已经被 C++17 弃用了。

auto_ptr 是复制赋值函数的。

void test0(){int * pInt = new int(10);// 创建auto_ptr对象接管资源auto_ptr<int> ap(pInt);cout << "*pInt:" << *pInt << endl;cout << "*ap:" << *ap << endl;
}

尽管会有 warning 提示,代码仍可通过。发现不用对pInt进行delete,也没有内存泄露。

auto_ptr 可以进行复制,但是存在隐患

auto_ptr<int> ap2(ap);
cout << "*ap2:" << *ap2 << endl; // ok
cout << "*ap:" << *ap << endl;  

当ap2复制了ap后,对ap2管理的资源进行访问没有问题,但是对ap解引用会导致段错误。

void test0(){int * pInt = new int(10);auto_ptr<int> ap(pInt);// 二选一// auto_ptr<int> ap2(pInt); // double freeauto_ptr<int> ap2(ap); // Segmentation faultcout << "*ap2:" << *ap2 << endl;cout << "*pInt:" << *pInt << endl;// 猜测:通过auto_ptr的拷贝构造// 从ap拷贝出ap2对象,实际是一个控制权移交的过程// ap的指针被置空了cout << "*ap:" << *ap << endl;
}

通过阅读源码的实现,ap的指针被置为了空指针。

template <class _Tp> 
class auto_ptr {
public:auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}_Tp* release() __STL_NOTHROW {_Tp* __tmp = _M_ptr;_M_ptr = nullptr;return __tmp;}private:_Tp* _M_ptr;
};

也就是说,auto_ptr<int> ap2(ap); 这一步表面上执行了拷贝操作,但是底层已经将右操作数ap所托管的堆空间的控制权交给了左操作数ap2,并且将ap底层的指针数据成员置空。该拷贝操作存在隐患,所以auto_ptr被弃用了。

int * pInt = new int(10);
auto_ptr<int> ap(pInt);
/* auto_ptr<int> ap2(pInt); */auto_ptr<int> ap2(ap);
cout << "*ap2:" << *ap2 << endl;
cout << "*pInt:" << *pInt << endl;
// 猜测:通过auto_ptr的拷贝构造
// 从ap拷贝出ap2对象
// 实际是一个控制权移交的过程
// ap的指针被置空了
cout << "*ap:" << *ap << endl;
unique_ptr的使用(重要)

unique_ptr 对 auto_ptr 进行了改进。

  • 特点1:不允许复制或者赋值,具备**对象语义**。

  • 特点2:独享所有权的智能指针

void test0(){unique_ptr<int> up(new int(10));cout << "*up:" << *up << endl; // =================获取值cout << "up.get(): " << up.get() << endl; // ======获取地址// 独享所有权的智能指针,对托管的空间独立拥有(不允许赋值或赋值)// ---------------- 拷贝构造已经被删除 ------------------// unique_ptr<int> up2 = up; // (复制)操作 error// ---------------- 赋值运算符函数也被删除 --------------unique_ptr<int> up3(new int(20));// up3 = up; // (赋值)操作 error
}

将 auto_ptr 的缺陷摒弃了,具有对象语义,语法层面不允许复制、赋值。

  • 特点3:作为容器元素

要利用移动语义的特点,可以直接传递 unique_ptr 的右值,构建右值的方式有:

1、std::move 的方式。

2、可以直接使用 unique_ptr 的构造函数,创建匿名对象(临时对象),构建右值。

void test(){vector<unique_ptr<Point>> vec;unique_ptr<Point> up4(new Point(10,20));// up4是一个左值,将up4这个对象作为参数传给了push_back函数,会调用拷贝构造// 但是unique_ptr的拷贝构造已经删除了,所以这样写会报错// vec.push_back(up4); // error 编译报错 use of deleted functionvec.push_back(std::move(up4)); // okvec.push_back(unique_ptr<Point>(new Point(1,3))); // ok   
}
shared_ptr的使用(重要)

智能指针独享资源的控制权固然是一种需求,但有些场景下也需要允许共享控制权。

shared_ptr 就是共享所有权的智能指针,可以进行复制或赋值,但复制或赋值时,并不是真正拷贝对象,而只是将引用计数加1。即 shared_ptr 引入了引用计数,其思想与COW技术类似,又称为是强引用的智能指针。

  • 特征1:共享所有权的智能指针,可以使用**引用计数**记录对象的个数。
  • 特征2:可以进行复制或者赋值,表明具备**值语义**。
  • 特征3:也可以作为容器的元素,作为容器元素的时候,即可以传递左值,也可以传递右值。(区别于unique_ptr只能传右值)
  • 特征4:也具备移动语义,表明也有移动构造函数与移动赋值函数。
shared_ptr<int> sp(new int(10));
cout << "sp.use_count(): " << sp.use_count() << endl; // 1cout << "执行复制操作" << endl;
shared_ptr<int> sp2 = sp;
cout << "sp.use_count(): " << sp.use_count() << endl; // 2
cout << "sp2.use_count(): " << sp2.use_count() << endl; // 2cout << "再创建一个对象sp3" << endl;
shared_ptr<int> sp3(new int(30));
cout << "sp.use_count(): " << sp.use_count() << endl; // 2
cout << "sp2.use_count(): " << sp2.use_count() << endl; // 2
cout << "sp3.use_count(): " << sp3.use_count() << endl; // 1cout << "执行赋值操作" << endl;
sp3 = sp;
cout << "sp.use_count(): " << sp.use_count() << endl; // 3
cout << "sp2.use_count(): " << sp2.use_count() << endl; // 3
cout << "sp3.use_count(): " << sp3.use_count() << endl; // 3
cout << "*sp:" << *sp << endl; 
cout << "*sp2:" << *sp2 << endl;
cout << "*sp3:" << *sp3 << endl;
cout << "sp.get():" << sp.get() << endl; 
cout << "sp2.get():" << sp2.get() << endl; 
cout << "sp3.get():" << sp3.get() << endl; 
void test0(){shared_ptr<int> sp(new int(10));// cout << "*sp: " << *sp << endl;// cout << sp.get() << endl;// cout << endl;cout << "sp.use_count(): " << sp.use_count() << endl; // 1//复制操作    shared_ptr<int> sp2 = sp;// cout << "*sp:" << *sp << endl;// cout << sp.get() << endl;// cout << "*sp2:" << *sp2 << endl;// cout << sp2.get() << endl;cout << "sp.use_count(): " << sp.use_count() << endl; // 2cout << "sp2.use_count(): " << sp2.use_count() << endl; // 2cout << endl;// 赋值操作shared_ptr<int> sp3(new int(20));sp2 = sp3;cout << "sp.use_count(): " << sp.use_count() << endl; // 减一变为1cout << "sp2.use_count(): " << sp2.use_count() << endl; // 2cout << "sp3.use_count(): " << sp3.use_count() << endl; // 2// 作为容器元素vector<shared_ptr<int>> vec;vec.push_back(sp);vec.push_back(std::move(sp2));
}
shared_ptr的循环引用

share_ptr循环引用产生原因及其解决方案

shared_ptr还存在一个问题 —— 循环引用问题。

我们建立一个Parent和Child类的一个结构

class Child;class Parent
{
public:Parent(){ cout << "Parent()" << endl; }~Parent(){ cout << "~Parent()" << endl; }// 只需要Child类型的指针,不需要类的完整定义shared_ptr<Child> spChild;
};class Child
{
public:Child(){ cout << "child()" << endl; }~Child(){ cout << "~child()" << endl; }shared_ptr<Parent> spParent;
};

由于shared_ptr的实现使用了引用计数,那么如果进行如下的创建

  • use_count() 可以查看引用计数
shared_ptr<Parent> parentPtr(new Parent());
shared_ptr<Child> childPtr(new Child());
// 获取到的引用计数都是1
cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;
cout << "childPtr.use_count():" << childPtr.use_count() << endl;
image-20240322202756063

——程序结束时,发现 Parent 和 child 的析构函数都没有被调用

shared_ptr<Parent> parentPtr(new Parent());
shared_ptr<Child> childPtr(new Child());
// 获取到的引用计数都是1
cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;
cout << "childPtr.use_count():" << childPtr.use_count() << endl;
parentPtr->spChild = childPtr;
childPtr->spParent = parentPtr;
// 获取到的引用计数都是2
cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;
cout << "childPtr.use_count():" << childPtr.use_count() << endl;
image-20231107161838102

childPtr 和 parentPtr 会先后销毁,但是堆上的 Parent 对象和 Child 对象的引用计数都变成了1,而不会减到0,所以没有回收。

image-20231107163016399

07_shared_ptr.cc

#include <iostream>
#include <memory>
using std::cout;
using std::endl;
using std::shared_ptr;
using std::weak_ptr;class Child;class Parent
{
public:Parent(){ cout << "Parent()" << endl; }~Parent(){ cout << "~Parent()" << endl; }//只需要Child类型的指针,不需要类的完整定义shared_ptr<Child> _spParent;
};class Child
{
public:Child(){ cout << "child()" << endl; }~Child(){ cout << "~child()" << endl; }shared_ptr<Parent> _spParent;
};void test0(){shared_ptr<Parent> parentPtr(new Parent());shared_ptr<Child> childPtr(new Child());// 获取到的引用计数都是1cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;cout << "childPtr.use_count():" << childPtr.use_count() << endl;// parentPtr是一个管理Parent对象的智能指针,可以利用箭头运算符访问它所管理的Parent对象的成员// _spChild就是Parent对象的成员,同时也是一个能够管理Child对象的智能指针// 因为shared_ptr类型的智能指针可以进行赋值操作,所以可以使_spChild也能管理childPtr所管理的对象parentPtr->_spParent = childPtr;childPtr->_spParent = parentPtr;// 获取到的引用计数都是2cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;cout << "childPtr.use_count():" << childPtr.use_count() << endl;
}int main(void){test0();return 0;
}
解决思路

希望某一个指针指向一片空间,能够指向,但是不会使引用计数加1,那么堆上的Parent对象和Child对象必然有一个的引用计数是1,栈对象再销毁的时候,就可以使引用计数减为 0。

shared_ptr 无法实现这一效果,所以引入了 weak_ptr

weak_ptr是一个弱引用的智能指针,不会增加引用计数。

shared_ptr是一个强引用的智能指针。

  • 强引用,指向一定会增加引用计数,只要有一个引用存在,对象就不能释放;

  • 弱引用并不增加对象的引用计数,但是它知道所托管的对象是否还存活

——循环引用的解法,将 Parent 类或 Child 类中的任意一个 shared_ptr 换成 weak_ptr 类型的智能指针

比如:将 Parent 类中的 shared_ptr 类型指针换成 weak_ptr

image-20240322202959393

栈上的childPtr对象先销毁,会使堆上的Child对象的引用计数减1,因为这个Child对象的引用计数本来就是1,所以减为了0,回收这个Child对象,造成堆上的Parent对象的引用计数也减1。

再当parentPtr销毁时,会再让堆上的Parent对象的引用计数减1,所以也能够回收。

image-20240322203519008 image-20240322203500311
#include <iostream>
#include <memory>
using std::cout;
using std::endl;
using std::shared_ptr;
using std::weak_ptr;
class Child;
class Parent
{
public:Parent(){ cout << "Parent()" << endl; }~Parent(){ cout << "~Parent()" << endl; }weak_ptr<Child> _wpChild; // ========== 注意 ==========
};class Child
{
public:Child(){ cout << "child()" << endl; }~Child(){ cout << "~child()" << endl; }shared_ptr<Parent> _spParent;
};void test0(){shared_ptr<Parent> parentPtr(new Parent());shared_ptr<Child> childPtr(new Child());cout << parentPtr.use_count() << endl; // 1cout << childPtr.use_count() << endl; // 1parentPtr->_wpChild = childPtr;childPtr->_spParent = parentPtr;cout << "parentPtr.use_count():" << parentPtr.use_count() << endl; // 2cout << "childPtr.use_count():" << childPtr.use_count() << endl; // 1
}int main(void){test0();return 0;
}
weak_ptr的使用

weak_ptr 是弱引用的智能指针,它是 shared_ptr 的一个补充,使用它进行复制或者赋值时,并**不会导致引用计数加1**,是为了解决 shared_ptr 的问题而诞生的。

weak_ptr 知道所托管的对象是否还存活,如果存活,必须要提升为 shared_ptr 才能对资源进行访问,不能直接访问

weak_ptr 初始化
// 1. 无参的方式创建weak_ptr
weak_ptr<int> wp;// 2. 也可以利用shared_ptr创建weak_ptr 
weak_ptr<int> wp2(sp);// 不能这样创建
// weak_ptr<int> wp2(new int(20)); // error
判断关联的空间是否还在
// ================= shared_ptr可以直接判断 ==================
if(sp){cout << "有一片空间" << endl;
}// ============= weak_ptr不能直接判断 ===================
// if(wp){} // error
  1. 可以直接使用 use_count() 函数

如果 use_count 的返回值大于0,表明关联的空间还在

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp;
wp = sp;
shared_ptr<int> sp2 = sp;
cout << wp.use_count() << endl; // 2
  1. lock() 函数将 weak_ptr 提升为 shared_ptr

下面这种==赋值操作可以让 wp 也能够托管这片空间,但是它作为一个 weak_ptr 仍不能够去管理,甚至不允许访问==(weak_ptr不支持直接解引用)。

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp; // 无参的方式创建weak_ptr
wp = sp; // =============== 赋值 ===============
cout << *sp << endl; // ok
// cout << *wp << endl; // error 必须要提升为shared_ptr才能对资源进行访问,不能直接访问

想要真正地去进行管理需要使用 lock() 函数将 weak_ptr 提升为 shared_ptr。

shared_ptr<int> sp3 = wp.lock(); // 使用lock函数将weak_ptr提升为shared_ptr
if(sp3){cout << "提升成功" << endl;cout << *sp3 << endl;
}else{cout << "提升失败,没有托管的空间" << endl;
}

如果托管的资源没有被销毁,就可以成功提升为 shared_ptr,否则就会返回一个空的 shared_ptr(空指针)

——查看 lock 函数的说明

std::shared_ptr<T> lock() const noexcept;
// 将weak_ptr提升成一个shared_ptr,然后再来判断shared_ptr,进而知道weak_ptr指向的空间还在不在
  1. 可以使用 expired() 函数
bool expired() const noexcept; // weak_ptr调用expired()函数去判断托管的资源有没有被回收

该函数返回 true 等价于 use_count() == 0

bool flag = wp.expired();
if(flag){cout << "托管的空间已经被销毁" << endl;
}else{cout << "托管的空间还在" << endl;
}

代码:

#include <iostream>
#include <memory>
using std::cout;
using std::endl;
using std::weak_ptr;
using std::shared_ptr;void test0(){shared_ptr<int> sp(new int(10));weak_ptr<int> wp;// 不能这样创建// weak_ptr<int> wp2(new int(20));// 赋值wp = sp;shared_ptr<int> sp2 = sp;cout << wp.use_count() << endl;cout << sp.use_count() << endl;cout << sp2.use_count() << endl;if(sp){cout << "有一片空间" << endl;}// 不允许// if(wp){} // errorcout << *sp << endl;// cout << *wp << endl; // error 必须要提升为shared_ptr才能对资源进行访问,不能直接访问shared_ptr<int> sp3 = wp.lock(); // 使用lock函数将weak_ptr提升为shared_ptrif(sp3){cout << "提升成功" << endl;cout << *sp3 << endl;}else{cout << "提升失败,没有托管的空间" << endl;}cout << endl;// expired()返回true代表没有空间被托管// 返回false代表有空间被托管if(!wp.expired()){cout << "有空间" << endl;}else{cout << "没有空间" << endl;}
}int main(void){test0();return 0;
}

智能指针删除器

很多时候我们都**用 new 来申请空间**,用 delete 来释放。库中实现的各种智能指针,默认也都是用 delete 来释放空间。

但是若我们**采用malloc申请的空间或是用fopen打开的文件,这时智能指针的默认处理方式就不能解决了,必须为智能指针定制删除器**,这样,我们的智能指针就可以定制化释放资源的方式了。

unique_ptr对应的删除器
image-20231107174351960

定义unique_ptr时,如果没有指定删除器参数,就会使用默认的删除器。点开std::default_delete的说明

image-20231107174454514

英文版:

image-20240325202858607

无论接管的是什么类型的资源,回收时都是会执行delete语句或delete [ ]

看下面这个例子,利用unique_ptr管理文件资源,出现问题

void test0(){string msg = "hello,world\n";FILE * fp = fopen("res1.txt","a+");fwrite(msg.c_str(),1,msg.size(),fp);fclose(fp);
}void test1(){string msg = "hello,world\n";unique_ptr<FILE> up(fopen("res2.txt","a+")); // ====== 定义智能指针的时候,没有传自定义的删除器 =====fwrite(msg.c_str(),1,msg.size(),up.get()); // get函数可以从智能指针中获取到裸指针// fclose(up.get()); // 可以注释,因为可以使用 unique_ptr 默认的删除器
}

问题的原因:

接管文件资源时,也使用了 delete 语句,导致错误(memcheck ./a.out发现很多报告)

——需要自定义删除器

仿照参考文档上默认删除器的示例,创建一个代表删除器的 struct,定义 operator() 函数。

struct FILECloser{void operator()(FILE * fp){if(fp){fclose(fp);cout << "fclose(fp)" << endl;}}
};

创建 unique_ptr 接管文件资源时,删除器参数使用我们自定义的删除器。

void test1(){string msg = "hello,world\n";unique_ptr<FILE, FILECloser> up(fopen("res2.txt","a+")); // ========== 传入自定义的删除器 ===========fwrite(msg.c_str(),1,msg.size(),up.get()); // get函数可以从智能指针中获取到裸指针
}

如果管理的是普通的资源,不需要写出删除器,就使用默认的删除器即可,只有针对FILE或者socket这一类创建的资源,才需要改写删除器,使用fclose之类的函数

shared_ptr对应的删除器

unique_ptr 和 shared_ptr 区别:

  • 对于unique_ptr,删除器是模板参数
image-20231107201437584
  • 对于shared_ptr,删除器是构造函数参数
image-20231107201537512

所以**传入删除器参数的位置不同**。

void test2(){string msg = "hello,world\n";FILECloser fc;// 在shared_ptr的构造函数参数中加入删除器对象shared_ptr<FILE> sp(fopen("res3.txt","a+"),fc); // ============= 传入删除器的位置不同 =============fwrite(msg.c_str(),1,msg.size(),sp.get());
}
  • 位置不同:
image-20240322174347471

智能指针的误用

智能指针被误用的情况,原因都是将一个原生裸指针交给了不同的智能指针进行托管,而造成一个对象被销毁两次

对于 shared_ptr 与 unique_ptr 都会产生这个问题。

  • unique_ptr 要注意的误用
void test0(){//需要人为注意避免Point * pt = new Point(1,2);unique_ptr<Point> up(pt);unique_ptr<Point> up2(pt);
}void test1(){unique_ptr<Point> up(new Point(1,2));unique_ptr<Point> up2(new Point(1,2));//让两个unique_ptr对象托管了同一片空间up.reset(up2.get());
}
image-20240322175206762
  • shared_ptr 要注意的误用

使用不同的智能指针托管同一片堆空间,即使是 shared_ptr 也是不行的。

之前进行的 shared_ptr 的复制、赋值的参数都是 shared_ptr 的对象,不能直接多次把同一个裸指针传给它的构造

void test2() {Point * pt = new Point(10,20);shared_ptr<Point> sp(pt);shared_ptr<Point> sp2(pt);
}void test3() {//使用不同的智能指针托管同一片堆空间shared_ptr<Point> sp(new Point(1,2));shared_ptr<Point> sp2(new Point(1,2));sp.reset(sp2.get());
}
  • 还有一种误用

给 Point 类加入了这样的成员函数:

Point * addPoint(Point * pt){_ix += pt->_ix;_iy += pt->_iy;return this;
}

使用时,这样还是使得 sp3 和 sp 同时托管了同一个堆对象。

shared_ptr<Point> sp(new Point(1,2));    
shared_ptr<Point> sp2(new Point(3,4));// 创建sp3的参数实际上是sp所对应的裸指针,效果还是多个智能指针托管了同一块空间
shared_ptr<Point> sp3(sp->addPoint(sp2.get())); // ======== 注意 ========
cout << "sp3 = ";
sp3->print();

——需要给sp3的构造函数传入shared_ptr<Point> 对象,而不是裸指针

解决思路通过 this 指针获取本对象的 shared_ptr

可以修改 Point 中的 addPoint 函数

shared_ptr<Point> addPoint(Point * pt) {_ix += pt->_ix;_iy += pt->_iy;return shared_ptr<Point>(this); 
}

但是这样写,在 addPoint 函数中创建的匿名智能指针对象接收的还是 sp 对应的裸指针,那么这个匿名对象和 sp 所托管的空间还是同一片空间。匿名对象销毁时会 delete 一次,sp 销毁时又会 delete 一次。

——使用智能指针辅助类 enable_shared_from_this 的成员函数 shared_from_this。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 Point 的 addPoint 函数中需要使用 shared_from_this 函数返回的 shared_ptr 作为返回值,要想在 Point 类中调用enable_shared_from_this 的成员函数,最佳方案可以让 Point 类继承 enable_shared_from_this 类。

这样修改 addPoint 函数后,问题解决。

class Point 
: public std::enable_shared_from_this<Point> 
{
public://...shared_ptr<Point> addPoint(Point & pt) {_ix += pt._ix;_iy += pt._iy;return shared_from_this();}
};
image-20240322181651703 image-20240322181616356

总结:智能指针的误用全都是使用了不同的智能指针托管了同一块堆空间(同一个裸指针)

这篇关于移动语义和智能指针的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1136337

相关文章

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

智能交通(二)——Spinger特刊推荐

特刊征稿 01  期刊名称: Autonomous Intelligent Systems  特刊名称: Understanding the Policy Shift  with the Digital Twins in Smart  Transportation and Mobility 截止时间: 开放提交:2024年1月20日 提交截止日

基于 YOLOv5 的积水检测系统:打造高效智能的智慧城市应用

在城市发展中,积水问题日益严重,特别是在大雨过后,积水往往会影响交通甚至威胁人们的安全。通过现代计算机视觉技术,我们能够智能化地检测和识别积水区域,减少潜在危险。本文将介绍如何使用 YOLOv5 和 PyQt5 搭建一个积水检测系统,结合深度学习和直观的图形界面,为用户提供高效的解决方案。 源码地址: PyQt5+YoloV5 实现积水检测系统 预览: 项目背景

我在移动打工的日志

客户:给我搞一下录音 我:不会。不在服务范围。 客户:是不想吧 我:笑嘻嘻(气笑) 客户:小姑娘明明会,却欺负老人 我:笑嘻嘻 客户:那我交话费 我:手机号 客户:给我搞录音 我:不会。不懂。没搞过。 客户:那我交话费 我:手机号。这是电信的啊!!我这是中国移动!! 客户:我不管,我要充话费,充话费是你们的 我:可是这是移动!!中国移动!! 客户:我这是手机号 我:那又如何,这是移动!你是电信!!

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非

用Unity2D制作一个人物,实现移动、跳起、人物静止和动起来时的动画:中(人物移动、跳起、静止动作)

上回我们学到创建一个地形和一个人物,今天我们实现一下人物实现移动和跳起,依次点击,我们准备创建一个C#文件 创建好我们点击进去,就会跳转到我们的Vision Studio,然后输入这些代码 using UnityEngine;public class Move : MonoBehaviour // 定义一个名为Move的类,继承自MonoBehaviour{private Rigidbo

单片机毕业设计基于单片机的智能门禁系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍程序代码部分参考 设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订

理解分类器(linear)为什么可以做语义方向的指导?(解纠缠)

Attribute Manipulation(属性编辑)、disentanglement(解纠缠)常用的两种做法:线性探针和PCA_disentanglement和alignment-CSDN博客 在解纠缠的过程中,有一种非常简单的方法来引导G向某个方向进行生成,然后我们通过向不同的方向进行行走,那么就会得到这个属性上的图像。那么你利用多个方向进行生成,便得到了各种方向的图像,每个方向对应了很多