C++专题:多态性与虚函数(详细!)

2023-11-14

目录

多态性的概念

虚函数

动态联编(晚绑定)和静态联编(早绑定)

覆盖(重写),重载,隐藏

成员函数覆盖(重写)

成员函数重载

成员函数隐藏(重定义)

纯虚函数

抽象类

虚析构函数

限制构造函数


多态性是面向对象程序设计的重要特征之一。多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。

多态性的概念

所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”,程序在运行时才决定调用哪个方法,是面向对象编程的核心概念。

我们先来看一段代码:

#include<iostream>
using namespace std;
class Base{
	public:
		void priMsg(){
			cout << __func__ << " line: " << __LINE__ << endl;//此处是13行
		}
};
class Subclass : public Base{
	public:
		void priMsg(){
			cout << __func__ << " line: " << __LINE__ << endl;
		}
};
void test(Base *p)
{
	p->priMsg();
}
int main(int argc, char *argv[])
{
	Base obj1;
	Subclass obj2;

	test(&obj1);
	test(&obj2);
    return 0;
}

可以看到我们在基类和派生类中都有同名的函数,我们在test函数形参定义一个基类的指针,接收基类对象obj1和派生类对象obj2,函数运行结果如何呢?是会分别调用基类和派生类的函数吗?

 这是因为我们定义的指针类型是基类Base,派生类向上隐式转换成为基类。

那么我们要如何实现用一个接口来调用基类和派生类里两个同名的函数呢??这就是我们接下来要讲到的虚函数。

虚函数

虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:

virtual 返回类型 函数名(形参表) {
    函数体
}

在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

注意虚函数的设置条件:

虚函数设置条件:
 *        1、非类的成员函数不能设置 为虚函数。
 *        2、类的静态成员不能定义 为虚函数。
 *        3、构造函数不能定义为 虚函数, 但是析构函数却能设置为  虚函数。
 *        4、成员函数声明时需要使用 Virtual关键字修饰,定义时不需要。
 *        5、基类成员函数设置为 虚函数, 那么派生类中同名函数(函数名、形参类型、个数、返回值类型全一样)自动成为虚函数。
 *  

看一段代码:


#include<iostream>
using namespace std;
class Base{
	public:
		virtual void priMsg(){
			cout <<“Base”<< __func__ << " line: " << __LINE__ << endl;
		}
		int priMsg(int x){}
};
class Subclass : public Base{
	private:
		void priMsg(){
			cout << "sublcass"<<__func__ << " line: " << __LINE__ << endl;
		}
		int priMsg(int x){}
};
void test(Base *p)
{
	p->priMsg();
}
int main(int argc, char *argv[])
{
	Base obj1;
	Subclass obj2;
	test(&obj1);
	test(&obj2);
    return 0;
}

当我们将基类的同名函数定义为虚函数后,结果会和我们之前有什么差别呢?

可以看到基类和派生类的同名函数都被调用了,这是因为 基类的成员函数设置为虚函数,那么派生类中同名函数(函数名,形参类型,个数,返回值类型全一样)自动成为虚函数,派生类的虚函数会覆盖调基类的同名虚函数!!!

动态联编(晚绑定)和静态联编(早绑定)

静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。也称为早绑定

运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。也称为晚绑定。(只有晚绑定才是真正的多态)

那么C++是如何在运行时做到晚绑定的呢,动态联编引入了一个虚函数表的概念!下面我们来看这张图!

 对象空间的前四个字节(注意这是在32位系统中,64位系统中为前8个字节)存放了虚函数表的首地址,接着存放了其他成员,我们可以通过虚函数表的地址去访问到对应的虚函数。看下面这段代码:

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Base{
	public:
		Base(int x, int y) : x(x), y(y){}
		virtual int getValue(){
			cout << __func__ << " line: " << __LINE__ << endl;
			return x;
		}
		virtual int getData(){
			cout << __func__ << " line: " << __LINE__ << endl;
			return 0;
		}
	private:
		int x, y;
};
typedef int (*PFUNC)();
int main(int argc, char *argv[])
{
	Base obj(23, 1);
	/*
	 * &obj: 得到对象首地址 Base*
	 * (int *)&obj : 将地址 Base* 强转为 int *
	 * *(int *)&obj: 将int *地址上的前 4字节内容取出,得到int类型的 16进制数值
	 * *(long *)&obj: 将long *地址上的前 8字节内容取出,得到 long类型的 16进制数值
	 * *(char *)&obj: 将char *地址上的前 1字节内容取出,得到 char类型的 16进制数值
	 *
	 * 32位机中,含有虚函数的类对象 头4字节存储 虚函数表首地址
	 * 64位机中,含有虚函数的类对象 头8字节存储 虚函数表首地址
	 * */
	cout << sizeof(int) << " " << sizeof(int *) << endl;
	long v = *(long *)&obj;
	PFUNC p = (PFUNC)(*(long *)v);
	p(); 
	p = (PFUNC)(*((long *)v+1));
	p();
	// int *p;   p+3 = p+3*sizeof(int);
	cout << " " << *((int *)&obj+2) << endl;
	cout << " " << *((int *)&obj+3) << endl;

	//p = (PFUNC)*((int *)&obj + 3);
	//p();
	return 0;
}

运行结果:

 这里我们是在64位系统下运行的所以,在进行类型转换我们用了long型,这样才能取到前八个字节的虚函数表地址,如果用int只能取到前四个字节。

覆盖(重写),重载,隐藏

成员函数覆盖(重写)

成员函数覆盖(也称为重写):是指派生类重新定义基类的虚函数

        1.作用域不同(分别位于派生类和基类)

        2.函数名字相同

        3.参数相同

        4.返回值相同

        5.基类函数必须有virtual关键字,不能有static

        6.重写函数的权限访问限定符可以不同

成员函数重载

是指函数名相同,参数不同(数量,类型,次序)

        1.相同的范围,在同一个作用域内

        2.函数名字相同

        3.参数不同

        4.返回值可以不同

        5.virtual关键字可有可无

成员函数隐藏(重定义)

1.不在同一个作用域

2.函数名字相同

3.返回值可以不同

4.参数不同时:此时不论有无virtual关键字,基类的函数将被隐藏

5.参数相同时,但基类没有virtual关键字,此时基类的函数被隐藏

我们来看一个例子具体说明他们三个之间的差别:

#define TRACE
    cout<<typeid(this).name()<<":"<<__func__<<":"<<__LINE__<<endl;
class Base{
    public:
        virtual void func(void)  {TRACE();}
        int func(int val)  {TRACE();}
};
class Inherit:public Base{
    public:
        void func(void) {TRACE();}
        int func(int val)  {TRACE();}
};
int main()
{
    Inherit obj;
    obj.func();
    obj.func(1);
    return 0;
}

说明:

1.派生类Inherit和基类Base都各自重载了func成员函数(在同一个作用域,参数不同返回值不同)

2.派生类Inherit的成员函数void func(void)覆盖了基类的成员函数void func(void)(在不同作用域,函数参数返回值都一样,且必须有virtual关键字)

3.派生类Inherit的成员函数int func(int)隐藏了基类的成员函数int func(int)(在不同作用域,参数相同,且没有virtual关键字)

4.派生类Inherit的成员函数 void func(void)隐藏了基类的成员函数 int func(int)(在不同的作用域,参数不同无论有无关键字virtual都被隐藏)

纯虚函数

纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Base{
	public:
		virtual void get() = 0;			//设置纯虚函数,该虚函数没有函数体,此时该类成为抽象类	
};
class Subclass : public Base{
	public:
		void get(){
			cout << "hello world" << endl;
		}
};
/*
 * 抽象类不能有实例,必须 派生出子类来使用;
 * 抽象类的子类中,必须 重写基类的纯虚函数;否则子类定义对象都是错的。
 *
 * */
int main(int argc, char *argv[])
{
	// Base obj;    //抽象类不能定义对象,不能有具体实例
	Base *p;
	Subclass obj2;
	Base &obj = obj2;
	return 0;
}

注意:

1.由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
2.不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
3.抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。
4.可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
5.如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

 

虚析构函数

如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。由此我们有了虚析构函数,它可以完全回收派生类和基类资源

#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	public:
		Base(){ pri(); }
		//虚析构函数,C++ 中常将 析构函数设置 虚函数。
		virtual ~Base(){ pri(); }
};
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		//派生类的析构函数自动成为 虚析构函数,覆盖 基类虚析构函数
		~Subclass(){ pri(); }
};

int main(int argc, char *argv[])
{
	Subclass *p = new Subclass;
	delete p;
	// 设置虚析构函数的目的是:当基类指针指向 派生类对象时,释放基类指针,能完全回收派生类和基类资源。
	Base *q = new Subclass;
	delete q;
	return 0;
}

 注意:虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数

限制构造函数

如果一个类的构造函数访问权限不是public,那么该类的构造函数就是限制构造函数

如果构造函数访问权限为保护,那么只能派生出子类,定义子类对象通过接口访问

#include<iostream>
using namespace std;
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	protected:
		//构造函数权限不是public,那么这就是限制构造函数
		Base(){ pri(); }
	public:
		virtual ~Base(){ pri(); }
		void get(){ pri(); }
};
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		~Subclass(){ pri(); }
};

int main(int argc, char *argv[])
{
	// Base obj;   //限制构造函数不能创建实例,
	// 如果构造函数权限为 protected,那么只能派生出子类,定义子类对象来访问接口函数
	Subclass obj;
	obj.get();
	return 0;
}

如果构造函数访问权限为私有,那么只能通过友元函数:

#include<iostream>
using namespace std;
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class Base{
	private:
		//构造函数权限不是public,那么这就是限制构造函数
		Base(){ pri(); }
	public:
		virtual ~Base(){ pri(); }
		void get(){ pri(); }
		friend Base *getObj();
		friend void freeObj(Base *);
};
#if 0
class Subclass : public Base{
	public:
		Subclass(){ pri(); }
		~Subclass(){ pri(); }
};
#endif
Base *getObj() //友元成员函数,打破类的封装,在函数可以访问 类的保护和私有成员
{
	return new Base; //开辟堆区空间时,系统会调用Base类默认构造函数
}
void freeObj(Base *p)
{
	delete p;
}

int main(int argc, char *argv[])
{
//	Base obj;   //限制构造函数不能创建实例,
	// 如果构造函数权限为 private,那么只能设计友元成员函数打破private的限制

//  Subclass obj; //错误,因为基类构造函数为 private权限,而派生类不能访问基类的 私有成员。
//	obj.get();
	Base *p = getObj();
	p->get();
	freeObj(p);
	return 0;
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++专题:多态性与虚函数(详细!) 的相关文章

  • 使用 TCP 套接字在本地代理视频

    我一直对向媒体浏览器添加对视频播客的支持非常感兴趣 我希望用户能够浏览可用的视频播客并从互联网上流式传输它们 这真的很容易 因为媒体播放器等将愉快地播放存在于云中的文件 问题是我想在本地缓存这些文件 因此同一集的后续观看将不涉及流式传输 而
  • 为什么在 lambda 内部引发异常是 C# 7 的一项功能? [复制]

    这个问题在这里已经有答案了 该语句在 VS2015 中无法编译 但在 VS2017 中可以编译 var example new Action gt throw new Exception 为了支持在 lambda 表达式内抛出异常 必须对
  • 如何创建语法突出显示文本框[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 如何使用 C Net 创建语法突出显示文本框 Take 闪烁网 http scintillanet codeplex com 并采取其
  • 更改图像颜色与透明背景

    我需要使用 c System Drawings 将透明背景上带有绿色圆圈的图像加载到位图图像中 这是最简单的部分 但是 我需要在将其添加到更大的图像之前更改圆圈的颜色 而不影响周围的透明度 就我而言 我需要将圆圈颜色更改为黄色并将其添加为太
  • Monitor.Pulse & Wait - 意外行为

    http www codeproject com Articles 28785 Thread synchronization Wait and Pulse demystified http www codeproject com Artic
  • .NET 中的 Class.forName() 等效项?

    动态获取对象类型然后创建它的新实例的 C 方法是什么 例如 如何在 C 中实现以下 Java 代码的结果 MyClass x MyClass Class forName classes MyChildClass newInstance Lo
  • C 中的 '\0' 和 printf()

    在 C 入门课程中 我了解到在存储字符串时存储空字符 0在它的最后 但是如果我想打印一个字符串怎么办 printf hello 虽然我发现它并没有结束 0通过以下声明 printf d printf hello Output 5 但这似乎不
  • 析构函数与成员函数竞赛

    当我在析构函数内时 其他线程是否可能开始执行对象的成员函数 遇到这种情况该如何处理呢 C 没有内在的保护来防止在删除对象后使用它 忘记竞争条件 另一个线程可以在完全删除你的对象后使用你的对象 Either 确保只有一个位置 代码拥有该对象
  • C++ 并行任务的开销

    我有以下简单的功能 include
  • 将视频上传/保存到数据库或文件系统

    我以前从未尝试过保存视频 所以我对此了解不多 我知道如果视频很小 我可以转换为字节数组并保存到数据库 但是为了提高效率 我想了解如何将任何上传的视频保存到我的服务器文件中 然后只保存该文件的文件路径我的数据库表中的视频 我完全不知道如何开始
  • 如何生成可变参数包?

    给定不相关的输入是否可以生成非类型参数包 我的意思是 我想改变这一点 template
  • 用 OpenCL C 编写快速线性系统求解器

    我正在编写一个 OpenCL 内核 它将涉及求解线性系统 目前我的内核太慢了 提高线性系统部分的性能似乎是一个不错的起点 我还应该注意 我并没有尝试使我的线性求解器并行 我正在研究的问题在宏观层面上已经是令人尴尬的并行 以下是我编写的 C
  • 可以通过模板间接访问基类中的私有类型

    我试图在编译时根据类型是否在给定范围内公开可用来选择要使用的类型 最好直接看代码 include
  • WCF 服务中的缓冲区大小

    我们有一个 WCF 服务 它执行某些存储过程并将结果返回给 silverlight 客户端 某些存储过程最多返回 80K 行 下面给出的是 web config 中服务的设置
  • 当一对迭代器初始化时,向量是否知道先保留?

    考虑以下代码 struct MyData MyData const BYTE pData size t uSize bucket pData pData uSize std vector
  • C中使用JNI从对象获取对象

    public class Student private People people private Result result private int amount 这是 Java 中类的示例 在C中 我试图获取 学生 中的 人 但失败了
  • 使用 DataGridViewCheckboxCell 真正禁用 DataGridView 中的复选框

    有谁知道如何使用 DataGridViewCheckboxCell 禁用 DataGridView 中的复选框 我可以将其设置为只读 并设置背景颜色 但我无法让复选框本身显示为禁用状态 有什么想法吗 Guess 你必须自己画 http so
  • C++ 在预处理器 #if 中对 sizeof() 比较抛出编译错误

    我有这个 它不会从 Visual Studio 编译错误 致命错误 C1017 无效的整数常量表达式 我该怎么做 template
  • 调用泛型类的方法

    这是上下文 我尝试编写一个映射器来动态地将域模型对象转换为 ViewModel 对象 我遇到的问题是 当我尝试通过反射调用泛型类的方法时 出现此错误 System InvalidOperationException 无法对 Contains
  • Selenium - 模式对话框存在 - 如何接受信息?

    我有以下问题 在页面上提交一些日期后 我有一个如图所示的模式对话框 我想单击 ENTER 来浏览该模式 但它不起作用 我有以下代码 driver FindElement By CssSelector input submit Click A

随机推荐

  • 关于Comparator接口和Comparable接口的对比(实例展示)

    我们首先来看看API上是如何解释的 Comparable接口 该接口对实现它的每个类的对象强加一个整体排序 这个排序被称为类的自然排序 类的compareTo方法被称为其自然比较方法 Collections sort 和Arrays sor
  • 用浏览器打不开eureka,且其他服务连接eureka异常

    原因 没有在eureka服务的启动类上添加 EnableEurekaServer注解
  • 回答一位同学的来信

    由于个人隐私的原因 将这位同学的名字匿掉了 因为事先并未征求该同学的意见就将这封信贴了上来 如果该同学认为我侵犯了你的隐私请及时告知我 我会将这篇文章撤掉 之所以发表这篇文章是因为个人觉得它很有代表性 很有可能别的同学也正在为此事困扰着 希
  • 拦截一切的CoordinatorLayout Behavior

    原文地址 http jcodecraeer com a anzhuokaifa androidkaifa 2016 0224 3991 html 如果没有深入CoordinatorLayout 你注定无法在探索Android Design
  • Kibana环境安装及常用命令

    已成功起飞 将自己进大厂前花4w买的java全套教程 整整300集 现在拿出来分享给大家 拿走不谢 从入门到精通 学会即可就业 哔哩哔哩 bilibili 1 安装包解压 tar zxvf kibana 6 4 3 linux x86 64
  • 服务器虚拟cd,如何在VMware虚拟机上安装CD或DVD数据?

    如何在VMware ESX或ESXi里使用CD安装诸如Windows XP Windows Server或Linux这样的操作系统 知道这些是在新子虚拟机上获取操作系统及其应用的关键 对于我们这些使用了好些年服务器和桌面虚拟化的人来说 这些
  • [Python人工智能] 三十六.基于Transformer的商品评论情感分析 (2)keras构建多头自注意力(Transformer)模型

    从本专栏开始 作者正式研究Python深度学习 神经网络及人工智能相关知识 前一篇文章利用Keras构建深度学习模型并实现了情感分析 这篇文章将介绍Transformer基础知识 并通过Keras构建多头自注意力 Transformer 模
  • Qt之右键菜单实现

    最近在看C GUI Qt4部分其中有个例子对于扩展一个应用程序提供一个上下文菜单 Qt中的部件的弹出菜单 ContextMenu 依据ContextMenuPolicy的值的不同 有四种形式 一 默认菜单 此时 ContextMenuPol
  • Python 10(Socket网络编程 )

    本节内容 Socket介绍 参数介绍 基本实例 Socket实现多连接处理 通过Socket实现简单SSH 通过Socket实现文件传送 Socket网络编程 此部分内容涉及到 计算机网络相关知识 OSI七层网络模型 大概的流程如下 通信实
  • 合肥工业大学编译原理实验一词法分析

    基本思路 词法分析是对输入语句串中一个个单词符号进行分析 最后格式化输出种别码 类型 位置等信息 那么 就可以考虑一次读入一个字符将它们拼接成一个字符串 当碰到空格或者分界符 时 就把前面已读的字符串格式化输出 再输出当前分界符 然后再往后
  • ThreadLocal详解-ThreadLocal这一篇就够了

    ThreadLocal简介 ThreadLocal 用于存储当前线程的变量 对其他线程是隔离 ThreadLocal为每个线程提供 get 或 set 方法来创建独立初始化的变量副本 ThreadLocal实例通常是类中希望将状态与线程相关
  • postgresql学习笔记之归档堆积处理(亲测有用)

    一 跟归档相关的参数 1 查看是否开启归档模式 show archive mode 仅在已完成的wal段进行归档 如果强制归档 归档段文件大小也是16M 2 查看归档相关参数 show archive timeout 超过这个时长就必须归档
  • 《信号与系统学习笔记》—拉普拉斯变换(二)

    注 本博客是基于奥本海姆 信号与系统 第二版编写 主要是为了自己学习的复习与加深 一 用拉普拉斯变换分析与表征线性时不变系统 1 在拉普拉斯变换的范畴内 一般称H s 为系统函数或转移函数 线性时不变系统的很多性质都与系统函数在s平面的特性
  • 基于STM32WIFI远程监控电压电流表(二)电流检测电路

    电流检测电路 电流检测的原理 电流检测电路 1 低侧检测 2 高侧检测 应用电路 电流检测的原理 电路检测电路常用于 高压短路保护 电机控制 DC DC换流器 系统功耗管理 二次电池的电流管理 蓄电池管理等电流检测等场景 对于大部分应用 都
  • Augment Reality(AR)现实增强的原理

    这篇博客将介绍OpenCV Augment Reality AR 增强现实的基础知识 增强现实技术指利用真实世界中的环境 然后通过计算机生成的程序来增强这些环境 从而不断丰富环境 通常 这是通过视觉 听觉和触觉 触觉交互的某种组合来实现的
  • lua调用shell 脚本

    Lua中 os execute可以执行dos命令 但是返回的是系统状态码 默认输出 io popen 也可以执行dos命令 但是返回一个文件 eg 复制代码 代码如下 local t io popen svn help local a t
  • vim编辑器显示与取消行号

    显示行号 set nu 取消显示行号 set nonu
  • 浅析 spring 事件驱动

    1 ApplicationContext publishEvent 同步还是异步 为什么 先说结论 默认情况下同步的 即事件发布者会阻塞等待事件处理完成 这是因为Spring的设计初衷是为了保证事件的有序处理和事务的一致性 看下代码实现 p
  • python开发面向对象基础:人狗大战学面向对象

    一 通过函数写人狗大战这个故事 1 usr bin env python 2 coding utf 8 3 4 1 函数 人模子 5 def person name level life value 6 p dic 7 name name
  • C++专题:多态性与虚函数(详细!)

    目录 多态性的概念 虚函数 动态联编 晚绑定 和静态联编 早绑定 覆盖 重写 重载 隐藏 成员函数覆盖 重写 成员函数重载 成员函数隐藏 重定义 纯虚函数 抽象类 虚析构函数 限制构造函数 多态性是面向对象程序设计的重要特征之一 多态性机制