(九)C++学习 | 多态的实现原理 虚析构函数 纯虚函数和抽象类


1. 简介

上文介绍了多态的基础概念和两个基于多态的简单的实例。本文将介绍多态的具体实现原理,以及在多态中构造函数和析构函数的使用方法。


2. 多态的实现原理

多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的基类还是派生类的函数,运行时才确定,这个过程又被称为动态联编。而动态联编的实现原理就是实现多态的关键。首先来看一段程序及其输出结果:

class Base {
public:
	int i;
	virtual void Print() {
		cout << "Base: Print";
	}
};
class Derived :public Base {
public:
	int n;
	virtual void Print() {
		cout << "Derived: Print";
	}
};
int main() {
	Derived d;
	cout << sizeof(Base) << "," << sizeof(Derived) << endl;
	return 0;
}

最终程序的输出是8,12。但是,根据相关知识,sizeof(Base)应该输出类Base的成员变量所占字节空间;sizeof(Derived)应该输出类Derived的成员变量所占字节空间。而这里的输出都多占了四个字节,这里就涉及多态实现的原理。

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都存放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的四个字节就是用来存放虚函数表的地址的。如下图是一个虚函数表:在这里插入图片描述
所以,多态的实现原理为:多态的函数调用语句被编译成一系列根据基类指针所指向对象中存放的虚函数表的地址,然后在虚函数表中查找函数的地址,进而完成函数的调用。这里,虚函数表的地址在整个地址的开始部分。所以,生成的虚函数表就是实现多态的关键。

class A {
public:
	virtual void Func() {
		cout << "A::Func" << endl;
	}
};
class B :public A {
public:
	virtual void Func() {
		cout << "B::Func" << endl;
	}
};
int main() {
	A a;	// 类A的对象a
	A* pa = new B();	// 使用类B初始化指针对象pa
	pa->Func();	// 根据多态原理,指针是使用类B初始化的,这里调用类B的函数
	long long* p1 = (long long*)&a;	// 将a的地址强制转化成long long型指针,为8字节,然后赋值给p1
	long long* p2 = (long long*)pa;	// 将pa强制转化成long long型指针,为8字节,然后赋值给p2
	*p2 = *p1;	//将指针p1所指向的内容复制给指针p2所指向的内容
	pa->Func();	// 由于上一条语句的作用,这里调用类A的函数
	return 0;
}

上述程序的输出是B::Func '\n' A::Func。首先,由于p1的大小为八个字节,由于p1指向a的地址,所以p1实际上是指向基类的虚函数表的地址;同理,p2的大小也为八个字节,指向派生类的对象。语句*p2 = *p1实现将p1所表示的虚函数表的地址赋值给p2,所以此时p2所指向的pa对象的派生类的虚函数表的地址已经改变,为基类的虚函数表的地址。所以,后一项的输出结果是A::Func


3. 虚析构函数

首先,通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,直观上应该在删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。我们可以通过将析构函数声明成虚函数来使得程序同时也会调用派生类的析构函数。这里,如果我们将基类的析构函数声明为虚函数,则派生的析构函数自动变成虚函数。同时,通过基类的指针删除派生类对象时,首先调用派生类的虚构函数,然后调用基类的析构函数。所以,一般来说,如果一个类定义了虚函数,则应该将其析构函数也定义为虚函数。或者,如果把一个类当作基类使用时,也应该将其析构函数定义为虚函数。注意,构造函数不能为虚函数。下面看一个程序实例:

class son {
public:
	~son() {
		cout << "bye from son" << endl;
	}
};
class grandson :public son {
public:
	~grandson() {
		cout << "bye from grandson" << endl;
	}
};
int main() {
	son* pson;
	pson = new grandson();
	delete pson;
	return 0;
}

上述程序的输出结果为bye from son,没有执行派生类中的析构函数。当我们将基类的析构函数声明为虚函数时,在删除对象时才会先调用基类的析构函数,然后再调用基类的析构函数。


4. 纯虚函数和抽象类

没有函数体的虚函数成为纯虚函数,我们在上一文已经使用过。其形式为:

virtual void Print() = 0;

我们将包含纯虚函数的类成为抽象类。抽象类只能作为基类来派生新类使用,不能独立地创建抽象类的对象;抽象类的指针和引用可以指向由抽象类派生出来的类的对象。如:

A a;		// 错误,抽象类A不能独立地创建对象
A* pa;		// 正确,可以定义抽象类的指针和引用
pa = new A;	// 错误,抽象类A不能独立地创建对象

注意,在抽象类的成员函数内可以调用纯虚函数,即多态;但是在构造函数或析构函数内不能调用纯虚函数。如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数时,它才能成为非抽象类。

class A {
public:
	virtual void f() = 0;	// 纯虚函数
	void g() {	// 抽象类的成员函数调用纯虚函数
		this->f();	// 或f();
	}
	A() {
		f();	// 错误
	}
};
class B :public A {
public:
	void f() {	// 派生类实现抽象基类的全部纯虚函数,此时B为非抽象类
		cout << "B::f()" << endl;
	}
};

将错误部分注释后,上述程序的输出结果是B::f()


5. 总结

本文内容为上一文的补充内容,简单介绍了多态的实现原理,以及虚函数的相关内容。至此,关于类的主要内容差不多简要地介绍完毕。后面讲介绍 C + + {\rm C++} 的文件操作、 S T L {\rm STL} 等。


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


猜你喜欢

转载自blog.csdn.net/Skies_/article/details/107291276