Inside the C++ Object Model阅读笔记:Chapter 2

来源:互联网 发布:java 泛型方法 返回值 编辑:程序博客网 时间:2024/05/20 04:09
Chapter 2 构造函数语义学
2.1 缺省构造函数语义学
一个没有缺省构造函数的类如果有成员变量,这个变量包含一个标准构造函数,则编译器会为它生成构造函数。这个构造函数将调用有标准构造函数变量的构造函数,而无视那些不需要构造函数的成员变量。
如果程序员写了标准构造函数但是没有初始化那些需要构造的成员变量,则系统自动调用这些成员变量的标准构造函数构造这些成员变量。
带有标准构造函数的基类的派生类如果没有构造函数,编译器将生成一个这样的函数。
如果派生类有构造函数,将不生成缺省构造函数。
如果一个类定义了虚函数或者继承了一个含有虚函数的基类,则编译器将生成缺省构造函数代码。这些代码将产生一个虚表并且和类绑定一个指向虚表的vptr。
对于一个有虚基类的类,在运行时将需要令使用派生类对象时能确定基类的位置(因为它们的位置再编译时不能确定)。
class X { public: int i; };
class A : public virtual X   { public: int j; };
class B : public virtual X   { public: double d; };
class C : public A, public B { public: int k; };
// cannot resolve location of pa->X::i at compile-time
void foo( const A* pa ) { pa->i = 1024; }
这时需要变换为:
// possible compiler transformation
void foo( const A* pa ) { pa->__vbcX->i = 1024; }
对__vbcX的构造由编译器在构造函数中生成相应代码。

2.2 拷贝构造函数
三种情况下一个对象通过另一个对象构造:
1---1
class X;
X x,xx=x;
2---2
void foo(X x);
...
X xx;
foo(xx);
3---3
X foo() { X xx; ... return xx; }
类的设计者可以定义拷贝构造函数(是一个以这个类本身类型为第一个参数的函数)。它可以有很多附加参数,只要有缺省值即可。
在类不提供构造函数的时候,内置数据类型将被直接拷贝,而非内置数据类型将被“依次初始化“。分两种可能,可能逐字节拷贝,也可能不这样。
不使用按位拷贝(可能通过memcpy实现)的四种情况:
1、如果一个类包含一个含有copy-constructor的对象(无论是代码包含的,还是编译器自动生成的),则不生成按位拷贝的copy-constructor。
2、类由一个包含拷贝构造函数的类(包括编译器自动生成的可能)继承。
3、类有一个或者多个虚函数。
4、类由一个继承链继承,链上又一个或者多个虚基类
原因3中之所以这么做的原因是为了保护vptr,因为可能存在通过派生类对象初始化基类对象的情况。若逐字节拷贝则可能破坏vptr。
原因4中之所以这么做的原因是虚拟机制需要在运行时定位虚基类子对象的位置。虚基类导致子对象中基类的位置不确定,从而可能出现问题。

2.3 程序中的代码变换
已知X x0,明确利用x0初始化新对象的代码:
X x1(x0);
X x2=x0;
X x3=X(x0);
这些代码将被变换为:
1、定义中的所有初始化代码都被消去。
2、取而代之的是以x0为参数的拷贝初始化函数。
对象作为函数参数传递时:
1、一种处理方式会创建一个临时对象,并且调用它的拷贝构造函数用实际参数初始化它。与此同时,函数将改为对临时对象的引用。
2、另外的方法是将参数直接复制到函数的栈上相应位置。
(附注:只有使用指针或者引用,多态性才得到体现,因此这里不考虑多态性。)
本地对象作为返回值时:
1、Stroustrup的方法是:首先在函数的参数中增加一个对返回值类型的引用,然后在函数返回之前将这个引用的(临时)对象用返回值初始化。这时,原始的返回参数将变换为void,而返回值是作为引用类型的第一个参数。
e.g.
X foo(...);
被处理为:
void foo(X&, ...);
(附注:C++代码最早是被编译为C代码的)
2、用户层优化:定义一个直接计算函数返回结果的构造函数,然后通过引用返回这个对象。这样就不需要再进行一次拷贝了。但是这样可能导致特定构造函数的膨胀。
e.g.
X foo(...);
被处理为:
void foo(X&, ...)
{return X(...);}
3、编译层优化(NRV优化):如果不通过临时对象来传递这个返回值,而是将真实对象返回,这就是NRV。
e.g.
X xx=foo();
被优化为:
X xx;
foo(&xx);
这样可以减少一次复制的负担。
(附注:根据inside所说,这个NRV需要一个拷贝构造函数,但是我实验了似乎不需要,g++ 4.2.3编译)
对NRV有一些指责:首先,它的实现并不清晰,依赖于编译器;其次,函数变得复杂的情况下,这种优化难于进行;最后,它可能破坏函数的对称性(不太理解)。
(附注:根据Inside书所说,NRV尚处于争议阶段,不过Inside是一本老书了,不知道NRV现在的情况如何)
关于是否应该直接给定一个拷贝构造函数,一般说,如果需要NRV优化,可以建立一个拷贝构造函数(附注:根据上面的实验,怀疑这一点)。使用memcpy可以加速拷贝,不过要注意vptr的存在可能因此被破坏。

2.4 成员初始化列表
必需使用成员初始化列表的情况:
1、初始化引用
2、初始化常量
3、向基类的构造函数传递参数
还有一种情况,就是使用了类对象作为成员变量,如果在类内用operator=,需要一个临时变量,这时效率没有初始化成员列表高。
编译时,编译器在真正接触构造函数代码之前先访问成员初始化列表。值得注意的一点是,通过这种方法构造的成员,构造的顺序不是按照列表中的顺序,而是按照类中定义的顺序进行构造。
(附注:Stallman在书的这里说了一句俏皮话。)
成员初始化将在所有构造代码之前进行,并且可以调用函数进行计算,但是并不推荐,因为函数的调用可能依赖于某些没有初始化的参数。

最后附注:CSDN论坛上iu_81的提示(http://topic.csdn.net/u/20080525/13/cd13a44f-778d-46f2-a3f4-e9444642d3c6.html)
一般的编译器都可以做到返回值优化(Return Value Optimization, RVO),即直接把返回值建立在函数返回值要初始化或者赋值的对象上。这个优化的目的是消除函数返回时的临时对象的创建以及拷贝。
有名返回值优化(Named Return Value Optimization, NRVO)。NRVO 指的是:如果函数中 return 的对象是一个局部变量且为非 const 或 volatile,编译器可以把这个局部变量直接建立在函数返回值要初始化或者赋值的对象上,因为它优化的对象是函数返回的局部变量,而这个局部变量是有名字的。
原创粉丝点击