在C++中,纯虚函数(Pure Virtual Function)是一种特殊的虚函数,它在基类中被声明但没有定义。纯虚函数用于在基类中创建一个接口,要求所有派生类必须提供该函数的具体实现。
纯虚函数通过在虚函数声明的末尾加 = 0 来标识。如果一个类包含一个或多个纯虚函数,那么这个类就被称为抽象类(Abstract Class)。抽象类不能被实例化(即创建对象),因为至少有一个成员函数(即纯虚函数)没有被实现。
一、纯虚函数
纯虚函数是在声明虚函数时被“被始化”为0的函数,它的声明一般形式是:
virtual 函数类型 函数名(参数表列)= 0
注意的事项:
- 纯虚函数没有函数体;
- 最后面的“= 0”并不表示函数返回为0,它只是形式上的作用,告诉编译系统这是“纯虚函数”;
- 这是一个声明语句,最后应有分号。
示例代码如下:
virtual float area() const = 0; //纯虚函数
二、抽象类
抽象类是一种特殊的类,它包含一个或多个纯虚函数。由于抽象类至少包含一个纯虚函数,因此它不能被实例化。抽象类主要用于定义接口,即一组相关操作但不提供这些操作的具体实现,具体的实现细节留给派生类来完成。
如果在抽象类所派生的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用;这个派生类就不是抽象类,而是可以用来定义对象的具体类。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。
抽象类不能定义对象,但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。
三、实例应用
这里在程序案例中使用虚函数和抽象基类,定义顶层抽象基类Shape(形状),派生出Point(点)、Circle(圆)、Cylinder(圆柱体)与Shape类的直接派生类和间接派生类。
1)抽象类基类Shape
在抽象基类Shape中定义两个虚函数(包含函数体),一个纯虚函数(无函数体),示例代码如下:
// 声明抽象基类Shape
class Shape{
public:
virtual float area() const { return 0.0; } //虚函数
virtual float volume() const { return 0.0; } //虚函数
virtual void shapeName() const = 0; //纯虚函数
};
2)派生类Point
在派生类Point中增加其相关属性数据成员和成员函数,并对纯虚函数进行再次定义。示例代码如下:
// 声明派生类Point
class Point: public Shape{
protected:
float x, y;
public:
Point(float x = 0.0, float y = 0.0): x(x), y(y){} //定义构造函数
void setPoint(float x, float y){
this->x = x;
this->y = y;
}
float getX(){ return x; }
float getY(){ return y; }
// 对虚函数再次定义
virtual void shapeName() const { cout <<"Point:"; }
friend ostream & operator <<(ostream &, const Point &);
};
// 定义Point类的重载运算符友函数
ostream & operator <<(ostream &output, const Point &p){
output <<'[' <<p.x <<',' <<p.y <<']' <<endl;
return output;
}
3)派生类Circle
在派生类Circle中增加其相关属性数据成员和成员函数,并对纯虚函数进行再次定义。另外,因为圆要重新计算面积,所以要对area()函数重新定义。示例代码如下:
// 声明派生类Circle
class Circle: public Point{
protected:
float radius;
public:
Circle(float x = 0.0, float y = 0.0, float r = 0.0): Point(x, y), radius(r){} //定义构造函数
void setRadius(float r){
this->radius = r;
}
float getRadius(){ return radius; }
// 因为圆中要计算面积,所以需要对area重新定义
virtual float area() const{
return 3.1415926 * radius * radius;
}
// 对虚函数再次定义
virtual void shapeName() const { cout <<"Circle:"; }
friend ostream & operator <<(ostream &, const Circle &);
};
// 定义Point类的重载运算符友函数
ostream & operator <<(ostream &output, const Circle &c){
output <<'[' <<c.x <<',' <<c.y <<']' <<", radius=" <<c.radius <<endl;
return output;
}
4)派生类Cylinder
在派生类Cylinder中增加其相关属性数据成员和成员函数,并对纯虚函数进行再次定义。另外,圆柱体中表面积除了上下两个圆,还有柱面面积,所以area()需要重新定义;还有圆柱体中,要对体积重新计算,需对volume()函数重新定义。示例代码如下:
// 声明派生类Cylinder
class Cylinder: public Circle{
protected:
float height;
public:
Cylinder(float x = 0.0, float y = 0.0, float r = 0.0, float h = 0.0): Circle(x, y, r), height(h){} //定义构造函数
void setHeight(float h){
this->height = h;
}
float getHeight(){ return height; }
// 因圆柱体中,表面积除了上下两个圆,还有柱面面积,所以area()需要重新定义
virtual float area() const{
return 2 * Circle::area() + 2 * 3.1415926 * radius * height;
}
// 因圆柱体中,需要对volume()函数重新定义
virtual float volume() const{
return Circle::area() * height;
}
// 对虚函数再次定义
virtual void shapeName() const { cout <<"Cylinder:"; }
friend ostream & operator <<(ostream &, const Cylinder &);
};
// 定义Point类的重载运算符友函数
ostream & operator <<(ostream &output, const Cylinder &c){
output <<'[' <<c.x <<',' <<c.y <<']' <<", radius=" <<c.radius <<endl;
return output;
}
5)静态关联
使用对象名直接调用进行静态关联输出类Point、Circle、Cylinder的对象信息,示例代码如下:
int main(){
Point point(3.2, 4.5); // 建立Point对象
Circle circle(2.4, 1.2, 5.6); // 建立Circle对象
Cylinder cylinder(3.5, 4.5, 5.2, 10.5); // 建立Cylinder对象
// 静态关联输出
point.shapeName();
cout <<point <<endl;
circle.shapeName();
cout <<circle <<endl;
cylinder.shapeName();
cout <<cylinder <<endl;
return 0;
}
运行后输出结果如下图:
6)动态关联
通过基类Shape指针变量进行动态关联输出类Point、Circle、Cylinder的对象信息。为了方便统一输出,将在类Point中增加一个重载运算符输出,形参的常引用变量更改为常指针变量,代码如下:
friend ostream & operator <<(ostream &, const Point *);
修改后的Point类代码如下:
// 声明派生类Point
class Point: public Shape{
protected:
float x, y;
public:
Point(float x = 0.0, float y = 0.0): x(x), y(y){} //定义构造函数
void setPoint(float x, float y){
this->x = x;
this->y = y;
}
float getX(){ return x; }
float getY(){ return y; }
// 对虚函数再次定义
virtual void shapeName() const { cout <<"Point:"; }
friend ostream & operator <<(ostream &, const Point &);
friend ostream & operator <<(ostream &, const Point *);
};
// 定义Point类的重载运算符友函数(常引用)
ostream & operator <<(ostream &output, const Point &p){
output <<'[' <<p.x <<',' <<p.y <<']' <<endl;
return output;
}
//定义Point类的重载运算符友函数(常指针)
ostream & operator <<(ostream &output, Point *p){
output <<'[' <<p->getX() <<',' <<p->getY() <<']' <<"\narea=" <<p->area() <<"\nvolume=" <<p->volume() <<endl <<endl;
return output;
}
因为基类Shape中无数据成员和输出坐标成员函数,点坐标在类Point中,而且Point类继承了Shape基类中的虚函数和纯虚函数,圆Circle和圆柱体直接继承或间接继承了类Point,故重载运算符定义在类Point中,能满足类Point、Circle、Cylinder信息的需求。
main函数代码如下:
int main(){
Point point(3.2, 4.5); // 建立Point对象
Circle circle(2.4, 1.2, 5.6); // 建立Circle对象
Cylinder cylinder(3.5, 4.5, 5.2, 10.5); // 建立Cylinder对象
cout <<"dynamic output:" <<endl;
Shape *pt; //定义基类指针
pt = &point; // 指针指向Point
pt->shapeName();
cout <<&point;
pt = &circle; // 指针指向Circle
pt->shapeName();
cout <<&circle;
pt = &cylinder; // 指针指向Cylinder
pt->shapeName();
cout <<&cylinder;
return 0;
}
运行后如果如下图:
7)总结
本例中可以得到以下结论:
- 一个基类如果包含一个或一个以上纯虚函数,就是抽象基类,抽象基类不能也不必要定义对象。
- 抽象基类与普通基类不同,它一般并不是现实存在的对象的抽象,它可以没有任何物理上的或其他实际意义方面的含义。
- 在类的层次结构中,顶层或最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
- 抽象基类是本类族的公共接口。
- 区别静态关联和动态关联。如果通过对象名调用虚函数,在编译阶段就能确定调用是哪个类的虚函数,属于静态关联。如果是通过基类指针调用虚函数,在编译阶段无法从语句本身确定调用哪个类的虚函数,只能在运行时确认指向某一类对象后,才能确定调用的是哪个类的虚函数,属于动态关联。
- 如果在基类声明了虚函数,则在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和参数类型的函数,均为虚函数(不论在派生类中是否用virtual声明)。
- 纯虚函数是在抽象基数在中声明的,只是在抽象类中它才称为纯虚函数,在其派生类中虽然继承了该函数,但除非再次用"=0"把它声明为纯虚函数,否则它就不是也不能称为纯虚函数。如示例中Point类的shapeName()函数不能称为纯虚函数,只是虚函数。
- 使用虚函数提高了程序的可扩充性。
利用虚函数和多态性,程序员的注意力集中在处理普遍性,而让执行环境处理特殊性。多态性把操作的细节留给设计者去完成。
虚函数允许在派生类(derived class)中重写(override)基类(base class)中的函数。当使用基类的指针或引用来引用派生类对象,并调用一个虚函数时,会执行派生类中的重写版本,而不是基类中的原始版本。这就是所谓的“动态绑定”或“运行时绑定”。
多态性是面向对象编程的四大特征之一(封装、继承、多态、抽象),它允许不同的对象对同一消息做出不同的响应。在C++中,多态性主要通过虚函数来实现。