1:重写与重载的区别?什么时候重写?什么时候重载?
#include <iostream> /* run this program using the console pauser or add your own getch, system("pause") or input loop */ using namespace std; class Parent { public: virtual void func() { cout<<"void func()"<<endl; } virtual void func(int i) { cout<<"void func(int i)"<<endl; } virtual void func(int i, int j) { cout<<" void func(int i; int j)"<<endl; } }; class Child: public Parent { public: virtual void func(int i,int j) { cout<<" void func(int i; int j)"<<" "<<i+j<<endl; } void func(int i, int j, int k) { cout<<""<<endl; } }; void run(Parent* p) { p->func(1,2); } int main(int argc, char** argv) { Parent p; p.func(); Child c; //c.func();//ERROR :45 9 E:\C++源代码\0903\main4.cpp [Error] no matching function for call to 'Child::func()',子类无法重载父类的同名函数 //重载只能发生在一个类中,不能发生在两个类之间。在两个类之间发生的是名称覆盖 c.Parent::func();//只能通过域名操作符来调用 run(&p); run(&c); return 0; } /* 函数重载: 必须在同一个类中进行; 子类无法重载父类的函数,父类同名函数将被覆盖 重载是在编译期间根据参数类型和个数决定调用函数 函数重写: 必须发生于父类与子类之间 并且父类与子类中的函数必须由完全相同的原型 使用virtual声明之后能够产生多态 多态是运行期间根据具体对象的类型决定调用函数。 */
函数重载:必须在同一个类中进行;子类无法重载父类的函数,父类同名函数将被覆盖重载是在编译期间感觉参数类型和个数决定调用函数
函数重写:必须发生于父类与子类之间并且父类与子类中的函数必须由完全相同的原型
使用virtual声明之后能够产生多态
多态是运行期间根据具体对象的类型决定调用函数。
2:虚函数深入理解
当类中声明虚函数时,编译器会在类中生产一个虚函数表
虚函数表示一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成和维护的
virtual成员函数会被编译器放入虚函数表中
存在虚函数时,每个对象中都有一个指向虚函数表的指针;
class Parent { public: virtual void fun() { cout<<"Parent::func()"<<endl; } virtual void func(int i) { cout<<"Parent::func(int i)"<<endl; } }; class Child: public Parent { public: virtual void fun() { cout<<"Child::func()"<<endl; } virtual void func(int i) { cout<<"Child::func(int i)"<<endl; } } void run(Parent * p) { p->func(); } Parent对象 VPTR指针-------->VTABLE表{ void Parent::func() void Parent::func(int i) } Child 对象 VPTR指针--------VTABLE表{ void Child::func() void Child::func(int i) } void run(Parent * p) { p->func(); } 首先编译器确定func()是否为虚函数? 如果是:编译器在对象VPTR所指向的虚函数表中查找func(),并调用,查找和调用在运行时完成; 如果不是:编译器直接可以确定并找到被调用的成员函数
为什么不将所有的函数定义为虚函数?
通过虚函数表指针VPTR调用重写函数时在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。
而普通成员函数是在编译的时候就确定了调用的函数。在效率上,虚函数的效率要低很多。
出于效率考虑,没必要将所有的函数定义为虚函数。
对象中的VPTR指针什么时候被初始化?
对象在创建的时候由编译器对VPTR指针进行初始化
只有当对象的构造完全结束的时候VPTR的指向才最终确定
父类对象的VPTR指向父类的虚函数表
子类对象的VPTR指向子类虚函数表
结论:构造函数中调用虚函数无法实现多态,因为此时VPTR的指向还没有最终确定
构造函数为什么不能使虚函数?
<1>从存储空间角度
虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
<2>从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
<3>构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
<4>从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数
<5>从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的V P T R必须是对于这个类的V TA B L E。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的 V TA B L E,等.直到最后的构造函数结束。V P T R的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。
但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R指向它自己的 V TA B L E。如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被调用后才会有最后的V TA B L E)。
3:纯虚函数与抽象类:
#include <iostream> /* run this program using the console pauser or add your own getch, system("pause") or input loop */ using namespace std; /* 抽象类可用于表示现实世界中的抽象概念 抽象类是一种只能定义类型,而不能产生对象的类 抽象类只能被继承并重写相关函数 抽象类的直接特征是纯虚函数 */ class Shape { public: virtual double area() = 0;//纯虚函数:纯虚函数是只声明函数原型,而故意不定义函数体的虚函数。 } ; class Circle:public Shape { private: double m_a; double m_b; public: Circle(double a, double b) { m_a = a; m_b = b; } virtual double area() { cout<<m_a*m_b<<endl; return m_a * m_b; } }; void area(Shape* s)//抽象类只能用于定义指针和引用 { cout<<s->area()<<endl; } int main(int argc, char** argv) { Circle c1(2,3); area(&c1); Circle c2(3,4); Shape * p = &c2; p->area(); // Shape s1;//ERROR /* 抽象类与纯虚函数: 抽象类不能用于定义对象 抽象类只能用于定义指针和引用 抽象中的虚函数必须被子类重写 */ return 0; }