- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
13.7 继承和动态内存分配
继承是怎样与动态内存分配(使用 new 和 delete)进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性。如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧。下面来看看这两种情况。
13.7.1 第一种情况:派生类不使用 new
假设基类使用了动态内存分配:
声明中包含了构造函数使用 new 时需要的特殊方法:析构函数、复制构造函数和重载赋值运算符。
现在,从 baseDMA 派生出 lackDMA 类,而后者不使用 new,也未包含其他一些不常用的、需要特殊处理的设计特性:
是否需要为 lackDMA 类定义显式析构函数、复制构造函数和赋值运算符呢?不需要。
首先,来看是否需要析构函数。如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数。实际上,派生类的默认构造函数总是要进行一些操作:执行自身的代码后调用基类析构函数。因为我们假设 lackDMA 成员不需执行任何特殊操作,所以默认析构函数是合适的。
接着来看复制构造函数。第 12 章介绍过,默认复制构造函数执行成员复制,这对于动态内存分配来说是不合适的,但对于新的 lacksDMA 成员来说是合适的。因此只需考虑继承的 baseDMA 对象。要知道,成员复制将根据数据类型采用相应的复制方式,因此,将 long 复制到 long 中是通过使用常规赋值完成的;但复制类成员或继承的类组件时,则是使用该类的复制构造函数完成的。所以,lacksDMA 类的默认复制构造函数使用显式 baseDMA 复制构造函数来复制 lacksDMA 对象的 baseDMA 部分。因此,默认复制构造函数对于新的 lacksDMA 成员来说是合适的,同时对于继承的 baseDMA 对象来说也是合适的。
对于赋值来说,也是如此。类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。
派生类对象的这些属性也适用于本身是对象的类成员。例如,第 10 章介绍过,实现 Stock 类时,可以使用 string 对象而不是 char 数组来存储公司名称。标准 string 类和本书前面创建的 String 类一样,也采用动态内存分配。现在,读者知道了为何这不会引发问题。Stock 的默认复制构造函数将使用 string 的复制构造函数来复制对象的 company 成员;Stock 的默认赋值运算符将使用 string 的赋值运算符给对象的 company 成员赋值;而 Stock 的析构函数(默认或其他析构函数)将自动调用 string 的析构函数。
13.7.2 第二种情况:派生类使用 new
假设派生类使用了 new:
在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。下面依次考虑这些方法。
派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。因此,hasDMA 析构函数必须释放指针 style 管理的内存,并依赖于 baseDMA 的析构函数来释放指针 label 管理的内存。
接下来看复制构造函数。BaseDMA 的复制构造函数遵循用于 char 数组的常规模式,即使用 strlen( ) 来获悉存储 C-风格字符串所需的空间、分配足够的内存(字符数加上存储空字符所需的 1 字节)并使用函数 strcpy( ) 将原始字符串复制到目的地:
hasDMA 复制构造函数只能访问 hasDMA 的数据,因此它必须调用 baseDMA 复制构造函数来处理共享的 baseDMA 数据:
需要注意的一点是,成员初始化列表将一个 hasDMA 引用传递给 baseDMA 构造函数。没有参数类型为 hasDMA 引用的 baseDMA 构造函数,也不需要这样的构造函数。因为复制构造函数 baseDMA 有一个 baseDMA 引用参数,而基类引用可以指向派生类型。因此,baseDMA 复制构造函数将使用 hasDMA 参数的 baseDMA 部分来构造新对象的 baseDMA 部分。
接下来看赋值运算符。BaseDMA 赋值运算符遵循下述常规模式:
由于 hasDMA 也使用动态内存分配,所以它也需要一个显式赋值运算符。作为 hasDMA 的方法,它只能直接访问 hasDMA 的数据。然而,派生类的显式赋值运算符必须负责所有继承的 baseDMA 基类对象的赋值,可以通过显式调用基类赋值运算符来完成这项工作,如下所示:
下述语句看起来有点奇怪:
但通过使用函数表示法,而不是运算符表示法,可以使用作用域解析运算符。实际上,该语句的含义如下:
当然编译器将忽略注释,所以使用后面的代码时,编译器将使用 hasDMA ::operator=( ),从而形成递归调用。使用函数表示法使得赋值运算符被正确调用。
总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。
13.7.3 使用动态内存分配和友元的继承示例
为演示这些有关继承和动态内存分配的概念,我们将刚才介绍过的 baseDMA、lacksDMA 和 hasDMA 类集成到一个示例中。程序清单 13.14 是这些类的头文件。除了前面介绍的内容外,这个头文件还包含一个友元函数,以说明派生类如何访问基类的友元。
程序清单 13.14 dma.h
程序清单 13.15 列出了类 baseDMA、lackDMA 和 hasDMA 的方法定义。
程序清单 13.15 dma.cpp
在程序清单 13.14 和程序清单 13.15 中,需要注意的新特性是,派生类如何使用基类的友元。例如,请考虑下面这个 hasDMa 类的友元:
作为 hasDMA 类的友元,该函数能够访问 style 成员。然而,还存在一个问题:该函数如不是 baseDMA 类的友元,那它如何访问成员 lable 和 rating 呢?答案是使用 baseDMA 类的友元函数 operator<<( )。下一个问题是,因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。因此,代码将参数 const hasDMA &转换成类型为 const baseDMA &的参数:
程序清单 13.16 是一个测试类 baseDMA、lackDMA 和 hasDMA 的小程序。
程序清单 13.16 usedma.cpp
程序清单 13.14~程序清单 13.16 组成的程序的输出如下:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论