c++继承与多态

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类(子类),被继承的类被称为基类(父类)。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。
C++的继承是相比于C语言很大的改进,大大提高了代码的可复用性,而且是C++实现多态的基础。
我们先来通过代码实现继承:

#include <iostream> 
using namespace std;
class A
{
public:
    void fun()
    { cout << "A : fun()" << endl;}
private: int m;
};
class B: public A
{
public:
    void fun()
    { cout << "B : fun()" << endl;}
private: 
int n;
};
class C: public B
{
public:
    void fun()
    { cout << "C : fun()" << endl;}
};
int main()
{
  A a;
  B b;
  C c;
  A *pb = &c; 
  pb->fun();
  return 0;
}

代码输出的是什么? 如果你已经了解了继承和多态,就应该轻易的得出 : C:fun() .
先来看我们定义了一个 类A,而类B定义时 采用了 class:B public A 这意思是,声明的B继承了类A,也就是说类A中的成员统统被类B继承,所以类B包含类A,同理定义的类C继承类B。
那么public是什么意思呢? 这是类的继承方式,分别有:
1public,共有继承,可以理解为类A被类B包含于类B的public中,类A的公有成员还是公有,私有成员m任然保持私有性质。
2private 私有继承, 可以理解为被继承于B的私有类中,若B为private继承,在B之后的所有类不能再继承B(比如代码中的C类)。
3protected 保护继承, 保护继承是指在B之后的类可以继承引用,但不可以用类的对象调用B。 有没有搞晕?我们用代码一一实现吧。

 #include <iostream> 
using namespace std;
class A
{
public:
     void fun()
    { cout << "A : fun()" << endl;}
    void Afun()
    {cout << "Afun()" << endl;}
private: int m;
};
class B: protected A
{
public:
    void fun()
    { cout << "B : fun()" << endl;}
private: 
int n;
};
class C: private B
{
public:
    void fun()
    { cout << "C : fun()" << endl;}
};
int main()
{
  A a;
  B b;
  C c;
  A *pb = &b; 
  pb->fun();
  return 0;

当将B继承类型改为保护型时,这样的代码就会报错了,因为保护继承不允许对象进行访问,但是我们如果 这样调用 b.Afun(); 代码就会输出: Afun(); 保护继承在子类内部使用父类的公有函数,是没有一点问题的。
再来看C类,他私有继承了B,这代表既不能被对象调用,也不可以再被其他类继承。A->B->C !->D. C不能再被D继承。

我们再来了解下 “重载”, 当一个类中两个函数 函数名相同,函数参数不同时,两个函数可以同时存在,这就叫做函数的重载,实质原因是 在编译过程中函数被命名为不同名
如: fun(); -> _3Zfun
fun(int ); -> _3Zfuni
fun(int, int); -> _3Zfunii
_3Z是不同编译器的不同标志。从函数名可以看出相同函数名不同参数是可以同时存在的,函数重载后的调用会根据客户提供的参数列表做出 最优匹配

       这里有一个面试题 : 当同函数名,同参数列表,不同返回值的函数可以重载吗? 

答案是 : 不可以! 因为编译器不依据返回值改写函数名,所以编译时会报多重定义的错误。 更深层的原因是,当两个函数只有返回值不同时,程序会当成相同函数,那么函数到底返回那种类型的返回值?所以编译器避免了这种Bug,你不被允许这样重载函数!

另一个相似的概念是 “隐藏”,当相同函数名的函数被定义在不同的有继承关系的类中,会出现什么结果?不管两函数的参数列表是否相同,只要函数名相同,派生类都会将基类的同名函数隐藏(注意 隐藏 二字与 覆盖 的不同)。什么意思呢? 上代码!

class A
{
public:
     void fun(double)
    { cout << "A : fun(double)" <<endl;
};
class B: public A
{
public:
    void fun(int B)
    { cout << "B : fun(int B)" << endl;}
};
int main()
{
   B b;
   b.fun(1);
   return 0;
}

输出结果是 : B : fun() 那为什么不输出 A:fun()呢? (要是你说是因为1是整形的,最优匹配,那你就可以回家等通知了~) 其实是因为同名的fun()函数将基类的fun()函数隐藏了。
我们这样调用 b.::Afun(1) 加上A类的作用域进行限制,那么就会输出 A(fun). 从这里就可以看出不是覆盖了,而是隐藏了(不厌其烦的说是因为为了和一会讲的多态区分开)。 编译器是这样认为的,如果派生类提供的fun()函数,那么被继承的基类就不必提供fun(),若你需要(加作用域),那么就会给你提供。

继承问题中还有一点要提一下,那就是继承过程中的父类、对象成员、子类的构造以及析构顺序。

class A
{
public:
    A()
    {cout << "构造A"<< endl;}
    ~A()
    {cout << "析构A" <<endl;}
   void fun(double)
    { cout << "A : fun(double)" <<endl;    
};
class B: public A
{
public:
    void fun(int B)
    { cout << "B : fun(int B)" << endl;}
};
class C: public B
{
public:
    void fun()
    { cout << "C : fun()" << endl;}
};
int main()
{
   C c;
   c.fun();
   return 0;
}

比如上面代码。要先构造C类的c,就先要构造父类的B,要构造B就得先构造B类的父类A。 所以构造顺序为 A->B->C **析构顺序与构造顺序完全相反。**C->B->A
派生类构造函数各部分的执行次序为:
1.调用基类构造函数,按它们在派生类定义的先后顺序,顺序调用。
2.调用成员对象的构造函数,按它们在类定义中声明的先后顺序,顺序调用。
3.派生类的构造函数体中的操作。
析构函数的功能是作善后工作。
只要在函数体内把派生类新增一般成员处理好就可以了,而对新增的成员对象和基类的善后工作,系统会自己调用成员对象和基类的析构函数来完成。
析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。


多态性(polymorphism)多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性。以虚基类为基础的运行时的多态性是面向对象程序设计的标志性特征。 体现了类推和比喻的思想方法。
多态最常见的实现方法就是声明基类指针,利用指针指向任意一个子类对象,调用相应的虚函数,可以根据指向子类的不同而实现不同的方法。
我们来看一下实现多态的代码:

 #include <iostream>
using namespace std;

class A
{
publicvirtual void fun()
    {cout << "A:fun()" << endl;}
    void show()
    {cout <<  "A:show()"  << endl;} 
};
class B : public A
{
public:
    void fun()
    {cout << "B:fun()" << endl;}
    void show()
    {cout << "B:show()" << endl;}
}
int main()
{
    A a;
    B b;
    A *pb = &a;
    pb->fun();
    pb->show();
    pb = &b;
    pb->fun();
    pb->show();
   return 0;
}

第一个pb->fun(),pb->show().很好理解,用基类的指针指向了基类的对象,调用基类的函数,因此输出的是: A:fun() A:show().
第二个就有点意思了,用的是基类的指针指向的子类对象。输出的是:
B:fun() A:show(). 这里B:fun()就体现了多态的用法,可以想,如果在定义一个C类继承于A类,fun(),show(),函数同名,那么 A* pb= &c, pb->fun()的结果就是输出 C:fun()。只需要一个接口(基类指针)就可以调用多个派生类的成员函数。
面试题:你如何用派生类的指针调用基类的成员函数?
利用强转 C pd = (C )&a; pb->fun(); 输出 : A:fun();

实现多态的关键字 virtual,virtual所修饰的函数为虚函数,在基类定义了虚函数,派生类的成员函数与虚函数名要求相同,派生类的成员函数可以不加virtual修饰,这样通过基类指针就可以访问派生类的成员函数了,这就是多态的实现。

class A
{
public:
    virtual void fun()
    {cout << "A:fun()" << endl;}
private:
  int m;
};

上面这个类A大小是多大? 当不加virtual时,我们知道大小应该是private 中m的大小,应该是4个字节。当声明了虚函数后呢?大小应该是16个字节,由于多了虚函数的指针(64位系统)加了8个字节,因为内存
对齐所以是8+4+4=16个字节。

在我们运用继承多态时,有一点特别需要注意!当子类继承父类时,父类指针并不能调用子类的析构函数,所以会导致,子类的函数不能被析构。解决方法是将父类析构函数定义为虚函数,子类的析构函数相当于对父类的重载。
用虚函数修饰析构函数,是C++中唯一不重名也能重载的函数!
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里写图片描述
当派生类被构造时,虚函数表先生成的是基类的虚函数然后是自己的函数,当基类用指针调用fun()时,虚函数表基类的虚函数地址A:fun()将被改写为派生类的B:fun()同名函数的地址,未被调用或者不是虚函数的不改写地址(注意虚函数表最后有一个“.”这是虚函数的结束节点,等同于’\0’).
多重继承时,一个派生类继承了多个基类,那么会对应每个基类生成相应的虚表。
一个基类被多个派生类继承时,各个派生类有自己对应基类的虚表。

纯虚函数
定义:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但是要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型体后加”=0” virtual void fun() = 0;
引入原因:为了方便使用多态特性,我们常常要在基态中定义虚拟函数。在很多情况下基类本身生成对象是不合理的。例如,动物作为一个基类可以派生出老虎,狮子,但动物本身并不是什么物种。如定义为纯虚函数,则编译器要求派生类中必须重写以实现多态,同时纯虚函数的类称为抽象类,它不能生成对象。

继承和多态需要掌握的知识点
1、继承过程中的访问属性
2、继承过程中的父类、对象成员、子类的构造以及析构顺序
3、继承过程中的函数同名隐藏
4、多继承中容易产生二义性,可以用::进行解决,如果是从同一个基类进行继承,那么要考虑是否 是有虚拟继承,即虚基类
5、类型兼容原则的类容
6、多态的实现必然以父类的指针或引用作为基础,如果以父类的对象进行调动,会出现子对象的切片现象
7、掌握多态的实现原理(虚指针、虚表),以及各种继承情况下的虚表图
8、纯虚函数以及抽象基类

                                                            染尘 16.4.12
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值