发布时间:2011-12-4 21:00
分类名称:C++
Default Constructor
在编译器需要的时候,会被合成出来。(谁需要?恩,编译器。何时才需要?以下四种情况需要。)
下面的Default Constructor可以是implicit和explicit
如果Default Constructor已经被定义出来,编译器会安插初始化成员代码。
如果Default Constructor已经被定义出来,编译器会安插调用上一层base class的Default constructor。 如果类本身已经有多个构造函数,编译器会在每个构造函数内部的最开始,插入调用Base Class
带有Virtual Function的Class。本身声明的或者从父类继承而来的。(这也就隐含了Member Class如果也有虚函数,编译器肯定会为此Class合成Default constructor出来,就回到了第一条,本Class也要合成出Default constructor来调用Member Class的Default constructor) 合成出来的Default Constructor是用来初始化虚指针等。
带有一个Virtual Base Class的Class。每个编译器在实现Virtual base class上,区别比较大。微软是再插入一个指针(vbptr),来指向虚基表。
注意
Copy Constructor
拷贝的三种形式:
赋值和初始化是有区别的,赋值操作并不会调用Copy Constructor,赋值操作调用的是operator=。初始化明显的一个特征就是在初始化实例的时候,前面一定有它的定义,即在定义中完成初始化操作,如下:
X x0;
X x(x0); // 初始化,调用copy constructor,如果没有,变会被隐式合成。
X x = x0; // 初始化,调用copy constructor,如果没有,变会被隐式合成。
赋值(assignment操作为:
X x0;
X x1;
x1 = x0; // 赋值操作,调用 operator= ,如果没有,变会被隐式合成。
传参和参数返回,为何也是调用Copy Constructor?因为这两种方式中间插入了临时变量,临时变量等于在定义的时候被初始化,初始化则调用Copy Constructor。
编译器是如何拷贝? 引自原文: "如果class没有提供一个explicit copy constructor,当class object以相同class的另一个object最为初值时,其内部是以所谓的default memberwise initialization手法完成的"。
default memberwise initialization:把每一个内建的或者派生的data member从某个object拷贝到另外一个object,而member class object不会被拷贝,编译器而是以递归的方式实行memberwise initialization(注意,没有default)。
编译器默认是以bitwise copy semantics来实现default memberwise initialization拷贝的(位拷贝)。如果编译器在需要的时候,需要合成拷贝构造函数,那么bitwise copy也就不会实施的。换过来说,如果class不展现bitwise copy semantics时候,编译器会合成出来一个nontrivial的copy constructor,当然在此函数中,如整数,指针,数组等等的nonclass members也会被复制。
我在使用VS2013调试模式测试发现,即便是class展现bitwise copy semantics,编译器同样会合成一个copy constructor,这又怎么解释呢?我觉得既然是要拷贝,即便是bitwise copy,你也得有条汇编来执行吧?如果你不合成任何函数或者汇编,这拷贝如何进行?所以我觉得书中说的,只有nontrivial的copy constructor才会被合成到程序中有问题,trivial的copy constructor也会被合成,只是他们做的工作不一样。nontrival做的工作会更多,除了执行bitwise copy,还要调用成员或父类等的copy constructor,虚指针调整等。而trivial只做bitwise copy。 本人拙见,不一定正确。
Bitwise Copy是很危险的,特别是处理成员指针的时候,所以Effective C++,建议为每个C++类都声明一个copy Constructor(拷贝)和assignment operator(赋值)。
何时不展现"Bitwise Copy Semantics"?和Default constructor类似:
合成出来的copy constructor是为了调用成员的copy constructor。所以这些代码会被编译器安插进来。
编译器的"程序转化语意学"
class X
{
public:
int a;
int b;
X(): a(1), b(2)
{
}
X(const X &x1)
{
a = x1.a;
b = x1.b;
}
};
Explicit initialization(初始化)
X xx;
X x1(xx);
X x2 = xx; // initializatin, not assignment
X x3 = X(xx); // initializatin, not assignment
编译器转化为俩个阶段:
转化后变为:
X x1;
X x2;
X x3;
x1.X::X(xx);
x2.X::X(xx);
x3.X::X(xx);
下面是我使用Visual studio 2010,编译出的汇编的代码,来解释这个操作:
X xx;
0095144E lea ecx,[xx] ;xx已经被分配出内存
00951451 call X::X (95112Ch) ;此处调用Default Constructor来初始化对象。
X x1(xx);
00951456 lea eax,[xx] ;取xx地址
00951459 push eax ;将xx地址压栈,也就是作为copy constructor的参数
0095145A lea ecx,[x1] ;x1(堆栈地址,ebp-XXX)的内存被分配(相当于X x1),这里把this指针,保存到ecx中
0095145D call X::X (95109Bh) ;调用copy constructor
X x2 = xx;
00951462 lea eax,[xx] ;和x1类似
00951465 push eax
00951466 lea ecx,[x2]
00951469 call X::X (95109Bh)
X x3 = X(xx);
0095146E lea eax,[xx] ;和x1类似
00951471 push eax
00951472 lea ecx,[x3]
00951475 call X::X (95109Bh)
Argument Initialization
void foo(X x0);
X xx;
foo(xx);
编译器使用一种导入暂时object的解决办法,将暂时object交给函数,转化为:
X __temp0;
__temp0.X::X(xx);
foo(__temp0);
下面是Visual Studio 2010产生的汇编代码:
foo(xx);
0095147A sub esp,8 ; 8是Class X的大小,即在堆栈中划出__temp0的内存地址为(esp-8)
0095147D mov ecx,esp ;将__temp0的地址保存到ecx中,也就是保存this指针到ecx中。
0095147F lea eax,[xx]
00951482 push eax ;将xx地址作为拷贝构造函数的参数传递
00951483 call X::X (95109Bh) ;调用拷贝构造函数
00951488 call foo (951172h) ;为何没有push__temp0呢?因为__temp0是在堆栈中分配的,已经在堆栈中了,无需再push了。
0095148D add esp,8 ; 调整前面减去的8的esp。再加回来。
看来微软编译器也是使用暂时object来操作的。
再看看声明为void foo2(X &x0); 的汇编:
Foo2(xx);
00951490 lea eax,[xx]
00951493 push eax ;直接取xx的地址,作为Foo2的参数
00951494 call Foo2 (95114Ah)
00951499 add esp,4
看来以引用的方式调用,不会构造暂时object,也就不用调用copy constructor,省去了很多代码。这就是引用传递的好处。
Return Value Initialization
X bar()
{
X xx;
// ... do something using xx;
return xx;
}
被转化为:
void bar(X &__result)
{
X xx;
xx.X::X(); //调用 default constructor
// ... do something using xx;
__result.X::X(xx); //调用copy constructor
return;
}
X xx = bar();
被转化为:
X xx;
bar(xx);
看看visual studio 2010的汇编:
X xx = bar();
00B2158E lea eax,[xx]
00B21591 push eax ;可以看出bar以xx的地址作为函数参数。
00B21592 call bar (0B21131h)
00B21597 add esp,4
X bar()
{
X xx;
00B2144E lea ecx,[xx]
00B21451 call X::X (0B2112Ch) ;调用default constructor
return xx;
00B21456 lea eax,[xx]
00B21459 push eax
00B2145A mov ecx,dword ptr [ebp+8] ;ebp+8就是传入的xx的地址,将this保存到ecx中
00B2145D call X::X (0B2109Bh) ;调用copy constructor
}
编译器可以将其进一步优化为:
void bar(X &__result)
{
__result.X::X();
// ... do something using __result;
return;
}
此优化称作:Named Return Value(NRV).
VC 2010中Release版本中默认是打开了NRV,Release版本cl的参数有一项优化为:Maximize Speed(/O2);
关于NRV,我的博客有转载其他文章介绍。(Release汇编代码没法看了,我使用ollydbg打开后,发现vs2010优化了太多代码了,连bar这个函数都没调用到。)
Member Initialization List
为了能够顺利编译,必须使用member Initialization list的情况:
当初始化一个reference member时
当初始化一个const member时
当调用一个base class的constructor,而且它有一组参数时
当调用一个member class的constructor,而且它有一组参数时
list中的初始化顺序是由class中的members声明的顺序决定的,不是由list中的排序顺序决定的。