C++--多态

一.多态性

1.多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念

多态(polymorphism),字面意思多种形状。

 

2.C++多态性是通过虚函数来实现的虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为遮蔽或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。

 

最常见的用法就是声明基类的指针,利用该指针指向任意一个派生类对象,调用相应的虚函数,可以根据指向的派生类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到派生类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。

 

//多态(一个接口多种方法)(重写虚函数)

就是在派生类中定义虚函数,然后又基类的指针指向派生类对象,调用虚函数

只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。

 

3.引用实现多态

int main(){

    People p("王志刚", 23);

    Teacher t("赵宏佳", 45, 8200);

   

    People &rp = p;

    People &rt = t;

   

    rp.display();

    rt.display();

    return 0;

}

 

修改上例中 main() 函数内部的代码,用引用取代指针

由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

 

不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。本例的主要目的是知道,除了指针,引用也可以实现多态。

 

4.虚函数

虚函数对于多态具有决定性的作用,有虚函数才能构成多态。

 

1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。

 

2) 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。

 

3) 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。

 

4) 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。

 

5) 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

 

6) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。

 

5.构成多态条件

必须存在继承关系;

继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系。

存在基类的指针,通过该指针调用虚函数。

 

什么时候声明虚函数

首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

 

二、C++纯虚函数和抽象类

1.C++中,可以将虚函数声明为纯虚函数,语法格式为:

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

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0表明此函数为纯虚函数。

最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

包含纯虚函数的类称为抽象类Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间

 

2.抽象类通常是作为基类

让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

 

3.本例中定义了四个类,它们的继承关系为:Line --> Rec --> Cuboid --> Cube

Line 是一个抽象类,也是最顶层的基类,在 Line 类中定义了两个纯虚函数 area() volume()

Rec 类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 Rec 仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类。

直到 Cuboid 类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。

可以发现,Line 类表示“线”,没有面积和体积,但它仍然定义了 area() volume() 两个纯虚函数。这样的用意很明显:Line 类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。

 

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

 

抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。

 

4.1 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。

4.2只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:

//顶层函数不能被声明为纯虚函数

void fun() = 0;   //compile error

class base{

public :

    //普通成员函数不能被声明为纯虚函数

    void display() = 0;  //compile error

};

 

 

 

三、Typeid运算符

typeid 运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:

对于基本类型(intfloat C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。

对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。

类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。

typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:

typeid( dataType )

typeid( expression )

dataType 是数据类型,expression 是表达式,这和 sizeof 运算符非常类似,只不过 sizeof 有时候可以省略括号( ),而 typeid 必须带上括号。

 

 

type_info 类的几个成员函数,下面是对它们的介绍:

name() 用来返回类型的名称。

raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。

hash_code() 用来返回当前类型对应的 hash 值。hash 值是一个可以用来标志当前类型的整数,有点类似学生的学号、公民的身份证号、银行卡号等。不过 hash 值有赖于编译器的实现,在不同的编译器下可能会有不同的整数,但它们都能唯一地标识某个类型。

遗憾的是,C++ 标准只对 type_info 类做了很有限的规定,不仅成员函数少,功能弱,而且各个平台的实现不一致。例如上面代码中的 name() 函数,nInfo.name()objInfo.name()VC/VS 下的输出结果分别是intclass Base,而在 GCC 下的输出结果分别是i4Base

 

C++ 标准规定,type_info 类至少要有如下所示的 4 public 属性的成员函数,其他的扩展函数编译器开发者可以自由发挥,不做限制。

 

1) 原型:const char* name() const;

返回一个能表示类型名称的字符串。但是C++标准并没有规定这个字符串是什么形式的,例如对于上面的objInfo.name()语句,VC/VS 下返回“class Base”,但 GCC 下返回“4Base”。

 

2) 原型:bool before (const type_info& rhs) const;

判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。

 

 

3) 原型:bool operator== (const type_info& rhs) const;

重载运算符==”,判断两个类型是否相同,rhs 参数是一个 type_info 对象的引用。

 

4) 原型:bool operator!= (const type_info& rhs) const;

重载运算符!=”,判断两个类型是否不同,rhs 参数是一个 type_info 对象的引用。

关于运算符重载,我们将在《C++运算符重载》一章中详细讲解。

raw_name() VC/VS 独有的一个成员函数,hash_code() VC/VS 和较新的 GCC 下有效。

 

2.typeid作用

判断类型是否相等

char *str;

int a = 2;

int b = 10;

float f;

类型比较

结果

类型比较

结果

typeid(int) == typeid(int)

true

typeid(int) == typeid(char)

false

typeid(char*) == typeid(char)

false

typeid(str) == typeid(char*)

true

typeid(a) == typeid(int)

true

typeid(b) == typeid(int)

true

typeid(a) == typeid(a)

true

typeid(a) ==

typeid(b)

true

typeid(a) == typeid(f)

false

typeid(a/b) == typeid(int)

true

 

3.类的比较

类的比较

class Base{};

 

class Derived: public Base{};

 

Base obj1;

Base *p1;

Derived obj2;

Derived *p2 =

new Derived;

p1 = p2;

表达式typeid(*p1) == typeid(Base)typeid(p1) == typeid(Base*)的结果为 true 可以说明:即使将派生类指针 p2 赋值给基类指针 p1p1 的类型仍然为 Base*

 

 

类型比较

结果

类型比较

结果

typeid(obj1) == typeid(p1)

false

typeid(obj1) == typeid(*p1)

true

typeid(&obj1) == typeid(p1)

true

typeid(obj1) == typeid(obj2)

false

typeid(obj1) == typeid(Base)

true

typeid(*p1) == typeid(Base)

true

typeid(p1) == typeid(Base*)

true

typeid(p1) == typeid(Derived*)

false

 

 

 

四、RTTI机制

1.#include <iostream>

using namespace std;

//基类

class Base{

public:

    virtual void func(); //虚函数

protected:

    int m_a;

    int m_b;

};

void Base::func(){ cout<<"Base"<<endl; }

//派生类

class Derived: public Base{

public:

    void func(); //虚函数的重写

private:

    int m_c;

};

 

void Derived::func(){ cout<<"Derived"<<endl; }

 

int main(){

    Base *p;

    int n;

  

    cin>>n;

    if(n <= 100){

        p = new Base();

    }else{

        p = new Derived();

    }

    cout<<typeid(*p).name()<<endl;

 

    return 0;

}

 

 

基类 Base 包含了一个虚函数,派生类 Derived 又定义了一个原型相同的函数遮蔽了它,这就构成了多态。p 是基类的指针,可以指向基类对象,也可以指向派生类对象;*p表示 p 指向的对象。

 

从代码中可以看出,用户输入的数字不同,*p表示的对象就不同,typeid 获取到的类型也就不同,编译器在编译期间无法预估用户的输入,所以无法确定*p的类型,必须等到程序真的运行了、用户输入完毕了才能确定*p的类型。

 

这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type IdentificationRTTI)。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。

 

 

多态(Polymorphism)是面向对象编程的一个重要特征,它极大地增加了程序的灵活性,C++C#Java 等“正统的”面向对象编程语言都支持多态。但是支持多态的代价也是很大的,有些信息在编译阶段无法确定下来,必须提前做好充足的准备,让程序运行后再执行一段代码获取,这会消耗更多的内存和 CPU 资源。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值