C++四大特性——多态 的总结

我们都知道,C语言和C++的区别就是,C语言是面对过程的程序设计,而C++是面对对象的程序设计。

面对对象的程序设计有4大特性:分别是,抽象、封装、继承、多态。

今天我们就来总结一下多态的内容。

多态:分为静态多态和动态多态:

静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。

比如一个简单的加法函数:

include<iostream>
using namespace std;

int Add(int a,int b)//1
{
    return a+b;
}

char Add(char a,char b)//2
{
    return a+b;
}

int main()
{
    cout<<Add(666,888)<<endl;//1
    cout<<Add('1','2');//2
    return 0;
}

显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。

动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
1、虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
2、通过基类类型的指针或引用来调用虚函数。

说到这,得插播一条概念:重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。

协变:是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。

//协变测试函数
#include<iostream>
using namespace std;

class Base
{
public:
    virtual Base* FunTest()
    {
        cout << "victory" << endl;
        return this;
    }
};

class Derived :public Base
{
public:
    virtual Derived* FunTest()
    {
        cout << "yeah" << endl;
        return this;
    }
};

int main()
{
    Base b;
    Derived d;

    b.FunTest();
    d.FunTest();

    return 0;
}

结果就是:
victory
yeah

看到重写的概念有没有点眼熟,没错,又是函数名称相同,这在前面已经遇到过不止一个了吧,现在来回想一下,刚刚总结过的同名隐藏,函数重载,都是这样子的吧!还是总结一下比较好吧!

同名成员函数的关系表

看完这些我们再来总结一下,什么是多态呢?
这种由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。

在前面为了解决菱形继承的二义性问题,我们引进了虚继承的概念,也就是在菱形的前两条边上加上关键字:virtual,相同的道理,我们把这个关键字加在成员函数的前面,那么这个成员函数就成了虚函数。

那么写虚函数要注意什么呢?

1、构造函数能不能作为虚函数呢?

class Base
{
public:
    Base()
        :_b(666) //初始化列表
    {
        cout << "Base()" << endl;
    }

    int _b;
};

int main()
{
    Base b;
    b._b = 8;
    return 0;
}

当我们把Base类中的构造函数前面加上 virtual 的时候,会发现编译不过去,报的错误是“inline是构造函数的唯一合法存储类”,这又是为什么呢?

构造函数的作用我们都知道,是创建对象的,而虚函数的调用是通过对象来进行的,这是矛盾的,所以说构造函数不能声明成虚函数。

2、静态成员函数可以声明成虚函数吗?

class Base
{
public:
    static void FunTest()
    {
        cout << "victory" << endl;
    }
};

int main()
{
    Base b;
    b.FunTest();
    Base::FunTest();
    return 0;
}

当我把static前面加上virtual的时候,立马就有一道红线出现在virtual下面,编译报错是“virtual不能和static一起使用”,我们来分析一下:

如果定义成静态成员函数,那么在这个函数可以通过类名和域作用符来调用,也就是说,不用创建对象就可以调用。

虚表地址的使用必须通过对象的地址才能获取。

3、赋值运算符使用虚函数还是非虚函数呢?

class Base
{
public:
    virtual Base& operator=(const Base& b)
    {
        return *this;
    }

    int _b;
};
class Derived :public Base
{
public:
    virtual Derived& operator=(const Derived& b)
    {
        return *this;
    }

    int _d;
};

int main()
{
    Base b1;
    Derived d1;
    b1._b = 1;
    d1._d = 2;

    Base& b2 = b1;
    b2 = d1;
    Base& b3 = d1;
    d1 = b2;//编译会报错
    return 0;
}

所以说,最好不要将赋值运算符重载函数声明成虚函数。

4、析构函数最好声明成虚函数,为什么?

那么我们分别看一下把析构函数声明成虚函数和非虚函数有什么区别吧!

class Base
{
public:
    Base()
        :_b(666)
    {}

    ~Base()
    {}
private:
    int _b;
};

class Derived:public Base
{
public:
    Derived()
        :_d(888)
    {}

    ~Derived()
    {}

private:
    int _d;
};

int main()
{
    Base* pb = new Derived;
    delete pb;

    return 0;
}

这里写图片描述

class Base
{
public:
    Base()
        :_b(666)
    {}

    virtual~Base()
    {}
private:
    int _b;
};

class Derived:public Base
{
public:
    Derived()
        :_d(888)
    {}

    virtual~Derived()
    {}

private:
    int _d;
};

int main()
{
    Base* pb = new Derived;
    delete pb;

    return 0;
}

这里写图片描述

从上面的两种不同的结果可以看出。析构函数没有声明成虚函数的时候,在调用过程中会忘掉派生类的析构函数,这样就会造成内存泄漏的问题。

主要原因是我们在定义pb的时候将它定义成了Base*的类型,所以最后delete的时候就会直接调用Base类的析构函数,而我们new的时候其实new的Derived类型的,所以还会调用Derived类的构造函数,这样就会造成内存泄露。

而把析构函数声明成虚函数的时候,会有效的避免这种情况。

还有一个重点注意的地方:

不要在构造函数和虚构函数中调用虚函数!因为在构造函数和虚构函数中对象是不完整的,调用虚函数可能会出现未定义的问题!

虚表指针——>虚表

当我们在测试函数的时候会发现,一个对象好像比以前大了点:

class Base
{
public:
    Base()
        :_b(6)
    {
        cout << "Base()" << endl;
    }

    virtual~Base()
    {
        cout << "~Base()" << endl;
    }
private:
    int _b;
};

class Derived :public Base
{
public:
    Derived()
        :_d(8)
    {
        cout << "Derived()" << endl;
    }

    virtual~Derived()
    {
        cout << "~Derived()" << endl;
    }

private:
    int _d;
};

int main()
{
    Derived d;
    cout << sizeof(d) << endl;
    return 0;
}

按照我们以往的经验,Derived继承了Base的成员,因此,结果应该是8,然而,答案是12,不必怀疑,不信自己验证。那么为什么会多出四个字节呢?

那么我们来调一下内存看一下多出来的四个字节是什么?

这里写图片描述

我们可以看到,下面的两个分别是_b和_d,而上面多了一行貌似是地址的东西,那么我们就来看一下这个地址到底是啥?

class A
{
public:
    A()
        :_a(1)
        , _b(2)
    {}

    virtual void Show()
    {
        cout << "A::Show()" << endl;
    }

    ~A()
    {}

    virtual void FunTest1()
    {
        cout << "A::FunTest1()" << endl;
    } 

private:
    int _a;
    int _b;
};

typedef void(*_pFun_t)();//_pFun为一个函数指针

void PrintVfptr(_pFun_t* _pPfun)
{
    while (*_pPfun)
    {
        (*_pPfun)();//调用虚函数
        _pPfun = (_pFun_t*)((int*)_pPfun + 1);
    }
}

void Test()
{
    A a;
    cout << sizeof(A) << endl;
    _pFun_t* pPFun = (_pFun_t *)(*(int*)&a); //定义一个虚表指针
    PrintVfptr(pPFun);
}

int main()
{
    Test();
    return 0;
}

这里写图片描述

从监视窗口可以看出,——vfptr即为虚表指针,这个指针下面凡是虚函数都包含在内,然后下面是两个成员变量。

这里写图片描述

  • 22
    点赞
  • 105
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值