1,多态是一种运行期绑定机制,通过这种机制,实现将函数名绑定到函数具体实现代码的目的。一个函数的名称与其入口地址是紧密相连的,入口地址是该函数在内存中的起始地址。如果对一个函数的绑定发生在运行时刻而非编译时刻,我们就称该函数是多态的。

2,C++多态的三个前提条件:(a)必须存在一个继承体系结构;(b)继承体系结构中的一些类必须具有同名的 virtual 成员函数(virtual 是关键字);(c)至少有一个基类类型的指针或者基类类型的引用可用来对 virtual 成员函数进行调用。

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4.  
  5. class TradesPerson {
  6. public:
  7. virtual void sayHi() {
  8. cout<<"Just hi."<<endl;
  9. }
  10. };
  11.  
  12. class Tinker : public TradesPerson {
  13. public:
  14. virtual void sayHi() {
  15. cout<<"Hi,I tinker."<<endl;
  16. }
  17. };
  18.  
  19. class Tailor : public TradesPerson {
  20. public:
  21. virtual void sayHi() {
  22. cout<<"Hi,I Tailor."<<endl;
  23. }
  24. };
  25.  
  26. int main() {
  27. TradesPerson *p;
  28. int which;
  29.  
  30. do {
  31. cout<<"1 == TradesPerson, 2 == Tinker, 3 == Tailor"<<endl;
  32. cin>>which;
  33. }while(which< || which > );
  34.  
  35. switch(which) {
  36. case : p = new TradesPerson; break;
  37. case : p = new Tinker; break;
  38. case : p = new Tailor; break;
  39. }
  40. p->sayHi();
  41. delete p;
  42.  
  43. return ;
  44. }

多态使用示例

3,因为基类类型的指针可以指向任何基类对象或派生类对象,所以上面程序我们并不需要强制类型转换。在上面的代码中,每个函数都使用了关键字 virtual ,但实际上并没有必要,因为当声明了基类的一个成员函数为虚函数后,那么即使该成员函数没有在派生类中被显式地声明为虚函数,但它在所有派生类中也将自动成为虚函数。在上面代码中,只是 TradesPerson 中声明为虚函数也是可以的,因为 Tinker::sayHi 仍然是虚函数,因为它与 TradesPerson::sayHi 具有相同的函数签名,而 Tradesperson::sayHi 是在基类中声明的虚函数。

4,如果虚函数在类声明之外定义,关键字 virtual 仅在函数声明时需要,不需在函数定义中使用 virtual 关键字。C++ 仅允许将成员函数定义为虚函数,顶层函数不能为虚函数。

5.1,C++ 使用 vtable (虚成员函数表)来实现虚成员函数的运行期绑定。虚成员函数表存在的用途是支持运行时查询,使得系统可以将某一函数名绑定到虚成员函数表中的特定入口地址。虚成员函数表的实现是与系统无关的。

  1. class B {
  2. public:
  3. virtual void m1() { /* */}
  4. virtual void m2() { /* */}
  5. };
  6.  
  7. class D : public B {
  8. public:
  9. virtual void m1() { /* */} // override m1
  10. };

vtable 示例

虚成员函数 入口地址示例 虚成员函数 入口地址示例
   B::m1     0x7723     D::m1     0x99a7
   B::m2     0x23b4     D::m2     0x23b4

在虚成员函数表中,对应于程序中的每一个虚成员函数,都有一个单独的入口地址,我们发现 B::m2 和 D::m2 有相同的入口地址(0x23b4),这是因为派生类 D 没有覆盖成员函数 m2,而是直接继承了其基类的 B。如果执行以下代码:

  1. int main() {
  2. B b;
  3. D d;
  4. B* p;
  5. //... // p is set to b's or d's address
  6. p->m1(); // vtable lookup for run-time binding
  7. //...
  8. }

vtable 使用示例

上述代码中,p->m1(); 绑定到虚成员函数表中的某一项,系统首先决定指针 p 指向哪个对象,如果 p 指向 B 的对象 b,系统将在虚成员函数表中查询 B::m1 的表项;如果指向 d,系统将在虚成员函数表中查询 D::m1 的入口地址,一旦查询完成,就可以执行相应的函数体。

note: 使用动态绑定的程序会影响效率,因为虚成员函数需要额外的存储空间,而且对虚成员函数表进行查询也需要额外的时间。

5.2,对象中的vptr指针什么时候初始化?

  1. #include <iostream>
  2.  
  3. using namespace std;
  4.  
  5. class Parent{
  6. public:
  7. Parent(int a=){
  8. this->a = a;
  9. print();
  10. }
  11. virtual void print(){ cout << "Parent" <<endl;}
  12. private:
  13. int a;
  14. };
  15.  
  16. class Son:public Parent{
  17. public:
  18. Son(int a=,int b=):Parent(a){
  19. this->b = b;
  20. print();
  21. }
  22. virtual void print(){ cout << "Son" <<endl;}
  23. private:
  24. int b;
  25. };
  26.  
  27. int main(){
  28. Son s;
  29. return ;
  30. }
  31.  
  32. // 输出结果
  33. // Parent
  34. // Son

虚表指针初始化过程

结论:Son s 的过程:

1)初始化 s.vptr 指针,初始化时分步;

2)  当执行父类的构造函数时,s.vptr 指向了父类的虚函数表,当父类构造函数运行完毕后,会把 s.vptr 指针指向子类的虚函数表

3)结论:子类的 s.vptr 指针初始化分步完成

6,构造函数和析构函数:构造函数不能是虚成员函数,但析构函数可以是虚成员函数。

7,虚析构函数:举一个例子来说明虚析构函数的重要性:

  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4.  
  5. class A {
  6. public:
  7. A() {
  8. cout<<endl<<"A() firing"<<endl;
  9. p = new char[]; // allocate 5 bytes
  10. }
  11. ~A() {
  12. cout<<"~A() firing"<<endl;
  13. delete[] p;
  14. }
  15. private:
  16. char* p;
  17. };
  18.  
  19. class Z : public A {
  20. public:
  21. Z() {
  22. cout<<"Z() firing"<<endl;
  23. q = new char[]; // allocate 500 bytes
  24. }
  25. ~Z() {
  26. cout<<"Z() firing"<<endl;
  27. delete[] q;
  28. }
  29. private:
  30. char* q;
  31. };
  32.  
  33. void f() {
  34. A* ptr;
  35. ptr = new Z();
  36. delete ptr;
  37. }
  38.  
  39. int main() {
  40. for(int i=;i<;++i)
  41. f();
  42. return ;
  43. }
  44.  
  45. /* output
  46.  
  47. A() firing
  48. Z() firing
  49. ~A() firing
  50.  
  51. A() firing
  52. Z() firing
  53. ~A() firing
  54.  
  55. A() firing
  56. Z() firing
  57. ~A() firing
  58. */

内存泄漏示例

在上述代码中,当我们通过 ptr 进行 delete 操作时,尽管 ptr 实际指向一个 Z 对象,但只有 ~A() 被调用,这是因为它们的析构函数不是虚成员函数,所以编译器实施的是静态绑定。编译器根据ptr 的数据类型 A* 来决定调用哪一个析构函数,因此,仅调用了~A(),而没有调用 ~Z(),这样就会造成内存泄漏。通过定义基类的析构函数 ~A() 为虚成员函数可以确保其派生类的析构函数也为虚成员函数。当通过 ptr 来删除其所指的对象时,编译器进行的是运行期绑定。在这里,因为 ptr 指向一个 Z 类型的对象,所以 ~Z() 被调用,接着 ~A() 也被调用,这是因为析构函数的调用是沿着继承树自下向上延伸的。

note: 通常来说,如果基类有一个指向动态分配内存的数据成员,并定义了负责释放这块内存的析构函数,就应该将这个析构函数声明为虚成员函数,这样做可以保证在以后添加该类的派生类时发挥多态性的作用。

8,对象成员函数和类成员函数:只有非静态成员函数才可以是虚成员函数,换句话说,只有对象成员函数才可以是虚成员函数。

  1. class C {
  2. public:
  3. static virtual void f(); // ERROR: static and virtual
  4. static void g(); // OK
  5. virtual h(); // OK
  6. };

错误示例

试图使一个成员函数既定义为虚成员函数又为静态函数,这是不允许的。

9,重载:在一个类中,成员函数可以有相同的函数名,只要它们的函数签名不同即可,我们将这种情况成为重载。重载与编译期绑定相对应,不管是成员函数还是顶层函数。编译器依据函数签名来进行绑定。在进行重载时,总是使用编译器绑定,在这个方面重载函数(不管是成员函数还是顶层函数)和虚函数是截然不同的,虚函数总是在运行期绑定。

  1. class C {
  2. C() { /* */ }
  3. C( int x ) { /* */}
  4. };
  5.  
  6. void f(double d) { /* */}
  7. void f(char c) { /* */}
  8.  
  9. int main() {
  10. C c1;
  11. C c2();
  12. f( 3.14 );
  13. f('z');
  14. //...
  15. }

重载函数使用示例

10,覆盖:假定基类 B 有一个成员函数 m,其派生类 D 也有一个具有相同函数签名的成员函数 m,如果这个成员函数是虚函数,则任何通过指针或引用对 m 的调用都会激活运行期绑定。对这种情况,我们称派生类的成员函数 D::m 覆盖了其基类的成员函数 B::m。如果成员函数不是虚函数,都 m 的任何调用均为编译器绑定。

  1. class B {
  2. public:
  3. void m() { cout<<"B::m"<<endl;}
  4. };
  5.  
  6. class D : class B {
  7. public:
  8. void m() { cout<<"D::m"<<endl;}
  9. };
  10.  
  11. int main() {
  12. B* p;
  13. p = new D;
  14. p->m(); // invoke m
  15. return ;
  16. }

覆盖示例

上述代码中,因为 m 不是虚函数,而在 C++ 中只有虚函数才会进行运行期绑定。编译器会使用 p 的数据类型 B* 进行绑定,结果是绑定到 B::m 。上述调用相当于 p->B::m。

11,遮蔽:假定基类拥有一个非虚函数 m ,其派生类 D 也有一个成员函数m,我们就说函数 D:m 遮蔽了继承而来的函数 B::m。如果派生类的同名成员函数与其基类的这个成员函数有不同的函数签名,那么这种遮蔽情况会相当复杂。

  1. class A {
  2. public:
  3. void m( int x ) { cout<<x<<endl; }
  4. };
  5.  
  6. class C : public A {
  7. public:
  8. void m() { cout<< "Hi"<<endl; }
  9. };
  10.  
  11. int main() {
  12. C c1;
  13. c1.m();
  14. // c1.m(26); // [Error] no matching function for call to 'C::m(int)'
  15.  
  16. return ;
  17. }

遮蔽示例

上面的程序将产生一条严重编译错误,因为 D 又定义了一个同名函数,因此 D 的本地函数 D::m 遮蔽了继承而来的函数 B::m 。要调用这个继承而来的带单个参数的基类函数 B::m,必须修改成以下格式:c.A::m(26);

note:虚函数和非虚函数都有可能产生名字遮蔽,实际上一旦派生类的虚函数不能覆盖基类的虚函数,就会产生虚函数遮蔽。将函数定义为虚函数并不能消除遮蔽现象,改正的办法就是显式调用 B::m;这样做虽然消除了编译错误,但不是好的编译风格,为了发挥多态的作用,B::m 和 D::m 应该具有相同的函数签名,而不仅是具有相同的函数名。

  1. class B {
  2. public:
  3. virtual void m(int x) { cout<<x<<endl;}
  4. };
  5.  
  6. class D : public B {
  7. public:
  8. virtual void m() { cout<<"Hi"<<endl;}
  9. };
  10.  
  11. int main() {
  12. D d1;
  13. d1.m();
  14. // d1.m(26); // Error: D's m takes no arguments
  15.  
  16. return ;
  17. }

虚函数遮蔽示例

12,名字共享:(a)顶层函数重载;(b)构造函数重载;(c)多态中的相同函数签名的函数。但是在类层次中共享函数名但函数签名不同时,将产生遮蔽,而遮蔽是非常危险的,建议要谨慎地运用这种遮蔽类型的名字共享机制。

13,抽象基类:抽象基类确保其派生类必须定义某些指定的函数,否则这个派生类就不能被实例化。要求 该类必须拥有一个纯虚成员函数,在纯虚成员函数声明的结尾加上 =0 就可以将这个函数定义为纯虚成员函数。

  1. class ABC {
  2. public:
  3. virtual void open() = ;
  4. };

纯虚成员函数

14,虽然不能创建一个抽象基类的对象,但抽象基类可以拥有派生类,从抽象基类派生来的类必须覆盖基类的所有纯虚成员函数,否则派生类也成为抽象基类,因而也不能用来创建对象。一个抽象基类可以有其他不是纯虚成员函数或甚至不是虚函数的成员函数,还可以有数据成员。抽象基类的成员可以是 private、protected 或 public。

15,定义纯虚成员函数的限制:只有虚函数才可以成为纯虚成员函数,非虚函数或顶层函数都不能声明为纯虚成员函数。

  1. void f() = ; // ERROR : not a virtual method
  2.  
  3. class {
  4. public:
  5. void open = ; // ERROR: not a virtual method
  6. };

纯虚函数错误定义示例

16,抽象基类作用:通过这种机制,可以用来指明某些虚函数必须被派生类覆盖,否则这些派生类就不能拥有对象。从这种意义上看,抽象基类实际上定义了一个公共接口,这个接口被所有从抽象基类派生的类共享。因为抽象基类通常只有 public 成员函数,所以经常使用关键字 struct(默认为 public ) 来声明抽象基类。

17,运行期类型识别(RTTI):一个基类指针不经过明确的转型操作,就能指向基类对象或派生类对象,反过来就不大一样了,将一个派生类指针指向基类对象是一种不明智的做法。当然,通过明确的转型操作可以做到这一点:

  1. class B {
  2. //...
  3. };
  4.  
  5. class D : public B {
  6. //...
  7. };
  8.  
  9. int main() {
  10. D* p;
  11. p = new B; // ERROR: explicit cast needed
  12. p = static_cast<D*>(new B); // caution
  13. //...
  14. return ;
  15. }

static_cast 用法示例

上述这种用法是合法的,但这种转型操作相当危险,可能会造成难以跟踪的运行期错误。static_cast 不能保证类型安全(type safely)。如果 p 不小心指向了一个没有定义 m 的 B 对象,将会导致错误。

  1. class B {
  2. f() { } // Note: no method m
  3. };
  4.  
  5. class D : public B {
  6. void m() { } // not in base class
  7. };
  8.  
  9. int main() {
  10. D* p;
  11. p = static_cast<D*>(new B);
  12. p->m(); // ERROR: there is no B::m
  13. return ;
  14. }

类型不安全使用示例

18,C++ 提供的 dynamic_cast 操作符可以在运行期检测某个转型动作是否类型安全。dynamic_cast 和 static_cast 有同样的语法,不过 dynamtic_cast 仅对多态类型(至少有一个虚函数的类)有效。

  1. class B {
  2. virtual f() { } // Note: no method m
  3. };
  4.  
  5. class D : public B {
  6. void m() { } // not in base class
  7. };
  8.  
  9. int main() {
  10. D* p;
  11. p = dynamic_cast< D* >(new B);
  12. if(p) // 如果转型动作安全,返回指向的对象指针 ptr
  13. p->m();
  14. else // 如果转型动作不安全,返回 false
  15. cout<<"Not safe for p to point to a B"<<endl;
  16.  
  17. return ;
  18. }

转换类型动作检查是否安全

19,dynamic_cast 的规则:假定基类 B 具有多态性,而类 D 是直接或间接从类 B 派生而来的。通过继承,类 D 也因此具有多态性,在这种情况下:

  (a)从派生类 D* 到基类 B* 的dynamic_cast 可以进行,这称为 向上转型(upcast)。

  (b)从基类 B* 到派生类 D* 的dynamic_cast 不能进行,这称为 向下转型(downcast)。

  假定类 A 和类 Z 都具有多态性,但它们之间不存在继承关系,在这种情况下,相互的转型均不能进行。

20,typeid 用法:操作符 typeid 返回一个 type_info 类对象的引用,type_info 是一个系统类,用来描述类型,这个操作符可以施加于类型名(包括类名)或 C++ 表达式。

  1. #include<typeinfo>
  2.  
  3. int main() {
  4. float x;
  5. long y;
  6. bool result = typeid(y) == typeid(x);
  7. cout<< boolalpha << result <<endl;
  8. return ;
  9. }

typeid 使用示例

c++中的类(class)-----笔记(类多态)的更多相关文章

  1. php笔记(七)PHP类于对象之多态

    <?php interface ICanEat{ public function eat($food);} class Human implements ICaneat{ public func ...

  2. java 学习笔记——类之间的关系之封装、继承与多态的详解

    封装 一个封装的简单例子 封装就是把对象的属性(状态)和方法(行为)结合在一起,并尽可能隐蔽对象的内部细节,成为一个不可分割的独立单位(即对象),对外形成一个边界,只保留有限的对外接口使之与外部发生联 ...

  3. 日历类和日期类转换 并发修改异常 泛型的好处 *各种排序 成员和局部变量 接口和抽象类 多态 new对象内存中的变化

    day07 ==和equals的区别? ==用于比较两个数值 或者地址值是否相同.  equals 用于比较两个对象的内容是否相同   String,StringBuffer.StringBuilde ...

  4. 【游戏开发】在Lua中实现面向对象特性——模拟类、继承、多态

    一.简介 Lua是一门非常强大.非常灵活的脚本语言,自它从发明以来,无数的游戏使用了Lua作为开发语言.但是作为一款脚本语言,Lua也有着自己的不足,那就是它本身并没有提供面向对象的特性,而游戏开发是 ...

  5. JavaFx 中常见的包和类(javafx笔记 )

    JavaFx 中常见的包和类(javafx笔记 ) 更多详细内容请参考<Pro JavaFX 8>. javafx.stage 包包含以下类: Stage 类 ​ Stage 类是任何 J ...

  6. python中的面向对象学习以及类的多态

    接下来类的第三个重要的特性:多态(一种接口,多种实现) 多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特 ...

  7. Java类的继承与多态特性-入门笔记

    相信对于继承和多态的概念性我就不在怎么解释啦!不管你是.Net还是Java面向对象编程都是比不缺少一堂课~~Net如此Java亦也有同样的思想成分包含其中. 继承,多态,封装是Java面向对象的3大特 ...

  8. Golang 中的 面向对象: 方法, 类, 方法继承, 接口, 多态的简单描述与实现

    前言: Golang 相似与C语言, 基础语法与C基本一致,除了广受争议的 左花括号 必须与代码同行的问题, 别的基本差不多; 学会了C, 基本上万变不离其宗, 现在的高级语言身上都能看到C的影子; ...

  9. 【转载】【游戏开发】在Lua中实现面向对象特性——模拟类、继承、多态

    [游戏开发]在Lua中实现面向对象特性——模拟类.继承.多态   阅读目录 一.简介 二.前提知识 三.Lua中实现类.继承.多态 四.总结 回到顶部 一.简介 Lua是一门非常强大.非常灵活的脚本语 ...

  10. Python笔记——类定义

    Python笔记——类定义 一.类定义: class <类名>: <语句> 类实例化后,可以使用其属性,实际上,创建一个类之后,可以通过类名访问其属性 如果直接使用类名修改其属 ...

随机推荐

  1. windows2012系统IE浏览器无法打开加载flashplayer内容

    添加角色和功能,用户界面和基础结构,桌面体检,安装完重启电脑

  2. 用myeclipse自动发布web程序

    在myeclipse忠配置好服务器 配置jdk 然后选择tomcat服务器

  3. sql中有一些保留字,当你的字段名是它的保留字时,这个时候sql语句的字段不加``就会报错

    sql中有一些保留字,当你的字段名是它的保留字时,这个时候sql语句的字段不加``就会报错

  4. ThinkPHP同时操作多个数据库

    除了在预先定义数据库连接和实例化的时候指定数据库连接外,我们还可以在模型操作过程中动态的切换数据库,支持切换到相同和不同的数据库类型.用法很简单, 只需要调用Model类的db方法,用法: $this ...

  5. mybatis初识

    mybatis采用弱连接,在一定程度上集中管理了sql的语句编写,又实现了自动映射bean. 此处以最基础的mybatis连接为例: 引入jar包: mybatis-3.4.5.jar ojdbc-6 ...

  6. mongo aggregate 用法记录

    mongo 聚合查询查询还是很方便的,做下记录     依赖的jar是org.springframework.data.mongodb 1.9.6  低版本可能不支持. 数据结构  大概是  这是一份 ...

  7. using关键字在C#中的3种用法

    using 关键字有两个主要用途:  (一).作为指令,用于为命名空间创建别名或导入其他命名空间中定义的类型.  (二).作为语句,用于定义一个范围,在此范围的末尾将释放对象. (一).作为指令 1. ...

  8. 尚硅谷springboot学习13-配置的加载

    配置可以有很多不同的来源,也有不同的加载顺序,下面来具体的看一下 配置文件加载位置 spring boot 启动会扫描以下位置的application.properties或者application. ...

  9. 单件模式——Head First

    一.定义 单件模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点. 二.适用性 1.当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时. 2.当这个唯一实 ...

  10. jQuery链式语法演示

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...