默认构造函数
构造函数是干啥的, 是在构造类对象的时候, 给程序员进行对象初始化操作的机会. 不仅如此, 同时也是给编译器进行对象初始化的机会. 当然程序员和编译器的扮演的角色是不一样的, 考虑的问题也是不一样的.
当程序员觉得这个类对象没有任何初始化的必要时, 他就不会特意去声明构造函数.
那么对于一个类, 当程序员没有声明任何构造函数的时候, 编译器有可能 会为该类声明一个default 构造函数. 之所以是'有可能', 是因为编译器也是很懒的, 如果他也觉得这个类没有任何初始化的必要时, 他其实也是不会真正构造default 构造函数的. 只有当他认为这个构造函数为nontrivial, 即他确实需要对类对象进行某些初始化操作时, 才会创建default 构造函数.
下面先举个例子, 从程序员的角度 , 啥时候需要构造函数,
class Foo { public: int val; Foo *pnext; };
void foo_bar()
{
// Oops: program needs bar's members zeroed out
Foo bar;
if ( bar.val || bar.pnext )
// ... do something
// ...
}
如上面的代码, 程序员期望创建对象的时候, 会生成default 构造函数对成员变量进行初始化清零, 而实际上在编译器看来, 这种活是程序员应该做的. 所以编译器不会特意构造default 构造函数来初始化类成员.
那么这段逻辑就是有bug的, 这也是新手很容易范的一个错,
Global objects are guaranteed to have their associated memory "zeroed out" at program start-up. Local objects allocated on the program stack and heap objects allocated on the free-store do not have their associated memory zeroed out; rather, the memory retains the arbitrary bit pattern of its previous use.
对于全局变量, 程序开始时会清零, 但对于在stack和heap上分配的内存, 会保持上次使用时的遗留数据, 所以如果不手工清零, 你有可能得到任意值. 而这步清零操作就是程序员应该负责在构造函数中调用的, 而不是编译器的责任.
那从编译器的角度 , 啥时候是一定需要在构造函数里面加上对象初始化操作的
有以下4种情况,
Member Class Object with Default Constructor
对于对象成员也是一个类对象, 并且该类定义了Default Constructor, 编译器必须保证该成员对象被正确的初始化, 即Default Constructor被调用.
Base Class with Default Constructor
同样编译器必须保证该base class被正确初始化
Class with a Virtual Function
这种情况有两种case,
1. The class either declares (or inherits) a virtual function
2. The class is derived from an inheritance chain in which one or more base classes are virtual
对于有虚函数的类, 编译器必须额外做下面两件事, 以保证多态能够正常工作,
1. A virtual function table (referred to as the class vtbl in the original cfront implementation) is generated and populated with the addresses of the active virtual functions for that class.
2. Within each class object, an additional pointer member (the vptr ) is synthesized to hold the address of the associated class vtbl.
说白了, 编译器必须创建虚表, 并在每个对象中添加指向虚表的指针.
Class with a Virtual Base Class
对于Virtual Base Class的实现, 各个编译器有很大的不同
what is common to each implementation is the need to make the virtual base class location within each derived class object available at runtime .
所以编译器必须保证在构造函数中确定虚基类在执行期的地址
对于上面的4种情况, 编译器必须创建Default Constructor来做特殊处理, 如果程序员已经定义了构造函数, 编译器也要在该构造函数中插入特殊处理. 除了上面4种情况, 编译器不会额外产生Default Constructor, 也不会对对象做任何额外的初始化操作, 如果想做什么, 程序员请自己搞定.
拷贝构造函数
有三种情况会使用到copy构造函数, 即以一个对象的内容作为另一个对象的初值.
对一个对象做明确的初始化操作,
class X { ... };
X x;
// explicit initialization of one class object with another
X xx = x;
另外两种情况是当object被当作参数交给某个函数, 或者当函数返回一个对象时.
同样copy构造函数用于当使用一个类对象初始化另一个类对象时, 给程序员或编译器一个做某些特殊处理的机会.
同样程序员和编译器需要考虑的问题是不同的,
对于程序员举个典型的例子来说明copy构造函数的意义,
当对象成员为指针时, 如果仅仅赋值指针本身, 会导致多个指针指向同一对象, 当一个指针把对象释放后, 其他地方就会报错.
所以程序员在copy构造函数中, 需要新创建指针成员指向对象的copy, 并将新对象的成员指针指向该copy, 这样才是完全拷贝.
当程序员不特意创建copy构造函数时, 编译器在大部分情况下默认为Bitwise Copy Semantics, 即把一个对象中的数据原封不动的按bit拷到需初始化的对象中. 所以对上面说的成员是指针的情况, 会导致两个对象的成员指针指向同一个对象实体的不合理的情况.
但是对于下面4种情况, 编译器必须要特意生成copy构造函数来保证copy的正确性,
1. When the class contains a member object of a class for which a copy constructor exists
2. When the class is derived from a base class for which a copy constructor exists
3. When the class declares one or more virtual functions
4. When the class is derived from an inheritance chain in which one or more base classes are virtual
1和2很容易理解, 编译器必须保证member object或base class中程序员所写的copy constructor被调到
3和4相对复杂一些, 其实对于这两种情况, 如果是相同类之间的对象互相初始化, 也是不用做特殊处理的, 看下面的例子
class ZooAnimal {......}
class Bear : public ZooAnimal {......}
Bear yogi;
Bear winnie = yogi; //对于这种情况, 直接拷贝就可以了
ZooAnimal franny = yogi; //但是这种情况, 编译器必须要调整franny的虚表指针, 因为yogi的vptr是指向Bear的, 要改成ZooAnimal
对于有虚函数的类, 基类与派生类之间的初始化, 编译器必须产生copy构造函数去调整vptr的值
对于有虚基类的情况, 同样也是当基类与派生类之间的初始化时, 编译器需要产生copy构造函数去仲裁虚基类对象的位置.
总结, 其实对于default构造函数和copy构造函数而言, 只有当在类成员对象或基类中有特殊的构造函数需要调用, 或类含有虚函数和虚基类需要做特殊处理外, 编译器不会对类对象做任何初始化操作, 大家不用怀疑编译背地里做了太多操作, 他也很懒的...
拷贝构造函数引起的程序转换
上面说了调用copy构造函数的3种情况, 可是他们都不是显式的调用, 所以编译器需要做程序转换, 下面就分情况说明
1. 显式的初始化
X x0;
the following three definitions each explicitly initialize its class object with x0:
void foo_bar() {
X x1( x0 );
X x2 = x0;
X x3 = x( x0 );
// ...
}
对于严谨的C++编译器, 定义就是指占用内存, 所以对于它而言, 定义和初始化式分开的
// Possible program transformation
// Pseudo C++ Code
void foo_bar() {
X x1;
X x2;
X x3;
// compiler inserted invocations
// of copy constructor for X
x1.X::X( x0 );
x2.X::X( x0 );
x3.X::X( x0 );
// ...
}
这个转换很好理解.
2. 参数的初始化
void foo( X x0 );
an invocation of the form
X xx;
// ...
foo( xx );
对于这样的参数传递, 其实编译器会做如下转换
// Pseudo C++ code
// compiler generated temporary
X __temp0;
// compiler invocation of copy constructor
__temp0.X::X ( xx );
// rewrite function call to take temporary
foo( __temp0 );
这儿会首先把xx的值通过copy构造函数初始化临时变量_temp0, 之所以需要临时变量, 是因为xx在函数外, 当进入foo函数后xx是不可见的, 所以先要保留一份xx的备份.
好, 这样进入foo函数后的第一件事,就是再把_temp0通过拷贝构造函数去初始化x0
这样传递一个参数, 调用了两遍拷贝构造函数, 挺低效的
所以, 这儿编译器可以把函数声明改写成
void foo( X& x0 );
这样的话,就可以直接把x0指向_temp0就可以了, 提高了效率
当然这边的改写策略是编译器相关的, 这儿只是一种方案, 其他的编译器方案就不介绍了.
3. 返回值初始化
X bar()
{
X xx;
// process xx ...
return xx;
}
对于返回值, 编译器的会做如下转换
// function transformation to reflect
// application of copy constructor
// Pseudo C++ Code
void
bar( X& __result )
{
X xx;
// compiler generated invocation
// of default constructor
xx.X::X();
// ... process xx
// compiler generated invocation
// of copy constructor
__result.X::X( xx );
return;
}
可见, 所谓的返回值, 其实编译器只是增加了一个result参数, 最后将xx通过拷贝构造函数初始化result
所以对于底下的调用
X xx = bar();
编译器会改写成
// note: no default constructor applied
X xx;
bar( xx );
为了提高效率, 对于返回值, 我们可以想些办法来避免拷贝构造函数的调用
a. 使用者优化
X bar( const T &y, const T &z )
{
X xx;
// ... process xx using y and z
return xx;
}
优化成,
X bar( const T &y, const T &z )
{
return X( y, z );
}
这段代码, 编译器转换为
// Pseudo C++ Code
void
bar( X &__result )
{
__result.X::X( y, z );
return;
}
这样就避免了拷贝构造函数的调用, 可以直接调用构造函数来产生返回值, 但这样最大的问题, 你必须定义新的构造函数来以y,z构造X, 会导致这种特殊用途的构造函数大量扩散. 这个方法不太靠谱
b.编译器优化
X bar()
{
X xx;
// ... process xx
return xx;
}
编译器如果比较聪明的话, 会自动对它进行优化, 直接用__result替换xx, 这样就不用后面费事调用拷贝构造函数了
void
bar( X &__result )
{
// default constructor invocation
// Pseudo C++ Code
__result.X::X();
// ... process in __result directly
return;
}
This compiler optimization, sometimes referred to as the Named Return Value (NRV) optimization.
这样的NRV优化对于需要大量调用该函数的case, 效率提高还是很明显的
但是这种优化对于逻辑比较复杂的函数, 也是无能为力的.
本文章摘自博客园,原文发布日期:2011-07-05