第八章 多态性

多态性:同一操作作用于不同的类的实例,将产生不同的执行结果,即不同类的对象收到相同的消息时,得到不同的结果。
多态性包含编译时的多态性、运行时的多态性。即:多态性也分静态多态性和动态多态性两种。
静态多态性。是指定义在一个类或者一个函数中的同名函数,它们根据函数标签(形参类型、个数 以及const)区别语义,并通过静态联编实现。(函数重载和运算符重载)
动态多态性。是指定义在一类层次的不同类中的重载对象,它们一般就用相同的函数,因此要根据指针指向的对象所在类来区别语义

各种运算符的重载
https://www.runoob.com/cplusplus/cpp-overloading.html

一、运算符的重载

如何实现复数的加法?
C++中没有复数,我们要自己定义一个复数结果,那么如何实现加法呢?我们可以为复数类重载加法运算符
什么是运算符重载?
将C++语法预定义好的运算符,针对新的自定义类型,赋予它新的含义

1 C++重载语法规则

  • C++几乎可以重载全部的运算符,而且只能够重载C++中已有的
    (不能重载的运算符 . .* :: ?: )
  • 重载之后运算符的优先级和结合性都不会改变
  • 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造
    例如:使复数类对象可以用“+”运算符实现加法

2 运算符的重载是通过函数来实现的

运算符重载的两种类型

  • 重载运算符为类的非静态成员函数(左操作数是自定义的对象 例如:复数+实数)
  • 重载运算符 为非成员函数(左操作数是不自定义的对象 可以是基本数据类型,也可以是库里面定义好的类)

二、双目运算符的重载

1 重载为类成员的运算符函数定义形式:

函数类型  operator 运算符(形参){
      ......
}

形参表中参数个数=原操作数个数-1(后置++、--除外)
原操作数个数?双目运算符操作数个数就是两个,形参里放的是第二个操作数;单目一个,形参不放参数

2 双目运算符重载规则

  • 如果要重载运算符B为类的成员函数,使之能够实现表达式oprd1 B oprd2,其中opr1为A类对象(如果想重载运算符B为类的成员函数,oper1必须是该类的对象),则B应该被重载为A的成员函数形参类型应该是oprd2所属的类型
  • 经重载后,表达式oprd1 B oprd2相当于oprd1.operator B(oper2)

3 双目运算符例子

image.png

代码

#include<iostream>
using namespace std;
class Complex {
public:
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    //运算符+重载成员函数
    Complex operator +(const Complex& c2)const;
    //运算符-重载成员函数
    Complex operator -(const Complex& c2)const;
    void display()const;//输出复数
private:
    double real, imag;//real复数实部,imag复数虚部
};
Complex  Complex::operator+(const Complex& c2)const {
    return Complex(real + c2.real, imag + c2.imag);//返回临时无名对象,节省资源
}
Complex  Complex::operator-(const Complex& c2)const {
    return Complex(real - c2.real, imag - c2.imag);//返回临时无名对象,节省资源
}
void Complex::display()const {
    cout <<"("<< real << "," << imag << ")"<<endl;
}
int main() {
    Complex c1(5, 4), c2(2, 10), c3;
    cout << "c1 ="; c1.display();
    cout << "c2="; c2.display();
    c3 = c1 + c2;//实际上是c1.operator +(c2)
    cout << "c3=c1 +c2 ="; c3.display();
    c3 = c1 - c2;//实际上是c1.operator -(c2)
    cout << "c3=c1 -c2 ="; c3.display();
}

结果:

c1 =(5,4)
c2=(2,10)
c3=c1 +c2 =(7,14)
c3=c1 -c2 =(3,-6)

三、单目运算符的重载

1 前置单目运算符重载规则

  • 如果要重载的运算符U为类的成员函数,使之能够实现表达式 U oprd,其中oprd为A类对象,则U应该被重载为A类的成员函数,无形参
  • 经重载后 U oprd 相当于 oprd.operator U()

2 后置单目运算符++和--重载规则

  • 由于单目运算符++和--的前置和后置函数名都相同,想区分前后置只能用参数表(重载函数)
  • 如果要重载++或--为类的成员函数,使之能够实现表达式oprd++或oprd--,其中oprd为A类对象,则++或--应该被重载为A类的成员函数,且具有一个int类型形参0
  • 经重载后,表达式 oprd++ 相当于oprd.operator ++(0)

3 前置和后置单目运算符代码

#include<iostream>
using namespace std;
class Clock {//时钟类定义
public:
    Clock(int hour = 0, int minute = 0, int second = 0);
    void showTime()const;
    //前置单目运算符
    Clock& operator ++();
    //后置单目运算符
    Clock operator ++(int);
private:
    int hour, minute, second;
};
//Clock::Clock(int hour = 0, int minute = 0, int second = 0) {//错误 带有默认值的函数,当声明和定义分开时,在声明时给出默认值,定义时不能再给出
Clock::Clock(int hour , int minute , int second ) {
    //if (0 <= hour < 24 && 0 <= minute < 60 && 0 <= second < 60) {  //错误不能这样写
    if (0 <= hour &&hour < 24 && 0 <= minute &&minute < 60 && 0 <= second&& second < 60) {  //错误不能这样写
        this->hour = hour;
        this->minute = minute;
        this->second = second;
    }
    
}
//返回的是本类对象的引用,++A 是先+1再使用,所以返回的是自己
Clock& Clock::operator ++() {
    this->second++;
    if (this->second >= 60) {
        this->minute++;
        this->second -= 60;
        if (this->minute >= 60) {
            this->hour++;
            this->minute -= 60;
            this->hour = this->hour % 24;
        }
    }
    return *this;
}
void Clock::showTime()const {
    cout << hour << ":" << minute << ":" << second << endl;
}
//A++ 先使用再+1,所以返回的是自己加1之前的值,但其实自身已经+1了
Clock Clock::operator ++(int) {
    Clock old =*this;
    ++(*this);
    return old;
}
int main() {
    Clock myclock(12, 54, 34);
    myclock.showTime();
    (myclock++).showTime();
    myclock.showTime();
    (++myclock).showTime();
    return 0;
}

结果

12:54:34
12:54:34
12:54:35
12:54:36

四、函数调用运算符()的重载

函数调用运算符()可以被重载用于类的对象。当重载()时,不是创造了一种新的函数调用方式,相反的,是创建了一个可以传递任意数目参数的运算符函数

语法

函数类型 operator ()(参数表){
}

调用形式

#include<iostream>
using namespace std;
class A {
public:
    A(int x = 0,int y=0) :i(x),j(y) { cout << "执行构造函数" << endl; }
    int operator ()(int x, int y);
private:
    int i;
    int j;
};
int A::operator ()(int x, int y) {
    cout << "执行函数调用运算符重载" << endl;
    return x + y;
}
int main() {
    A a(2,3);
    a(2, 3);//a.operator(2,3)
        A(3.4);//创造无名对象,并调用函数调用运算符的重载

}

结果

执行构造函数
执行函数调用运算符重载
执行函数调用运算符重载

五、运算符重载为非成员函数(全局)

1语法

返回值类型 operator 操作符(参数表)

2运算符重载为非成员函数的规则

  • 函数的形参代表依自左至右次序排列的各操作数
  • 重载为非成员函数时
    • 参数个数=原操作数个数(后置++ --除外)
    • 至少应该有一个自定义类型的参数(不能两个都是基本数据类型,我们只能重载自定义类型的操作符)
  • 后置单目运算符++和--的重载函数,形参列表中要增加一个int,但不必写形参名
  • 如果在运算符的重载函数中需要操作其某类对象的私有成员,可以将此函数声明为该类的友元(调用该类的公有接口也可以实现,不过友元效率高)
  • 双目运算符B重载后
表达式 oprd1 B oprd2
等同于  operator B(oprd1,oprd2)//注意和成员函数的区别 成员函数oprd1.operator B(oprd2)
  • 前置单目运算符B重载后
表达式 B oprd
等同于  operator B(oprd)
  • 后置单目运算符B重载后(++和--)
表达式  oprd B
等同于  operator B(oprd,0)

3 实例

image.png

代码

#include<iostream>
using namespace std;
class Complex {
public :
    Complex(double r=0.0,double i=0.0):real(r),imag(i){}
    friend Complex operator +(const Complex& c1, const Complex& c2);//常引用效率比较高,而且是单向传递
    friend Complex operator -(const Complex& c1, const Complex& c2);//常引用效率比较高,而且是单向传递
    friend ostream& operator <<(ostream& out, const Complex& c);//常引用效率比较高,而且是单向传递
private:
    double real, imag;
}; 
Complex operator +(const Complex& c1, const Complex& c2) {
    return Complex(c1.real + c2.real, c1.imag + c2.imag);//返回临时无名对象
}
Complex operator -(const Complex& c1, const Complex& c2) {
    return Complex(c1.real - c2.real, c1.imag - c2.imag);//返回临时无名对象
}
ostream & operator <<( ostream &out, const Complex &c) {
    out << "(" << c.real <<"," << c.imag << ")" ;
    return out;//将第一个输出流对象返回,所以可以实现级联输出
    //所以可以实现cout<<a<<b  该表达式实际上相当于 operator <<(cout,b)   operator << (operator <<(cout,b) ,b)
}
int main() {
    Complex c1(5, 4), c2(2, 10), c3;
    cout << "c1 =" << c1 << endl;//第一个<<调用的基本的,第二个是我们自己定义的函数
    cout << "c2 =" << c2 << endl;//第一个<<调用的基本的,第二个是我们自己定义的函数
    c3 = c2 - c1;
    cout << "c3 = c2 - c1 =" << c3 << endl;//第一个<<调用的基本的,第二个是我们自己定义的函数
    c3 = c2 + c1;
    cout << "c3 = c2 + c1 =" << c3 << endl;//第一个<<调用的基本的,第二个是我们自己定义的函数

}

结果

c1 =(5,4)
c2 =(2,10)
c3 = c2 - c1 =(-3,6)
c3 = c2 + c1 =(7,14)

六、虚函数

虚函数是实现动态绑定的函数,通过虚函数可以实现运行时多态

1 什么时候需要用到虚函数?

如果打算通过基类的指针调用对象(当前对象是派生类的对象)的成员函数时,就要用到虚函数
例如

class A{
   void test();
}
class B :public A{
void test()
}
int main(){
 A*p=new B;
B.test();//想调用B类的test函数
}

要想实现调用B类的test函数,就必须使用虚函数,否则B.test()调用的是基类的test()函数

2 语法:

virtual 返回值类型 函数名(形参表)
  • virtual关键字是指定编译器不要在编译截断将这个函数静态绑定,而是在运行阶段动态绑定做好准备
  • 虚函数不能写成内联函数(内联函数是在编译时处理,和虚函数含义冲突),所以虚函数的定义必要写在类外

3 初识虚函数

  • 虚函数使用virtual关键字声明的函数
  • 虚函数是实现运行时多态性基础
  • C++中的虚函数是动态绑定的函数
  • 虚函数必须是非静态成员函数,虚函数经过派生之后就可以实现运行过程中的多态
  • 基类用virtual声明了虚函数,派生类用不用virtual声明都是虚函数。

4 例子

image.png
image.png

image.png

5 什么函数可以是虚函数?

  • 一般的成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数(注意析构可以构造不能)

6 一般虚函数

  • 虚函数的声明
    virtual 函数类型 函数名(形参表)
  • 虚函数的声明只能出现在类定义中函数原型声明中,而不能在成员函数实现的时候(声明的时候加virtual关键字,定义的时候不加virtual关键字
  • 在派生类中可以对基类中的成员函数进行覆盖(之前不用虚函数,覆盖基类的成员函数后达不到我们想要的目的,现在有了虚函数就可以实现了)
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的

7 virtual关键字

编译器会检查派生类的函数名 参数以及返回值,如果和基类的虚函数一样,就自动确定为虚函数


image.png

总结:可以不加,但是最好还是加上,增加可读性


七、虚析构函数

1 什么时候会用到虚析构函数?

  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数称为虚函数,否则执行delete的结果是不正确的。
  • 有时候会让一个基类指针指向用new运算符动态生成的派生类对象
    A *p=new B;//A是B的基类
    我们知道建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存
    delete p
    这时候会自动调用析构函数,由于析构函数是静态绑定的,编译时,只知道它是A类型指针,只会调用A类的析构函数,不会调用B的析构,这显然不符合我们的预期,想要解决这种情况,就要将A和B的析构函数声明为虚函数,实现动态绑定.

例子
(1)基类和派生类的析构函数不是虚函数

#include<iostream>
using namespace std;
class Base {
public:
    ~Base();//不是虚函数
};
Base::~Base() {
    cout << "Base destructor" << endl;
}
class Derived :public Base {
public:
    Derived();
    ~Derived();
private:
    int* p;
};
Derived::Derived() {
    p = new int(0);
}
Derived::~Derived(){
    cout << "Derived destructor" << endl;
}
void fun(Base* b) {
    delete b;//静态绑定,只会调用~Base()
}

测试1 不删除b,看看系统会不会自动回收b?

int main() {
    Base* b = new Derived();
    return 0;
}

结果,控制台什么都没有输出,没有调用任何析构函数,b没有被删除,所以new的空间必须要delete掉


测试2 删除指针b,看看析构函数调用情况

int main() {
    Base* b = new Derived();
    fun(b);
    return 0;
}

结果,只调用了基类Base类的析构函数,显然不符合我们的预期,我们想要删除的是Derived类,产生这样结果的原因是,在静态绑定的时候b只知道它是Base类指针,只知道调用Base类析构函数

Base destructor

(2)基类和派生类的析构函数是虚函数

#include<iostream>
using namespace std;
class Base {
public:
    virtual ~Base();//不是虚函数
};
Base::~Base() {
    cout << "Base destructor" << endl;
}
class Derived :public Base {
public:
    Derived();
    virtual ~Derived();
private:
    int* p;
};
Derived::Derived() {
    p = new int(0);
}
Derived::~Derived(){
    cout << "Derived destructor" << endl;
}
void fun(Base* b) {
    delete b;//动态绑定,根据指针b的类型调用对应类的析构函数
}

测试

int main() {
    Base* b = new Derived();
    fun(b);
    return 0;
}

结果

Derived destructor//析构函数执行顺序和构造相反,先执行派生类 再执行基类
Base destructor

八、虚表与动态绑定( 重点)

1 虚表

  • 每个多态类有一个虚表(virtual table)
  • 虚表中有当前类的各个虚函数的入口地址(指针)
  • 每个对象有一个指向当前类的虚表的指针(虚指针vptr)

2 动态绑定

  • 构造函数中为对象的虚指针赋值
  • 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用虚函数的入口地址
  • 通过该入口地址调用虚函数
    image.png

    编译阶段Base类和Derived类都有虚函数,于是编译器就得为动态绑定做好准备,编译器会分别为这两个类生成虚表虚表中有指向虚函数的指针(虚函数包括:从基类中继承且没有被覆盖的虚函数、派生类自己新增的虚函数和派生类改造的虚函数也就是覆盖基类的虚函数)。
    每一个具有虚表的类它的对象中都隐含了一个指向虚表的指针
    在运行时,如果有一个指针指向了对象,是派生自己的指针也好基类的指针也好,当要通过这个指针调用一个函数的时候:
  • 首先会通过这个指针找到对象里的虚表指针
  • 根据虚表指针找到虚表
  • 虚表里找到指向虚函数的指针然后去调用相应的函数体

九、抽象类(太虚的没有对象 但可以当基类)

1 纯虚函数

  • 纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本。
  • 纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表)=0;//=0表示没有函数体,并不是结果为0
  • 纯虚函数,只需要声明,不需要定义

2 抽象类(不能定义对象!!!)

  • 带有纯虚函数的类就是抽象类(只要有一个纯虚函数就是抽象类,之所以是抽象类是因为这样的类还有些东西没有实现)
  • 抽象类不能定义对象但抽象类可以作基类

语法:

class 类名{
virtual 类型  函数名(参数表)=0;
//其他成员
}

派生类的作用

抽象类虽然不能实例化,但它能规定对外接口的统一形式,可以让我们将基类对象和派生类对象都按照统一的方式进行处理,因为它们有同样的接口,我们可以通过基类指针接受不同派生类的地址,然后去调用派生类实现的函数

image.png

image.png

image.png


十、override和final

override

如果我们想在派生类中声明一个虚函数来隐藏基类中的同名虚函数,但是在实际编写时派生类的虚函数函数签名(函数签名包括: 函数名 参数列表 const)跟基类的不一样导致派生类的虚函数没有覆盖掉基类的虚函数,这样会造成一些意想不到的错误,而且编译器不会报错。我们可以在基类声明要重载的虚函数时,在前面加override关键字,它会进行检查看看基类中有没有相同的函数名,若没有则会报错

image.png

image.png

image.png

final

final不允许被继承或者被修改
语法:
class 类名 final //该类不允许被继承
返回值类型 函数名() final;//该函数不允许被覆盖、修改


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355