十八. 继承和多态
● 继承的概念
继承(inheritance): 以旧类为基础创建新类, 新类包含了旧类的数据成员和成员函数(除了构造函数和析构函数), 并且可以派生类中定义新成员. 形式: class <派生类名>: <继承方式1> <基类名1> <继承方式2> <基类名2> ..., <继承方式n> <基类名n> { <派生类新定义的成员> } |
#include <iostream> using namespace std; class Person { public: int i; Person() { i=11; j=12; k=13; } private: int j; protected: int k; }; class People:public Person //以公有继承方式建立基类People { //下面在基类中建立新的成员 public: void Show() { cout<<i<<endl; //cout<<j<<endl; //j在基类中是私有成员, 在子类中不可见 cout<<k<<endl; //j在基类中是保护成员, 在子类中可见 } }; void main() { People p; p.Show(); } |
● 派生类对对基类成员的访问能力
把握三者的"兼并"能力: public<protected<private |
● 根据结果查看构造函数和析构函数的调用顺序
#include <iostream> using namespace std; class Person { public: int i; Person() //父类的构造函数 { cout<<"Peron"<<endl; } ~Person() //父类的析造函数 { cout<<"~Person"<<endl; } }; class People:private Person { public: People() //子类的构造函数 { cout<<"People"<<endl; } ~People() //子类的析造函数 { cout<<"~People"<<endl; } }; void main() { People p; } |
调用顺序为: 父类构造函数→子类构造函数→父类析构函数→子类析构函数 ※ 注意: 父类的构造函数不可以被子类继承, 我们看到p对象被创造之前构造函数已经被调用了,所以子类没有继承基类的构造函数, 不过基类的构造函数还是会被系统自动调用(这是初始化对象). 析构函数也不会被继承, 如果要继承, 要加关键字virtual. (virtual不能修饰构造函数) |
● 联编(binding) & 多态(polymorphism)
例如: class A { void func() {cout<<"It's A"<<endl; }; class B { void func() {cout<<"It's B"<<endl; }; int main() { func(); } 联编就是决定将main()函数中的fun()的函数调用映射到A中的func函数还是B中的func函数的过程。 main()函数和fun()函数之间的映射关系就是联编. ※设两个集合A和B,和它们元素之间的对应关系R,如果对于A中的每一个元素,通过R在B中都存在唯一一个元素与之对应,则该对应关系R就称为从A到B的一个映射(mapping). 映射/射影,在数学及相关的领域经常等同于函数。 联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序自身彼此关联的过程。 按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 静态联编说的是在编译时就已经确定好了调用和被调用两者的关系。 动态联编说的是程序在运行时才确定调用和被调用者的关系。 ※ 多态性包括编译时的多态性和运行时的多态性 拿上面的例子来说,静态联编就是在编译的时候就决定了main函数中调用的是A中的func还是B中的func; 动态联编在编译的时候还不知道到底应该选择哪个func函数,只有在真正执行的时候,它才确定。 静态联编和动态联编都是属于多态性的, 它们是在不同的阶段进对不同的实现进行不同的选择; 实多态性的本质就是选择, 因为存在着很多选择,所以就有了多态。 C++多态有两种形式,动态多态和静态多态: 静态多态通过模板来实现,因为这种多态实在编译时实现,所以称为静态多态。 动态多态是指一般的多态,通过类继承和虚函数来实现多态;因为这种多态实在运行时实现,所以称为动态多态。 |
● 运算符的重载(operator overloading)
运算符实际上是一个内置的函数, 所以运算符的重载实际上就是函数的重载. Operators can be extended to work out not just with variables of built-in types but also with objects of classes. 重载运算符的声明形式: <返回类类型> operator <运算符符号> (<参数表>) { <函数体> } ※ 一下操作符不能重载: "::"、"."、"*"、"?:"、sizeof、typedef、new、delete、static_cast、dynamic_cast、const_cast、reinterpret_cast。 |
//通过成员函数实现对象的相加 #include <iostream> using namespace std; class Book { public: Book (int page) { bk_page=page; } int Add(Book bk) //以对象bk作为函数的形参, 这种情况一般会设计对象成员的访问 { return bk_page+bk.bk_page; //bk_page是本对象的数据成员, bk.bk_page另一个对象的数据成员 } protected: int bk_page; }; void main() { Book bk1(10); Book bk2(20); cout<<bk1.Add(bk2)<<endl; //bk1.add(bk2)的意思调用对象bk1的成员函数Add(), 对象bk1本身有一个数据成员bk_page, bk2也有一个数据成员bk2.bk_page } |
//通过运算符的重载实现对象的相加 #include <iostream> using namespace std; class Book { public: Book (int page) { bk_page=page; } void display() { cout << bk_page << endl; } Book operator+(Book bk) //运算符的重载, 此时运算符"+"也是Book类的成员函数了 { return Book(bk_page+bk.bk_page); //Book可以省略; book_page指的是当前对象的数据成员, bk.book_page代表另一个对象的数据成员 } operator int() //将转换运算符int()进行重载 { return bk_page; } protected: int bk_page; }; void main() { Book bk1(10); Book bk2(20); Book tmp(0); tmp= bk1+bk2; //bk1+bk2的结果是Book类类型的, 所以这个结果只能赋给Book型的tmp对象 tmp.display(); cout<<int(bk1)+int(bk2)<<endl; //int()是转换运算符, 即将bk1和bk2由Book类类型转换至int类型 } //上面是在类体内, "+"会重载为Book类的成员函数. //双目运算符"+"有两个操作数, 如果定义在类体内, 参数要少一个; 在类体外, 参数是两个. //也就是说, 当运算符重载为类的成员函数时, 函数的参数个数比原来的操作数个数要少一个(后置的++和—除外);当重载为非成员函数时, 参数个数与原操作数个数相同. 原因是: 第一个操作数(第一个形参)就是对象本身, 它仅以this指针的形式隐式存在与参数表中. #include <iostream> using namespace std; class Book { public: Book (int page) { bk_page=page; } void display() { cout << bk_page << endl; } int bk_page; //在类体外重载运算符, 此时bk_page变量的属性不能是私有或protected }; Book operator+(Book bk_1, Book bk_2) //不能写成(Book x,y) { return bk_1.bk_page+bk_2.bk_page; //在类体外重载运算符, "+"不是Book类的重载运算符 } void main() { Book bk1(10); Book bk2(20); Book tmp(0); tmp= bk1+bk2; //bk1+bk2的结果是Book类类型的, 所以这个结果只能赋给Book型的tmp对象 tmp.display(); } |
//通过运算符的重载实现对象和普通类型变量的相加 #include <iostream.h> class Add { public: int m_Operand; Add() //构造函数 { m_Operand=0; } Add(int value) //重载构造函数 { m_Operand=value; } }; Add operator+(Add a, int b) //在类体外声明重载运算符, 此时运算符"+"不是Book类的成员函数 { Add sum; sum.m_Operand=a. m_Operand +b; return sum; } void main() { Add a(5),b; b=a+8; cout<<"the sum is: "<<b.m_Operand<<endl; } |
● 重载/过载(overload) & 重写/覆盖(override) & 隐藏(hide)/重定义(redefine)
重载与重写都与C++语言的多态性有关, 即不同功能的函数可以用同一个函数名. 下面是几个版本的比较: 版本一: 一、重载(overload) 指函数名相同,但是它的参数个数/顺序/类型不同, 但是不能靠返回类型来判断某函数是否为重载函数 )相同的范围(在同一个作用域中) ; )函数名相同; )参数不同; )virtual 关键字可有可无 )返回值可以相同或不同, 但参数个数/顺序/类型必须不同 二、重写(也称为覆盖 override) 是指派生类重新定义基类的虚函数,特征是: ※ 虚函数: ① 概念: 被virtual关键字修饰的成员函数 ② 格式: virtual 函数返回类型 函数名(参数表){函数体}; ③ 实现多态性: 将父类指针或引用指向派生类对象, 从而访问派生类中同名成员函数. )不在同一个作用域(分别位于派生类与基类) ; )函数名相同; )参数相同; )基类函数必须有 virtual 关键字,不能有 static; 子类函数可加也可不加virtual, 但为了保险起见, 最好加virtual关键字. 另外: 一个类中将所有的成员函数都尽可能地设置为虚函数总是有益的。(钱能, C++程序设计教程) 常见的不能声明为虚函数的有:普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数。 )返回值相同(或是协变),否则报错; )重写函数的访问修饰符可以不同。如果 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的 三、重定义(也成隐藏) )不在同一个作用域(分别位于派生类与基类) ; )函数名相同; )返回值可以不同; )如果参数不同, 那么不论有没有 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆) 。 )参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。 版本二: 父类是virtual方法 形参表相同 ---> 构成重写 父类是virtual方法 形参表不同 ---> 隐藏父类方法 父类不是virtual方法 形参表相同 --->隐藏父类方法 父类不是virtual方法 形参表不同 --->隐藏父类方法 版本三 从函数的实际调用来理解三者的区别:: ①函数重载:同一作用域,寻找适当函数的过程 ②函数重写:用于父类与子类之间的虚函数, 虚函数是通过关键字virtual来实现的,在具体的实现时父类函数的指针会被子类相同的函数指针所覆盖,所以才称为override ③函数重定义(个人觉得称为隐藏更恰当):函数调用时,在两个不同作用域(大的作用域包含小的作用域情况下),在小的作用域中寻找到合适的函数后直接调用,不用再在大的作用域中搜索,故可以称为隐藏. |
● 隐藏与重写的区别
隐藏的案例 |
#include <iostream> using namespace std; class A { public: void print() { cout<<"This is A"<<endl; } }; class B:public A { public: void print() { cout<<"This is B"<<endl; } }; int main() { A a; B b; a.print(); b.print(); b.A::print(); //使用派生类的对象访问基类中被派生类隐藏了的函数或变量 } |
上面的案例并不是多态性的实现. 多态性的有一个关键: 我们应该用指向基类的指针或引用来操作基类或派生类的对象: |
#include <iostream> using namespace std; class A { public: virtual void print() { cout<<"This is A"<<endl; //现在成了虚函数了 } }; class B:public A { public: void print() //子类虚函数的virtual可以省略, 不过最好写上, 使程序更加清晰 { cout<<"This is B"<<endl; } }; int main() { A a; B b; A* p1=&a; //p1基类A指针, 指向派生类对象a A* p2=&b; //p2也是基类A指针, 指向派生类对象b p1->print(); p2->print(); p2->A::print(); //使用派生类的对象访问基类中被派生类隐藏了的函数或变量(比较Python中的super()方法) } |
如果不用基类指针, 那么在基类中定义的虚函数就会被子类的同名函数隐藏, 与第一个隐藏案例的结果一样, 也就是说, 如果不用基类指针, 这个虚函数的定义没有用武之地 |
#include <iostream> using namespace std; class A { public: virtual void print() { cout<<"This is A"<<endl; //现在成了虚函数了 } }; class B:public A { public: void print() { cout<<"This is B"<<endl; } }; int main() { A a; B b; a.print(); b.print(); } |
如果不定义虚函数, 但还是硬要使用指向基类的指针或引用来操作基类或派生类的对象, 那么就算基类A的指针p2明明指向的是派生类B的对象b, 它调用的还是基类A的print()函数 |
#include <iostream> using namespace std; class A { public: void print() { cout<<"This is A"<<endl; } }; class B:public A { public: void print() { cout<<"This is B"<<endl; } }; int main() { A a; B b; A* p1=&a; //p1基类A指针, 指向派生类对象a A* p2=&b; //p2也是基类A指针, 指向派生类对象b p1->print(); p2->print(); } |
● 再一个虚函数案例
#include <iostream> using namespace std; class Animal { public: virtual void Breathe() { cout<<"Breathe with a kind of organ"<<endl; } }; class Mammal:public Animal { public: void Breathe() { cout<<"Breathe with lung"<<endl; } }; class Fish:public Animal { public: void Breathe() { cout<<"Breathe with gill"<<endl; } }; void main() { Animal animal1; Mammal mammal1; Fish fish1; Animal *p1=&animal1; Animal *p2=&mammal1; Animal *p3=&fish1; p1->Breathe(); p2->Breathe(); p3->Breathe(); } |
● 多重继承 (multiple inheritance) & 二义性(ambiguity) & 虚继承(virtual inheritance)
多重继承: 子类从多个父类继承 二义性: 有两种情况 情况1: 当派生类Derived的对象obj访问fun()函数时, 由于无法确定是访问基类Base1中的fun()函数, 还是Base2中的fun()函数, 如下面的a图所示; 情况2: 当一个派生类(如Derived2类)从多个基类派生(如Derived11类和Derived12类), 而这些基类又有一个共同的基类(如Base类), 当对该基类中说明的成员进行访问时,可能出现二义性, 如下面的b图所示;
|
使用作用域解析运算符进行限定的一般格式: <对象名>.<基类名>::<数据数据> <对象名>.<基类名>::<数据成员>(<参数表>) 例如: obj.Base1::fun() //调用Base1的函数 obj.Base2::fun() //调用Base2的函数 |
虚继承:在继承定义中包含了virtual关键字的继承关系; 虚基类:被虚继承的基类(不是包含虚函数或纯虚函数的基类) //虚继承是为了解决上面的第二种二义性问题; 例如, A类是B类和C类的父类, B类和C类是D类的父类, 在D类中将存在两个A类的复制, 那么如何在D类中使其只存在一个A类呢. #include <iostream> using namespace std; class Animal //定义一个动物类 { public: Animal() //定义构造函数 { cout << "动物类被构造"<< endl; } void Move() //定义成员函数 { cout << "动物能运动"<< endl; } }; class Bird : virtual public Animal //从Animal类虚继承Bird类 { public: Bird() //定义构造函数 { cout << "鸟类被构造"<< endl; } void Fly() //定义成员函数 { cout << "鸟能飞翔"<< endl; } void Breath() //定义成员函数 { cout << "鸟能呼吸"<< endl; //输出信息 } }; class Fish: virtual public Animal //从CAnimal类虚继承CFish { public: Fish() //定义构造函数 { cout << "鱼类被构造"<< endl; } void Swim() //定义成员函数 { cout << "鱼能游"<< endl; } void Breathe() //定义成员函数 { cout << "鱼能呼吸"<< endl; //输出信息 } }; class WaterBird: public Bird, public Fish //多重继承, 从Bird和Fish类派生子类WaterBird { public: WaterBird() //定义构造函数 { cout << "水鸟类被构造"<< endl; } void Action() //定义成员函数 { cout << "水鸟能飞又能游"<< endl; } }; int main() { WaterBird waterbird; //定义水鸟对象 } |
● 声明纯虚函数的形式为
声明纯虚函数的形式为: virtual 返回类型 函数名(参数列表)=0; 抽象类: ① 包含有纯虚函数(pure virtual function)的类称为抽象类, 一个抽象类至少有一个纯虚函数 ② 抽象类可以作为基类派生出新的子类, 但抽象类的纯虚函数不可以被继承
抽象类的意义: 在开发程序的过程中, 并不是所有代码都是由软件构造师自己写的, 有时需要调用库函数(很多库函数的功能可以自己写代码实现, 但很麻烦), 有时候分给别人写. 一名软件设计师可以通过纯虚函数建立接口, 然后让程序员填写代码实现接口, 而自己主要负责建立抽象类. |
#include <iostream> using namespace std; const double PI=3.14; class Figure //基类, 一个抽象类 { public: virtual double GetArea() =0; //纯虚函数 }; //////////////////////////////// class Circle : public Figure //派生类Circle { private: double radius; public: Circle(double x) { radius=x; } double GetArea() //实现抽象类的成员函数 { return radius*radius*PI; } }; //////////////////////////////// class Rectangle : public Figure //派生类Rectangle { protected: double height,width; public: Rectangle(double x,double y) { height=x; width=y; } double GetArea() //实现抽象类的成员函数 { return height*width; } }; //////////////////////////////// void main() { Figure *fg1; //声明一个抽象基类的指针, 目的是用基类指针来访问基类和派生类的同名函数 fg1= new Rectangle(4.0,5.0); //动态构造一个子类, 即Rectangle类型的对象(动态对象), 然后将基类, 即Figure类型的指针指向该动态对象, 这样就可以实现c++中的动态绑定功能. 因为基类Figure中一个成员函数是virtual,在子类Rectangle中又重载了该函数,那么通过Figure会调用Rectangle中的函数. cout << fg1->GetArea() << endl; //根据动态绑定的内容, 就可以知道调用那些成员函数来实现 delete fg1; fg1=NULL; Figure *fg2; fg2= new Circle(4.0); cout << fg2->GetArea() << endl; delete fg2; } |