代码编织梦想

类的拷贝、赋值、销毁和移动

拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做了什么

拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做了什么

拷贝构造函数

拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数时拷贝构造函数

(使用引用是因为,如果不引用,调用拷贝构造函数时形参会拷贝实参,但是拷贝实参又要调用拷贝构造函数,无限循环)

class Foo {
    public:
    	Foo();			//默认构造函数
    	Foo(const Foo&);	//拷贝构造函数
    
};

与合成默认构造函数不同,即使定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。

一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。

拷贝

  • 类类型成员,使用其拷贝构造函数拷贝
  • 内置类型成员:直接拷贝
  • 数组:逐元素拷贝

拷贝初始化

string dots(10, '.');		//直接初始化
string s(dots);				//直接初始化
string s = dots;			//拷贝初始化
string null_book = "999999";	//拷贝初始化
string nines = string(100, '9');	//拷贝初始化

直接初始化:要求编译器使用普通的函数匹配来选择与提供参数最匹配的构造函数

拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要可能会进行类型转化。

拷贝初始化通常使用拷贝构造函数来完成

拷贝初始化发生的情况

  • 用=定义变量
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 将一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

拷贝赋值运算符

合成拷贝赋值运算符:将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。

合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。

析构函数

名字由波浪线加类名组成。

没有返回值,也不接收参数,因此不能被重载,唯一。

析构函数释放对象使用的资源,并销毁对象的非static成员

析构函数先执行函数体,再销毁成员,按初始化顺序的逆序销毁。销毁类类型成员会调用成员自己的析构函数,销毁内置类型不需要操作。

销毁内置指针类型成员不会delete它所指向的对象。智能指针有析构函数,因此会自动销毁

三/五法则

需要析构函数的类也需要拷贝构造函数和拷贝赋值运算符

主要原因是合成的析构函数不会delete一个指针数据成员

阻止拷贝

旧标准:将拷贝构造函数和拷贝赋值运算符声明为private

新标准:=delete

对象移动

右值引用

https://blog.csdn.net/Appleeatingboy/article/details/129811772

右值引用:必须绑定到右值的引用,只能绑定到一个将要销毁的对象,通过&&获取

一般而言,左值表达式表示的是一个对象的身份,右值表达式表示的是一个对象的值

常规引用可以称为左值引用。不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。

右值引用相反:可以将右值引用绑定到这类表达式,但不能将一个右值引用绑定到一个左值上。

int i = 42;
int &r = i;
int &&r1 = i;			//错误,不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;		//错误,i * 42是一个右值
const int &r3 = i * 42; //正确,可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42;		//正确

标准库move函数

https://www.zhihu.com/question/64205844/answer/2401017464

std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。

调用move来获得绑定到左值上的右值引用

int &&rr1 = 42;		//正确:字面常量是右值
int &&rr2 = rr1;	//错误,rr1是左值
int &&rr3 = std::move(rr1);	//ok

可以销毁一个移后源对象,可以赋予它新值,但不能使用一个移后源对象的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XFYEwQwM-1684891997879)(C:/Users/87242/AppData/Roaming/Typora/typora-user-images/image-20230511094650816.png)]

移动赋值运算符

移动赋值运算符是一种复合赋值运算符,用于将一个变量的值移动到另一个变量中。在C++和许多其他编程语言中,移动赋值运算符使用右值引用语法(&&)来实现,以便在不需要拷贝对象的情况下转移其资源所有权。

例如,在C++中,一个简单的移动赋值运算符可能如下所示:

Copy Codeclass MyClass {
public:
    // 移动构造函数
    MyClass(MyClass&& other) {
        // 转移资源所有权
        data_ = other.data_;
        other.data_ = nullptr;
    }
    
    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) {
        if (this != &other) {
            // 释放当前资源
            delete data_;
            
            // 转移资源所有权
            data_ = other.data_;
            other.data_ = nullptr;
        }
        
        return *this;
    }
    
private:
    int* data_;
};

在这个例子中,移动赋值运算符接受一个右值引用参数,并将其内部指针数据成员的值转移到当前对象中,同时将原始对象置为无效状态。这样可以避免通过拷贝构造函数或拷贝赋值运算符进行昂贵的资源拷贝操作,从而提高程序的性能和效率。

但是你的移动赋值运算符函数体里面不是用的“=”吗?这不应该表示拷贝吗?为什么说是移动?

确实,在移动赋值运算符的函数体中出现了“=”符号,但这里的赋值并非拷贝操作,而是将右侧对象的资源所有权转移到左侧对象中。这个过程称为移动语义(move semantics)。

在C++11之前,我们只能使用传统的复制构造函数和赋值运算符来进行对象的复制,这种方式对于大型对象和动态内存分配的对象来说效率很低。而通过引入移动语义,我们能够避免昂贵的内存拷贝操作,提高程序的性能。

具体来说,移动赋值运算符接受一个右值引用参数,并使用std::move()函数将其转换为一个右值,从而允许其内部成员指针直接指向原始对象的内存空间,而不需要进行拷贝。同时,由于右侧对象的数据成员已经无效,因此可以将其置为nullptr或其他适当的无效状态,以避免释放重复的资源。

总之,移动赋值运算符是一种高效的资源管理方式,可以将对象的资源所有权快速地转移给其他对象,从而避免昂贵的拷贝操作。

那我怎么判断出现的“=”符号是拷贝还是移动语义呢?

在C++11中,我们可以通过特殊的右值引用符号(&&)和std::move()函数来判断一个赋值操作是拷贝还是移动。

具体来说,如果一个类定义了移动构造函数和移动赋值运算符,并且在这些函数中使用了右值引用符号(&&),那么我们就可以通过使用std::move()函数将一个对象转换为右值引用,从而触发其移动语义。例如:

Copy CodeMyClass a;
MyClass b(std::move(a)); // 使用移动构造函数

MyClass c;
c = std::move(b); // 使用移动赋值运算符

在上面的代码中,std::move()函数将对象a和b分别转换为右值引用,从而触发了移动构造函数和移动赋值运算符。如果没有使用std::move()函数,那么运行时将会调用拷贝构造函数和拷贝赋值运算符,这样会导致额外的性能开销。

总之,在C++11中,我们可以通过使用右值引用符号和std::move()函数来显式地触发移动语义,从而避免不必要的内存拷贝和资源占用。

是不是使用右值引用赋值的时候就会触发移动语义?

使用右值引用赋值并不一定会触发移动语义,需要具体看代码实现。

在C++11中,如果一个类定义了移动构造函数和移动赋值运算符,并且这些函数都使用了右值引用(&&)来传递参数,那么当我们使用对象的右值引用进行赋值操作时,就会触发移动语义。例如:

Copy Codeclass MyClass {
public:
    // 移动构造函数
    MyClass(MyClass&& other) {
        // 转移资源所有权
        data_ = other.data_;
        other.data_ = nullptr;
    }
    
    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) {
        if (this != &other) {
            // 释放当前资源
            delete data_;
            
            // 转移资源所有权
            data_ = other.data_;
            other.data_ = nullptr;
        }
        
        return *this;
    }
    
private:
    int* data_;
};

// 使用移动语义进行赋值
MyClass a;
MyClass b(std::move(a)); // 触发移动构造函数

MyClass c;
c = std::move(b); // 触发移动赋值运算符

在上面的代码中,我们使用std::move()函数将对象a转换为右值引用,从而触发了其移动构造函数;同时,使用对象b的右值引用对对象c进行赋值操作,从而触发了其移动赋值运算符。

然而,如果一个类没有定义移动构造函数和移动赋值运算符,那么当我们使用其右值引用进行赋值操作时,将会触发其拷贝构造函数和拷贝赋值运算符,而非移动语义。因此,在实现类的时候,需要根据具体情况来定义或者不定义移动语义相关的函数。

在移动构造函数内部是如何触发移动语义的?

在移动构造函数中,我们通常会使用右值引用(&&)来接受一个对象,并将其内部成员“转移”(move)给当前对象。这个过程就是通过移动语义来实现的。

具体来说,当我们在移动构造函数中接受一个右值引用参数时,该参数的生命周期即将结束,因此它所占用的资源也即将被释放。为了避免额外的内存拷贝和资源占用,我们可以直接将当前对象的指针成员指向原始对象所占用的内存空间,并将原始对象的指针成员置为nullptr或其他适当的无效状态。这样,虽然当前对象和原始对象都指向同一个内存空间,但由于原始对象已经无效,因此不会发生意外的内存释放或者重复的资源占用。

例如,在下面的代码中,我们定义了一个简单的Moveable类,其中包含有一个指针成员data_:

Copy Codeclass Moveable {
public:
    // 移动构造函数
    Moveable(Moveable&& other) {
        data_ = other.data_;
        other.data_ = nullptr; // 置为空指针
    }
    
private:
    int* data_;
};

在这个例子中,当我们使用右值引用来调用Moveable对象的移动构造函数时,它将会接受一个将要结束生命周期的临时对象作为参数。然后,它将该对象的指针成员data_转移到当前对象中,并将原始对象的指针成员置为nullptr。这样,我们就成功地实现了移动语义。

需要注意的是,移动构造函数只能用于右值引用参数,不能用于左值引用或者const引用参数。因此,在使用移动语义时,我们需要保证传递的对象是一个右值类型。如果传入的是一个左值对象,那么编译器将会调用拷贝构造函数而非移动构造函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RO1kyn9B-1684891997880)(C:/Users/87242/AppData/Roaming/Typora/typora-user-images/image-20230511101220967.png)]


在这个例子中,当我们使用右值引用来调用Moveable对象的移动构造函数时,它将会接受一个将要结束生命周期的临时对象作为参数。然后,它将该对象的指针成员data_转移到当前对象中,并将原始对象的指针成员置为nullptr。这样,我们就成功地实现了移动语义。

需要注意的是,移动构造函数只能用于右值引用参数,不能用于左值引用或者const引用参数。因此,在使用移动语义时,我们需要保证传递的对象是一个右值类型。如果传入的是一个左值对象,那么编译器将会调用拷贝构造函数而非移动构造函数。

[外链图片转存中...(img-RO1kyn9B-1684891997880)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmwE00wz-1684891997881)(C:/Users/87242/AppData/Roaming/Typora/typora-user-images/image-20230511101354507.png)]
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/bigblood_/article/details/130840383

c++:类拷贝控制 - 拷贝构造函数 & 拷贝赋值运算符_岁月斑驳7的博客-爱代码爱编程

一、拷贝控制 当定义一个类时,我们可以显式或隐式地指定此类型的对象拷贝、移动、赋值和销毁时做什么。 一个类可以通过定义五种特殊的成员函数来控制这些操作,包括:++拷贝构造函数++、++拷贝赋值函数++、++移动构造函数+

类 --对象 控制(拷贝 赋值 移动 销毁)[一]_geniusiotboy的博客-爱代码爱编程

类 -> 对象控制 拷贝 拷贝构造函数 1: 拷贝构造函数 定义 第一个参数是自身的引用类型(必须是引用类型),且任何额外参

【c++】定义类的拷贝、赋值、移动操作以及注意事项_hello_ape的博客-爱代码爱编程

当定义一个类时,需要考虑5种特殊的成员函数。分别是:拷贝构造、拷贝赋值、移动构造、移动赋值和析构函数。这些函数操作称为拷贝控制操作。 拷贝构造函数 如果一个构造函数,第一个参数是自身类类型的引用,其他参数有默认值。那么这个构造函数称为拷贝构造函数。 自身类类型必须是一个引用,通常使用const修饰。如果不是引用类型(传值参数),那么在实参给形参赋值时,

c++:类的拷贝和移动、初始化和赋值-爱代码爱编程

C++:类的拷贝和移动、初始化和赋值 测试代码 《C++Primer》学到拷贝控制这一章开始有点犯晕,拷贝和移动的各种使用条件和限制很不好理解。同时,在使用类对象的时候,明显能够感觉到正如《C++Primer》中所写的,

C++类的拷贝控制成员(拷贝构造、拷贝赋值、移动构造、移动赋值和析构函数)-爱代码爱编程

类中定义了五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作,包括拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数,统称为拷贝控制操作。 拷贝构造函数和移动构造函数定义了 当用同类型的另一个对象初始化正在创建的对象时需要做什么;拷贝赋值运算符和移动赋值运算符定义了 将一个对象赋给另一个同类型的对象时需要做什么;析构函数定义了 当

c++中类的拷贝/移动/赋值/销毁-爱代码爱编程

前言 第一次看C++ Primer时拷贝控制这章完全没看明白,只是很粗略地过了一遍。时隔一年,具备了一定的工程经验后,再次看这一章对于类的拷贝构造/赋值函数、移动构造/赋值函数以及析构函数有了完全不一样的认识,在博客里记录一下,C++ Primer每看一遍都会有不同的收获倒真不是乱说。 拷贝构造/赋值函数 拷贝构造函数第一个参数必须是一个引用类型,且

C++构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值之二-爱代码爱编程

一、概述 拷贝和移动是操作类对象的另外两种重要的方式。在类中的定义方式如下: class T{ public: T(const T&); //拷贝构造 T(T&&); //移动构造 T& operator=(const T&); //拷

移动构造函数、移动赋值运算符 和 拷贝构造函数、拷贝赋值运算符的区别-爱代码爱编程

移动构造函数和拷贝构造函数的 移动构造函数 和 移动赋值运算符 是C++11新引进的特性。 在C++11以前,对象的拷贝主要由三个函数控制: 拷贝构造函数 、 拷贝赋值运算符 、 析构函数 。在C++11引入了 移动构造函数 和 移动赋值运算符 两个函数。 A(A& exp)//拷贝构造函数 { if(exp.

C++学习笔记(拷贝、赋值、销毁)-爱代码爱编程

C++学习笔记(拷贝、赋值、销毁) 文章目录 C++学习笔记(拷贝、赋值、销毁)拷贝:拷贝构造函数:直接初始化与拷贝初始化:拷贝构造函数使用场景:explict关键字:拷贝赋值运算符重载:析构函数:阻止拷贝:对象移动:左值引用与右值引用移动构造函数与移动构造运算符 在对类进行定义时,除了对类对象可执行操作等定义 还会显示或隐式地指定在此类型

c++拷贝、赋值与销毁-爱代码爱编程

一、概述 当我们定义一个类时,我们显式或隐式地指定在此类型进行对象拷贝、移动、赋值和销毁操作 一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,这些操作称为拷贝控制函数 二、拷贝构造函数 1、如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函

c++类的拷贝销毁与赋值(上)_小萝卜爱吃兔子`的博客-爱代码爱编程

文章目录 拷贝销毁与赋值拷贝构造函数拷贝初始化 拷贝赋值运算符重载赋值运算符 析构函数三者相结合三五法则使用=default阻止拷贝定义删除的函数析构函数不能是删除的 拷贝控制和资源管理定义类值