首页 > 代码库 > 【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.多态:同样的调用语句有多种不同的表现形态

 

 

 

 

 

 

 

 

 

 

 


p指向子类对象

 

那么,在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++第十课】---继承和多态