C++之新的类功能与STL的变化
默认的移动构造和移动复制
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。(要求比较苛刻,之前是不写构造函数就编译器可以自动生成默认构造函数)默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉(浅拷贝),⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造:
#include <iostream>
#include <utility>
class Resource {
public:
Resource() { std::cout << "Resource()--->构造" << std::endl; }
~Resource() { std::cout << "~Resource()--->析构" << std::endl; }
Resource(const Resource&) { std::cout << "Resource(const Resource&)--->拷贝构造" << std::endl; }
Resource& operator=(const Resource&) {
std::cout << "Resource& operator=(const Resource&)--->移动赋值" << std::endl;
return *this;
}
};
class MyClass {
public:
Resource res;
int value;
MyClass(int val) : value(val) {}
// 没有自定义移动构造函数和移动赋值运算符
};
int main() {
MyClass a(10);
MyClass b = std::move(a); // 调用默认的移动构造函数
std::cout << "Value of b: " << b.value << std::endl;
return 0;
}
这段代码展示了一个简单的C++程序,其中包含一个Resource
类和一个MyClass
类。MyClass
包含一个Resource
对象和一个整型成员value
。MyClass
没有自定义的移动构造函数和移动赋值运算符,因此编译器会为它生成默认的移动构造函数和移动赋值运算符。
在main
函数中,创建了一个MyClass
对象a
,然后使用std::move(a)
将a
移动构造到新对象b
。由于MyClass
没有自定义移动构造函数,编译器会生成一个默认的移动构造函数,这个默认移动构造函数会调用Resource
的拷贝构造函数,因为Resource
没有移动构造函数。
下面是程序的输出:
Resource()--->构造
Resource(const Resource&)--->拷贝构造
~Resource()--->析构
Value of b: 10
~Resource()--->析构
输出解释:
Resource()--->构造
:当MyClass a(10)
被构造时,Resource
对象res
被默认构造。Resource(const Resource&)--->拷贝构造
:当MyClass b = std::move(a)
执行时,由于没有自定义移动构造函数,编译器生成的默认移动构造函数会调用Resource
的拷贝构造函数,而不是移动构造函数。~Resource()--->析构
:a
的Resource
对象被析构,因为它的资源被移动到了b
。Value of b: 10
:输出b
的value
成员。~Resource()--->析构
:b
的Resource
对象被析构,当main
函数结束时。
注意,尽管我们使用了std::move
,但是由于Resource
类没有移动构造函数,所以实际上发生的是拷贝构造,而不是移动构造。这就是为什么输出中显示的是拷贝构造而不是移动构造的原因。如果Resource
类有一个移动构造函数,那么输出将会显示移动构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果你提供了移动构造函数或移动赋值运算符,编译器不会自动提供拷贝构造函数和拷贝赋值运算符。这是因为移动操作的存在意味着资源的所有权转移,而拷贝操作则意味着资源的复制,两者在语义上是不同的。
声明时给缺省值
可以看看:类的6大默认成员函数(深入)😝https://blog.csdn.net/Small_entreprene/article/details/140418660?fromshare=blogdetail&sharetype=blogdetail&sharerId=140418660&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link
default和delete关键字
C++11中引入的两个特性:default
关键字和delete
关键字。这两个关键字允许程序员更精细地控制类成员函数的行为,包括默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符。
default关键字
default
关键字用于显式要求编译器生成默认版本的成员函数。这在你已经提供了类的其他成员函数(如自定义的拷贝构造函数或析构函数)时特别有用,因为通常这会阻止编译器自动生成默认的移动构造函数或移动赋值运算符。
class MyClass {
public:
MyClass() = default; // 显式要求编译器生成默认构造函数
MyClass(const MyClass&) = default; // 显式要求编译器生成默认拷贝构造函数
MyClass(MyClass&&) = default; // 显式要求编译器生成默认移动构造函数
MyClass& operator=(const MyClass&) = default; // 显式要求编译器生成默认拷贝赋值运算符
MyClass& operator=(MyClass&&) = default; // 显式要求编译器生成默认移动赋值运算符
};
我们要注意: “如果你提供了移动构造函数或移动赋值运算符,编译器不会自动提供拷贝构造函数和拷贝赋值运算符” 这个注意点:
-
问题场景
假设你有一个类MyClass
,它管理了一些资源(如动态分配的内存)。你提供了一个移动构造函数来优化资源的转移,但没有提供拷贝构造函数。在某些情况下,你可能会遇到需要拷贝MyClass
对象的场景,但由于你没有提供拷贝构造函数,编译器也不会自动生成,这会导致编译错误。
-
使用
default
关键字解决
如果你确定MyClass
的拷贝构造行为应该是默认的(即深拷贝资源),你可以显式地使用default
关键字来要求编译器生成默认的拷贝构造函数和拷贝赋值运算符,即使你已经提供了移动构造函数或移动赋值运算符。
class MyClass {
public:
// 资源管理成员,例如动态分配的内存
int* data;
MyClass() : data(new int(0)) {
std::cout << "Default constructor called" << std::endl;
}
~MyClass() {
delete data;
std::cout << "Destructor called" << std::endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
std::cout << "Move constructor called" << std::endl;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
std::cout << "Move assignment operator called" << std::endl;
return *this;
}
// 显式要求编译器生成默认的拷贝构造函数和拷贝赋值运算符
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = default;
};
在这个例子中,即使我们提供了移动构造函数和移动赋值运算符,我们也使用default
关键字来允许编译器生成默认的拷贝构造函数和拷贝赋值运算符。这样,MyClass
既可以通过移动语义高效地转移资源,也可以在需要时进行深拷贝。
-
注意事项
使用default
关键字时,你需要确保这种默认行为是安全的,特别是在涉及到资源管理时。如果你的类包含不能被安全拷贝的资源(如文件句柄、网络连接等),那么提供默认的拷贝构造函数和拷贝赋值运算符可能会导致程序错误或资源泄漏。在这种情况下,你应该使用delete
关键字来显式禁止拷贝操作
delete关键字
delete
关键字用于阻止编译器生成特定的成员函数。这在你想要阻止某些操作(如拷贝或移动)时非常有用,比如当你的类管理了一个不能被拷贝或移动的资源时。(如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁而已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数)
我们在写程序中,有时候并不期望某个类去进行拷贝,就像IO流就不希望得到拷贝(istream/ostream)(因为IO流拷贝的话会牵扯到缓冲区等等一系列的各种问题)等,就像以下代码,他就会报错:
#include<iostream>
using namespace std;
//传值去写会报错,因为要调用ostream的拷贝构造
void func(ostream out)
{};
int main()
{
func(cout);
//会报错,因为调用ostream的拷贝构造了,这个类是不允许拷贝的,这个类为什么可以不允许拷贝,是因为这个类的实现使用了C++11提供的关键字delete
return 0;
}
这个报错是我们由于通过传值方式造成的,我们这里必须使用引用才能解决该问题:
void func(ostream& out)
{};
final和override关键字
finalhttps://blog.csdn.net/Small_entreprene/article/details/141546591?fromshare=blogdetail&sharetype=blogdetail&sharerId=141546591&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link
override https://blog.csdn.net/Small_entreprene/article/details/142171827?fromshare=blogdetail&sharetype=blogdetail&sharerId=142171827&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link
补充:additional:STL中一些变化
- 新的容器
以下圈起来的就是C++11增加的新的容器:
(是实际最有⽤的是unordered_map和unordered_set)
- 新的容器接口
STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等
- 容器的范围for遍历