c++对象模型大总结:第1-4章、对象初探与构造函数
深度探索c++对象模型大总结、上
--第一~四章
作者:July、吴黎明。
声明:版权所有,侵权必究。
二零一一年三月十七日。
说明:
本份资料主要是参考侯捷先生译的《深度探索c++对象模型》一书,原创而成。但文中章节并不与书上章节一一对应。初稿由吴黎明大哥制作,而后我稍加修改、修饰,最终整理而成了此份珍贵的资料。版权所有,严禁转载。
引言:
许久之前,看过侯捷先生翻译的一本书,叫做《深度探索c++对象模型》,作者是Lippman大师。曾经看了一遍又一遍,也有想过要做一份笔记,但后来深感整理此类东西东西比较费力,就作罢了。
今群内的一位朋友,黎明大哥把这份资料共享出来了,我细细一看,发现整的非常好。我征得黎明大哥的同意,便把这份资料发布出来,以供各位学习、享用。文章版权主要归属于黎明大哥,我只做了为数不多的编辑、整理与修饰。
声明:
黎明大哥主页:http://hi.csdn.net/wuliming_sc。
版权所有,严禁任何人,任何网站,在未经本人书面许可的情况下,转载。否则,尽我一切,永久追究法律责任的权利。
最后,我说一句,基础真的非常重要,我见识到很多的初学者基础都很不牢,至少,很不够彻底。
作者:July、吴黎明。
二零一一年三月十七日声明。
第一章、C++中对象的大小
需要多少内存才能表现一个class object?一般而言要有:
1、其nonstatic data members的总和大小;
2、加上任何由于字节对齐需要而填充上去的空间(可能存在与members之间,也可能存在于集合体边界);
3、加上为了支持virtual而由内部产生的任何额外负担。
C++中实际对象的大小,不同编译器的实现是不一样的,以下仅讨论.net2008,其他编译的可能出现的结果以下也做了分析和猜测。在反推不同编译器实现的C++对象的大小时,对齐是一个很重要也容易被遗忘的问题。
1、class A{};
看上去一个空的类A事实上并不是空的,它有一个隐含的1byte,那是被编译器安插进去的一个char,这使得这个class的两个objects得以在内存中配置独一无二的地址:
A a,b;
If ( &a == &b ) cerr<<”yipes!”<<endl;
2、class B:public virtual A{};
B类是对A类的虚继承,B中一般会有指向A的实例的指针,在IA-32下为4bytes。这里不同编译器的实现差别很大,有的编译器在B的对象中也会保留A类的那个隐含的char,于是就有1+4=5个bytes,再考虑对齐,有些编译器产生的结果为8bytes,但是在.net2008中优化过了,不会有A中的char,也就不需要对齐,只有4bytes大小。
3、class C:public virtual A{};
同上
4、class D:public B,public C{};
D为8,如果编译器不优化A中的char就会有1(A)+8(B)+8(C)-4(B对A虚继承)-4(C对A虚继承)+3(对齐)=12bytes。
5、class E
{
int i;
};
很明显4bytes。
6、class F
{
double d;
};
很明显8bytes
7、class G
{
double num;
char in;
};
8bytes对齐,所以是8(double)+4(int)+4(对齐)=16
8、class H
{
int num;
double in;
};
同上
9、class I
{
int num;
double in;
public:
virtual ~I(){};
};
8(double)+4(int)+4(对齐)+4(vptr)+4(对齐)=24
10、class J
{
double num;
int in;
public:
virtual ~J(){};
};
同上8(double)+4(int)+4(对齐)+4(vptr)+4(对齐)=24
11、class K
{
int i;
int k;
public:
virtual ~K(){};
};
4(int)+4(int)+4(vptr)=12
12、class L
{
int i;
int j;
L(){};
public:
float ll(int i)
{
return 0.0;
}
static int hhh(int i)
{
return 0.0;
}
virtual ~L(){};
virtual ji(){};
};
虚函数表的指针vptr,只有类中出现虚函数才会出现,它指向虚函数表,所有虚函数的地址存放在此表中。4(int)+4(int)+4(vptr)=12从中看出,不管有多少虚函数,大小不变,因为类中只保存虚函数表。不管成员函数有多少,类大小也不变,因为他们不保存在对象中,无论是否是静态。
ok,下面,看一个例子,我想经过上述的讲解,你对以下的结果应该不会感到奇怪:
测试环境:windows xp + vc 6.0.
参考资料:
《深度探索 C++对象模型》P83~88
二零一一年三月十七日。
第二章、成员初始化表
#include <string>
class Account {
public:
Account();
Account( const char*, double=0.0 );
Account( const string&, double=0.0 );
Account( const Account& );
// ...
private:
// ...
};
//注意:构造函数的初始化列表只在构造函数的定义中指定,而不在声明中指定
inline Account::Account( const char* name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = get_unique_acct_nmbr();
}
成员初始化列表跟在构造函数的原型后,以冒号开头。成员名是被指定的,后面是括在括号中的初始值,类似于函数调用的语法。如果成员是类对象,则初始值变成被传递给适当的构造函数的实参,该构造函数然后被应用在成员类对象上。
在我们的例子中,name被传递给应用在_name上的string构造函数,_balance 用参数opening_bal 初始化。类似地,下面是另一个双参数Account构造函数:
inline Account::Account( const string& name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = get_unique_acct_nmbr();
}
在这种情况下,string的拷贝构造函数被调用,把成员类对象_name 初始化成string 参数name。
C++新手关注的一个常见问题是,使用初始化列表和在构造函数内使用数据成员的赋值之间有什么区别。例如,以下代码:
inline Account::Account( const char *name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = get_unique_acct_nmbr();
}
和
inline Account::Account( const char *name, double opening_bal )
{
_name = name;
_balance = opening_bal;
_acct_nmbr = get_unique_acct_nmbr();
}
它们的区别是什么?
两种实现的最终结果是一样的。在两个构造函数调用的结束处,三个成员都含有相同的值,区别是成员初始化表只提供该类数据成员的初始化。在构造函数体内对数据成员设置值是一个赋值操作。区别的重要性取决于数据成员的类型。
在构造函数初始化列表中没有显示提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中,它们被初始化为0(《c++ primer》4th p388)。
在概念上,我们可以认为构造函数的执行过程被分成两个阶段:隐式或显式初始化阶段,以及一般的计算阶段:
初始化阶段可以是显式的或隐式的,取决于是否存在成员初始化表。隐式初始化阶段按照声明的顺序依次调用所有基类的缺省构造函数,然后是所有成员类对象的缺省构造函数。
计算阶段由构造函数体内的所有语句构成。在计算阶段中,数据成员的设置被认为是赋值,而不是初始化。没有清楚地认识到这个区别是程序错误和低效的常见源泉。
例如,当我们写如下代码:
inline Account::Account()
{
_name = "";
_balance = 0.0;
_acct_nmbr = 0;
}
则初始化阶段是隐式的,在构造函数体被执行之前,先调用与_name相关联的缺省string构造函数,这意味着把空串赋给_name的赋值操作是没有必要的。
对于类对象,在初始化和赋值之间的区别是巨大的。成员类对象应该总是在成员初始化表中被初始化,而不是在构造函数体内被赋值。缺省Account构造函数的更正确的实现如下:
inline Account::Account() : _name( string() )
{
_balance = 0.0;
_acct_nmbr = 0;
}
它之所以更正确,是因为我们已经去掉了在构造函数体内不必要的对_name的赋值。但是,对于缺省构造函数的显式调用也是不必要的,下面是更紧凑但却等价的实现:
inline Account::Account()
{
_balance = 0.0;
_acct_nmbr = 0;
}
剩下的问题是,对于两个被声明为内置类型的数据成员,其初始化情况如何?例如,用成员初始化表和在构造函数体内初始化_balance 是否等价?回答是不。对于非类数据成员的初始化或赋值,除了两个例外,两者在结果和性能上都是等价的。更受欢迎的实现是用成员切始化表:
// 更受欢迎的初始化风格
inline Account::Account() : _balanae( 0.0 ), _acct_nmbr( 0 )
{ }
两个例外是指任何类型的const 和引用数据成员。const 和引用数据成员也必须是在成员初始化表中被初始化,否则,就会产生编译时刻错误。例如,下列构造函数的实现将导致编译时刻错误:
class ConstRef {
public:
ConstRef( int ii );
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef( int ii )
{ // 赋值
i = ii; // ok
ci = ii; // 错误: 不能给一个 const 赋值
ri = i; // 错误 ri 没有被初始化
}
当构造函数体开始执行时,所有const 和引用的初始化必须都已经发生。因此,只有将它们在成员初始化表中指定这才有可能。正确的实现如下:
// ok: 初始化引用和 const
ConstRef::ConstRef( int ii ):ci( ii ), ri( i )
{ i = ii; }
每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类中被声明的顺序决定的。例如,给出下面的Account 数据成员的声明顺序:
class Account {
public:
// ...
private:
unsigned int _acct_nmbr;
double _balance;
string _name;
};
下面的缺省构造函数:
inline Account::
Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )
{}
的初始化顺序为_acct_nmbr、_balance,然后是_name。但是在初始化表中出现(或者在被隐式初始化的成员类对象中)的成员,总是在构造函数体内成员的赋值之前被初始化。例如,在下面的构造函数中:
inline Account::Account( const char *name, double bal )
: _name( name ), _balance( bal )
{
_acct_nmbr = get_unique_acct_nmbr();
}
初始化的顺序是_balance、_name,然后是_acct_nmbr。
由于这种“实际的初始化顺序”与“初始化表内的顺序”之间的明显不一致,有可能导致以下难于发现的错误,当用一个类成员初始化另一个时:
class X {
int i;
int j;
public:
// 喔! 你看到问题了吗?
X( int val ): j( val ), i( j )
{}
// ...
};
尽管看起来j 好像是用val 初始化的,而且发生在它被用来初始化i之前,但实际上是i先被初始化的,因此它是用一个还没有被初始化的j 初始化的。我们的建议是,把“用一个成员对另一个成员进行初始化(如果你真的认为有必要)”的代码放到构造函数体内。
补充:
为了让你的程序能够顺利编译,在下面4种情况下,必须使用member initialization list:
1、当初始化一个reference member时;
2、当初始化一个const member时;
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对它们的赋值,这样是不被允许的。
3、当调用一个base class的constructor,而它拥有一组参数时;
4、当调用一个member class的constructor,而它拥有一组参数时;
我们知道类的对象的初始化其实就是调用它的构造函数完成,如果没有写构造函数,编译器会为你默认生成一个。如果你自定义了带参数的构造函数,那么编译器将不生成默认构造函数。这样这个类的对象的初始化必须有参数。如果这样的类的对象来做另外某个类的成员,那么为了初始化这个成员,你必须为这个类的对象的构造函数传递一个参数。同样,如果你在包含它的这个类的构造函数里用“=”,其实是为这个对象“赋值”而非“初始化”它。所以一个类里的所有构造函数都是有参数的,那么这样的类如果做为别的类的成员变量,你必须显式的初始化它,你也只能通过成员初始化列表来完成初始化。
参考材料:
《c++ primer》3th、4th
《深度探索C++对象模型》
第三章、缺省构造函数
首先看看下面一段程序代码:
class Foo {
public:
int val;
Foo *pnext;
};
void foo_bar()
{
Foo bar;
if( bar.val || bar.pnext )//程序假设bar的两个成员都已被清零
//... do something
//...
}
在上面这个例子中,正确的程序语意是要求Foo有一个缺省构造函数,可以将两个members初始化为0。但是实际情况却不是这样的。程序如果有需要,那是设计程序的人的责任;只有当编译器需要的时候,才会合成出一个缺省构造函数,并且合成出来的构造函数只执行编译器所需的行动。也就是说,即使有需要为class Foo合成一个缺省构造函数,那个constructor也不会将两个数据成员 val和pnext初始化为0。
C++ Standard[ISO-C++95]的Section12.1上这么说:
对于Class X,如果没有任何用户声明的构造函数,那么会有一个缺省构造函数被暗中声明出来……一个被暗中声明出来的缺省构造函数将是一个trivial(浅薄无能,没啥用的) constructor……
C++ Standard然后开始一一叙述在什么样的情况下这个暗中声明出来的缺省构造函数会被视为trivial。一个nontrivial缺省构造函数就是编译器需要的那种,必要的话会由编译器合成出来。下面将分别讨论nontrivial 缺省构造函数的四种情况。
带有Default Constructor的Member class object
如果一个类没有任何构造函数,但是它内含一个member object,而后者有一个缺省构造函数,那么这个类的implicit default constructor就是“nontrivial”,编译器需要为此合成一个default constructor。不过这个合成操作只有在constructor真正需要被调用时才会发生。举个例子,在下面的程序片段中,编译器为class Bar合成一个default constructor:
class Foo { public: Foo(); Foo(int)... };
class Bar { public: Foo foo; char *str; };//内含Foo的对象
void foo_bar()
{
Bar bar;
if( str ){ … }
//...
}
被合成的Bar default constructor内含必要的代码,能够调用class Foo的缺省构造函数来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str。将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。被合成出来的default constructor看起来可能像这样:
inline Bar::Bar()
{
//C++伪码
foo.Foo::Foo();
}
假设程序员经由下面的default constructor提供了str的初始化操作:
Bar::Bar() { str = 0; }
现在程序的需求获得满足了,但是编译器还需要初始化member object foo。由于缺省构造函数已经被明确定义出来,编译器没办法合成第二个,于是编译器采取如下行动:“如果class A内含一个或一个以上的member class object,那么class A的每一个constructor必须调用每一个member classes的缺省构造函数”。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code在被执行之前,先调用必要的缺省构造函数。延续前一个例子,扩张后得constructors可能像这样:
Bar::Bar() { //C++伪码
foo.Foo::Foo();
str = 0;
}
如果有多个class member objects都要求constructor初始化操作,将如何呢?C++语言要求以“members objects在class中的声明次序”来调用各个constructors。这一点由编译器完成,它为每一个constructor安插程序代码,以“member声明次序”调用每一个member所关联的default constructors。这些代码安插explicit user code之前。
带有Default Constructor的Base class
类似的道理,如果一个没有任何构造函数的类派生自一个带有缺省构造函数的基类,那么这个派生类的缺省构造函数被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据它们的声明次序)。对一个后继派生的class而言,这个合成的constructor和一个“被明确提供的default construnctor”没什么差异。
如果设计者提供多个构造函数,但是其中却没有缺省构造函数呢?编译器会扩张现有的每一个构造函数,将“用以调用所有必要之default constructors”的程序代码加进去。它不会合成一个新的缺省构造函数,这是因为其它由用户提供的缺省构造函数存在的原因。如果同时亦存在着带有缺省构造函数的member class objects,那些缺省构造函数也会被调用—在所有基类构造函数都被调用之后。
带有一个虚函数的class
另外有两种情况,需要合成缺省构造函数:
class 声明(或继承)一个虚函数
class派生自一个继承串链,其中有一个或更多的虚基类
不管那一种情况,由于缺乏由user声明的构造函数,编译器会详细记录合成一个default constructor的必要信息。以下面这个程序片段为例:
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);
}
下面两个扩张操作会在编译期间发生:
一个虚函数表会被编译器产生出来,内放类的虚函数地址
在每一个对象中,一个额外的vptr(虚函数表指针)会被编译器合成出来,内含相关的虚函数表的地址
此外,widget.flip()会被重新改写,以使用widget的vptr和vtbl中的flip()条目:
( *widget.vptr[1] )( &widget )
// 1表示flip()在虚表中的固定索引
// &widget代表要交给“被调用的某个flip()函数实体”的this指针
为了让这个机制发挥功效,编译器必须为每一个Widget或其派生类的对象的vptr设定初值,放置适当的虚表地址。对于class所定义的每一个构造函数,编译器会安插一些代码来做这些事情。对于那些未声明任何构造函数的类,编译器会为它们合成一个缺省构造函数,以便正确地初始化每一个类对象的vptr。
带有一个虚基类的class
虚基类的实现法则在不同的编译器之间有很大的差别。然而,每一种实现方法的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够于执行期准备妥当。例如下面这段程序代码中:
class X { public: int i; };
class A : public virtual X { public : int j; };
class B : public virtual X { public : int double d; };
class C : public A, public B { public : int k; };
//无法在编译时期决定出pa->X::i的位置
void foo( const A* pa ){ pa->i =1024; }
main()
{
foo( new A );
foo( new C );
//...
}
编译器无法固定住foo()之中“经由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变“执行存取操作”的那些代码,使X::i可以延迟到执行期才决定下来。原先cfront的做法是靠“在派生类对象的每一个虚基类中安插一个指针”来完成。所有“经由引用或指针来存取一个虚基类”的操作都可以通过相关指针完成。在这个例子中,foo()可以被改写如下,以符合这样的实现策略:
void foo( const A* pa ){ pa->__vbcX->i = 1024; }
其中__vbcX表示编译器所产生的指针,指向虚基类X。__vbcX(或编译器所产生的某个什么东西)是在类对象建构期间被完成的。对于类所定义的每一个构造函数,编译器会安插那些“允许每一个虚基类的执行期存取操作”的代码。如果类没有声明任何构造函数,编译器必须为它合成一个缺省构造函数。
总结
在以上4种情况下,会导致编译器必须为未声明构造函数的类合成一个缺省构造函数。C++ Standard把那些合成物称为implicit nontrivial default constructors。被合成出来的constructor只能满足编译器(而非程序)的需要。至于没有存在以上4种情况而又没有声明任何构造函数的类,我们说它们拥有的是implicit tirvial default constructors,它们实际上并不会被合成出来。
在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其它的nonstatic data member,如整数、整数指针、整数数组等等都不会被初始化。这些初始化对程序而言或许有必要,但对编译器则并非必要。如果程序需要一个“把某指针设为0”的缺省构造函数,那么提供它的人应该是程序员。
C++新手一般有两个常见的误解:
1、任何类如果没有定义缺省构造函数,就回被合成出一个来;
2、编译器合成出来的缺省构造函数会明确设定“class内每一个data member的默认值”。
如上面所阐述的,以上没有一个是正确的。
参考资料:
《深度探索C++对象模型》P39~47
第四章、拷贝构造函数
有三种情况,会以一个object的内容作为另一个class object的初值。最明显的一种情况当然就是对一个object做明确得初始化操作,像这样:
class X{…};
X x;
//明确地以一个object的内容作为另一个class object的初值
X xx = x;
另两种情况是当object被当做参数交给某个函数时,以及当函数传回一个class object时。
假设class设计者明确定义了一个拷贝构造函数,像下面这样:
X::X( const X& x );
Y::Y( const Y& y, int = 0 );
那么在大部分情况下,当一个class object以另一个同类实体作为初值时,上述的拷贝构造函数就会被调用。这可能会导致一些临时性对象的产生或程序代码的蜕变。
Default Memberwise Initialization
如果类没有提供一个explicit copy constructor又当如何?当class object以相同类的另外一个object作为初值时,其内部是以所谓的default memberwise initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某一个对象拷贝一份到另外一个对象身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。例 如,考虑下面这个类声明:
class String{
public:
//...
private:
char *str;
int len;
};
一个String object的default memberwise initialization发生在这种情况之下:
String noun(“book”);
String verb = noun;
其完成方式就好像个别设定每一个members一样:
//语意相等
verb.str = noun.str;
verb.len = noun.len;
如果一个String object被声明为另一个class的member,像下面这样:
class Word{
public:
//...
private:
int _occurs;
String _word;
};
那么一个Word object的default memberwise initialization会拷贝其内建的member _occurs,然后再于String member object _word身上递归实施memberwise initialization。
就像缺省构造函数一样,C++ Standard上说,如果类没有声明一个拷贝构造函数,就会有隐含的声明(implicitly declared)或隐含的定义(implicitly defined)出现。和以前一样,C++ Standard把拷贝构造函数区分为trivial和nontrivial两种。只有nontrivial的实体才会被合成于程序之中。决定一个拷贝构造函数是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。
Bitwise Copy Semantics(位逐次拷贝)
在下面的程序片段中:
#include "word.h"
Word noun("book");
void foo()
{
Word verb = noun;
//...
}
很明显verb是根据noun来初始化。但是在尚未看过Word类的声明之前,我们不可能预测这个初始化操作的程序行为。如果Word的设计者定义了一个拷贝构造函数,verb的初始化操作就会调用它。但是如果该类没有定义explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就得视该class是否展现“bitwise copy semantics”而定。举个例子,已知下面得class Word声明:
class Word{
public:
Word( const char * );
~Word(){ delete[] str; }
//...
private:
int cnt;
char *str;
};
这种情况下,并不需要合成一个缺省拷贝构造函数,因为上述声明展现了“default copy semantics”,而verb的初始化操作也就不需要以一个函数调用收场(当然,该类的定义存在着严重的缺陷)。然而,如果class object是这样声明:
class Word{
public:
Word( const String& );
~Word(){ }
//...
private:
int cnt;
String str;
};
其中String声明了一个explicit copy constructor:
class String{
public:
String( const char * );
String( const String & );
//...
};
在这个情况下,编译器必须合成一个拷贝构造函数以便调用member class string object的拷贝构造函数:
//C++伪码
Inline Word::Word( const Word& wd )
{
str.String::String(wd.str);
cnt = wd.cnt;
}
有一点需要特别注意:在这个被合成出来的拷贝构造函数中,如整数、指针、数组等等的nonclass members也都会被复制,正如我们所期待的一样。
不要Bitwise Copy Semantics!
什么时候一个类不展现“bitwise copy semantics”呢?有以下4种情况:
当类内含一个member object,而后者的class声明有一个拷贝构造函数时(不论是被类设计者明确地声明,或是被编译器合成的)
当类继承自一个基类而后者存在有一个拷贝构造函数时(再次强调,不论是被明确声明或是被合成而得)
当类声明了一个或多个虚函数时
当类派生自一个继承串链,其中有一个或多个虚基类时
前两种情况中,编译器必须将member或base class的“copy constructors调用操作”安插到被合成的拷贝构造函数中。后面两种情况较为复杂一些,接下来将详细地讨论。
重新设定虚表的指针
假设类声明了一个或多个虚函数,编译期间会进行程序扩张操作:
增加一个虚函数表,内含每一个有作用的虚函数的地址
将一个指向虚函数表的指针,安插在每一个类对象中
显然,如果编译器对于每一个新产生的类对象的vptr不能成功而正确地设好其初值,将导致错误的结果。因此,当编译器导入一个vptr到class中时,该class就不再展现bitwise semantics了。现在,编译器需要合成出一个copy constructor,以便将vptr适当地初始化,下面是个例子:
首先,定义两个类,ZooAnimal和Bear:
class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
//...
private:
//ZooAnimal的animate()和draw()所需要的数据
}
class Bear : public ZooAnimal{
public:
Bear();
void animate(); //虽然没有明写virtual,它实际上也是virtual
void draw(); //虽然没有明写virtual,它实际上也是virtual
virtual void dance();
//....
private:
//ZooAnimal的animate()、draw()和dance()所需要的数据
}
ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠“bitwise copy semantics”完成(除了可能会有的pointer member之外,为了简化,这里不考虑这种情况)。举例:
Bear yogi;
Bear winnie = yogi;
yogi会被default Bear consturctor初始化。而在构造函数中,yogi的vptr被设定指向Bear class的virtual table。因此,把yogi的vptr值拷贝给winnie的vptr是安全的。
当一个基类对象以其派生类的对象做初始化操作时,其vptr复制操作也必须保证安全,例如:
ZooAnimal franny = yogi;//这会发生切割(sliced)行为
franny的vptr不可以被设定为指向Bear class的virtual table。合成出来的ZooAnimal copy constructor会明确设定object的vptr指向ZooAnimal class的virtual table,而不是从右手边的class object中将其vptr现值拷贝过来。
处理Virtual Base Class Subobject
每一个编译器对虚拟继承的支持承诺,都表示必须让派生类对象中的virtual base class subobject位置在执行期就准备妥当。维护“位置的完整性”是编译器的责任。“bitwise copy semantics”可能会破坏这个位置,所以编译器必须在它自己合成出来的拷贝构造函数中做出仲裁。举个例子,在下面的声明中,ZooAnimal成为Raccoon的一个虚拟基类,同时RedPanda public继承自Raccoon:
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”就绰绰有余了:
Raccoon rocky;
Raccoon little_critter = rocky;
然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断“后续当程序员企图存取其ZooAnimal subobject时是否能够正确地执行”:
RedPanda rocky;
Raccoon little_critter = rocky;
在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个拷贝构造函数,安插一些代码以设定virtual base class pointer/offset的初值,对每一个members执行必要的memberwise初始化操作,以及执行其它的内存相关操作。
参考资料:
《深度探索C++对象模型》P48~60。第一~四章完。
版权归本人、吴黎明、CSDN共同所有,任何人,任何网站,在未经本人书面许可的情况下,严禁转载。
否则,尽我一切,永久追究法律责任的权利。July、二零一一年三月十七日声明。