C++入门06 --多态,虚函数,虚函数表,纯虚函数,抽象类

多态

  • 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态;
  • 多态是面向对象的非常重要的一个特性;
    • 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果;
    • 在运行时,可以识别出真正的对象类型,调用对应子类中的函数;
#include <iostream>

using namespace::std;

class Animal {

public:
    void run(){
        cout << "Animal run()" << endl;
    }
};

class Dog : public Animal{

public:
    void run(){
        cout << "Dog run()" << endl;
    }
};

class Cat : public Animal{

public:
    void run(){
        cout << "Cat run()" << endl;
    }
};

class Pig : public Animal{

public:
    void run(){
        cout << "Pig run()" << endl;
    }
};

void liu(Animal *a){
    a->run();
}

int main(int argc, const char * argv[]) {
    
    liu(new Dog());
    liu(new Cat());
    liu(new Pig());
    
    return 0;
}
  • 由于用Animal *a来接收形参,那么执行run方法是,调用的都是Animal类中的方法,并没有实现根据不同的对象类型,调用对应的函数方法;

虚函数

  • C++中多态时通过虚函数来实现的;

  • 虚函数:被virtual关键字修饰的函数,称为虚函数;

  • 只要在父类中声明为虚函数,子类中的重写的函数也自动变成虚函数,也就是说子类中的函数可以省略virtual关键字;

  • 多态实现的要素:

    • 子类重写父类的成员函数;
    • 父类指针指向子类对象;
    • 利用父类指针调用子类的成员函数,且成员函数为虚函数;从基类开始虚,从中间类开始虚,也不能实现多态的功能;
#include <iostream>

using namespace::std;

class Animal {

public:
    virtual void run(){
        cout << "Animal run()" << endl;
    }
};

class Dog : public Animal{

public:
    void run(){
        cout << "Dog run()" << endl;
    }
};

class Cat : public Animal{

public:
    void run(){
        cout << "Cat run()" << endl;
    }
};

class Pig : public Animal{

public:
    void run(){
        cout << "Pig run()" << endl;
    }
};

void liu(Animal *a){
    a->run();
}

int main(int argc, const char * argv[]) {
    
    liu(new Dog());
    liu(new Cat());
    liu(new Pig());
    
    return 0;
}
  • 上述的代码实现了多态的功能,即不同的子类对象调用属于自己的函数方法;

虚表

  • 虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫做虚函数表;
#include <iostream>

using namespace::std;

class Animal {
public:
    int m_age;
    
    virtual void speak(){
        cout << "Animal speak()" << endl;
    }
    
    virtual void run(){
        cout << "Animal run()" << endl;
    }
};

class Cat : public Animal{
public:
    int m_life;
    
    void speak(){
        cout << "Cat speak()" << endl;
    }
    
    void run(){
        cout << "Cat run()" << endl;
    }
};

int main(int argc, const char * argv[]) {
    
    Animal *cat = new Cat();
    cat->m_age = 3;
    cat->speak();
    cat->run();
    
    cout << cat << endl;
    
    return 0;
}
  • 当前测试环境是在MacOS,即x64环境,则指针占用8个字节;
  • 当Animal没有虚函数时,cat实例对象有m_age,m_life有两个成员变量,所以cat实例对象占用8个字节;
  • 当Animal有虚函数时,即有virtual关键字修饰函数时,cat实例对象会占用16个字节,前面8个字节存储的是Cat类的虚函数表的地址;
  • cat实例对象的内部布局如下所示:
Snip20210813_147.png
  • 可通过汇编分析证明上面的描述:
Snip20210813_148.png
  • movl $0x3, 0x8(%rax):rax存储的是cat实例对象的内存地址,由于虚函数地址占了前8个字节,所以m_age的偏移量为8;
  • movq (%rax), %rcx:取出cat实例对象的内存地址中前8个字节内容即虚函数的地址,存入rcx寄存器;
  • callq *(%rcx):取出虚函数地址的前8个字节内容,即speak函数内存地址,然后调用speak函数;
  • 同理callq *0x8(%rcx),从虚函数地址的第8个字节开始的后8个字节的内容,即run函数内存地址,然后调用run函数;

总结:

  • 所有cat实例对象,不管在栈区,全局区,堆区都共用一份虚表,也就是说每个类都有自己独立的一份虚表;
  • cat实例对象的前8个字节存储了虚表的内存地址,虚表中存储着cat类的所有虚函数;
  • 若cat类存储虚函数,且cat子类没有实现run方法,那么虚表中会存储父类Animal的run方法,所以会调用Animal的run方法,这也就是说调用目标方法,如果子类没有实现,就调用父类的实现;

子类调用父类的成员函数

#include <iostream>

using namespace::std;

class Animal {
public:
    int m_age;
    
    virtual void speak(){
        cout << "Animal speak()" << endl;
    }
    
    virtual void run(){
        cout << "Animal run()" << endl;
    }
};

class Cat : public Animal{
public:
    int m_life;
    
    void speak(){
        Animal::speak();
        cout << "Cat speak()" << endl;
    }
    
    void run(){
        Animal::run();
        cout << "Cat run()" << endl;
    }
};

int main(int argc, const char * argv[]) {
    
    Animal *cat = new Cat();
    cat->m_age = 3;
    cat->speak();
    cat->run();
    
    cout << cat << endl;
    
    cout << sizeof(Animal) << endl;
    
    return 0;
}
  • 直接在子类的函数中,调用父类的函数;

虚析构函数

  • 含有虚函数的类,应该将析构函数声明为虚函数即虚析构函数;
  • delete父类指针时,才会调用子类的析构函数,保证析构的完整性,否则会造成内存泄漏;
#include <iostream>

using namespace::std;

class Animal {
public:
    int m_age;
    
    virtual void speak(){
        cout << "Animal speak()" << endl;
    }
    
    virtual void run(){
        cout << "Animal run()" << endl;
    }
    
    virtual ~Animal(){
        cout << "~Animal()" << endl;
    }
};

class Cat : public Animal{
public:
    int m_life;
    
    void speak(){
        Animal::speak();
        cout << "Cat speak()" << endl;
    }
    
    void run(){
        Animal::run();
        cout << "Cat run()" << endl;
    }
    
    ~Cat(){
        cout << "~Cat()" << endl;
    }
};

int main(int argc, const char * argv[]) {
    
    Animal *cat = new Cat();
    cat->m_age = 3;
    cat->speak();
    cat->run();
    
    delete cat;
    
    cout << cat << endl;
    
    cout << sizeof(Animal) << endl;
    
    return 0;
}
  • virtual ~Animal():父类Animal的析构函数声明为虚函数,否则Animal *cat = new Cat(),不会调用Cat类的析构函数,造成内存泄漏;

纯虚函数

  • 纯虚函数:没有函数体且初始化为0的虚函数,用来定义借口规范;
class Animal {
public:
    virtual void speak() = 0;
    
    virtual void run() = 0;
};

抽象类

  • 含有纯虚函数的类即为抽象类,不可以实例化创建对象;例如上述的Animal类,在现实世界中是没有animal动物实例对象的,它是一个抽象的概念;
  • 抽象类也可以包含非纯虚函数;
  • 如果父类是抽象类,子类没有全部实现纯虚函数,那么这个子类依然是抽象类,不能实例化创建对象;

推荐阅读更多精彩内容