【C++自学笔记】详细解读——C++面向对象之多态

一、多态的定义及实现

多态:通俗来说,就是多种形态,具体点就是完成某个行为,当不同的对象去完成时会产生出不同的状态;

1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为。

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

  • 必须通过基类的指针或者引用调用虚函数;
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

2、虚函数(被关键字 virtual 修饰的类成员函数被称为虚函数)

class Person{
public:
    virtual void Test(){
        cout << "Hello World" << endl;
    }
};

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

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

class Person {
public:
	virtual void BuyTicket() {
		cout << "全" << endl;
	}
};
class Studetn :public Person {
public:
	virtual void BuyTicket() {
		cout << "半" << endl;
	}
};
void Useing(Person& p) {
	p.BuyTicket();
}
void Test() {
	Person PS;
	Studetn ST;
	
	Useing(PS);
	Useing(ST);
}

虚函数重写存在两个列外:

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

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

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

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

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

class Preson {
public:
	virtual ~Preson() {
		cout << "~Preson" << endl;
	}
};
class Student :public Preson {
public:
	virtual ~Student() {
		cout << "~Student" << endl;
	}
};
void Test() {
	Preson* p1 = new Preson;
	Preson* p2 = new Student;

	delete p1;
	delete p2;
}

3、C++11 override 和 final

C++对函数重写要很严格,但是如果在编写程序的过程中出现了比如把函数名写错的这种错误,在编译期间是不会有提示的,只会时在程序的运行过程中出错。所以C++11引入了 override 和 final 这两个关键字进行检验,从而解决这个问题。

1、final:修饰虚函数,表示该虚函数不能再被继承

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

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

class Car {
public:
	virtual void Drive()  {

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

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

 

【C++自学笔记】详细解读——C++面向对象之多态_第1张图片

二、抽象类

1、抽象类的概念

再虚函数的后面写上 =0,则这个函数为纯虚函数;

包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象!!派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能 实例化出对象,纯虚函数规范了派生类必须重写,另外春旭函数更加体现除了接口继承。

class Car {
public:
	virtual void Drive() = 0;
};
class Benz :public Car {
public:
	virtual void Drive() {
		cout << "nice" << endl;
	}
};
class BMW :public Car {
public:
	virtual void Drive() {
		cout << "using" << endl;
	}
};
void Test() {
	Car* pBenz = new Benz;
	pBenz->Drive();

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


	delete pBenz;
	delete pBMW;
}

2、接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的时函数的实现。

虚函数的继承是一种接口继承,派生类继承的时基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

所以如果不是实现多态,就不要函数定义为虚函数。

三、多态的原理

1、虚函数表

看一段代码,求sizeof(Base)等于多少? ==>> 答案是8(VS2019 Debug x86)

class Base {
public:
	virtual void Fun1() {
		cout << "Fun1" << endl;
	}
private:
	int _b = 1;
};
void Test(){
	Base b;
}

【C++自学笔记】详细解读——C++面向对象之多态_第2张图片

通过测试 b 对象是 8 bytes,除了 _b 成员,还多了一个 _vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个和平台有关),对象中的这个指针类型的 _vfptr 叫做虚函数表指针(v表示virtual,f代表function)。

一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。

这个表里到底放了什么?看一段代码:

class Base {
public:
	virtual void Fun1() {
		cout << "Fun1" << endl;
	}
	virtual void Fun2() {
		cout << "Fun2" << endl;
	}
	virtual void Fun3() {
		cout << "Fun3" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base {
public:
	virtual void Fun1() {
		cout << "Derive::Fun1" << endl;
	}
private:
	int _d = 2;
};
void Test(){
	/*int i = sizeof(Base);
	cout << i << endl;*/
	Base b;
	Derive d;
}

【C++自学笔记】详细解读——C++面向对象之多态_第3张图片

通过观察,可以得到以下几点:

  1. 派生类对象d中也有一个虚标指针,d对象由两部分组成,一部分是父类继承下来的成员和虚表指针,存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,Fun1完成了重写,所以d的虚表中存的是重写的 Deriver::Fun1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法;
  3. Fun2继承下来后是虚函数,所以放进了虚表,Fun3也继承下来了,但不是虚函数,所以不会放进虚表;
  4. 虚函数表本质是一个存虚函数的指针数组,这个数组最后面放了一个 nullptr;
  5. 派生类的虚表生成:a.先将基类中的虚标内容拷贝一份到派生类虚表中;b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;c.派生类自己新增加的虚函数按其所在派生类中的声明次序增加到派生类虚表的最后;
  6. 虚表中存的是虚函数指针,不是虚函数,虚函数和普通函数一样存在于代码段,只是他们的指针存在于虚表中,对象中存的不是虚表,是虚表指针。虚表在VS平台下是存在于代码段;

2、多态的原理

还是以调试代码的形式来理解,我个人认为最可靠,也更加容易理解

class Preson {
public:
	virtual void BuyTicket() {
		cout << "全" << endl;
	}
};
class student :public Preson {
public:
	virtual void BuyTicket() {
		cout << "半" << endl;
	}
};
void Func(Preson& p) {
	p.BuyTicket();
}
void Test(){
	Preson Mike;
	Func(Mike);

	student John;
	Func(John);
}

进入调试界面,实时监控 Mike 和 John 这两个对象:

【C++自学笔记】详细解读——C++面向对象之多态_第4张图片

可以看到,Mike对象实际在运行过程中调用的是 Preson::BuyTicket,而 John对象在实际运行过程中调用的是 重写的Student::BuyTicket。

结论:

  • 可以得出两个首先多态必须满足的条件:虚函数覆盖 和 对象的指针或引用调用虚函数;
  • 满足多态以后的函数的调用,不是在编译的时候决定的,而是在程序运行起来以后到对象中去找的;

3、动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态;(比如函数重载)

动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态;

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

1、单继承中的虚函数表

class Base {
public:
	virtual void Fun1() {
		cout << "Base::Fun1" << endl;
	}
	virtual void Fun2() {
		cout << "Base::Fun2" << endl;
	}
private:
	int a;
};
class Derive :public Base {
public:
	virtual void Fun1() {
		cout << "Derive::Fun1" << endl;
	}
	virtual void Fun3() {
		cout << "Derive::Fun3" << endl;
	}
	virtual void Fun4() {
		cout << "Derive::Fun4" << endl;
	}
private:
	int b;
};

void Test() {
	Base b;
	Derive d;

}

调试窗口:

【C++自学笔记】详细解读——C++面向对象之多态_第5张图片 

在派生类虚函数表指针_vfptr中没有出现 Fun3 和 Fun4,是被编译器隐藏了,所以我们可以打印出所有虚函数的地址进行查看:

【C++自学笔记】详细解读——C++面向对象之多态_第6张图片

2、多继承中的虚函数表

class Base1 {
public:
	virtual void Fun1() {
		cout << "Base1::Fun1" << endl;
	}
	virtual void Fun2() {
		cout << "Base1::Fun2" << endl;
	}
private:
	int b1;
};
class Base2 {
public:
	virtual void Fun1() {
		cout << "Base2::Fun1" << endl;
	}
	virtual void Fun2() {
		cout << "Base2::Fun2" << endl;
	}
private:
	int b2;
};
class Derive :public Base1, public Base2 {
public:
	virtual void Fun1() {
		cout << "Derive::Fun1" << endl;
	}
	virtual void Fun2() {
		cout << "Derive::Fun3" << endl;
	}
private:
	int d1;
};
void Test() {
	Derive d;

}

调试:

【C++自学笔记】详细解读——C++面向对象之多态_第7张图片

注意:多继承多态的派生类中没有重写的虚函数放在第一个继承基类部分的虚函数表中;

 

 

 

你可能感兴趣的:(【C/C++】,面向对象三大特性之多态,详细理解多态)