C++多态的实现

C++的魔力

在C++中,通过继承,子类可以获得父类的成员,通过多态,C++可以实现在运行期根据对象的实际类型调用正确的虚函数,C++还有C语言不能做到的重载行为…C++的这种魔力是怎么实现的呢?

实际上,C++是使用C语言作为代码生成语言的,就好像当写完一个C++程序时,C++预处理器先将C++代码转化为C语言代码,然后再由C语言编译器生成可执行文件一样。当使用继承时,子类获得父类的成员并不是C++具有什么神奇魔力,而是编译器把父类中那些可以被继承的成员复制——把代码拷贝一份,放到子类声明中——到了子类中,当通过对象调用成员函数时,这个调用操作会被转化为非成员函数,并添加指向调用该函数的对象的指针作为额外参数,当使用重载函数时,重载函数名会被根据参数数目、类型、常量性、static与否几个指标修改为独一无二的名称,定义一个对象,预处理器会把调用相应类函数的构造函数写到相应的位置,对象离开作用域时,析构函数也会被添加到正确的位置,这造成了构造函数与析构函数被自动调用一样(是自动调用,只是实际上这些动作是由编译器做的,并不是C++语言就具有这种神奇能力)……最后C++程序被改写成了一个C语言程序,只是我们看不到这个C语言程序而已。

C++多态的正确使用形式

C++中的多态是指通过指向derived class object之base class指针或引用进行调用,以达到在运行期根据base class指针所指对象的实际子类型做出不同的操作。两个重点,第一是代码表面看起来是操作基类,运行时的操作由指针的所指对象的实际类型决定,如果是用derived class指针指向base class object就不是多态的正确用法;第二是指针或引用,不能使base class object “指向”derived class object,这样做不但达不到多态的效果,还可能会因为对象切割而得到错误。多态为什么必须使用base class指针指向derived class object,为什么必须使用指针或引用呢?先来解答第二个问题。

为什么是指针或引用

这是因为指针或引用是一种存储内存地址的数据类型,为了存储指针或引用,计算机内存中必须有一块区域提供给存放指针或引用,通常这块区域的大小是4byte(32位机上),这也就是“任何指针占用的空间一样”的意思,在这个4byte空间里放置了一个值,这个值指向了内存中的另一个位置,当计算机要访问一个指针指向的对象的时候,会根据指针变量里存储的值跳转到这个位置,然后根据指针的类型,解释这个位置后的一定空间内数据的意思。而每定义一个对象的时候,只能得到被定义的对象本身的地址。因此,如果使用base class object是不能“指向”derived class object的,object只能指向自身(变量名会被翻译为变量存储的地址,计算机就是根据这个地址访问变量的)。而使用指针或引用呢,根据指针或引用变量的值,就可以找到指向的对象,如果只是为了找到这个对象而不做任何其他操作,那么指向的对象是任何类型都无所谓,但是如果要读取对象的实际数据,就需要知道怎么解释这个位置上的数据了。因此使用指向derived class object的base class指针可以找到derived class object。但是为了调用derived class object中表现多态的函数,就需要另外一项设施了。是的,virtual table,就是它。

为什么是父类指针指向子类对象

virtual table是存放在object外的一张表,表里面放置了指向虚函数的指针vptr,嗯…实际实现中还在表的第一行存放了一个指向关于类定义本身一些信息的指针,typeid函数就是根据这个指针运作的。而在对象里只存储了指向这张表的指针,如果在每个对象中都放置一张同样的表岂不是很浪费空间?为了进一步节约空间,在不需要virtual table的对象里就不放置vptr,怎么确定哪些类需要?很简单,声明了virtual函数的那个类和他的子类都需要(基类有一张自己的表,子类也有一张自己的表,两个类里的vptr不会指向同一张表)!有了virtual table后,里面的函数指针怎么放置呢,六个法则:1)表头放置指向type_info的指针;2)第二项放置指向virtual析构函数的指针;3)其他虚函数按声明的顺序存放在接下来的表项里;4)子类中与父类中相同的虚函数(重载版本也要对应,这里说的相同不仅仅是函数名相同)所在的表项位置索引是一样的;5)子类改写了父类的虚函数,那么就放置子类改写后的函数指针,如果没有改写,就把父类中这个虚函数的指针拷贝过来,放在相应的位置上;6)子类新添加的虚函数按在子类中的添加顺序放在接下来的位置,这使得子类和父类的虚函数表不一样大,如果子类没有添加新的虚函数,那么子类与父类的虚函数表是一样大的。

virtual table已经准备就绪,多态就能展现了,将一个base class指针指向某个对象,这个对象可能是一个base class object,可能是derived class A object,也可能是一个derived class B object,这些都无所谓,只需要从这个对象占据的空间里,找到vptr,再根据函数名找到它的索引号,由索引号就能找到想要调用的函数所在的地址,于是多态得以实现。这个查找过程是由编译器执行的,编译器知道每个类中虚函数的索引号,当写下Base* pBase = new SomeClass,pBase->SomeVirtualFunc(),后一语句会在编译器内部转化为pBase->vptr【index】()。这样看来,程序并不是真正的运行期多态,需要的行为还是在编译期确定下来的,只是原来需要手工做的事情交由C++预处理器来做了。这就解释了为什么要为展现多态的基类添加一个virtual destructor,如果不是的话,虚析构函数就不会出现在virtual table中,当调用delete删除一个base class指针时,总是调用base class的析构函数,不会调用子类的析构函数,这就可能造成内存泄露。

以上只是简单继承中多态的展现,当使用多重继承时,子类如果从n个基类中继承,那么就会有n张virtual tables,每个基类导致子类添加一张virtual table,当使用base class A指针指向子类时,就会使用与base class A相关的那张virtual table,为了做到这一点,预处理器有需要做出更多的工作,如初始化或删除base class指针时,都要做出必要的指针调整步骤,以确保正确。如果使用的是虚拟继承,那么情况就更复杂了,这个我还没弄懂。

如果是用derived class指针指向base class object的话,那么就总是使用base class中的vptr,那么行为就总是属于base class的。一般而言,总是子类表现出更特殊的行为,也是子类们拥有更多的表现形式,而这正是多态的意思,如果以derived class指针指向base class object,那么就是把多态丢弃了。另一个问题也得到了解释,使用继承所要承受的“额外”开销:1)vptr们占用的空间;2)编译期更多的时间。但是这种额外并不绝对,想想为了表现多态自己手工打造而花费的空间和时间吧。

结语

好几次听人说起只要编程思想好,用C语言也能写出面向对象的程序来,是的,这句话不假,看完C++多态实现的方式,这句话的正确性得到进一步肯定。但是如果想用C语言写面向对象的程序,还不如趁早改为C++,那些重复的事情为什么不交给计算机做呢?省力而且更靠谱——这里是指使用预处理器。

时间: 2024-10-05 15:40:45

C++多态的实现的相关文章

C#多态

通过继承,一个类可以用作多种类型:可以用作它自己的类型.任何基类型,或者在实现接口时用作任何接口类型.这称为多态性.C# 中的每种类型都是多态的.类型可用作它们自己的类型或用作 Object 实例,因为任何类型都自动将 Object 当作基类型. 多态性不仅对派生类很重要,对基类也很重要.任何情况下,使用基类实际上都可能是在使用已强制转换为基类类型的派生类对象.基类的设计者可以预测到其基类中可能会在派生类中发生更改的方面.例如,表示汽车的基类可能包含这样的行为:当考虑的汽车为小型货车或敞篷汽车时

Java基础(八):多态

一.多态的理解: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作,如图所示: 多态性是对象多种表现形式的体现:现实中,比如我们按下 F1 键这个动作:如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档:如果当前在 Word 下弹出的就是 Word 帮助:在 Windows 下弹出的就是 Windows 帮助和支持:同一个事件发生在不同的对象上会产生不同的结果. 二.多态的优点和必要条件: 多态的优点:1. 消除类型之间的耦合关系2

当this指针成为指向之类的基类指针时,也能形成多态

this指针: 1)对象中没有函数,只有成员变量 2)对象调用函数,通过this指针告诉函数是哪个对象自己谁. 1 #include<iostream> 2 using namespace std; 3 class Shape 4 { 5 public: 6 //void cal_display(Shape* this) 7 void cal_display(){ 8 display(); 9 this->display(); 10 } 11 private: 12 virtual vo

Java多态

多态不是方法的重载,不是方法名一样方法的参数不一样,不是一个参数有多种态度就称之为多态,那是不正确的,如果这就是多态的话那么何必有方法的重载?直接改名多态就行了.父类 a = 子类对象 就是子类对象可以披上父类的衣服,只要穿上了父类的衣服就装扮成了父类 可以做父类的一些事情灵活性强.多态最重要的目的就是为了让子类转换成父类. 面向对象编程之上还有一种叫做面向功能编程,面向功能编程还可以转换成面向父类编程.比如:现实生活中,有小宝.大宝 大宝是小宝的父亲.有一天大宝不在家,小宝接到打给大宝的电话

C++中多态的实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的. 3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性. 4. 多态用虚函数来实现,结合动态绑定. 5. 纯虚函数是虚函数再加上= 0. 6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()= 0:即抽象类!必须在子类实

OC多态

多态:不同对象以自己的方式响应相同的消息的能力叫做多态. 由于每个类都属于该类的名字空间,这使得多态称为可能.类定义中的名字和类定义外的名字并不会冲突.类的实例变量和类方法有如下特点:和C语言中结构体中的数据成员一样,类的实例变量也位于该类独有的名字空间.类方法也同样位于该类独有的名字空间.与C语言中的方法名不同,类的方法名并不是一个全局符号.一个类中的方法名不会和其他类中同样的方法名冲突.两个完全不同的类可以实现同一个方法.方法名是对象接口的一部分.对象收到的消息的名字就是调用的方法的名字.因

多态的内存分析-转载

java运行时,在内存里分四个部分.栈,堆,数据区和代码区..举个例子String str=new String("AAA");str就放在栈里,字符串"AAA"放在堆里.所有的方法代码都放在了代码区. public class A{public void show(){System.out.println("A");}} public class B extends A{public void show(){System.out.println

多态and接口

一.多态 1.什么是多态? 解析:不同的对象对于同一个操作,做出的响应不同 具有表现多种形态的能力的特征 2.使用多态的优点 解析:为了实现统一调用 一个小例子:<父类类型作为参数> 父类(Pet) 子类(Gog,Penguin) 主人类(Master)测试类(Test) Pet public abstract class Pet { public abstract void eat(); } Dog public class Dog extends Pet{ @Override public

初始继承和多态

一.子类与父类 1.子类:父类 例如: Dog(子类):Anomal(父类) 子类(派生类)父类(基类和超类) 2.子类可以继承父类那些成员 (非私有成员,但是从技术角度,可以认为是父类的所有成员) 软件系统中的两个类符合is a时可以使用继承 例如: student is a person se is a employee 鸵鸟(ostrish)is a bird(错误结论!!!) ☆:继承模式下子类构造背后到底发生了什么? 如果我们想构建一个子类对象 //在Animal父类中 class A

2、C#面向对象:封装、继承、多态、String、集合、文件(上)

面向对象封装 一.面向对象概念 面向过程:面向的是完成一件事情的过程,强调的是完成这件事情的动作. 面向对象:找个对象帮你完成这件事情. 二.面向对象封装 把方法进行封装,隐藏实现细节,外部直接调用. 打包,便于管理,为了解决大型项目的维护与管理. 三.什么是类? 将相同的属性和相同方法的对象进行封装,抽象出 “类”,用来确定对象具有的属性和方法. 类.对象关系:人是类,张三是人类的对象. 类是抽象的,对象是具体的.对象可以叫做类的实例,类是不站内存的,对象才占内存. 字段是类的状态,方法是类执