多态性是面向对象程序设计的一个重要特征,利用多态性可以设计和实现一个易于扩展的系统。
在C++语言中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。
(1)虚函数的概念
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为 动态链接,或 后期绑定。
C++ 中虚函数可以继承,当一个成员函数被声明为虚函数后,其派生类中的,同名函数都自动称为虚函数,但如果派生类没有覆盖基类的虚函数,调用时则直接调用基类的虚函数。
演示代码如下:
#include <iostream>
using namespace std;
class Personal
{
public:
virtual void print()
{
cout << "我是一个人" << endl;
}
};
class Student :public Personal
{
public:
void print()
{
cout << "我是一个学生" << endl;
}
};
int main()
{
Student student;
student.print();
return 0;
}
基类和派生类都有无参数的print函数,一般情况下,基类和派生类不能有声明和定义完全一样的函数,否则编译器会报错。然而,在基类的函数前面添加 virtual 关键字,此时,派生类允许存在和基类一模一样的函数。相当于,基类的 print 函数被派生类的 print 函数覆盖了。
此时,Student 类中的 print 函数自动称为虚函数。
(2)利用虚函数实现动态绑定
多态主要体现在虚函数上,只要有虚函数存在,对象类型就会在程序运行时进行动态绑定。动态绑定的实现方法是定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象,通过该指针变量调用此虚函数。
演示代码如下:
#include <iostream>
using namespace std;
class Personal
{
public:
virtual void print()
{
cout << "我是一个人" << endl;
}
};
class Student :public Personal
{
public:
void print()
{
cout << "我是一个学生" << endl;
}
};
class Teacher :public Personal
{
public:
void print()
{
cout << "我是一个教师" << endl;
}
};
int main()
{
Personal* student = new Student(); // 上转型
student->print();
Personal* teacher = new Teacher(); // 上转型
teacher->print();
return 0;
}
通过上转型的方式,指针是基类的指针,指向派生类。
(3)纯虚函数
通过上面的例子,我们发现,基类中的 print 函数实现了,但是没有被使用,在程序设计中,可能也会存在这种场景,如果是这种场景的话,干脆将基类中的 print 函数设置为纯虚函数。
class Personal
{
public:
virtual void print() = 0;
};
print() = 0 告诉编译器,函数没有主体,这个函数为纯虚函数。
(4)虚函数的限制
虚函数有以下几方面的限制:
(1)只有类的成员函数才能为虚函数;
(2)静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象;
(3)内联函数不能是虚函数,因为内联函数不能在运行中动态确定其位置;
(4)构造函数不能是虚函数,析构函数通常是虚函数;
(5)多继承
多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
image.png类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:
#include <iostream>
using namespace std;
class A
{
protected:
int a;
};
class B:public A
{
protected:
int b;
};
class C :public A
{
protected:
int c;
};
class D :public B, public C
{
protected:
int d;
public:
void setA(int a)
{
this->a = a;
}
void setB(int b)
{
this->b = b;
}
void setC(int c)
{
this->c = c;
}
void setD(int d)
{
this->d = d;
}
};
int main()
{
D d;
return 0;
}
代码中,setA函数的代码是错误的,编译器会直接报错,因为 a 的来源并不明确,类D继承类B和类C,然而变量 a,不知道来源于 B 还是 C,因此产生冲突。
想要解决这个问题,就必须告诉编译器 a 的来源:
void setA(int a)
{
this->B::a = a;
}
或者
void setA(int a)
{
this->C::a = a;
}
(6)虚继承和虚基类
为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。
将类B和类C虚继承类A:
class B:virtual public A
{
protected:
int b;
};
class C :virtual public A
{
protected:
int c;
};
加入关键字 virtual 关键字,使得派生类D中只保留一份成员变量 a,直接访问就不会存在冲突。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。
其中,这个被共享的基类就称为 虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
如果重新梳理菱形继承,如图:
image.png(7)抽象类
包含有纯虚函数的类称为抽象类,一个抽象类至少具有一个纯虚函数。抽象类只能作为基类派生出新的子类,而不能在程序中被实例化,但是可以使用指向抽象的指针。
在程序设计中,可以使用纯虚函数建立接口,然后让程序员填写代码实现接口。
下面看一个形状类:
class Shape // 形状
{
public:
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
Shape 类中的 getArea 函数是纯虚函数,所以 Shape 类是 抽象类。getArea 计算形状的面积,但是我们根本不知道是什么形状,所以面积也无法计算,只能将计算面积的方法抽象化,含有抽象函数的类必然也是抽象类。
抽象类不能直接实例,它必须做为基类,给派生类继承使用。
此时,定义一个矩形,继承形状类:
class Rectangle : public Shape // 矩形
{
public:
int getArea()
{
return (width * height);
}
};
通过传入矩形的长和宽,调用 getArea 函数就可以求出矩形的面积:
Rectangle* rect = new Rectangle();
rect->setHeight(10);
rect->setWidth(10);
cout << "矩形的面积:" << rect->getArea() << endl;
[本章完...]
网友评论