C++ 多态

1.多态的概念

多态(polymorphism)通俗来说就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点是运行时多态,编译时多态主要就是我们前面的函数重载和函数模版,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票就是优惠买票;军人买票是优先买票。

2.多态的定义及实现

2.1多态的构成条件

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

2.1.1实现多态还有两个必须重要条件

  • 必须是基类的指针或者引用调用虚函数。
  • 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

说明:要实现多态效果,第一:必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二:派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

2.1.2虚函数

类成员函数前面加virtual修饰,那么这个成员函数就被称为虚函数。注意非成员函数不能加virtual修饰。

class Person
{
public:
	virtual void Func()
	{
		cout << "买票—全价" << endl;
	}
};

2.1.3虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(既派生类虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派生类的虚函数在不加virual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用(可能在考试选择题中,经常会故意埋这个坑,判断是否构成多态)。

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

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

//void Func(Person& ptr)//引用也可以
//void Func(Person ptr)//直接调用不行,不能构成多态
void Func(Person* ptr)
{
    //这里可以看到虽然Person指针Ptr在调用BuyTicket但是跟ptr没有关系,
    //而是由ptr指向的对象决定的
	ptr->BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(&p);
	Func(&s);

	return 0;
}

2.2多态场景的一个选择题

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

运行出来的结果是B->1,为什么会是这个结果呢?

这个p是子类的指针,但是子类中没有重写test(),所以此时的指针指向的是父类的test,调用父类的func,构成了多态,子类的func完成了重写。

重写的本质是重新写虚函数的实现。B中的func重写A中的func时并没有走B的func函数,而是在原A函数的基础上用B的fuhc函数体覆盖A的func函数体。(多态生效时,函数体使用B::func,但默认参数仍取自A::func

如果A中的func有默认参数val=1,B中的func有默认参数val=0,但B没有重写test(),那么当通过B对象调用test()(继承自A的test),test()内部调用func(),此时func()的默认参数来自A的声明,即val=1,但实际调用的函数体是B的func,所以输出B->1。

2.3虚函数重写的一些其他问题

  • 协变

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

class A
{};

class B : public A
{};

class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "买票—全价" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票—半价" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(&ps);
	Func(&st);

	return 0;
}
  • 析构函数的重写

基类的析构函数为虚函数,此时派生类析构函数只有定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成重写。

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

class B : public A
{
public:
	virtual ~B()
	{
		cout << "~B() -> delete:" << _p << endl;
		delete [] _p;
	}

protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;

	return 0;
}

为什么基类中的析构函数建议设计为虚函数

上面代码可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中有资源释放

2.4 override和final关键字

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

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

class Benz : public Car
{
public:
	virtual void func() override
	{
		cout << "Benz:舒适" << endl;
	}
};

error C3668: “Benz::func”: 包含重写说明符“override”的方法没有重写任何基类方法。

但如果不写override则不会检测出来

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

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

在基类的成员函数加上final则不会被重写。

2.5 重载/重写/隐藏的对比

3. 纯虚函数和抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

class Car
{
public:
	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 car;//“Car”: 无法实例化抽象类

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

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

	return 0;
	return 0;
}

4.多态的原理

4.1 虚函数表指针

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

这个运行结果是12bytes,除了_b和_ch成员,还多一个_vfptr放在对象的前面(注意:有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所以虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.2多态的原理

4.2.1 多态是如何实现的

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。第二张图,ptr指向的Person对象,调用的是Person的虚函数;第三张图,ptr指向的Student对象,调用的是Student的虚函数。

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	string _name;
};

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
	string _id;
};

class Soldier : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
	string _codename;
};

void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
	// 多态也会发⽣在多个派⽣类之间。
	Person ps;
	Student st;
	Soldier sr;
	Func(&ps);
	Func(&st);
	Func(&sr);
	return 0;
}


4.2.2 动态绑定与静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定的,也就是在运行时到指向对象的虚函数表中找到函数的地址,叫做动态绑定。

4.3虚函数表

  • 基类对象的虚函数表中存放基类所以虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。

  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的是这里继承下来的基类部分虚函数指针和基类对象的虚函数指针不是同一个。就像基类对象的成员和派生类对象中的基类对象成员也独立的。
  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数的地址。
  • 派生类的虚函数表中包含:(1)基类的虚函数的地址;(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三部分。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组后面放一个0x00000000标记。(这个C++并没有规定,各个编译器自行定义,vs系列编译器会在后面放个0x00000000标记,g++系列编译器不会放)
  • 虚函数存在哪里呢?虚函数和普通函数一样,编译好后是一段指令,都是存在代码段,只是虚函数的地址又存到了虚表中。
  • 虚函数表存在哪里?这个问题严格说没有标准答案C++标准并没有规定,我们写下面代码可以对比验证一下。vs下是存在代码段(常量区)。
#include<iostream>
using namespace std;

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

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

	void func3()
	{
		cout << "Base::Func3" << endl;
	}
protected:
	int a = 1;
};

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

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

	void Func5()
	{
		cout << "Base::Func5" << endl;
	}

protected:
	int b = 2;
};

int main()
{
	Base b;
	Base c;
	Derive d;
	return 0;
}

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::Func1);
	printf("普通函数地址:%p\n", &Base::Func3);
	return 0;
}

5.虚函数的面试题

正确选B,A选项被virtual修饰的成员函数才能称为虚函数;C选项虚函数virtual关键字只有在声明时加上,在类外实现时不能加;D选项静态虚拟成员函数static和virtual是不能同时使用的


正确选项是D,选项A友元函数不属于成员函数,不能成为虚函数;选项B静态成员函数不能设置为虚函数;选项C静态成员函数不能设置为虚函数,是因为静态成员函数与具体对象无关,属于整个类,核心关键是没有this指针,可以通过 类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数。


答案是B,选项A抽象类不能实例化对象,所以以对象返回是错误的;选项B抽象类可以定义指针,其目的就是用父类指针指向子类从而实现多态;选项C参数为对象,所以错误;选项D直接实例化对象是不允许的。


答案是D,选项A在多继承的时候就可能会有多张虚表;选项B父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象;选项C虚表是在编译期间生成的。


答案选择A,B选项,虽然子类函数是私有的,但多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化;C选项,不强制也可以直接赋值,因为赋值兼容规则做出了保证;D选项,如果基类中的虚函数是public,即使派生类中的覆盖版本是private,通过基类指针调用仍然是允许的,因为编译器在编译时检查的是基类的访问权限,而不是派生类的。


答案是C,new B时先调用父类A的构造函数,执行test()函数,再调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以此时执行func函数为父类的func函数,打印0,构造完父类后又执行子类构造函数,又调用test函数,然后执行func(),由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1,最后通过父类指针p->test(),也是执行子类的func,所以还是m_iVal加1,最后打印2,所以答案是C。


答案是B。A选项,D类中有几个父类(父类中有虚函数),就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表;C选项,子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚表不需要存储,因为存储了也不能调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值