读书笔记-EffectiveC++-07+纯虚函数,虚函数

探讨C++中虚函数与多态的概念,包括虚函数表的解析、纯虚函数和虚析构函数的重要性,以及如何通过虚函数实现多态性。文章深入分析了虚函数在继承和覆盖中的作用,以及在多重继承下的表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

构造/析构/赋值

  1. 多态基类声明virtual析构函数

    class TimeKeeper
    {
    public:
    	TimeKeeper();
    	~TimeKeeper();
    };
    
    class AtomicClock :public TimeKeeper {};
    class WatchClock :public TimeKeeper {};
    class WristWatch :public TimeKeeper {};
    

只想在程序中使用时间,不像操心时间如何计算等细节,这时候我们可以设计factory(工厂)函数,返回指针指向一个即使对象。factory函数会返回一个base class指针,指向新生成的derived对象。

TimeKeeper* ptk = getTimeKeeper();
~~~
delete ptk;

//从TimeKeeper继承体系,获得一个动态分配对象
//释放,避免内存泄漏

为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。因为避免泄露内存和其他资源,将factory()返回的每个对象适当的delete很重要。

问题是: getTimeKeeper() 返回的指针指向一个derived class对象,而这个对象去油一个base class 指针被删除,而目前的base class有一个non-virtual析构函数。

C++指出,当derived class对象经由一个base class指针被删除,而该base class 带着一个non-virtual析构函数,其结果未有定义–实际执行时通常发生的时对象的derived 成分没有被销毁。云上面的例子分析,getTimeKeeper()函数返回一个指针指向AtomicClock对象,其内的AtomicClock可能没被销毁,而AtomicClock的析构函数也未能执行起来然而base class这一部分被销毁,造成“局部销毁”对象。易造成资源泄露,败坏数据结构。

解决方案:**给base class一个virtual 析构函数。**会销毁整个对象,包括所有的derived class成分。因为virtual函数的目的时允许derived class实现得以客制化(条款34)。例如TimeKeeper可能拥有一个virtual getCurrentTime,他在不同的derived classes中有不同的实现码,任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数

class不含virtual函数,通常表示他并不意图被用作一个base class。当其不被作为base class时,令其析构函数为virtual往往时馊主意。例如:

class Point{
public:
	Point(int xCoord, int yCoord);
	~Point();
private:
	int x, y;
};

如果int占32bits,那么point对象可以塞入一个64-bit缓存器,甚至独享可以被用作“64-bit量”传给其他语言C或者FORTRAN撰写的函数。

但是virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数被调用。这份信息通常由一个vptr(virtual table pointer)指针指出,vptr指向一个有函数指针构成的数组,成为vtbl(virtual table);每一个带有virtual函数的class都有一个vtbl。当对象调用某一函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl–编译器在其中寻找适当的函数指针。

因此对象提及会增加,不再和其他语言具有相同的结构(C没有vptr)不再具有移植性。

无端声明virtual和未声明它们为virtual一样是错误的,只有当class内含有至少一个virtual函数,才为它声明virtual析构函数。

既是class 完全不带virtual函数,也可能出现“non-virtual析构函数问题”

class SpecialString:public std::string{
    //std::string 有个non-virtual析构函数
}

在程序某处无意间声明一个pointer-to-SpecialString转换为pointer-to-string,然后再将转换后的指针delete后,造成和前面一样的问题。

SpecialString* pss = new SpecialString("beautiful girl");
std::string* ps;
ps = pss;
delete ps;

包括所有的STL容器,如:vector, list,set, trl::unordered_map(条款54)等等。都会造成上述的问题。

pure virtual 函数(纯虚函数),pure virtual函数导致abstract(抽象)classes–不可以被实例化的class。由于抽象类总是被当作基类,基类应该有个虚析构函数,可以在抽象类里声明一个纯虚析构函数。

在派生子类中,实现析构函数的定义。

Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphic),就不该声明virtual析构函数。如:STL和string设计不被作为base class,input_iterator_tagzhege base class 不被用作多态。

remember:

  1. polymorphic(带有多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,他就应该拥有一个virtual析构函数。
  2. Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphic),就不该声明virtual析构函数。

该点涉及纯虚函数,虚函数

下面文章转自 C++ 虚函数表解析 作者:haoel,来源优快云

纯虚函数

纯虚函数是指被表明为不具体实现的虚拟成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。

纯虚函数定义格式

virtual 返回值类型 函数名(参数表)= 0

含有纯虚函数的基类是不可以定义对象的。纯虚函数无实现部分,不能产生对象,所以含有虚函数的类是抽象类

纯虚函数需要注意

  1. 定义纯虚函数时,不能定义纯虚函数的实现部分。即使是函数体为空也不可以,函数体为空就可以执行,只是什么也不做就返回。而纯虚函数不能调用。(其实可以写纯虚函数的实现部分,编译器也可以通过,但是永远也无法调用。因为其为抽象类,不能产生自己的对象,而且子类中一定会重写纯虚函数,因此该类的虚表内函数一定会被替换掉,所以可以说永远也调用不到纯虚函数本身)

  2. "=0"表明程序将不定义该函数,函数声明是为派生类保留一个位置。“=0”的本质是将指向函数体的指针定为NULL。

  3. 在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用来定义对象。(如果不重写进行覆盖,程序会报错)

虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。 *虚函数表*

对C++ *了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(*Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。*C++*的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

当一个类有子类时,该类的析构函数必须是虚函数,原因:会有资源释放不完全的情况;

假设我们有这样的一个类:

class Base {
 public:
	virtual void f() { cout << "Base::f" << endl; }
	virtual void g() { cout << "Base::g" << endl; }
	virtual void h() { cout << "Base::h" << endl; }
};

按照上面的说法,我们可以通过Base的实例来得到虚函数表。下面是实际例程:

typedef void(*Fun)(void); 
Base b;
Fun pFun = NULL;
cout << "*虚函数表地址:*" << (int*)(&b) << endl;
cout << "*虚函数表* — *第一个函数地址:*" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function 
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)

虚函数表地址:0012FED4

虚函数表 — 第一个函数地址:0044F148

Base::f

通过这个示例,我们可以看到,我们可以通过强行把*&b转成int **,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()Base::h(),其代码如下:

(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()

这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:

img

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。

一般继承(无虚函数覆盖)

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

img

请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示

对于实例:Derive d; 的虚函数表如下:

img

我们可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

img

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

img

我们从表中可以看到下面几点,

1)覆盖的*f()*函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序

Base *b = new Derive();
 b->f();

b所指的内存中的虚函数表的*f()的位置已经被Derive::f()*函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承(无虚函数覆盖)

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数

img

对于子类实例中的虚函数表,是下面这个样子:

img

我们可以看到:

1) 每个父类都有自己的虚表。

2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。

下图中,我们在子类中覆盖了父类的*f()*函数。

img

下面是对于子类实例中的虚函数表的图:

img

我们可以看见,三个父类虚函数表中的*f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()*了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f() 
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性

一、通过父类型的指针访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->f1(); //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反*C++*语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

如:

class Base {
  private:
	virtual void f() { cout << "Base::f" << endl; 
}; 

class Derive : public Base{
};

typedef void(*Fun)(void);

void main() {

  Derive d;
  Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
  pFun();
}
附录一:VC中查看虚函数表

我们可以在VCIDE环境中的Debug状态下展开类的实例就可以看到虚函数表了(并不是很完整的)

img

附录 二:例程

下面是一个关于多重继承的虚函数表访问的例程:

#include <iostream>
using namespace std;

class Base1 {
public:
	virtual void f() { cout << "Base1::f" << endl; }
	virtual void g() { cout << "Base1::g" << endl; }
	virtual void h() { cout << "Base1::h" << endl; }
};

class Base2 {
public:
	virtual void f() { cout << "Base2::f" << endl; }
	virtual void g() { cout << "Base2::g" << endl; }
	virtual void h() { cout << "Base2::h" << endl; }
};

class Base3 {
public:
	virtual void f() { cout << "Base3::f" << endl; }
	virtual void g() { cout << "Base3::g" << endl; }
	virtual void h() { cout << "Base3::h" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
	virtual void f() { cout << "Derive::f" << endl; }
	virtual void g1() { cout << "Derive::g1" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	Fun pFun = NULL;
	Derive d;

	int** pVtab = (int**)&d;

	//Base1's vtable\****

	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);

	pFun = (Fun)pVtab[0][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);

	pFun = (Fun)pVtab[0][1];
	pFun();

	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
	pFun = (Fun)pVtab[0][2];
	pFun();

	//Derive's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
	pFun = (Fun)pVtab[0][3];
	pFun();

	//The tail of the vtable
	pFun = (Fun)pVtab[0][4];
	cout << pFun << endl;

	//Base2's vtable\****
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[1][0];
	pFun();

	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[1][1];
	pFun();
	pFun = (Fun)pVtab[1][2];
	pFun();

	//The tail of the vtable
	pFun = (Fun)pVtab[1][3];
	cout << pFun << endl;
	
	//Base3's vtable\****
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[2][0];
	pFun();

	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[2][1];
	pFun();

	pFun = (Fun)pVtab[2][2];
	pFun();

	//The tail of the vtable
	pFun = (Fun)pVtab[2][3];
	cout << pFun << endl;

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值