多态

1.多态的概念

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

运行时多态,具体就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。

2.多态的定义和实现

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

(1) 实现多态必须得两个重要条件

● 必须指针或者引用调用虚函数

● 被调用的函数必须是虚函数

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

(2) 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意 非成员函数不能加virtual修饰。
class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

(3) 虚函数的重写/覆盖

 虚函数的重写/覆盖派生类中有一个根基类完全相同的虚函数(即派生类虚函数与基类函数的返回值类型,函数名字、参数列表完全相同【参数类型相同而非参数名】),称派生类的虚函数重写了基类的虚函数。

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

// 虚函数实现在父类和子类之间
class Person {
public:
	// 完成虚函数的重写
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	// 派生类的虚函数可以不写virtual(但不规范)
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
//多态的实现
// 必须是基类的指针或引用
void Func(Person* ptr)// 父类子类对象赋值兼容转换(切片)
{
	// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);// 满足多态时传谁调谁,不满足时调的都是基类
	Func(&st);// 传派生类会切片,把派生类中基类的部分切出来
	return 0;
}
class Animal
{
public:
	virtual void talk() const
	{}
};
class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};
class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};
// 虚函数实现在多个子类之间
void letsHear(const Animal& animal)
{
	animal.talk();
}
int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}

(4) 多态场景选择题

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
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;
}

1、满足多态

main函数中,创建了一个B类的指针p,并调用p->test()。由于test函数是虚函数,并且在B类中没有重写,所以会调用A类中的test函数,test函数内是this ->func()。注意:通过指向派生类对象的基类指针调用成员函数时,在该成员函数内部的 this 指针类型确实是基类指针类型,即 A*。(这是因为在编译期间,编译器是根据指针的静态类型来确定 this 的类型。虽然实际指向的对象是派生类对象,但从静态类型的角度看,这个指针被视为基类指针,所以 this 的类型为基类指针类型。然而,由于虚函数的动态绑定特性,在运行时会根据实际对象的类型(B类型)来调用正确的函数版本)。满足条件必须是基类的指针或引用调用虚函数,且派生类对虚函数进行了重写,满足多态。

2、虚函数重写 / 覆盖

而在A类的test函数中调用了func函数,在运行时,实际test函数的this指向的对象是 B 类对象,通过虚函数机制,最终会调用 B 类中重写的 func 函数,但由于没有传入参数,此时会使用A类中func函数的默认参数值 1。又因为多态性,最终实际调用的是B类中重写的func函数,但是传入的参数是 1,所以最终输出是 “B->1”,选B。

★★★ 重点:派生类中对虚函数重写的部分只有函数实现部分,返回值类型,函数名字、参数列表都是集成继承而来的,所以绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数都是静态绑定。

(5) 虚函数重写的⼀些其他问题

① 协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数分会派生类对象的指针或引用,称为协变。协变的实际意义不大,所以了解一下即可。

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

② 析构函数的重写

class A
{
public:
	// 如果基类的析构函数不是虚的,那么当通过基类指针删除派生类对象时,只会调用基类的析构函数
	// ~A()
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	// 规定:编译器调用完派生类析构函数后会自动调用基类析构!-> 保证先子后父的顺序 (这个过程是递归的)
	~B()// 构成重写,编译器对所有析构函数做特殊处理,处理成destructor() 
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派生类B的析构函数重写了A的析构函数,下面的delete对象调用析构函数,
// 才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	// destructor() + operator delete
    // 当使用delete删除一个对象时,C++运行时会调用对象的析构函数。
    // 如果析构函数是虚的,运行时系统会通过虚函数表来调用正确的析构函数。
    // 但是,如果析构函数不是虚的,运行时系统不会查找虚函数表,而是直接调用指针类型对应的析构函数。
	delete p1;
	delete p2;

		return 0;
}

1、如果基类的析构不是虚函数

 基类和派生类此时不构成多态,而是构成隐藏关系(函数名相同destructor)。如果基类的析构函数不是虚的,那么当通过基类指针删除派生类对象时,只会调用基类的析构函数因为当使用delete删除一个对象时,C++运行时会调用对象的析构函数。如果析构函数不是虚的,运行时系统不会查找虚函数表,而是使用直接调用指针类型对应的析构。这样程序运行的结果就是调用两次基类析构,而该派生类B内有资源,只使用基类的析构会导致内存泄漏。这种处理方式是不良的。

2、基类析构时虚函数 

 派生类B的析构函数重写了A的析构函数才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。因为当使用delete删除一个对象时,C++运行时会调用对象的析构函数。如果析构函数是虚的,运行时系统会通过虚函数表来调用正确的析构函数。规定:编译器完成派生类析构后会自动调用基类的析构函数(保证先子后父的顺序)。这时编译器就能正确delete两个对象。

(6) override 和 final关键字

class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	// error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类⽅法
	//virtual void Drive() override { cout << "Benz-舒适" << endl; }
	virtual void Dirve() override { cout << "Benz-舒适" << endl; }// BinGou

};
// class Car final 一个无法被继承的类
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	// error C3248: “Car::Drive”: 声明为“final”的函数无法被“Benz::Drive”重写
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

(7) 重载/重写/隐藏的对比

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()
{
	// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
	Car car;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

4.多态的原理

(1) 虚函数表指针

下⾯编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	cout << sizeof(b._b) << endl;
	cout << sizeof(b._ch) << endl;
	return 0;
}
上面题目运行结果12bytes(32位平台),除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

(2) 多态的原理

① 多态是怎么实现的 

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	int _name;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
	int _id;
};
class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
	int _age;
};
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;
}

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

② 虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};
int main()
{
	Base b;
	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::func5);
	return 0;
}

对比判断虚表函数存在哪里?

运行结果:
:010FF954
静态区:0071D000
:0126D740
常量区:0071ABA4
Person虚表地址:0071AB44
Student虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF

 ③ 动态绑定与静态绑定

// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值