9.2.2 基类的派生类
让我们返回到在第8章开头看到的那个拥有public数据成员的CBox类:
- // Header file Box.h in project Ex9_01
- #pragma once
-
- class CBox
- {
- public:
- double m_Length;
- double m_Width;
- double m_Height;
-
- CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0):
- m_Length(lv), m_Width(wv), m_Height(hv){}
- };
新建一个名为Ex9_01的空WIN32控制台项目,将上面的代码存入该项目中名为Box.h的新头文件。#pragma once指令确保在编译过程中CBox的定义只能出现一次。该类有一个构造函数,因此我们可以在声明对象的同时将它们初始化。假设我们现在需要另一个对象类CCandyBox,该类的对象与CBox对象相同,但多了一个指向字符串的指针成员,被指向的字符串用于标识箱子容纳的内容。
我们可以像下面这样以CBox类为基类,将CCandyBox定义成一个派生类:
- // Header file CandyBox.h in project Ex9_01
- #pragma once
- #include "Box.h"
- class CCandyBox: CBox
- {
- public:
- char* m_Contents;
-
- CCandyBox(char* str = "Candy") // Constructor
- {
- m_Contents = new char[ strlen(str) + 1 ];
- strcpy_s(m_Contents, strlen(str) + 1, str);
- }
-
- ~CCandyBox() // Destructor
- { delete[] m_Contents; };
- };
把这个头文件添加到Ex9_01项目中。我们需要嵌入Box.h头文件的#include指令,因为代码中引用了CBox类。如果我们省略这条指令,则编译器将不识别CBox,因此我们的代码将不能编译。基类的名称CBox出现在派生类的名称CCandyBox之后,二者以冒号分开。在所有其他方面,这部分代码看起来都像是正常的类定义。我们添加了一个新成员m_Contents,因为它是个指向字符串的指针,所以需要有初始化该指针的构造函数和释放字符串所占用内存的析构函数。在构造函数中,我们还为描述CCandyBox对象所包含内容的字符串设定了默认值。CCandyBox类的对象包含基类CBox的所有成员,还包含新增的数据成员m_Contents。
注意最初在第6章看到的strcpy_s()函数的用法。这里有3个实参-- 复制操作的目标、目标缓冲区的长度和数据源。如果两个数组都是静态的-- 即都不是在堆上分配的,那么我们可以省略第二个实参,而只提供目标和数据源的指针。这样做是可以的,因为strcpy_s()函数还可以用作能够自动推断目标字符串长度的模板函数。因此,当我们处理静态字符串的时候,调用该函数时只需提供目标和源字符串作为实参即可。
试一试:使用派生类
现在,让我们在示例中看一下派生类的工作过程。将下面的代码添加到Ex9_01项目中,将其保存为名为Ex9_01.cpp的源文件:
- // Ex9_01.cpp
- // Using a derived class
- #include <iostream> // For stream I/O
- #include <cstring> // For strlen() and strcpy()
- #include "CandyBox.h" // For CBox and CCandyBox
- using std::cout;
- using std::endl;
-
- int main()
- {
- CBox myBox(4.0, 3.0, 2.0); // Create CBox object
- CCandyBox myCandyBox;
- CCandyBox myMintBox("Wafer Thin Mints");// Create CCandyBox object
-
- cout << endl
- << "myBox occupies " << sizeof myBox // Show how much memory
- << " bytes" << endl // the objects require
- << "myCandyBox occupies " << sizeof myCandyBox
- << " bytes" << endl
- << "myMintBox occupies " << sizeof myMintBox
- << " bytes";
-
- cout << endl
- << "myBox length is " << myBox.m_Length;
-
- myBox.m_Length = 10.0;
-
- // myCandyBox.m_Length = 10.0; // uncomment this for an error
-
- cout << endl;
- return 0;
- }
示例说明
这里的代码包含嵌入CandyBox.h头文件的#include指令,因为我们知道CandyBox.h包含嵌入Box.h的#include指令,所以这里不必再写一条嵌入Box.h的指令。我们可以在该文件中放上嵌入Box.h的#include指令,那样Box.h中的#pragma once指令将防止Box.h被多次嵌入。该功能非常重要,因为每个类只能被定义一次,所以代码中两次定义同一个类将引起错误。
在声明一个CBox对象和两个CCandyBox对象之后,我们输出每个对象所占用的字节数。输出如下所示:
- myBox occupies 24 bytes
- myCandyBox occupies 32 bytes
- myMintBox occupies 32 bytes
- myBox length is 4
第一行与我们根据第8章的讨论所预期的输出一致。CBox对象有3个double类型的数据成员,它们各自占用8个字节,总共是24个字节。两个CCandyBox对象大小相同-- 都占用32个字节。字符串的长度不影响对象的大小,因为容纳字符串的内存是在自由存储器上分配的。32个字节的构成情况如下:从基类CBox继承的3个double成员总共占用了24个字节,指针成员m_Contents要占用4个字节,加起来一共是28个字节。那么,其他4个字节从何而来?原因在于编译器要在8字节的倍数地址上对齐成员。通过再给CCandyBox类添加一个成员-- 比如是int类型,应该能够证实这一点。我们将发现,类对象的大小仍然是32个字节。
我们还输出了CBox对象myBox的m_Length成员。虽然我们没有任何难度地访问了CBox对象的m_Length成员,但如果解除main()函数中
- // myCandyBox.m_Length = 10.0; // uncomment this for an error
这条语句的注释状态,则程序将不能编译,编译器将生成下面的消息:
- error C2247: 'CBox::m_Length' not accessible because 'CCandyBox' uses
- 'private' to inherit from 'CBox'
该消息非常清楚地说明,来自基类的m_Length成员不可访问,原因是m_Length在派生类中已经成为私有成员。之所以如此,是因为当我们定义派生类时,默认的基类访问说明符是private,就好像派生类定义的第一行被写成下面这行代码一样:
- class CCandyBox: private CBox
为了控制派生类中从基类继承的成员的状态,我们必须总是写出基类的访问说明符。如果省略基类的访问说明符,则编译器将默认该说明符是private。如果我们将CandyBox.h文件中的CCandyBox类定义修改成如下形式:
- class CCandyBox: public CBox
- {
- public:
- char* m_Contents;
-
- CCandyBox(char* str = "Candy") // Constructor
- {
- m_Contents = new char[ strlen(str) + 1 ];
- strcpy_s(m_Contents, strlen(str) + 1, str);
- }
-
- ~CCandyBox() // Destructor
- { delete[] m_Contents; };
- };
则派生类继承的m_Length成员将成为public,因此在main()函数中是可以访问的。通过给基类指定public访问说明符,那些原来在基类中被指定为public的成员,在被派生类继承之后将仍然具有相同的访问级别。