C++(week12): C++基础 第十章:移动语义与智能指针

十、移动语义与智能指针

1.移动语义

1.移动语义的含义:移交控制权

2.移动语义的提出原因:希望减少临时对象new一次的开销


String s1("hello");

//拷贝构造
String s2 = s1;

//先构造,再拷贝构造
//利用"hello"这个字符串创建了一个临时对象
//并复制给了s3
//这一步实际上new了两次
String s3 = "hello";

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


(1)左值与右值

1.能对表达式取地址的,称为左值;不能取地址的,称为右值。
2.左值和右值是针对表达式而言的,左值是指表达式执行结束后依然存在的持久对象,右值是指表达式执行结束后就不再存在的临时对象。

在实际使用过程中,字面值常量、匿名对象(临时对象)、匿名变量(临时变量),都称为右值。右值又被称为即将被销毁的对象。
字面值常量,也就是10, 20这样的数字,属于右值,不能取地址。
字符串常量,“world",是属于左值的,位于内存中的文字常量区。


(2)右值引用

1.引用:
①非const左值引用:只能绑定左值,不能绑定右值。引用那些我们希望改变值的对象。
②const左值引用:既可以绑定左值,又可以绑定右值。引用那些我们不希望改变值的对象,比如常量。
右值引用:只能绑定右值,不能绑定左值。所引用对象的值在我们使用之后就无需保留了,比如临时变量。

const左值引用、右值引用,绑定右值后,延长了右值的生命周期。和引用一起,到作用域结束。


2.C++11为什么要引入右值引用?
右值引用可以识别右值并绑定右值。
引入右值引用,可以实现移动语义,通过移动而不是复制,可以显著提高性能,避免多new一次的系统调用开销。

右值引用,可以识别出右值,且能够绑定右值
在这里插入图片描述

//非const左值引用,只能绑左值,不能绑定右值
int a = 10;
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

在这里插入图片描述


(3)移动构造函数 (重要)

1.移动构造函数:移动构造没有new,只移交管理权。提高了效率。

//移动构造:移交管理权:浅拷贝,把之前的指针置空
String(String && rhs)
: _pstr(rhs._pstr)
{
    cout << "String(String&&)" << endl;
    rhs._pstr = nullptr;
}

拷贝构造和移动构造 进行函数重载,移动构造的优先级更高。

//移动构造: String s1 = String("hello");
String(String && rhs)
: _pstr(rhs._pstr) //浅拷贝
{
    cout << "移动构造" << endl;
    rhs._pstr = nullptr; //移交管理权 
}

//拷贝构造: String s2 = s1;
String(const String & rhs)
: _pstr(new char[strlen(rhs._pstr) + 1]())
{
    cout << "String(const String &)" << endl;
    strcpy(_pstr, rhs._pstr);
}

2.移动构造函数的特点:
(1)如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动构造,对右值的复制会调用移动构造。
(2)如果显式定义了拷贝构造,而没有显式定义移动构造,那么对右值的复制会调用拷贝构造。
(3)如果显式定义了拷贝构造和移动构造,那么对右值的复制会调用移动构造。

总结:移动构造函数优先级高于拷贝构造函数。

可以理解为:如果显式定义了拷贝构造和移动构造,利用一个已存在的对象创建一个新对象时,会先尝试调用移动构造,如果这个对象是右值,就使用移动构造函数创建出新对象,如果这个对象是左值,移动构造使用不了,就会调用拷贝构造。


在这里插入图片描述


①去除优化效果参数
g++ String.cpp -fno-elide-constructors

(4)移动赋值函数 (重要)

1.移动赋值运算符函数:
①判断自复制
②释放左操作数申请的堆空间
③浅拷贝
④右操作数置为空指针

在这里插入图片描述

String & operator=(String && rhs){
	cout << "String& operator=(String&&)" << endl;
	if(this != &rhs){
    delete [] _pstr;
    //浅拷贝
    _pstr = rhs._pstr;
    rhs._pstr = nullptr;  
    }
    return *this;
}

2.移动赋值函数的特点:
(1)如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动赋值函数。使用右值的内容进行赋值会调用移动赋值函数。
(2)如果显式定义了赋值运算符函数,而没有显式定义移动赋值函数,那么使用右值的内容进行赋值会调用赋值运算符函数。
(2)如果显式定义了移动赋值函数和赋值运算符函数,那么使用右值的内容进行赋值会调用移动赋值函数。

移动赋值函数优先级也是高于赋值运算符函数


3.自赋值判断有必要。如果没有自赋值,可能会变成空指针
在这里插入图片描述


3.移动语义
总结:
(1)将拷贝构造函数和赋值运算符函数称为具有复制控制语义的函数;
(2)将移动构造函数和移动赋值函数称为具有移动语义的函数(移交控制权)
具有移动语义的函数优于具有复制控制语义的函数执行。


(5)std::move函数

std::move(a); 返回值将左值转为右值。但数据本身还是左值。

int a = 1;
&(std::move(a)); //error,返回值从左值转成了右值,但a本身还是左值

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


在这里插入图片描述


如果String str2 = std::move(左值),则左值对象会被修改。需要重新赋值才能继续使用。
在这里插入图片描述


(6)右值引用本身的性质

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

2.move + 右值引用,可以绑定左值

int func(){
    return 1;
}

void test(){
    /* &func(); //没有名字的右值引用,是右值。不能取地址 */
   int && ref = func(); //有名字的右值引用,是左值。可以取地址。
   cout << &ref << endl;
}

string func2(){
    string str1("wd");
    return str1;
}

void test2(){
    /* &func2(); //编译错误,右值不能取地址 */
    string && ref = func2();
    cout << &ref << endl; //有名字的右值引用为左值,可以取地址
}

int globalNumber = 10;

int && func3(){
    return std::move(globalNumber);
}

void test3(){
    /* &func3(); //没有名字的右值引用是右值,不能取地址 */
    int && ref = func3();
    cout << & ref << endl; //有名字的右值引用是左值,可以取地址
    cout << ref << endl;
}
String func2(){
    String str1("wangdao");
	str1.print();
    return str1;
}

void test2(){
    func2();
    //&func2(); //error,右值
   	String && ref = func2();
    &ref;  //右值引用本身为左值
}

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

在这里插入图片描述


1.当返回的对象其本身生命周期即将结束,就不再调用拷贝构造函数,而是调用移动构造函数。
2.如果返回的对象其本身生命周期大于func3函数,执行return语句时还是调用拷贝构造函数
总结:当类中同时定义移动构造函数和拷贝构造函数,需要对以前的规则进行补充,调用哪个函数还需要取决于返回的对象本体的生命周期


在这里插入图片描述



2.资源管理

(1)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技术的本质:利用栈对象的生命周期管理资源,因为栈对象在离开作用域时候,会执行析构函数。

在这里插入图片描述

#include <iostream>
using std::cout;
using std::endl;

class Point
{
public:
    Point(int x,int y)
    : _ix(x)
    , _iy(y)
    {
        cout << "Point(int,int)" << endl;
    }

    void print(){
        cout << "(" << _ix
            << "," << _iy
            << ")" << endl;
    }
private:
    int _ix;
    int _iy;
};


template <class T>
class RAII
{
public:
    RAII(T * data)
    : _data(data)
    {
        cout << "RAII(T*)" << endl; 
    }

    ~RAII(){
        cout << "~RAII()" << endl;
        if(_data){
            delete _data;
            _data = nullptr;
        }
    }

    T * operator->(){
        return _data;
    }

    T & operator*(){
        return *_data;
    }

    //获取裸指针 (获取底层的指针)
    T * get() const{
        return _data;
    }

    //重新管理一片资源
    void reset(T * data){
        if(_data){
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    RAII(const RAII & rhs) = delete;
    RAII & operator=(const RAII & rhs) = delete;
private:
    T * _data;
    //int * _data;
    //char * _data;
    //Point * _data;
};


void test0(){
    int * p = new int(10);
    RAII<int> raii(p);
    /* RAII<int> raii(new int(10)); */

    cout << p << endl;
    cout << raii.get() << endl;
    cout << *raii << endl;
    cout << raii.operator*() << endl;
    /* delete p; */
}

void test1(){
    RAII<Point> raii(new Point(1,2));
    raii->print();
    (*raii).print();
    cout << raii.get() << endl;
    raii.reset(new Point(10,9));
    raii->print();
}

int main(void){
    test1();
    return 0;
}

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


(2)智能指针

头文件<memory>,它们都是类模板

①auto_ptr的使用

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

特点:
1.auto_ptr是有复制、赋值函数的。
2.是移动语义的,会移交管理权,存在隐患。

在这里插入图片描述

#include <memory>

void test(){
	int * p = new int(10);
	auto_ptr<int> ap(p);
	cout << *ap << endl; //auto_ptr内部重载了解引用运算符
	//delete p; //再手动delete会double free
}

void test2(){
	//智能指针通常在构造函数参数初始化时直接使用new表达式
	auto_ptr<int> ap(new int(20));
	cout << *ap << endl;
	cout << endl;

	auto_ptr<int> ap2(ap);
	//虽然调用了拷贝构造,但是底层的实现是移交管理权的操作
	//ap失去了对之前的资源空间的管理权
	//cout << *ap << endl;  //访问空指针,造成程序中断
	cout << *ap2 << endl;
}

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


unique_ptr的使用 (重要)

unique_ptr是独占型智能指针,独自管理资源,独享控制器。unique_ptr对auto_ptr进行了改进。

特点1:不允许复制或者赋值 (删除了拷贝构造)
具备对象语义。
特点2:独享所有权的智能指针


unique_ptr:不能进行复制或赋值
在这里插入图片描述

在这里插入图片描述


移动,移交了管理权
在这里插入图片描述

在这里插入图片描述


特点3:作为容器元素,只能将右值作为元素放入vector
要利用移动语义的特点,可以直接传递右值属性的unique_ptr作为容器的元素。如果传入左值形态的unique_ptr,会进行复制操作,而unique_ptr是不能复制的。

构建右值的方式有:
①std::move的方式
②可以直接使用unique_ptr的构造函数,创建匿名对象(临时对象),构建右值


shared_ptr的使用 (重要)

共享型智能指针
智能指针独享资源的控制权固然是一种需求,但有些场景下也需要允许共享控制权。
shared_ptr就是共享所有权的智能指针,可以进行复制或赋值,但复制或赋值时,并不是真正拷贝了被管理的对象,而只是将引用计数加1了。即shared_ptr引入了引用计数,其思想与COW技术类似,又称为是强引用的智能指针。

特征1:共享所有权的智能指针
可以使用引用计数记录对象的个数。

特征2:支持拷贝构造、赋值
具备值语义。拷贝构造支持用小括号、等号

shared_ptr<int> sp(new int(10));
cout << *sp << endl;
cout << sp.get() << endl;
cout << sp.use_count() << endl;
//shared_ptr支持拷贝构造和赋值
//拷贝构造:可以用 ()
shared_ptr<int> sp2(sp); 
//拷贝构造:也可以用 =
shared_ptr<int> sp3 = sp2; 
//赋值
shared_ptr<int> sp4;
sp4 = sp3;  

特征3:也可以作为容器的元素
作为容器元素的时候,即可以传递左值,也可以传递右值。(区别于unique_ptr只能传右值)

特征4:也具备移动语义
表明也有移动构造函数与移动赋值函数。

在这里插入图片描述

复制或赋值,引用计数会改变
在这里插入图片描述


3.作为容器的元素,若传递的是左值,则引用计数+1。
在这里插入图片描述

若传递的是右值,是移交管理权,引用计数不变。原来的sp被置空,无法解引用。
在这里插入图片描述

shared_ptr<int> sp(new int(10));
cout << "sp.use_count(): " << sp.use_count() << endl;

cout << endl;
cout << "执行复制操作" << endl;
shared_ptr<int> sp2 = sp;
cout << "sp.use_count(): " << sp.use_count() << endl;
cout << "sp2.use_count(): " << sp2.use_count() << endl;

cout << endl;
cout << "再创建一个对象sp3" << endl;
shared_ptr<int> sp3(new int(30));
cout << "sp.use_count(): " << sp.use_count() << endl;
cout << "sp2.use_count(): " << sp2.use_count() << endl;
cout << "sp3.use_count(): " << sp3.use_count() << endl;

cout << endl;
cout << "执行赋值操作" << endl;
sp3 = sp;
cout << "sp.use_count(): " << sp.use_count() << endl;
cout << "sp2.use_count(): " << sp2.use_count() << endl;
cout << "sp3.use_count(): " << sp3.use_count() << endl;

cout << endl;
cout << "作为容器元素" << endl;
vector<shared_ptr<int>> vec;
vec.push_back(sp);
vec.push_back(std::move(sp2));

shared_ptr的循环引用

shared_ptr循环引用,造成无法析构,造成内存泄露

在这里插入图片描述

先销毁childPtr(栈对象,后创建先析构)
在这里插入图片描述


数据成员恰好又是一个shaed_ptr,共同指向对方的数据成员
在这里插入图片描述

在这里插入图片描述


回收栈上对象,发现引用计数没减到0。就仅回收了栈对象,没有调用析构函数去回收堆上的数据成员,造成内存泄露。
在这里插入图片描述


解决:如何解开循环引用?引入weak_ptr

weak_ptr是一个弱引用的智能指针,不会增加引用计数。
shared_ptr是一个强引用的智能指针。
强引用,指向一定会增加引用计数,只要有一个引用存在,对象就不能释放;
弱引用并不增加对象的引用计数,但是它知道所托管的对象是否还存活。

在这里插入图片描述

将Parent类中的shared_ptr改为weak_ptr
在这里插入图片描述
在这里插入图片描述


反过来也一样,也能避免造成循环引用。
~Parent()~Child()
在这里插入图片描述


⑤weak_ptr的使用

weak_ptr仅仅增加了指向,不增加引用计数,也不能直接访问资源

weak_ptr是弱引用的智能指针,它是shared_ptr的一个补充,使用它进行复制或者赋值时,并不会导致引用计数加1,是为了解决shared_ptr的问题而诞生的。
weak_ptr知道所托管的对象是否还存活,如果存活,必须要提升为shared_ptr才能对资源进行访问,不能直接访问。

1.初始化

weak_ptr<int> wp;//无参的方式创建weak_ptr

//也可以利用shared_ptr创建weak_ptr 
weak_ptr<int> wp2(sp);

2.判断关联的空间是否还在
(1)可以直接使用use_count函数
如果use_count的返回值大于0,表明关联的空间还在

(2)使用lock函数,将weak_ptr提升为shared_ptr

shared_ptr<int> sp2 = wp.lock();
if(sp2){
cout << "提升成功" << endl;
cout << *sp2 << endl;
}else{
cout << "提升失败,托管的空间已经被销毁" << endl;
}

在这里插入图片描述
lock()函数是返回 临时shared_ptr对象。也就是说lock()是临时提升。
在这里插入图片描述


(3)可以使用expired函数
该函数返回true等价于use_count() == 0.

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

①shared_ptr和weak_ptr的相互转换
<1>用shared_ptr创建weak_ptr

在这里插入图片描述

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;  // 用shared_ptr创建weak_ptr

    std::cout << "Shared pointer count: " << sp.use_count() << std::endl; // 引用计数为1

    return 0;
}

<2>weak_ptr转为shared_ptr:lock()

要从 weak_ptr 获取对对象的访问权,需要将其转换为 shared_ptr。这可以通过调用 weak_ptr 的 lock() 方法来实现。



(3)删除器

①unique_ptr对应的删除器

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

但如果我们是用fopen打开文件,这时智能指针的默认处理方式就不能解决了,必须为智能指针定制删除器,也就是定制化释放资源的方式。


问题:直接使用智能指针去包含FILE *
问题1:默认的删除器是delete,而不是fclose。不调用fclose,不会向文件流中写入内容
问题2:如果手动调用fclose,会造成double free。而且也不符合智能指针的含义。

在这里插入图片描述


问题的根本原因:接管文件资源时,智能指针在析构时也是使用delete语句来回收资源,导致错误
——需要自定义删除器
仿照参考文档上默认删除器的示例,创建一个代表删除器的struct,定义operator()函数

在这里插入图片描述

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

创建时,写上手动的删除器 (文件资源,需要自定义删除器)
在这里插入图片描述


②shared_ptr对应的删除器

unique_ptr 的删除器是模板参数。
而shared_ptr 自定义的删除器,是在构造函数里添加。
在这里插入图片描述

举例:
在这里插入图片描述


(4)智能指针的误用

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


举例1:一个裸指针交给两个不同的unique_ptr进行托管。
在结束时,up和up2都会去自动回收,造成double free。需要人为避免。

Point * p = new Point(1,2);
unique_ptr<Point> up(p);
unique_ptr<Point> up2(p);

为了避免这种情况,可以直接在构建智能指针的时候使用new表达式,构造匿名对象。

/* Point * p = new Point(1,2); */
unique_ptr<Point> up(new Point(1,2));
unique_ptr<Point> up2(new Point(1,2));

举例2:一个裸指针交给两个不同的shared_ptr进行托管。
也会造成double free。因为引用计数还是1。
在这里插入图片描述

正确的共享应该是这样:
在这里插入图片描述


举例3:用get()又获取了裸指针
在这里插入图片描述

相当于将Point(1,2)匿名对象的裸指针,又交给了sp3。即又把同一个裸指针交给了不同的智能指针进行托管,在析构时会造成double free。
在这里插入图片描述

解决:
思路1:将addPoint的返回值类型,从Point * 改为shared_ptr
在这里插入图片描述
问题:把问题提前到了return时,还是会创建一个匿名的shared_ptr,与sp共同托管了同一个裸指针。


思路2:重新构建一个shread_ptr<Point>。虽然没有double free,但是这样构建的sp3和sp管理的并不是同一片空间。

在这里插入图片描述


思路3:使用智能指针辅助类 enable_shared_from_this的成员函数shared_from_this
在这里插入图片描述
要用Point类 继承 ennable_shared_from_this 类,才能调用 shared__frorm_this()成员函数
在这里插入图片描述

不仅没有double free,还可以真正让sp3和sp管理同一片空间
在这里插入图片描述


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


作业:文本查询程序再探

在这里插入图片描述

在这里插入图片描述

解决思路:继承、多态、运算符重载、智能指针
C++ Primer第五版,15.9
在这里插入图片描述

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员爱德华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值