首页 > 代码库 > 【C++第十课】---继承和多态
【C++第十课】---继承和多态
一、函数的重写
什么叫函数的重写呢?在子类中和在父类中定义的函数类型是一样的就叫做函数的重写,注意这里的函数重写和函数重载的区别。
问题的引入:那么如果发生了函数的重写那该怎么办,编译器是如何解析的呢?
要想解决这个问题,那么我们首先得搞清楚到底什么是函数重写,下面举例说明:
1.在子类中定义与父类中原型相同的函数
2.函数重写只发生在父类与子类之间
#include <iostream> using namespace std; class Parent { public: void print() { cout<<"voidParent::print() ... "<<endl; } }; class Child: public Parent { public: void print() { cout<<"voidChild::print() ... "<<endl; } }; int main(int argc, char **argv) { Childchild; child.print(); child.Parent::print(); cin.get(); return0; } 上述实例打印的内容: void Child::print() ... void Parent::print() ...
由此可得出结论:
1.父类中被重写的函数依然会继承给子类
2.默认情况下子类中重写的函数将隐藏父类中的函数
3.通过作用域分辨符::可以访问到父类中被隐藏的函数
二、现在已经明白了函数重写是怎么回事,那么现在开始第二个问题,当我们的函数重写遇到赋值兼容性原则又该怎么办呢?
比如下面的程序将输出什么内容呢
#include <iostream> using namespace std; class Parent { public: void print() { cout<<"voidParent::print() ... "<<endl; } }; class Child:public Parent { public: void print() { cout<<"voidChild::print() ... "<<endl; } }; int main(int argc, char **argv) { Childchild; Parent parent; Parent *P1 = &child; Parent *P2 = &parent; Parent& P3 = child; Parent& P4 = parent; P1->print(); P2->print(); P3.print(); P4.print(); cin.get(); return0; }
要想知道答案,我们还得了解编译器的行为:
1.C++与C相同,是静态编译型语言
2.在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象
3.所以编译器认为父类指针指向的是父类对象(根据赋值兼容性原则,这个假设合理)
4.由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
5.从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数
看了上面的描述,我想答案一目明了,出于安全性的考虑,那么输出结果就是:
void Parent::print() ... void Parent::print() ... void Parent::print() ... void Parent::print() ...
三、那么上面的结果使我们想要的么,我们明明赋值的是一个子类对象,但是指向的还是父类对象,那么解决方案又是什么呢?
我们需要的结果:
1.根据实际的对象类型来判断重写函数的调用
2.如果父类指针指向的是父类对象则调用父类中定义的函数
3.如果父类指针指向的是子类对象则调用子类中定义的重写函数
面向对象中的多态
1.根据实际的对象类型决定函数调用语句的具体调用目标
2.多态:同样的调用语句有多种不同的表现形态
|
那么,在C++中多态是如何实现的呢?下面来看多态的本质
1.C++中通过virtual关键字对多态进行支持
2.使用virtual声明的函数被重写后即可展现多态特性
没错,这就是传说中的虚函数
那么下面我们就对上述函数进行改进,如下:
#include <iostream> using namespace std; class Parent { public: virtual void print() { cout<<"voidParent::print() ... "<<endl; } }; class Child:public Parent { public: virtual void print() { cout<<"voidChild::print() ... "<<endl; } }; int main(int argc, char **argv) { Childchild; Parent parent; Parent *P1 = &child; Parent *P2 = &parent; Parent& P3 = child; Parent& P4 = parent; P1->print(); P2->print(); P3.print(); P4.print(); cin.get(); return0; } 看看打印的内容: void Child::print() ... void Parent::print() ... void Child::print() ... void Parent::print() ... 这样就符合我们预期的要求了。
四、前面我们一直提到要区别开重载和重写的概念,那么重载和重写到底有什么区别呢?
下面先罗列出两者的区别:
函数重载
1.必须在同一个类中进行
2.子类无法重载父类的函数,父类同名函数将被覆盖
3.重载是在编译期间根据参数类型和个数决定调用函数
函数重写
1.必须发生于父类与子类之间
2.并且父类与子类中的函数必须有完全相同的原型
3.使用virtual声明之后能够产生多态
4.多态是在运行期间根据具体对象的类型决定调用函数
概念上陈述清楚了,那么看一个实例
#include <iostream> using namespace std; class Parent { public: virtual void print() { cout<<"Parent::print() ... "<<endl; } virtual void print(int i) { cout<<"Parent::print(inti) ... "<<endl; } virtual void print(int i,int j) { cout<<"Parent::print(inti,int j) ... "<<endl; } }; class Child:public Parent { public: virtual void print(int i,int j) { cout<<"Child::print(inti,int j) ... "<<endl; } virtual void print(int i,int j,int k) { cout<<"Child::print(inti,int j,int k) ... "<<endl; } }; int main(int argc, char **argv) { Child child; child.print(1,2); //实现多态 child.print(1,2,3);//调用子类中的成员函数 child.print(1); //报错,因为父类成员函数被覆盖 cin.get(); return0; }
主函数里面解释的很清楚了,正是因为父类会被子类覆盖,而不会发生重载,所以报错。
五、关于多态的一些概念性的东西已经解释清楚了,下面就来深入了解虚函数,在C++中多态的实现原理
1.当类中声明虚函数时,编译器会在类中生成一个虚函数表
2.虚函数表是一个存储类虚成员函数指针的数据结构
3.虚函数表是由编译器自动生成与维护的
4.virtual成员函数会被编译器放入虚函数表中
5.存在虚函数时,每个对象中都有一个指向虚函数表的指针
这是一张虚函数表建立的模型图
毫无疑问:通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。出于效率考虑,没有必要将所有成员函数都声明为虚函数。
现在又提出一个问题:对象中的VPTR指针什么时候被初始化?
1.对象在创建的时候由编译器对VPTR指针进行初始化
2.只有当对象的构造完全结束后VPTR的指向才最终确定
3.父类对象的VPTR指向父类虚函数表
4.子类对象的VPTR指向子类虚函数表
所以说:构造函数中调用虚函数无法实现多态。
这里自己一试便知,不再举例:
六、最后来看个纯虚函数
在项目经常用到虚函数和纯虚函数,那么到底是怎么回事呢?
面向对象中的抽象概念
在进行面向对象分析时,会有一些抽象的概念!也就是其实是不存在的概念,比如让你求一个图形的面积,但是却不告诉你什么图形,那该怎么求呢?这个对象的设计完全脱离实际,没有任何的意义!如下:
class Shape { public: virtual double area() = 0; };
这就是纯虚函数的格式
那如何让他变得有意义呢?那么这就要在我们的继承类中对他进行实现,比如:
class Rectangle : public Shape { private: double R_a; double R_b; public: Rectangle(int a, int b) { R_a= a; R_b= b; } virtual double area() { return( (R_a) * (R_b) ); } };
初步尝试纯虚函数的使用方法后,进行深一步了解纯虚函数
面向对象中的抽象类
1.抽象类可用于表示现实世界中的抽象概念
2.抽象类是一种只能定义类型,而不能产生对象的类
3.抽象类只能被继承并重写相关函数
4.抽象类的直接特征是纯虚函数
纯虚函数是只声明函数原型,而故意不定义函数体的虚函数。
抽象类与纯虚函数
1.抽象类不能用于定义对象
2.抽象类只能用于定义指针和引用
3.抽象中的纯虚函数必须被子类重写
上面的area是纯虚函数,= 0 告诉编译器,这个函数故意只声明不定义。
最后,我们看一个纯虚函数实现的例子:
#include <iostream> using namespace std; class Shape { public: virtual double area() = 0; }; class Rectangle : public Shape { private: double R_a; double R_b; public: Rectangle(int a, int b) { R_a= a; R_b= b; } virtual double area() { return( (R_a) * (R_b) ); } }; class Circle : public Shape { private: double C_r; public: Rectangle(int r) { C_r= r; } virtual double area() { return( 3.14 *(C_r) * (C_r) ); } }; void area(Shape* s) { cout<<s->area()<<endl; } int main(int argc, char **argv) { Rectanglerect(10, 5); Circle circle(5); cin.get(); return0; }
这也和我们上面所说的一定要咋子类中实现是一致的。
七、总结一下:
1.函数重写是面向对象中很可能发生的情形
2.函数重写只可能发生在父类与子类之间
3.需要根据实际对象的类型确定调用的具体函数
4.virtual关键字是C++中支持多态的唯一方式
5.被重写的虚函数即可表现出多态的特性
6.函数重载与函数重写不同
7.多态是通过虚函数表实现的
8.虚函数在效率上会受到影响
9.抽象类可用于表示现实世界中的抽象概念
10.抽象类是通过纯虚函数实现的
【C++第十课】---继承和多态