【C++之进阶提升】深剖面向对象三大特性之多态

前言

面向对象三大特性是:封装,继承和多态。前面我们已经学习了封装和继承,今天我们重点就是来学习最后一个特性:多态。在现实生活中通常会有这样场景:不同的身份去请求同一个服务,结果是不一样的。为什么会出现这样的结果?这就是我们今天要解决的问题:多态。

一、多态的概念

多态的概念:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。简单的理解就是我们在前言中介绍的那样,不同的对象去请求同一个服务,或者做同一件事情,得到的结果是不一样的,比如:在商场里买东西,普通的顾客和VIP顾客去买同一件东西,享用的折扣显然是不一样,我们在买票的时候,不同发身份去购买票的时候,得到的服务也是不一样的,比如:普通的顾客去买的时候就是正常买票全家售卖,学生买票的时候就是半价售卖,军人买票的时候是优先买票,全家售卖。

二、多态的定义及实现

  1. 虚函数
    虚函数就是被关键字vrtual修饰的函数,比如:
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
  1. 虚函数的重写
    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。子类在继承的过程中也会继承父类的虚函数,继承父类的虚函数需要注意的是继承的是父类虚函数的声明而不是定义,如果父类的虚函数中存在函数参数的缺省值,则也会继承。如果父类中的函数是虚函数,则子类会继承父类虚函数的特性,也就是子类继承过来的该函数就算在子类中不写virtual,也是虚函数
  2. 多态的构成条件
  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

注意:首先一定存在父子继承关系,一般情况下,我们是将最普通的身份设置成基类以供继承,其他一些特殊身份会去继承这个基类,并且对基类中的虚函数完成继承之后进行重写,这个过程相当于可以理解为给特殊的身份设置一些特殊的权限。调用的虚函数在子类和父类中必须满足函数名,参数和返回值相同。还注意的是:子类继承父类的虚函数时,继承的是父类虚函数的声明接口,并且如果父类的虚函数存在参数缺省值,则同样会被子类继承。

下面通过一个买票的例子简单理解一些多态:

  • 代码1:父类对象的指针去调用虚函数
// 写一些特殊的身份来继承基类
// 学生买票:半价
class Student:public Person
{
public:
	void BuyTicket()
	{
		cout << "学生买票,半价售卖:50¥" << endl;
	}

};

// 军人买票:优先买票,全家售卖
class Soldier :public Person
{
public:
	void BuyTicket()
	{
		cout << "军人买票,优先买票,全家售卖:100¥" << endl;
	}
};

// 支付函数
void Pay(Person* p)
{
	p->BuyTicket();
}


int main()
{
	// 实现一个简单的场景
	Person p;
	Student st;
	Soldier sl;

	// 普通人去买票
	Pay(&p);

	// 学生买票
	Pay(&st);

	// 军人买票
	Pay(&sl);


	return 0;
}

运行结果:
在这里插入图片描述
分析:上面的代码中,我们实现三个对象,分别是普通人,学生,军人,然后我们分别让这三个对象去调用支付函数,支付函数再调用买票,调用支付函数的时候会存在将父类对象的地址传给父类对象的指针,子类对象的地址传给父类对象的指针,这就是继承中学习过的父子类之间的赋值兼容,也叫切片或者切割,然后统一让父类对象指针去调用虚函数,从而形成多态机制。调用的过程:主要是看父类对象的指针指向的是哪个对象,如果指向的是父类对象,则调用的是父类对象中的虚函数,如果指向的是子类对象,则调用的是子类对象中的虚函数。

  • 代码2:父类对象的引用去调用虚函数
//父类对象的引用去调用虚函数
//写一个普通的身份作为基类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票,全价售卖:100¥" << endl;
	}
};

// 写一些特殊的身份来继承基类
// 学生买票:半价
class Student :public Person
{
public:
	void BuyTicket()
	{
		cout << "学生买票,半价售卖:50¥" << endl;
	}

};

// 军人买票:优先买票,全家售卖
class Soldier :public Person
{
public:
	void BuyTicket()
	{
		cout << "军人买票,优先买票,全家售卖:100¥" << endl;
	}
};

// 支付函数
void Pay(Person& p)
{
	p.BuyTicket();
}


int main()
{
	// 实现一个简单的场景
	Person p;
	Student st;
	Soldier sl;

	// 普通人去买票
	Pay(p);

	// 学生买票
	Pay(st);

	// 军人买票
	Pay(sl);


	return 0;
}

运行结果:
在这里插入图片描述
分析:上面的代码中,存在父子类的继承关系,父类中存在虚函数,子类中完成对父类虚函数的重写(函数名相同,参数相同,返回值相同,虚函数的定义不同),父类的对象的引用去调用虚函数,显然能够构成多态机制,和上面是同样的道理,父类对象的引用引用哪个对象,就去调用哪个对象的虚函数,如果父类对象引用的是父类对象,则调用的就是父类对象中的虚函数,如果父类对象引用的是子类对象,则调用的就是子类对象的虚函数。

  • 代码3:父类对象去调用虚函数
//父类对象去调用虚函数
//写一个普通的身份作为基类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票,全价售卖:100¥" << endl;
	}
};

// 写一些特殊的身份来继承基类
// 学生买票:半价
class Student :public Person
{
public:
	void BuyTicket()
	{
		cout << "学生买票,半价售卖:50¥" << endl;
	}

};

// 军人买票:优先买票,全家售卖
class Soldier :public Person
{
public:
	void BuyTicket()
	{
		cout << "军人买票,优先买票,全家售卖:100¥" << endl;
	}
};

// 支付函数
void Pay(Person p)
{
	p.BuyTicket();
}


int main()
{
	// 实现一个简单的场景
	Person p;
	Student st;
	Soldier sl;

	// 普通人去买票
	Pay(p);

	// 学生买票
	Pay(st);

	// 军人买票
	Pay(sl);


	return 0;
}

编译结果:
在这里插入图片描述
运行结果:
在这里插入图片描述
分析:从上面的运行结果可以看出,我们使用父类对象去调用虚函数时,普通人,学生和军人去调用支付函数进而调用买票函数,最终调用的都是普通人的买票函数,显然是不对的,本质就是我们上述调用支付函数的时候会发生一些赋值兼容:父类对象传给父类对象,子类对象传给子类对象,通过我们对赋值兼容的理解,父类对象传给父类对象就是最基本的赋值过程,存在f两个父类对象,子类对象传给父类对象,本质有一个父类对象,一个子类对象,切片的过程就是将子类对象中继承自父类对象的成员的内容赋值给父类对象,所以显然赋值之后,让父类对象去调用虚函数时和子类显然没有任何关系,所以调用的都是父类中的虚函数。

总结:通过赋值兼容的学习,我们要知道:如果将子类对象赋值给父类对象,本质就是将子类对象中继承自父类对象的成员的内容赋值给父类对象,如果将子类的指针或者引用赋值给父类对象的指针或者引用,本质就是将父类对象的指针或者引用指向子类对象中继承自父类对象的成员,其能够管到的范围就是子类对象中继承自父类对象中的成员。综上,让父类对象去调用虚函数的话,无论是将父类对象传给父类对象,还是将子类对象传给父类对象,本质都是父类对象去调用虚函数,如果是让父类对象的指针或者引用去调用虚函数,如果是将父类对象的指针或者引用传给父类对象的指针或者引用,那么就是调用父类对象的虚函数。如果是将子类对象的指针或者引用传给父类对象的指针或者引用,我们知道本质就是父类对象的指针或者引用指向子类对象,所以最终调用的是子类的虚函数。

三、虚函数重写的两个例外

  1. 协变
    根据前面的介绍,我们知道虚函数的重写必须满足函数的函数名,参数和返回值相同,但是协变是虚函数重写的一个特例,就是允许虚函数的返回值不同,父子类的虚函数的返回值必须满足父子类的引用或者指针。基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
  • 代码1:常规情况的多态
// 常规情况

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

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

int main()
{
	Person p;
	Person* ptr1 = &p;// 指向父类
	Person* ptr2 = new Student;// 指向子类
	ptr1->Func();
	ptr2->Func();
	return 0;
}

运行结果:
在这里插入图片描述
分析:上面的代码中父子类的虚函数的返回值是相同的,显然可以构成多态。

  • 代码2:父子类的虚函数返回值满足父子类的指针关系
// 虚函数的返回值满足父子类的指针
class A
{
};
// B类继承A类
class B :public A
{

};
class Person
{
public:
	virtual A* Func()
	{
		cout << "Person::Func()" << endl;
		return nullptr;
	}
};

class Student :public Person
{
public:
	virtual B* Func()
	{
		cout << "Student::Func()" << endl;
		return nullptr;
	}
};

int main()
{
	Person p;
	Person* ptr1 = &p;// 指向父类
	Person* ptr2 = new Student;// 指向子类
	ptr1->Func();
	ptr2->Func();
	return 0;
}

运行结果:
在这里插入图片描述
分析:构成多态

  • 代码3:父子类的虚函数返回值满足父子类的引用关系
// 虚函数的返回值满足父子类的引用
class A
{
};
// B类继承A类
class B :public A
{

};
class Person
{
public:
	virtual A& Func()
	{
		cout << "Person::Func()" << endl;
		A a;
		return a;
	}
};

class Student :public Person
{
public:
	virtual B& Func()
	{
		cout << "Student::Func()" << endl;
		B b;
		return b;
	}
};

int main()
{
	Person p;
	Person* ptr1 = &p;// 指向父类
	Person* ptr2 = new Student;// 指向子类
	ptr1->Func();
	ptr2->Func();
	return 0;
}

运行结果:
在这里插入图片描述
分析:构成多态

  • 代码4:虚函数的返回值构成父子类关系
// 虚函数的返回值满足父子类
class A
{
};
// B类继承A类
class B :public A
{

};
class Person
{
public:
	virtual A Func()
	{
		cout << "Person::Func()" << endl;
		A a;
		return a;
	}
};

class Student :public Person
{
public:
	virtual B Func()
	{
		cout << "Student::Func()" << endl;
		B b;
		return b;
	}
};

int main()
{
	Person p;
	Person* ptr1 = &p;// 指向父类
	Person* ptr2 = new Student;// 指向子类
	ptr1->Func();
	ptr2->Func();
	return 0;
}

编译结果:
在这里插入图片描述
分析:返回值不同,不是协变,不构成多态

  1. 析构函数的重写(基类与派生类析构函数的名字不同编译器会处理)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

  • 代码1:基类的析构函数不定义为虚函数(常规情况)
class A
{
public:
	A(int a)
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
protected:
	int _a;
};

class B :public A
{
public:
	B(int a,int b)
		:A(a)
	{
		cout << "B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}

private:
	int _b;
};

int main()
{
	A a(1);
	B b(1,2);

	return 0;
}

运行结果:
在这里插入图片描述

  • 代码2:基类的析构函数不定义为虚函数(不常规情况)
class A
{
public:
	A(int a)
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
protected:
	int _a;
};

class B :public A
{
public:
	B(int a, int b)
		:A(a)
	{
		cout << "B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}

private:
	int _b;
};

int main()
{
	A* pa = new A(1);
	delete pa;

	pa = new B(1, 2);
	delete pa;

	return 0;
}

运行结果:
在这里插入图片描述
分析:在delete子类对象的时候,显然调用了父类的析构函数,这并不是我们想要的结果,正常的结果应该是:父类对象析构时调用父类的析构函数,子类对象析构时调用子类的析构函数。

  • 代码3:基类的析构函数定义为虚函数
class A
{
public:
	 A(int a)
	{
		cout << "A()" << endl;
	}

	 virtual ~A()
	{
		cout << "~A()" << endl;
	}
protected:
	int _a;
};

class B :public A
{
public:
	B(int a, int b)
		:A(a)
	{
		cout << "B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}

private:
	int _b;
};

int main()
{
	A* pa = new A(1);
	delete pa;

	pa = new B(1, 2);
	delete pa;

	return 0;
}

运行结果:
在这里插入图片描述

四、C++中的两个关键字:override 和 final

  1. final:修饰父类的虚函数,修饰之后该虚函数将不能被子类重写。
  • 代码:
// 基类
class A
{
	virtual void Func() final
	{
		cout << "A::Func()" << endl;
	}
};

class B :public A
{
	void Func()
	{
		cout << "B::Func()" << endl;
	}
};

编译结果:
在这里插入图片描述
一旦父类的虚函数被final关键字修饰,则不能再被子类重写。
2. override:修饰子类的虚函数,检查子类中是否正确完成对父类虚函数的重写

  • 代码1:正确重写
// 基类
class A
{
	virtual void Func()
	{
		cout << "A::Func()" << endl;
	}
};

class B :public A
{
	void Func() override
	{
		cout << "B::Func()" << endl;
	}
};

int main()
{
	A a;
	B b;
	return 0;
}

编译结果:
在这里插入图片描述

  • 代码2:错误重写
// 基类
class A
{
	virtual void Func()
	{
		cout << "A::Func()" << endl;
	}
};

class B :public A
{
	int Func() override
	{
		cout << "B::Func()" << endl;
	}
};

int main()
{
	A a;
	B b;
	return 0;
}

编译结果:
在这里插入图片描述
当子类虚函数被override修饰时,override就会检查子类是否正确完成重写,如果是,则编译通过,如果不是,则编译报错。

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

在这里插入图片描述

  1. 重载是针对同一个作用域的函数而言的,函数名相同,参数不同的函数构成重载
  2. 重写是在多态机制中提出的概念,重写是针对父子类的虚函数而言的,当子类继承父类的虚函数之后,在函数名参数和返回值相同的情况下,重新定义该虚函数,此过程就算虚函数的重写
  3. 隐藏是继承中提出的概念,是存在于父子类的作用域类同名的函数或者成员变量

六、纯虚函数和抽象类

  1. 纯虚函数:在纯虚函数的声明后加上=0的函数就是纯虚函数,纯虚函数只需要声明即可,一般不需要定义,就算定义了也没有价值。
  2. 抽象类:包含纯虚函数的类型就是抽象类,抽象类不能实例化出对象,但是可以被继承,同时,继承出的类必须对该抽象类中的纯虚函数完成重写,否则,继承该抽象类的类中同样含有纯虚函数,所以也不能实例化出对象。
  • 代码1:使用抽象类实例化对象
// 抽象类
class A
{
public:
	virtual void Func() = 0;
};


int main()
{
	A a;

	return 0;
}

编译结果:
在这里插入图片描述
分析:抽象类不能实例化出对象

  • 代码2:继承抽象类的类不对纯虚函数进行重写
// 抽象类
class A
{
public:
	virtual void Func() = 0;
};

class B:public A
{};


int main()
{
	B b;

	return 0;
}

编译结果:
在这里插入图片描述

分析:继承抽象类如果没有对抽象类中的纯虚函数进行重写,那么子类同样会继承该纯虚函数,所以子类中也包含纯虚函数,因此,该子类也属于抽象类,所以也不能实例化出对象。

  • 代码3:继承抽象类的类对纯虚函数进行重写
// 抽象类
class A
{
public:
	virtual void Func() = 0;
};

class B :public A
{
	void Func()
	{
		cout << "B::Func()" << endl;
	}
};


int main()
{
	B b;

	return 0;
}

编译结果:
在这里插入图片描述
分析:当该类继承抽象类之后,并且对该纯虚函数完成重写,那么该子类中就不包含纯虚函数,所以这个子类就不是抽象类,所以可以实例化出对象。

七、接口继承和实现继承

  1. 接口继承:一般是针对函数而言的,函数一般包含声明和实现,在多态机制中,子类继承父类时,对于其中的虚函数,子类是采用接口继承的方式继承父类的虚函数,注意:如果父类中的虚函数包含参数缺省值,同样会继承之。
  2. 实现继承:普通的成员函数的继承一般采用实现继承,如:一个子类继承父类,则子类会继承父类中的函数,继承的方式是实现继承。

八、多态的原理(代码观察)

  1. 虚函数表指针:如果一个类型中存在虚函数,则在这个类型中除了成员变量之外,还会维护一个指针,这个指针是指向一个存放虚函数地址的表格(数组),这个指针就是虚函数表指针。
  2. 虚函数表:类型中的虚函数的地址一般都是存放在一个虚函数表中,每一个类型都会公用同一份虚函数表,同一个类型实例化出来的所有对象也会公用同一份虚函数表,虚函数表存放在代码段中。
  • 代码1:观察含有虚函数的类实例化出的对象中的结构

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

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

运行结果:
在这里插入图片描述
调试:
在这里插入图片描述
分析:通过上面的调试,由该类创建出来的对象中不仅存在对应的成员变量,还存在一个指针,这个指针指向的内容存放了一个函数的地址。上面的调试结果中的vfptr就是虚函数表指针,这个虚函数表指针会指向一个虚函数表,虚函数表中会存放这个类中的所有虚函数的地址。

  • 代码2:观察父子类实例化出的对象中的结构
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;
}

调试结果:
在这里插入图片描述

分析:通过上面的调试我们可以看到:基类对象和派生类对象中都各自存放一张虚函数表存放各自的虚函数,其中我们知道,上面的代码中,子类对父类中的Func1()虚函数进行了重写,没有对Func2()虚函数进行重写,所以结果就如上面:父子类对象中Func2()的地址是一样的,也就是属于同一个虚函数,Func1()的地址不一样,也就不属于同一个函数。

总结:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也是存在的一部分,另一部分是自己的成员
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
  5. 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  6. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中

九、多态的原理+父子类的赋值兼容转化

通过上面的代码演示,我们知道了虚函数的地址是存放在一个虚函数表中的,而每一个对象中会维护一个虚函数表指针。我们知道,父子类的赋值兼容转化中支持子类的对象赋值给父类对象,子类对象的地址赋值给父类指针,子类对象赋值给父类的引用。但是其中子类对象的地址赋值给父类对象的指针和子类对象赋值给父类对象的引用可以构成多态,而子类对象赋值给父类对象不能构成多态。

  • 子类对象赋值给父类对象:这个过程做的工作是将子类对象中继承自父类对象的成员赋值给一个父类对象,所以这个父类对象去调用虚函数时,是去查找自己的虚函数表指针,从而找到虚函数表,然后根据调用的函数名找到调用的虚函数的地址,从而进行调用。
  • 子类对象赋值给父类对象的引用:这个过程做的工作是让父类对象的引用指向子类对象中继承自父类对象的成员,本质就是指向子类对象,由于子类对象中只有一个虚函数表指针,所以在调用虚函数时,会通过这个引用找到子类对象中的虚函数表指针,从而找到虚函数表,然后根据调用的虚函数名找到调用的虚函数的地址,从而进行调用。
  • 子类对象的地址赋值给父类对象的指针:这个过程是将父类对象的指针指向子类对象中继承自父类对象的成员,本质就是指向子类对象,由于子类对象中只有一个虚函数表指针,所以在调用虚函数时,会通过这个指针找到子类对象中的虚函数表指针,从而找到虚函数表,再根据调用的虚函数名找到调用的虚函数的地址,从而进行调用。

十、单继承中的虚函数表

  • 代码:
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

// 上面的函数的返回类型为void,没有参数,所以函数指针类型为:void (*)()
typedef void (*VF_Ptr)();// 将函数指针类型void (*)()重定义为VF_Ptr

// 打印虚函数表
void PrintVTable(VF_Ptr* vp)
{
	printf("虚函数表的地址:%p\n", vp);
	for (int i = 0; vp[i]; i++)
	{
		printf("第%d个虚函数的地址:%p\n",i, vp[i]);
		// 调用对应的虚函数
		vp[i]();
	}
	printf("\n");
}


int main()
{
	Base b;
	Derive d;
	// 打印a中的虚函数表
	PrintVTable((VF_Ptr*)*(int*)&b);
	cout << endl;
	// 打印b中的虚函数表
	PrintVTable((VF_Ptr*)*(int*)&d);
	return 0;
}

调试结果:

在这里插入图片描述
在这里插入图片描述

运行结果:
在这里插入图片描述

分析:通过调试中的监视窗口可以看到:监视窗口中的虚函数表只能看到从父类继承下来的虚函数,而不能看到子类新增的虚函数,此时我们只能通过内存去进行观察,在求虚函数表指针的时候,首先我们要对对象的地址进行强制类型转化为int*,然后解引用,这个时候可以拿到对象中的前四个字节的内容,因为我们知道这四个字节的内容就是虚函数表的地址,类型就是虚函数指针类型的地址,所以这个时候对其进行强制类型转化为对应的类型,然后传给打印虚函数表的函数即可。

十一、多继承中的虚函数表

  • 代码:
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;
}

调试:
在这里插入图片描述
运行结果:
在这里插入图片描述

分析:在上面的代码中,Base1有两个虚函数:Base1::fun1c(),Base2::func2(),Base2有两个虚函数:Base2::func1(),Base2::func2(),然后Derive类先后继承了Base1和Base2,并对其中的func1()进行了重写,然后新增了func3()虚函数。显然,通过结果我们可以看出,Derive类的对象中有两份虚函数表,一份来自于Base1,一份来自于Base2,Derive中对func1()进行重写会将对来自于Base1和来自于Base2的虚函数表中的func1()函数进行覆盖,然后新增的func3()函数会添加到第一张虚函数表中。

另一个需要注意的是求Base2的虚函数表指针:可以有两种方式:

  1. VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));首先将d的地址强制类型转化为char*,然后增加Base1的大小个字节就到了Base2的地址,因为Base2的前四个字节的内容就是Base2的虚函数表指针,所以此时需要强制类型转化为int*然后解引用,就可以拿到Base2前四个字节的内容,最后强转为虚函数指针的类型。
  2. vTableb2 = (VFPTR*)(*(int*)(((Base1*)&d + 1)));首先对d的地址强制类型转化为Base*,然后+1,就跳过了Base1,到Base2,此时就是要拿到Base2前四个字节的内容,操作和上面一致。

总结:多继承中,继承几个类,就会存在几份虚函数表,如果在子类中新增虚函数,则默认会将该虚函数的地址存放到第一份虚函数表中。

十二、继承和多态结合常见题

  1. 关于虚函数的描述正确的是( )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
    B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数
    D:虚函数可以是一个static型的函数

分析: A:派生类的虚函数是继承基类虚函数的接口的,所以派生类的虚函数和基类的虚函数的函数名,参数和返回值是相同的,故A错。
B:当我们在内联函数前加上inline关键字时i,发现程序并不会报错,此时只是编译器忽略了内联函数的内敛属性,所以内联函数可以是虚函数,但是在多态机制下内敛属性会被编译器忽略。故B正确。
C:当派生类继承基类的虚函数时,可以对基类的虚函数进行重写,也可以不进行重写,当不进行重写时,虚函数和基类的虚函数是同一个,重写时就会对父类该虚函数进行覆盖,故C错误。
D:虚函数通常需要父类的指针或者引用指向的对象去调用,但是static修饰的函数没有this指针,所以在对象中不存在该函数的地址,所以不能调用到,所以static修饰的函数不能是虚函数,故D错误。

2. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表

分析:
A:当一个没有继承其他类或者只继承了一个只有一张虚函数表的父类的时候,这个类就只有一张虚函数表,如果这个子类继承了多个父类,或者继承了拥有多张虚函数表的父类的时候,那么这个子类就会拥有多张虚函数表,故A错误。
B:一个类型的虚表是独享的,父类和子类属于不同的类型,所以父类和子类的虚函数表是不同的,故B错误。
C:虚表是在编译期间形成,在构造函数的初始化列表中初始化的,故C错误。
D:一个类的虚表是这个类实例化出的对象所共享的,故D正确。

3. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

分析:
A、B:一个类实例化出的对象中会存储这个类的虚函数表地址,也就是虚函数表指针,但是这个指针具体存在这个对象的哪一个部分是不确定的,故A、B错误。
C:A类对象和B类对象显然属于不同类型的对象,所以它们的虚函数表是不同的,因此虚函数表地址自然不同,故C错误。
D:因为B类继承了A类,并且B类中没有新增其他的虚函数,所以A类对象中的虚函数表和B类对象中的虚函数表中存储的虚函数的个数是相同的。故D正确。

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

#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

分析:
本题考察的是菱形虚拟继承的模型,由代码可以知道:A类是最上面的基类,B类和C类都虚继承了A类,D类继承了B类和C类,所以在D的结构中,最开始的一部分是A类的成员,然后是B类的成员,接着是C类的成员,最后是D类的成员,构造函数的初始化列表的初始化顺序和初始化列表中的先后顺序没有关系,变量在对象中的声明顺序有关系,根据上面的分析,应该先调用A的构造函数完成A类成员的初始化,再调用B类的构造函数完成B类成员的初始化,接着调用C类的构造函数完成C类的初始化,最后调用D类的构造函数完成D类成员的初始化。故选A。

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

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

分析:本题主要考察的是继承中父子类之间的赋值兼容,上面的代码中:Derive继承了Base1类和Base2类,所以在Derive类中会存在Base1成员和Base2的成员,现在有一个Derive的对象,将其地址分别赋值给Base1的指针和Base2的指针和Derive的指针,Base1的指针指向的是d对象中的Base1成员,Base2指针指向的是d对象中的Base2成员,Derive指针指向的就是d对象,因为Derive先继承Base1再继承Base2,所以Base1是在d对象的前面,然后才是Base2的成员,所以Base1指针和Derive的指针从值的意义上来讲是一样的,但是它们能看到的内容是不一样的。显然Base2的指针和Base1指针的值是不一样的。故选C。

  1. 以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

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

分析:
本题考察的是继承和多态中的函数调用,首先要明白代码中的内容:有A类和B类,A类中包含func()和test()两个虚函数,B类继承了A类,并且对A类中的func()函数进行了重写。调用处:p是指向一个动态的B对象的,此时由p去调用test函数,那么此时是一个普通调用,B类的test的函数是继承A类,有一个隐藏的细节:这个test函数的参数中隐藏一个this指针,这个this指针是A*
const this,在调用的时候会发生一个父子类的赋值兼容,就是由B* p传给了A* const
this,再在test函数中由这个this指针去调用func,此时要知道this指针虽然是A*类型的,但是实际上指向的对象是B,所以完成的是多态调用因此调用的是B类型中的func()函数,又因为虚函数的继承是接口继承,A类型中的func()函数有函数参数缺省值,所以B类中的func()函数会继承A类函数中func函数的缺省值,所以最终打印的结果就是:B->1。故B正确。

  1. inline函数可以是虚函数吗?
    inline函数可以是虚函数,但是在多态调用机制时,编译器会忽略内联函数的Inline属性,因为多态中是需要通过查找虚函数表中调用函数的地址来进行调用的,如果函数有inline属性,是不存在地址的。

  2. 静态成员可以是虚函数吗?
    不可以,普通的虚函数的地址是存放在对象中的虚函数表的,调用虚函数的时候是通过某个对象去这个对象中的虚函数表中找到调用函数的地址从而进行调用,静态成员函数没有this指针,故无法完成上述调用。

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

虚函数的地址是存在虚函数表的,虚函数表的初始化是在构造函数的初始化列表阶段才初始化的,所以构造函数不能是虚函数。
10.对象访问普通函数快还是虚函数更快?

如果是普通的调用的函数,访问普通函数和访问虚函数都是从符号表查找地址然后进行访问,但是在多态调用机制的时候,访问虚函数需要通过对象找到对象中的虚函数表指针找到虚函数表,从而找到调用的虚函数的地址进行调用,所以在多态调用机制中调用普通函数比调用虚函数快。
11. 虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段生成的,一般存在常量区(代码段),需要知道的是虚函数表是在对象调用构造函数时的初始化列表阶段初始化的。

  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值