虚函数

C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新

定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。


+++++++++++++++++++++++++++++++++


++++++++++++++++++++++++++++++++



什么是C++虚函数、虚函数的作用和使用方法

我们知道,在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。例如在例12.1(具体代码请查看: C++多态性的一个典型例子)程序中,在Circle类中定义了 area函数,在Circle类的派生类Cylinder中也定义了一个area函数。这两个函数不仅名字相同,而且参数个数相同(均为0),但功能不同,函数体是不同的。前者的作用是求圆面积,后者的作用是求圆柱体的表面积。这是合法的,因为它们不在同一个类中。 编译系统按照同名覆盖的原则决定调用的对象。在例12.1程序中用cy1.area( ) 调用的是派生类Cylinder中的成员函数area。如果想调用cy1 中的直接基类Circle的area函数,应当表示为 cy1.Circle::area()。用这种方法来区分两个同名的函数。但是这样做 很不方便。

人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。 在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量 pt 赋以不同的值(使之指向不同的类对象)即可。

打个比方,你要去某一地方办事,如果乘坐公交车,必须事先确定目的地,然后乘坐能够到达目的地的公交车线路。如果改为乘出租车,就简单多了,不必查行车路线,因为出租车什么地方都能去,只要在上车后临时告诉司机要到哪里即可。如果想访问多个目的地,只要在到达一个目的地后再告诉司机下一个目的地即可,显然,“打的”要比乘公交车 方便。无论到什么地方去都可以乘同—辆出租车。这就是通过同一种形式能达到不同目的的例子。

C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

请分析例12.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。

[例12.2] 基类与派生类中有同名函数。在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。
  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. //声明基类Student
  5. class Student
  6. {
  7. public:
  8. Student(int, string,float);//声明构造函数
  9. void display();//声明输出函数
  10. protected: //受保护成员,派生类可以访问
  11. int num;
  12. string name;
  13. float score;
  14. };
  15. //Student类成员函数的实现
  16. Student::Student(int n,stringnam,float s)//定义构造函数
  17. {
  18. num=n;
  19. name=nam;
  20. score=s;
  21. }
  22. void Student::display()//定义输出函数
  23. {
  24. cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
  25. }
  26. //声明公用派生类Graduate
  27. class Graduate:public Student
  28. {
  29. public:
  30. Graduate(int, string,float,float);//声明构造函数
  31. void display();//声明输出函数
  32. private:float pay;
  33. };
  34. // Graduate类成员函数的实现
  35. void Graduate::display()//定义输出函数
  36. {
  37. cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
  38. }
  39. Graduate::Graduate(int n,stringnam,float s,float p):Student(n,nam,s),pay(p){}
  40. //主函数
  41. int main()
  42. {
  43. Student stud1(1001,"Li",87.5);//定义Student类对象stud1
  44. Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1
  45. Student *pt=&stud1;//定义指向基类对象的指针变量pt
  46. pt->display();
  47. pt=&grad1;
  48. pt->display();
  49. return 0;
  50. }
运行结果如下:num:1001(stud1的数据)name:Liscore:87.5num:2001 (grad1中基类部分的数据)name:wangscore:98.5假如想输出grad1的全部数据成员,当然也可以采用这样的方法:通过对象名调用display函数,如grad1.display(),或者定义一个指向Graduate类对象的指针变量ptr,然后使ptr指向gradl,再用ptr->display()调用。这当然是可以的,但是如果该基类有多个派生类,每个派生类又产生新的派生类,形成了同一基类的类族。每个派生类都有同名函数display,在程序中要调用同一类族中不同类的同名函数,就要定义多个指向各派生类的指针变量。这两种办法都不方便,它要求在调用不同派生类的同名函数时采用不同的调用方式,正如同前面所说的那样,到不同的目的地要乘坐指定的不同的公交车,一一 对应,不能搞错。如果能够用同一种方式去调用同一类族中不同类的所有的同名函数,那就好了。用虚函数就能顺利地解决这个问题。下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即    virtual void display( );这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:num:1001(stud1的数据)name:Liscore:87.5num:2001 (grad1中基类部分的数据)name:wangscore:98.5pay=1200 (这一项以前是没有的)看!这就是虚函数的奇妙作用。现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。用同一种调用形式“pt->display()”,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有 不同的响应方式。说明:本来基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。在程序修改前,是无法通过基类指针去调用派生类对象中的成员函数的。虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。 要注意的是,只有用virtual声明了虚函数后才具有以上作用。如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数是不行的

虚函数的以上功能是很有实用意义的。在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类开发的时间。但是,从基类继承来的某些成员函数不完全适应派生类的需要,例如在例12.2中,基类的display函数只输出基类的数据,而派生类的display函数需要输出派生类的数据。过去我们曾经使派生类的输出函数与基类的输出函数不同名(如display和display1),但如果派生的层次多,就要起许多不同的函数名,很不方便。如果采用同名函数,又会发生同名覆盖。

利用虚函数就很好地解决了这个问题。可以看到:当把基类的某个成员函数声明为虚函数后,允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。虚函数的使用方法是:
  1. 在基类用virtual声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。
  2. 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
  3. 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
  4. 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

+++++++++++++++++++++++++++++++++

++++++++++++++++++++++++++++++

1.当在构造函数中调用虚函数时,虚函数表现为该类中虚函数的

行为,即父类构造函数调用虚函数,则虚函数为父类中的虚函

数;子类构造函数中调用虚函数,则调用的是子类中的虚函数;

2.如果不是在构造函数中调用虚函数,则会首先查看虚函数表,

如果有实例实现,则调用实例。比如:父类中有虚函数

watchTv,则调用父类中watchTv时,则因为子类实现了

watchTv,则调用子类的watchTv。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

虚函数必须是类的成员函数,但不能是静态成员函数。如果在圆形的类中定义一个圆心的坐标,并且坐标是在堆中申请的内存,则在main函数中通过父类指针操作子类对象的成员函数的时候是没有问题的可是在销毁对象内存的时候则只是执行了父类的析构函数,子类的析构函数却没有执行,会导致内存泄漏如果delete后边跟父类的指针则只会执行父类的析构函数,如果delete后面跟的是子类的指针,那么它即会执行子类的析构函数,也会执行父类的析构函数普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。

静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。

内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。构造函数不能是虚函数,否则会出现编译错误。虚函数实现原理1)函数指针:函数的指针与普通的指针本质上是一样的,也是由四个基本的内存单元组成,存储着内存的地址,这个地址就是函数的首地址。2)多态的实现原理虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针,这个指针指向一个虚函数表的起始位置,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA函数的覆盖和隐藏aaaaaaaaaaaaaaaaaaaaaaaaa父类和子类出现同名函数称为隐藏。    父类对象.函数函数名(...);     //调用父类的函数    子类对象.函数名(...);           //调用子类的函数      子类对象.父类名::函数名(...);//子类调用从父类继承来的函数。bbbbbbbbbbbbbbbbbbbbbbbbbbb父类和子类出现同名虚函数称为覆盖    父类指针=new 子类名(...);父类指针->函数名(...);//调用子类的虚函数。cccccccccccccccccccccccccccc虚析构函数的实现原理[:->虚析构函数的特点:    当我们在父类中通过virtual修饰析构函数之后,通过父类指针指向子类对象,通过delete接父类指针就可以释放掉子类对象[:->理论前提:    执行完子类的析构函数就会执行父类的析构函数BBBBBBBBBBBBBB虚析构函数的实现原理:

      如果父类当中定义了虚析构函数,那么父类的虚函数表当中就会

有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子

类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是

子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete

接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而

找到虚函数表,再虚函数表中找到子类的虚析构函数,从而使得子类的析构

函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。

这个是虚析构函数的实现原理。

+++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++

子类被构造的时候回先调用父类的构造函数
子类析构的 时候先析构子类后析构父类
如果直接用子类构造一个父类的对象,删除这个父类的对象不会调用子类的析构函数(父类的析构函数为虚函数除外)
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

示例代码:
  1. //A是一个父类 , 析构函数不是虚函数  
  2. class A  
  3. {  
  4. public:  
  5.      A()  
  6.     {  
  7.         cout << " A constructor" << endl;  
  8.     }  
  9.       ~A()  
  10.     {  
  11.         cout << " A destructor" << endl;  
  12.     }  
  13. };  
  14.   
  15. //B是A的子类  
  16. class B : public A  
  17. {  
  18. public:  
  19.     B()  
  20.     {  
  21.         cout << " B constructor" << endl;  
  22.     }  
  23.     ~B()  
  24.     {  
  25.         cout << " B destructor" << endl;  
  26.     }  
  27. };  
  28.   
  29. //C是一个父类 , 析构函数是虚函数  
  30. class C  
  31. {  
  32. public:  
  33.     C()  
  34.     {  
  35.         cout << " C constructor" << endl;  
  36.     }  
  37.     virtual ~C()  
  38.     {  
  39.         cout << " C destructor" << endl;  
  40.     }  
  41. };  
  42.   
  43. //D是C的子类  
  44. class D : public C  
  45. {  
  46. public:  
  47.   
  48.     D()  
  49.     {  
  50.         cout << " D constructor" << endl;  
  51.     }  
  52.     ~D()  
  53.     {  
  54.         cout << " D destructor" << endl;  
  55.     }  
  56. };  
测试程序
  1. A *a = new B();  
  2.     delete a;  
  3.     cout << "-----------------------------" << endl<<endl;  
  4.     B * b = new B();  
  5.     delete b;  
  6.     cout << "-----------------------------" << endl << endl;  
  7.     C *c = new D();  
  8.     delete c;  
先分析下输出的顺序
A *a = new B();//用子类B构造一个父类A的对象 先调用父类A的构造方法,然后调用子类B的构造方法delete a;//删除父类,这时候由于A的析构函数不是虚函数,所以直接调用父类A的构造方法,不会调用子类B的构造方法cout << "-----------------------------" << endl<<endl;B * b = new B();//先调用父类A的构造方法,然后调用子类B的构造方法delete b;//删除子类,这时候会先调用子类B的析构函数,再调用父类A的析构函数cout << "-----------------------------" << endl << endl;C *c = new D();//同理,先调用父类C的构造方法,然后调用子类D的构造方法delete c;//删除父类,这时候由于A的析构函数是虚函数,所以直接先调用子类D的析构函数,再调用父类C的构造方法
哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈
运行结果:
昨天面试的时候面试官出了道此知识点的小题,说难倒了好多人,当时信心满满的写出了答案,还解释了一通,结果错了 (尴了个尬)
哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

总结:1、纯虚函数声明如下:virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

在程序中不是通过不同的对象名去不同派生层中的同名函数,而是通过指针调用它们。只需要在调用前给指针变量pt赋以不同的值(使之指向不同的类对象) 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

比如:模板技术,RTTI技术,虚函数技术

 Cat()
    {
        cout<<"Cat类 无参构造函数"<<endl;
    }

    Cat(Cat& obj)
    {
        cout<<"Cat类 拷贝构造函数"<<endl;
    }

    ~Cat()
    {
        cout<<"Cat类 析构函数 "<<endl;
    }

指针的类型

int *ptr; //指针的类型是int *  
char *ptr; //指针的类型是char *  
int **ptr; //指针的类型是 int **  
int (*ptr)[3]; //指针的类型是 int(*)[3]  
int *(*ptr)[4]; //指针的类型是 int *(*)[4]
int *ptr; //指针所指向的类型是int  
char *ptr; //指针所指向的的类型是char  
int **ptr; //指针所指向的的类型是 int *  
int (*ptr)[3]; //指针所指向的的类型是 int()[3]  
int *(*ptr)[4]; //指针所指向的的类型是 int *()[4]



BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

 C++编程语言是一款应用广泛,支持多种程序设计的计算机编程语言。我们今天就会为大家详细介绍其中C++多态性的一些基本知识,以方便大家在学习过程中对此能够有一个充分的掌握。

  多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphism),字面意思多种形状。
  C++多态性是通过虚函数来实现的,虚函数允许子类重新定义

成员函数,而子类重新定义父类的做法称为覆盖(override),或


者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
  多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。



哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈


  那么多态的作用是什么呢,封装可以使得代码模块化,继承

可以扩展已存在的代码,他们的目的都是为了代码重用。而多态

的目的则是为了接口重用。也就是说,不论传递过来的究竟是那

个类的对象,函数都能够通过同一个接口调用到适应各自对象的


实现方法。


  最常见的用法就是声明基类的指针,利用该指针指向任意一

个子类对象,调用相应的虚函数,可以根据指向的子类的不同而

实现不同的方法。如果没有使用虚函数的话,即没有利用C++多

态性,则利用基类指针调用相应的函数的时候,将总被限制在基

类函数本身,而无法调用到子类中被重写过的函数。因为没有多

态性,函数调用的地址将是一定的,而固定的地址将始终调用到

同一个函数,这就无法实现一个接口,多种方法的目的了。


哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈

笔试题目:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class A  
  5. {  
  6. public:  
  7.     void foo()  
  8.     {  
  9.         printf("1\n");  
  10.     }  
  11.     virtual void fun()  
  12.     {  
  13.         printf("2\n");  
  14.     }  
  15. };  
  16. class B : public A  
  17. {  
  18. public:  
  19.     void foo()  
  20.     {  
  21.         printf("3\n");  
  22.     }  
  23.     void fun()  
  24.     {  
  25.         printf("4\n");  
  26.     }  
  27. };  
  28. int main(void)  
  29. {  
  30.     A a;  
  31.     B b;  
  32.     A *p = &a;  
  33.     p->foo();  
  34.     p->fun();  
  35.     p = &b;  
  36.     p->foo();  
  37.     p->fun();  
  38.     return 0;  
  39. }  
       第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
    第二个输出结果就是1、4。p->foo()和p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。
  笔试的题目中还有一个另类测试方法。即

       B *ptr = (B *)&a;  ptr->foo();  ptr->fun();
  问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
  并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。
  而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //小结:1、有virtual才可能发生多态现象  
  2. // 2、不发生多态(无virtual)调用就按原类型调用  
  3. #include<iostream>  
  4. using namespace std;  
  5.   
  6. class Base  
  7. {  
  8. public:  
  9.     virtual void f(float x)  
  10.     {  
  11.         cout<<"Base::f(float)"<< x <<endl;  
  12.     }  
  13.     void g(float x)  
  14.     {  
  15.         cout<<"Base::g(float)"<< x <<endl;  
  16.     }  
  17.     void h(float x)  
  18.     {  
  19.         cout<<"Base::h(float)"<< x <<endl;  
  20.     }  
  21. };  
  22. class Derived : public Base  
  23. {  
  24. public:  
  25.     virtual void f(float x)  
  26.     {  
  27.         cout<<"Derived::f(float)"<< x <<endl;   //多态、覆盖  
  28.     }  
  29.     void g(int x)  
  30.     {  
  31.         cout<<"Derived::g(int)"<< x <<endl;     //隐藏  
  32.     }  
  33.     void h(float x)  
  34.     {  
  35.         cout<<"Derived::h(float)"<< x <<endl;   //隐藏  
  36.     }  
  37. };  
  38. int main(void)  
  39. {  
  40.     Derived d;  
  41.     Base *pb = &d;  
  42.     Derived *pd = &d;  
  43.     // Good : behavior depends solely on type of the object  
  44.     pb->f(3.14f);   // Derived::f(float) 3.14  
  45.     pd->f(3.14f);   // Derived::f(float) 3.14  
  46.   
  47.     // Bad : behavior depends on type of the pointer  
  48.     pb->g(3.14f);   // Base::g(float)  3.14  
  49.     pd->g(3.14f);   // Derived::g(int) 3   
  50.   
  51.     // Bad : behavior depends on type of the pointer  
  52.     pb->h(3.14f);   // Base::h(float) 3.14  
  53.     pd->h(3.14f);   // Derived::h(float) 3.14  
  54.     return 0;  
  55. }  
令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
上面的程序中:
(1)函数Derived::f(float)覆盖了Base::f(float)。
(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。


C++纯虚函数
 一、定义
  纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 
  virtual void funtion()=0 
二、引入原因
   1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 
   2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
三、相似概念
   1、多态性 
  指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。 
  a、编译时多态性:通过重载函数实现 
  b、运行时多态性:通过虚函数实现。 

  2、虚函数 
  虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
  3、抽象类 
  包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象

BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值