(C/C++学习笔记) 十八. 继承和多态

时间:2021-11-21 02:19:19

十八. 继承和多态

● 继承的概念

继承(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();

}

(C/C++学习笔记) 十八. 继承和多态

● 派生类对对基类成员的访问能力

把握三者的"兼并"能力: public<protected<private

(C/C++学习笔记) 十八. 继承和多态

● 根据结果查看构造函数和析构函数的调用顺序

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

}

(C/C++学习笔记) 十八. 继承和多态

调用顺序为: 父类构造函数→子类构造函数→父类析构函数→子类析构函数

※ 注意: 父类的构造函数不可以被子类继承, 我们看到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

}

(C/C++学习笔记) 十八. 继承和多态

//通过运算符的重载实现对象的相加

#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类型

}

(C/C++学习笔记) 十八. 继承和多态

//上面是在类体内, "+"会重载为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();

}

(C/C++学习笔记) 十八. 继承和多态

//通过运算符的重载实现对象和普通类型变量的相加

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

}

(C/C++学习笔记) 十八. 继承和多态

● 重载/过载(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();    //使用派生类的对象访问基类中被派生类隐藏了的函数或变量

}

(C/C++学习笔记) 十八. 继承和多态

上面的案例并不是多态性的实现.

多态性的有一个关键:

我们应该用指向基类的指针或引用来操作基类或派生类的对象:

#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()方法)

}

(C/C++学习笔记) 十八. 继承和多态

如果不用基类指针, 那么在基类中定义的虚函数就会被子类的同名函数隐藏, 与第一个隐藏案例的结果一样, 也就是说, 如果不用基类指针, 这个虚函数的定义没有用武之地

#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();

}

(C/C++学习笔记) 十八. 继承和多态

如果不定义虚函数, 但还是硬要使用指向基类的指针或引用来操作基类或派生类的对象, 那么就算基类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();

}

(C/C++学习笔记) 十八. 继承和多态

● 再一个虚函数案例

#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();

}

(C/C++学习笔记) 十八. 继承和多态

● 多重继承 (multiple inheritance) & 二义性(ambiguity) & 虚继承(virtual inheritance)

多重继承:

子类从多个父类继承

二义性: 有两种情况

情况1:

当派生类Derived的对象obj访问fun()函数时, 由于无法确定是访问基类Base1中的fun()函数, 还是Base2中的fun()函数, 如下面的a图所示;

(C/C++学习笔记) 十八. 继承和多态

情况2:

当一个派生类(如Derived2类)从多个基类派生(如Derived11类和Derived12类), 而这些基类又有一个共同的基类(如Base类), 当对该基类中说明的成员进行访问时,可能出现二义性, 如下面的b图所示;

(C/C++学习笔记) 十八. 继承和多态

  • 解决二义性的方法: ① 使用作用域运算符; ② 使用同名覆盖(函数隐藏)的原则; ③虚继承

使用作用域解析运算符进行限定的一般格式:

<对象名>.<基类名>::<数据数据>

<对象名>.<基类名>::<数据成员>(<参数表>)

例如: 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;    //定义水鸟对象

}

(C/C++学习笔记) 十八. 继承和多态

● 声明纯虚函数的形式为

声明纯虚函数的形式为:

virtual 返回类型 函数名(参数列表)=0;

抽象类:

① 包含有纯虚函数(pure virtual function)的类称为抽象类, 一个抽象类至少有一个纯虚函数

② 抽象类可以作为基类派生出新的子类, 但抽象类的纯虚函数不可以被继承

  1. 抽象类不能在程序中被实例化为对象, 但是可以使用指向抽象类的指针
  2. 在抽象类的派生类中, 我们必须给出基类中纯虚函数的定义, 或在该派生类中再声明其为纯虚函数

抽象类的意义: 在开发程序的过程中, 并不是所有代码都是由软件构造师自己写的, 有时需要调用库函数(很多库函数的功能可以自己写代码实现, 但很麻烦), 有时候分给别人写. 一名软件设计师可以通过纯虚函数建立接口, 然后让程序员填写代码实现接口, 而自己主要负责建立抽象类.

#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++学习笔记) 十八. 继承和多态

(C/C++学习笔记) 十八. 继承和多态的更多相关文章

  1. python3&period;4学习笔记&lpar;十八&rpar; pycharm 安装使用、注册码、显示行号和字体大小等常用设置

    python3.4学习笔记(十八) pycharm 安装使用.注册码.显示行号和字体大小等常用设置Download JetBrains Python IDE :: PyCharmhttp://www. ...

  2. Java基础学习笔记十八 异常处理

    什么是异常?Java代码在运行时期发生的问题就是异常. 在Java中,把异常信息封装成了一个类.当出现了问题时,就会创建异常类对象并抛出异常相关的信息(如异常出现的位置.原因等). 异常的继承体系 在 ...

  3. Python3学习笔记十八

    1.    MTV M:   model     与数据库相关 T:   Template    与html相关 V:   views      与逻辑相关 一.    URL配置 启动:python ...

  4. scala 学习笔记十二 继承

    1.介绍 继承是面向对象的概念,用于代码的可重用性.可以通过使用extends关键字来实现继承. 为了实现继承,一个类必须扩展到其他类,被扩展类称为超类或父类.扩展的类称为派生类或子类. Scala支 ...

  5. Java学习笔记十八&colon;Java面向对象的三大特性之封装

    Java面向对象的三大特性之封装 一:面向对象的三大特性: 封装 继承 多态   二:封装的概念: 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访 ...

  6. MYSQL进阶学习笔记十八:MySQL备份和还原!(视频序号:进阶&lowbar;37&rpar;

    知识点十九:MySQL的备份的还原(38) 一.mysql的备份 1.通过使用mysqldump的命令备份 使用mysqldump命令备份,mysqldump命令将数据库中的数据备份成一个文本文件.表 ...

  7. JavaScript权威设计--事件冒泡,捕获,事件句柄,事件源,事件对象&lpar;简要学习笔记十八&rpar;

    1.事件冒泡与事件捕获 2.事件与事件句柄   3.事件委托:利用事件的冒泡技术.子元素的事件最终会冒泡到父元素直到跟节点.事件监听会分析从子元素冒泡上来的事件. 事件委托的好处:     1.每个函 ...

  8. python 学习笔记十八 django深入学习三 分页,自定义标签,权限机制

    django  Pagination(分页) django 自带的分页功能非常强大,我们来看一个简单的练习示例: #导入Paginator>>> from django.core.p ...

  9. SharpGL学习笔记&lpar;十八&rpar; 解析3ds模型并显示

    笔者设想的3D仿真中的元件,是不可能都是“画”出来的.这样就玩复杂了,应该把任务分包出去,让善于制作模型的软件来制作三维模型,我们只需要解析并且显示它即可. 3dsmax制作三维模型的方便,快捷,专业 ...

随机推荐

  1. selinux

    root@lujie ~]# vim /etc/sysconfig/selinux # This file controls the state of SELinux on the system. # ...

  2. Linux物理内存相关数据结构

    节点:pg_data_t typedef struct pglist_data { zone_t node_zones[MAX_NR_ZONES]; zonelist_t node_zonelists ...

  3. mkisofs出错解决办法

    使用mkisofs遇到错误: genisoimage: Uh oh, I cant find the boot catalog directory 'beini/boot/isolinux'! 使用的 ...

  4. 武汉科技大学ACM :1004&colon; 华科版C语言程序设计教程(第二版)课后习题3&period;7

    Problem Description 输入无符号短整数k[hex.]和p[oct.],将k的高字节作为结果的低字节,p的高字节作为结果的高字节组成一个新的整数. Input k[hex.]和p[oc ...

  5. &lbrack;LeetCode&rsqb;&lbrack;Python&rsqb;Reverse Integer

    # -*- coding: utf8 -*-'''__author__ = 'dabay.wang@gmail.com'https://oj.leetcode.com/problems/reverse ...

  6. Java中三种比较常见的数组排序

    我们学习数组比较常用的数组排序算法不是为了在工作中使用(这三个算法性能不高),而是为了练习for循环和数组.因为在工作中Java API提供了现成的优化的排序方法,效率很高,以后工作中直接使用即可 . ...

  7. &num;7 找出数组中第k小的数

    「HW面试题」 [题目] 给定一个整数数组,如何快速地求出该数组中第k小的数.假如数组为[4,0,1,0,2,3],那么第三小的元素是1 [题目分析] 这道题涉及整数列表排序问题,直接使用sort方法 ...

  8. day07----字符编码解码、文件操作(1)

    字符编码: 什么是字符编码? 字符编码是将人识别的字符转换成计算机能识别的二进制字符(01),转换的规则就是编码表. 人能识别的字符串  与  计算机能识别的二进制字符 两者之间对应关系构成的结构称为 ...

  9. 【iCore4 双核心板&lowbar;ARM】例程二十八:FSMC实验——读写FPGA

    实验现象: 1.先烧写FPGA程序,再烧写ARM程序,ARM程序烧写完毕后即开始读写RAM测试,测试成功,绿色ARM·LED亮,测试失败,红色ARM·LED闪烁. 2.测试成功,ARM通过映射寄存器来 ...

  10. memcpy in place 数组内拷贝

    首先看一段代码 #include <stdio.h> #include <pthread.h> int main(){ ]; ; ; i++) { t1[i] = i; pri ...