【C++】多态

本文详细介绍了多态的概念,包括其定义、实现机制(如虚函数、重写、C++11中的override和final),以及抽象类和接口继承。重点讲述了多态的原理,特别是动态绑定和静态绑定的区别。
摘要由CSDN通过智能技术生成

目录

一.多态的概念

二.多态的定义和实现

1.多态构成的条件

1.1多态构成条件

1.2虚函数

1.3虚函数重写

1.4 C++11 override 和 final

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

三.抽象类

1.概念

2.接口继承和实现继承

四.多态的原理

4.1原理

4.2动态绑定与静态绑定


一.多态的概念

多态通俗来说就是多种形态,做一件事不同的人去完成会产生不同的状态

二.多态的定义和实现

1.多态构成的条件

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

1.1多态构成条件

在继承中构成多态的条件:

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

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

1.2虚函数

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

class Person {
public:
 virtual void Buy() { cout << "原价" << endl;}
};

1.3虚函数重写

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

例如学生购买有的商家有教育优惠:

#include<iostream>
using namespace std;


class Person {
public:
	virtual void Buy() { cout << "原价" << endl; }
};
class Student : public Person {
public:
	virtual void Buy() { cout << "教育优惠" << endl; }
};
void Func(Person& p)
{
	p.Buy();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

结果:

注意:虚函数重写时,基类为函数添加virtual关键字,派生类不加也同样构成虚函数重载。应为派生类在继承虚函数时也把虚函数属性继承下来了。

虚函数重写的两个例外:

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

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

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

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

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

1.4 C++11 override 和 final

C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

a.final:修饰虚函数表示该虚函数不能再被重写

class Person {
public:
	virtual void Buy() final { cout << "原价" << endl; }
};
class Student : public Person {
public:
	virtual void Buy() { cout << "教育优惠" << endl; }
};

b.override:用于检查派生类虚函数是否重写了基类的某个虚函数,没重写就编译错误


class Person {
public:
	virtual void Buy()  { cout << "原价" << endl; }
};
class Student : public Person {
public:
	virtual void Buy() override { cout << "教育优惠" << endl; }
};

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

三.抽象类

1.概念

在虚函数后面加上=0,这个函数就是纯虚函数,包括纯虚函数的类叫抽象类(接口类),抽象类不能实例化出对象。就算派生类继承了基类在没重写纯虚函数时也不能实例化出对象,只有派生类重写了纯虚函数才能实例化出对象。

例如:

class Person 
{
public:
	virtual void Buy() = 0;
};
class Student : public Person 
{
public:
	virtual void Buy() override 
	{ 
		cout << "教育优惠" << endl; 
	}
};
class Teacher
{
public:
	virtual void Buy()
	{
		cout << "教育优惠" << endl;
	}
};

int main()
{

	//Person p;  纯虚函数不能实例化出对象
	Student st;
	Teacher tt;
	return 0;
}

2.接口继承和实现继承

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

四.多态的原理

4.1原理

先提一个问题:

class A
{
public:

	virtual void f()
	{
		cout<<"f()" << endl;
	}
protected:
	int _a;
};

int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

上面这段代码中sizeof(a)大小是多少?

输出结果:

答案是8个字节(x86环境)。

我们通过调试可以看到A里面除了存_a还存了一个虚表

除了_a成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。

继续看下面这个例子:

class A
{
public:

	virtual void f()
	{
		cout<<"f()" << endl;
	}
	virtual void f1()
	{
		cout << "f1()" << endl;
	}
	void f2()
	{
		cout << "f2()" << endl;
	}
private:

	int _a = 1;
};
class B : public A
{
public:

	virtual void f()
	{
		cout<< "B::f()" << endl;
	}
private:
	int _b = 2;
};

int main()
{
	A a;
	B b;
	return 0;
}

1. 派生类对象b中也有一个虚表指针,db对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

2. 基类a对象和派生类b对象虚表是不一样的,这里我们发现f完成了重写,所以b的虚表中存的是重写的b::f,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. 另外f1继承下来后是虚函数,所以放进了虚表,f2也继承下来了,但是不是虚函数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5. 总结一下派生类的虚表生成:

        a.先将基类中的虚表内容拷贝一份到派生类虚表中

        b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

        c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

用上面的代码指向的对象是a就在A的虚表里面找函数,指向b就在B的虚表里面找函数:

 我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。

所以为什么是这两个条件?

可以通过汇编来看看。

可以看到首先将引用a的前四个字节(x86环境下)给了eax

然后相当于把对象的前四个字节(虚表指针)移到edx里面

call   eax里面的指针

总结:编译器并不是在编译的时候决定调用哪个虚函数的,而是在运行的时候,去取虚表指针指向的函数,最后call这个函数。这样构成多态的。

4.2动态绑定与静态绑定

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

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

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函数 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值