C++虚函数和多态学习笔记 1、什么是虚函数和多态 虚函数是在类中被声明为virtual的成员函数,当编译器看到通过指针或引用调用此类函数时,对其执行晚绑定,即通过指针(或引用)指向的类的类型信息来决定该函数是哪个类的。通常此类指针或引用都声明为基类的,它可以指向基类或派生类的对象。多态指同一个方法根据其所属的不同对象可以有不同的行为(根据自己理解,不知这么说是否严谨)。举个例子说明虚函数、多态、早绑定和晚绑定: 李氏两兄妹(哥哥和妹妹)参加姓氏运动会(不同姓氏组队参加),哥哥男子项目比赛,妹妹参加女子项目比赛,开幕式有一个参赛队伍代表发言仪式,兄妹俩都想去露露脸,可只能一人去,最终他们决定到时抓阄决定,而组委会也不反对,它才不关心是哥哥还是妹妹来发言,只要派一个姓李的来说两句话就行。运动会如期举行,妹妹抓阄获得代表李家发言的机会,哥哥参加了男子项目比赛,妹妹参加了女子项目比赛。比赛结果就不是我们关心的了。现在让我们来做个类比(只讨论与运动会相关的话题):(1)类的设计:李氏兄妹属于李氏家族,李氏是基类(这里还是抽象的纯基类),李氏又派生出两个子类(李氏男和李氏女),李氏男会所有男子项目的比赛(李氏男的成员函数),李氏女会所有女子项目的比赛(李氏女的成员函数)。姓李的人都会发言(基类虚函数),李氏男和李氏女继承自李氏当然也会发言,只是男女说话声音不一样,内容也会又差异,给人感觉不同(李氏男和李氏女分别重新定义发言这个虚函数)。李氏两兄妹就是李氏男和李氏女两个类的实体。(2)程序设计:李氏兄妹填写参赛报名表。(3)编译:李氏兄妹的参赛报名表被上交给组委会(编译器),哥哥和妹妹分别参加男子和女子的比赛,组委会一看就明白了(早绑定),只是发言人选不明确,组委会看到报名表上写的是“李家代表”(基类指针),组委会不能确定到底是谁,就做了个备注:如果是男的,就是哥哥李某某;如果是女的,就是妹妹李某某(晚绑定)。组委会做好其它准备工作后,就等运动会开始了(编译完毕)。(4)程序运行:运动会开始了(程序开始运行),开幕式上我们听到了李家妹妹的发言,如果是哥哥运气好抓阄胜出,我们将听到哥哥的发言(多态)。然后就是看到兄妹俩参加比赛了。。。但愿这个比喻说清楚了虚函数、多态、早绑定和晚绑定的概念和它们之间的关系。再说一下,早绑定指编译器在编译期间即知道对象的具体类型并确定此对象调用成员函数的确切地址;而晚绑定是根据指针所指对象的类型信息得到类的虚函数表指针进而确定调用成员函数的确切地址。
2、揭密晚绑定的秘密编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟。 编译器对每个包含虚函数的类创建一个表(称为V TA B L E)。在V TA B L E中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V
TA B L E、初始化V P T R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。(《C++编程思想》)在任何类中不存在显示的类型信息,可对象中必须存放类信息,否则类型不可能在运行时建立。那这个类信息是什么呢?我们来看下面几个类: class no_virtual { public: void fun1() const{} int fun2() const { return a; } private:
int a; } class one_virtual { public: virtual void fun1() const{} int fun2() const { return a; } private: int a; } class two_virtual { public: virtual void fun1() const{} virtual int fun2() const { return a; } private: int a; } 以上三个类中: no_virtual没有虚函数,sizeof(no_virtual)=4,类no_virtual的长度就是其成员变量整型a的长度;
one_virtual有一个虚函数,sizeof(one_virtual)=8; two_virtual有两个虚函数,sizeof(two_virtual)=8; 有一个虚函数和两个虚函数的类的长度没有区别,其实它们的长度就是no_virtual的长度加一个void指针的长度,它反映出,如果有一个或多个虚函数,编译器在这个结构中插入一个指针( V P T R)。在one_virtual 和two_virtual之间没有区别。这是因为V P T R指向一个存放地址的表,只需要一个指针,因为所有虚函数地址都包含在这个表中。这个VPTR就可以看作类的类型信息。那我们来看看编译器是怎么建立VPTR指向的这个虚函数表的。先看下面两个类:
class base { public: void bfun(){} virtual void vfun1(){} virtual int vfun2(){} private: int a; } class derived : public base { public: void dfun(){} virtual void vfun1(){} virtual int vfun3(){} private: int b; } 两个类VPTR指向的虚函数表(VTABLE)分别如下: base类 —————— VPTR——>
|&base::vfun1 | —————— |&base::vfun2 | —————— derived类 ——————— VPTR——> |&derived::vfun1 | ——————— |&base::vfun2 | ——————— |&derived::vfun3 | ——————— 每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。
VPTR常常位于对象的开头,编译器能很容易地取到VPTR的值,从而确定VTABLE的位置。VPTR总指向VTABLE的开始地址,所有基类和它的子类的虚函数地址(子类自己定义的虚函数除外)在VTABLE中存储的位置总是相同的,如上面base类和derived类的VTABLE中vfun1和vfun2的地址总是按相同的顺序存储。编译器知道vfun1位于VPTR处,vfun2位于VPTR+1处,因此在用基类指针调用虚函数时,编译器首先获取指针指向对象的类型信息(VPTR),然后就去调用虚函数。如一个base类指针pBase指向了一个derived对象,那pBase->vfun2()被编译器翻译为
VPTR+1 的调用,因为虚函数vfun2的地址在VTABLE中位于索引为1的位置上。同理,pBase->vfun3()被编译器翻译为 VPTR+2的调用。这就是所谓的晚绑定。我们来看一下虚函数调用的汇编代码,以加深理解。 void test(base* pBase) { pBase->vfun2(); } int main(int argc, char* argv[]) { derived td; test(&td); return 0; } derived td;编译生成的汇编代码如下: mov DWORD
PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable' 由编译器的注释可知,此时PTR _td$[esp+24]中存储的就是derived类的VTABLE地址。 test(&td);编译生成的汇编代码如下: lea eax, DWORD PTR _td$[esp+24] mov DWORD PTR __$EHRec$[esp+32], 0 push eax call ?test@@YAXPAVbase@@@Z ; test 调用test函数时完成了如下工作:取对象td的地址,将其压栈,然后调用test。
pBase->vfun2();编译生成的汇编代码如下: mov ecx, DWORD PTR _pBase$[esp-4] mov eax, DWORD PTR [ecx] jmp DWORD PTR [eax+4] 首先从栈中取出pBase指针指向的对象地址赋给ecx,然后取对象开头的指针变量中的地址赋给eax,此时eax的值即为VPTR的值,也就是VTABLE的地址。最后就是调用虚函数了,由于vfun2位于VTABLE的第二个位置,相当于 VPTR+1,每个函数指针是4个字节长,所以最后的调用被编译器翻译为
jmp DWORD PTR [eax+4]。如果是调用pBase->vfun1(),这句就该被编译为jmp DWORD PTR [eax]。 [code]#include [/code] using std::cout; using std::endl; class Shape { public: void function1() { cout << "This is call Shape's function1!" << endl; //这里并没有用虚函数,所以不能实现多态 } virtual void
function2() { cout << "This is call virtual function2 of Shape class!" << endl; //这里用了虚拟函数,所以可以实现多态 } }; class Rect: public Shape { public: void function1() { cout << "This is call Rect's function1!" << endl; //不能实现多态 } virtual void function2() { cout << "This
is call virtual function2 of Rect class!" << endl; //实现多态 } }; class Circle: public Shape { public: void function1() { cout << "This is call Circle's function1!" << endl; //不能实现多态 } virtual void function2() { cout << "This is call virtual function2 of Circle
class!" << endl; //实现多态 } }; int main() { Shape *shapeptr; Shape shape; Rect rect; Circle circle; shapeptr = &shape; shapeptr->function1(); //调用Shape类里的 shapeptr->function2(); //调用Shape类里的 cout << endl; shapeptr = ▭ shapeptr->function1(); //调用Shape类里的,因为该函数不是虚函数,所以不能实现多态
shapeptr->function2(); //调用Rect类里的,因为该函数是虚函数,实现了多态 cout << endl; shapeptr = &circle; shapeptr->function1(); //调用Shape类里的,因为该函数不是虚函数,所以不能实现多态 shapeptr->function2(); //调用circle类里的,因为该函数是虚函数,实现了多态 cout << endl; return 0; } 其实虚函数的作用是代替了一个switch结构,我们的基类指针可以指向派生类对象,而在这个过程中,虚函数必须是指针所指实际对象的类里的函数,就是刚才例子里我们看到的,指针是基类的,而指向派生类对象,但执行时调用的是派生类里的虚函数,而對於非虚函数则调用的仍然是基類里的函数
分享到:
相关推荐
C++ 类和对象多态学习笔记 本节笔记主要介绍了 C++ 中的多态概念,包括静态多态和动态多态,及其使用条件和优点。 多态是 C++ 面向对象三大特性之一,分为两类:静态多态和动态多态。静态多态是指函数重载和运算符...
5. 虚函数的实现是C++实现多态的核心。虚函数允许基类指针或引用调用派生类对象中的重写版本函数,这是实现运行时多态的关键机制。基类通过维护一个虚函数表(虚表),其中包含了指向虚函数的指针,每个派生类可以...
【C语言和C++Builder学习笔记】 C语言和C++是两种广泛使用的编程语言,它们在软件开发领域占据着重要地位。C语言以其简洁、高效和底层操作能力著称,而C++则在C的基础上引入了面向对象编程,极大地扩展了其应用范围...
在C++编程语言中,虚...总的来说,理解和掌握虚函数的使用是C++中实现面向对象设计的关键,它允许代码更加灵活,具有更好的扩展性和可维护性。在设计类层次结构时,合理地运用虚函数可以提高程序的复用性和可适应性。
Effective C++是一本深入探讨C++编程...总的来说,理解并有效地利用C++的虚函数和虚拟析构函数是编写高效、可维护代码的重要方面。通过遵循Effective C++中的最佳实践,开发者可以写出更健壮、更易于扩展的C++程序。
在C++中,虚函数是实现多态性的重要机制,特别是在面向对象编程中。当一个类含有虚函数时,编译器会为该类创建一个虚拟表(VTABLE),其中包含了类中所有虚函数的地址。这个VTABLE使得在运行时能够根据对象的实际...
C++学习笔记和常见面试知识点,C++11特性,包括多态、虚表、移动语义、友元函数、符号重载、完美转发、智能指针、const和static、数组指针和指针数组、struct内存对齐、enum和union关键字等等。帮助了解C++的特性,...
多态则是通过虚函数和接口实现,使得不同的对象能够对同一消息作出不同的响应。 在C++中,模板和异常处理也是重要的部分。模板用于实现泛型编程,可以创建可以在不同数据类型上工作的函数或类。异常处理则提供了...
7. **多态**:多态分为静态多态(函数重载、运算符重载)和动态多态(虚函数、纯虚函数、抽象类)。动态多态主要体现在运行时,通过指针或引用调用父类方法实现不同子类的特定实现。 8. **模板**:C++的模板功能...
- 多态与继承:掌握虚函数、抽象类、纯虚函数的用法,理解继承的实现原理。 - 面向对象编程:封装、继承、多态三大特性,以及模板和STL的运用。 - 构造与析构:理解构造函数和析构函数的作用,了解拷贝构造和移动...
2. **面向对象编程**:C++的核心特性之一是其面向对象编程(OOP)概念,包括封装、继承和多态。封装允许我们将数据和操作这些数据的方法打包到一个类中。继承使我们能创建新类,这些新类可以从现有类(基类)继承...
笔记还涉及了一些高级主题,如运算符重载、静态成员、动态内存分配、虚函数、多态、抽类象、虚析构和动态绑定等。这些都是C++面向对象编程的关键特性,它们使得C++能够实现更灵活的代码设计和复用。此外,还提到了...
多态性主要通过虚函数和纯虚函数实现,允许不同的子类对同一函数有不同的实现。 4. **模板**:C++的模板机制使得我们可以创建泛型代码,不仅可以用在函数上,也可以用于定义泛型类,增加了代码的灵活性和可重用性。...
通过这些学习笔记,我们可以了解到《Effective C++》不仅为我们提供了C++编程中的一些基本和高级技巧,也教会我们如何遵循最佳实践,编写出更加高效、健壮的C++代码。学习这些知识点,对于提高C++编程能力和解决实际...
6. **继承与多态**:派生类、虚函数、抽象类和接口,以及多态性的实现。 7. **模板**:泛型编程,包括函数模板和类模板。 8. **异常处理**:try-catch 语句用于错误处理。 9. **STL 使用**:容器、迭代器的使用,...
多态是指同一消息可以有不同的行为,这在运行时通过虚函数和抽象类实现。 10. **模板**:C++的模板机制允许创建泛型代码,可用于不同类型的数据,增加了代码的通用性。 11. **异常处理**:C++的异常处理机制提供了...
8. **虚函数和多态性**:虚函数是实现多态的关键,它允许通过基类指针或引用调用派生类的成员函数,实现动态绑定。多态性增强了代码的灵活性和可扩展性。 9. **string类与字符串处理**:C++标准库中的`std::string`...
这份"C++培训课程详细学习笔记"是深入理解和掌握C++语言的重要资源,旨在帮助初学者及有经验的开发者巩固基础,提升技能。 一、C++基础 1. 变量与数据类型:C++支持基本数据类型(如int、float、char)以及构造数据...
《C++ Primer 学习笔记》是一份针对C++初学者的重要参考资料,它基于C++ Primer第三版的内容,旨在帮助读者深入理解C++这门强大的编程语言。C++ Primer是学习C++的经典书籍,以其全面、深入且易于理解的讲解闻名,这...