十八. 继承和多态
● 继承的概念
继承(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; } |
(C/C++学习笔记) 十八. 继承和多态的更多相关文章
-
python3.4学习笔记(十八) pycharm 安装使用、注册码、显示行号和字体大小等常用设置
python3.4学习笔记(十八) pycharm 安装使用.注册码.显示行号和字体大小等常用设置Download JetBrains Python IDE :: PyCharmhttp://www. ...
-
Java基础学习笔记十八 异常处理
什么是异常?Java代码在运行时期发生的问题就是异常. 在Java中,把异常信息封装成了一个类.当出现了问题时,就会创建异常类对象并抛出异常相关的信息(如异常出现的位置.原因等). 异常的继承体系 在 ...
-
Python3学习笔记十八
1. MTV M: model 与数据库相关 T: Template 与html相关 V: views 与逻辑相关 一. URL配置 启动:python ...
-
scala 学习笔记十二 继承
1.介绍 继承是面向对象的概念,用于代码的可重用性.可以通过使用extends关键字来实现继承. 为了实现继承,一个类必须扩展到其他类,被扩展类称为超类或父类.扩展的类称为派生类或子类. Scala支 ...
-
Java学习笔记十八:Java面向对象的三大特性之封装
Java面向对象的三大特性之封装 一:面向对象的三大特性: 封装 继承 多态 二:封装的概念: 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访 ...
-
MYSQL进阶学习笔记十八:MySQL备份和还原!(视频序号:进阶_37)
知识点十九:MySQL的备份的还原(38) 一.mysql的备份 1.通过使用mysqldump的命令备份 使用mysqldump命令备份,mysqldump命令将数据库中的数据备份成一个文本文件.表 ...
-
JavaScript权威设计--事件冒泡,捕获,事件句柄,事件源,事件对象(简要学习笔记十八)
1.事件冒泡与事件捕获 2.事件与事件句柄 3.事件委托:利用事件的冒泡技术.子元素的事件最终会冒泡到父元素直到跟节点.事件监听会分析从子元素冒泡上来的事件. 事件委托的好处: 1.每个函 ...
-
python 学习笔记十八 django深入学习三 分页,自定义标签,权限机制
django Pagination(分页) django 自带的分页功能非常强大,我们来看一个简单的练习示例: #导入Paginator>>> from django.core.p ...
-
SharpGL学习笔记(十八) 解析3ds模型并显示
笔者设想的3D仿真中的元件,是不可能都是“画”出来的.这样就玩复杂了,应该把任务分包出去,让善于制作模型的软件来制作三维模型,我们只需要解析并且显示它即可. 3dsmax制作三维模型的方便,快捷,专业 ...
随机推荐
-
selinux
root@lujie ~]# vim /etc/sysconfig/selinux # This file controls the state of SELinux on the system. # ...
-
Linux物理内存相关数据结构
节点:pg_data_t typedef struct pglist_data { zone_t node_zones[MAX_NR_ZONES]; zonelist_t node_zonelists ...
-
mkisofs出错解决办法
使用mkisofs遇到错误: genisoimage: Uh oh, I cant find the boot catalog directory 'beini/boot/isolinux'! 使用的 ...
-
武汉科技大学ACM :1004: 华科版C语言程序设计教程(第二版)课后习题3.7
Problem Description 输入无符号短整数k[hex.]和p[oct.],将k的高字节作为结果的低字节,p的高字节作为结果的高字节组成一个新的整数. Input k[hex.]和p[oc ...
-
[LeetCode][Python]Reverse Integer
# -*- coding: utf8 -*-'''__author__ = 'dabay.wang@gmail.com'https://oj.leetcode.com/problems/reverse ...
-
Java中三种比较常见的数组排序
我们学习数组比较常用的数组排序算法不是为了在工作中使用(这三个算法性能不高),而是为了练习for循环和数组.因为在工作中Java API提供了现成的优化的排序方法,效率很高,以后工作中直接使用即可 . ...
-
#7 找出数组中第k小的数
「HW面试题」 [题目] 给定一个整数数组,如何快速地求出该数组中第k小的数.假如数组为[4,0,1,0,2,3],那么第三小的元素是1 [题目分析] 这道题涉及整数列表排序问题,直接使用sort方法 ...
-
day07----字符编码解码、文件操作(1)
字符编码: 什么是字符编码? 字符编码是将人识别的字符转换成计算机能识别的二进制字符(01),转换的规则就是编码表. 人能识别的字符串 与 计算机能识别的二进制字符 两者之间对应关系构成的结构称为 ...
-
【iCore4 双核心板_ARM】例程二十八:FSMC实验——读写FPGA
实验现象: 1.先烧写FPGA程序,再烧写ARM程序,ARM程序烧写完毕后即开始读写RAM测试,测试成功,绿色ARM·LED亮,测试失败,红色ARM·LED闪烁. 2.测试成功,ARM通过映射寄存器来 ...
-
memcpy in place 数组内拷贝
首先看一段代码 #include <stdio.h> #include <pthread.h> int main(){ ]; ; ; i++) { t1[i] = i; pri ...