定义一个类时,可以显式或隐式地指定此类型对象拷贝、移动、赋值和销毁时的行为。我们可以通过定义五种特殊的方法来控制这些操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
拷贝/赋值/销毁
copy-constructor
定义:一个构造函数,第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是copy-constructor。其第一个参数必须是引用类型,否则调用时,其参数也需要被拷贝,如此无限循环。
如下,定义Obj类与其拷贝构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Obj { public: int count{}; Obj() { } Obj(int c) : count(c) {} Obj(const Obj & obj, int c = 0) { cout << "call copy-constructor" << endl; this->count = c; } }; int main() { Obj obj1(12); Obj obj2(obj1, 12); cout << obj2.count << endl; return 0; }
|
其中拷贝构造函数第二个参数具有默认值0。编译运行:
1 2 3
| > g++ main.cc -O3 -g -o main && ./main call copy-constructor 12
|
通过以下例子,查看拷贝构造函数调用的时刻,并为Obj类添加输出提示信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| class Obj { public: int count{}; Obj() { cout << "call constructor" << endl; } Obj(int c) : count(c) { cout << "call Obj(int) constructor" << endl; } Obj(const Obj & obj) { cout << "call copy-constructor" << endl; this->count = obj.count; } };
class Temp { public: Obj obj; ~Temp() { cout << "call Temp destructor" << endl; } };
void foo(Obj obj) { return; }
Obj bar() { return Obj(1); }
int main() { cout << "编译器掠过拷贝初始化:\n"; Obj o = 1; cout << o.count << endl; cout << "列表初始化: \n"; vector<Obj> objs{1,2,3}; for (auto & obj : objs) cout << obj.count << ' '; cout << endl; cout << "传递非引用参数: \n"; foo(o); cout << "返回非引用参数: \n"; Obj x = bar();
return 0; }
|
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| > g++ main.cc -g -o main && ./main 编译器掠过拷贝初始化: call Obj(int) constructor 1 列表初始化: call Obj(int) constructor call Obj(int) constructor call Obj(int) constructor call copy-constructor call copy-constructor call copy-constructor 1 2 3 传递非引用参数: call copy-constructor 返回非引用参数: call Obj(int) constructor rda@pa ~/T/proj (slave2)>
|
再看一个稍微复杂的例子,判断有几次拷贝构造函数的调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Obj global;
Obj bar(Obj arg) { Obj local = arg, *heap = new Obj(global); *heap = local; Obj pa[4] = { local, *heap };
return *heap; }
int main() { bar(global);
return 0; }
|
输出结果为
1 2 3 4 5 6 7
| > g++ main.cc -g -o main && ./main call copy-constructor call copy-constructor call copy-constructor call copy-constructor call copy-constructor call copy-constructor
|
总而言之,当存在使用一个已有的类对象去初始化另一个类对象时会调用copy-constructor。
another example
再看一个例子,验证合成拷贝构造函数的行为。
我们定义一个新的类Num,自定义其默认构造函数,为每个对象生成一个唯一序号,保存在数据成员id中。编译器为其生成合成拷贝构造函数和拷贝赋值运算符。
1 2 3 4 5 6 7 8 9 10 11 12
| class Num { public: static int next_id; int id{}; Num() { id = next_id++; } };
int Num::next_id = 1;
|
如下foo和bar函数打印传入参数的id,其中foo接受非引用参数,而bar接受引用类型参数
1 2 3 4 5 6 7 8 9 10 11
| void foo(const Num num) { cout << num.id << endl; return; }
void bar(const Num & num) { cout << num.id << endl; return; }
|
main函数如下,第一次调用foo
1 2 3 4 5 6 7 8 9
| int main() { Num a, b = a, c = b; foo(a); foo(b); foo(c);
return 0; }
|
输出为
1 2 3 4
| > g++ main.cc -g -o main && ./main 1 1 1
|
因为默认的拷贝构造函数会直接复制非static成员。
man函数中调用bar。输出不变。
自定义拷贝构造函数,main函数调用bar
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Num { public: static int next_id; int id{}; Num() { id = next_id++; } Num(const Num & num) { id = next_id++; } };
|
打印出预期的信息
1 2 3 4
| > g++ main.cc -g -o main && ./main 1 2 3
|
调用foo则不同,因为传参时拷贝构造了参数对象,其id和传入的参数不同
1 2 3 4
| > g++ main.cc -g -o main && ./main 4 5 6
|
拷贝赋值运算符
类可以控制其对象如何赋值,即通过重载赋值运算符
1 2
| Obj obj1, obj2; obj1 = obj2;
|
重载赋值运算符,即重载operate=函数,通常返回左侧对象的引用。如下,为Obj类重载赋值运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Obj { public: int count{}; Obj() { cout << "call constructor" << endl; }
Obj(int c) : count(c) { cout << "call Obj(int) constructor" << endl; }
Obj & operator=(const Obj & o) { count = o.count; cout << "call operator = " << endl; return *this; }
Obj(const Obj & obj) { cout << "call copy-constructor" << endl; this->count = obj.count; }
~Obj() { cout << "call deconstructor" << endl; } };
|
合成赋值运算符
如果一个类未定义其拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符,默认将右侧对象的每个非static成员赋予左侧对象,并返回一个指向左侧对象的引用。
析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数没有返回值,不接受参数。
何时调用
当一个对象被销毁时,自动调用:
- 变量离开作用域,如函数的局部变量和非引用类型的参数
- 一个对象被销毁时其成员对象被销毁
- 对于动态分配的对象,对指向它的指针应用delete
等等。
仍然是Obj类的例子,在其析构函数中添加输出信息,main函数中调用foo函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class Obj { public: int count{}; Obj() { } Obj(int c) : count(c) { } Obj(const Obj & obj) { this->count = obj.count; } ~Obj() { cout << "call deconstructor" << endl; } };
void fcn(const Obj * obj1, Obj obj2) { Obj obj3(*obj1), obj4(obj2); return; }
Obj global; int main() { Obj *p = new Obj(); fcn(p, global);
delete p;
return 0; }
|
输出结果:
1 2 3 4 5 6 7 8 9
| > g++ main.cc -g -o main && ./main call copy-constructor call copy-constructor call copy-constructor call deconstructor call deconstructor call deconstructor call deconstructor call deconstructor
|
值行为类
定义类VPtr,
指针行为类
定义类PPtr,
swap
动态内存管理类