C++三大特性之一——多态

多态:

多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。

 重载:

我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

#include<bits/stdc++.h>

using namespace std;

class A
{
    void fun() {};
    void fun(int i) {};
    void fun(int i, int j) {};
    void fun1(int i,int j){};
};

重写:

是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

示例:

#include<bits/stdc++.h>

using namespace std;

class A
{
public:
    virtual    void fun()
    {
        cout << "A";
    }
};
class B :public A
{
public:
    virtual void fun()
    {
        cout << "B";
    }
};
int main(void)
{
    A* a = new B();
    a->fun();//输出B,A类中的fun在B类中重写
}
重写原理:
例子中,A 为基类,其中的函数为虚函数。子类 B 继承并重写了基类的函数, 从结果分析子类体现了多态性,那么为什么会出现多态性,其底层的原理是什么?这里需要引出虚表和虚基表指针的概念。
  • 虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表
  • 虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针

上图中展示了虚表和虚表指针在基类对象和派生类对象中的模型,下面阐述实现多态的过程:
1 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
2 编译器会在每个对象的前四个字节中保存一个虚表指针,即 vptr ,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr ,从而让 vptr 指向正确的虚表,从而在调用虚函数时,能找到正确的函数
3 所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“ 看到了 父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
4 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性

多态以及虚函数常考面经:

构造函数能否声明为虚函数或者纯虚函数,析构函数呢?

析构函数:

  • 析构函数可以为虚函数,并且一般情况下基类析构函数要定义为虚函数。

  • 只有在基类析构函数定义为虚函数时,调用操作符delete销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据。

  • 析构函数可以是纯虚函数,含有纯虚函数的类是抽象类,此时不能被实例化。但派生类中可以根据自身需求重新改写基类中的纯虚函数。

构造函数:

  • 构造函数不能定义为虚函数。在构造函数中可以调用虚函数,不过此时调用的是正在构造的类中的虚函数,而不是子类的虚函数,因为此时子类尚未构造好。

  • 虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr,无法找到vtable,所以构造函数不能是虚函数。

为什么析构函数一般写成虚函数

由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。

#include <iostream>
using namespace std;
class Parent{
public:
 Parent(){
 cout << "Parent construct function" << endl;
 };
 ~Parent(){
 cout << "Parent destructor function" <<endl;
 }
};
class Son : public Parent{
public:
 Son(){
 cout << "Son construct function" << endl;
 };
 ~Son(){
 cout << "Son destructor function" <<endl;
 }
};
int main()
{
 Parent* p = new Son();
 delete p;
 p = NULL;
 return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Parent destructor function

将基类的析构函数声明为虚函数:


#include <iostream>
using namespace std;
class Parent{
public:
 Parent(){
 cout << "Parent construct function" << endl;
 };
 virtual ~Parent(){
 cout << "Parent destructor function" <<endl;
 }
};
class Son : public Parent{
public:
 Son(){
 cout << "Son construct function" << endl;
 };
 ~Son(){
 cout << "Son destructor function" <<endl;
 }
};
int main()
{
 Parent* p = new Son();
 delete p;
 p = NULL;
 return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Son destructor function
//Parent destructor function
基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

首先整理一下虚函数表的特征:

  • 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成

  • 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段

  • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中

根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示: 虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。 一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区 C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

构造函数、析构函数、虚函数可否声明为内联函数

首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。 register关键字:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率 举个例子:

#include <iostream>
using namespace std;
class A
{
public:
 inline A() {
 cout << "inline construct()" <<endl;
 }
 inline ~A() {
 cout << "inline destruct()" <<endl;
}
 inline virtual void virtualFun() {
 cout << "inline virtual function" <<endl;
 }
};
int main()
{
 A a;
 a.virtualFun();
 return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct()

构造函数和析构函数声明为内联函数是没有意义的《Effective C++》中所阐述的是:将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。其次,class中的函数默认是inline型的,编译器也只是有选择性的inline,将构造函数和析构函数声明为内联函数是没有什么意义的。将虚函数声明为inline,要分情况讨论有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定 的,而虚函数是运行期决定的,即在不知道将要调用哪个函数的情况下,如何将函数内联呢? 上述观点看似正确,其实不然,如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开综上,当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开;当是对象本身调 用虚函数时,会内联展开,当然前提依然是函数并不复杂的情况下。

构造函数为什么不能为虚函数?析构函数为什么要虚函数?

1、 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。 2、 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 3、构造函数不须要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。 4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。 5、当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向 它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

构造函数和析构函数可以调用虚函数吗,为什么
  1. 在C++中,提倡不在构造函数和析构函数中调用虚函数;

  2. 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;

  3. 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编;

  4. 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

静态函数能定义为虚函数吗?常函数呢?说说你的理解

1、static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。

2、静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针。

虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr。

这就是为何static函数不能为virtual,虚函数的调用关系:this -> vptr -> vtable ->virtual function。

虚函数的代价?
  1. 带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;

  2. 带有虚函数的类的每一个对象,都会有有一个指向虚表的指针,会增加对象的空间大小;

  3. 不能再是内联的函数,因为内联函数在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定到低是采用哪种函数,虚函数不能是内联函数。

哪些函数不能是虚函数?把你知道的都说一说
  1. 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

  2. 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

  3. 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。

  4. 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

  5. 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

什么是纯虚函数,与虚函数的区别

虚函数和纯虚函数区别

  • 虚函数是为了实现动态编联产生的,目的是通过基类类型的指针指向不同对象时,自动调用相应的、和基类同名的函数(使用同一种调用形式,既能调用派生类又能调用基类的同名函数)。虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。

  • 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化。

纯虚函数首先是虚函数,其次它没有函数体,取而代之的是用“=0”。

既然是虚函数,它的函数指针会被存在虚函数表中,由于纯虚函数并没有具体的函数体,因此它在虚函数表中的值就为0,而具有函数体的虚函数则是函数的具体地址。

一个类中如果有纯虚函数的话,称其为抽象类。抽象类不能用于实例化对象,否则会报错。抽象类一般用于定义一些公有的方法。子类继承抽象类也必须实现其中的纯虚函数才能实例化对象。

举个例子:

#include <iostream>
using namespace std;
class Base
{
public:
 virtual void fun1()
 {
 cout << "普通虚函数" << endl;
 }
 virtual void fun2() = 0;
 virtual ~Base() {}
};
class Son : public Base
{
public:
 virtual void fun2() 
 {
 cout << "子类实现的纯虚函数" << endl;
 }
};
int main()
{
 Base* b = new Son;
 b->fun1(); //普通虚函数
 b->fun2(); //子类实现的纯虚函数
 return 0;
}
虚函数表里存放的内容是什么时候写进去的?

虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入

除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。

只包含虚函数的类占几字节?

虚函数占8个字节(64位系统中,若是32位系统占4字节),因为有指向虚函数的指针;

扩展:

一个空类所占字节为1: class Stu2{};

2、类所占空间只包含变量,不包括普通函数;

3、虚函数占8个字节(64位系统中),因为有指向虚函数的指针;

4、类所占字节应满足内存对齐原则;

5、静态变量或者静态函数不占类内存空间;

原则:C++中,每个类对象的所占用的存储空间,只是该数据部分所占的存储空间,而不包括函数所占的存储空间。同一个类的多个对象共享函数代码。
 

class Stu {
public:
	//注意:内存对齐原则
	int a;  //4 Bytes
	int b; //4 Bytes
	double c; //8 Bytes
 
	static int d;//不占类内存空间
	static void sFun();//不占类内存空间
 
	
	void func(int x);//普通函数不占内存空间
 
 
	virtual void Test() = 0;//虚函数占8个字节(64位系统中),因为虚函数的指针
};

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值