C++知识点随笔(二):继承、多态

一、继承

继承是类之间的,对象之间没有什么关系,比如子类继承了父类的成员属性,并且子类的对象修改了这个成员属性,可是修改之后我们再去输出父类的成员属性发现并没有发生改变,原因是这两个对象本来就是两块空间里的,没有什么联系。
继承只是继承父类的成员属性,成员函数并不会发生继承,因为本来就只有一份儿,只不过子类可以使用父类的成员函数罢了。
我们用C语言的结构体来模拟一下C++的继承,其实就相当于在子结构体中定义了一个父结构体而已:

struct AA
{
    int a;
};

struct BB
{
    AA a;
    int b;
};

如果子类拥有和父类同名的成员属性,可以通过类的作用域来区分:

cout << son.CFather::m_nMoney << endl;
cout << son.CSon::m_nMoney << endl;

1. 继承的构造、析构函数:
继承的子类属性里面默认有一个父类的构造函数,并且放在首位,所以在执行子类的构造函数的时候,先执行初始化列表,而初始化列表的执行顺序是根据定义的顺序,由于父类的构造函数在子类定义成员属性的首位,所以先执行父类的构造函数,所以就会发生:先执行父类构造函数,再执行子类构造函数。析构的时候:先执行子类析构函数,后执行父类析构函数
注意:子类的构造函数默认调用的是父类的无参的构造函数,如果父类的构造函数是有参数的,我们也可以手动在子类构造函数的初始化列表中定义(初始化列表的作用就是可以给当前类中new出的对象的构造函数传参数,上一篇中有介绍)。

2. 继承的方式:
public继承:父类的访问修饰符到子类没有变
protected继承:把public变成保护
private继承:把public、protected 变成 private
注意:
private成员继承了,但是不能直接用,想用就通过父类公共的函数;除了private成员其他的成员属性在子类中都可以用,但出了子类就要怎么继承的。只有公共的成员属性才可以在子类外使用,私有的和保护的在子类外都不能使用。私有继承的成员函数在子类外也不能使用。
总结:
无论哪种继承方式,都是改变父类的成员属性或成员函数在子类中的状态,并不会改变子类本身的成员,而我们在子类外使用子类和父类的成员的时候,只需要看他当前是什么状态,只有是public的才能在外部使用,其余的都不可以。

3. 父类指针指向子类对象:

CFather* pp = new CSon;

这样做的目的是为了:这样就可以使父类使用子类的成员了,通过多态来实现。需要注意的是,父类指针即使指向了子类的对象,可是父类指针也只是能用自己的成员而已,因为子类对象的空间里,前面的部分是父类的继承,后面的部分才是子类本身,当我们用父类的指针指向子类的对象的时候,只能使用子类前面继承父类的那部分空间,然而我们就是通过这部分空间,结合多态来实现父类使用子类成员的目的。
注意的是:构造函数调用的顺序和正常new子类对象一样,先执行父类的构造函数,再执行子类的构造函数。
我们在delelte这个指针的时候:

delete pp;   //把 pp 指向的对象全部删除,就是 new 出的部分全部删除,但是调用析构看当前这个pp的类型

由于pp无法使用子类的空间,所以只会调用父类的析构函数,但是子类的成员(除了new的成员外)也会delete掉了,因为父类的析构函数里面会调用delete,而这个delete不会只销毁父类的留下子类的,而是将他们全部销毁。可是子类中new的成员属性一定是在子类的析构函数中delete的,所以因为我们没有调用子类的析构函数,所以这部分成员属性并不会被销毁,从而造成内存泄露。
所以我们如果不想造成内存泄露,就应该:

delete (CSon*)pp;   //这样调用了子类的析构函数之后还会继续调用父类的析构,从而销毁了全部内容

注意:强转的作用是,把这个指针当做(CSon*)用一下,用过之后pp还是原来的没有变。
虽然父类指针可以指向子类的对象,但是子类的指针不能指向父类的对象。不能用大的指针去指向一块儿小的地址,编译无法通过。

4. 重写、重载、隐藏:
(1) 重写
子类重写父类的函数,函数名和参数必须相同,返回值可以不同,但是仅限于返回类型为父类或子类的指针这种类型的不同,如果返回int和char这种不同是不可以的。重写之后的函数可以通过加作用域的方式来调用。重写的函数必须是虚函数。
(2) 重载
相同的函数名,不同的参数,根据传参不同调用不同的函数。
(3) 隐藏 (或纵向重载)
把重载的两个函数分别放入父类和子类中,这样在调用的时候就不能通过参数的不同来调用了,而是通过作用域的不同来使用。

二、多态

多态的作用就是提高代码的扩展性,其实我们在C语言中也有提高代码扩展性的应用,比如:函数指针,我们只需要把函数的地址传进去就可以调用相应的功能,而不用把某个函数写死了。
多态实现代码如下:

#include <iostream>
using namespace std;

class CWater
{
public:
    virtual void Show()     //virtual虚函数是实现多态的方法
    {
        cout << "CWater::Show"<< endl;
    }
};

class CMilk : public CWater
{
public:
    void Show()
    {
        cout << "CMilk::Show"<< endl;
    }
};

class CCoffee : public CWater
{
public:
    void Show()
    {
        cout << "CCoffee::Show"<< endl;
    }
};

class CBeer : public CWater
{
public:
    void Show()
    {
        cout << "CBeer::Show"<< endl;
    }
};

void Bottle(CWater* water)  //无论传进来什么对象,都用父类的指针调用虚函数就可以实现
{
    water->Show();
}

int main()
{

    CMilk* milk = new CMilk;
    Bottle(milk);

    CCoffee* coffee = new CCoffee;
    Bottle(coffee);

    CBeer* beer = new CBeer;
    Bottle(beer);

    CWater* water = new CMilk;
    Bottle(water);

    system("pause");
    return 0;
}

输出结果:
这里写图片描述
注意:父类本身的对象在使用虚函数的时候也会正常执行。

虚函数列表
一个空类的大小是1个字节,用来占位。一个类中只包含一个虚函数,那么它的大小是4个字节。从对象的首地址开始,前4个字节装的是虚函数指针,指向虚函数列表。虚函数列表装的都是本身虚函数和重写虚函数的指针。虚函数列表是在编译的时候就创建的,所以一个类只有一个虚函数列表,而每个对象都有一个指向虚函数列表的指针。而指向虚函数列表的指针是创建对象的时候才有的。
下面我们根据代码详细的说一下虚函数列表是如何工作的:

class CFather
{
public:
    ~CFather()
    {
        cout << "~CFather" << endl;
    }
    virtual void Show()
    {
        cout << "CFather::sShow" << endl;
    }
    virtual void BB()
    {
        cout << "CFather::BB" << endl;
    }
};

class CSon : public CFather
{
public:
    ~CSon()
    {
        cout << "~CSon" << endl;
    }
    virtual void Show()   //这里需要注意:重写虚函数的时候如果函数名、参数、返回值都相同的话,不写 virtual 也是默认为虚函数的。
    {
        cout << "CSon::sShow" << endl;
    }
};

//  虚函数列表是在 编译的时候创建的 
//  指向虚函数列表的指针是在定义对象的的时候才有的

int main()
{
    CFather* father = new CSon;
    father->Show();
    delete father;
    father = NULL;

    return 0;
}

在上述的代码中,我们用父类的指针指向了子类的对象。
(1) 虚函数列表创建过程:
首先在子类的初始化列表中调用了父类的构造函数,父类在编译期间就创建了虚函数列表,这个表中第一项为 virtual void Show() 的地址,第二项为 virtual void BB() 的地址。等到父类构造完毕之后,子类继承了父类的虚函数列表。如果子类中有重写父类的虚函数,如上面代码中,重写了 void Show() 的虚函数,那么就会在原来的虚函数列表中将第一项的地址替换为当前重写的 void Show() 的地址。如果没有重写父类的虚函数,那么虚函数列表中原来父类所添加的虚函数指针就不变。如果子类有自己的虚函数,那么这个虚函数的指针也将加在父类的虚函数指针之后,即现在这个表中的第三项。
注意:如果父类有虚函数,那么继承他的所以子类也都有各自的一份儿虚函数列表,并且都有使用父类继承来的指针来指向自己类的虚函数列表,实例如下:

#include <iostream>
using namespace std;

class CFather
{
public:
    virtual void AA(){}
    virtual void BB(){}
};

class CSon : public CFather
{
public:
    virtual void AA(){}
};

int main()
{
    typedef void (*PFUN)();

    CFather* pFatherFather = new CFather;
    int* addrFatherVftale = (int*)*(int*)pFatherFather;   //父类虚表地址

    CFather* pFatherSon = new CSon;
    int* addrSonVftale = (int*)*(int*)pFatherSon;   //子类类虚表地址

    system("pause");
    return 0;
}

监视结果:
这里写图片描述
我们看到他们的地址是不同的,所以是有着各自虚表。

(2) 虚函数列表的访问过程:
由于我们new了一个子类的对象,那么我们在创建这个对象的时候,开始的前4个字节就是虚函数指针,这个指针指向的是虚函数列表。我们在使用父类的指针 father->show() 的时候,调用的是父类的函数(因为之前讲过父类指针是无法使用子类成员的),但是在调用时发现这个show()函数是一个虚函数,那么就先去这个类中的虚函数列表里访问,从而查询到,当前虚函数列表中第一项是指向子类重写虚函数的指针,所以就根据这个指针调用了子类的虚函数(如果子类中没有重写这个虚函数,那么虚函数列表里面调用的还是原来的父类的虚函数)。所以我们就可以通过new不同的对象,通过查找虚函数列表,然后调用不同类中重写的虚函数(如果重写了的话),从而提高了代码的扩展性。

为什么虚函数列表要在编译的时候创建?
虚函数列表在编译的时候就把所有的虚函数都加在了表中,这样以后我们在创建对象的时候只需要创建一个指向虚函数列表的指针即可,不需要再重复创建虚函数列表,从而提高了性能。类的成员函数的创建也是一样的,一个类只需要有一份儿成员函数,这些普通的成员函数默认都会有一个this指针,然后在之后创建不同的对象的时候,我们就可以通过this指针指向当前对象的成员属性了,同样也是减少了反复创建成员函数的开销。另外要强调的是:成员函数在编译的时候就被创建在了代码区,所有对象共享这份代码,所以成员函数并不占用对象的内存空间

跟踪查看虚函数列表内容:

CFather* father = new CSon;

typedef void (*PFUN)();

PFUN a = (PFUN)*((int*)*(int*)father+0);    //第一项
PFUN b = (PFUN)*((int*)*(int*)father+1);    //第二项
PFUN c = (PFUN)*((int*)*(int*)father+2);    //第三项

通过下断点,再将a、b、c分别放入监视中即可查看值。

多态也有一些缺点需要注意:
1. 安全性问题:
(1)正常情况下父类调用子类中未覆盖父类的成员函数(通过虚函数列表覆盖)一定会发生编译错误。因为父类指针无法使用子类除了继承父类之外的自己的空间。可是因为有了多态,在子类自己的虚函数指针被放入虚函数列表的情况下,父类的指针就可以通过对象首地址找到虚函数列表,从而轻松的访问这个子类的虚函数了。
(2)如果父类的虚函数是 private 或者 protected 的,他们也同样会被放入虚函数列表中,这样我们就可以通过虚函数列表轻松的访问这些 non-public 的函数了。
2. 占用内存:
虚函数列表是一个指针数组,那么我们将虚函数指针放进去的时候自然就会增大系统内存的开销。
3. 查找效率:
正常成员函数都是编译的时候放在代码区的,我们在通过函数地址调用他们的时候效率都是O(1),而虚函数列表是一个指针数组,所以我们在查找的时候效率是O(n)。(数组通过下标遍历的时候时间复杂度才是O(1))。

如何解决多态的安全性问题?
我们知道虚函数列表就是一个指针数组,那么其实我们可以自己写一个指针数组,将虚函数的地址放进去就可以了,然后我们将这个指针数组设为private的,就不会存在安全性的问题了。后面在重写虚函数的时候我们也可以添加一些判断从而修改这个指针数组的值。值得注意的是,由于虚函数列表要在编译的时候就创建完成,所以这个指针数组我们要定义为 static 的。

什么样的函数应该写为虚函数?
每一个子类都要实现父类的这个功能,但是实现的具体方式都各有不同,就应该将这个功能写成虚函数。

空指针调用成员函数问题
之前我们了解到,一个空指针是可以调用成员函数的,因为成员函数在编译的时候就创建了。可是如果这个成员函数是虚函数呢?我们在调用他们的时候一定会崩掉。因为虚函数的调用要是有虚函数列表,而虚函数列表的查找需要虚指针,虚指针是在创建对象的时候才创建的,因为我们是空指针,还没有对象呢,所以会崩。

抽象类与接口类:
1. 抽象类:包含纯虚函数的父类。

virtual void Show()=0;

纯虚函数一定要子类来实现(重写),并且抽象类不能实例化对象。如果子类没有重写纯虚函数,那么子类也包含一个纯虚函数,也变为抽象类,同样不能实例化对象。
我们之所以使用纯虚函数是因为:父类不知道该如何实现,但是还需要给子类提供多态的功能。
注意:子类实例化对象的时候如果没有重写父类的纯虚函数,那么编译器就会报错:不能实例化抽象类!
2. 接口类:这个类中所有的成员函数都是纯虚函数。
(没有java好,java中可以使用关键字来定义接口类)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值