重要的事情说三遍:
在深入学习本章节之前,您应该对指针和类继承有充分的理解。如果您不确定以下表达式的含义,建议您复习对应的章节:
语句: | 解释章节: |
---|---|
int A::b(int c) { } | 类 |
a->b | 数据结构 |
class A: public B {}; | 友元和继承 |
指向基类的指针
类继承的一个关键特性是指向派生类的指针与指向其基类的指针在类型上是兼容的。多态性就是利用这个简单但强大和灵活的特性。
关于矩形和三角形类的例子可以重新编写,使用指针来体现这一特性:
// 指向基类的指针
#include <iostream>
using namespace std;
class Polygon {
protected:
int width, height;
public:
void set_values (int a, int b)
{ width=a; height=b; }
};
class Rectangle: public Polygon {
public:
int area()
{ return width*height; }
};
class Triangle: public Polygon {
public:
int area()
{ return width*height/2; }
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
ppoly1->set_values (4,5);
ppoly2->set_values (4,5);
cout << rect.area() << '\n';
cout << trgl.area() << '\n';
return 0;
}
在这个例子中,main
函数声明了两个指向 Polygon
的指针(分别为 ppoly1
和 ppoly2
)。这些指针被分别赋值为 rect
和 trgl
的地址,这两个对象分别是 Rectangle
和 Triangle
类型。这样的赋值是合法的,因为 Rectangle
和 Triangle
都是从 Polygon
派生的类。
解引用 ppoly1
和 ppoly2
(使用 ppoly1->
和 ppoly2->
)是合法的,并且允许我们访问它们所指向对象的成员。例如,以下两条语句在前面的例子中是等价的:
ppoly1->set_values (4,5);
rect.set_values (4,5);
但是,由于 ppoly1
和 ppoly2
的类型是指向 Polygon
的指针(而不是指向 Rectangle
或 Triangle
的指针),所以只能访问从 Polygon
继承的成员,而不能访问派生类 Rectangle
和 Triangle
的成员。这就是为什么上面的程序直接使用 rect
和 trgl
来访问它们的 area
成员,而不是通过指针访问;指向基类的指针不能访问 area
成员。
如果 area
是 Polygon
的成员而不是派生类的成员,那么可以通过指向 Polygon
的指针访问 area
,但是问题在于 Rectangle
和 Triangle
实现了不同版本的 area
,因此在基类中没有一个通用版本可以实现。
虚成员(Virtual members)
虚成员是指可以在派生类中重新定义的成员函数,同时通过引用保留其调用属性。要使函数成为虚函数,需要在其声明前加上 virtual
关键字:
// 虚成员
#include <iostream>
using namespace std;
class Polygon {
protected:
int width, height;
public:
void set_values (int a, int b)
{ width=a; height=b; }
virtual int area ()
{ return 0; }
};
class Rectangle: public Polygon {
public:
int area ()
{ return width * height; }
};
class Triangle: public Polygon {
public:
int area ()
{ return (width * height / 2); }
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon poly;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
Polygon * ppoly3 = &poly;
ppoly1->set_values (4,5);
ppoly2->set_values (4,5);
ppoly3->set_values (4,5);
cout << ppoly1->area() << '\n';
cout << ppoly2->area() << '\n';
cout << ppoly3->area() << '\n';
return 0;
}
在这个例子中,所有三个类(Polygon
、Rectangle
和 Triangle
)都有相同的成员:width
、height
,以及函数 set_values
和 area
。
成员函数 area
在基类中被声明为虚函数,因为它在每个派生类中都会被重新定义。非虚成员也可以在派生类中重新定义,但通过基类的引用无法访问派生类的非虚成员:即,如果在上述例子中去掉 area
声明中的 virtual
关键字,所有三次对 area
的调用都将返回零,因为在所有情况下,都会调用基类的版本。
因此,virtual
关键字的作用是允许派生类中与基类同名的成员函数在从基类指针调用时能够正确地调用,特别是当指针类型是指向基类的指针并指向派生类的对象时,如上例所示。
声明或继承虚函数的类称为多态类。
注意,尽管其成员之一是虚函数,Polygon
仍然是一个常规类,甚至还实例化了一个对象(poly
),其成员 area
的定义始终返回 0。
抽象基类(Abstract base classes)
抽象基类与上例中的 Polygon
类非常相似。它们是只能用作基类的类,因此允许有不带定义的虚成员函数(称为纯虚函数)。语法是将它们的定义替换为 =0
(等号和零):
抽象基类 Polygon
可能如下所示:
// 抽象类 Polygon
class Polygon {
protected:
int width, height;
public:
void set_values (int a, int b)
{ width=a; height=b; }
virtual int area () =0;
};
注意 area
没有定义;它被替换为 =0
,这使其成为纯虚函数。包含至少一个纯虚函数的类称为抽象基类。
抽象基类不能用于实例化对象。因此,这个抽象基类版本的 Polygon
不能用来声明对象,如:
Polygon mypolygon; // 如果 Polygon 是抽象基类,则无法工作
但抽象基类并不是完全无用。它可以用来创建指向它的指针,并利用其所有的多态能力。例如,以下指针声明是合法的:
Polygon * ppoly1;
Polygon * ppoly2;
并且当它们指向派生(非抽象)类的对象时,可以实际解引用。以下是完整的例子:
// 抽象基类
#include <iostream>
using namespace std;
class Polygon {
protected:
int width, height;
public:
void set_values (int a, int b)
{ width=a; height=b; }
virtual int area (void) =0;
};
class Rectangle: public Polygon {
public:
int area (void)
{ return (width * height); }
};
class Triangle: public Polygon {
public:
int area (void)
{ return (width * height / 2); }
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
ppoly1->set_values (4,5);
ppoly2->set_values (4,5);
cout << ppoly1->area() << '\n';
cout << ppoly2->area() << '\n';
return 0;
}
在这个例子中,不同但相关的对象类型使用统一的指针类型(Polygon*
)进行引用,并且每次都调用正确的成员函数,只是因为它们是虚函数。这在某些情况下非常有用。例如,甚至可以让抽象基类 Polygon
的成员使用特殊指针 this
来访问正确的虚成员,即使 Polygon
本身没有这个函数的实现:
// 纯虚成员可以从抽象基类调用
#include <iostream>
using namespace std;
class Polygon {
protected:
int width, height;
public:
void set_values (int a, int b)
{ width=a; height=b; }
virtual int area() =0;
void printarea()
{ cout << this->area() << '\n'; }
};
class Rectangle: public Polygon {
public:
int area (void) override
{ return (width * height); }
};
class Triangle: public Polygon {
public:
int area (void) override
{ return (width * height / 2); }
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
ppoly1->set_values (4,5);
ppoly2->set_values (4,5);
ppoly1->printarea();
ppoly2->printarea();
return 0;
}
override
关键字确保这些方法正确地覆盖了基类 Polygon
的纯虚函数 area
。如果基类的函数签名发生了变化(例如函数名或参数列表变化),编译器会报错,而不是静默地生成一个新的成员函数。
虚成员和抽象类赋予了 C++ 多态特性,这在面向对象项目中非常有用。当然,上述示例是非常简单的用例,但这些特性可以应用于对象数组或动态分配的对象。
这里是一个结合了最近章节中的一些特性,如动态内存、构造函数初始化器和多态性的例子:
// 动态分配和多态性
#include <iostream>
using namespace std;
class Polygon {
protected:
int width, height;
public:
Polygon (int a, int b) : width(a), height(b) {}
virtual int area (void) =0;
void printarea()
{ cout << this->area() << '\n'; }
};
class Rectangle: public Polygon {
public:
Rectangle(int a,int b) : Polygon(a,b) {}
int area() override
{ return width*height; }
};
class Triangle: public Polygon {
public:
Triangle(int a,int b) : Polygon(a,b) {}
int area() override
{ return width*height/2; }
};
int main () {
Polygon * ppoly1 = new Rectangle (4,5);
Polygon * ppoly2 = new Triangle (4,5);
ppoly1->printarea();
ppoly2->printarea();
delete ppoly1;
delete ppoly2;
return 0;
}
注意 ppoly
指针:
Polygon * ppoly1 = new Rectangle (4,5);
Polygon * ppoly2 = new Triangle (4,5);
被声明为“指向 Polygon
的指针”类型,但分配的对象直接声明为派生类类型(Rectangle
和 Triangle
)。