【C++】面向对象编程:继承与多态的魅力
目录
继承
继承的概念
继承的定义
继承基类(父类)成员访问方式的变化
继承类模板
基类与派生类间的转换
继承中的作用域
派生类的默认成员函数
继承与友元
多继承及其菱形继承问题
多态
多态概念
多态的定义与实现
多态的构成条件
实现多态的必要条件
虚函数
基类虚函数的重写 / 覆盖
虚函数重写存在的问题
析构函数的重写
override和final关键字
重载/重写/隐藏的对比
纯虚函数和抽象类
多态原理
多态如何实现的
动态绑定与静态绑定
虚函数表
继承
假设我们设计了两个类student和teacher,两个类中都有姓名、地址、年龄和电话,在没学继承概念时,我们需要将这些属性设计到两个类中,这会使得程序很冗余,但学了继承后,只需要将这些属性设计在一个Person中,然后让student和teacher分别继承这个类就行,这样就避免了属性冗余的情况
继承的概念
继承机制是面向对象程序设计使代码可以复用的重要手段。它允许我们在原有类的特性的基础上进行扩展,增加方法和属性,这样产生新的类,称派生类。
继承的定义
//class 派生类名 :继承方式 基类名class Person{ //....};class Studen:public Person//定义继承{ //...};*************************************************************#include#includeusing namespace std;class Person{public:protected:string _name;string _address;int _age=18;string _tel;};class teacher:public Person{public:void identity(){cout << \"teacher:\" << \"void identity()\" << endl;}protected:string _title;};class student:public Person{public:void identity(){cout << \"student:\" << \"void identity()\" << endl;}protected:int _stuid;};int main(){student s;teacher t;s.identity();t.identity();return 0;}
继承的方式(与访问限定符很像)
-
public继承
-
protected继承
-
private继承
继承基类(父类)成员访问方式的变化
基类private成员在派⽣类中无论以什么方式继承都是不可见的。
基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。
实际上⾯的表格我们进行⼀下总结会发现,基类的私有成员在派⽣类都是不可见。基类的其他成员 在派生类的访问方式==Min(成员在基类的访问限定符,继承⽅式),public >protected> private。
使⽤关键字class时默认的继承方式是private,使⽤struct时默认的继承方式是public,不过最好显示的写出继承方式。
在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
继承类模板
namespace liu{//template//class vector//{};// stack和vector的关系,既符合is - a,也符合has - atemplateclass stack : public std::vector{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域,// 否则编译报错: error C3861 : “push_back”:找不到标识符// 因为stack实例化时,也实例化vector了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到//push_back(x);vector::push_back(x);//push_back(x);}void pop(){vector::pop_back();}const T& top(){return vector::back();}bool empty(){return vector::empty();}};}templatevoid Print(const Container& c){ //这里需要注意,因为这里是模板,所以需要在Container前面加typename。 //因为这是一个模板,Container是模板参数,编译器不知道Container是什么类型,它可能是一个内嵌类型,也可能是一个静态成员变量,因此,我们需要在前面加上一个typename告诉编译器Container是一个类型, typename Container::const_iterator it1=c.begin(); while(it1!=c.end()) { cout<<*it1<<endl; ++it1; } cout<<endl;}int main(){ return 0;}
基类与派生类间的转换
-
public继承的派⽣类对象可以赋值给基类的指针/基类的引用。这⾥有个形象的说法叫切片或者切割。寓意把派生 类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分
-
基类对象不能赋值给派生类对象。
-
基类的指针或者引用可以通过强制类型转换赋值给派⽣类的指针或者引用。但是必须是基类的指针 是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-TimeType Information)的dynamic_cast 来进⾏识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下
class Person{protected:string _name;string _sex;int _age;};class student:public Person{public:int _stuid;};int main(){student sobj;//赋值兼容转换,也叫切片或者切割。是一种特殊处理//派生类对象赋值给基类的指针或者引用Person* pp = &sobj;Person& rp = sobj; //当然,派生类对象也能给基类对象。 //将派生类对象中基类的部分切割出来赋值给基类对象,只不过在语义上,将派生类对象赋值给基类的指针或者引用被叫做切割和切片return 0;}
继承中的作用域
隐藏规则
-
派生类与基类都有独立的作用域
-
派生类和基类有同名成员,派生类则会屏蔽基类中的同名成员,这种情况叫做隐藏。
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
-
注意:实际中在继承体系⾥⾯最好不要定义同名的成员。容易混淆
class Person{protected:string _name=\"老王\";int _num = 111;};class studen :public Person{public:void Print(){cout <<\"姓名\"<< _name << endl;cout <<\"身份证号\"<< Person::_num << endl;cout <<\"学号\"<<_num << endl;}protected:int _num = 999;};int main(){studen s;s.Print();return 0;}
对于析构函数,派生类中的析构函数与基类中的析构函数构成隐藏关系,因为析构函数会被统一处理成destructor()。为什么会被处理成destructor(),因为在多态中的某些场景需要对析构函数进行重写,重写的条件之一就是函数名相同,所以,若析构函数前不加virtural,则基类的析构函数与派生类的析构函数会构成隐藏关系。
派生类的默认成员函数
-
派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。若有默认构造,无需显示调用,编译器会自动调用基类的构造函数
-
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
-
派生类的operator=必须要显示调⽤基类的operator=完成基类的复制,因为构成隐藏关系
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员
-
构造先构造基类对象再构造派生类,析构先析构派生类对象再析构基类
-
因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲 解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
实现⼀个不能被继承的类
- 方法一:将基类的构造函数实现为私有
class A{private:A(){}};class B :public A{public:B(){cout << \"B()\" << endl;}};
- 方法二:C++11新增了⼀个final关键字,final修饰基类,派生类就不能继承了。
class A final{public:A(){}};class B :public A{public:B(){cout << \"B()\" << endl;}};
继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员
class Student;class Person{public:friend void Display(const Person& p, const Student& s);protected:string _name;//姓名};class Student :public Person{protected:string _stuNum;//学号};void Display(const Person& p, const Student& s){cout << p._name << endl;//cout << s._stu<<endl; cout << endl;}int main(){Person p;Student s;Display(p,s);return 0;}
多继承及其菱形继承问题
单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的⼀种特殊情况。存在数据冗余和二义性,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承。
为了解决菱形继承,c++给出了virtual关键字,将它放在菱形继承的腰部就能解决菱形继承了。
class Person{public:string _name;};class student:virtual public Person{protected:int _num;//学号};class teacher :virtual public Person{protected:int _id;//职工编号};class Assistant : public student, public teacher{protected:string _majorCourse; // 主修课程};int main(){//_name访问不明确Assistant a;//a._name = \"peter\";a.student::_name = \"xxx\";a.teacher::_name = \"yyy\";return 0;}
多态
多态概念
通俗来说,就是多种形态。
多态分类
编译时多态(静态多态)
-
函数重载
-
函数模板
运行时多态(动态多态)
- 完成某个行为(函数),可以传不同的对象完成不同的行为,得到的结果不一样
- 成年人买票,买的是全价票,学生买票,买的是学生优惠票,军人买票,则可以优先买票。不同的人去买票,所呈现的形态是不一样的
编译时多态:他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。
指在编译时编译器会根据参数个数、参数类型、参数顺序和函数是否const来决定调用哪一个同名函数,或者根据模板参数来生成相应的模板类
c++输入输出会自动识别类型,实际上是调用了两个重载函数,匹配整数i,就用int的类型打印,匹配浮点数d,就调用double的类型去打印
多态的定义与实现
多态的构成条件
多态是⼀个继承关系下的类对象,去调用同⼀函数,产生不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象优惠买票。
实现多态的必要条件
-
必须是指针或者引用调用虚函数
-
被调用的函数必须是虚函数
要实现多态效果,第⼀必须是基类的指针或引用,因为只有基类的指针或引用才能既能指向基类,也能派生类对象;第⼆派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到
多态:不同的类型实现某一行为时达到不同的形态的本质是“调用不同的函数”
虚函数
类成员函数前+“virtual”修饰,那么这个成员函数就是虚函数(非成员函数不能加)
1.必须是基类的指针或者引用,因为只有基类的指针或者引用才能即指向基类又指向派生类
2.派生类必须对基类的虚函数进行重写或者覆盖,重写或者覆盖了才能有不同的函数,才能达到多态的效果。
class Person{public:virtual void BuyTicket(){cout << \"全价票\" << endl;}};class Student:public Person{public:virtual void BuyTicket(){cout << \"半价票\" << endl;}};//与ptr无关,与ptr指向的对象有关void func(Person& ptr){ptr.BuyTicket();}int main(){Person ps;Student st;func(ps);func(st);return 0;}
基类虚函数的重写 / 覆盖
派生类中必须有一个与基类虚函数完全相同的虚函数(函数返回值类型,函数名,参数列表完全相同)。若派生类中的虚函数不写virtual也构成多态,因为基类的虚函数被继承下来后依然保持虚函数的属性,但不建议这么写。
class Animal{public:virtual void call()const {};};class dog:public Animal{public: void call()const{cout << \"旺旺\" << endl;}};class cat :public Animal{public: void call()const{cout << \"喵喵\" <call();}int main(){dog d;cat c;letsHear(&d);letsHear(&c);return 0;}
多态场景的选择题
以下程序输出结果是什么()A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
class A { public: virtual void func(int val = 1){ std::cout<\"<< val <<std::endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val = 0){ std::cout<\"<< val <test(); return 0; }/*答案:B通过test函数的this指针调用func函数,此时this是基类的类型,满足多态的条件①,观察基类与派生类的func函数,是虚函数的重写覆盖,满足多态条件②,所以构成多态,但是!!!在多态条件下,派生类的虚函数重写可以理解为是用virtual void func(int val = 1)来进行重写的,也就是拿基类的虚函数的除函数体外的部分来进行重写,所以val的值是1*/
满足多态的情况,才能如此
虚函数重写存在的问题
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类类型的指针或者引用,派生类虚函数返回派生类类型的指针或者引用时,称为协变。协变的实际意义并不大,了解⼀下即可。
class A {};class B :public A{};class Person {public:virtual A* BuyTicket(){cout << \"买票全价\" << endl;return nullptr;}};class Student : public Person {public:virtual B* BuyTicket(){cout << \"买票打折\" <BuyTicket();}
析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写
class A{public:virtual ~A(){cout << \"~A()\" << endl;}};class B : public A {public:~B(){cout <delete:\" << _p << endl;delete _p;}protected:int* _p = new int[10];};int main(){A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;}
override和final关键字
override:帮助用户检测是否重写
final:如果我们不想让派 ⽣类重写这个虚函数,那么可以用final去修饰。
class Car{public: void Drive() {};//没有重写,报错};class Benz :public Car{public:virtual void Drive()override { cout << \"Benz\" <Drive();}------------------------------ class Car{public:virtual void Drive()final {};};class Benz :public Car{public:virtual void Drive()//报错,因为Drive被final修饰{ cout << \"Benz\" <Drive();}
重载/重写/隐藏的对比
这个概念对比经常考,大家得理解记忆⼀下
纯虚函数和抽象类
纯虚函数就是在虚函数后面写上=0。纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
class Car{public:virtual void Drive() = 0;};class Benz:public Car{public:virtual void Drive(){cout << \"Benz\" <Drive();}int main(){//Car c;//纯虚函数不能用来实例化对象Benz c;//若派生类也没定义纯虚函数的实现,那么派生类也无法用来实例化对象Func(&c);return 0;}------------------------------- /* 虽然抽象类不能实例化对象,但能定义指针 */class Car{public:virtual void Drive() = 0;};class Benz:public Car{public:virtual void Drive(){cout << \"Benz\" << endl;}};class BMW :public Car{public:virtual void Drive(){cout << \"BMW操控\" <Drive();return 0;}
多态原理
虚函数表指针
下⾯编译为32位程序的运⾏结果是什么()A.编译报错 B.运⾏报错 C.8 D.12
class Base { public: virtual void Func1() { cout << \"Func1()\" << endl; } protected: int _b = 1; char _ch = \'x\'; }; int main() { Base b; cout << sizeof(b) << endl; return 0; }/* 答案是D _b四字节 _ch1字节 还要个虚函数表指针四字节 当内存对齐完后,再加上虚函数表指针,一个12字节*/
多态如何实现的
运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
动态绑定与静态绑定
不满足多态条件的函数调用,是在编译时确定调用的函数的地址的叫静态绑定
满足多态条件的函数调用,是在运行时确定调用的函数的地址,叫做动态调用
虚函数表
1.基类对象的虚函数表中存放基类所有虚函数的地址。
2.派生类:
a.继承下来的基类
i.若继承下来的基类有虚函数表了,自己不会再生成
ii.继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象 的成员和派生类对象中的基类对象成员也独立的
b.自己的虚函数成员
c.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址
d,派生类的虚函数表包含
i.基类的虚函数地址
ii.派生类重写的虚函数地址
iii.派生类自己的虚函数地址三个部分
e.虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函 数的地址⼜存到了虚表中。
f.虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000 标记,g++系列编译不会放
g.虚表存储在哪?虚表存储在每个类的对象实例中。
虚表对于每个类只有一个实例,并且所有该类的对象共享同一个虚表。这是因为虚表包含的是对于特定类的虚函数的地址,而不是具体对象的成员函数。