《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

简介:

本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第2章,第2.21节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.21 确保抽象的可靠性——类不变式和断言

C++面向对象高效编程(第2版)
任何抽象都必须与客户履行它的契约(contract)。当客户使用类时,他希望类的对象像其发布描述的那样运行正常。另一方面,类的实现者必须千方百计地确保对象运行正常。但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责。例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer)。只有满足此前提条件,成员函数才能保证它的行为。因此,客户必须履行一些义务。换言之,如果客户履行了她那部分契约,对象的实现者必须尊重客户那部分契约,并确保正当的行为。

在类和成员函数的文档中说明契约,这个主意不错。但是,把这些契约条件作为类的一部分实现代码,在执行类实现时检测契约条件,效果更好。这体现了断言(assertion)的巨大价值所在。

通常,断言是一个用于评估真假的表达式。如果表达式评估为假,则断言失败。例如,在TlaserDiscPlayer类中,Play成员函数会包含一个托盘关闭的断言。稍后即将介绍它的语法。

2.21.1 类不变式

进一步研究发现,在每个成员函数中(或甚至在一个成员函数的内部的多处)都包含一个断言可能并不方便。每个类都会在对象中包含一些恒为真的条件,无论对象调用任何成员函数,这些条件都必须为真。这样的条件称为类不变式(class invariant)。顾名思义,在对象中这些条件恒为真。如果我们以某种方式给类添加这些条件,并保证每个成员函数的代码都检查这些条件,将会非常方便。

2.21.2 前置条件和后置条件

除这些类不变式之外,成员函数可能会包含其他条件,在执行代码前必须保证这些条件为真。这些在操作开始被调用之前必须为真的条件,称为前置条件(precondition)。

C++:

在C和C++中,断言已经使用很长一段时间。所有的C和C++编译器都支持assert宏。该宏接受一个表达式,而且必须判断表达式的真假。倘若表达式判断为真,则继续执行;倘若表达式为假,则程序停止,并显示错误消息表明断言失败。消息中包含文件名,违规的语句源代码行号。这是最简单的(且唯一可用的)断言形式。Play成员函数的断言代码如下:

TLaserDiscPlayer::Play(unsigned atChapter)
{
  // 这是断言代码
  assert(this->_trayStatus == eClosed);
  // 略去其余代码
}```
Play成员函数在被调用之前,要求影碟播放机托盘关闭,这个条件表示为断言。
一旦成员函数完成它的操作,将会执行某些条件必须为真的断言。换言之,如果成员函数成功地执行完毕,它将生成一个满足某些条件的结果,这样的条件被称为后置条件(postcondition)。

Eiffel:

在Eiffel中,前置条件和后置条件都是非常流行的概念,而且得到了很好的支持。Eiffel运行期系统检查这些条件,并确保它们为真,否则,停止正在运行的程序。在require子句中,前置条件置于操作的开始。操作的后置条件将在操作末尾的ensure子句中说明。按照这样的方式,每个操作不管在何处被调用,都可以自由地使用任何前提和后置条件。

在进入和退出每个操作(成员函数)时,都必须检查类不变式。为了让实现者便于使用它,我们最好能将所有的不变式都置于类内部的已知区域。类不变式就是在对象的生命期内,必须保证对象状态的语句。例如,Person抽象中可能包含某个不变式,用于保证人的出生日期必须为有效数据,且姓名正确。与此类似,不允许透支的BankAccount抽象中可能包含余额不能小于零的不变式。这些不变式功能强大,它们允许实现者清楚地规定类的某些特性。客户在使用类的对象时,可以认为这些条件为真。

C++和Smalltalk都没有内置支持类不变式,但是Eiffel内置支持类不变式。

Eiffel:

在Eiffel中,类不变式定义在类内部的invariant区,每个操作内部都会检查它们。特别是,在进入和退出每个操作时,会检查类不变式是否为真。这确保可以随时预测类对象的行为。实际上,操作的每个前置条件和后置条件都包含对类不变式的判断。

###2.21.3 使用断言实现不变式和条件
既然C++未直接支持类不变式,我们就必须设法构造一些策略来达到类似的效果。assert宏可以在这派上用场,以下就是一个简单的策略。

简单地定义一对宏:PRE_CONDITION和POST_CONDITION,它们使用assert宏。

define PRE_CONDITION(condition) assert(condition)

define POST_CONDITION(condition) assert(condition)`

如果需要,可以在这些宏中添加消息,当断言失败时打印消息。

#define PRE_CONDITION(message, condition) assert( (message, (condition)))
#define POST_CONDITION(message, condition) assert( (message, (condition)))```
当断言失败,希望打印失败消息时,这会有所帮助。例如,你可以编写以下代码:

`PRE_CONDITION(“Laser disc tray is not closed”, (_trayStatus==eClosed));`
虽然这样的前置条件和后置条件只是简单的断言,但是,它们让程序更可靠、更易读,而且更加易于理解。

然而,添加对类不变式的支持并不容易。记住,在每次进入和退出每个成员函数时,类不变式都必须为真。要保证这一点,需要在一个函数(名为InvariantChecker)中定义一组条件,且在每次进入和退出每个成员函数时,都调用该函数。这样的方法冗长且易出错,因为每个成员函数必须要调用这个函数。如果在类中添加新函数,这样的方法甚至更易出错。但是,却没有其他可行的办法,有总比没有好。如果没有其他办法,至少要在文档中清楚地说明,用户才能明确地知道实现者所保证的契约是什么。很多类的设计者提供非常清楚的类不变式文档,并遵循一些策略确保不变式被强制执行。

注意:
当使用任何一种策略时,都要根据需要添加可关闭或开启策略的支持(一对`#ifdef`)。
###2.21.4 高效使用断言
高效使用断言可实现更可靠的程序。当条件不成立时,断言至少保证程序不会继续执行。但是发生断言失败,就不可能再复原。要解决这个问题,需要用到C++支持的真正的异常管理工具(参见第10章)。但是,理解和使用异常并不容易,它需要适当的架构和高效的设计。断言简单且易于实现,众多程序员已经使用多年。

hand即使你并不打算精心设计程序,但至少要练习使用简单的断言。

不要认为一旦开始使用断言,就不能转用其他更好的处理方式。如果将来决定使用真正的异常管理,只需在代码中找到所有调用assert的地方,然后用适当的throw语句替换即可。
相关文章
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
190 0
|
6月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
281 0
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
324 12
|
9月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
189 16
|
9月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
9月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
9月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
491 6
|
9月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
10月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。