C++——多态

C++知识总结目录索引

1. 何为多态

  多态一词最初来源于希腊语,意思是具有多种形式或形态的情形,在C++中是指同样的消息被不同类型的对象接收时导致不同的行为,这里讲的消息就是指对象的成员函数的调用,而不同的行为是指不同的实现,也就是调用了不同的函数。简而言之就是“一种接口,多种实现(方法)”。
  


2. 多态的分类

多态可分为静态多态和动态多态,具体的分类情况如下:
这里写图片描述

区别:

  1. 静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早早绑定的,静态多态也往往被叫做静态联编。

  2. 动态多态则是指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定,这就属于晚绑定,动态多态也往往被叫做动态联编。

  3. 动态联编必须满足以下两个条件:
    1. 必须是虚函数
    2. 通过基类类型的引用或者指针调用虚函数

关于重载这里就不多说了,主要要讲的是虚函数。


3. 虚函数

  在菱形继承中有虚继承,这里又引入了虚函数的概念,在这要说一句,虚继承和虚函数虽然都是使用的同一个关键字 virtual ,但是这两者是两个不同的概念,没有必然的联系。

虚函数:在类的成员函数前加上 virtual 关键字,这个成员函数就是虚函数。

虚函数重写:当在子类的定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。这里说的相同是指函数名、参数列表、返回值均要相同(协变例外)。

下面给一个虚函数重写的示例:

class Person{
public:
    virtual void SurfInternet()
    {
        cout << "Person:" << "上网" << endl;
    }
};


class Teenagers : public Person{
public:
    virtual void SurfInternet() 
    {   //子类中的SurfInternet()函数重写了父类的虚函数
        cout << "Teenagers:" << "未成年人禁止上网" << endl;
    }
}; 

void func(Person* p)
{
    // 究竟调用哪个虚函数,和指针的类型无关,和它指向的类型有关
    // 1. 指向父类就调用父类的虚函数
    // 2. 指向子类就调用子类的虚函数 
    p->SurfInternet();
}

int main()
{
    Person a;
    Teenagers b;
    func(&a); // 调用父类的虚函数
    func(&b); // 调用子类的虚函数

    system("pause");
    return 0;
}

这里写图片描述

1. 为什么最好把析构函数定义成虚函数?

我们先看如下一段代码:

class A{
public:
    A()
    {
        cout << "A()" << endl;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A{
public:
    B()
    {
        cout << "B()" << endl;
    }
    ~B()
    {
        cout << "~B()" << endl;
    }
};

int main()
{
    A* p = new B;
    delete p;
    return 0;
}

运行结果:
这里写图片描述
  我们看到这里析构时只调用了父类的析构函数,没有调用子类的析构函数,这在一定程度上会造成内存泄漏,就像你拆房子只拆了地基其他的不拆一样。要解决这种问题可以把析构函数定义成虚函数。虚函数的调用会根据指针指向的类型决定,此时p指向B类型,所以会调用B的析构函数,这样就不会造成内存泄漏。
  关于把析构函数,建议是最好定义成虚函数,并不是一定要这么做。因为用一个基类的指针或引用指向一个派生类的情形本就不常见,而且如果派生类中没有free、close这些操作也不会造成资源泄漏。

2. 不要在构造函数和析构函数中调用虚函数。

  在构造派生类对象时,首先调用基类构造函数初始化对象的基类部分,再调用派生类构造函数。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时的对象还不是一个派生类对象(不完整)。

  析构派生类对象时,首先调用的是派生类析构函数,一旦开始执行派生类析构函数,对象内派生类的成员变量便呈现未定义值,此时对象便不完整。
  
  为了适应这种不完整,编译器将对象的类型视为在调用构造/析构函数时发生了变换,即:视对象的类型为当前构造函数/析构函数所在的类的类型。由此造成的结果是:在基类构造函数或者析构函数中,会将派生类对象当做基类类型对象对待。而这样一个结果,会对构造函数、析构函数调用期间调用的虚函数类型的动态绑定对象产生影响,最终的结果是:如果在构造函数或者析构函数中调用虚函数,运行的都将是为构造函数或者析构函数自身类类型定义的虚函数版本。

class A
{
public:
    A()
    {
        cout << "the size is " << fun() << endl;
    }

    ~A()
    {

    }
private:
    virtual size_t fun()
    {
        return sizeof(*this);
    }
    int _b;
};

class B : public A
{
    //本意是想构造B对象时打印对象的大小。
public:
    B()
    {
        cout << "the size is " << fun() << endl;
    }

    ~B()
    {

    }

private:
    virtual size_t fun()
    {
        return sizeof(*this);
    }
    int _b;
};

int main()
{
    B b;

    return 0;
}

这里写图片描述
  本来构造B类对象只希望它调用B的虚函数,但由于先执行A的构造函数,此时对象还不完整,编译器为了适应这种不完整,会在A的构造函数内把它看做一个A类对象,从而调用A类的虚函数,析构亦是如此。

总结:

  1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)

  2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性(只要在基类的成员函数前加上virtual,派生类中该函数不加virtual也是虚函数)。

  3. 只有类的成员函数才能定义为虚函数。

  4. 静态成员函数不能定义为虚函数。

  5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。

  6. 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆。

  7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。

  8. 最好把基类的析构函数声明为虚函数。


4. 纯虚函数

  纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,例如:
virtual void funtion()=0

  包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

1. 为何要纯虚函数
  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 
  2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

  纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
  
  定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。定义纯虚函数就是为了让基类不可实例化,因为实例化这样的抽象数据结构本身并没有意义。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
上文来源:https://blog.csdn.net/hackbuteer1/article/details/7558868


4. 多态的实现机制

  虚函数表实际上是一个指针数组,里面存都是虚函数的地址,这个数组以0结尾。所有有虚函数的对象实例中都有一个虚表指针,这个指针指向虚函数表,表里面存了该对象实际调用的虚函数。

class A{
public:
    virtual void f1()
    {}
    virtual void f2()
    {}

    int _a;
};

class B : public A{
public:
    virtual void f1()
    {}
    int _b;
};

int main()
{
    B b;
    b._a = 1;
    b._b = 2;
    return 0;
}

这里写图片描述
  图中对象a有一个虚指针指向a的虚函数表,里面存着a的两个虚函数的地址。对象b也有一个虚指针指向b的虚函数表,里面存着f1()的地址,本来b里面f1()的地址应该和a里面f1()的相同,但因为子类的 f1() 函数重写了父类的f1(),所以b的虚函数表里f1()的地址和a中的有所差异。

  通常我们调试时会用监视窗口查看变量的值,但对于多继承的虚函数来说,使用监视查看对象的虚函数会出现一个bug,如下面一段代码:

class A{
public:
    virtual void f1() {cout << "A::f1()" << endl;}
    virtual void f2() {cout << "A::f2()" << endl;}
    int _a;
};

class B : public A{
public:
    virtual void f1() {cout << "B::f1()" << endl;} //重写了A的f1
    virtual void f3() {cout << "B::f3()" << endl;}
    virtual void f4() {cout << "B::f4()" << endl;}
    int _b;
};

typedef void(*FUNC) ();
//打印虚函数表
void PrintVTable(int* VTable)
{
    cout << "虚表地址:" << VTable << endl;
    for (int i = 0; VTable[i] != 0; ++i)
    {
        printf("第%d个虚函数地址 : 0X%x --> ", i, VTable[i]);
        FUNC f = (FUNC)VTable[i];
        f();
    }
    cout << endl;
}

int main()
{
    B b;
    A a;
    b._a = 1;
    b._b = 2;
    a._a = 3;
    PrintVTable((int*)(*((int*)(&a))));
    PrintVTable((int*)(*((int*)(&b))));

    system("pause");
    return 0;
}

这里写图片描述

  监视中查看b的成员,这里只能看到从A继承过来的两个虚函数f1、f2,但是B中定义的两个虚函数f3、f4却看不到,此时我们只能把虚函数表打印出来查看。
这里写图片描述

1. 多继承
class A{
public:
    virtual void f1(){cout << "A::f1()" << endl;}
    virtual void f2(){cout << "A::f2()" << endl;}
    int _a;
};

class B {
public:
    virtual void f1(){ cout << "B::f1()" << endl;}
    virtual void f2(){ cout << "B::f2()" << endl;}
    int _b;
};

class D : public A, public B{
public:
    virtual void f1(){cout << "D::f1()" << endl;}
    virtual void f3(){cout << "D::f3()" << endl;}
    int _d;
};

int main()
{
    D d;
    d._a = 1;
    d._b = 2;
    d._d = 3;
    cout << "sizeof(A):" << sizeof(A) << endl;
    cout << "sizeof(B):" << sizeof(B) << endl;
    cout << "sizeof(D):" << sizeof(D) << endl << endl;

    PrintVTable((int*)(*((int*)(&d))));
    PrintVTable((int*)(*((int*)(&d) + sizeof(A)/4)));  //打印第二张虚函数表

    return 0;
}

这里写图片描述
首先看看A、B、C三个类型的大小:
A大小是8,包含一个int类型变量,一个函数表指针。
B大小也是8,和A类似。
C大小是20,本身一个int类型变量,两个继承自A、B的int类型变量,两个虚函数表指针。
这里写图片描述

总结:(多继承)
1. 子类虚函数表指针的个数 = 含有虚函数表指针的父类的个数之和。
2. 父类中含有虚函数,且子类没有发生重写,那么子类会生成一个和父类一样的虚函数表;如果子类发生了重写,那么子类中生成的虚函数表中关于这个重写的虚函数这项会被修改(指向重写后的虚函数)。
3. 如果子类中定义的某个虚函数没有构成重写,那么这个虚函数指针会存放在继承第一个的父类的虚函数表(注意这个虚函数表是子类的表)中。


2. 带有虚函数的菱形继承
class A{
public:
    virtual void fa(){ cout << "A::fa()" << endl; }
    int _a;
};

class B : public A {
public:
    virtual void fa(){ cout << "B::fa()" << endl; }
    virtual void fb(){ cout << "B::fb()" << endl; }
    int _b;
};

class C : public A{
public:
    virtual void fa(){ cout << "C::fa()" << endl; }
    virtual void fc(){ cout << "C::fc()" << endl; }
    int _c;
};

class D : public B, public C{
public:
    virtual void fa(){ cout << "D::fa()" << endl; }
    int _d;
};

int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 1;
    d._b = 2;
    d._c = 3;
    d._d = 4;
    return 0;
}

这里写图片描述

  带有虚函数的基类发生继承时,如果子类重写了虚函数,那么子类的对象的虚函数表就会被修改,虚函数表中的对应的函数指针会指向重写的虚函数。如果子类没有重写父类的虚函数,那么子类的虚函数表和父类的虚函数表内容一致。


3. 带有虚函数的菱形虚继承

还是使用上述代码,只不过B、C继承时加上virtual关键字。

这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值