C++多态与虚函数_多态c++虚函数,2024年最新想找工作的你还不看这份资料就晚了

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<“今年”<<m_age<<“岁了,是个无业游民。”<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
void display();
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<“今年”<<m_age<<“岁了,是一名教师,每月有”<<m_salary<<“元的收入。”<<endl;
}
int main(){
People *p = new People(“王志刚”, 23);
p -> display();
p = new Teacher(“赵宏佳”, 45, 8200);
p -> display();
return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。

我们直观上认为,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是本例的运行结果却告诉我们,当基类指针 p 指向派生类 Teacher 的对象时,虽然使用了 Teacher 的成员变量,但是却没有使用它的成员函数,导致输出结果不伦不类(赵宏佳本来是一名老师,输出结果却显示人家是个无业游民),不符合我们的预期。

换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。

为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。

更改上面的代码,将 display() 声明为虚函数:

#include
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<“今年”<<m_age<<“岁了,是个无业游民。”<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
virtual void display(); //声明为虚函数
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<“今年”<<m_age<<“岁了,是一名教师,每月有”<<m_salary<<“元的收入。”<<endl;
}
int main(){
People *p = new People(“王志刚”, 23);
p -> display();
p = new Teacher(“赵宏佳”, 45, 8200);
p -> display();
return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。

和前面的例子相比,本例仅仅是在 display() 函数声明前加了一个virtual关键字,将成员函数声明为了虚函数(Virtual Function),这样就可以通过 p 指针调用 Teacher 类的成员函数了,运行结果也证明了这一点(赵宏佳已经是一名老师了,不再是无业游民了)。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

上面的代码中,同样是p->display();这条语句,当 p 指向不同的对象时,它执行的操作是不一样的。同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态。

多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

前面我们说过,通过指针调用普通的成员函数时会根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数,但是通过本节的分析可以发现,这种说法并不适用于虚函数,虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。

但是话又说回来,对象的内存模型是非常干净的,没有包含任何成员函数的信息,编译器究竟是根据什么找到了成员函数呢?我们将在《C++虚函数表精讲教程,直戳多态的实现机制》一节中给出答案。

借助引用也可以实现多态

引用在本质上是通过指针的方式实现的,这一点已在《C++引用在本质上是什么,它和指针到底有什么区别?》中进行了讲解,既然借助指针可以实现多态,那么我们就有理由推断:借助引用也可以实现多态。

修改上例中 main() 函数内部的代码,用引用取代指针:

int main(){
People p(“王志刚”, 23);
Teacher t(“赵宏佳”, 45, 8200);

People &rp = p;
People &rt = t;

rp.display();
rt.display();
return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。

由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。本例的主要目的是让读者知道,除了指针,引用也可以实现多态。

多态的用途

通过上面的例子读者可能还未发现多态的用途,不过确实也是,多态在小项目中鲜有有用武之地。

接下来的例子中,我们假设你正在玩一款军事游戏,敌人突然发动了地面战争,于是你命令陆军、空军及其所有现役装备进入作战状态。具体的代码如下所示:

#include
using namespace std;
//军队
class Troops{
public:
virtual void fight(){ cout<<“Strike back!”<<endl; }
};
//陆军
class Army: public Troops{
public:
void fight(){ cout<<“–Army is fighting!”<<endl; }
};
//99A主战坦克
class _99A: public Army{
public:
void fight(){ cout<<“----99A(Tank) is fighting!”<<endl; }
};
//武直10武装直升机
class WZ_10: public Army{
public:
void fight(){ cout<<“----WZ-10(Helicopter) is fighting!”<<endl; }
};
//长剑10巡航导弹
class CJ_10: public Army{
public:
void fight(){ cout<<“----CJ-10(Missile) is fighting!”<<endl; }
};
//空军
class AirForce: public Troops{
public:
void fight(){ cout<<“–AirForce is fighting!”<<endl; }
};
//J-20隐形歼击机
class J_20: public AirForce{
public:
void fight(){ cout<<“----J-20(Fighter Plane) is fighting!”<<endl; }
};
//CH5无人机
class CH_5: public AirForce{
public:
void fight(){ cout<<“----CH-5(UAV) is fighting!”<<endl; }
};
//轰6K轰炸机
class H_6K: public AirForce{
public:
void fight(){ cout<<“----H-6K(Bomber) is fighting!”<<endl; }
};
int main(){
Troops *p = new Troops;
p ->fight();
//陆军
p = new Army;
p ->fight();
p = new _99A;
p -> fight();
p = new WZ_10;
p -> fight();
p = new CJ_10;
p -> fight();
//空军
p = new AirForce;
p -> fight();
p = new J_20;
p -> fight();
p = new CH_5;
p -> fight();
p = new H_6K;
p -> fight();
return 0;
}

运行结果:
Strike back!
–Army is fighting!
----99A(Tank) is fighting!
----WZ-10(Helicopter) is fighting!
----CJ-10(Missile) is fighting!
–AirForce is fighting!
----J-20(Fighter Plane) is fighting!
----CH-5(UAV) is fighting!
----H-6K(Bomber) is fighting!

这个例子中的派生类比较多,如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。

从这个例子中也可以发现,对于具有复杂继承关系的大中型程序,多态可以增加其灵活性,让代码更具有表现力。

C++虚函数注意事项以及构成多态的条件

C++ 虚函数对于多态具有决定性的作用,有虚函数才能构成多态。上节《C++多态和虚函数快速入门教程》我们已经介绍了虚函数的概念,这节我们来重点说一下虚函数的注意事项。

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  2. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。关于名字遮蔽已在《C++继承时的名字遮蔽》一节中进行了讲解。
  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
  5. 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,这点我们将在下节中讲解。

构成多态的条件

站在“学院派”的角度讲,封装、继承和多态是面向对象的三大特征,封装、继承分别在《C++类成员的访问权限以及类的封装》《C++继承和派生简明教程》中进行了讲解,而多态是指通过基类的指针既可以访问基类的成员,也可以访问派生类的成员。

下面是构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类的指针,通过该指针调用虚函数。

下面的例子对各种混乱情形进行了演示:

#include
using namespace std;
//基类Base
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<“void Base::func()”<<endl;
}
void Base::func(int n){
cout<<“void Base::func(int)”<<endl;
}
//派生类Derived
class Derived: public Base{
public:
void func();
void func(char *);
};
void Derived::func(){
cout<<“void Derived::func()”<<endl;
}
void Derived::func(char *str){
cout<<“void Derived::func(char *)”<<endl;
}
int main(){
Base *p = new Derived();
p -> func(); //输出void Derived::func()
p -> func(10); //输出void Base::func(int)
p -> func(“http://c.biancheng.net”); //compile error
return 0;
}

在基类 Base 中我们将void func()声明为虚函数,这样派生类 Derived 中的void func()就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。

语句p -> func();调用的是派生类的虚函数,构成了多态。

语句p -> func(10);调用的是基类的虚函数,因为派生类中没有函数覆盖它。

语句p -> func("http://c.biancheng.net");出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

什么时候声明虚函数

首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

C++虚析构函数的必要性

上节我们讲到,构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。

这是原因之一,另外还有一个原因:C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。下节将会讲解虚函数表的概念。

析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。

为了说明虚析构函数的必要性,请大家先看下面一个例子:

#include
using namespace std;
//基类
class Base{
public:
Base();
~Base();
protected:
char *str;
};
Base::Base(){
str = new char[100];
cout<<“Base constructor”<<endl;
}
Base::~Base(){
delete[] str;
cout<<“Base destructor”<<endl;
}
//派生类
class Derived: public Base{
public:
Derived();
~Derived();
private:
char *name;
};
Derived::Derived(){
name = new char[100];
cout<<“Derived constructor”<<endl;
}
Derived::~Derived(){
delete[] name;
cout<<“Derived destructor”<<endl;
}
int main(){
Base *pb = new Derived();
delete pb;
cout<<“-------------------”<<endl;
Derived *pd = new Derived();
delete pd;
return 0;
}

运行结果:
Base constructor
Derived constructor
Base destructor

Base constructor
Derived constructor
Derived destructor
Base destructor

本例中定义了两个类,基类 Base 和派生类 Derived,它们都有自己的构造函数和析构函数。在构造函数中,会分配 100 个 char 类型的内存空间;在析构函数中,会把这些内存释放掉。

pb、pd 分别是基类指针和派生类指针,它们都指向派生类对象,最后使用 delete 销毁 pb、pd 所指向的对象。

从运行结果可以看出,语句delete pb;只调用了基类的析构函数,没有调用派生类的析构函数;而语句delete pd;同时调用了派生类和基类的析构函数。

在本例中,不调用派生类的析构函数会导致 name 指向的 100 个 char 类型的内存空间得不到释放;除非程序运行结束由操作系统回收,否则就再也没有机会释放这些内存。这是典型的内存泄露。

  1. 为什么delete pb;不会调用派生类的析构函数呢?

因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数;也就是说,指针指向哪个类就调用哪个类的函数,这在前面的章节中已经多次强调过。pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。

  1. 为什么delete pd;会同时调用派生类和基类的析构函数呢?

pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的,这在《C++析构函数》一节中已经讲到了。

更改上面的代码,将基类的析构函数声明为虚函数:

class Base{
public:
Base();
virtual ~Base();
protected:
char *str;
};

运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor

Base constructor
Derived constructor
Derived destructor
Base destructor

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调用哪个类的函数。pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。如此一来也就解决了内存泄露的问题。

在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。

注意,这里强调的是基类,如果一个类是最终的类,那就没必要再声明为虚函数了。

C++纯虚函数和抽象类详解

C++中,可以将虚函数声明为纯虚函数,语法格式为:

virtual 返回值类型 函数名 (函数参数) = 0;

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

纯虚函数使用举例:

#include
using namespace std;
//线
class Line{
public:
Line(float len);
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
Line::Line(float len): m_len(len){ }
//矩形
class Rec: public Line{
public:
Rec(float len, float width);
float area();
protected:
float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }
//长方体
class Cuboid: public Rec{
public:
Cuboid(float len, float width, float height);
float area();
float volume();
protected:
float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_lenm_width + m_lenm_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }
//正方体
class Cube: public Cuboid{
public:
Cube(float len);
float area();
float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }
int main(){
Line *p = new Cuboid(10, 20, 30);
cout<<"The area of Cuboid is "<area()<<endl;
cout<<"The volume of Cuboid is "<volume()<<endl;

p = new Cube(15);
cout<<"The area of Cube is "<area()<<endl;
cout<<"The volume of Cube is "<volume()<<endl;
return 0;
}

运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375

本例中定义了四个类,它们的继承关系为:Line --> Rec --> Cuboid --> Cube。

Line 是一个抽象类,也是最顶层的基类,在 Line 类中定义了两个纯虚函数 area() 和 volume()。

在 Rec 类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 Rec 仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类。

直到 Cuboid 类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。

可以发现,Line 类表示“线”,没有面积和体积,但它仍然定义了 area() 和 volume() 两个纯虚函数。这样的用意很明显:Line 类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。

关于纯虚函数的几点说明
  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:

//顶层函数不能被声明为纯虚函数
void fun() = 0; //compile error
class base{
public :
//普通成员函数不能被声明为纯虚函数
void display() = 0; //compile error
};

C++虚函数表精讲教程,直戳多态的实现机制

前面我们一再强调,当通过指针访问类的成员函数时:

  • 如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针是哪个类的类型就调用哪个类的函数。其原理已在《C++函数编译原理和成员函数的实现》中讲到。
  • 如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable

我们以下面的继承关系为例进行讲解:

#include
#include
using namespace std;
//People类
class People{
public:
People(string name, int age);
public:
virtual void display();
virtual void eating();
protected:
string m_name;
int m_age;
};
People::People(string name, int age): m_name(name), m_age(age){ }
void People::display(){
cout<<“Class People:”<<m_name<<“今年”<<m_age<<“岁了。”<<endl;
}
void People::eating(){
cout<<“Class People:我正在吃饭,请不要跟我说话…”<<endl;
}
//Student类
class Student: public People{
public:
Student(string name, int age, float score);
public:
virtual void display();
virtual void examing();
protected:
float m_score;
};
Student::Student(string name, int age, float score):
People(name, age), m_score(score){ }
void Student::display(){
cout<<“Class Student:”<<m_name<<“今年”<<m_age<<“岁了,考了”<<m_score<<“分。”<<endl;
}
void Student::examing(){
cout<<“Class Student:”<<m_name<<“正在考试,请不要打扰T啊!”<<endl;
}
//Senior类
class Senior: public Student{
public:
Senior(string name, int age, float score, bool hasJob);
public:
virtual void display();
virtual void partying();
private:
bool m_hasJob;
};
Senior::Senior(string name, int age, float score, bool hasJob):
Student(name, age, score), m_hasJob(hasJob){ }
void Senior::display(){
if(m_hasJob){
cout<<“Class Senior:”<<m_name<<“以”<<m_score<<“的成绩从大学毕业了,并且顺利找到了工作,Ta今年”<<m_age<<“岁。”<<endl;
}else{
cout<<“Class Senior:”<<m_name<<“以”<<m_score<<“的成绩从大学毕业了,不过找工作不顺利,Ta今年”<<m_age<<“岁。”<<endl;
}
}
void Senior::partying(){
cout<<“Class Senior:快毕业了,大家都在吃散伙饭…”<<endl;
}
int main(){
People *p = new People(“赵红”, 29);
p -> display();
p = new Student(“王刚”, 16, 84.5);
p -> display();
p = new Senior(“李智”, 22, 92.0, true);
p -> display();
return 0;
}

运行结果:
Class People:赵红今年29岁了。
Class Student:王刚今年16岁了,考了84.5分。
Class Senior:李智以92的成绩从大学毕业了,并且顺利找到了工作,Ta今年22岁。

各个类的对象内存模型如下所示:
img

图中左半部分是对象占用的内存,右半部分是虚函数表 vtable。在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

仔细观察虚函数表,可以发现基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

当通过指针调用虚函数时,先根据指针找到 vfptr,再根据 vfptr 找到虚函数的入口地址。以虚函数 display() 为例,它在 vtable 中的索引为 0,通过 p 调用时:

p -> display();

编译器内部会发生类似下面的转换:

( *( *(p+0) + 0 ) )§;

下面我们一步一步来分析这个表达式:

  • 0是 vfptr 在对象中的偏移,p+0是 vfptr 的地址;
  • *(p+0)是 vfptr 的值,而 vfptr 是指向 vtable 的指针,所以*(p+0)也就是 vtable 的地址;
  • display() 在 vtable 中的索引(下标)是 0,所以( *(p+0) + 0 )也就是 display() 的地址;
  • 知道了 display() 的地址,( *( *(p+0) + 0 ) )(p)也就是对 display() 的调用了,这里的 p 就是传递的实参,它会赋值给 this 指针。

可以看到,转换后的表达式是固定的,只要调用 display() 函数,不管它是哪个类的,都会使用这个表达式。换句话说,编译器不管 p 指向哪里,一律转换为相同的表达式。

转换后的表达式没有用到与 p 的类型有关的信息,只要知道 p 的指向就可以调用函数,这跟名字编码(Name Mangling)算法有着本质上的区别。

再来看一下 eating() 函数,它在 vtable 中的索引为 1,通过 p 调用时:

p -> eating();

编译器内部会发生类似下面的转换:

( *( *(p+0) + 1 ) )§;

对于不同的虚函数,仅仅改变索引(下标)即可。

以上是针对单继承进行的讲解。当存在多继承时,虚函数表的结构就会变得复杂,尤其是有虚继承时,还会增加虚基类表,更加让人抓狂,这里我们就不分析了,有兴趣的读者可以自行研究。

C++ typeid运算符:获取类型信息

typeid 运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:

  • 对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
  • 对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。

类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。

typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:

typeid( dataType )
typeid( expression )

dataType 是数据类型,expression 是表达式,这和 sizeof 运算符非常类似,只不过 sizeof 有时候可以省略括号( ),而 typeid 必须带上括号。

typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。typeid 的使用非常灵活,请看下面的例子(只能在 VC/VS 下运行):

#include
#include
using namespace std;
class Base{ };
struct STU{ };
int main(){
//获取一个普通变量的类型信息
int n = 100;
const type_info &nInfo = typeid(n);
cout<<nInfo.name()<<" | “<<nInfo.raw_name()<<” | “<<nInfo.hash_code()<<endl;
//获取一个字面量的类型信息
const type_info &dInfo = typeid(25.65);
cout<<dInfo.name()<<” | “<<dInfo.raw_name()<<” | “<<dInfo.hash_code()<<endl;
//获取一个对象的类型信息
Base obj;
const type_info &objInfo = typeid(obj);
cout<<objInfo.name()<<” | “<<objInfo.raw_name()<<” | “<<objInfo.hash_code()<<endl;
//获取一个类的类型信息
const type_info &baseInfo = typeid(Base);
cout<<baseInfo.name()<<” | “<<baseInfo.raw_name()<<” | “<<baseInfo.hash_code()<<endl;
//获取一个结构体的类型信息
const type_info &stuInfo = typeid(struct STU);
cout<<stuInfo.name()<<” | “<<stuInfo.raw_name()<<” | “<<stuInfo.hash_code()<<endl;
//获取一个普通类型的类型信息
const type_info &charInfo = typeid(char);
cout<<charInfo.name()<<” | “<<charInfo.raw_name()<<” | “<<charInfo.hash_code()<<endl;
//获取一个表达式的类型信息
const type_info &expInfo = typeid(20 * 45 / 4.5);
cout<<expInfo.name()<<” | “<<expInfo.raw_name()<<” | "<<expInfo.hash_code()<<endl;
return 0;

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

const type_info &baseInfo = typeid(Base);
cout<<baseInfo.name()<<" | “<<baseInfo.raw_name()<<” | “<<baseInfo.hash_code()<<endl;
//获取一个结构体的类型信息
const type_info &stuInfo = typeid(struct STU);
cout<<stuInfo.name()<<” | “<<stuInfo.raw_name()<<” | “<<stuInfo.hash_code()<<endl;
//获取一个普通类型的类型信息
const type_info &charInfo = typeid(char);
cout<<charInfo.name()<<” | “<<charInfo.raw_name()<<” | “<<charInfo.hash_code()<<endl;
//获取一个表达式的类型信息
const type_info &expInfo = typeid(20 * 45 / 4.5);
cout<<expInfo.name()<<” | “<<expInfo.raw_name()<<” | "<<expInfo.hash_code()<<endl;
return 0;

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-TcFaThGQ-1713288685413)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值