构造函数语意学
- 构造函数语意学
- Default Constructor的构造操作
- "带有Default Constructor"的Member Class Object
- "带有Default Constructor"的Base Class
- "带有一个Virtual Function"的Class
- "带有一个Virtual Base Function"的Class
- Copy Constructor的构造操作
- Default Memberwise Initialization
- Bitwise Copy Semantics(位逐次拷贝)
- 不要Bitwise Copy Semantics!
- 重新设定Virtual Table的指针
- 处理Virtual Base Class Subobject
- 程序转化语意学
- 显式的初始化操作
- 参数的初始化
- 返回值的初始化
- 在使用者层面做优化
- 在编译器层面做优化
- Copy Constructor:要还是不要
- 成员们的初始化队伍
- NRV在构造函数的行为
- initialization list和初始化顺序不同的影响
构造函数语意学
关于C++,编译器背着程序员做了太多事情,我们一个一个来看
Default Constructor的构造操作
C++ Annotated Reference Manual(ARM)[ELLIS90]这么说“Default Constructor在需要的时候被编译器产生出来”
什么时候需要?
有一段代码是书上的可以参考一下理解
class Foo {
public:
int val;
int l;
};
int main() {
Foo bar;
if (bar.val || bar.l) {
// do something
}
return 0;
}
此时要注意他调用了bar.val || bar.l,那么就得在他的构造函数里面对这两个变量进行初始化,可是明显不成形成一个Default Constructor去初始化他们,这是类的设计者的职责。
类中变量编译器永远不会自动用构造函数初始化,这一点必须由程序设计者完成
那么什么时候才会有一个Default Constructor呢?当编译器需要他的时候!,因为Default Constructor只指向编译器所需的动作,也就是说,即使形成了一个Default Constructor,他也不会把这两个变量进行初始化。
下面来看看四种编译器自动生成Default Constructor的情况
"带有Default Constructor"的Member Class Object
如果一个类没有任何constructor,但他包含一个member object(内部类对象),而后者有Default Constructor,那么这个class的implicit Default Constructor就是”nontrivial“,编译器就需要为该class合成出一个Default Constructor,不过这个合成操作只有在constructor真正需要被调用时才会发生。
class Foo {
public:
Foo(); // 这是组合,不是继承
...
};
class Bar {
public:
Foo foo;
char* str;
};
void foo_bar() {
Bar bar; // Bar::foo必须在此初始化
...
}
此时合成的Default Constructor必须能够调用Foo的Default Constructor来处理Bar::foo,但他并不产生任何代码来初始化Bar::str。将Bar::foo是编译器的责任,将Bar::str是设计者的责任
自动生成的Default Constructor是这样的
inline Bar::Bar() {
// C++伪码
foo.Foo::Foo();
}
如果我们要初始化这个变量,得自己写一个
inline Bar::Bar() { str = 0; }
现在有两个,怎么办?编译器会把他们进行合成的
inline Bar::Bar() {
// C++伪码
foo.Foo::Foo();
str = 0;
}
inline,在C++各个不同的编译模块中,编译器如何避免合成出多个Default Constructor呢?解决方法就是把合成的构造函数都以inline完成,不会被文件外者看到,如果函数太复杂,就会做成一个non-inline static实例
如果有多个类A,B,C,就按照顺序进行合成Default Constructor
inline Bar::Bar() {
// C++伪码
a.A::A();
b.B::B();
c.C::C();
str = 0;
}
"带有Default Constructor"的Base Class
类似的道理,如果一个没有任何constructor的class派生自一个带有Default Constructor的base class,那么这个derived class的Default Constructor会被视为nontrivial,并因此需要被合成出来,他将调用上一层base class 的Default Constructor
此时是在继承
"带有一个Virtual Function"的Class
class声明(或继承)一个virtual function。
class Widget {
public:
virtual void flip() = 0;
};
void flip(const Widget& widget) { widget.flip(); }
// 其中Bell和Whistle都派生自Widget
void foo() {
Bell b;
Whistle w;
flip(b);
flip(w);
}
我们知道,会发生以下两件事情:
- 一个虚函数表会被编译器产生出来内放class的虚函数
- 在每一个类对象中,都会产生一个额外的虚函数指针指向虚函数表
此外,widget.flip()的虚拟调用操作很明显会被重新改写,以使用widget的vptr和vtbl中断 flip条目
// 1表示flip在vtbl中的固定索引
// &widget代表要交给“被调用的某个flip()函数实例”的this指针
(*widget.vptr[1])(&widget)
为了让这个机制生效,编译器必须为每一个Widget(或其派生类)对象的vptr设定初值,放置适当的vtbl地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这样的事情(这个我们后期再讲)。对于那些未声明constructor的class,编译器会为他们合成一个Default Constructor,以便正确的初始化每一个class object的vptr
此时我们知道了为什么拥有虚函数的类的对象会有一个额外的指针(vptr)了,因为编译器默认放在构造函数里面的,所以要求这个类必须有构造函数
"带有一个Virtual Base Function"的Class
虚基类的实现法在不同的编译器之间有较大的差异,然而,每一种实现法的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够于执行器准备妥当。
Copy Constructor的构造操作
一般一个类中会显式定义一个拷贝构造函数
X::X(const X& x);
Default Memberwise Initialization
如果没有提供呢?当类对象以“相同class的另一个object”作为初值,其内部是以所谓的Default Memberwise Initialization完成的,也就是把每一个内奸的或派生的data member的值,从某个object拷贝一份到另一个object身上(但他不会拷贝member class object),如下
class String {
public:
// 没有explicit copy constructor
private:
char* str;
int len;
};
String nuon("book");
String verb = nuon;
其是怎么实现的呢?
verb.str = nuon.str;
verb.len = nuon.len;
是的,普通的member是这样实施的,可是上面有个特殊情况(我加粗标出来了),当一个类中有一个内部子类对象要拷贝的时候怎么办?还是得靠默认拷贝构造函数,那这个又是怎么产生的呢?当需要的时候,即Bitwise Copy Semantics
Bitwise Copy Semantics(位逐次拷贝)
如果里面没有内部子类对象,用Default Memberwise Initialization即可,如果有呢?
class Word {
public:
Word(const String&);
~Word();
private:
int cnt;
String str;
};
其中String类显示声明了一个拷贝构造
class String {
public:
String(const char*);
String(const String&);
~String():
};
这种情况下,编译器必须合成一个copy constructor,以便调用member class String object的copy constructor
inlinc Word::Word(const Word& wd) {
str.String::String(wd.str);
cnt = wd.cnt;
}
有一点注意,在这被放出来的拷贝构造中,整数、指针、数组钉钉的nonclass member也会被复制
和默认构造函数有些像蛤
不要Bitwise Copy Semantics!
什么时候一个class不展现出“Bitwise Copy Semantics”呢?
- 一个类中内涵一个类对象而后者的类声明中有一个拷贝构造
- 一个类继承一个积累而后者存在一个拷贝构造
- 一个类声明了一个或多个虚函数
- 一个类派生自一个继承串链,其中有一个或多个虚基类
重新设定Virtual Table的指针
编译期间两个程序扩张操作
- 增加vtbl,内涵虚函数地址
- 一个vptr,安插在每一个class object中
当这种操作是允许的
class A {}; // 拥有虚函数的类
class B: A {};
B b1;
B b2 = b1;
其中b1和b2的vptr都指向的是B的虚函数表,这是毋庸置疑的,但是
A a = b1;
这种情况下会导致a的vptr指向的是B的虚函数表,就是说a对象可以调用B类中的函数,这是不可能的!
在一般情况下,合成出来的基类拷贝构造函数会显式设定对象的vptr指向基类的虚函数表,而不是直接从右手边的类对象中将其vptr现值拷贝过来。
处理Virtual Base Class Subobject
虚基类,后面讨论
程序转化语意学
#include "X.h"
X foo() {
X xx;
return xx;
}
其中返回的是一个X类的对象,但是此时是调用了拷贝构造函数的
显式的初始化操作
x1(x0);
// 等于
x1.X::X(x0);
// 即调用
X::X(const X& xx);
参数的初始化
C++standard说,当把一个类对象当作参数传给一个函数(或一个函数的返回值),相当于以下悉尼港是的初始化操作
// xx:形参或返回值
// arg:代表真正的参数值
X xx = arg;
此时在函数中也会重新产生一个临时对象,一般情况下有两种方法对付他
- 其中class X声明了一个destructor,他会在函数完成之后被调用,析构临时对象
- 第二种情况就是传入的时候或者传出的时候是一个右值(没有内存空间的),也就是所谓的移动拷贝构造
返回值的初始化
同上,不做概述
在使用者层面做优化
如
class X {};
X add(const T& x, const T& y) {
return X(x, y);
}
结果直接被计算了出来,而不是拷贝构造得到的(这种方法提高了效率)
在编译器层面做优化
其实就是偷偷做拷贝构造的意思
X bar() {
X xx;
return xx;
}
// 相当于
X bar() {
result.X::X();
_result.X::X(result);
return _result;
result.X:~X();
}
编译器偷偷做了一个拷贝构造,被称为NRV(named value)优化,真正好不好,视情况而定
Copy Constructor:要还是不要
如果没有内部类对象,不用是最好的,如果需要放入函数作为参数或者返回值,让编译器做NRV优化,最好还是写一个拷贝构造,视情况而定。
成员们的初始化队伍
在以下几种情况,为了让程序能够顺利编译,最好使用member initialization
- 当初始化一个reference member时
- 当初始化一个const member时
- 当调用一个base class的constructor,而他拥有一组参数
- 当调用一个member class的constructor,而他拥有一组参数时
NRV在构造函数的行为
如下
class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};
我们大多数人对于无参构造函数是这样写的,其实这两个是等价的,只不过下面这个比第一种还不堪
Word() {
}
以第一种为例,根据编译器的NRV原则,其在函数体内部做了如下动作
Word() {
// 调用string的默认构造函数
_name.String::String();
// 产生临时性对象
String temp = String(0);
// 移动赋值
_name.String::operator==(temp);
// 析构临时对象
temp.String::~String();
_cnt = 0;
}
是不是很麻烦?所以我们这么写
Word(): _name(0), _cnt(0) {
}
// 相当于
Word() {
_name.String::String(0);
_cnt = 0;
}
效率提高了好多吧!
都怪NRV!
initialization list和初始化顺序不同的影响
class X {
int i, j;
public:
X(int val): j(val), i(j) {
}
...
};
看着毫无问题?但是,由于声明顺序的缘故,initialization list中的i(j)其实比j(val)更早执行,但因为j一开始没有初值,所以i(j)的执行结果导致i无法预知其值!
臭虫!目前只有g++,GNU C++编译器可以做到发出一个警告
其实可以这么写来改变初始化顺序
// 此时j比i先初始化
X:X(int val): j(val) {
i = j;
}