首页 > 代码库 > C++话题
C++话题
1、多态地实现
A:C++中多态的实现原理是怎样的?
Q:通过迟邦定技术(late binding)实现。
具体实现原理如下:
1. 基类中函数带virtual关键字,表示该方法为虚函数。
2. 子类继承基类,并对虚函数重写(亦可以不重写)。
3. 编译器为每个包含虚函数的类都会创建一个虚表(vtable)存放虚函数的地址。
4. 子类若重写父类虚函数,则子类虚表存储重写的函数入口地址。
5. 编译器为每个类的对象提供一个虚表指针(vptr),指向对象所属类的虚表。
6. 子类对象实例化时首先调用基类构造函数,且vptr指向基类vtable。
7. 子类对象实例化调用完基类构造函数后调用自身构造函数,若虚函数重写则vptr指向子类vtable。否则,不改变。
8. 基类指针指向子类对象不影响vptr的指向。
ATTENTION:
1、 每一个类(基类有虚函数)都有虚表。
2、 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同
2、虚继承
其内存分布与编译器相关
虚继承 是面向对象编程中的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。
举例来说:假如类A和类B各自从类X派生(非虚继承且假设类X包含一些数据成员),且类C同时多继承自类A和B,那么C的对象就会拥有两套X的实例数据(可分别独立访问,一般要用适当的消歧义限定符)。但是如果类A与B各自虚继承了类X,那么C的对象就只包含一套类X的实例数据。对于这一概念典型实现的编程语言是C++。
这一特性在多重继承应用中非常有用,可以使得虚基类对于由它直接或间接派生的类来说,拥有一个共同的基类对象实例。避免由于带有歧义的组合而产生的问题(如“菱形继承问题”)。
其原理是,间接派生类(C)穿透了其父类(上面例子中的A与B),实质上直接继承了虚基类X。
这一概念一般用于“继承”在表现为一个整体,而非几个部分的组合时。在C++中,基类可以通过使用关键字virtual来声明虚继承关系。
虚基类的初始化
由于虚基类是多个派生类共享的基类,因此由谁来初始化虚基类必须明确。C++标准规定,由最派生类直接初始化虚基类。因此,对间接继承了虚基类的类,也必须能直接访问其虚继承来的祖先类,也即应知道其虚继承来的祖先类的地址偏移值。
例如,常见的“菱形”虚继承例子中,两个派生类、一个最派生类的构造函数的初始化列表中都可以给出虚基类的初始化;但只由最派生类的构造函数实际执行虚基类的初始化。
g++与虚继承
g++编译器生成的C++类实例,虚函数与虚基类地址偏移值共用一个虚表(vtable)。类实例的开始处即为指向所属类的虚指针(vptr)。实际上,一个类与它的若干祖先类(父类、祖父类、...)组成部分共用一个虚表,但各自使用的虚表部分依次相接、不相重叠。
g++编译下,一个类实例的虚指针指向该类虚表中的第一个虚函数的地址。如果该类没有虚函数(或者虚函数都写入了祖先类的虚表,覆盖了祖先类的对应虚函数),因而该类自身虚表中没有虚函数需要填入,但该类有虚继承的祖先类,则仍然必须要访问虚表中的虚基类地址偏移值。这种情况下,该类仍然需要有虚表,该类实例的虚指针指向类虚表中一个值为0的条目。
该类其它的虚函数的地址依次填在虚表中第一个虚函数条目之后(内存地址自低向高方向)。虚表中第一个虚函数条目之前(内存地址自高向低方向),依次填入了typeinfo(用于RTTI)、虚指针到整个对象开始处的偏移值、虚基类地址偏移值。因此,如果一个类虚继承了两个类,那么对于32位程序,虚继承的左父类地址偏移值位于vptr-0x0c,虚继承的右父类地址偏移值位于vptr-0x10.
一个类的祖先类有复杂的虚继承关系,则该类的各个虚基类偏移值在虚表中的存储顺序尊重自该类到祖先的深度优先遍历次序。
Microsoft Visual C++与虚继承
Microsoft Visual C++与g++不同,把类的虚函数与虚基类地址偏移值分别放入了两个虚表中,前者称为虚函数表vftbl,后者称虚基类表vbtbl。因此一个类实例可能有两个虚指针分别指向类的虚函数表与虚基类表,这两个虚指针分别称为虚函数表指针vftbl与虚基类表指针vbtbl。当然,类实例也可以只有一个虚指针,或者没有虚指针。虚指针总是放在类实例的数据成员之前,且虚函数表指针总是在虚基类表指针之前。因而,对于某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在类实例的0字节偏移处,也可能在类实例的4字节偏移处(对于32位程序来说),这给类成员函数指针的实现带来了很大麻烦。
一个类的虚基类指针指向的虚基类表的首个条目,该条目的值是虚基类表指针所在的地址到该类的实例的内存首地址的偏移值。即&(obj.vbtbl) - &obj。虚基类第2、第3、... 个条目依次为该类的最左虚继承父类、次左虚继承父类、...的内存地址相对于虚基类表指针自身地址,即 &(obj.vbtbl)的偏移值。
如果一个类同时有虚继承的父类与祖父类,则虚祖父类放在虚父类前面。
另外需要注意的是,类的虚函数表的第一项之前的项(即*(obj.vftbl-1))为最派生类实例的内存首地址到当前虚函数表指针的偏移值,即mostDerivedObj-obj.vftbl。派生类的虚函数覆盖基类的虚函数时,在基类的虚函数表的对应条目写入的是一个“桩”(thunk)函数的入口地址,以调整this指针指向到派生类实例的地址,再调用派生类的对应的虚函数。例如:this -= offset; call DerivedClass:virtFunc;
http://zh.wikipedia.org/wiki/%E8%99%9A%E7%BB%A7%E6%89%BF
3、虚函数
虚函数是面向对象程序设计中的一个重要的概念。只能适用于指针和参考的计算机工程运算.当从父类中继承的时候,虚函数和被继承的函数具有相同的签名。但是在运行过程中,运行系统将根据对象的类型,自动地选择适当的具体实现运行。虚函数是面向对象编程实现多态的基本手段。
虚函数在设计模式方面扮演重要角色。例如,《设计模式》一书中提到的23种设计模式中,仅5个对象创建模式就有4个用到了虚函数(抽象工厂、工厂方法、生成器、原型),只有单例没有用到。
双击打开pdf
4.c++重载、覆盖、隐藏的区别和执行方式
既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:
(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。
4.4 三种情况怎么执行:
4.4.1 重载:看参数。
4.4.2 隐藏:用什么就调用什么。
4.4.3 覆盖:调用派生类。
4、多重继承和虚继承的内存布局
http://blog.csdn.net/littlehedgehog/article/details/5442430
这篇文章主要讲解虚继承的C++对象内存分布问题,从中也引出了dynamic_cast和static_cast本质区别、虚函数表的格式等一些大部分C++程序员都似是而非的概念。原文见这里(By Edsko de Vries, January 2006)
敬告: 本文是介绍C++的技术文章,假定读者对于C++有比较深入的认识,同时也需要一些汇编知识。
本文我们将阐释GCC编译器针对多重继承和虚拟继承下的对象内存布局。尽管在理想的使用环境中,一个C++程序员并不需要了解这些编译器内部实现细节,实际上,编译器针对多重继承(特别是虚拟继承)的各种实现细节对于我们编写C++代码都或多或少产生一些影响(比如downcasting pointer、pointers to pointers 以及虚基类构造函数的调用顺序)。如果你能明白多重继承是如何实现的,那么你自己就能够预见到这些影响,进而能够在你的代码中很好地应对它们。再者,如果你十分在意的代码的运行效率,正确地理解虚继承也是很有帮助的。最后嘛,这个hack的过程是很有趣的哦:)
多重继承
首先我们先来考虑一个很简单(non-virtual)的多重继承。看看下面这个C++类层次结构。
1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : public Top
14 {
15 public:
16 int c;
17 };
18
19 class Bottom : public Left, public Right
20 {
21 public:
22 int d;
23 };
24
用UML表述如下:
注意到Top类实际上被继承了两次,(这种机制在Eiffel中被称作repeated inheritance),这就意味着在一个bottom对象中实际上有两个a属性(attributes,可以通过bottom.Left::a和 bottom.Right::a访问) 。
那么Left、Right、Bottom在内存中如何分布的呢?我们先来看看简单的Left和Right内存分布:
[Right 类的布局和Left是一样的,因此我这里就没再画图了。刺猬]
注意到上面类各自的第一个属性都是继承自Top类,这就意味着下面两个赋值语句:
1 Left* left = new Left();
2 Top* top = left;
left和top实际上是指向两个相同的地址,我们可以把Left对象当作一个Top对象(同样也可以把Right对象当Top对象来使用)。但是Botom对象呢?GCC是这样处理的:
但是现在如果我们upcast 一个Bottom指针将会有什么结果?
1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
这段代码运行正确。这是因为GCC选择的这种内存布局使得我们可以把Bottom对象当作Left对象,它们两者(Left部分)正好相同。但是,如果我们把Bottom对象指针upcast到Right对象呢?
1 Right* right = bottom;
如果我们要使这段代码正常工作的话,我们需要调整指针指向Bottom中相应的部分。
通过调整,我们可以用right指针访问Bottom对象,这时Bottom对象表现得就如Right对象。但是bottom和right指针指向了不同的内存地址。最后,我们考虑下:
1 Top* top = bottom;
恩,什么结果也没有,这条语句实际上是有歧义(ambiguous)的,编译器会报错: error: `Top‘ is an ambiguous base of `Bottom‘。其实这两种带有歧义的可能性可以用如下语句加以区分:
1 Top* topL = (Left*) bottom;
2 Top* topR = (Right*) bottom;
这两个赋值语句执行之后,topL和left指针将指向同一个地址,同样topR和right也将指向同一个地址。
虚拟继承
为了避免上述Top类的多次继承,我们必须虚拟继承类Top。
1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : virtual public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : virtual public Top
14 {
15 public:
16 int c;
17 };
18
19 class Bottom : public Left, public Right
20 {
21 public:
22 int d;
23 };
24
上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式)。
对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了。我们再用Bottom的内存布局作为例子考虑,它可能是这样的:
这种内存布局的优势在于它的开头部分(Left部分)和Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。不过,我们再来考虑考虑Right:
1 Right* right = bottom;
这里我们应该把什么地址赋值给right指针呢?理论上说,通过这个赋值语句,我们可以把这个right指针当作真正指向一个Right对象的指针(现在指向的是Bottom)来使用。但实际上这是不现实的!一个真正的Right对象内存布局和Bottom对象Right部分是完全不同的,所以其实我们不可能再把这个upcasted的bottom对象当作一个真正的right对象来使用了。而且,我们这种布局的设计不可能还有改进的余地了。这里我们先看看实际上内存是怎么分布的,然后再解释下为什么这么设计。
上图有两点值得大家注意。第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。第二点,类中增加了vptr指针,这些是被编译器在编译过程中插入到类中的(在设计类时如果使用了虚继承,虚函数都会产生相关vptr)。同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作。Vptr指针指向了一个“virtual table”。在类中每个虚基类都会存在与之对应的一个vptr指针。为了给大家展示virtual table作用,考虑下如下代码。
1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
3 int p = left->a;
第二条的赋值语句让left指针指向和bottom同样的起始地址(即它指向Bottom对象的“顶部”)。我们来考虑下第三条的赋值语句。
1 movl left, %eax # %eax = left
2 movl (%eax), %eax # %eax = left.vptr.Left
3 movl (%eax), %eax # %eax = virtual base offset
4 addl left, %eax # %eax = left + virtual base offset
5 movl (%eax), %eax # %eax = left.a
6 movl %eax, p # p = left.a
总结下,我们用left指针去索引(找到)virtual table,然后在virtual table中获取到虚基类的偏移(virtual base offset, vbase),然后在left指针上加上这个偏移量,这样我们就获取到了Bottom类中Top类的开始地址。从上图中,我们可以看到对于Left指针,它的virtual base offset是20,如果我们假设Bottom中每个成员都是4字节大小,那么Left指针加上20字节正好是成员a的地址。
我们同样可以用相同的方式访问Bottom中Right部分。
1 Bottom* bottom = new Bottom();
2 Right* right = bottom;
3 int p = right->a;
right指针就会指向在Bottom对象中相应的位置。
这里对于p的赋值语句最终会被编译成和上述left相同的方式访问a。唯一的不同是就是vptr,我们访问的vptr现在指向了virtual table另一个地址,我们得到的virtual base offset也变为12。我们画图总结下:
当然,关键点在于我们希望能够让访问一个真正单独的Right对象也如同访问一个经过upcasted(到Right对象)的Bottom对象一样。这里我们也在Right对象中引入vptrs。
OK,现在这样的设计终于让我们可以通过一个Right指针访问Bottom对象了。不过,需要提醒的是以上设计需要承担一个相当大的代价:我们需要引入虚函数表,对象底层也必须扩展以支持一个或多个虚函数指针,原来一个简单的成员访问现在需要通过虚函数表两次间接寻址(编译器优化可以在一定程度上减轻性能损失)。
Downcasting
如我们猜想,将一个指针从一个派生类到一个基类的转换(casting)会涉及到在指针上添加偏移量。可能有朋友猜想,downcasting一个指针仅仅减去一些偏移量就行了吧。实际上,非虚继承情况下确实是这样,但是,对于虚继承来说,又不得不引入其它的复杂问题。这里我们在上面的例子中添加一些继承关系:
1 class AnotherBottom : public Left, public Right
2 {
3 public:
4 int e;
5 int f;
6 };
这个继承关系如下图所示:
那么现在考虑如下代码
1 Bottom* bottom1 = new Bottom();
2 AnotherBottom* bottom2 = new AnotherBottom();
3 Top* top1 = bottom1;
4 Top* top2 = bottom2;
5 Left* left = static_cast<Left*>(top1);
下面这图展示了Bottom和AnotherBottom的内存布局,同时也展示了各自top指针所指向的位置。
现在我们来考虑考虑从top1到left的static_cast,注意这里我们并不清楚对于top1指针指向的对象是Bottom还是AnotherBottom。这里是根本不能编译通过的!因为根本不能确认top1运行时需要调整的偏移量(对于Bottom是20,对于AnotherBottom是24)。所以编译器将会提出错误: error: cannot convert from base `Top‘ to derived type `Left‘ via virtual base `Top‘。这里我们需要知道运行时信息,所以我们需要使用dynamic_cast:
1 Left* left = dynamic_cast<Left*>(top1);
不过,编译器仍然会报错的 error: cannot dynamic_cast `top‘ (of type `class Top*‘) to type `class Left*‘ (source type is not polymorphic)。关键问题在于使用dynamic_cast(和使用typeid一样)需要知道指针所指对象的运行时信息。但是,回头看看上面的结构图,我们就会发现top1指针所指的仅仅是一个整数成员a。编译器没有在Bottom类中包含针对top的vptr,它认为这完全没有必要。为了强制编译器在Bottom中包含top的vptr,我们可以在top类里面添加一个虚析构函数。
1 class Top
2 {
3 public:
4 virtual ~Top() {}
5 int a;
6 };
这就迫使编译器为Top类添加了一个vptr。下面来看看Bottom新的内存布局:
是的,其它派生类(Left、Right)都会添加一个vptr.top,编译器为dynamic_cast生成了一个库函数调用。
1 left = __dynamic_cast(top1, typeinfo_for_Top, typeinfo_for_Left, -1);
__dynamic_cast定义在libstdc++(对应的头文件是cxxabi.h),有了Top、Left和Bottom的类型信息,转换得以执行。其中,参数-1代表的是类Left和类Top之间的关系未明。如果想详细了解,请参看tinfo.cc的实现。
总结
最后,我们再聊聊一些相关内容。
二级指针
这里的问题初看摸不着头脑,但是细细想来有些问题还是显而易见的。这里我们考虑一个问题,还是以上节的Downcasting中的类继承结构图作为例子。
1 Bottom* b = new Bottom();
2 Right* r = b;
(在把b指针的值赋值给指针r时,b指针将加上8字节,这样r指针才指向Bottom对象中Right部分)。因此我们可以把Bottom*类型的值赋值给Right*对象。但是Bottom**和Right**两种类型的指针之间赋值呢?
1 Bottom** bb = &b;
2 Right** rr = bb;
编译器能通过这两条语句吗?实际上编译器会报错: error: invalid conversion from `Bottom**‘ to `Right**‘
为什么? 不妨反过来想想,如果能够将bb赋值给rr,如下图所示。所以这里bb和rr两个指针都指向了b,b和r都指向了Bottom对象的相应部分。那么现在考虑考虑如果给*rr赋值将会发生什么。
1 *rr = b;
注意*rr是Right*类型(一级)的指针,所以这个赋值是有效的!
这个就和我们上面给r指针赋值一样(*rr是一级的Right*类型指针,而r同样是一级Right*指针)。所以,编译器将采用相同的方式实现对*rr的赋值操作。实际上,我们又要调整b的值,加上8字节,然后赋值给*rr,但是现在**rr其实是指向b的!如下图
呃,如果我们通过rr访问Bottom对象,那么按照上图结构我们能够完成对Bottom对象的访问,但是如果是用b来访问Bottom对象呢,所有的对象引用实际上都偏移了8字节——明显是错误的!
总而言之,尽管*a和*b之间能依靠类继承关系相互转化,而**a和**b不能有这种推论。
虚基类的构造函数
编译器必须要保证所有的虚函数指针要被正确的初始化。特别是要保证类中所有虚基类的构造函数都要被调用,而且还只能调用一次。如果你写代码时自己不显示调用构造函数,编译器会自动插入一段构造函数调用代码。这将会导致一些奇怪的结果,同样考虑下上面的类继承结构图,不过要加入构造函数。
1 class Top
2 {
3 public:
4 Top() { a = -1; }
5 Top(int _a) { a = _a; }
6 int a;
7 };
8
9 class Left : public Top
10 {
11 public:
12 Left() { b = -2; }
13 Left(int _a, int _b) : Top(_a) { b = _b; }
14 int b;
15 };
16
17 class Right : public Top
18 {
19 public:
20 Right() { c = -3; }
21 Right(int _a, int _c) : Top(_a) { c = _c; }
22 int c;
23 };
24
25 class Bottom : public Left, public Right
26 {
27 public:
28 Bottom() { d = -4; }
29 Bottom(int _a, int _b, int _c, int _d) : Left(_a, _b), Right(_a, _c)
30 {
31 d = _d;
32 }
33 int d;
34 };
35
先来考虑下不包含虚函数的情况,下面这段代码输出什么?
1 Bottom bottom(1,2,3,4);
2 printf("%d %d %d %d %d/n", bottom.Left::a, bottom.Right::a, bottom.b, bottom.c, bottom.d);
你可能猜想会有这样结果:
1 1 2 3 4
但是,如果我们考虑下包含虚函数的情况呢,如果我们从Top虚继承派生出子类,那么我们将得到如下结果:
-1 -1 2 3 4
如本节开头所讲,编译器在Bottom中插入了一个Top的默认构造函数,而且这个默认构造函数安排在其他的构造函数之前,当Left开始调用它的基类构造函数时,我们发现Top已经构造初始化好了,所以相应的构造函数不会被调用。如果跟踪构造函数,我们将会看到
Top::Top()
Left::Left(1,2)
Right::Right(1,3)
Bottom::Bottom(1,2,3,4)
为了避免这种情况,我们应该显示地调用虚基类的构造函数
1 Bottom(int _a, int _b, int _c, int _d): Top(_a), Left(_a,_b), Right(_a,_c)
2 {
3 d = _d;
4 }
到void* 的转换
1 dynamic_cast<void*>(b);
最后我们来考虑下把一个指针转换到void *。编译器会把指针调整到对象的开始地址。通过查vtable,这个应该是很容易实现。看看上面的vtable结构图,其中offset to top就是vptr到对象开始地址。另外因为要查阅vtable,所以需要使用dynamic_cast。
指针的比较
再以上面Bottom类继承关系为例讨论,下面这段代码会打印Equal吗?
1 Bottom* b = new Bottom();
2 Right* r = b;
3
4 if(r == b)
5 printf("Equal!/n");
先明确下这两个指针实际上是指向不同地址的,r指针实际上在b指针所指地址上偏移8字节,但是,这些C++内部细节不能告诉C++程序员,所以C++编译器在比较r和b时,会把r减去8字节,然后再来比较,所以打印出的值是"Equal".
5、构造函数可以调用虚函数吗?语法上通过吗?语义上通过吗?
A:
C++箴言:绝不在构造或析构期调用虚函数
因为这样的调用不会匹配到当前执行的构造函数或析构函数所属的类的更深的派生层次。
大体情况可以看如下的代码:
[cpp] view plaincopy
- class Base
- {
- public:
- Base()
- {
- Fuction();
- }
- virtual void Fuction()
- {
- cout << "Base::Fuction" << endl;
- }
- };
- class A : public Base
- {
- public:
- A()
- {
- Fuction();
- }
- virtual void Fuction()
- {
- cout << "A::Fuction" << endl;
- }
- };
- // 这样定义一个A的对象,会输出什么?
- A a;
首先回答标题的问题,调用当然是没有问题的,但是获得的是你想要的结果吗?或者说你想要什么样的结果?
有人说会输出:
[html] view plaincopy
- A::Fuction
- A::Fuction
如果是这样,首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:
[html] view plaincopy
- Base::Fuction
- A::Fuction
最后,总结一下关于虚函数的一些常见问题:
1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。
2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。
5) 纯虚函数通常没有定义体,但也完全可以拥有。
6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
7) 非纯的虚函数必须有定义体,不然是一个错误。
8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
6、析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?
C++标准指明《析构函数》不能、也不应该抛出异常:
C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
more effective c++提出两点理由(析构函数不能抛出异常的理由):
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外
构造函数可以抛出异常。
7、拷贝构造函数作用与用途,什么时候需要定义拷贝构造函数。
A:在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。
1). 一个对象以值传递的方式传入函数体
2). 一个对象以值传递的方式从函数返回
3). 一个对象需要通过另外一个对象进行初始化
4). 初始化顺序容器中的元素
5). 根据元素初始化列表初始化数组元素。
有些累必须对复制对象是发生的事情加以控制。
1、一般这种类经常有一只数据成员是指针。
2、有成员表示在构造函数中分配的其他资源。
3、在创建对象时必须做一些特定的工作。(举例:静态成员变量统计对象个数)
http://blog.csdn.net/feiyond/article/details/1807068
http://blog.csdn.net/lwbeyond/article/details/6202256
该题深入下去可以在讨论默认构造函数,浅拷贝,深拷贝
8、Static变量问题
1、Static的数据成员必须在类定义体的外部定义。
即在类内进行static 声明变量,在类的外部进行初始化,在main函数之前初始化,main结束销毁。
#include <stdio.h>
class A{
public:
A(){printf("constructor of A\n");}
~A(){printf("destruction of A\n");}
};
class B{
public:
static A a;
B(){printf("constructor of B\n");}
};
A B::a;
int main()
{
printf("main\n");
B b;
B c;
return 0;
}
2、函数内部局部static变量
c++把函数内部static变量的初始化推迟到了caller的第一次调用,程序结束时销毁, 而不是像其他global变量一样,在main之前就进行它们的初始化。在C语言中是编译不通过的!
#include <stdio.h>
class A{
public:
A(){printf("constructor of A\n");}
~A(){printf("destruction of A\n");}
};
int caller()
{
static A a;
printf("caller\n");
return 0;
}
int main()
{
printf("main\n");
caller();
caller();
return 0;
}
3、全局static变量
我们并不能确定全局静态变量的初始化顺序!Effective C++中就是用在函数中返回局部静态变量的方式来解决全局变量初始化顺序未知的问题的。
全局静态变量在main函数开始前初始化,在main函数结束后销魂。
#include <stdio.h>
class A{
public:
A(){printf("constructor of A\n");}
~A(){printf("destruction of A\n");}
};
static A a;
int main()
{
printf("main\n");
return 0;
}
http://www.douban.com/note/100606486/
9、函数对象
http://www.cnblogs.com/ly4cn/archive/2007/07/21/826885.html
函数对象或 functor,是任何类型实现运算符 ()。 标准模板库 (STL) 使用函数对象主要用作排序条件的容器和算法。
函数对象提供两个主要优点与直线段的函数调用。 第一是函数对象可以包含状态。 第二个是函数对象是类型可以用作模板参数。
函数对象与函数指针相比,有两个优点:第一是编译器可以内联执行函数对象的调用;第二是函数对象内部可以保持状态。
10、初始化成员列表
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:
1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2.const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
1.内置数据类型,复合类型(指针,引用)
在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
2.用户定义类型(类类型)
结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
初始化列表的成员初始化顺序:
C++初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
C++话题