《C++语言程序设计基础》学习之多态

2021/10/6 22:11:12

本文主要是介绍《C++语言程序设计基础》学习之多态,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

多态,顾名思义就是操作接口,具有表现多种不同形态的能力,在不同的环境下,对不同的对象,具有不同的处理方式,多态实际上是通过绑定来实现的,所谓绑定就是将一个标志符名称,与一段函数代码结合起来,比如说,我们的函数调用表达式,跟函数体结合,这就是一种绑定。
绑定按照它的实现的时机,分成编译时的绑定,和运行时的绑定。
编译时的绑定也叫早绑定,那么也就是在编译阶段,编译器就把这个绑定完成了
那么运行时的绑定是等到运行的时候,才将这个标志符,和相应的函数代码结合起来,叫运行时的绑定,或者叫晚绑定,函数重载实际上也是一种多态性,它是静态多态性的一种体现,也就是函数重载,是通过早绑定,或者是编译时的绑定来完成的,运算符重载也是一种多态。

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

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

    {

           ......

    }

    参数个数=原操作数个数-1   (后置++、--除外)

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

复数类加减法运算重载为成员函数,+和-都只在类重载函数调用时起作用
要求:将+、-运算重载为复数类的成员函数。
规则:实部和虚部分别相加减。
操作数:两个操作数都是复数类的对象。

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;//复数实部

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

        int a = 1, j = 10;

        cout << "a + j = "<<a + j << endl;

        cout << "c1 = "; c1.display();

        cout << "c2 = "; c2.display();

        c3 = c1 - c2;//使用重载运算符完成复数减法

        cout << "c3 = c1 - c2 = "; c3.display();

        c3 = c1 + c2;//使用重载运算符完成复数加法

        cout << "c3 = c1 + c2 = "; c3.display();

        return 0;

}

 前置单目运算符重载规则:
如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。经重载后,表达式 U oprd 相当于 oprd.operator U()
后置单目运算符 ++和--重载规则
如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。多的这个int类型形参,是为了区分前置单目运算符重载和后置单目运算符重载
经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)

前置单目运算符,重载函数没有形参
后置++运算符,重载函数需要有一个int形参
操作数是时钟类的对象。
实现时间增加1秒钟。
前置++,对对象直接+1,返回当前对象的引用;后置++,表现为先使用后+1,实际是返回旧对象的复制副本,当前对象会+1

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 , int minute, int second ) {

        if (0 <= hour && hour < 24 && 0 <= minute && minute < 60 && 0 <= second && second < 60) {

               this->hour = hour;

               this->minute = minute;

               this->second = second;

        }

        else

               cout << "Time error!" << endl;

}

void Clock::showTime() const {  //显示时间

        cout << hour << ":" << minute << ":" << second << endl;

}

//重载前置++和后置++为时钟类成员函数

Clock& Clock::operator++() {//前置++

        second++;

        if (second >= 60) {

               second -= 60; minute++;

               if (minute >= 60) {

                       minute -= 60; hour = (hour + 1) % 24;

               }

        }

        return *this;//返回值是当前对象的引用

}

Clock Clock::operator ++ (int) {//后置++

        //注意形参表中的整型参数

        Clock old = *this;

        ++(*this);//调用前置“++”运算符

        return old;

}

//重置前置++和后置++为时钟类成员函数

int main(){

        Clock myClock(23, 59, 59);

        cout << "First time output:            ";

        myClock.showTime();

        cout << "Show myClock++:    ";

        (myClock++).showTime();//输出不变的myClock,背后已经+1的

        cout << "Show ++myClock:    ";

        (++myClock).showTime();

        return 0;

}

运行的结果:
First time output:         23:59:59
Show myClock++:    23:59:59
Show ++myClock:    0:0:1

非成员函数:
用cout整个输出一个复数对象了,重载插入运算符就是只能重载为类外的成员函数,因为它的左操作数虽然是一个类的对象,但是它是系统里面预定义好的,这个类库里面的一个输出流类的对象cout,不是程序员自定义的对象,无法在类库里面,这个输出流类里面去加一个重载运算符函数,没有权限去加的,所以把它重载为类外的非成员函数,实现对于复数对象的整体输出的功能

运算符重载为非成员函数:有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象
运算符重载为非成员函数的规则:
函数的形参代表依自左至右次序排列的各操作数。
重载为非成员函数时
参数个数=原操作数个数(后置++、--除外)
至少应该有一个自定义类型的参数。

后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名。
如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。

运算符重载为非成员函数的规则:
双目运算符 B重载后,表达式oprd1 B oprd2    等同于operator B(oprd1,oprd2 )
前置单目运算符 B重载后,表达式 B oprd     等同于operator B(oprd )
后置单目运算符 ++和--重载后,表达式 oprd B     等同于operator B(oprd,0 )

重载Complex的加减法和“<<”运算符为非成员函数
将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。 • 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以支持下面形式的输出:

cout << a << b;

该输出调用的是:

operator << (operator << (cout, a), b);

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); //左操作数是ostream的引用

private:

        double real;//复数实部

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

}

int main(){

        Complex c1(5, 4), c2(2, 10), c3;

        cout << "c1 = " << c1 << endl;

        cout << "c2 = " << c2 << endl;

        c3 = c1 - c2;//使用重载运算符完成复数减法

        cout << "c3= c1 - c2 = " << c3 << endl;

        c3 = c1 + c2;//使用重载运算符完成复数加法

        cout << "c3 = c1 + c2 = " << c3 << endl;

        return 0;

}

虚函数:
之前的例子

class Base1 { //基类Base1定义

public:

        void display() const {

               cout << "Base1::display()" << endl;

        }

};

class Base2 : public Base1 { //公有派生类Base2定义

public:

        void display() const {

               cout << "Base2::display()" << endl;

        }

};

class Derived : public Base2 { //公有派生类Derived定义

public:

        void display() const {

               cout << "Derived::display()" << endl;

        }

};



void fun(Base1 *ptr) {  //参数为指向基类对象的指针

        ptr->display();     //"对象指针->成员名"

}

int main() {    //主函数

        Base1 base1;    //声明Base1类对象

        Base2 base2;    //声明Base2类对象

        Derived derived;    //声明Derived类对象



        fun(&base1);    //用Base1对象的指针调用fun函数

        fun(&base2);    //用Base2对象的指针调用fun函数

        fun(&derived);     //用Derived对象的指针调用fun函数



        return 0;

}

运行的结果:
Base1::display()
Base1::display()
Base1::display()

可以看到输出的都是Base1类的display函数,fun(Base1 *ptr)函数中形参是Base1类的指针,编译的时候就确定的,所以调用的都是Base1类的display()函数
原因就是在编译阶段编译器根据指针无法去判断在运行时它会指向一个什么类型的对象,所以它只能说指针是什么类型的它就调用那个类定义的display函数,这种情况下希望告诉编译器在编译阶段你没法正确地决定,推迟这个决定在编译的时候先别确定这个display函数调用表达式跟函数体到底哪个函数体跟它对应,先别对应把它留着留到运行时再确定,那么运行时当然就能够知道指针在某个时刻指向的实际对象是什么了,怎么告诉编译器这件事,就用一个virtual关键字。
指示编译器不要在编译阶段做静态绑定,要为运行阶段做动态绑定做好准备,既然不要它在编译阶段处理,要求它在运行阶段再去决定对display的调用该执行哪个函数体,而内联函数呢是在编译阶段处理的,就要把它嵌入到程序代码中去,所以这两者显然矛盾,所以这样的虚函数加了virtual的函数,都要在类外去实现,函数体不能写成内联的了,再看同样Base2也有display函数,同样的原型也写成虚函数,Derived也是同原型的函数写成虚函数,都是在类外有各自不同的实现。
进行同样地测试,希望的效果就达到了每次分别送给它不同的基类派生类对象的地址送到这个形参里面去,这个指针在运行的时候不同的时刻它可能就指向了不同的类型的对象,虽然都是这个基类指针来调用display,但是运行时能够正确地找到每个对象自己的display去运行,因为说了它是虚函数,所以编译器在编译的时候它不做决定不确定该调用哪个函数体,而是到运行的时候再确定该调用哪个函数体。
用virtual关键字说明的函数就是虚函数,虚函数是实现运行时多态的基础,C++中用virtual指定的函数就是要实现动态绑定的函数,但是虚函数必须是非静态的成员函数,也就是说虚函数应该是属于对象的,不是属于整个类的,它是需要在运行的时候用指针去定位到它指向的对象是谁,然后决定调用哪个函数体,所以虚函数当然得是属于对象的而不是属于类的函数,那么虚函数经过派生以后,就可以实现这种运行中的多态。
初识虚函数:
用virtual关键字说明的函数
虚函数是实现运行时多态性基础
C++中的虚函数是动态绑定的函数
虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态。
一般成员函数可以是虚函数
构造函数不能是虚函数
析构函数可以是虚函数

一般虚函数成员:虚函数的声明:virtual 函数类型 函数名(形参表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
在派生类中可以对基类中的成员函数进行覆盖。
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。

virtual 关键字
派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
       该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型;该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值;
如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。
派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式。
一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性。

class Base1 { //基类Base1定义

public:

        virtual void display() const;//虚函数

};

void Base1::display() const {

        cout << "Base1::display()" << endl;

}

class Base2 : public Base1 { //公有派生类Base2定义

public:

        virtual void display() const;//虚函数

};

void Base2::display() const {

        cout << "Base2::display()" << endl;

}

class Derived : public Base2 { //公有派生类Derived定义

public:

        virtual void display() const;//虚函数

};

void Derived::display() const {

        cout << "Derived::display()" << endl;

}

void fun(Base1 *ptr) {  //参数为指向基类对象的指针

        ptr->display();     //"对象指针->成员名"

}

int main() {    //主函数

        Base1 base1;    //声明Base1类对象

        Base2 base2;    //声明Base2类对象

        Derived derived;    //声明Derived类对象



        fun(&base1);    //用Base1对象的指针调用fun函数

        fun(&base2);    //用Base2对象的指针调用fun函数

        fun(&derived);     //用Derived对象的指针调用fun函数

        return 0;

}

虚析构函数:
为什么需要虚析构函数? - 可能通过基类指针删除派生类对象; - 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。
不使用虚析构函数

运行的结果:Base destructor

只调用Base类的析构函数,没有调用Derived类的析构函数,fun()绑定到Base中,静态绑定,只会调用~Base()
改为虚析构函数以后,析构函数都调用的

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;//静态绑定,只会调用~Base()

}

int main(){

        Base*b = new Derived();

        fun(b);

        return 0;

}

运行的结果:
Derived destructor
Base destructor

 虚表与动态绑定:
虚表:每个多态类有一个虚表(virtual table);虚表中有当前类的各个虚函数的入口地址;每个对象有一个指向当前类的虚表的指针(虚指针vptr)
动态绑定的实现:构造函数中为对象的虚指针赋值;通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址;通过该入口地址调用虚函数
虚表示意图

抽象类:
派生类如果不去实现那些在基类中没有实现的纯虚函数,那么这个派生类仍然继续作为抽象类,还是没办法实例化没办法定义对象,只有到某一级派生类实现了这个纯虚函数,它不再纯虚了它有函数体那么这一个派生类才不是抽象类了,它才可以用来定义对象,所以抽象类是用来作为基类使用的,是不能够定义对象。 
纯虚函数:
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表) = 0;
带有纯虚函数的类称为抽象类
抽象类:带有纯虚函数的类称为抽象类:     class 类名 { virtual 类型 函数名(参数表)=0; //其他成员…… }
抽象类作用:抽象类为抽象和设计的目的而声明;将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意:抽象类只能作为基类来使用。不能定义抽象类的对象。

class Base1 {

public:

        virtual void display() const = 0;//纯虚函数

};

class Base2 :public Base1 {

public:

        virtual void display() const;//覆盖基类的虚函数

};

void Base2::display() const {

        cout << "Base2::display()" << endl;

}

class Derived : public Base2 {

public:

        virtual void display() const;//覆盖基类的虚函数

};

void Derived::display()const {

        cout << "Derived::display()" << endl;

}

void fun(Base1 *ptr) {

        ptr->display();

}

int main(){

        Base2 base2;

        Derived derived;

        fun(&base2);

        fun(&derived);

        return 0;

}

C++11:override 与 final
override:

多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
覆盖要求: 函数签名(signatture)完全一致
函数签名包括:函数名 参数列表 const
下列程序就仅仅因为疏忽漏写了const,导致多态行为没有如期进行

显式函数覆盖
C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。 - 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误。
final
C++11提供的final,用来避免类被继承,或是基类的函数被改写 例: struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
struct Base2 { virtual void f() final; };
struct Derived2 : Base2 { void f(); // 编译错误:Base2::f 为final,不允许被覆盖 };



这篇关于《C++语言程序设计基础》学习之多态的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程