【C++】虚函数相关常见问题

【C++】虚函数相关常见问题

1.说说为什么要虚析构?

虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。

  1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构

  2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构 的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑 定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构 的时候就要根据指针绑定的对象来调用对应的析构函数了。

2. C++默认的析构函数为什么不是虚函数?

是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。 而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

3. 构造函数能不能是虚函数

不能虚构造

  1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)

  2. 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

  3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

4. 说说什么是虚继承,解决什么问题,如何实现?

虚继承:虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。

解决存在的两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将派生类对象的地址 赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的 地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面提到的两个问题。

#include <iostream>
using namespace std;


class A {
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

class C :virtual public A
{
public:
	int _c;
};

class D :public B, public C
{
public:
	int _d;
};

//菱形继承和菱形虚继承的对象模型


int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	cout << sizeof(D) << endl;
	
	return 0;
}

分别从菱形继承和虚继承来分析:

在这里插入图片描述

菱形继承中A在B,C,D,中各有一份,虚继承中,A共享。 上面的虚继承表实际上是一个指针数组。B、C实际上是虚基表指针,指向虚基表。 虚基表:存放相对偏移量,用来找虚基类

5. 说说什么是虚函数

虚函数的作用:C++中的虚函数的作用主要是实现了多态的机制。

多态:关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种 形态”,这是一种泛型技术。

如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。

虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

class Person {
public:
	//虚函数
	virtual void GetName() {
		cout << "PersonName:xiaosi" << endl;
	};
};

class Student :public Person {
public:
	void GetName() {
		cout << "StudentName:xiaosi" << endl;
	};
};

int main() 
{
	//指针
	Person* person = new Student();
	//基类调用子类的函数
	person->GetName();//StudentName:xiaosi
	return 0;
}

6.说说虚函数的实现原理

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

7.说说纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”, virtual void GetName() = 0

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类, 但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0) 否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。

定义纯虚函数的目的:使派生类仅仅只是继承函数的接口。

纯虚函数的意义:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

// 抽象类
class Person {
public:
	//纯虚函数

	virtual void GetName() = 0;
};

class Student :public Person {
public:
	Student() {
	};
	void GetName() 
	{
		cout << "StudentName:xiaosi" << endl;
	};
};

int main() 
{
	Student student;
	return 0;
}

8.说说纯虚函数能实例化吗?为什么?派生类要实现吗?为什么?

纯虚函数不可以实例化,但是可以用其派生类实例化

class Base
{
public:
	virtual void func() = 0;
};

class Derived :public Base
{
public:
	void func() override
	{
		cout << "Derived::func()" << endl;
	}
};

int main()
{
	Base* b = new Derived();
	b->func();
	return 0;
}

纯虚函数为什么不能实例化:

虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。 即纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”

所以纯虚函数不能实例化。

派生类要实现方法吗?

纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。

为什么?

定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

9.说说C++中虚函数与纯虚函数的区别

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。

  2. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

  3. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  5. 虚函数的定义形式: virtual{} ;纯虚函数的定义形式: virtual { } = 0 ;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

10.请问构造函数中能不能调用虚方法

不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。 派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。 所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。

11.如何理解抽象类?

  1. 抽象类的定义如下: 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现 方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。

  2. 抽象类有如下几个特点:

    1)抽象类只能用作其他类的基类,不能建立抽象类对象。

    2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。

    3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

12.除了虚函数,还有什么方式能实现多态?

多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,代码如下:

class A
{
public:
	void do(int a);
	void do(int a, int b);
};

13.说说什么是虚基类,可否被实例化?

在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:

class A
class B1 :public virtual A;
class B2 :public virtual A;
class D :public B1, public B2;

虚继承的类可以被实例化,举例如下:

class Animal {/* ... */ };

class Tiger : virtual public Animal { 
public:
	void getWeight()
	{
		cout << "getWeight()" << endl;
	}
};

class Lion : virtual public Animal { /* ... */ };

int main()
{
	Tiger lg;
	/*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK*/
	lg.getWeight();
	return 0;
}

14. C++中那些函数不能被声明为虚函数?

常见的不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

  1. 为什么C++不支持普通函数为虚函数?

    普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此 编译器会在编译时绑定函数。

  2. 为什么C++不支持构造函数为虚函数?

    这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象 成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。 另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函 数来完成你想完成的动作。(这不就是典型的悖论) 构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚 未形成,所以不能将构造函数定义为虚函数

  3. 为什么C++不支持内联成员函数为虚函数?

    其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在 继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展 开,虚函数在运行时才能动态的绑定函数) 内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数 为虚函数

  4. 为什么C++不支持静态成员函数为虚函数?

    这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没 有要动态绑定的必要性。 静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

  5. 为什么C++不支持友元函数为虚函数?

    因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

15. 虚函数表里存放的内容是什么时候写进去的?

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

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

猜你喜欢

转载自blog.csdn.net/qq_58325487/article/details/131009474