-
2021-06-29 22:54:31
面试官夺命连环CALL.
C++三特性:继承,封装,多态。什么是多态?- 多态分为静态多态(编译阶段)和动态多态(运行阶段) - 静态多态:函数重载和泛型编程 - 动态多态:虚函数 :根据绑定的类型调用响应的函数执行!
动态多态依靠虚函数来实现:动态多态三要素:
1. 父类有虚函数; 2. 子类改写了虚函数; 3. 通过父类的指针或引用来调用虚函数, 在运行时,绑定到不同的子类中,产生不同的行为
-
什么是虚函数?
虚函数是希望子类改写的函数,将其声明为虚函数
我们声明了一个动物类animal, 类中声明了一个speak函数,表示哺乳动物都会叫。
狗类dog,猫类cat都是哺乳动物的子类,但是狗和猫的叫声都不同,那么就应该将
animal类中的speak函数声明为虚函数,而dog类和cat类分别实现各自的speak函数纯虚函数virtual void fun() = 0; 有纯虚函数的类不能生成对象,成为抽象类,就像动物可以派生为狗和猫,但是动物本身生成对象是不合适的。
派生类改写的虚函数参数和返回值必须和父类的一样,否则就是函数重载,和继承没有关系
基类中的虚函数在派生类中默认也是虚函数,所以在派生类中就没有必要加virtual关键字了
虚函数时如何实现多态的呢?
答:使用动态绑定
什么是动态绑定?
只有虚函数才存在动态绑定,就是在运行时才知道绑定的对象的类型。普通函数在编译阶段就已经绑定了对象的类型,而virtual函数在运行阶段才知道绑定的对象类型。
-
为什么只能通过基类指针或引用来实现多态,对象赋值就不行?下面来解答这个问题!!!
对象的存储模型
对象只存储了成员变量和vptr,那么成员函数去哪了?
如果每个对象都存储一份函数,每个函数都是相同的,造成空间浪费。
编译器的做法是,一个类只存放一份函数代码。
函数作为类的公共使用部分,被单独用一段空间来存放,这样节省内存空间;函数存在代码段中,在编译时就已经确定,这个段中一般为只读。
那如何才能把不同对象的成员赋值给同一个函数,如何区分呢?使用this指针!每个对象有指向自己的this指针,
类的虚函数表的虚函数指针每个类的虚函数表只有一份,同一类的所有对象都指向了相同的虚函数表:每个对象生成一个指针Vptr_来指向这个虚函数表;
那么父类对象如何根据对象的类型调用不同的函数,实现多态?
只能通过指针或引用!!!
复制有三类:值复制和引用复制、指针复制
因为对象里只保存了成员和虚函数指针,那么对象之间的拷贝运算符只复制变量,不复制虚函数表指针?错的,对象之间的复制同样需要复制虚函数表指针,保证虚函数的正常调用,但是这个指针的复制 是编译器负责进行复制。
程序员自己定义的赋值运算符只需要负责将每个右侧运算对象的每个static成员赋值给左侧对象的对应成员。
编译器保证每个类的虚函数指针只在同类型对象中有用,如子类对象之间的复制,虚函数指针会进行正常复制,但是如果将一个子类对象赋值给一个父类对象,编译器保证该父类对象的虚函数还是指向父类的虚函数表,不会指向子类对象的虚函数表。
如果通过指针复制:char* A, *B; A=B; 那么A和B指向了同一块内存区域。那么如果将子类对象赋值给基类的指针,**那么二者就指向了同一块区域,就可以通过基类指针对象调用子类函数,实现多态。**如下图
但是如果使用值复制:将子类对象赋值给基类对象时,只会将成员变量进行复制,子类对象的虚函数指针并不会被复制,因此,也无法实现多态,如下图:
下面是多态的例子
//多态的例子 #include<iostream> 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++多态的实现及原理详细解析
2021-01-01 04:22:314. 多态用虚函数来实现,结合动态绑定。5. 纯虚函数是虚函数再加上= 0。6. 抽象类是指包括至少一个纯虚函数的类。 纯虚函数:virtual void breathe()=0;即抽象类!必须在子类实现这个函数!即先有名称,没内容,在... -
c++多态类动物的叫声
2021-06-12 22:06:24c++多态类动物的叫声 -
详解C++ 多态的两种形式(静态、动态)
2020-12-17 00:44:26C++中的多态性具体体现在编译和运行两个阶段。编译时多态是静态多态,在编译时就可以确定使用的接口。运行时多态是动态多态,具体引用的接口在运行时才能确定。 静态多态和动态多态的区别其实只是在什么时候将函数... -
C++ 多态 虚函数 虚函数表 最是详细
2017-10-01 14:08:56高质量的C++多态讲解,详细讲解虚函数,虚函数表,虚函数继承,虚函数继承下的内存分配等 -
C++多态
2020-10-20 17:15:01C++多态前言动态多态多态的构造多态的使用多态底层剖析解答纯虚数函数和抽象类虚析构和纯虚析构总结 前言 多态是C++面向对象三大特性之一 多态分为两类 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名 ...前言
多态是C++面向对象三大特性之一
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别: - 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
静态多态比较简单,如果想要对静态多态中的函数重载了解,
可以参考这篇博文:
C++函数提高
动态多态
通过一个例子来了解一下动态多态。
#include <iostream> #include <string> using namespace std; class Animal { public: virtual void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); system("pause"); return 0; }
多态的构造
如果我们想要使用多态技术,前提得要构造一个多态。
- 需要构造继承关系
- 父类一定要定义虚函数
- 子类一定要重写
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
子类中重写函数virtual标识符可写可不写多态的使用
我们在使用多态之前,一定要构造,这是非常重要的。
如果你已经对多态进行了构造,那么多态的使用就变的很简单了。
父类引用或指针指向子类对象多态底层剖析
我们先把父类中的虚函数中标识符virtual给去掉,看看Animal类在内存中占多少个字节,以及显示的是哪个作用域下的speak函数。
#include <iostream> #include <string> using namespace std; class Animal { public: void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); printf("%d", sizeof(Animal)); system("pause"); return 0; }
我们可以看到,当把virtual标识符去掉以后,调用的speak是Animal作用域下的speak,且此时对象在内存中所占一个字节空类在内存中占一个字节
现在我们将virtual标识符 给加上,我们再看下效果。
#include <iostream> #include <string> using namespace std; class Animal { public: virtual void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); printf("%d", sizeof(Animal)); system("pause"); return 0; }
仅仅只是在函数的前面加上了virtual标识符,结果却大不相同。
现在调用的speak函数是在Cat作用域下的,并且此时对象在内存中所占内存的大小为4个字节。解答
对于去掉virtual时,对象在内存中所占字节数为1,这个很简单,以为此类该类为空类,因为空类在内存中所占字节数为1。那为什么加上virtual时,对象在内存中所占字节数为4呢,字节为4,大家脑子里应该想的都是指针吧。
没错,就是指针,这个指针的名字是vfptr,也就是虚函数指针,这个指针指向的是虚函数表,而在这虚函数表中,就存放了虚函数的地址。
因此,就是通过这个vfptr来调用子类或父类作用域下的函数的。一个类中,无论虚函数有多少个,只要没有成员属性,那么sizeof of 的结果就是4.
现在,我们来看看我说的到底对不对。
- 打开VS2019开发者命令行工具(每个版本都有)
- 切换到源文件所在路径
- cl /dl reportSingleClassLayout类名 "源文件名.cpp"
cd :切换目录
dir :查看目录下的成员首先看看,如果没有virtual表示符,Animal和Cat类是什么样子的。
我们发现,此时Animal类所占字节确实是为1.
然后我们看下Cat类的情况。
因为Cat的父类是Animal,所以Cat继承了Animal的诸多属性,因此Cat类在内存中所在字节数为1。
那现在我们看下加上virtual标识符,结果又会是怎么样
这个时候,Animal类所在字节为4,也就是上图中vfptr占4个字节,vfptr中存储着vftable的首地址,也就是说vfptr指向vftable,而在这个vftable中,存储着&Animal::speak 函数的地址。因此,通过vfptr就可以调用子类中的成员函数
那我们再来看看Cat类的情况怎么样,你可以猜猜,因为Cat类是Animal类在子类,所以Cat会继承Animal的成员属性,也就是vfptr中的内容和Animal中vfptr中的内容相同。并且vftable都指向&Animal::speak函数。
哦,好像不对,虽然Cat类中也有一个vfptr,指向Cat::vftable,但是vftable中存储的内容并不是&Animal::speak,而是&Cat::speak。为什么呢?
因为,当子类对父类中的虚函数重写的时候,会对vftable中的内容进行重写,把存储&Animal::speak的存储块中的内容重新写,写进去的内容为&Cat::speak。
这样的话,当父类的引用或指针指向子类对象时,如果父类中没有添加virtual,那么这个时候就会调用父类中的speak函数,如果加上virtual,那么就会调用子类中的speak。又因为同一个父类可以有多个子类,因此指向不同子类的对象时,调用不同子类作用域下的函数。
这就是博文开篇所提到的,在运行阶段确定函数的地址,因为不同的子类对象,地址是不一样的。纯虚数函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base { public: //纯虚函数 //类中只要有一个纯虚函数就称为抽象类 //抽象类无法实例化对象 //子类必须重写父类中的纯虚函数,否则也属于抽象类 virtual void func() = 0; }; class Son :public Base { public: virtual void func() { cout << "func调用" << endl; }; }; void test01() { Base * base = NULL; //base = new Base; // 错误,抽象类无法实例化对象 base = new Son; base->func(); delete base;//记得销毁 } int main() { test01(); system("pause"); return 0; }
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,这样就会造成内存泄漏
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别: - 如果是纯虚析构,该类属于抽象类,无法实例化对象
只要类中存在一个纯虚函数,那么该类就是是抽象类,无法实例化对象。
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
#include <iostream> #include <string> using namespace std; class Animal { public: Animal() { cout << "Animal 构造函数调用!" << endl; } virtual void Speak() = 0; //析构函数加上virtual关键字,变成虚析构函数 //virtual ~Animal() //{ // cout << "Animal虚析构函数调用!" << endl; //} ~Animal() { cout <<"Animal 析构函数调用"<<endl; } //virtual ~Animal() = 0; }; //Animal::~Animal() //{ // cout << "Animal 纯虚析构函数调用!" << endl; //} //和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。 class Cat : public Animal { public: Cat(string name) { cout << "Cat构造函数调用!" << endl; m_Name = new string(name); } void Speak() { cout << *m_Name << "小猫在说话!" << endl; } ~Cat() { cout << "Cat析构函数调用!" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } } public: string* m_Name; }; void test01() { Animal* animal = new Cat("Tom"); animal->Speak(); //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏 //怎么解决?给基类增加一个虚析构函数 //虚析构函数就是用来解决通过父类指针释放子类对象 delete animal; } int main() { test01(); system("pause"); return 0; }
我们知道,当定义一个子类时,是先调用父类构造函数,再调用子类构造函数,析构函数的调用方式相反。
我们运行上述代码,发现调用不了子类中的析构代码,这就会造成内存泄漏。而如果我们将父类中的析构函数改为虚析构函数的话,那么这个问题就可以解决了。
但是我们要注意,虚析构函数是要有主体的,不能和虚函数那样不需要函数主体。#include <iostream> #include <string> using namespace std; class Animal { public: Animal() { cout << "Animal 构造函数调用!" << endl; } virtual void Speak() = 0; //析构函数加上virtual关键字,变成虚析构函数 //virtual ~Animal() //{ // cout << "Animal虚析构函数调用!" << endl; //} virtual ~Animal() = 0; }; Animal::~Animal() { cout << "Animal 纯虚析构函数调用!" << endl; } //和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。 class Cat : public Animal { public: Cat(string name) { cout << "Cat构造函数调用!" << endl; m_Name = new string(name); } void Speak() { cout << *m_Name << "小猫在说话!" << endl; } ~Cat() { cout << "Cat析构函数调用!" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } } public: string* m_Name; }; void test01() { Animal* animal = new Cat("Tom"); animal->Speak(); //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏 //怎么解决?给基类增加一个虚析构函数 //虚析构函数就是用来解决通过父类指针释放子类对象 delete animal; } int main() { test01(); system("pause"); return 0; }
#include <iostream> #include <string> using namespace std; class Animal { public: Animal() { cout << "Animal 构造函数调用!" << endl; } virtual void Speak() = 0; //析构函数加上virtual关键字,变成虚析构函数 //virtual ~Animal() //{ // cout << "Animal虚析构函数调用!" << endl; //} virtual ~Animal() = 0; }; Animal::~Animal() { cout << "Animal 纯虚析构函数调用!" << endl; } //和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。 class Cat : public Animal { public: Cat(string name) { cout << "Cat构造函数调用!" << endl; m_Name = new string(name); } void Speak() { cout << *m_Name << "小猫在说话!" << endl; } ~Cat() { cout << "Cat析构函数调用!" << endl; if (this->m_Name != NULL) { delete m_Name; m_Name = NULL; } } public: string* m_Name; }; void test01() { Animal* animal = new Cat("Tom"); animal->Speak(); //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏 //怎么解决?给基类增加一个虚析构函数 //虚析构函数就是用来解决通过父类指针释放子类对象 delete animal; } int main() { test01(); system("pause"); return 0; }
我们发现,这个时候Cat析构函数被调用了,就不会造成内存泄漏。注意:虚析构函数或纯虚析构函数一定要有函数体,这点和虚函数不一样
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
总结
本篇博文对多态有了个大概的了解,可能再日后的学习过程当中会有更深地理解,届时再对多态进行深入的解析。
-
详解C++ 多态的实现及原理
2021-01-20 05:55:41C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用... -
c++多态总结
2018-08-30 13:25:38c++多态总结,vptr总结,多态原理探究 -
C++多态Demo
2019-05-01 09:52:12长方形和三角形均继承于形状,拥有高和宽,分别求面积 -
C++ 多态 超详细讲解
2021-05-16 11:46:58文章目录多态概念引入1、C++中多态的实现1.1 多态的构成条件1.2 虚函数1.3虚函数的重写1.4 C++11 override && final1.5 重载,覆盖(重写),重定义(隐藏)2、抽象类2.1 抽象类的概念2.2 接口继承和实现继承3...文章目录
多态概念引入
多态字面意思就是多种形态。
我们先来想一想在日常生活中的多态例子:买票时,成人买票全价,如果是学生那么半价,如果是军人,就可以优先买票。不同的人买票会有不同的实现方法,这就是多态。1、C++中多态的实现
1.1 多态的构成条件
C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写
我们来看看具体实现。class Person //成人 { public: virtual void fun() { cout << "全价票" << endl; //成人票全价 } }; class Student : public Person //学生 { public: virtual void fun() //子类完成对父类虚函数的重写 { cout << "半价票" << endl;//学生票半价 } }; void BuyTicket(Person* p) { p->fun(); } int main() { Student st; Person p; BuyTicket(&st);//子类对象切片过去 BuyTicket(&p);//父类对象传地址 }
调用的两个BuyTicket() 答案是什么呢?
如果不满足多态呢?
这说明了很重要的一点,如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。1.2 虚函数
用virtual修饰的关键字就是虚函数。
虚函数只能是类中非静态的成员函数。virtual void fun() //error! 在类外面的函数不能是虚函数 {}
1.3虚函数的重写
子类和父类中的虚函数拥有相同的名字,返回值,参数列表,那么称子类中的虚函数重写了父类的虚函数,或者叫做覆盖。
class Person { public: virtual void fun() { cout << "Person->fun()" << endl; } }; class Student { public: //子类重写的虚函数可以不加virtual,因为子类继承了父类的虚函数, //编译器会认为你是想要重写虚函数。 //void fun() 可以直接这样,也对,但不推荐。 virtual void fun()//子类重写父类虚函数 { cout << "Student->fun()" << endl; } };
虚函数重写的两个例外:
- 协变:
子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。
class Person { public: virtual Person* fun()//返回父类指针 { cout << "Person->fun()" << endl; return nullptr; } }; class Student { public: //返回子类指针,虽然返回值不同,也构成重写 virtual Student* fun()//子类重写父类虚函数 { cout << "Student->fun()" << endl; return nullptr; } };
也可以这样,也是协变,
class A {}; class B : public A {}; //B继承A class Person { public: virtual A* fun()//返回A类指针 { return nullptr; } }; class Student { public: //返回B类指针,虽然返回值不同,也构成重写 virtual B* fun()//子类重写父类虚函数 { return nullptr; } };
2.析构函数的重写
析构函数是否需要重写呢?
让我们来考虑这样一种情况,//B继承了A,他们的析构函数没有重写。 class A { public: ~A() { cout << "~A()" << endl; } }; class B : public A { public: ~B() { cout << "~B()" << endl; } }; A* a = new B; //把B的对象切片给A类型的指针。 delete a; //调用的是谁的析构函数呢?你希望调用谁的呢?
显然我们希望调用B的析构函数,因为我们希望析构函数的调用跟指针指向的对象有关,而跟指针的类型无关。这不就是多态吗?但是结果却调用了A的析构函数。
所以析构函数要实现多态。But,析构函数名字天生不一样,怎么实现多态?
实际上,析构函数被编译器全部换成了Destructor,所以我们加上virtual就可以。
只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。
这也解释了为什么子类不写virtual可以构成重写,因为编译器怕你忘记析构。class A { public: virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: virtual ~B() { cout << "~B()" << endl; } };
1.4 C++11 override && final
C++11新增了两个关键字。用final修饰的虚函数无法重写。用final修饰的类无法被继承。final像这个单词的意思一样,这就是最终的版本,不用再更新了。
class A final //A类无法被继承 { public: virtual void fun() final //fun函数无法被重写 {} }; class B : public A //error { public: virtual void fun() //error { cout << endl; } };
被override修饰的虚函数,编译器会检查这个虚函数是否重写。如果没有重写,编译器会报错。
class A { public: virtual void fun() {} }; class B : public A { public: //这里我想重写fun,但写成了fun1,因为有override,编译器会报错。 virtual void fun1() override { cout << endl; } };
1.5 重载,覆盖(重写),重定义(隐藏)
这里我们来理一理这三个概念。
1.重载:重载函数处在同一作用域。
函数名相同,函数列表必须不同。
2.覆盖:必须是虚函数,且处在父类和子类中。
返回值,参数列表,函数名必须完全相同(协变除外)。
3.重定义:子类和父类的成员变量相同或者函数名相同,
子类隐藏父类的对应成员。
子类和父类的同名函数不是重定义就是重写。2、抽象类
2.1 抽象类的概念
再虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫做接口类。抽象类无法实例化出对象。抽象类的子类也无法实例化出对象,除非重写父类的虚函数。
class Car { public: virtual void fun() = 0; //不用实现,只写接口就行。 }
这并不意味着纯虚函数不能写实现,只是我们大部分情况下不写。
那么虚函数有什么用呢?
1,强制子类重写虚函数,完成多态。
2,表示某些抽象类。2.2 接口继承和实现继承
普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,不要把函数写成虚函数。
纯虚函数就体现了接口继承。下面我们来一道题,展现一下接口继承。class A { public: virtual void fun(int val = 0)//父类虚函数 { cout <<"A->val = "<< val << endl; } void Fun() { fun();//传过来一个子类指针调用fun() } }; class B: public A { public: virtual void fun(int val = 1)//子类虚函数 { cout << "B->val = " << val << endl; } }; B b; A* a = &b; a->Fun();
结果是什么呢?
B->val = 0
子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0.3、 多态的原理
3.1 虚函数表
多态是怎样实现的呢?
先来一道题目,class A { public: virtual void fun() {} protected: int _a; };
sizeof(A)是多少?是4吗?NO,NO,NO!
答案是8个字节。
我们定义一个A类型的对象a,打开调试窗口,发现a的内容如下
我们发现除了成员变量_a以外,还多了一个指针。这个指针是不准确的,实际上应该是_vftptr(virtual function table pointer),即虚函数表指针,简称虚表指针。在计算类大小的时候要加上这个指针的大小。那么虚表是什么呢?虚表就是存放虚函数的地址地方。每当我们去调用虚函数,编译器就会通过虚表指针去虚表里面查找。
下面我们用一个小栗子来说明虚函数的使用会用指针。class A { public: void fun1() {} virtual void fun2() {} }; A* ap = nullptr; ap->fun1(); //调用成功,因为这是普通函数的调用 ap->fun2(); //调用失败,虚函数需要对指针操作,无法操作空指针。
我们先来看看继承的虚函数表。
class A { public: virtual void fun1() {} virtual void fun2() {} }; class B : public A { public: virtual void fun1()//重写父类虚函数 {} virtual void fun3() {} }; A a; B b; //我们通过调试看看对象a和b的内存模型。
子类跟父类一样有一个虚表指针。
子类的虚函数表一部分继承自父类。如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。
本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。
所以,如果继承了虚函数,那么
1 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
2 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
3 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。
下面来一道面试题:
虚函数存在哪里?
虚函数表存在哪里?
虚函数是带有virtual的函数,虚函数表是存放虚函数地址的指针数组,虚函数表指针指向这个数组。对象中存的是虚函数指针,不是虚函数表。
虚函数和普通函数一样存在代码段。
那么虚函数表存在哪里呢?
我们创建两个A对象,发现他们的虚函数指针相同,这说明他们的虚函数表属于类,不属于对象。所以虚函数表应该存在共有区。
堆?堆需要动态开辟,动态销毁,不合适。
静态区?静态区存放全局变量和静态变量不合适。
所以综合考虑,把虚函数表也存放在了代码段。3.2多态的原理
我们现在来看看多态的原理。
class Person //成人 { public: virtual void fun() { cout << "全价票" << endl; //成人票全价 } }; class Student : public Person //学生 { public: virtual void fun() //子类完成对父类虚函数的重写 { cout << "半价票" << endl;//学生票半价 } }; void BuyTicket(Person* p) { p->fun(); }
这样就实现了不同对象去调用同一函数,展现出不同的形态。
满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。
普通函数的调用是编译时就确定的。3.3动态绑定与静态绑定
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
我们说的多态一般是指动态多态。
这里我附上一个有意思的问题:
就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?#include <iostream> using namespace std; class Base { public: virtual void func() { cout << "Base func\n"; } }; class Son : public Base { public: void func() { Base::func(); cout << "Son func\n"; } }; int main() { Son b; b.func(); return 0; }
输出:Base func
Son func
这是C++提供的一个回避虚函数的机制
通过加作用域(正如你所尝试的),使得函数在编译时就绑定。
(这题来自:虚函数)4 、继承中的虚函数表
4.1 单继承中的虚函数表
这里DV继承BV。
class BV { public: virtual void Fun1() { cout << "BV->Fun1()" << endl; } virtual void Fun2() { cout << "BV->Fun2()" << endl; } }; class DV : public BV { public: virtual void Fun1() { cout << "DV->Fun1()" << endl; } virtual void Fun3() { cout << "DV->Fun3()" << endl; } virtual void Fun4() { cout << "DV->Fun4()" << endl; } };
我们想个办法打印虚表,
typedef void(*V_PTR)(); //typedef一下函数指针,相当于把返回值为void型的 //函数指针定义成 V_PTR. void PrintPFTable(V_PTR* table)//打印虚函数表 { //因为虚表最后一个为nllptr,我们可以利用这个打印虚表。 for (size_t i = 0; table[i] != nullptr; ++i) { printf("table[%d] : %p->", i, table[i]); V_PTR f = table[i]; f(); cout << endl; } } BV b; DV d; // 取出b、d对象的前四个字节,就是虚表的指针, //前面我们说了虚函数表本质是一个存虚函数指针的指针数组, //这个数组最后面放了一个nullptr // 1.先取b的地址,强转成一个int*的指针 // 2.再解引用取值,就取到了b对象前4个字节的值,这个值就是指向虚表的指针 // 3.再强转成V_PTR*,这是我们打印虚表函数的类型。 // 4.虚表指针传给PrintPFTable函数,打印虚表 // 5,有时候编译器资源释放不完全,我们需要清理一下,不然会打印多余结果。 PrintPFTable((V_PTR*)(*(int*)&b)); PrintPFTable((V_PTR*)(*(int*)&d));
结果如下:
4.2 多继承中的虚函数表
我们先来看一看一道题目,
class A { public: virtual void fun1() { cout << "A->fun1()" << endl; } protected: int _a; }; class B { public: virtual void fun1() { cout << "B->fun1()" << endl; } protected: int _b; }; class C : public A, public B { public: virtual void fun1() { cout << "C->fun1()" << endl; } protected: int _c; }; C c; //sizeof(c) 是多少呢?
sizeof( c )的大小是多少呢?是16吗?一个虚表指针,三个lnt,考虑内存对齐后确实是16.但是结果是20.
我们来看看内存模型。在VS下,c竟然有两个虚指针
每个虚表里都有一个fun1函数。
所以C的内存模型应该是这样的,
而且如果C自己有多余的虚函数,会按照继承顺序补在第一张虚表后面。
下面还有一个问题,可以看到C::fun1在两张虚表上都覆盖了,但是它们的地址不一样,是不是说在代码段有两段相同的C::fun1呢?
不是的。实际上两个fun1是同一个fun1,里面放的是跳转指令而已。C++也会不犯这个小问题。
最后,我们来打印一下多继承的虚表。//Derive继承Base1和Base2 class Base1 { public: virtual void fun1() { cout << "Base1->fun1()" << endl; } virtual void fun2() { cout << "Base1->fun2()" << endl; } }; class Base2 { public: virtual void fun1() { cout << "Base2->fun1()" << endl; } virtual void fun2() { cout << "Base2->fun2()" << endl; } }; class Derive : public Base1, public Base2 { public: virtual void fun1() { cout << "Derive->fun1()" << endl; } virtual void fun3() { cout << "Derive->fun3()" << endl; } };
打印的细节,从Base2继承过来的虚表指针放在第一个虚表指针后面,我们想要拿到这个指针需要往后挪一个指针加上一个int的字节,但是指针的大小跟操作系统的位数有关,所以我们可以用加上Base2的大小个字节来偏移。
这里注意要先强转成char*,不然指针的加减会根据指针的类型来确定。Derive d; PrintPFTable((V_PTR*)(*(int*)&d)); PrintPFTable((V_PTR*)(*(int*)((char*)&d+sizeof(Base2))));
Ret:
(全文完)
- 协变:
-
C++多态理解与认识
2022-01-24 23:40:241.什么是多态? 多态是指函数调用的多种形态,使我们调用函数更加灵活。 多态分为静态多态与动态多态 1)静态多态:静态多态指的是编译时的多态,通过函数重载实现。根据函数命名规则找到函数地址,从而实现调用不同...1.什么是多态?
多态是指函数调用的多种形态,使我们调用函数更加灵活。
多态分为静态多态与动态多态
1)静态多态:静态多态指的是编译时的多态,通过函数重载实现。根据函数命名规则找到函数地址,从而实现调用不同的方法。2)动态多态(运行时):父类指针或引用调用重写虚函数
a.父类指针或引用指向父类,调用的就是父类的虚函数,指向哪个子类就去调用哪个子类里相应的虚函数。动态的多态构成的条件:
1)必须通过基类的指针或者引用调用虚函数
2)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。满足多态的条件时:
跟对象有关,父类的指针或引用指向哪个对象就调用哪个对象的虚函数
不满足多态的条件:
跟类型有关,调用的类型是谁,就调用谁。虚函数的定义:只有类的非静态成员函数才可以称作是虚函数。
虚函数的virtual与虚继承的virtual没有关系。
虚函数是为了实现多态而虚继承是为了解决菱形继承带来的数据冗余和二义性。虚函数的重写:
派生类中有一个跟基类完全相同的虚函数(即返回值类型,函数名,参数列表完全相同)则称子类的虚函数重写了基类的虚函数。虚函数的重写的例外:
1)协变(函数名和参数相同,返回值是指针或者引用,俩个返回值须有父子关系)基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类的指针或者引用。称为协变
class A{}; class B : public A {}; class Person { public: virtual A* f() {return new A;} }; class Student : public Person { public: virtual B* f() {return new B;} };
2)
析构函数的重写(基类与派生类析构函数的名字不同)但底层都是destructor如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
class Person { public: virtual ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。 int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }
关键字final
1)修饰虚函数,此虚函数不能被重写。
2)修饰一个类,则这个类就成为叶子类,不能被继承。class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() {cout << "Benz-舒适" << endl;} };
关键字override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则会出现编译错误。
重载,覆盖(重写),隐藏(重定义)的对比
抽象类
包含纯虚函数的类叫做抽象类
纯虚函数:在虚函数后面写上 =0,则这个函数为纯虚函数。抽象类的特性
1.不能直接实例化出对象,纯虚函数规范了派生类必须进行重写,(如果不重写,子类依然是抽象类,依然不能实例化出对象)更好地体现了接口继承。接口继承和实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用基类的函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。继承的是接口。
如果不实现多态,不要把函数定义成虚函数。多态的原理:
1.虚函数表// 这里常考一道笔试题:sizeof(Base)是多少?sizeof(Base) = 8 class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; };
原因是类中开头有一个虚表指针。
除了_b成员,还多一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,vs下是放在最前面),对象中的这个指针叫做虚函数表指针。class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
虚表是在编译阶段生成的,虚函数跟普通函数一样,都是被放在代码段(常量区)的 ,虚表里面存放的是虚函数的地址。
而虚表指针初始化是在初始化列表阶段生成的。多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
验证该结论:class Base1 { public: virtual void func1() {cout << "Base1::func1" << endl;} virtual void func2() {cout << "Base1::func2" << endl;} private: int b1; }; class Base2 { public: virtual void func1() {cout << "Base2::func1" << endl;} virtual void func2() {cout << "Base2::func2" << endl;} private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() {cout << "Derive::func1" << endl;} virtual void func3() {cout << "Derive::func3" << endl;} private: int d1; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) { cout << " 虚表地址>" << vTable << endl; for (int i = 0; vTable[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; } int main() { Derive d; VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); PrintVTable(vTableb1); VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1))); PrintVTable(vTableb2); return 0; }
反思一下达到多态的两个条件
1)虚函数覆盖->为了使不同的对象去完成同一行为时,展现出不同的形态。
2)必须是基类对象的指针或者引用调用虚函数->父类指针或者引用,切片时指向或者引用父类和子类对象中的切出来的那一部分;如果函数参数为父类对象,此时切片只会将成员变量拷贝过去,而不会拷贝虚函数表指针vfptr,因此拷贝过去的话不合理(父类对象不应该存在子类的vfptr)同类型的对象,虚表是一样的,所以他们共享同一张虚表。
-
C++多态的理解
2022-03-15 17:11:05c++的多态性具体体现在编译和运行两个阶段。编译时多态是静态多态,在编译时就可以确定使用的接口。运行时多态是动态多态,具体引用的接口在运行时才能确定。 静态多态和动态多态区别其实只是在什么时候将函数实现... -
C++多态的实现机制深入理解
2020-12-26 01:00:24在面试过程中C++的多态实现机制经常会被面试官问道。大家清楚多态到底该如何实现吗?下面小编抽空给大家介绍下多态的实现机制。 1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。 2. 存在虚... -
C++ 多态的简单例子
2022-01-17 16:28:34// 多态测试 参数是指针类型或引用类型 void drawShape(Shape* s) {// (Shape &s) s->Draw(); //s->Draw(); } int main() { // 同一个函数根据参数的不同类型体现出不同的结果 cout; Circle c; drawShape(&c);// ... -
C++多态案例
2021-10-30 20:50:27多态简介 1.静态多态:函数重载和运算符重载属于静态多态,复用函数名 2.动态多态:派生类和虚函数实现运行多态 两者区别: 1.静态多态的函数地址早绑定——编译阶段确定函数地址 2.动态多态函数地址晚绑定——运行... -
C++多态的好处和作用(用实例说话)
2021-01-03 21:26:06C++多态的好处和作用(用实例说话) 在面向对象的程序设计中,使用多态能够增强程序的可扩充性,即程序需要修改或增加功能时,只需改动或增加较少的代码。此外,使用多态也能起到精简代码的作用。本节通过两个实例来... -
C++ 多态的基本语法
2022-01-20 15:06:09C++ 多态的基本语法 我们先来看一下下面的代码 #include "iostream" using namespace std; class FATHER { public: void fun(void){ cout << "FATHER- printf" <<endl; } }; class SON : public ... -
18.1C++ 多态的应用
2021-11-23 18:55:17} void func2(Animal& p){不同的对象进来做不同的事,多态 p.eat(); } int main() { Pig pig; Dog dog; Mankind man; func2(pig); func2(dog); func2(man); return 0; } virtual 返回类型 函数名(参数列表):通常... -
C++多态的实例
2021-10-12 10:26:441.1 多态的概念 ...动态分为动态多态,静态多态,函数多态和宏多态,编程者的动态多态通常是指动态多态,是是基于继承机制和虚函数实现的。 2.1动态的实例 (1)用指针来来实现多态: #include<iostrea -
面试必考题c++多态的理解
2021-10-27 23:47:18多态和封装继承作为c++面向对象的三个基本特征.封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用! 2.实现... -
C++多态以及通过C语言实现多态
2021-12-16 17:25:04通过C语言实现C++多态 一.C++多态的原理 在C++中,多态机制是通过虚表以及虚表指针来实现的。 我们都知道,当要实现多态功能的时候,都会通过virtual关键字来讲基类中的函数变为虚函数,然后通过子类继承后重写就... -
C++多态概念和意义
2020-09-04 10:12:312、C++如何支持多态概念 3、多态内部运行剖析 4、多态的意义 5、修改示例代码 四、静态联编和动态联编 五、小结 一、什么叫重写 1、子类中可以重定义父类中已经存在的成员函数; 2、这种重定义发生在继承中... -
c++多态及其实现
2021-05-01 21:44:40c++是一门面向对象的语言,具有继承、封装和多态三大特性,本文我们总结一下c++中多态的特性及其实现方法。多态指的是调用同一接口,表现出不同的特性,分为静态多态和动态多态。静态多态指在编译期就能确定其特性的... -
C++多态与多态原理以及重载和重写(覆盖)的区别
2020-05-30 21:41:04C++多态与多态原理以及虚函数表多态多态原理重载和重写(覆盖)的区别 多态 多态分为两类:静态多态(编译时多态)和动态多态(运行时多态) 静态多态就是我们所熟悉的函数重载和运算符重载,而派生类和虚函数... -
C++ 多态
2017-08-08 23:39:21参考: C++ 多态C++ 多态定义当类之间存在层次结构,并且类之间是通过继承关联时,可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的,类的多态通过虚函数实现。虚函数虚函数 ... -
C语言实现C++多态
2021-11-15 22:12:42C语言实现C++多态 C++中多态实现是基于虚函数表实现的,每个具备多态性对象的内部都会有一个隐藏的虚函数表,虚函数表里面的函数指针指向具体的函数实现,可能是父类中的实现,或是子类重写了的方法。C语言没有天然... -
C++ 多态的几种非典型场景
2020-11-07 00:10:29多态的几种情形: 在非构造函数,非析构函数的成员函数中调用虚函数,是多态; class Base { public: void func1() { func2(); } virtual void func2() { cout << "base func2" << endl; } }; ...