类的拷贝、赋值、销毁和移动
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做了什么
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做了什么
拷贝构造函数
拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数时拷贝构造函数
(使用引用是因为,如果不引用,调用拷贝构造函数时形参会拷贝实参,但是拷贝实参又要调用拷贝构造函数,无限循环)
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)]