9.6.5 抽象类
包含纯虚函数的类被称为抽象类,因为我们不能定义包含纯虚函数的类的对象。抽象类存在的唯一用途,就是定义派生类。如果抽象类的派生类将基类的纯虚函数仍然定义为纯虚函数,则该派生类也是抽象类。
我们不应该从上一个CContainer类的示例中得出抽象类不能拥有数据成员的结论。抽象类可以拥有数据成员和函数成员。纯虚函数是否存在是判断给定的类是否是抽象类的唯一条件。同样的道理,抽象类可以拥有多个纯虚函数。这种情况下,派生类必须给出基类中每个纯虚函数的定义,否则将仍然是抽象类。如果我们忘记将派生类的Volume()函数指定为const,则派生类同样将仍然是抽象类,因为它不仅包含我们定义的非const函数Volume(),还包含const纯虚函数成员Volume()。
试一试:抽象类
我们可以连同原来的CBox类一起再实现一个CCan类-- 它可能表示啤酒或可乐罐,令这两个类都派生自9.6.4小节定义的CContainer类。作为CContainer类的子类,CBox类的定义如下所示:
- // Box.h for Ex9_10
- #pragma once
- #include "Container.h" // For CContainer definition
- #include <iostream>
- using std::cout;
- using std::endl;
-
- class CBox: public CContainer // Derived class
- {
- public:
-
- // Function to show the volume of an object
- virtual void ShowVolume() const
- {
- cout << endl
- << "CBox usable volume is " << Volume();
- }
-
- // Function to calculate the volume of a CBox object
- virtual double Volume() const
- { return m_Length*m_Width*m_Height; }
-
- // Constructor
- CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0)
- :m_Length(lv),
m_Width(wv), m_Height(hv){} - protected:
- double m_Length;
- double m_Width;
- double m_Height;
- };
不带阴影的行与以前CBox类的版本相同。CBox类实质上与以前的示例中一样,只是这次我们将其指定为从CContainer类派生。Volume()函数在这个类中被完全定义(如果CBox类要用来定义对象,那么就必须如此)。仅有的其他选择是将其指定为纯虚函数,因为该函数在基类中就是纯虚函数,但那样我们将不能创建CBox对象。
我们可以像下面这样在Can.h头文件中定义CCan类:
- // Can.h for Ex9_10
- #pragma once
- #include "Container.h" // For CContainer definition
- extern const double PI; // PI is defined elsewhere
-
- class CCan: public CContainer
- {
- public:
- // Function to calculate the volume of a can
- virtual double Volume() const
- { return 0.25*PI*m_Diameter*m_Diameter*m_Height; }
-
- // Constructor
- CCan(double hv = 4.0, double dv = 2.0):
m_Height(hv), m_Diameter(dv){} -
- protected:
- double m_Height;
- double m_Diameter;
- };
CCan类也定义了Volume()函数,只不过依据的公式是hπr2,这里的h是罐的高度,r是罐体横截面的半径。CCan对象的体积是高乘以底面积。该函数中的表达式假定有一个已定义的全局常量PI,因此我们使用一条extern语句来指出PI是个在其他地方定义的const double类型的全局变量-- 该变量在本程序中是在Ex9_10.cpp文件中定义的。另外注意,我们在CBox类中重新定义了ShowVolume()函数,但在CCan类中却没有这样做。当我们得到程序的输出时,将看出结果有所不同。
我们可以用下面这个包含main()函数的源文件,练习一下这些类的用法。
- // Ex9_10.cpp
- // Using an abstract class
- #include "Box.h" // For CBox and CContainer
- #include "Can.h" // For CCan (and CContainer)
- #include <iostream> // For stream I/O
- using std::cout;
- using std::endl;
-
- const double PI= 3.14159265; // Global definition for PI
-
- int main(void)
- {
- // Pointer to abstract base class
- // initialized with address of CBox object
- CContainer* pC1 = new CBox(2.0, 3.0, 4.0);
-
- // Pointer to abstract base class
- // initialized with address of CCan object
- CContainer* pC2 = new CCan(6.5, 3.0);
-
- pC1->ShowVolume(); // Output the volumes of the two
- pC2->ShowVolume(); // objects pointed to
- cout << endl;
-
- delete pC1; // Now clean up the free store
- delete pC2; // ....
-
- return 0;
- }
示例说明
在这个程序中,我们声明了两个指向基类CContainer的指针。我们虽然不能定义CContainer对象(因为CContainer是抽象类),但仍然可以定义指向CContainer的指针,然后即可使用这种指针来存储派生类对象的地址。事实上,我们可以使用该指针存储类型为CContainer的直接或间接子类的任何对象的地址。指针pC1被赋予在自由存储器中由new运算符创建的CBox对象的地址。第二个指针以类似的方式被赋予CCan对象的地址。
注意:
当然,由于派生类对象是动态创建的,因此我们不再需要它们时必须使用delete运算符清理自由存储器。我们可以翻到第4章回顾关于delete运算符的内容。
该示例产生的输出如下:
- CBox usable volume is 24
- Volume is 45.9458
因为我们已经在CBox类中定义了ShowVolume()函数,所以CBox对象将调用该函数的派生类版本。我们在CCan类中没有定义这个函数,因此CCan对象将调用从基类继承的基类版本。因为Volume()函数在两个派生类中都是以虚函数形式实现的(必须如此,因为该函数在基类中是纯虚函数),所以对该函数的调用是在程序执行时被解析的,被选择的函数版本将属于被指向的对象所属的类。因此,对指针pC1来说,被调用的是CBox类的函数版本。对指针pC2而言,被调用的却是CCan类的函数版本。因此在每种情况下,我们都能获得正确的结果。
我们还可以只使用一个指针,并赋予该指针对象CCan的地址(在调用CBox对象的Volume()函数之后)。基类指针可以包含任何派生类对象的地址,即使相同的基类派生出多个不同的子类也无妨。因此,我们可以在整个派生类的范围内自动选择适当的虚函数。本小节的内容确实给我们留下了深刻的印象,不是吗?