C++ 多态知识解密【静态联编】【动态联编】【函数重载】【函数重写】【函数重定义】【虚函数表与vptr指针】【纯虚函数】【抽象类】【接口】

C++ 多态知识解密

0 - 前言

本文主要参考以下两篇大佬博客:

C/C++中的多态理解

c++中的多态机制

1 - 多态是什么

简单来说就是使一条语句有多种状态。

2 - 多态实现方式

这里涉及到静态联编动态联编

  • 静态联编:在程序的编译期间就能确定具体的函数调用;如函数重载非虚函数重写;静态联编又叫静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错

  • 动态联编:在程序实际运行后才能确定具体的函数调用;如虚函数重写,switch 语句和 if 语句;动态联编又叫动态多态在程序运行时根据父类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。

那么又延伸出三个概念:重载重写重定义

1、函数重载:在同一作用域内(如类内),函数名相同,形参个数/形参类型不同,返回值可以相同也可以不同。但是,两个函数函数名、形参个数、形参类型都相同,只有返回值不同时,无法构成重载。下面例子中,两个func()函数就是属于函数重载的情况

扫描二维码关注公众号,回复: 12896673 查看本文章
class Child : public Parent
{
    
    
public:
    void func(int i, int j)
    {
    
    
        cout << "Child::void func(int i, int j) : " << i + j << endl;
    }
    
    void func(int i, int j, int k)
    {
    
    
        cout << "Child::void func(int i, int j, int k) : " << i + j + k << endl;
    }
};

2、函数重写

在不同作用域内(两个函数一个在父类,一个在子类),函数名、形参个数、形参类型、返回值类型都相同。注意的是,函数重写一定是子类函数重写父类函数,也就是说一定要出现继承关系。

这里分为两种情况:1、非虚函数重写(静态);2、虚函数重写(动态),真正意义上的重写

这两种情况的区别来看一个例子:

//非虚函数重写
class Parent {
    
    
public:    
    void print() {
    
    
        cout << "I'm Parent." << endl;
    }     
};

class Child : public Parent {
    
    
public:    
    void print() {
    
    
        cout << "I'm Child." << endl;
    }
};

void how_to_print(Parent* p) {
    
    
    p->print();
}

int main() {
    
    
   Parent p;
   Child c;
    
   how_to_print(&p);   // I'm Parent    // Expected to print: I'm Parent.
   how_to_print(&c);   // I'm Parent    // Expected to print: I'm Child.
   
    return 0;
}

可以看到,在非虚函数重写中:使用父类指针指向父类对象能够调用父类函数,但是,使用父类指针调用子类对象,调用的仍旧是父类函数,并没有调用子类重写的函数。这是因为:在编译期间,编译器只能根据指针的类型判断所指向的对象;根据赋值兼容,编译器认为父类指针指向的是父类对象;因此,编译结果只可能是调用父类中定义的同名函数

那么虚函数重写呢?再看下面的例子:

//虚函数重写
class Parent {
    
    
public:    
    virtual void print() {
    
    
        cout << "I'm Parent." << endl;
    }     
};

class Child : public Parent {
    
    
public:    
    void print() {
    
    
        cout << "I'm Child." << endl;
    }
};

void how_to_print(Parent* p) {
    
    
    p->print();
}

int main() {
    
    
   Parent p;
   Child c;
    
   how_to_print(&p);   // I'm Parent    // Expected to print: I'm Parent.
   how_to_print(&c);   // I'm Child    // Expected to print: I'm Child.
   
    return 0;
}

可见,将父类函数声明为虚函数以后(使用virtual关键字,注意子类重写函数带不带virtual无所谓),父类指针再次指向子类对象时,调用的子类函数就是重写之后的子类函数

3、函数重定义:不同作用域内的两个函数(一个在父类一个在子类),只要函数名相同,且不构成重写,均称之为重定义

3 - 动态多态的实现原理

其实就是父类指针(引用)调用实际指向对象的过程实现原理

1. 即当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;

2. 即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;

实现动态多态有三个条件:

1. 要有继承

2. 要有虚函数重写(被 virtual 声明的函数叫虚函数)

3. 要有父类指针(父类引用)指向子类对象

关于动态多态的实现原理,就涉及到了虚函数表与vptr指针

1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表;

2. 虚函数表是一个存储类成员函数指针的数据结构;

3. 虚函数表是由编译器自动生成与维护的;

4. virtual成员函数会被编译器放入虚函数表中;

5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。

多态执行过程:

  1. 在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;

  2. 用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;

  3. 在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;

​ 》 如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数

​ 》 如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数

4 - 纯虚函数、抽象类、接口

纯虚函数与普通虚函数的区别在于:有没有定义具体的函数体

//普通虚函数
virtual void print() {
    
    
    cout << "I'm Parent." << endl;
}
//纯虚函数
virtual void print() = 0;

纯虚函数只有被实现以后才能成为虚函数,也就是说,父类中的纯虚函数不能被调用,只能在子类中重写此函数

含有纯虚函数的类就成为抽象类;如果子类没有重写纯虚函数,那么子类也是抽象类;抽象类不能用来创建对象

接口:类中没有任何成员变量 && 所有成员函数全是公有纯虚函数,这个类就是一个接口

用来判断一个类时抽象类还是接口的依据:类中全是纯虚函数就是接口;类中部分是纯虚函数就是抽象类

猜你喜欢

转载自blog.csdn.net/weixin_44484715/article/details/114961083