王清欢Randy 王清欢Randy
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
  • C语言基础

    • 数据类型
    • 指针与字符串
    • 结构类型
    • 链表
    • 程序结构
    • 文件
  • C++面向对象编程

    • C++面向对象

      • 从 C 到 C++
      • C++ 类和对象基础
      • C++ 构造函数与析构函数
      • C++ 类和对象提高
      • 运算符重载
      • C++ 继承
      • C++ 多态
        • 虚函数和多态的概念
        • 纯虚函数和抽象类
        • 多态的实现原理
          • 虚函数表
        • Reference
    • C++ STL

      • C++ 输入输出流
      • C++ 泛型编程
      • C++ string类
      • C++ 标准模板库 STL 概述
      • C++ 标准模板库 STL 顺序容器
      • C++ 标准模板库 STL 函数对象
      • C++ 标准模板库 STL 关联容器
      • C++ 标准模板库 STL 容器适配器
    • C++ 新特性

      • C++11 新特性
  • C&Cpp学习笔记
  • C++面向对象编程
  • C++面向对象
王清欢
2023-11-18
目录

C++ 多态

# C++ 多态

# 虚函数和多态的概念

虚函数:

  • 在类的定义中,有 virtual 关键字的成员函数就是虚函数,形如:

    class Base{
        public:
        	virtual int get();  
    };
    int Base::get(){} // virtual 关键字只需要在类内声明
    
    class Derived:public Base{
        public:
        	virtual int get();
    };
    int Derived::get(){}
    
    int main(){
        Derived de;
        Base b;
        Base* p = & de;
        Base& r = de;
        p->get(); // 多态 调用派生类虚函数
        r.get();
        p = & b;
        p->get(); // 多态 调用ji类虚函数
        return 0;
    }
    
  • virtual 关键字只用在类定义内的函数声明中使用,在类外写函数体时不用写 virtual 关键字。

  • 构造函数和静态成员函数不能是虚函数

  • 虚函数和普通函数的区别在于:虚函数能够参与多态,而普通函数不可以

  • 在非构造函数和非析构函数的成员函数中调用虚函数,就是多态。反之在构造函数和析构函数中调用虚函数,不是多态;因为在编译时就能确定被调用的虚函数是基类还是派生类的,不用等到运行才确定。**为什么构造函数中调用函数不是多态?**因为派生类调用构造函数期间会先调用基类构造函数,如果基类构造函数中存在多态并调用了派生类的虚函数,这时派生类还未调用构造函数进行初始化,这将导致错误的运行结果。

  • 在派生类中和基类中虚函数同名同参数表的函数,不加 virtual 关键字也自动成为虚函数

#include <iostream>
using namespace std;

class Base{
    public:
        void func1(){ func2(); } // void func1(){ this->func2(); }  this指针指向当前被调对象 多态
        virtual void func2(){cout<< "Base func2 called!"<<endl;}
        virtual void hello(){cout<<"Base hello"<<endl;}
};

class Derived:public Base{
    public:  
        virtual void func2(){cout<< "Derived func2 called!"<<endl;}
        void hello(){cout<<"Derived hello"<<endl;}
        Derived(){hello();} // 不是多态
        ~Derived(){hello();}
};

int main(){
    Derived d;
    Base* pb = &d;
    pb->func1();
    return 0;
}

多态的表现形式:

  • 通过基类指针调用基类和派生类中的同名虚函数时:
    • 指针指向一个基类的对象,那么被调用的是基类的虚函数
    • 指针指向一个派生类的对象,那么被调用的是派生类的虚函数
  • 通过基类引用调用基类和派生类中的同名虚函数时:
    • 引用引用的是一个基类的对象,那么被调用的是基类的虚函数
    • 引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数

多态的作用:

​ 在面向对象的程序设计中使用多态能够增强程序的可扩充性,即程序需要修改或增加功能的时候,避免大量代码的改动和增加。但是多态的机制会增加程序执行时在时间和空间上的开销,空间上是由于存在虚函数的类的每个对象在创建时都会多出 4 个字节的额外空间开销用于存放虚函数表的地址;时间上是由于在查虚函数表的过程中需要消耗一定的时间。

虚析构函数:

​ 当通过基类指针销毁派生类对象时,通常只有基类的构造函数被调用,一般是先调用派生类析构函数,再调用基类构造函数,这就出现了析构不完整的情况。为此,可以将基类的析构函数声明为虚函数,即虚析构函数,派生类的析构函数就不需要进行虚函数声明,自动成为虚函数;这时,通过基类指针析构派生类对象时调用的就是派生类的析构函数,派生类的析构函数中会调用基类的析构函数。

​ 一般来说,一个类定义了虚函数,则应该将该类的析构函数声明为虚函数;另外,如果一个类要被作为基类使用,则也应该将该类的析构函数声明为虚函数。

# 纯虚函数和抽象类

纯虚函数:没有函数体的虚函数,virtual void Print() = 0;

抽象类:包含纯虚函数的类称为抽象类

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
  • 抽象类的指针/引用可以用来指向/引用由抽象类派生出来的类的对象
  • 抽象类的成员函数内可以调用纯虚函数(如果能够创建抽象类的对象,而其成员函数调用了没有函数体的纯虚函数将导致程序错误),但是在其构造函数和析构函数中不能调用纯虚函数
  • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数时,它才能成为非抽象类(类似 Java 中的接口)

# 多态的实现原理

​ 多态的关键在于通过基类指针或基类引用调用虚函数时,编译时还不确定该语句调用的是基类函数还是派生类函数,直到运行时才能确定,这种机制也称为动态联编。

# 虚函数表

​ 每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针(可以认为这是由编译器自动添加到构造函数中的指令完成的)。虚函数表中列出了该类的虚函数地址,有虚函数的类会多出 4 个字节(64 位中是 8 个字节)用于存放虚函数表的地址。虚函数表是编译器生成的,程序运行时被载入内存。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    virtual void func() {cout << "A func" <<endl;}
    virtual void func2() {}
};
class B : public A
{
    int j;
    void func() {cout << "B func" <<endl;}
};
int main()
{
    cout << sizeof(A) << ", " << sizeof(B);  // A B都多出4/8个字节存虚函数表地址
    A* pa = new B();
    pa->func();
    // 64位程序指针是 8 个字节与 long long * 所占空间相同
	A a;
    long long * p1 = (long long *)& a; // 取对象 a 的前 8 个字节即虚函数表地址存储位置
    long long * p2 = (long long *) pa; 
    * p2 = * p1; // 将虚函数表地址互换
    pa->func(); // pa 的虚函数表地址变成了 a 的
    return 0;
}

​ 上述类 A 和类 B 的内存空间占用如下图所示,首部 4 个字节用于存放该类的虚函数表地址接着再存成员变量。根据虚函数表可以更好的理解多态机制,在编译过程中,存在多态的函数语句被编译成一系列依据基类指针/基类引用所指向/引用的对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令。例如上述代码中第一个 pa->func(); ,pa 指向 B 的对象,则依据类 B 的虚函数表地址找到虚函数表,然后在虚函数表中找到 虚函数 B::func 的地址,最后根据地址找到并调用虚函数 B:func 的指令 。

虚函数表

多态实例-野怪

  • 每个野怪类都要有 Attack(),FightBack(),Hurted() 成员函数
  • Attack() 函数表现攻击动作,攻击某个野怪,并调用被攻击野怪的 Hurted() 函数,以减少被攻击野怪的生命值,同时也调用被攻击野怪的 FightBack() 成员函数,遭受被攻击野怪反击。
// 基类 Creature
class Creature{
    protected:
    	int m_nLifeValue, m_nPower;
    public:
    	virtual void Attack(Creature* pC){}
    	virtual void Hurted(int nPower){}
    	virtual void FightBack(Creature* pc){}
};

class Dragon:public Creature{
    public:
    	virtual void Attack(Creature* pC);
    	virtual void Hurted(int nPower);
    	virtual void FightBack(Creature* pc);    	
};

void Dragon::Attack(Creature* p){
    p->Hurted(m_nPower);
    p->FightBack(this);
}
void Dragon::Hurted(int nPower){
    m_nLifeValue -= nPower;
}
void Dragon::FightBack(Creature* p){
    p->Hurted(m_nPower/2);
}

class Wolf:public Creature{ ... };
class Bird:public Creature{ ... };
class Fish:public Creature{ ... };

int main(){
    Dragon dragon;
    Wolf wolf;
    Bird bird;
    Fish fish;
    dargon.Attack(& wolf); // 多态 调用 wolf.Hurted(); wolf.FightBack();
    dargon.Attack(& bird); // 多态 调用 bird.Hurted(); bird.FightBack();
    dargon.Attack(& fish); // 多态 调用 fish.Hurted(); fish.FightBack();
    return 0;
}

多态实例-几何形体

  • 输入若干个几何形体的参数,输出根据每个几何形体的面积排序输出
  • 输入:
    • 第一行输入指定几何形体的个数 N,接下来每一行由代表几何形体的字符开头,后面跟其计算面积的参数
    • R 表示矩形,带宽高两个参数;C 表示圆带一个表示半径的参数;T 表示三角形,带三条边三个参数
  • 输出:根据每个几何形体的面积排序分别输出其类别和面积大小
  • 一个输入输出示例:
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;

class Shape{
    public:
        virtual double area() = 0; // 纯虚函数 Shape 无法计算面积 没有信息
        virtual void printInfo() = 0;
    	virtual ~Shape(){}; // 虚析构函数 delete pr/pc/pt; 析构完整
};

class Rectangle:public Shape{
    public:
        int width, height;
        virtual double area();
        virtual void printInfo();
};

class Circle:public Shape{
    public:
        int radius;
        virtual double area();
        virtual void printInfo();
};

class Triangle:public Shape{
    public:
        int a, b, c;
        virtual double area();
        virtual void printInfo();
};

double Rectangle::area(){
    return width*height;
}

double Circle::area(){
    return 3.14*pow(radius,2);
}

double Triangle::area(){
    double temp = (a+b+c)/2.0;
    return sqrt(temp*(temp-a)*(temp-b)*(temp-c));
}

void Rectangle::printInfo(){
    cout << "Rectangle:" << area() << endl;
}

void Circle::printInfo(){
    cout << "Circle:" << area() << endl;
}

void Triangle::printInfo(){
    cout << "Triangle:" << area() << endl;
}

Shape* pShape[100];
int compare(const void * s1, const void * s2){
    Shape** p1;
    Shape** p2; // p1 p2 都是指向指针的指针
    p1 = (Shape**)s1; // 从 pShape 中取几何形体都是指向该几何形体 Shape* 类型 所以要用 * 号取值
    p2 = (Shape**)s2;
    double a1,a2;
    a1 = (*p1)->area(); // p1 p2 是基类指针,被调用的 area() 取决与 p1 p2 指向的派生类, 所以此处是多态
    a2 = (*p2)->area();
    if(a1 < a2){
        return -1;
    }
    else if (a2 < a1)
    {
        return 1;
    }
    else{
        return 0;
    }
    
} 

int main(){

    int n;
    cin >> n;
    for(int index = 0; index < n; index++){
        char c;
        cin >> c;
        switch (c)
        {
        case 'R':
            Rectangle* pr;
            pr = new Rectangle();
            cin >> pr->width >> pr->height;
            pShape[index] = pr;
            delete pr;
            break;
        case 'C':
            Circle* pc;
            pc = new Circle();
            cin >> pc->radius;
            pShape[index]= pc;
            delete pc;
            break;
        case 'T':
            Triangle* pt;
            pt = new Triangle();
            cin >> pt->a >> pt->b >> pt->c;
            pShape[index] = pt;
            delete pt;
            break;
        default:
            break;
        }
    }

    qsort(pShape,n,sizeof(Shape*),compare);
    for(int i=0; i<n; i++){
        pShape[i]->printInfo();
    }

    return 0;
}
/* Input:
3
R 3 5
C 9
T 3 4 5
// Output:
Triangle:6
Rectangle:15
Circle:254.34
*/

# Reference

C++虚函数表(多态的实现原理) (opens new window)

上次更新: 2023/11/19, 12:55:48
C++ 继承
C++ 输入输出流

← C++ 继承 C++ 输入输出流→

Theme by Vdoing | Copyright © 2023-2024 Wang Qinghuan | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式