C++第十天:多态(定义,抽象类,原理,常见面试题)

1.多态概念

多态:完成某个行为,当不同的对象去完成时会产生出不同的状态。
例子:不同的人去火车站买票,有的人买的是成人票,有的人买的是学生票,这就是多态的行为。

2 多态的定义

条件:
1.调用函数的对象必须是指针或者引用
2.被调用的函数必须是虚函数,且完成了虚函数的重写
什么是虚函数?
虚函数:就是在类的成员函数前面加virtual

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "支付半价票" << endl;
	}
};

什么是虚函数的重写
派生类有一个和基类完全一样的虚函数,**完全相同是指:**函数名,参数吗,返回值相同。
不规范写法:派生类不写virtual也能实现。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "支付半价票" << endl;
	}
};

class Student : public Person
{
public:
	//void BuyTicket()//不规范写法
		
	virtual void BuyTicket()
	{
		cout << "支付全价票" << endl;
	}
};

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

void Test()
{
	Person p;
	Func(p);

	Student s;
	Func(s);
}

虚函数重写的例外:协变
重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。

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;
	}
};

析构函数的重写
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。由于编译器对他们做了特殊处理,所以即使函数名不一样,两者也为虚构函数。

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;
	//只有派生类Student的析构函数重写了Person的析构函数,
	//下面的delete对象调用析构函数,才能构成多态,才能保证
	//p1和p2指向的对象正确的调用析构函数。 
	delete p1;
	delete p2;

	system("pause");
	return 0;
}

接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数吗,可以使用函数,继承的是函数的实现。派生类的继承的是函数的缺口,目的是为了重写,为了实现多态,继承的是接口。

3.抽象类

在虚函数后面写 = 0,则这个函数被称为纯虚函数。包含纯虚函数的类被叫做抽象类(接口类),派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象。纯虚函数更体现了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};

class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz Good" << endl;
	}
};

override关键字
描述:override保留字表示当前函数重写了基类的虚函数。
目的:1.在函数比较多的情况下可以提示读者某个函数重写了基类虚函数(表示这个虚函数是从基类继承,不是派生类自己定义的)\\\\\\\\\\\\\\
2.强制编译器检查某个函数是否重写基类虚函数,如果没有则报错。
用法:在类的成员函数参数列表后面添加该关键字既可。
例子:
class Base {
virtual void f();
};
class Derived : public Base {
void f() override; // 表示派生类重写基类虚函数f
void F() override;//错误:函数F没有重写基类任何虚函数
};
注意:override只是C++保留字,不是关键字,这意味着只有在正确的使用位置,oerride才启“关键字”的作用,其他地方可以作为标志符(如:int override;是合法的)。

final关键字
用于修饰基类的虚函数,使其不能重写。

、、final关键字
class Car
{
public:
	virtual void Drive() final;
};
	
class Benz : public Car
{
public:
	virtual void Drive() //导致错误。
	{
		cout << "Benz Good" << endl;
	}
};

4.多态的原理

问:结果为多少

class Base
{
public:
	virtual void Func()
	{
		cout << "Func() " << endl;
	}
private:
	int _b;
};
int main()
{
	Base b;
	printf("%d", sizeof(b));
	system("pause");
	return 0;
}

在这里插入图片描述
答案是:8,为什么?我们来看下监视窗口:
在这里插入图片描述
我们发现除了_d之外还多了一个_vfptr在对象的前边(有的平台可能在后边),对象中的这个指针叫做虚函数表指针。一个含有虚函数的类中至少有一个虚函数指针,因为虚函数的地址要被放到虚表中。
改进函数,方便研究

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

	virtual void Func2()
	{
		cout << "Base::Fun2()" << endl;
	}

	void Func3()
	{
		cout << "Base::Fun3()" << endl;
	}
private:
	int _b = 1;
};

class Drive : public Base
{
public:
	virtual void Func()
	{
		cout << "Drive :: Func" << endl;
	}
private:
	int _d = 2;
};


int main()
{
	Base b;
	Drive d;
	system("pause");
	return 0;
}

在这里插入图片描述
通过测试我们可以总结以下几点:
1.派生对象d中也有一个虚基表,d对象由两部分构成,一部分是从a中继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2.基类b对象和派生对象d 虚表并不是一样的。我们在上图中可以看到Func1完成了重写,所以d的虚表中存在的是Drive::Func(),所以虚函数的重写也叫作覆盖。重写是语法层面的叫法,覆盖是原理层面的叫法。
3.d中继承下来的是虚函数,因为Func3()不是虚函数,所以不会被继承下来。
4.虚函数的本质是存虚函数指针的指针数组,这个数组最后放了一个nullptr。
5.派生类的虚表生成:a.先将基类的虚表内容拷贝一份到派生类的虚表中,b。如果派生类重写了基类中某个虚函数,则用派生类的虚函数覆盖虚表中基类的虚函数,c.派生类自己新增加的虚函数按派生类中声明次序增加到派生表的后面。
6.虚表存的是虚函数的指针,不是虚函数,虚函数和普通函数一样,都存在代码段中。

5.多态的原理

我们开头的第一个例子来验证:
这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

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 Test()
{
	Person p;
	Func(p);

	Student s;
	Func(s);
}

在这里插入图片描述
再让我们看下个例子:
在这里插入图片描述
由图可知:
1.红色的箭头表示,people指向p对象,people->BuyTicket在虚表中找到的虚函数是Person :: BuyTicket()
2.绿色的箭头表示,people指向s对象,people->BuyTicket在虚表中找到的函数是Student::BuyTicket()
3.体现了不同对象调用不同的状态。
4.达成多态的条件:1.虚函数覆盖 2.一个对象的指针或者引用调用虚函数。

5.满足多态之后的函数调用,不是在编译的时候确定的,而是在运行起来之后到对象的栈中去找的。不满足多态的函数用时是在编译就确定的。

动态绑定与静态绑定
1.静态绑定(前期绑定/早绑定),在程序编译期间确定了程序的行为,也成为静态多态。如:函数重载
2.动态绑定(后期绑定/晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数。也被称为动态多态

6.单继承和多继承的虚基表

单继承的虚基表

class Base{
public:
	virtual void fun1(){ cout << "Base::fun1()" << endl; }
	virtual void fun2(){ cout << "Base::fun2()" << endl; }
private:
	int _b;
};

class Drive : public Base
{
public:
	virtual void fun1(){ cout << "Drive :: fun1()" << endl; }
	virtual void fun3(){ cout << "Drive :: fun3()" << endl; }
	virtual void fun4(){ cout << "Drive :: fun4()" << endl; }
private:
	int _d;

};

int main()
{
	Base b;
	Drive d;
	system("pause");
	return 0;
}

在这里插入图片描述
我们发现没有fun3和fun4的存在。这是因为监视窗口故意隐藏了这两个函数,但是我们可以用别的方法来查看虚表。

常见面试题

1.什么是多态
答:不同对象进行完成某个行为发生的不同状态。
2.什么是重载,重定义,重写?
答:重载:两个函数在同一作用域,函数名相同
重定义(隐藏):两个函数分别在基类和派生类的作用域,函数名相同,作用域不同,只要不构成重写就构成重定义
重写(覆盖):两个函数在基类和派生类的作用域,函数名/参数/返回值都必须相同,两个都必须是虚函数。
3.多态的实现原理
答:虚函数表
4.Inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,不能把地址放到虚函数表中
5.静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方法无法访问虚函数表,所以静态成员函数无法放进虚表
6.构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针实在构造函数初始化列表阶段才初始化的。
7.析构函数可以是虚函数吗?什么场景下的析构函数是虚函数?
答:可以,最好把基类的析构函数也定义成虚函数。
8.对象访问普通函数快还是虚函数更快?
答:如果都是普通对象,那就是一样快的。如果是指针对象或者是引用对象,那就是普通函数快,因为多态,运行时调用虚函数需要在虚函数表中查找
9.虚函数表在什么时候生成,存在哪里?
答:虚函数在编译阶段就形成,一般情况下存在静态区间。
10.C++菱形继承问题?虚继承的原理?
答:数据冗余,二义性。虚函数表。
11.什么是抽象类?抽象类的作用?
答:在虚函数后边加上 = 0,或者在派生类后边加上override。作用:强制重写虚函数,体现了接口继承问题。

猜你喜欢

转载自blog.csdn.net/l477918269/article/details/88800079