C++多态

多态的概念:

  • 多态(polymorphism)的概念:通俗来说,就是多种形态。
  • 多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。
  • 编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
  • 运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就''(>^ω^<)喵'',传狗对象过去,就是"汪汪''。

以下主要是以运行时多态为主:

多态的定义与实现:

多态的构成条件:

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。
⽐如Student继承了Person。Person对象买票全价,Student对象优惠买票。

实现多态的两个必要条件:

  • 必须是指针或者引用来调用虚函数;
  • 被调用的函数必须是虚函数;

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

虚函数:

在认识运行时多态时,我们首先要先认识什么是虚函数:

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

虚函数的重写/覆盖:

派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表(对于形参来说,谈论的本质是类型,与形参的名字无关,与是否有缺省值,缺省值是否相等无关)完全相同),称派⽣类的虚函数重写了基类的虚函数。(简称三同)
注意:
在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态;
虚函数的重写本质是不重写缺省值的,如果继承的基类参数带有缺省值的话,题目有可能会依此挖坑
票价:
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	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;
}

下面题目是对以上注意事项的考察:

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
#include<iostream>
using namespace std;

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继承了A;
  • B当中的func有没有重写A当中的func?

虽然B的func函数没有+virtual,但是满足三同:返回值类型,函数名字,参数列表(指的是形参类型),缺省值不重要,构成重写,因此,满足重写;

  • 虚函数可以重写也可以不重写,因此A的test虚函数被B继承下来了;

所以new的B对象,p指针可以去调用test(走的是继承)

  • test的this需要传一个对象,这个this是A*还是B*?

这时候有两种方向的思考:

第一种:test在A里边,那么this就是A*!!!

第二种:这里的调用不是A对象的指针在调用,是B对象的指针在调用,test又被B继承下来了,那么this应该是B*!!!

虽然test被B继承下来,但是this还是A*,继承是一个形象的说法。编译器真的会在继承时,把A的成员函数,成员变量,不是重写的这种都会在B里拷贝一份吗?这显然是不会的。继承下来的的是搜索规则,从隐藏上讲,在不构成重写的时候,会在B里边搜索有没有test,没有的话再去A里搜索,所以说继承并不会真的将test弄下来,他只是一个形象的说法

  • 接着this调用func(),也就是this->func(),那么构成多态吗?

是构成的。

第一:this是A的指针,是基类的指针去调用func();

第二:满足虚函数的重写(三同);

  • 因为满足多态,func被B的指针对象调用,调用B的虚函数;

  • 但是,答案并不是D

对于重写,对于B重写A的虚函数来说,对于派生类B来说,重写可以理解为重写的本质是重写实现,这也是为什么重写的虚函数前可以不写virtual,也就是派生类B的重写的虚函数func()是由基类A的声明和B重写虚函数的内容构成的:

  • 也就是绝不重新定义继承而来的缺省参数值

在编程中,如果你在子类中重写了一个继承自父类的方法,并且这个方法有参数,那么在子类中重写的方法不应该改变父类方法的默认参数值。这是因为默认参数值是在函数定义时确定的,如果在子类中改变默认参数值,可能会导致调用时的行为与预期不符。

扩展题:

A.1 0

B.0 1

C.0 1 2

D.2 1 0

E.不可预期

F. 以上都不对

#include<iostream>
using namespace std;
class A
{
public:
    A() :m_iVal(0) { test(); }
    virtual void func() { std::cout << m_iVal << " "; }
    void test() { func(); }
public:
    int m_iVal;
};
class B : public A
{
public:
    B() { test(); }
    virtual void func()
    {
        ++m_iVal;
        std::cout << m_iVal << " ";
    }
};
int main(int argc, char* argv[])
{
    A* p = new B;
    p->test();
    return 0;
}

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

协变: 

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤。
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,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。(这也是构成隐藏的解释)
下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
基类的析构函数建议+virtual
注意:这个问题⾯试中经常考察,⼤家⼀定要结合类似下⾯的样例才能讲清楚,为什么基类中的析构 函数建议设计为虚函数。
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
	A* p1 = new A;//指向父类
	A* p2 = new B;//指向子类
	delete p1;
	delete p2;
	return 0;
}

这里的带来的问题原因是:一个父类的指针有可能指向父类也有可能指向子类

在对于释放操作时,delete p1的时候没有问题,因为调用的是父类的析构函数;

在delete p2时,我们期望的是调用子类的析构函数,假设我们A的析构函数没有virtual修饰,也就是A与B的析构函数不构成多态,这时候,delete p2会因为p2是A*类型的指针而去调用A的析构,就会导致B类当中的动态开辟的_p数组没有被释放,导致内存泄漏;

当A与B的析构函数满足多态的条件,就可以实现指向谁去析构谁;

据上图:其实也是不需要显示调用父类的析构的,因为他调不到,运行时会跳过 

override和final关键字:

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

override的使用:

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

final的使用:

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

类比:一个类用final修饰就是最终类,无法被继承,以上是为了不能被重写

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

纯虚函数和抽象类: 

在虚函数的后⾯写上 =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;
}

多态的原理:

虚函数表指针:(虚表指针)

以下是一个小程序,我们可以通过调试窗口来进行观察理解:

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

所以,以上代码的类的大小应该为12

多态的实现:

我们可以使用更加丰富的代码来理解: 

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

通过代码的调试窗口观察:我们可以每个对象里边都有一个虚函数表指针:

说明:Person有自己的虚函数表指针,Student继承了Person,也有了自己的虚函数表指针,Soldier继承了Person,也有了自己的虚函数表指针,子类就是重写了父类的虚函数表,这是重写的其中意义

那我们是如何实现指向谁调用谁的呢? 

所有的代码语句都会转化成汇编指令,编译在检查语法时,会先判断该程序代码满不满足多态的要求,如果满足多态,就在这段指令的时候,就把这段指令变成从虚函数表指针指向的虚函数表中去找对应虚函数的地址,从而找到对应的虚函数;

然而继承关系是具有切片行为的,所以ptr无论是指向Person·Student·Soldier哪种场景,从内存的角度,都是看到的是对应Student·Soldier切片出的Person对象,所以无论是哪种场景,只管是去找对应的Person对象,去找指向虚函数表的指针,找到对应的虚函数表里找到虚函数指针调用;

总之:指向谁,调用谁:指向哪个对象,运行时,到指向对象的虚函数表中找到对应虚函数的地址,进行调用。

我们可以从汇编角度来观察满足与不满足多态的汇编语言的区别:

可以看出,如果不满足多态,就跟指向的对象没有关系,这时,ptr只是单单的Person指针,调用Person的BuyTicket()函数,不管ptr指向谁,都只会调用Person的函数,这是在汇编时就已经确定了;

而满足多态, 中间的代码指令我们可以理解为是指针指向虚函数表,找到虚函数的地址,将虚函数的地址给eax,在运行的时候再去调用虚函数,这是在运行时确定的;

动态绑定与静态绑定:

(根据上图)

  • 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  • 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
// 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)

虚函数表:

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;
}
  • 基类对象的虚函数表中存放基类所有虚函数的地址。

  • 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
  • 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
  • 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。
  • 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
  • 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
  • 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)
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;
}

C++中的多态(Polymorphism)是指在父类和子类之间的相互转换,以及在不同对象之间的相互转换。 C++中的多态性有两种:静态多态和动态多态。 1. 静态多态 静态多态是指在编译时就已经确定了函数的调用,也称为编译时多态C++中实现静态多态的方式主要有函数重载和运算符重载。 函数重载是指在同一作用域内定义多个同名函数,但它们的参数列表不同。编译器根据传递给函数的参数类型和数量来确定调用哪个函数。例如: ```c++ void print(int num) { std::cout << "This is an integer: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } int main() { int a = 10; double b = 3.14; print(a); // 调用第一个print函数 print(b); // 调用第二个print函数 } ``` 运算符重载是指对C++中的运算符进行重新定义,使其能够用于自定义的数据类型。例如: ```c++ class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} Complex operator+(const Complex& other) const { return Complex(m_real + other.m_real, m_imag + other.m_imag); } private: double m_real; double m_imag; }; int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // 调用Complex类中重载的+运算符 } ``` 2. 动态多态 动态多态是指在运行时根据对象的实际类型来确定调用哪个函数,也称为运行时多态C++中实现动态多态的方式主要有虚函数和纯虚函数。 虚函数是在父类中定义的可以被子类重写的函数,使用virtual关键字声明。当一个对象的指针或引用指向一个子类对象时,调用虚函数时会根据实际的对象类型来确定调用哪个函数。例如: ```c++ class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ``` 纯虚函数是在父类中定义的没有实现的虚函数,使用纯虚函数声明(如virtual void func() = 0;)。父类中包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类来派生子类。子类必须实现父类的纯虚函数才能实例化。例如: ```c++ class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值