Bootstrap

【C++】多态

目录

1、多态的概念

2、多态的定义与实现

2.1 虚函数

2.2 虚函数的重写(覆盖)

2.2.1 重写的定义

2.2.2 两类特殊的重写

2.3 多态的构成条件

2.4 重载、重写(覆盖)、隐藏(重定义)的区别

2.5 C++11 final和override

2.5.1 final

2.5.2 override

3、抽象类

4、多态的原理

4.1 虚函数表

4.2 动态绑定与静态绑定


1、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是七五折买票;军人买票时是优先买票

在举个例子:动物这个类斗鱼叫这个接口,但是不同的动物调用时结果是不同的

面向对象就是在模拟现实世界的行为,多态就是现实世界的一些模型中需要的一种形态

2、多态的定义与实现

2.1 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数

注意:virtual只能修饰非静态成员函数

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

2.2 虚函数的重写(覆盖)

2.2.1 重写的定义

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

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};

这里看起来与隐藏类似,但是因为基类的成员函数BuyTicket经过virtual修饰变成了虚函数,所以就不叫作隐藏,而叫做重写了。注意,基类的BuyTicket一定要用virtual修饰,否则就不是虚函数了,子类的重写的BuyTicket可以加virtual,也可以不加virtual,为了规范,加上会比较好

2.2.2 两类特殊的重写

1. 协变(虚类与派生类的返回值可以不同)

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

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

这是构成重写的,当然,也不一定要是A、B类

class A {};
class B : public A {};
class Person {
public:
	virtual Person* f() { return new Person; }
};
class Student : public Person {
public:
	virtual Student* f() { return new Student; }
};

这样子也是构成重写的,只要基类返回的那个指针或引用的类型与子类返回的那个指针或引用的类型构成父子类关系即可

2. 析构函数的重写(基类与派生类析构函数的名字不同)

首先,我们要知道为什么要对析构函数写成虚函数然后进行重写呢?

当我们不写成虚函数时

class Person {
public:
	~Person() {
		cout << "~Person" << endl;
	}
};
class Student : public Person {
public:
	~Student() {
		cout << "~Student" << endl;
	}
};
int main()
{
	Person p1;
	Student s;
	return 0;
}

 结果时,是正常的

但是若是这样

int main()
{
	Person* p1 = new Student;
	delete p1;
	return 0;
}

结果是 ,为什么p1指向的是子类对象,但是调用的却是父类的析构函数呢?并且若是此时子类有开辟空间,则会造成内存泄漏

所以,需要实现多态,实现多态后,p1指向的是子类空间,则会调用子类的析构函数

class Person {
public:
	virtual ~Person() {
		cout << "~Person" << endl;
	}
};
class Student : public Person {
public:
	virtual ~Student() {
		cout << "~Student" << endl;
	}
};
int main()
{
	Person* p1 = new Student;
	delete p1;
	return 0;
}

此时结果就正常了 

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

2.3 多态的构成条件

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

多态有两个条件,且这两个条件缺一不可

1. 必须通过基类的指针或引用调用虚函数

2. 被调用的函数必须是虚函数,且子类必须对基类的虚函数进行重写

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func1(Person& p)
{
	p.BuyTicket();
}
void Func2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func1(ps);
	Func1(st);
	Func2(&ps);
	Func2(&st);
	return 0;
}

结果是

与谁调用虚函数无关,即与p.BuyTicket和p->BuyTicket无关,与实际对象有关

多态就是不同类型的对象,会去调用自己这个类型的虚函数

注意,不能指定BuyTicket的域

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func1(Person& p)
{
	p.Person::BuyTicket();
}
void Func2(Person* p)
{
	p->Person::BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func1(ps);
	Func1(st);
	Func2(&ps);
	Func2(&st);
	return 0;
}

结果是

多态不一定是发生在父子类之间,也可以发生在多个子类之间

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier :public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
void Func1(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func1(ps);
	Func1(st);
	Func1(sd);
	return 0;
}

结果是

2.4 重载、重写(覆盖)、隐藏(重定义)的区别

我们以上面的Person类和Student类为例,会发现

int main()
{
	Person ps;
	Student st;
	st.BuyTicket();
	st.Person::BuyTicket();
	return 0;
}

结果是

这不是正好满足隐藏吗?所以,重写就是一种特殊的隐藏

总结,多态调用:看指向对象的类型,指向谁调用谁的虚函数

           普通调用:看调用对象的类型,调用调用对象的函数 

2.5 C++11 final和override

2.5.1 final

修饰虚函数,表示该虚函数不能再被重写

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

此时就会报错

另外,final还可以来设计一个不想被继承的类,称为最终内

class A final
{};
class b :public A
{};

此时会报错

当然,若是不使用final设计一个类,同样也可以让一个类不能被继承,就是将这个类的构造函数设置成私有

2.5.2 override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
 

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

3、抽象类

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

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

所以。实践中,一个类型在现实中没有实体对象,不想实例化出对象,设计成抽象类

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

做一个题,下面程序的输出结果是什么?

答案是B->1

首先,B是A的子类,并且func完成了重写,注意,虽然func在A类和B类中缺省值不同,但是看的是类型,不是缺省值,所以是构成重写的。指针p的类型是B,当p->test()时,在B类中找不到test函数,所以会到父类A中去找,这其中实际上是用子类对象中的那个父类成员去调用父类的test()函数,即A*->test,所以是构成多态的。构成多态则于谁调用func无关,看的是原来对象是什么,所以调用的是B的func.重写虚函数时,实际上是重写了虚函数的实现,即构成多态时,时使用几台的接口去调用子类的实现

 若将子类改成private,效果是一样的,因为父类中仍然是public,不管子类

到这,再以动物的叫声来实现一个多态

class Animal
{
public:
	virtual void sound() const = 0;
};
class Cat :public Animal
{
public:
	virtual void sound() const
	{
		cout << "喵喵" << endl;
	}
};
class Dog :public Animal
{
public:
	virtual void sound() const
	{
		cout << "汪汪" << endl;
	}
};
void AnimalSound(const Animal& anm)
{
	anm.sound();
}
int main()
{
	AnimalSound(Cat());
	AnimalSound(Dog());
	return 0;
}

 结果是

4、多态的原理

4.1 虚函数表

首先来看一道笔试题,sizeof(Base)是多少?

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

会发现此时Base的大小是16字节,并且Base类的对象中除了成员变量_b外,还有_vfptr

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

实际上,这个_vfptr就是虚函数表,用来存放虚函数的地址,注意只有虚函数才会放入。编译完成之后,函数被编译成指令,第一句指令的地址就是函数的地址,这些指令仍然是放在代码段的,即虚函数和普通函数编译成的指令都是放在代码段的,只是虚函数还会把地址放进虚函数表中

可以打印出这些函数的地址来看看,但是注意,不能直接cout<<&Base::Func1<<endl这样打印,因为ostream的operator<<中,对函数的地址进行了重载,所以只能使用printf来打印函数的地址,与前面提到过的char*类似

int main()
{
	printf("%p\n", &Base::Func1);
	printf("%p\n", &Base::Func2);
	printf("%p\n", &Base::Func3);
	return 0;
}

结果是 ,会发现这几个函数的地址很近,所以都是放在代码段的

所以定义多了虚函数,会使这个类的对象变大。虚函数是为例实现多态,若不需要多态,就没必要将函数定义为虚函数

通过观察测试我们发现b对象除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,虚函数表实际上就是一个函数指针数组。那么派生类中这个表放了些什么呢?我们接着往下分析
 

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void Func1() // 对Func1进行重写
	{
		cout << "Derive::Func1" << endl;
	}
private:
	int _d = 2;
};
void Func1(Base* p)
{
	p->Func1();
	p->Func3();
}
// Func1放进虚表了,Func3没有放进虚表,那么放进虚表和没放进虚表有什么区别呢?
int main()
{
	Func1(new Base);
	Func1(new Derive);
	return 0;
}

结果是,因为Fun3没有放进虚表,所以Base和Derive对象都是调用Base类中的Fun3 ,而Func1放入了虚表,Base类和Derive类对象调用的就不是一个,Base对象调用的是Base类中的那个,Derive对象调用的是Derive类中的那个

实际上,在父类对象的虚函数表中存放的是虚函数的地址,而子类对象的虚函数表中,存放的是重写的虚函数和父类中没有重写的虚函数

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void Func1() // 对Func1进行重写
	{
		cout << "Derive::Func1" << endl;
	}
private:
	int _d = 2;
};
void Func1(Base* p)
{
	p->Func1();
	p->Func3();
}
// Func1放进虚表了,Func3没有放进虚表,那么放进虚表和没放进虚表有什么区别呢?
int main()
{
	Base b1;
	Derive d1;
	Func1(new Base);
	Func1(new Derive);
	return 0;
}

通过调试也可以很好的观察出来

会发现,对象b1和对象d1的虚表中有一个一样,有一个不一样

现在,我们知道了虚函数表中都有了什么,因此,可以来了解多态具体是怎样发生的了

即需要看这段代码是如何运行起来的

void Func1(Base* p)
{
	p->Func1();
	p->Func3();
}

当传过来的p是Base类型的,则取Base类型的虚表中寻找对应的虚函数。当传过来的p是Derive类型的,则会进行切割,因为Derive是Base的子类,所以会切割出Derive中Base的那一部分,所以这个指针p仍然是指向一个Base类型,只是这个Base类型是从Derive中切割出来的,所以此时的虚表是子类的虚表

4.2 动态绑定与静态绑定

动态绑定/运行时绑定:虚函数是动态绑定/运行时绑定的,即运行时去指向的对象的虚表中找到函数的地址

静态绑定/编译时绑定:普通函数是静态绑定/编译时绑定的,即若有函数定义,则编译时直接用地址,若只有声明,则等到链接时去符号表中寻找,与指向的对象没有关系

找到函数的地址以后就可以正常调用了

满足多态是动态,不满足多态是静态

若指定了类域,则编译器不会识别为多态

void Func1(Base* p)
{
	p->Person::Func1();
	p->Func3();
}

此时有一个问题,虚表的指针放在对象中,虚函数放在代码段,那虚表放在哪里呢?

方法:虚表只可能在4个区域,栈、堆、常量区(代码段)、静态区,所以我们只需要定义出这4个类型的变量,在根据虚表的地址来判断与那个变量的地址最接近,即可判断出虚表在哪里

然后现在又有一个问题,就是怎么去打印出一个对象的虚函数表的地址呢?

因为在64位下一个指针的大小是8字节,所以这个虚函数表的地址就是这个对象的前8个字节,而double就刚好是8个字节,但不能将Base类对象直接强转成double类型的对象,因为只有一些有关联的类型才能支持转换,像int和double和char,int和int*,指针之间也是可以互相转换的,很明显Base和double没有关联,所以只能通过指针来进行转换

当然也可以通过调试来查看虚表的地址

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1); // p1是栈上的,但是p1的地址是堆上的
	printf("常量区(代码段):%p\n", p2);
	Base b;
	printf("%p\n", *((double*)&b));
	return 0;
}

结果是

可以发现,虚表是放在常量区的,因为同一类型是共享虚表的

int main()
{
	Base b1;
	Base b2;
	Base b3;
	return 0;
}

会发现,虚表的地址都是相同的

看一个题

选C

先继承的对象在前面,切割之后就变成了

;