C++多态

C++多态

多态的概念

多态的概念: 通俗来说,就是多种形态,具体点就是当不同的对象去完成同一件事时会产生出不同的状态。

举一个生活中的例子,比如说:

  • 对于生活中买票的这个行为而言,当普通人买票时,是全价票;学生买票时,是半价买票;而军人买票时则是优先买票。不同身份的人去买票,所产生的行为是不同的,这就是所谓的多态。
多态的定义及实现
多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如说下面Student继承了Person类,Person对象买票全价,Student对象买票半价。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票半价" << endl;
	}
};

void Func(Person& people)
{
	people.BuyTicket();
}

int main()
{
	Person Jack;
	Func(Jack);

	Student Mike;
	Func(Mike);

	return 0;
}

运行结果:

在这里插入图片描述

需要注意的是如果想在继承中构成多态还要有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
虚函数

虚函数:被virtual修饰的类成员函数就被成为虚函数。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票--全价" << endl;
	}
};

注意:

  • 只有类的非静态成员函数前面才可以加virtual,静态成员函数不能再前面加virtual,因为它里面没有this指针,具体细节等我们讲了多态的原理就清楚了。
  • 虽然虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是他们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承那里的virtual是为了解决菱形继承的数据冗余和二义性问题的。
虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同——简称三同)这时我们称子类的虚函数重写了基类的虚函数。

比如说,下面我们的Student类就重写了Person类的虚函数

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票--全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票--半价" << endl;
	}
};

//父类对象的引用
void Func(Person& people)
{
	people.BuyTicket();
}

//父类对象的指针
void Fun(Person* people)
{
	people->BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Func(ps);//全价
	Func(st);//半价
	cout << endl;

	Fun(&ps);//全价
	Fun(&st);//半价

	return 0;
}

运行结果:

在这里插入图片描述

可以看到我们这里的子类是完成了对父类虚函数的重写,并且是构成多态的,不同的对象调用相同的函数发生了不同的行为。

我们这里还需要特别强调一下:只要不满足多态构成条件的任意一条,就都不会像上面那样传子类对象调用子类的函数,传父类对象调用父类的函数。

  1. 不是父类的指针或者引用去调用虚函数

    class Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "普通人买票--全价" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "学生买票--半价" << endl;
    	}
    };
    
    
    void Func(Person people)
    {
    	people.BuyTicket();
    }
    
    
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);//全价
    
    	cout << endl;
    	Func(st);//全价
    	return 0;
    }
    

    运行结果:

在这里插入图片描述

  1. 不构成重写

    class Person
    {
    public:
    	virtual void BuyTicket(char )
    	{
    		cout << "普通人买票--全价" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	virtual void BuyTicket(int )
    	{
    		cout << "学生买票--半价" << endl;
    	}
    };
    
    
    void Func(Person& people)
    {
    	people.BuyTicket(10);
    }
    
    
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);//全价
    
    	cout << endl;
    	Func(st);//全价
    	return 0;
    }
    

    运行结果:

在这里插入图片描述

对于上面的这些情况我总结了两句话来帮助大家更好的理解:

  • 构成多态,跟person的类型没有关系,我们传参的时候传的是哪个类型的对象,就调用这个类型对象的虚函数——跟对象有关
  • 不构成多态,调用的就是person类型的函数——跟类型有关

注意: 在重写基类虚函数的时候,派生类的虚函数不加virtual关键字,也可以构成重写。这是因为继承后基类的虚函数被继承下来了,在派生类中依然保持虚函数属性。但是这种写法不是很规范,不建议大家这样使用,因此我们在派生类重写基类虚函数的时候在前面加上virtual关键字。

虚函数的重写的两个例外
  1. 协变(基类与派生类函数返回值类型不同)

    派生类重写虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

    class A{};
    class B :public A{};
    
    class Person
    {
    public:
    	virtual A* BuyTicket()
    	{
    		cout << "普通人买票--全价" << endl;
    		return new A;
    	}
    };
    
    class Student : public Person
    {
    public:
    	virtual B* BuyTicket()
    	{
    		cout << "学生买票--半价" << endl;
    		return new B;
    	}
    };
    
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    
    
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);//全价
    
    	cout << endl;
    
    	Func(st);//半价
    	return 0;
    }
    

    运行结果:

在这里插入图片描述

  1. 析构函数的重写(基类与派生类析构函数的名字不同)

    我们之前在继承的时候讲过,因为多态的一些原因,任何类的析构函数名都会被统一处理成destructor(),编译器认为子类的析构函数和父类的析构函数构成隐藏。因此我们如果想调用父类的析构函数,我们需要指定作用域。但是呢, 编译器为了保证析构时,保持先子再父的后进先出的顺序析构,子类析构函数完成后,会自动去调用父类的析构函数。

    因此如果我们基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都会与基类的析构函数构成重写,虽然基类与派生类析构函数的名字看起来不同。

    比如说,下面的父类Person和子类Student的析构函数构成重写

    class Person
    {
    public:
    	virtual ~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student :public Person
    {
    public:
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    };
    

    像我下面平时这种写法好像根本就不需要多态

    class Person
    {
    public:
    	virtual ~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student :public Person
    {
    public:
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    };
    
    
    int main()
    {
    	// 普通对象,析构函数是否虚函数,是否完成重写,都正确调用了
    	Person p;
    	Student s;
    
    	return 0;
    }
    

    运行结果:

在这里插入图片描述

这个时候大家可能会有一个疑问: 究竟在什么场景下,我们才需要完成对析构函数的重写构成多态呢?

在这种场景下: 动态申请的父子对象,如果都交给了父子指针去管理,那么就需要我们的析构函数是虚函数完成重写,构成多态,那样才能正确调用析构函数。

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student :public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};


int main()
{

	//动态申请的父子对象,如果都交给了父类指针管理
	//那么需要析构函数是虚函数,完成重写构成多态
	//这样才能够正确调用析构函数
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}

运行结果:

在这里插入图片描述

C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名的字母次序写反而无法构成重写,而这种错误在编译期间是不会报出来的,只有在程序运行时没有得到预期结果才来调试会得不偿失,因此,C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  • final: 修饰虚函数,表示该虚函数不能再被重写

    比如说下面父类中的Drive虚函数被final修饰之后则表示该虚函数不能再被重写了,子类若是执意要去重写父类的Drive虚函数编译器就会报错

    class Car
    {
    public:
    	virtual void Drive() final
    	{}
    };
    
    class Benz :public Car
    {
    public:
    	virtual void Drive()
    	{
    		cout << "Benz-舒适" << endl;
    	}
    };
    

    编译器报错:

在这里插入图片描述

  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错

    比如说子类Benz中的Drive虚函数被override修饰,编译器时就会检查子类是否对这个虚函数完成了重写,如果没有重写编译器就会报错。

    完成了重写:

    class Car
    {
    public:
    	virtual void Drive()
    	{}
    };
    
    class Benz :public Car
    {
    public:
    	//override 检查派生类虚函数是否重写了基类中的某个虚函数,如果没有重写编译报错
    	virtual void Drive()override //完成了重写,编译器不会报错
    	{
    		cout << "Benz-舒适" << endl;
    	}
    };
    

    未完成重写:

    class Car
    {
    public:
    	virtual void Drive()
    	{}
    };
    
    class Benz :public Car
    {
    public:
    	//override 检查派生类虚函数是否重写了基类中的某个虚函数,如果没有重写编译报错
    	virtual void Drive(int )override //没有完成重写,编译器报错
    	{
    		cout << "Benz-舒适" << endl;
    	}
    };
    

    编译器报错:

在这里插入图片描述

重载、覆盖(重写)、隐藏(重定义)的对比

下面我用一幅图来解释重载、覆盖(重写)、隐藏(重定义)这三个概念的对比

在这里插入图片描述

抽象类
概念

在虚函数的后面写上=0,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能够实例化出对象

class Car
{
public:
	//纯虚函数一般只声明,不实现
	//可以实现,但是我们一般不实现,因为实现了也没价值
	virtual void Drive() = 0
	{
		cout << "virtual void Drive() = 0" << endl;
	}
};

int main()
{
	Car c;//不能实例化出对象

	return 0;
}

编译器报错:

在这里插入图片描述

派生类继承了抽象类之后也不能实例化出对象,只有重写了纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。

class Car
{
public:
	//纯虚函数一般只声明,不实现
	//可以实现,但是我们一般不实现,因为实现了也没意义
	//virtual void Drive() = 0
	//{
	//	cout << "virtual void Drive() = 0" << endl;
	//}

	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	//重写了纯虚函数可以实例化出对象
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car
{
public:
	//重写了纯虚函数可以实例化出对象
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

int main()
{
	//Car c;//不能实例化出对象

	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();

	return 0;
}

运行结果:

在这里插入图片描述

可能大家现在有一个问题:抽象类既然不能够实例化出对象,那我们什么样的类才需要被定义成抽象类呢?

  • 抽象——在现实世界中没有对应的实物
  • 一个类型,如果一般在现实世界中,没有具体的对应实物就定义成抽象类比较好

注意:

  • 纯虚函数的类,本质上强制子类去完成虚函数重写
  • override只是在语法上检查是否完成重写。
接口继承和实现继承
  • 实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。因此如果不打算实现多态,就不要把函数定义成虚函数。
多态的原理
虚函数表

我们下面先来做一道题:你认为下面这个类它多大呢?

class Base
{
public:
	virtual void Func()
	{
		cout << " Func()" << endl;
	}

private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

运行结果:

在这里插入图片描述

大家看到这个结果可能会有个疑问:欸,我这里只有一个成员变量,按理来说运行结果应该是4啊,为啥运行结果却是8?

我们通过调试来看一下:
在这里插入图片描述

我们通过监视窗口发现,b这个对象里面除了有一个成员变量_b之外,还多了一个指针 _vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)因此经过内存对齐之后,我们的b对象大小是8而不是4.

这个时候可能又有人问了:那这个指针是个什么东西?为什么会多出来一个指针呢?

对象中的这个指针我们叫做虚函数表指针,简称虚表指针,一个含有虚函数的类至少都有一个虚函数表指针,虚函数表指针指向一个函数表,这个表简称为虚表

此时我还有一个问题:那这个虚函数表里面都放了些什么呢?

下面我们来看一段代码

class Base
{
public:
	virtual void Func1()
	{
		cout << " Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << " Func2()" << endl;
	}

private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

通过监视窗口我们发现虚函数表存的是我们虚函数的地址

在这里插入图片描述

针对上面的代码我们做出以下的改造之后再来看一下会是个什么样子

  1. 我们增加一个派生类去Drive去继承Base
  2. Drive中重写Func1
  3. Base类中增加一个普通函数Func3
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 Drive :public Base
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
private:
	int _d;
};

int main()
{
	Base b;

	Drive dd;
	return 0;
}

通过调试我们可以发现父类对象b和派生类对象dd,他们除了自己的成员变量之后,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表。

在这里插入图片描述

并且可以看到父类和派生类虚函数表中虚函数的地址有些是不一样的,其实派生类会先将基类的虚表内容拷贝一份到自己的虚表中,如果派生类对基类的某个虚函数完成了重写,那么派生类会用自己刚刚重写的虚函数的地址覆盖掉虚表中基类虚函数的地址。

通过观察和测试,我总结了以下几点:

  • 派生类对象dd中也有一个虚表指针,dd对象由两部分构成,一部分是从父类继承下来的成员,一部分是自己对象中的成员。
  • 基类b对象和派生类dd对象的虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1的地址,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  • 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是因为Func3不是虚函数,所以不会放进虚表。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

下面我们来总结一下派生类虚表的生成

  1. 派生类会先将基类中的虚表内容拷贝一份到自己的虚表中
  2. 如果派生类对基类的某个虚函数完成了重写,那么派生类会用自己刚刚重写的虚函数的地址覆盖掉虚表中基类虚函数的地址。
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

知道了虚表是如何生成之后大家可能还有以下两个问题: 虚函数表指针是在什么时候初始化的?虚函数是存在哪的?虚表又是存在哪的?

虚函数表指针是在构造函数初始化列表阶段初始化的很多同学可能会以为虚函数是存在虚表里面,然后虚表存在对象里面,其实这种想法是错误的。需要注意的是虚表中存的是虚函数的地址,不是虚函数,另外对象中存的不是虚表,而是虚表指针。虚函数和我们的普通函数一样,都是存在代码段的.

至于虚表是存在哪里的,我们可以通过下面的代码进行判断

int main()
{
	int* p = (int*)malloc(4);
	printf("堆:%p\n", p);

	int a = 0;
	printf("栈:%p\n", &a);

	static int b = 0;
	printf("数据段:%p\n", &b);

	const char* str = "aaaaaa";
	printf("代码段:%p\n", str);


	Base bb;
	//取对象的前四个字节
	printf("虚函数表:%p\n", *((int*)&bb));


	return 0;
}

运行结果:

在这里插入图片描述

我们通过这段代码打印了对象bb中虚函数表指针即虚表的地址,可以看到虚表的地址与代码段是非常接近的,由此我们可以认为虚表它实际上是存在代码段的。

多态的原理

了解了虚函数表之后,下面我们再来说一下多态的原理吧

下面我们先来看一段代码(基类的引用调用虚函数)

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--全价" << endl;
	}
	int _p = 1;
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--半价" << endl;
	}
	int _s = 2;
};



int main()
{
	Person Mike;
	Student Jack;

	Person& p1 = Mike;
	Person& p2 = Jack;

	p1.BuyTicket();//买票--全价
	p2.BuyTicket();//买票--半价


	return 0;
}

运行结果:

在这里插入图片描述

可以看到我们这里如果Person对象是父类的对象的别名,调用BuyTicket函数时就调用父类中的BuyTicket,如果Person对象是子类对象的别名就调用的是子类中的BuyTicket。那为什么会这样呢?

通过调试可以发现,对象Mike中包含一个成员变量_p和一个虚表指针,对象Jack中包含两个成员变量 _p和 _s以及一个虚表指针,这两个对象的虚表指针分别指向自己的虚表。

在这里插入图片描述

p1是Mike对象的引用,因此它可以拿到Mike对象中的虚表指针从而通过虚表找到对应的虚函数进行调用。还记得我们之前说的派生类虚表的生成嘛——派生类会先将基类中的虚表内容拷贝一份到自己的虚表中,如果派生类对基类的某个虚函数完成了重写,那么派生类会用自己刚刚重写的虚函数的地址覆盖掉虚表中基类虚函数的地址。因为这里子类对象对于父类对象的虚函数完成了重写,所以在子类对象中虚表填的虚函数地址就是自己重写的虚函数的地址。而p2是Jack对象的引用,子类对象赋值给父类对象的引用这里会发生切片,所以他是子类对象中父类的那部分的别名,因此它可以拿到子类的虚表指针从而通过虚表找到对应的虚函数进行调用。

下面我们再来看一段代码(基类的指针调用虚函数)

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--全价" << endl;
	}
	int _p = 1;
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--半价" << endl;
	}
	int _s = 2;
};



int main()
{
	Person Mike;
	Student Jack;

	Person* p1 = &Mike;
	Person* p2 = &Jack;

	p1->BuyTicket();//买票--全价
	p2->BuyTicket();//买票--半价


	return 0;
}

运行结果:

在这里插入图片描述

下面我们通过调试来看一下

在这里插入图片描述

p1指针指向Mike对象,因此它可以拿到Mike对象中的虚表指针从而通过虚表找到对应的虚函数进行调用。而子类对象赋值给父类对象的引用或者父类指针指向子类对象时会发生切片。因此此时p2指针会指向子类对象中父类的那一部分,因此它可以拿到子类的虚表指针从而通过虚表找到对应的虚函数进行调用。

最后我们再来看一段代码(基类对象调用虚函数)

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--全价" << endl;
	}
	int _p = 1;
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票--半价" << endl;
	}
	int _s = 2;
};



int main()
{
	Person Mike;
	Student Jack;

	Person p1 = Mike;
	Person p2 = Jack;

	p1.BuyTicket();//买票--全价
	p2.BuyTicket();//买票--全价


	return 0;
}

运行结果:

在这里插入图片描述

我们继续通过调试来看一下

在这里插入图片描述

我们可以看到此时p2对象中的虚表地址和Jack中的虚表地址是不一样的,但是p2对象中的虚表地址和Mike对象的虚表地址是一样的,这是为什么呢?

这是因为父子类对象切片时,会调用父类的拷贝构造对子类中父类的那部分成员变量进行拷贝构造,其中虚表指针不参与父子类的切片,切片的是普通成员。如果父子类对象切片时,虚表指针也参与切片则会导致父类对象中的虚函数关系混乱。因为同类型的对象共享一张虚表,所以他们的虚表指针指向的虚表是一样的。

经过上面的各种分析之后,现在我们就可以明白多态的原理了:

  • 当父类的指针指向Mike对象或者父类的引用是Mike对象的别名时,调用BuyTicket函数就会去Mike对象中的虚表中去找对应的虚函数,调用的就是Person::BuyTicket
  • 当父类的指针指向Jack对象或者父类的引用是Jack对象中父类那部分的别名时,调用BuyTicket函数就会去Jack对象中的虚表中去找对应的虚函数,调用的就是Student::BuyTicket

现在我们再来回过头来看多态构成的两个条件:一是必须使用基类的指针或者引用去调用虚函数,二是被调用的必须是虚函数,且派生类必须完成对父类虚函数的重写。派生类必须完成对父类虚函数的重写是为了完成派生类虚表中虚函数地址的覆盖,而必须使用基类的指针或者引用去调用虚函数,因为只有这样才可以拿到子类对象的虚表指针从而实现多态。

总结:

  1. 构成多态,父类的指针指向谁或者父类引用是谁的别名就调用谁的虚函数,跟对象有关
  2. 不构成多态,对象类型是上面就调用谁的虚函数,跟类型有关。
动态绑定和静态绑定
  • 静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定: 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
单继承和多继承关系中的虚函数表
单继承中的虚函数表

我们通过下面的单继承关系,来研究一下基类和派生类的虚表模型

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}

private:
	int _b = 1;
};

class Drive :public Base
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Drive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Drive::Func4()" << endl;
	}
private:
	int _d;
};

我们通过调试打开监视窗口来观察一下它的类对象模型

在这里插入图片描述

我们可以看到通过监视窗口我们在派生类中只能看到func1和func2,func3和func4我们看不见。这是为什么呢?这是编译器的监视窗口故意隐藏了这两个函数,也可以认为这是他的一个小bug,那么我们看到派生类中完整的虚表呢?下面来给大家介绍两种方法。

一、使用内存窗口查看

我们知道内存是不会骗人的,因此我们可以通过使用内存窗口,然后输入派生类对象中的虚表指针之后,我们就可以看到派生类需表中存储的四个虚函数地址。

在这里插入图片描述

二、使用代码打印虚表内容

我们可以通过下面的这段代码,来打印派生类和基类中完整的虚表内容,在打印过程中我们可以通过函数指针去调用对应的虚函数,从而打印出虚函数的函数名,这样我们就可以确定虚表中存储的是哪一个函数的地址。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}

private:
	int _b = 1;
};

class Drive :public Base
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Drive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Drive::Func4()" << endl;
	}
private:
	int _d;
};


//对函数指针进行typedef
typedef void(*VF_PTR)();

void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		//通过函数指针调用这个虚函数
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	PrintVFTable((VF_PTR*)(*(int*)&b));

	Drive dd;
	PrintVFTable((VF_PTR*)(*(int*)&dd));
	return 0;
}

运行结果:

在这里插入图片描述

现在我们就可以知道派生类中新增的虚函数会按照虚函数的声明次序放在虚表的最后。

多继承中的虚函数表

我们通过下面的多继承关系,来研究一下基类和派生类的虚表模型

class Base1
{
public:
	virtual void Func1()
	{
		cout << "Base1::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2::Func2()" << endl;
	}

private:
	int _b1;
};

class Base2
{
public:
	virtual void Func1()
	{
		cout << "Base1::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2::Func2()" << endl;
	}

private:
	int _b2;
};

class Drive :public Base1,public Base2
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Drive::Func3()" << endl;
	}
private:
	int _d;
};

我们继续通过调试打开监视窗口来观察一下它的类对象模型

在这里插入图片描述

可以看到我们这里还是会出现上面的问题没有看到func3,并且可以看到派生类对象中有两张虚表。我们还发现了一个奇怪的现象,我们派生类对象对基类中的func1虚函数完成了重写,按理来说两张虚表func1函数的地址应该是一样的,但是在两张虚表中func1函数的地址却是不一样的,这是为什么呢?

这是因为这里虚表中func1的地址其实不是func1函数的真实地址。这两个地址都分别指向一个jump指令,两个地址中的jump指令最终都会跳转到同一个func1虚函数中。

下面我们继续通过上面的那两种方式来看一下派生类对象完整的虚表内容

一、使用内存窗口查看

在这里插入图片描述

二、使用代码打印虚表内容

需要注意的是:我们派生类第二张虚表的地址并不是就在第一张虚表地址的后面,而是还要通过在在第一张虚表地址的基础上向后偏移sizeof(Base1)个字节之后才能拿到第二种虚表的地址。

int main()
{
	Base1 b1;
	Base2 b2;

	Drive dd;

	cout << "Base1的虚表: " << endl;
	PrintVFTable((VF_PTR*)(*(int*)&b1));

	cout << "Base2的虚表: " << endl;
	PrintVFTable((VF_PTR*)(*(int*)&b2));

	cout << "派生类的第一张虚表" << endl;
	PrintVFTable((VF_PTR*)(*(int*)&dd));

	cout << "派生类的第二张虚表" << endl;
	PrintVFTable((VF_PTR*)(*(int*)((char*)&dd+sizeof(Base1))));

	return 0;
}

运行结果:

在这里插入图片描述

由此我们就可以看到,在多继承中派生类中未重写的虚函数会放在第一个继承基类部分的虚函数表中。

继承和多态常见的面试问题
概念考察
  1. 下面哪种面向对象的方法可以让你变得富有( )

A: 继承 B: 封装 C: 多态 D: 抽象

  1. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。

    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

  2. 面向对象设计中的继承和组合,下面说法错误的是?()

A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用

B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用

C:优先使用继承,而不是组合,是面向对象设计的第二原则

D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

  1. 以下关于纯虚函数的说法,正确的是( )

    A:声明纯虚函数的类不能实例化对象

    B:声明纯虚函数的类是虚基类

    C:子类必须实现基类的纯虚函数

    D:纯虚函数必须是空函数

  2. 关于虚函数的描述正确的是( )

    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型

    B:内联函数可以是虚函数

    C:派生类必须重新定义基类的虚函数

    D:虚函数可以是一个static型的函数

  3. 关于虚表说法正确的是( )

    A:一个类只能有一张虚表

    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

    C:虚表是在运行期间动态生成的

    D:一个类的不同对象共享该类的虚表

  4. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )

A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址

B:A类对象和B类对象前4个字节存储的都是虚基表的地址

C:A类对象和B类对象前4个字节存储的虚表地址相同

D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

  1. 下面程序输出结果是什么? ()

    #include <iostream>
    using namespace std;
    class A
    {
    public:
    	A(char* s) { cout << s << endl; }
    	~A() {};
    };
    class B : virtual public A
    {
    public:
    	B(char* s1, char* s2)
    		:A(s1)
    	{
    		cout << s2 << endl;
    	}
    };
    class C : virtual public A
    {
    public:
    	C(char* s1, char* s2)
    		:A(s1)
    	{
    		cout << s2 << endl;
    	}
    };
    class D : public B, public C
    {
    public:
    	D(char* s1, char* s2, char* s3, char* s4)
    		:B(s1, s2)
    		, C(s1, s3)
    		, A(s1)
    	{
    		cout << s4 << endl;
    	}
    };
    int main()
    {
    	D* p = new D("class A", "class B", "class C", "class D");
    	delete p;
    	return 0;
    }
    
    

    A:class A class B class C class D

    B:class D class B class C class A

    C:class D class C class B class A

    D:class A class C class B class D

  2. 多继承中指针偏移问题?下面说法正确的是( )

class Base1
{
public:
	int _b1;
};
class Base2
{
public:
	int _b2;
};
class Derive : public Base1, public Base2
{
public:
	int _d;
};
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

A:p1 == p2 == p3

B:p1 < p2 < p3

C:p1 == p3 != p2

D:p1 != p2 != p3

  1. 以下程序输出结果是什么()

    #include <iostream>
    using namespace std;
    class A
    {
    public:
    	virtual void func(int val = 1)
    	{
    		cout << "A->" << val << endl;
    	}
    	virtual void test()
    	{
    		func();
    	}
    };
    class B : public A
    {
    public:
    	void func(int val = 0)
    	{
    		cout << "B->" << val << endl;
    	}
    };
    int main()
    {
    	B* p = new B;
    	p->test();
    	return 0;
    }
    
    

    A: A->0 B: B->1 C: A->1 D: B->0

    E: 编译出错 F: 以上都不正确

参考答案:

1. A 2. D 3. C 4. A 5. B
6. D 7. D 8. A 9. C 10. B
问答题
  1. 内联函数可以是虚函数吗?

内联函数可以是虚函数,我们知道内联函数会在调用的地方被展开,也就是说内联函数没有地址,但是内联函数只是编译器的一种建议。调用时,如果不构成多态,这个函数会保持内联属性。但是如果构成多态,当我们把内联函数定义成虚函数后,编译器会忽略函数的内联属性,这个函数就不再是内联函数了,因为调用是到对象的虚函数表中找到虚函数的地址,实现调用。

  1. 静态成员函数可以是虚函数吗?

    不可以,因为静态成员函数没有this指针,使用类型::成员函数无法访问虚函数表,所以静态成员函数无法放进虚函数表中

  2. 构造函数可以是虚函数吗?

构造函数不能是虚函数,并且构造函数成为虚函数没有价值。虚函数的意义是构成多态调用,那么多态调用要去虚函数表中查找虚函数,对象中的虚函数表指针是在构造函数初始化列表初始化的,因此这就变成了一个先有鸡还是先有蛋的问题。

  1. 析构函数可以是虚函数嘛?什么场景下析构函数是析构函数

    可以,并且最好把基类的析构函数定义成虚函数。具体情况在前面已经说过了。

  2. 对象访问普通函数快还是虚函数更快?

    首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中进行查找,然后才能进行调用。

  3. 虚函数表是在什么阶段生成的?存在哪的?

    虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)

  4. 什么是抽象类?抽象类的作用?

    在前面已经介绍过了,忘记的小伙伴可以去看上面写的内容这里就不再重复介绍了。

以上就是本篇文章的所有内容了,如果觉得文章对你有帮助的话,可以给作者三连支持一波。

  • 12
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值