C++ - 多态

多态:

  1. “多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。

  2. 编译时的多态(静态多态) 主要是指函数的重载(包括运算符的重载)、模版,对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数

    1. 函数重载(Function Overloading):同一作用域内,函数名相同但参数列表不同(参数类型、个数或顺序不同)。
    2. 运算符重载(Operator Overloading):如 +、<< 等运算符的重载,本质上也是函数重载。
    3. 模板(Templates):函数模板和类模板在编译时实例化不同的版本,也属于静态多态。
  3. 运行时的多态(动态多态) 则和继承、虚函数、函数重写、动态绑定等概念有关。

    1. 继承:派生类继承基类的属于和方法。
    2. 虚函数:基类用virtual声明虚函数,派生类通过override重写虚函数(override可以省略不写,但是子类函数的 名称、参数列表、返回类型 与父类完全一致(或返回类型是协变的,如派生类指针)。
    3. 动态绑定:通过基类指针或引用调用虚函数时,实际执行派生类的实现。

虚函数:

  1. C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。

  2. 虚函数(Virtual Function) : 使用虚函数非常简单,只需要在函数声明前面增加 virtual关键字,作用是实现函数地址晚绑定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #include<iostream>
    using namespace std;
    class Animal
    {
    public:
    //speak函数就是虚函数 前面加关键字virtual
    virtual void speak()
    {
    cout<<"动物在说话"<<endl;
    }
    };
    class Cat:public Animal
    {
    public:
    //重写 函数返回值类型 函数名 参数列表 完全相同 virtual可写可不写
    void speak()
    {
    cout<<"小猫在说话"<<endl;
    }
    };
    class Dog:public Animal
    {
    public:
    void speak()
    {
    cout<<"小狗在说话"<<endl;
    }
    };
    //地址早绑定 在编译阶段确定函数地址
    //如果想执行让猫说话 那么这个函数地址就不能提前绑定 需要在运行阶段进行绑定 地址晚绑定 采用虚函数
    void doSpeak(Animal &animal)//Animal &animal=Cat;
    //如果不采用多态 地址会早绑定 不论传入是父类或者是子类对象 都会执行父类的数据 动物在说话
    //如果不采用虚函数 传入子对象 一样会执行父类数据 比如这个如果不采用 会打印出 动物在说话
    {
    animal.speak();//这样打印出来的是子类的数据
    }
    void test01()
    {
    Cat cat;
    doSpeak(cat);
    Dog dog;
    doSpeak(dog);
    }
    int main()
    {
    test01();
    system("pause");
    return 0;
    }
  3. 简单总结

    1) virtual 修饰的函数将变为虚函数

    2) 多态的实现需要使用父类的指针开辟子类的空间

    3) 虚函数创建后会占用类的四字节空间(无论个数)

    4)动态多态满足条件:

    1. 有继承关系
    2. 子类重写父类的虚函数

    5)动态多态使用:

    1. 父类的指针或者引用 指向子类对象

    **重写:**函数返回值类型 函数名 参数列表 完全相同 virtual可写可不写

多态的原理:

多态1

纯虚函数和抽象类:

  1. 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数

  2. 语法格式:

    1. virtual 返回值类型 函数名 (参数列表)= 0;
    2. 纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。(最后的=0 并不表示函数返回值为 0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。)
  3. 当类中有了纯虚函数,这个类也称为抽象类

  4. 抽象类特点:

    • 无法实例化对象
    • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream>
using namespace std;
class Base
{
public:
//纯虚函数
virtual void func()=0;
};
class Son1:public Base
{
public:

};
class Son2:public Base
{
public:
//重写纯虚函数
virtual void func()
{
cout<<"func函数调用"<<endl;
}
};
void test01()
{
//Base b;抽象类无法实例化对象 这是在栈上
//new Base;在堆上一样无法实例化对象
//Son1 s;因为没有重写父类的纯虚函数,也属于抽象类 无法实例化对象
//new的那个类 调用的就是那个类里面的数据
Base *base=new Son2;//多态的使用条件:父类指针或者引用指向子类对象
base->func();//这个就是表示调用的是Son2里面的func()函数
}
int main()
{
test01();
system("pause");
return 0;
}

虚析构和纯虚析构:

  1. 在使用多态时,如果派生类在其构造函数中分配了堆内存(或持有其他需要释放的资源,比如实例化对象),而基类未声明虚析构函数,那么当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,从而导致派生类中分配的资源无法被正确释放,造成内存泄漏(或资源泄漏)。
  2. 解决方式:将父类中的析构函数改为虚析构或者纯虚析构
  3. 虚析构和纯虚析构共性
    • 可以解决父类指针释放子类对象
    • 都需要有具体的函数实现
  4. 虚析构和纯虚析构区别:
    • 如果是纯虚析构,该类属于抽象类,无法实例化对象
  5. 虚析构语法:virtual ~类名(){}
  6. 纯虚析构语法:
    • virtual ~类名()=0;
    • 类名::~类名(){}
  7. 纯虚析构函数和虚析构函数只需要写一个就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<iostream>
using namespace std;
#include<string>
class Animal
{
public:
Animal()
{
cout<<"Animal构造函数调用"<<endl;
}
//利用虚析构可以解决 父类指针释放子类对象时不干净的问题
//virtual ~Animal()
//{
// cout<<"Animal虚析构函数调用"<<endl;
//}

//纯虚析构 需要声明也需要实现
//有了纯虚析构之后 这个类也属于抽象类,无法实例化对象

virtual ~Animal()=0;//声明
//纯虚函数
virtual void speak()=0;
};
//纯虚析构实现
Animal::~Animal()
{
cout<<"Animal纯虚析构函数调用"<<endl;
}
class Cat:public Animal
{
public:
Cat(string name)
{
cout<<"Cat构造函数调用"<<endl;
m_Name=new string(name);
}
virtual void speak()
{
cout<<*m_Name<<"小猫在说话"<<endl;
}
//如果没有父类的虚析构或纯虚析构 就不会运行子类的析构代码 如果子类有堆区的数据从而会造成堆区内存泄漏
~Cat()
{
if(m_Name!=NULL)
{
cout<<"Cat析构函数调用"<<endl;
delete m_Name;
m_Name=NULL;
}
}
string *m_Name;
};
void test01()
{
Animal *animal=new Cat("Tom");
animal->speak();
delete animal;
}
int main()
{
test01();
system("pause");
return 0;
}

总结:

  1. 虚析构和纯虚析构就是用来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
  3. 拥有纯虚析构函数的类也属于抽象类

参考链接: