《C++编程风格(修订版)》——3.4 封装

简介:

本节书摘来自异步社区出版社《C++编程风格(修订版)》一书中的第3章,第3.4节,作者:【美】Tom Cargill,更多章节内容可以访问云栖社区“异步社区”公众号查看。

3.4 封装

C++编程风格(修订版)
新的堆栈抽象更简单,程序的代码量也更小,并且也使用了更少的内存。然而,在StackIndex及其派生类的关系中还存在着尚未暴露出来的问题。在本章的前面部分,我们已经注意到,派生类的成员函数push()和pop()隐藏了它们从基类继承而来的同名成员函数。不过,我们也可以在IntStack和CharStack对象上来调用这些基类函数,这就需要使用函数的完全解析名字:StackIndex::push()和StackIndex::pop()。事实上,在它们各自的派生类成员函数中,也正是通过这种方式来调用StackIndex::push()和StackIndex::pop()的。

不过,即使不使用作用域解析运算符,我们还是可以访问到基类的成员函数push()和pop()。例如,当通过StackIndex类型的指针或者引用来使用IntStack或CharStack的对象时,那么调用的将是基类的成员函数push和pop。然而,这种基类成员函数的可访问性将会严重地破坏堆栈的封装性:程序中的客户代码可能会使得堆栈对象处于不一致的状态。在下面的函数中给出了破坏IntStack封装性的3种方法:
image

通过调用基类的公有成员函数,violate()将堆栈的索引推进了一位,但却没有提供一个数值压入到堆栈中。这样导致的结果就是堆栈的大小增加了1,但由于在入栈的时候并没有提供一个值,因此在随后调用pop()时,所返回的值将是未定义的:这个值将是进行压入操作时,在相应数组元素内存位置上的任意值。

我们已经看到了由于客户代码可以直接操纵堆栈的实现,因此将导致了堆栈对象处于未定义的状态,这样堆栈抽象的封装性就被破坏了。在我们修改代码来消除基类中的指针数组时,这个问题并没有被暴露出来。其实,在最初的代码中,这个封装性的漏洞就已经存在了。现在我们必须堵上这个漏洞。
image

与继承相关的还有一个问题:基类的析构函数并没有被声明为虚函数。如果动态创建了一个IntStack对象,并通过基类型的指针来删除这个对象时,那么将只会调用基类的析构函数。从这个意义来看,析构函数的行为与其他成员函数的行为一样:析构函数的调用取决于指针的类型。

由于派生类的析构函数没有被调用,因此程序在使用IntStack或者CharStack时就存在着潜在的内存泄漏。在第2章中,string类的内存泄漏问题是由于在成员函数中遗漏了对delete的调用。而在本程序中,即使派生类的析构函数是正确的,也还会产生内存泄漏。只有当派生类的析构函数被调用时,函数中的delete才会执行,而现在当我们通过基类型的指针来删除派生类的对象时,派生类的析构函数并不会被调用。如果为堆栈数据分配的数组没有被删除,那么在程序中将会不断积累垃圾内存并将最终耗尽内存空间。在软件运行的早期,内存泄漏很难被检测出来,而小规模的测试并不会消耗过多的内存,因此也无法产生明显的错误现象。在第7章中将给出如何通过程序来监视内存的使用情况以及如何发现内存泄漏。

我们可以通过在基类中声明一个虚的析构函数来改正这个潜在的内存泄漏,但这样做只是一种权宜的解决方案,并不能反映出代码中真正的结构性问题。对于这些类,我们可以找出更好的解决方案。我们将在第4章和第9章中再次讨论基类的虚析构函数问题。

事实上,我们可以有两种解决方案,这两种方案都可以同时解决封装性的漏洞问题和内存泄漏问题。第一种方案是将StackIndex作为一个私有继承的基类。私有继承不但能够防止基类的公有接口成为派生类公有接口的一部分,还能够防止将基类型的指针或者引用指向派生类的对象。如果将StackIndex作为一个私有的基类,那么在编译函数violate()时将产生一系列的错误信息:
image

任何对私有基类成员的访问都是非法的,同样,任何将私有基类型的指针或者引用指向派生类的对象也是非法的。这样,在析构过程中的问题也就同时得到了解决。由于私有基类型的指针不能指向派生类的对象,因此通过基类型的指针来对派生类对象进行的delete操作也就不存在。

相关文章
|
2月前
|
存储 安全 数据管理
探索C++中回调函数的数据结构和封装的权衡以及示例
探索C++中回调函数的数据结构和封装的权衡以及示例
74 4
|
4月前
|
编译器 C++ 容器
【C++学习手札】基于红黑树封装模拟实现map和set
【C++学习手札】基于红黑树封装模拟实现map和set
|
9天前
|
C++
C++ 访问说明符详解:封装数据,控制访问,提升安全性
C++ 中的访问说明符(public, private, protected)用于控制类成员的可访问性,实现封装,增强数据安全性。public 成员在任何地方都可访问,private 只能在类内部访问,protected 则允许在类及其派生类中访问。封装提供数据安全性、代码维护性和可重用性,通过 setter/getter 方法控制对私有数据的访问。关注公众号 `Let us Coding` 获取更多内容。
24 1
|
17天前
|
编译器 C语言 C++
【C++成长记】C++入门 | 类和对象(上) |面向过程和面向对象初步认识、类的引入、类的定义、类的访问限定符及封装
【C++成长记】C++入门 | 类和对象(上) |面向过程和面向对象初步认识、类的引入、类的定义、类的访问限定符及封装
|
24天前
|
存储 编译器 程序员
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
【C++】类和对象①(什么是面向对象 | 类的定义 | 类的访问限定符及封装 | 类的作用域和实例化 | 类对象的存储方式 | this指针)
|
1月前
|
C++
【C++】C++封装成DLL并调用(初学者快速入门)
【C++】C++封装成DLL并调用(初学者快速入门)
|
1月前
|
JSON Linux API
一个C++版本的Sqlite3封装--SmartDb
一个C++版本的Sqlite3封装--SmartDb
14 0
|
2月前
|
编译器 API C++
【C++ 动态库设计】动态库中的模板函数:解决如果将模板函数封装成API库
【C++ 动态库设计】动态库中的模板函数:解决如果将模板函数封装成API库
50 0
|
2月前
|
Unix Linux 测试技术
C++封装详解——从原理到实践
C++封装详解——从原理到实践
47 0
|
2月前
|
C++
C++ 数据封装的方法,重点是其编程思想
在C++中,数据封装一般指的是将数据和操作这些数据的函数绑定在一起的程序设计方式。通常使用C++的类来实现
37 7