C++多态(万字详!!!)

文章目录

  • 多态的概念
  • 多态的定义及实现
    • 多态的构成条件
    • 虚函数
    • 虚函数的重写
    • 虚函数重写的两个例外:
    • C++11 override和final
    • 重载、覆盖(重写)、隐藏(重定义)的对比
  • 抽象类
    • 概念
    • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
    • 多态的原理
    • 动态绑定和静态绑定
  • 单继承和多继承关系的虚函数表
    • 单继承中的虚函数表
    • 多继承中的虚函数表

多态的概念

多态、多态、就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

例如:
在平时我们买票的时候,学生买票是半价、而其他人成年人买票是全价

多态的定义及实现

多态的构成条件

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

在继承中要构成多态两个条件:

  • 父类的指针或者引用去调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对父类的虚函数进行重写。
class Person
{
public:
	//被virtual修饰的类成员函数、虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student : public Person
{
public:
	//被virtual修饰的类成员函数、虚函数
	void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
void Func(Person& ptr)
{
	ptr.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	
	Func(p);
	Func(s);
	
	return 0;
}

虚函数

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

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

只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual。

虚函数的virtual和虚继承的virtual是同一个关键字,但是它们之间没有关系。虚函数这里的virtual是为了实现多态,虚继承的virtual而是为了解决菱形继承的数据冗余和二义性。

虚函数的重写

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

我们用Student子类重写了父类Person的虚函数。

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

此时我们就可以通过父类Person的指针或者引用调用虚函数BuyTicket(),此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

//父类的引用调用虚函数
void Func(Person& p)
{
	p.BuyTicket();
}
//父类的指针调用虚函数
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;   //普通人
	Student s;  //学生
	//引用
	Func(p);  //买票-全价
	Func(s);  //买票-半价
	//指针
	Func(&p);  //买票-全价
	Func(&s);  //买票-半价
	return 0;
}

虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值类型不同)

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

class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "正常排队-全价买票" << endl;
		return this;
	}
};
class Student : public Person
{
public:
	virtual Student* BuyTicket()
	{
		cout << "正常排队-半价买票" << endl;
		return this;
	}
};
class Soldier : public Person
{
public:
	virtual Soldier* BuyTicket()
	{
		cout << "优先排队-全价买票" << endl;
		return this;
	}
};
int main() {
	Person* p = new Person;
	Student st;
	Soldier so;
	//通过父类Person的指针调用虚函数fun,
	//父类指针若指向的是父类对象,则调用父类的虚函数,
	//父类指针若指向的是子类对象,则调用子类的虚函数。
	p->BuyTicket();  // Output: "正常排队-全价买票"
	
	p = &st;
	p->BuyTicket();  // Output: "正常排队-半价买票"

	p = &so;
	p->BuyTicket();  // Output: "优先排队-全价买票"
	return 0;
}

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

在下面这种情况下,如果delete p1, p2,会发生什么捏???

class Person{};
class Student : public Person{};
int main()
{
	//分别new一个父类对象和子类对象,并均用父类指针指向它们
	Person* p1 = new Person;
	Person* p2 = new Student;

	//使用delete调用析构函数并释放对象空间
	delete p1;
	delete p2;
	return 0;
}

答案是:—>父类和子类的析构函数没有构成重写就可能会导致内存泄漏,因为此时delete p1和delete p2都是调用的父类的析构函数,而我们所期望的是p1调用父类的析构函数,p2调用子类的析构函数。此时只有父类和子类的析构函数构成了重写,才能使得delete按照我们的预期进行析构函数的调用,才能实现多态。因此,为了避免出现这种情况,比较建议将父类的析构函数定义为虚函数。

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

class Person
{
public:
	//~Person()      //构成重写
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	//~Student()      //构成重写
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person p;
	Student s;
	
	Person* p1 = new Person;	//new
	Person* p2 = new Student;	//new
	
	delete p1;//p1->destructor() + operator delete(p1)
	delete p2;//p2->destructor() + operator delete(p2)

	return 0;
}

C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

  • final:修饰虚函数,表示该虚函数不能再被重写
    父类Car的虚函数Drive被final修饰后就不能再被重写了,子类若是重写了父类的Drive函数则编译报错。
class Car
{
public:
	//final修饰该虚函数,则表面该虚函数不能被重写
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() {cout << "Benz-舒适" << endl;}
};

C++多态(万字详!!!)_第1张图片

  • override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
    子类Benz虚函数Drive被override修饰,编译时就会检查子类的这两个Drive函数是否重写了父类的虚函数,如果没有则会编译报错。
class Car 
{
public:
	virtual void Drive(int x) {}
};
class Benz :public Car 
{
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

C++多态(万字详!!!)_第2张图片

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

C++多态(万字详!!!)_第3张图片

抽象类

概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象

#include 
using namespace std;
//抽象类(接口类)
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
int main()
{
	Car c; //抽象类不能实例化出对象,error
	return 0;
}

C++多态(万字详!!!)_第4张图片

派生类继承后也不能实例化出对象只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

#include 
using namespace std;
//抽象类(接口类)
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
//派生类
class Benz : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
//派生类
class BMV : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "BMV-操控" << endl;
	}
};
int main()
{
	//派生类重写了纯虚函数,可以实例化出对象
	Benz b1;
	BMV b2;
	//不同对象用基类指针调用Drive函数,完成不同的行为
	Car* p1 = &b1;
	Car* p2 = &b2;
	p1->Drive();  //Output: "Benz-舒适"
	p2->Drive();  //Output: "BMV-操控"
	return 0;
}

接口继承和实现继承

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

多态的原理

虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
	cout << "Func1()" << endl;
}
private:
	int _b = 1;
};

通过观察测试,我们发现Base类实例化的对象b的大小是8个字节。
b对象当中除了_b成员外,实际上还有一个_vfptr
C++多态(万字详!!!)_第5张图片
对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

虚函数表中有什么东西呢?

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
   //	//虚函数
   virtual void Func1()
   {
   	cout << "Base::Func1()" << endl;
   }
   //	//虚函数
   virtual void Func2()
   {
   	cout << "Base::Func2()" << endl;
   }
   //普通成员函数
   void Func3()
   {
   	cout << "Base::Func3()" << endl;
   }
private:
   int _b = 1;
};
class Derive : public Base
{
public:
   //虚函数
   virtual void Func1()
   {
   	cout << "Derive::Func1()" << endl;
   }
private:
   int _d = 2;
};
int main()
{
   Base b;
   Derive d;
   
   return 0;
}

C++多态(万字详!!!)_第6张图片
我们发现了

  1. 实际上虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。
  2. 子类对父类的虚函数进行重写,因此子类对象d的虚表当中存储的是父类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

那么虚函数表和虚函数存在哪里??
虚函数表是在构造函数初始化列表阶段进行初始化的,注意虚函数表当中存的是虚函数的地址而不是虚函数
虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。
验证虚函数表存在的位置

class Base 
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

//
//静态区:0057C3FC
//栈:00AFFB80
//堆 : 0105EF58
//代码段 : 00579B70
//虚表 : 00579B34
//虚函数地址 : 0057146A
//普通函数地址 : 005713DE
int main()
{
	Base b1;
	Base b2;

	static int a = 0;
	int b = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", &b);
	printf("堆:%p\n", p1);
	printf("代码段:%p\n", p2);
	printf("虚表:%p\n", *((int*)&b1));
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", func);

	return 0;
}

从上面的结果我们发现虚函数表地址与代码段的地址非常接近,由此我们可以得出虚表实际上是存在代码段的。

多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
C++多态(万字详!!!)_第7张图片

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}
  1. 当p指向对象mike时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket;
  2. 当p指向对象johnson时,p->BuyTicket在johson的虚表中
    找到虚函数是Student::BuyTicket;
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。多态
    C++多态(万字详!!!)_第8张图片
    此时我们再次回顾一下构成多态的两个条件
  • 父类的指针或者引用去调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对父类的虚函数进行重写。
    必须完成虚函数的重写是因为我们需要完成子类虚表当中虚函数地址的覆盖,那为什么必须使用父类的指针或者引用去调用虚函数呢?为什么使用父类对象去调用虚函数达不到多态的效果呢?
  • 使用父类指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。
  • 使用父类对象时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是一样的,最终调用的函数也是一样的,也就无法构成多态。

动态绑定和静态绑定

静态绑定在编译时确定的,动态绑定在运行时确定的。
静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载;
动态绑定:又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态;

//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//下面方式调用BuyTicket函数,则不构成多态,函数的调用是在编译时确定的
int main()
{
	Student Johnson;
	Person p = Johnson; //不构成多态
	p.BuyTicket();
	return 0;
}

//下面方式调用BuyTicket函数,则构成多态,函数的调用是在运行时确定的
//在运行时,先到指定对象的虚表中找到要调用的虚函数,然后才能进行函数的调用。
int main()
{
	Student Johnson;
	Person& p = Johnson; //构成多态
	p.BuyTicket();
	return 0;
}

单继承和多继承关系的虚函数表

单继承中的虚函数表

class Base 
{
public :
	virtual void func1() { cout<<"Base::func1" <<endl;}
	virtual void func2() {cout<<"Base::func2" <<endl;}
private :
	int a;
};
class Derive :public Base 
{
public :
	virtual void func1() {cout<<"Derive::func1" <<endl;}
	virtual void func3() {cout<<"Derive::func3" <<endl;}
	virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
	int b;
};

下图中的监视窗口中我们看不见func3和func4。这里是编译器的监视窗口故意隐藏了
C++多态(万字详!!!)_第9张图片

看到派生类对象完整的虚表有两个方法
1、使用内存监视窗口
C++多态(万字详!!!)_第10张图片

2、使用代码打印虚表内容


typedef void (*VFUNC)();//虚函数指针类型重命名
//打印虚表地址
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
		//(*f)();
	}
	printf("\n");
}
int main()
{
	Base b;
	PrintVFT((VFUNC*)(*(int*)&b)); //打印基类对象b的虚表地址
	Derive d;
	PrintVFT((VFUNC*)(*(int*)&d)); //打印派生类对象d的虚表地址
	return 0;
}

C++多态(万字详!!!)_第11张图片

多继承中的虚函数表

class Base1 
{
public:
	virtual void func1() {cout << "Base1::func1" << endl;}
	virtual void func2() {cout << "Base1::func2" << endl;}
private:
	int b1;
};
class Base2 
{
public:
	virtual void func1() {cout << "Base2::func1" << endl;}
	virtual void func2() {cout << "Base2::func2" << endl;}
private:
	int b2;
};
class Derive : public Base1, public Base2 
{
public:
	virtual void func1() {cout << "Derive::func1" << endl;}
	virtual void func3() {cout << "Derive::func3" << endl;}
private:
	int d1;
};

typedef void (*VFUNC)();//虚函数指针类型重命名
void PrintVFT(VFUNC* a)
{
	for (size_t i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
		//(*f)();
	}
	printf("\n");
}

int main()
{
	Derive d;
	PrintVFT((VFUNC*)(*(int*)&d));
	PrintVFT((VFUNC*)(*(int*)((Base1*)&d + 1)));
	
	return 0;
}

C++多态(万字详!!!)_第12张图片
在多继承关系当中,派生类的虚表生成过程如下:

  • 分别继承各个基类的虚表内容到派生类的各个虚表当中。
  • 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
  • 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。
    C++多态(万字详!!!)_第13张图片

你可能感兴趣的:(C/C++,c++,开发语言)