重要的话写在前面
- 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数,当然,C++11还增添了移动构造函数。
1. C++编译器会为类生成很多缺省函数,如果你没有定义它们且它们被需要的话
1.1 被生成的缺省函数的样子
class Example {
public:
Example(); // 默认构造函数
Example(const Example& other); // 拷贝构造函数
Example& operator=(const Example& other); // 拷贝赋值运算符
Example(Example&& other); // 移动构造函数
Example& operator=(Example&& other); // 移动赋值运算符
~Example(); // 析构函数,编译器缺省的析构函数是non-virtual,除非这个class的base class自身声明有virtual析构函数
};
没错,自C++11起又多了两种缺省的函数,分别是移动构造函数和移动赋值运算符。
1.2 缺省的构造函数和析构函数
- 缺省构造函数:在默认构造函数中,编译器会调用基类(如果有)的构造函数和非静态成员变量的构造函数,以确保对象的所有部分都被正确地初始化。
- 缺省析构函数:在析构函数中,编译器会调用基类(如果有)和非静态成员变量的析构函数,以确保对象的所有部分都被正确地清理。此时需注意子类的默认析构函数的
virtual
属性根据基类而来。 - 注意:如果你声明了构造函,那么编译器将不会为你再生成默认的构造函数了,哪怕你调用了无参的构造函数,举个栗子:
class Example {
public:
Example(int num):val(num),str(){//这里的列表初始化的书写次序可以与声明次序不同,但初始化次序相同
cout << "this is example";
}
private:
string str;
int val;
};
//此时不可以使用无参构造了
Example* e = new Example();//会报错
1.3 缺省的拷贝构造函数和赋值运算符
- 缺省的拷贝构造:默认拷贝构造函数将使用另一个同类型对象的成员变量值来初始化新对象的成员变量。
- 缺省赋值运算符:默认赋值运算符则将一个同类型对象的成员变量值赋值给另一个对象的成员变量。
- 注意:这些缺省函数只会复制类的
non-static
成员变量。对于指针等动态分配的资源,使用默认函数可能会导致浅拷贝,进而导致一些不可预测的结果,如释放同一内存块两次,等等。 - 建议:我们最好根据类的特点自行实现拷贝构造函数和赋值构造函数,以确保成员变量的正确复制。
-举个浅拷贝的例子:
#include <iostream>
using namespace std;
class Example {
public:
Example(int size) {
ptr = new int[size];
this->size = size;
for (int i = 0; i < size; i++) {
ptr[i] = 0;
}
}
int getSizePos(int size) {//获取下标为size的ptr的值,这里没有边界检查,只是为了说明问题
return ptr[size];
}
void setSizePos(int pos, int num) {//设置下标为size的ptr的数值
ptr[pos] = num;
}
~Example() {
delete[] ptr;
}
private:
int size;
int* ptr;
};
int main() {
Example* e = new Example(2);
Example* f = e;//这里使用了编译器提供的缺省赋值操作符
cout << "e.sizePos:" << e->getSizePos(1) << " f.sizePos:" << f->getSizePos(1) << endl;
//输出:e.sizePos:0 f.sizePos:0
e->setSizePos(1, 1);
cout << "e.sizePos:" << e->getSizePos(1) << " f.sizePos:" << f->getSizePos(1) << endl;
//输出:e.sizePos:1 f.sizePos:1,这里只更改了e的部分内容,但是f的内容也相应更改了,很不妙的感觉。
delete e;
delete f;//这里会引发程序异常,是因为同一片区域释放了两次
return 0;
}
- 如何解决浅拷贝的问题:自行书写拷贝构造函数和赋值操作符,书写如下:
class Example {
public:
Example(int size) {
ptr = new int[size];
this->size = size;
for (int i = 0; i < size; i++) {
ptr[i] = 0;
}
}
Example(const Example& other) { // 拷贝构造函数
size = other.size;
ptr = new int[size];
for (int i = 0; i < size; i++) {
ptr[i] = other.ptr[i];
}
}
Example& operator=(const Example& other) { // 赋值操作符
if (this == &other) {
return *this;
}
delete[] ptr;
size = other.size;
ptr = new int[size];
for (int i = 0; i < size; i++) {
ptr[i] = other.ptr[i];
}
return *this;
}
int getSizePos(int size) {//获取下标为size的ptr的值,这里没有边界检查,只是为了说明问题
return ptr[size];
}
void setSizePos(int pos, int num) {//设置下标为size的ptr的数值
ptr[pos] = num;
}
~Example() {
delete[] ptr;
}
private:
int size;
int* ptr;
};
1.4 缺省的移动拷贝构造函数和移动赋值运算符
- C++11为我们带来了全新的移动拷贝构造函数和移动赋值运算符,是一种更高效、更快速、更经济的方式来处理对象和容器的拷贝和分配,特别是在处理大型对象时。
- 优点:
- 更高的性能:移动操作通常比拷贝操作更快,因为移动只是将指针从一个对象移动到另一个对象,而不需要实际拷贝数据。这对于大型对象和容器特别有用。
- 更少的内存分配:移动操作通常不需要分配新的内存,因为它们只是将已经存在的资源从一个对象移动到另一个对象。
- 更少的对象拷贝:移动操作可以减少对象拷贝的数量,从而减少内存开销和运行时间。
- 以上面的例子写出缺省的移动拷贝构造函数和移动赋值运算符
class Example {
public:
Example(int size) {
ptr = new int[size];
this->size = size;
for (int i = 0; i < size; i++) {
ptr[i] = 0;
}
}
Example(Example&& other) : size{other.size}, ptr{other.ptr}
{
other.size = 0;
other.ptr = nullptr;
}
Example& operator=(Example&& other)
{
if (this != &other) {
delete[] ptr;
size = other.size;
ptr = other.ptr;
other.size = 0;
other.ptr = nullptr;
}
return *this;
}
int getSizePos(int size) {//获取下标为size的ptr的值,这里没有边界检查,只是为了说明问题
return ptr[size];
}
void setSizePos(int pos, int num) {//设置下标为size的ptr的数值
ptr[pos] = num;
}
~Example() {
delete[] ptr;
}
private:
int size;
int* ptr;
};
其中,移动构造函数使用other
对象的指针和大小成员变量初始化当前对象,然后将other
对象的指针设置为nullptr
,以避免在other
对象被销毁时出现重复释放内存的问题。
移动赋值操作符首先检查是否为自我赋值,如果是则直接返回当前对象的引用。然后释放当前对象中的内存,并使用other
对象的指针和大小成员变量初始化当前对象。最后将other
对象的指针设置为nullptr
,以避免在 other 对象被销毁时出现重复释放内存的问题。
打完收工!都看到这了,给个赞再走呗~