C++对象模型:constructor
构造函数constructor
explicit
的引入,是为了能够制止“单一参数的constructor”被当作一个conversion运算符
带有默认构造函数的对象成员
若一个类中包含对象成员,且该对象有默认构造函数,此时:
- 若该类没有构造函数
则编译器会合成一个默认构造函数,且发生在真正调用时 - 若该类有构造函数,但没有初始化对象成员
则在已有构造函数中按声明顺序生成构造对象成员的代码,且发生在构造最开始的时候
被合成的默认构造函数,只满足编译器的需要,而不是程序的需要
如初始化类对象成员是编译器的需要,初始化其他类成员变量则是程序的需要
编译器是如何避免合成多个默认构造函数的呢?如在不同文件中构造同一个类
编译器会将合成的默认构造函数,拷贝构造函数,析构函数,赋值运算符重载以inline
的方式完成,inline
函数有静态链接static linkage,不会被文件以外者看到,若函数太复杂,则会合成出一个explicit non-inline static实例
带有默认构造函数的父类
- 若子类没有构造函数,则会合成默认构造函数调用父类构造函数
- 若子类已有构造函数,则会生成调用父类构造函数的代码,且发生在初始化对象成员之前
带有虚函数的类
还有两种情况也需要合成默认构造函数
- 类中声明了虚函数
- 类的继承链中,包含虚继承关系
class Widget{
public:
virtual void flip()=0;
};
void flip(const Widget& w){ w.flip(); }
会生成虚函数表vtbl
和其指针vptr
w.flip()
的虚调用操作会被改写,以满足多态
(*w.vptr[1])(&w); // &w 为当前对象的this指针
在初始化时,编译器会给每个对象的vptr
设定初值,放置适当的vtbl
地址
此时会合成,或在已有构造函数中生成相关初始化代码
带有虚基类的类
class X{ public: int i; };
class A:public virtual X{ public: int j; };
class B:public virtual X{ public: int n; };
class C:public A,public B{ public: int m; };
void foo(const A* pa){ pa->i=1024; }
编译器无法知道X::i
的实际偏移量(经由pa
存取),因为pa
的真正类型是可改变的
编译器必须改变“执行存取操作”的代码,使X::i
可以延迟至执行期决定
cfront的做法是在子类对象中安插虚基类指针
如pa->_vbcX->i=1024;
,其中_vbcX
是编译器产生的指针,指向虚基类X
此时会合成,或在已有构造函数中生成相关代码,来初始化该指针
为什么虚函数和虚基类只有在运行时才能确定
因为编译器不解析赋值操作
拷贝构造函数copy constructor
如下场景,会调用类的拷贝构造函数
对象的显式初始化操作
X xx=x;
对象作为参数传递给函数
extern void foo(X x);
foo(xx);
对象作为函数返回值
X foo_bar(){
X xx;
return xx;
}
default memberwise initialization
若一个类没有提供显式的拷贝构造函数,则在拷贝构造时,内部以default memberwise initialization方式完成
即拷贝原生类型的成员变量,但对于成员对象,会采用递归的方式施行memberwise initialization
(在同类对象间拷贝构造时)
决定一个类是否生成默认的拷贝构造函数,在于该类是否展现出bitwise copy semantics
(不展现才会生成)
位逐次拷贝bitwise copy semantics
以下声明展现了bitwise copy semantics
class Word{
public:
Word(const char*);
~Word(){ delete[] str; }
private:
int cnt;
char* str;
};
这种情况下,不需要合成一个default copy constructor,因为上述声明展现了default copy semantics
而对象的拷贝初始化操作也就不需要以一个函数调用收场
什么时候一个类不展现bitwise copy semantics呢?
1.成员变量中包含成员对象,且对象的类型有拷贝构造函数(包括编译器生成)
2.继承自父类,且父类有拷贝构造函数(包括编译器生成)
3.类中声明了虚函数
4.类的继承链中,包含虚继承关系
前两种情况,会在编译器默认生成的拷贝构造函数中,调用相关成员对象或父类的拷贝构造函数
第三种情况,以子类对象初始化父类对象时,需要保证vptr
的操作安全,此时生成的父类的拷贝构造函数会设定vptr
的值,而不是直接从子类中拷贝,这也解释了上一章代码
ZooAnimal za= b;
za.rotate();
调用的是ZooAnimal::rotate()
第四种情况
class Raccoon:public virtual ZooAnimal{
public:
Raccoon(){}
Raccoon(int val){}
private:
};
class RedPanda:public Raccoon{
public:
RedPanda(){}
RedPanda(int val){}
private:
};
若以一个Raccoon object作为另一个Raccoon object的初值,则bitwise copy绰绰有余
若以RedPanda object作为Raccoon object的初值,编译器则需要生成拷贝构造函数,并初始化virtual base class pointer/offset
下面这种情况,编译器无法知道bitwise copy semantics是否还保持,因为无法知道Raccoon指针是指向Raccoon object还是derived class object
Raccoon *ptr;
Raccoon little_critter= *ptr;
程序转化program transformation
显式初始化
T t1(t0);
T t2= t0;
T t1= T(t0);
会转化成:先声明,再拷贝构造
T t1;
T t2;
T t3;
t1.T::T(t0);
t2.T::T(t0);
t3.T::T(t0);
参数初始化
将一个class object当作函数实参,或函数返回值
参数传递时,会以memberwise方式进行
在编译器实现计算上,有以下两种转化策略
策略一
引入临时性对象,并用拷贝构造函数初始化,再以bitwise方式传递给形参
函数形参也必须被转化,需要以引用方式声明
策略二
将实参对象实际拷贝构造在函数堆栈中
返回值初始化
cfront中采用双阶段转化
- 声明一个class object的引用
__result
- 在return前,使用返回值来拷贝构造传入的引用
对于函数指针
X (*pf)();
pf= bar;
转化为
void (*pf)(X&);
pf= bar;
在使用者层面优化
定义一个构造函数constructor,可以直接计算返回值,而不是调用拷贝构造函数
在编译器层面优化
将返回值直接使用__result
代替,称为Named Return Value(NRV)优化
NRV优化现在被认为是C++编译器义不容辞的优化操作
如下代码
class Test{
friend Test foo(double);
public:
Test(){
memset(arr, 0, 100*sizeof(double));
}
private:
double arr[100];
};
此时,编译器不会做NRV优化,因为没有拷贝构造函数,如下加上inline copy constructor
inline Test(const Test& t){
memcpy(this, &t, sizeof(test));
}
是否需要拷贝构造函数
没有任何理由要提供一个拷贝构造函数,因为编译器自动实施了最好的行为
若一个class要大量memberwise初始化操作,则提供一个copy constructor的explicit inline函数实例是合理的(在编译器提供NRV的前提下)
若使用更有效率的memset()
或memcpy()
作为拷贝构造函数的实现,则需要在class中不含任何由编译器产生的内部成员
成员的初始化
必须使用成员初始化列表的情况
1.初始化引用reference成员
2.初始化const
成员
3.调用基类构造函数,且其有一组参数
4.调用成员的构造函数,且其有一组参数
class Word{
String name_;
int cnt_;
public:
Word(){
name_= 0;
cnt_= 0;
}
};
编译器会生成一个临时对象,可能的转化如下
public:
Word(){
// 默认构造
name_.String::String();
// 临时对象
String temp= String(0);
// memberwise拷贝
name_.String::operator=(temp);
temp.String::~String();
cnt_= 0;
}
若使用列表初始化
Word::Word():name_(0){
cnt_= 0;
}
编译器可能的转化如下
Word::Word() {
// 直接调用构造函数
name_.String::String(0);
cnt_= 0;
}
成员初始化列表到底做了什么,是不是简单的函数调用
可以回答,当然不是简单的函数调用
编译器会在构造函数中生成代码,将初始化列表中的变量按在类中的声明顺序初始化
如下代码
class X{
int i;
int j;
X(int val):j(val),i(j){}
};
由于声明顺序的缘故,i
会比j
先执行,会导致问题,这种情况,GNU C++编译器g++会做出告警
建议做出如下调整
class X{
int i;
int j;
X(int val):j(val){
i= j;
}
};
能否调用成员函数,以初始化数据成员
X::X(int val):i(xfoo(val)),j(val){}
成员函数的使用是合法的,因为此时this
指针已经被构造
编译器可能的生成代码如下
X::X(int val){
i= this->xfoo(val);
j= val;
}
能否使用子类成员函数返回值,作为父类构造函数的实参
class FooBar:public X{
int fval_;
public:
int fval(){ return fval_; }
FooBar(int val):fval_(val),X(fval()){}
};
编译器可能的生成代码如下
FooBar::FooBar(int val){
X::X(this, this->fval());
fval_= val
}
可知,这不是一个好主意