C++多态详解

面试官夺命连环CALL.
C++三特性:继承,封装,多态。什么是多态?

- 多态分为静态多态(编译阶段)和动态多态(运行阶段)  
- 静态多态:函数重载和泛型编程
- 动态多态:虚函数 :根据绑定的类型调用响应的函数执行!

动态多态依靠虚函数来实现:动态多态三要素:

1. 父类有虚函数;
2. 子类改写了虚函数;
3. 通过父类的指针或引用来调用虚函数, 在运行时,绑定到不同的子类中,产生不同的行为
  1. 什么是虚函数?

    虚函数是希望子类改写的函数,将其声明为虚函数
    我们声明了一个动物类animal, 类中声明了一个speak函数,表示哺乳动物都会叫。
    狗类dog,猫类cat都是哺乳动物的子类,但是狗和猫的叫声都不同,那么就应该将
    animal类中的speak函数声明为虚函数,而dog类和cat类分别实现各自的speak函数

    纯虚函数virtual void fun() = 0; 有纯虚函数的类不能生成对象,成为抽象类,就像动物可以派生为狗和猫,但是动物本身生成对象是不合适的。

    派生类改写的虚函数参数和返回值必须和父类的一样,否则就是函数重载,和继承没有关系

    基类中的虚函数在派生类中默认也是虚函数,所以在派生类中就没有必要加virtual关键字了

    虚函数时如何实现多态的呢?

    答:使用动态绑定

    什么是动态绑定?

    只有虚函数才存在动态绑定,就是在运行时才知道绑定的对象的类型。普通函数在编译阶段就已经绑定了对象的类型,而virtual函数在运行阶段才知道绑定的对象类型。


  2. 为什么只能通过基类指针或引用来实现多态,对象赋值就不行?下面来解答这个问题!!!

    对象的存储模型
    对象只存储了成员变量和vptr,那么成员函数去哪了?
    如果每个对象都存储一份函数,每个函数都是相同的,造成空间浪费。
    C++多态详解_第1张图片
    编译器的做法是,一个类只存放一份函数代码。
    C++多态详解_第2张图片

    函数作为类的公共使用部分,被单独用一段空间来存放,这样节省内存空间;函数存在代码段中,在编译时就已经确定,这个段中一般为只读。
    那如何才能把不同对象的成员赋值给同一个函数,如何区分呢?使用this指针!每个对象有指向自己的this指针,


    类的虚函数表的虚函数指针

    每个类的虚函数表只有一份,同一类的所有对象都指向了相同的虚函数表:每个对象生成一个指针Vptr_来指向这个虚函数表;
    C++多态详解_第3张图片

    那么父类对象如何根据对象的类型调用不同的函数,实现多态?

    只能通过指针或引用!!!
    复制有三类:值复制和引用复制、指针复制
    因为对象里只保存了成员和虚函数指针,那么对象之间的拷贝运算符只复制变量,不复制虚函数表指针?错的,对象之间的复制同样需要复制虚函数表指针,保证虚函数的正常调用,但是这个指针的复制 是编译器负责进行复制。
    程序员自己定义的赋值运算符只需要负责将每个右侧运算对象的每个static成员赋值给左侧对象的对应成员。
    编译器保证每个类的虚函数指针只在同类型对象中有用,如子类对象之间的复制,虚函数指针会进行正常复制,但是如果将一个子类对象赋值给一个父类对象,编译器保证该父类对象的虚函数还是指向父类的虚函数表,不会指向子类对象的虚函数表


    如果通过指针复制:char* A, *B; A=B; 那么A和B指向了同一块内存区域。那么如果将子类对象赋值给基类的指针,**那么二者就指向了同一块区域,就可以通过基类指针对象调用子类函数,实现多态。**如下图
    C++多态详解_第4张图片

    但是如果使用值复制:将子类对象赋值给基类对象时,只会将成员变量进行复制,子类对象的虚函数指针并不会被复制,因此,也无法实现多态,如下图:
    C++多态详解_第5张图片

下面是多态的例子

//多态的例子
#include 
using namespace std;

class animal {
     
private:
    string animal_name;
public:
    virtual void speak() {
     
        std::cout<< "animal speak"<<std::endl;
    }
    void print_animal() {
     
        std:: cout<<"animal"<<std::endl;
    }
};

class dog :  public animal {
     
private:
    string dog_name;
    int dog_age
public:
    void speak() override {
            //改写继承的虚函数;虚函数的参数和返回类型必须和基类时一样的
        std::cout << "dog speak! "<<std::endl;
    }  
    void speak(int i) {
            //"并不是改写继承的虚函数, 而是子类独有的函数(函数重载:静态多态)"
        std::cout<<"dog i speak! "<<std::endl;
    }
    void print_dog() {
     
        std::cout<<"dog"<<std::endl;
    }
};

int main() {
     
    
    dog d;
    animal a1;
    a1.speak();
    a1.print();

    animal *a2 = new animal;
    a2 -> speak();
    a2 ->print();

    animal *a3 = new dog;
    a3->speak(); //发生动态绑定
    a3->print();

    delete a2;
    delete a3;
    return 0;

}

animal speak
animal
animal speak
animal
dog speak!
animal

ps:内存分区:
BSS段:未初始化的全局变量
数据段:已经初始化的全局变量
代码段:非静态函数,字符常量,整形常量等
堆:动态分配的区域
栈:存放临时变量,所以程序运行时,一般的栈溢出就是成员数组什么的发生越界。

你可能感兴趣的:(C++,c++)