《深入探索C++对象模型》笔记 Chapter2 构造函数
《深入探索C++对象模型》笔记 Chapter3 成员变量
《深入探索C++对象模型》笔记 Chapter4 成员函数
第2章 构造函数
2.1 默认构造函数
之前以为没有声明构造函数,编译器就会自动生成一个默认构造函数,然而事实上并不是这样。默认构造函数分为 trival 和 nontrival ,对于无关紧要的默认构造函数,编译器不会生成。(这里多说一句,一个类的一些构造函数以及析构函数是否 trival 等特性,可以用 type_traits 提取出来,在模板编程中十分有用,比如删除一个区间的元素,如果析构函数是 trival ,那么就不需要一个个去调用析构函数。以上内容《STL源码剖析》有详细解释)。
nontrival 默认构造函数又分为以下几种情况:
- 成员变量有默认构造函数
即使用户自己实现了构造函数,也要扩张构造函数,在用户代码执行之前,调用必需的默认构造函数。 - 继承的基类有默认构造函数
- 声明了虚函数
在默认构造函数中初始化 vptr - 虚继承
2.2 拷贝构造函数
拷贝分为两种,memberwise copy
和 bitwise copy
,前者是深拷贝,会递归拷贝成员变量,后者是浅拷贝,将内存中的每个字节拷贝过去。
诸如A a1;A a2=a1;
这样的代码,即使A没有声明拷贝构造函数,编译器也可以完成上述的行为,依赖的就是memberwise copy
和 bitwise copy
。
同默认构造函数一样,并不是没有声明拷贝构造函数而编译器需要时就会生成一个,只有判断 nontrival 才会生成。而是否 nontrival 取决于一个类是不是符合 bitwise copy 语义。判断方式和2.1列的几点类似。
2.3节有一句话说的很好,判断是否 trival 关键在于 “class 是否含有编译器内部产生的 member”。例如有虚函数,编译器会给class 添加一个 vptr 成员,此时默认构造函数和拷贝构造函数都是 nontrival 了,因为编译器要做 vptr 的设置和修改。
以及,如果成员都是POD类型, 那么没有必要自己声明拷贝构造函数,因为编译器做的已经是最优了。
2.3 NRV优化
Named Return Value 优化就是说一个函数生成并返回一个临时对象,需要调用构造函数,拷贝构造函数,临时对象的析构函数,这个过程效率很低下,编译器可以做优化。步骤如下:
- 函数添加一个参数,为欲返回对象的引用
- 在调用函数前,先创建该对象
- 进入函数后,就可以直接对这个对象的引用进行操作
- 返回值就可以为空了
于是 A a = fun();
相当于A a; fun(a);
只有一次构造函数的消耗。可以写个demo验证一下:
#include <iostream>
using namespace std;
class B{
public:
B(){
cout<< "this is constructor without parameter of B" <<endl;
}
B(int num):k(num){
cout<< "this is constructor with parameter of B" <<endl;
}
B(const B& b){
k=b.k;
cout<< "this is copy constructor of B"<<endl;
}
B& operator=(const B& b){
k=b.k;
cout << "this is operator= of B" <<endl;
return *this;
}
~B(){
cout<< "this is destroyor of B"<<endl;
}
private:
int k;
};
B func(){
B b;
return b;
}
int main(){
cout<< "result of func(b)-----------"<<endl;
B b=func();
return 0;
}
打印结果为
result of func(b)-----------
this is constructor without parameter of B
this is destroyor of B
2.4 初始化成员列表
必须使用 member initialization list 的情况:
- 成员是引用
- 成员是 const member
- 基类的构造函数有一组参数
- 成员变量的构造函数有一组参数
如果不使用 member initialization list ,而是在构造函数的函数体内进行赋值操作,那么编译器可能会产生一个临时对象,再调用赋值操作符拷贝临时对象,最后再将临时对象删除,效率会十分低下。
这里可以继续前面的demo验证一下:
#include <iostream>
using namespace std;
class B{
public:
B(){
cout<< "this is constructor without parameter of B" <<endl;
}
B(int num):k(num){
cout<< "this is constructor with parameter of B" <<endl;
}
B(const B& b){
k=b.k;
cout<< "this is copy constructor of B"<<endl;
}
B& operator=(const B& b){
k=b.k;
cout << "this is operator= of B" <<endl;
return *this;
}
~B(){
cout<< "this is destroyor of B"<<endl;
}
private:
int k;
};
class A{
public:
A(int num):k(num){}
A(){
k=2;
}
private:
B k;
};
int main(){
cout<< "result of A(1)--------------"<<endl;
A* a1=new A(1);
delete a1;
cout<< "result of A()---------------"<<endl;
A* a2=new A();
delete a2;
return 0;
}
输出结果为:
result of A(1)--------------
this is constructor with parameter of B
this is destroyor of B
result of A()---------------
this is constructor without parameter of B
this is constructor with parameter of B
this is operator= of B
this is destroyor of B
this is destroyor of B
其中,带参的构造函数使用了初始化成员列表,无参的构造函数采取了函数体内赋值,打印结果说明后者多出了临时对象的构造、拷贝和析构等步骤。
另外需注意,初始化顺序按照成员的声明顺序。
总结
不知不觉间,编译器已经帮你做到了最好。
网友评论