C++面向对象总结——多态
引言
了解c++的三大特性是对c++的整体的认识。
-
封装性: 类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态(用访问修饰符设置)
- 继承:继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。作用:避免公用代码的重复开发,减少代码和数据冗余。
-
多态:多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。比如函数重载、运算符重载、虚函数等
前些章已经介绍了继承,重载,本篇就在此基础上详说一下多态。
一,C++ 多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
下面的实例中,基类 Shape 被派生为Rectangle类,如下所示:
#include <iostream> using namespace std; class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} int area() { cout << "Parent class area :" << endl; return 0; } }; //将Rectangle类继承Shape类 class Rectangle : public Shape { public: Rectangle(int a,int b) :Shape(a, b) { } int area() { cout << "Rectangle class area :" <<width*height<< endl; return 0; } }; // 程序的主函数 int main() { Shape* shape;//定义shpae类指针 Rectangle rec(10, 7);//派生类对象 // 基类指针指向派生类对象(存储矩形的地址) shape = &rec; // 调用矩形的求面积函数 area shape->area(); return 0; }
可以发现运行结果和我们期望的不一样。什么原因造成的呢?
我们直观上认为,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是本例的运行结果却告诉我们,当基类指针 shape指向派生类 Rectangle的对象时,虽然使用了 Rectangle的成员变量,但是却没有使用它的成员函数,导致输出结果不符合我们的预期。
换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。
但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:
class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} virtual int area() { cout << "Parent class area :" << endl; return 0; } };
修改后,当编译和执行前面的实例代码时,它会产生以下结果:(运行成功!)
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
二,虚函数
虚函数对于多态具有决定性的作用,有虚函数才能构成多态,这节我们来重点说一下虚函数的注意事项。
- 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
- 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。
C++继承时的名字遮蔽
1️⃣如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。(即使用派生类新增的成员)
2️⃣基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
- 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
- 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为
virtual void func();
,派生类虚函数的原型为virtual void func(int);
,那么当基类指针 p 指向派生类对象时,语句p -> func(100);
将会出错,而语句p -> func();
将调用基类的函数。 - 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
- 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
🧡构成多态的条件
下面是构成多态的条件:
- 必须存在继承关系;
- 继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系。
- 存在基类的指针,通过该指针调用虚函数。
下面的例子对各种混乱情形进行了演示:
#include <iostream> using namespace std; //基类Base class base { public: virtual void func() { cout << "void Base::func()" << endl; } virtual void func(int) { cout << "void Base::func(int)" << endl; } }; //派生类Derived class Dervied :public base { public: void func() { cout << "void Derived::func()" << endl; } void func(char*str) { cout << "void Derived::func(char *shr)" << endl; } }; int main() { base* p = new Dervied();//创建基类指针*p指向派生类对象 p->func(); p->func(10); //p->func("学习c++");//报错 }
输出结果:
在基类 Base 中我们将void func()
声明为虚函数,这样派生类 Derived 中的void func()
就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。
语句p -> func();
调用的是派生类的虚函数,构成了多态(由于派生类遮蔽了基类函数)
语句p -> func(10);
调用的是基类的虚函数,因为派生类中没有函数遮蔽它。
语句p -> func("学习c++");
出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。
💙纯虚函数和抽象类
如果我们想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数,语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
我们可以把基类中的虚函数 area() 改写如下:
class Shape { protected: int width, height; public: Shape(int a,int b):width(a),height(b){} //纯虚函数 virtual int area() = 0; };
area()= 0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
纯虚函数使用举例:
#include <iostream> using namespace std; //基类 line class line { public: line(float len):m_len(len){}//初始化列表 virtual float area() = 0;//纯虚函数 virtual float volume() = 0;//纯虚函数 protected: float m_len; }; //派生类 rect:基类 line class rect :public line { public: rect(float len,float width):line(len),m_width(width){} float area() { return m_len * m_width; } protected: float m_width; }; //派生类 cuboid:基类 rect class cuboid :public rect { public: cuboid(float len, float width, float height) :rect(len, width), m_height(height) {} float volume() { return m_len * m_width * m_height; } protected: float m_height; }; int main() { line* p = new cuboid(10,20,30);//基类指针指向派生类对象 cout << "The area of Cuboid is " << p->area() << endl; cout << "The volume of Cuboid is " << p->volume() << endl; }
本例定义了三个类,其继承关系为:line->rect->cuboid。
line是一个抽象类,也是最顶层的基类,在 line类中定义了两个纯虚函数 area() 和 volume()。
- 在 rect类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 rect类仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 rect也仍然是抽象类。
- 直到 cuboid类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。
可以发现,line类表示“线”,没有面积和体积,但它仍然定义了 area() 和 volume() 两个纯虚函数。这样的用意很明显:line类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。
抽象基类除了约束派生类的功能,还可以实现多态。指针 p 的类型是 line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 line类中将这两个函数定义为纯虚函数;如果不这样做,后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。
关于纯虚函数的几点说明:
1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。