C++中的多态
创始人
2024-05-22 20:11:25
0

多态的概念

态换句话来说就是多种形态,具体点就是不同的对象去完成某一个行为时会产生不同的状态。比如买票这个行为,成年人去买是全价,未成年人去买是半价,而军人去买则是优先购票。

多态的定义及实现

构成多态的条件

首先来看看构成多态的条件:

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

那么在继承中,需要以下条件才能构成多态:

①被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写/覆盖。

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

class Person
{
public://虚函数virtual void BuyTick() { cout << "Person--买票-全价" << endl; }
};//虚函数的重写/覆盖
//重写的条件是:三同,即函数名、参数、返回值都相同class Student :public Person
{virtual void BuyTick() { cout << "Student--买票-半价" << endl; }
};class Soldier :public Person
{virtual void BuyTick() { cout << "Soldier--买票-优先" << endl; }
};//构成多态:1.虚函数重写2.基类的指针或引用去调用虚函数
void Func(Person& p)//也可以是Perosn* p,但不可以Person p
{p.BuyTick();
}int main()
{Person pn; Student st;Soldier sd;Func(pn);Func(st);Func(sd);return 0;
}

结果显示:

结论:

对于普通调用,跟调用对象的类型有关。对于多态调用,跟指针/引用--指向的对象有关。即如果上面代码不构成多态,那么这三个结果都是"Person--买票全价",因为它调用对象的类型是Person&或Person*,然后在传入st和sd的时候,切片之后拷贝了一份给参数p。但是构成多态后,就便是与sd、st有关了。

虚函数

什么是虚函数

构成多态条件是1.虚函数重写2.基类的指针或引用去调用虚函数。那么虚函数就是被virtual修饰的类成员函数称为虚函数。

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

虚函数的重写

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

需要注意的是:

①如果需要构成重写,那么基类就必须是虚函数,即必须加上virtual。

②派生类对基类重写的虚函数,可以不加上virtual,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但一般而言最好加上去,提高代码的阅读性。

虚函数重写/覆盖的两个特殊情况

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

三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或者引用。

//协变:返回值可以不同,但是返回值必须是一个父子类关系的指针或引用
class Person
{
public:virtual Person* BuyTick() { cout << "Person--买票-全价" << endl; return this; }
};class Student :public Person
{virtual Student* BuyTick() { cout << "Student--买票-半价" << endl; return this;}
};class Soldier :public Person
{virtual Person* BuyTick() { cout << "Soldier--买票-优先" << endl; return this;}
};

只要是父子类关系的都可以,不一定是Person、Student和Soldier。

class A
{};class B:public A
{};
//协变:返回值可以不同,但是返回值必须是一个父子类关系的指针或引用
class Person
{
public:virtual A* BuyTick() { cout << "Person--买票-全价" << endl; return this; }
};class Student :public Person
{virtual B* BuyTick() { cout << "Student--买票-半价" << endl; return this;}
};class Soldier :public Person
{virtual A* BuyTick() { cout << "Soldier--买票-优先" << endl; return this;}
};

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

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

class Person
{
public:virtual ~Person()//虚析构函数{cout << "Person delete:" << _p << endl;delete[] _p;}
protected:int* _p = new int[10];
};class Student : public Person
{
public:~Student(){cout << "Student delete:" << _s << endl;delete[] _s;}
protected:int* _s = new int[20];
};int main()
{Person* ptr1 = new Person;//类型是Person*,指向的对象是Person类型Person* ptr2 = new Student;//类型是Person*,指向的对象是Student类型delete ptr1;//会调用Person的析构//因为构成多态,析构函数重写了,即使是Person*类型的,但是在调用的时候,不看类型,是看//指向的对象,指向的是Student,因此会去调用Student的析构函数//又因为Student类是Person的子类,所有会在Student类的析构函数结束后//再去调用Perosn类的析构函数delete ptr2;return 0;
}

 

析构函数重写而构成多态的作用就是,如果使用了基类的类型去创建对象,但对象指向的却是派生类,那么delete就会准确地调用析构函数。在上面的代码例子中,只有派生类Student的析构函数重写了Person的析构函数,delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

C++11 override 和 final

到这里,我们可以看到构成多态的条件比较严格,所有有时候我们会难免疏忽一下,可能是函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。因此,C++11中提供了overrid和final来帮助我们去检查是否构成重写。

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

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }//报错,不能构成重写
};

final除了可以修饰虚函数以外,还能修饰类,被修饰的类表示不能被继承。这里可以小小地总结两点,关于需要写出一个不能被继承的类。

①私有构造函数;②final修饰类

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

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

注意:final是写在不让被重写的虚函数上,overrid是写在重写了基类的虚函数的派生类的虚函数上。

最后我们来看下重载、隐藏/重定义、重写/覆盖三者的区别:

⭐重载:

①重载的函数必须是同一个作用域中。需要注意的是父子类不是同一作用域,是独立的,就算父子类中有两个看上去是重载的函数,但那不是重载,是构成隐藏!

②重载的函数必须是函数名相同。

如果函数名相同、参数相同,但是返回值不同,不构成重载。
如果函数名相同、参数不同,但是返回值,构成重载。
如果函数名相同、参数不同,但是返回值不同,构成重载。

⭐隐藏/重定义:

①两个函数必须分别在基类和派生类当中。

②函数名相同

③如果基类和派生类的两个同名函数不构成重写那就是隐藏

⭐重写/覆盖:

①两个函数必须分别在基类和派生类当中。

②三同:函数名相同、参数相同、返回值相同

③两个函数必须是虚函数

其实我们可以这样理解重写:因为重写的条件三同,并且重写,写的是函数的主体内容,这就可以理解成,如果我们用基类创建对象,然后指针/引用指向了派生类,那么,在使用这个对象去调用重写了的函数的时候,就是去基类中拿到函数的接口,再到派生类对应的虚函数的里面去指向里面的代码。

抽象类

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

//抽象类---不能实例化出对象
class Car
{
public://纯虚函数virtual void Drive() = 0;
};class BMW :public Car
{
public:virtual void Drive(){cout << "别摸我" << endl;}
};int main()
{Car c;//error  报错了,不能实例出对象BMW b;//通过纯虚函数的重写,可以实例出对象return 0;
}

接口继承和实现继承

①实现继承:

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

②接口继承:

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。这也是上面对重写的小小理解进行了一次总结。

多态的原理

有这样一份代码:

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

sizeof(Base)的结果为:12;我们来分析一下为啥是12:

首先,Base的成员变量有int类型和char类型,内存对齐后,大小为8(在32为平台4字节下)。

接着是虚表的存在,又称虚函数表,虚函数表本质上是一个函数指针数组,存放的是类中虚函数的地址。因此,指向虚函数表的是虚表指针,在32位平台下,指针的大小是4,因此一共是12个字节。

 

那么,在派生类中,这个虚函数表存放的是什么呢?我们都知道了,基类的虚函数表里面存放的是虚函数的地址,派生类重写了基类的虚函数,那么派生类的虚函数表是否也存放着跟基类同一份的虚函数地址呢?我们接下来看看:

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;char _ch;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

可以看到,派生类对象d自己也有虚表指针_vftptr,并且d对象由两部分,一部分的从父类继承下来的成员,另一部分是自己的,并且虚表指针中存放的虚函数地址也有自己的。

其实基类b对象和派生类d对象虚表是不一样的,Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。而Func2继承下来后是虚函数,所以放进了虚表,但是并没有重写,因此,基类和派生类中的虚函数表,对于Func2的地址是一模一样的!Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

总结派生类的虚表生成:

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

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

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

④虚表是存放在代码段中的。

 因此,多态的原理就是:

(这里再用Person类、Student类举例子)

当基类Person创建了对象p,p的类型是Person*或Person&,指向的对象是派生类,在调用重写的虚函数时,就会到指向的对象的类里面找虚表!从虚表中得到这个虚函数的地址,从而去调用这个函数!如果指向的对象是基类自己,那么就会到基类里面找到基类的虚表。

动态绑定与静态绑定

①静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也就是说已经确定好要调用的函数的地址了。静态绑定也称为静态多态,比如函数重载。

②动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即上面所说的,会先到虚表中找具体的函数的地址,再去调用。动态绑定也称为动态多态。

反思构成多态的条件

通过分析,我们可以好好反思一下构成多态的条件,为什么要虚函数重写,为什么要基类对象的指针或引用调用虚函数。

①为什么虚函数覆盖/重写:

因为要对派生类的虚表进行覆盖。在调用重写的函数的时候,如果指向的是派生类对象,那么就必须从这个派生类的虚表中拿到这个虚函数的地址。

②为什么要基类对象的指针或引用去调用虚函数:

首先,虚函数必须写在基类中。其次,基类的对象指针和引用,在切片的时候,指向的是派生类对象中属于基类成员的那一部分,但总体来说依然是指向派生类的,当需要调用重写的虚函数的时候,就会去基类成员那一部分中找接口,再去派生类中找定义。不是切片的话,就会自己调用自己的指定的虚函数。

如果不是指针或引用,那么在切片的时候,会将属于基类成员的那一部分拷贝给调用的基类对象,此时就不会构成多态了。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...