c++入门:多态

目录

一.多态

二.多态两要素

 虚函数重写的特例:

1.返回值协变。

2.析构函数

3.派生类可以不写virtual

三.多态实现原理

虚函数表

多态

四.虚表详解

打印虚表

五.抽象类

六.final和override关键字 

final

override


一.多态

 什么是多态?先来直观感受一下

struct person {//普通类
    virtual void ticket() { cout << "全票" << endl; }
    int _id;
};
struct stutent:public person {//学生类,继承自person类
    virtual void ticket() { cout << "半票" << endl; }
    int _num;
};
void display(person* p) {//调用票价函数
    p->ticket();
}
int main() {
    person* Jige = new person;//分别定义一个person和stutent对象
    stutent* Caixukun = new stutent;
    
    display(Jige);
    display(Caixukun);
}

运行结果如下: 

 注意:display函数传入的参数是一个父类对象的指针,同样是去调用一个父类对象的成员函数,为什么会有不同的展示结果?

这里解释一下对于传入Caixukun对象:Caixukun对象是一个stutent类,根据继承的赋值兼容特性,display函数是把Caixukun切片作为变量,切片是一个父类对象。

这就是多态的特点:指向谁调用谁,同一事件不同结果

二.多态两要素

如何实现多态呢?或者说如何去写一个多态调用呢?看下面

多态有两个要素:

  1. 构成虚函数重写(virtual关键字)
  2. 父类的指针或引用去调用虚函数

虚函数重写:

        父类和子类函数名、参数类型、返回值类型完全相同,且父类加上virtual关键字

 虚函数重写的特例:

1.返回值协变。

        派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变是编译器自动识别,也满足多态。

比如:
​​​​​​​​​​​​​​

struct person {

    virtual person* f() { return new person; }
    int _id;
};
struct stutent:public person {
    virtual stutent* f() { return new stutent; }
    int _num;
};

2.析构函数

        析构函数经过编译后函数名都统一处理为destructor。父类和子类的析构函数不用加virtual关键字也构成重写,而且即使函数名不同,也构成重写。

这是为什么呢?

如果析构函数不满足重写:那下面这种情况就极有可能发生内存泄漏

struct person {
     ~person() { cout << "~person()" << endl;; }
    int _id;
};
struct stutent:public person {
     ~stutent() { cout << "~stutent()" << endl;; }
    int _num;
};

int main() {
    person* Jige = new person;
    person* Caixukun = new stutent;//两个不同类型对象都用perosn指针来接收
    delete Jige;
    delete Caixukun;
}

       我们知道delete是封装的free()函数和对应的析构函数的,如果是指向父类对象,this->person::destructor就会去调用父类的析构,指向派生类,this->stutent::destrcutor就会去调用子类以及自动调用父类析构;如果是通过派生类切片来释放空间,因为是person类指针,就只会去调用父类的析构,而不会析构子类,造成内存泄漏

new 和 new[]是对malloc和构造函数的封装,delete和delete[]是对free和析构函数的封装。调用delete函数时会把对象指针传给this,通过this调用其析构函数。

加上virtual关键字构成重写后,就可以避免这个问题

struct person {
    virtual ~person() { cout << "~person()" << endl;; }
    int _id;
};
struct stutent:public person {
    virtual ~stutent() { cout << "~stutent()" << endl;; }
    int _num;
};

int main() {
    person* Jige = new person;
    person* Caixukun = new stutent;//两个不同类型对象都用perosn指针来接收
    delete Jige;
    delete Caixukun;
}

3.派生类可以不写virtual

在实现多态时,派生类可以不写virtual关键字,但父类必须写。

三.多态实现原理

 多态这么神奇,作为面向对象三大特性之一,是如何实现指向谁调用谁的呢?

关键在于一个结构:虚函数表(简称虚表)

虚函数表

        虚函数表是存储虚函数地址的一张表(类似于函数指针数组),如果一个类中有虚函数,就会生成虚函数表,存储虚函数地址。

我们先来观察一下虚函数表在对象中的存储结构。

一个person类占8个字节的内存,并不是只有一个int类型变量,一个stutent类占12个字节,好像都比预期值多了4个字节;通过监视窗口我们可以发现类中还有一个_vfptr数组,而这个_vfptr表就占4个字节,而且最先声明。实际上这就是虚函数表,每个元素就是虚函数地址

 

可以发现_vfptr中元素的值就是地址,且和内存窗口中看到的地址相同,这就是虚函数对应的地址。 

这是父类的内存结构,那么派生类呢?虚表也会被继承吗?

可以看到,派生类只有一张虚表,但是注意虚表中是重写后的虚函数地址,通过观察前面的域作用限定符stutent::可以发现。所以得出结论:继承后父类的虚表也会被继承,且共用同一张虚表,如果虚函数构成重写则覆盖父类的虚函数。

多态

现在我们可以知道多态是如何实现的了。

重写的虚函数被放进虚表中,通过指向不同对象的父类指针或引用调用虚函数时,会在虚表中找对应的虚函数地址,然后call虚函数,由于重写后父类和子类中虚表不同,如果是指向派生类对象就调用派生类重写的虚函数,如果是指向父类对象就调用父类虚函数从而实现多态调用,伟大的面向对象!!!

四.虚表详解

void display(person* p) {
    p->ticket();
}
struct base {
    virtual void func1() { cout << "void base::func1()" << endl; }
    virtual void func2() { cout << "void base::func2()" << endl; }
    int a;
};
struct derive :public base{
    virtual void func2() { cout << "void derive::func2()" << endl; }
    virtual void func3() { cout << "void derive::func3()" << endl; }
    virtual void func4() { cout << "void derive::func4()" << endl; }
    int b;
};
int main() {
    base a;
    derive b;
    a.a = 1;
    b.a = 1;
    b.b = 2;
}

 派生类derive有4个虚函数,但是在虚表中只有两个重写的虚函数,这并不是矛盾的,要怪就怪VS编译器的监视窗口。这里最好还是用内存窗口去观察。

在虚表中,很明显前8个字节是func1()和func2()的地址,后面8个字节也很像指针,但是又不能观察,怎么办呢?

这里提出一个方法:打印虚表

打印虚表

打印虚表需要虚函数地址,而我们一旦得到虚函数地址,不仅可以打印,还可以调用,如果不是函数地址的话调用就会崩溃。所以这样我们就可以验证后面八个字节是不是虚函数地址了。

 把循环改成4个,试试调用后面8个字节

 简直是打开了新世界,验证了后面8个字节就是派生类虚函数地址。

所以派生类中的虚表继承自基类,且共用一张虚表。

struct base {
    virtual void func1() { cout << "void base::func1()" << endl; }
    virtual void func2() { cout << "void base::func2()" << endl; }
    int a;
};
struct derive :public base{
    virtual void func2() { cout << "void derive::func2()" << endl; }
    virtual void func3() { cout << "void derive::func3()" << endl; }
    virtual void func4() { cout << "void derive::func4()" << endl; }
    int b;
};
typedef void (*vfp)();
void print_VFPTR(vfp*vf) {
    for (int i = 0; i < 4; i++) {
        printf("0x%p\n", vf[i]);
        vfp p = vf[i];
        p();
    }
}
int main() {
    base a;
    derive b;
    a.a = 1;
    b.a = 1;
    b.b = 2;
    vfp* p = (vfp*)(*(int*)(&b));
    print_VFPTR(p);
}

五.抽象类

函数头部 =0 的函数称为纯虚函数包含纯虚函数的类称为抽象类抽象类不能实例化。顾名思义,抽象就是不存在的东西,所以抽象类也不能被实例化出对象。

如果继承抽象类,派生类也是抽象类。所以抽象类的意义就在于强制重写纯虚函数。

还有个意义,就是通过指针或引用进行多态调用,说明抽象类可以定义指针和引用

六.final和override关键字 

final

在父类虚函数后加final关键字表示该虚函数不能被重写

如何使一个类无法被继承?

​​​​​​​​​​​​​​

把父类构造函数放进私有中,屌不屌!!!

 使用final关键字修饰类,表示最终类,不能被继承

override

 用override可以检查是否重写成功,不成功会报错。

封装的特性我们下次再写一篇博客具体介绍一下,对于面向对象之多态的特性就先到这里,欢迎广大读者提出建议和指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值