- 现代C:概念剖析和编程实践
- (德)延斯·古斯泰特
- 2675字
- 2025-04-08 00:44:21
5.6 命名常量
即使在小程序中,一个常见的问题是,它们为达到某些目标使用特殊的值,而这些目标在文本中到处经常重复。如果由于这样或那样的原因这个值改变了,程序就会崩溃。举一个假设的例子,我们有字符串数组[1],我们想对其执行一些操作:
这里我们在几个地方使用了常量3
,它们有三个不同的“含义”。例如,向corvid中添加内容需要修改两个单独的代码。在实际的设置中,代码中可能有更多的地方依赖于这个特定的值,而在大型代码库中,维护这个值可能非常烦琐。
要点5.38 所有具有特定含义的常量都必须命名。
区分相等的常量同样重要,但相等只是一种巧合。
要点5.39 必须区分所有具有不同含义的常量。
令人惊讶的是,C几乎没有规定命名常量的方法,而且它的术语甚至会导致很多混淆,不知道哪些结构可以有效地生成编译时的常量。因此,在研究C所提供的唯一恰当命名的常量之前,我们首先必须弄清楚术语(5.6.1节):枚举常量(5.6.2节)。后者将帮助我们用更具解释性的内容替换示例中3
的不同版本。第二,通用机制使用简单的文本替换来补充此功能:宏(5.6.3节)。正如我们所看到的,宏只有在其替换是由基类型的字面量组成时,才会产生编译时的常量。如果我们想为更复杂的数据类型提供一些接近常量的东西,我们必须将它们作为临时对象提供(5.6.4节)。

5.6.1 只读对象
不要将术语常量(其在C语言中有非常特殊的含义)与不能修改的对象混淆。例如,在前面的代码中,根据我们的术语bird
、pronoun
和ordinal
不是常量,它们是常量限定的对象。这个限定符C规定我们无权更改此对象。对于bird
,无论是数组项还是实际的字符串都不能被修改,如果你尝试这样做,编译器应该会为你提供诊断:
要点5.40 常量限定类型的对象是只读的。
这并不意味着编译器或运行时系统可能不会更改这样一个对象的值:程序的其他部分可能在没有限定条件的情况下看到该对象并更改它。你不能直接写银行账户的摘要(但只能读),但这并不意味着它会随着时间的推移而一直保持不变。
不幸的是,还有另一类只读对象,其没有受到其类型的保护而不被修改:字符串文字。
要点5.41 字符串字面量是只读的。
如果今天引入,字符串字面量的类型肯定是char const
[]
,一个常量限定字符组成的数组。不幸的是,C语言引入const
关键字的时间比字符串字面量要晚得多,因此为了向后兼容,对它予以保留[2]。
像bird
等数组还使用另一种技术来处理字符串字面量。它们使用指针C类型char const*const
来“指向”字符串字面量。这种数组的可视化如下所示:

也就是说,字符串字面量本身并不存储在数组bird
中,而是存储在其他地方,而bird
只是指向这些地方。我们将在后面的6.2节和第11章中看到这个机制是如何工作的。
5.6.2 枚举
C语言有一个简单的机制来命名诸如我们在示例中需要的小整数,这称为枚举C。

这个声明了一个新的整型类型enum
corvid
,我们知道它有四个不同的值。
要点5.42 枚举常量要么有明确值,要么有位置值。
你可能已经猜到,位置值从0
开始,所以在我们的示例中,raven
的位置值为0,magpie
的位置值为1,jay
的位置值为2,corvid_num
的位置值为3。最后这个3显然是我们感兴趣的3。

注意,这对数组项使用了与以前不同的顺序,这是使用枚举方法的优点之一:我们不必手动跟踪在数组中使用的顺序。枚举类型中固定的顺序会自动执行此操作。
现在,如果我们想在corvid中添加新内容,只需把它放在列表中,放在corvid_num
之前的任何地方:
清单5.1 枚举类型和相关的字符串数组

对大多数其他窄类型来说,声明枚举类型的变量没有太多利害关系。无论如何,对于索引和算术运算,它们将被转换成一个更大的整数。甚至枚举常量本身也不是枚举类型:
要点5.43 枚举常量的类型为signed int
。
所以真正感兴趣的是常量,而不是新创建的类型。因此,我们可以命名任何需要的signed int
型常量,甚至不需要为类型名提供标记C:

要定义这些常量,我们可以使用整型常量表达式C(ICE)。这样的ICE提供了一个编译时整型值,并且受到很大的限制。它的值不仅在编译时必须确定(不允许函数调用),而且对象的计算也不能作为值的操作数参与:

这里,o42
是一个对象,但仍然是常量限定的,所以c52
的表达式不是“整型常量表达式”。
要点5.44 整型常量表达式不计算任何对象的值。
因此,ICE主要由带有整型字面量、枚举常量、_Alignof
和offsetof
子表达式,和一些sizeof
子表达式的任何操作数组成[3]。
但是,即使该值是一个ICE,为了能够使用它来定义枚举常量,你也必须确保该值适合signed
类型。
5.6.3 宏
不幸的是,除了C语言严格意义上的signed int
之外,没有其他机制可以声明其他类型的常量。相反,C提出了另一种强大的机制来引入程序代码的文本替换:宏C。宏由预处理程序C #define
引入:

这个宏定义的作用是,标识符M_PI
在下面的程序代码中被double
常量代替。这样一个宏观定义包括五个不同的部分:
1. 开头的#
字符必须是行中的第一个非空字符
2. 关键字define
3. 要声明的标识符,这里是M_PI
4. 替换文本,这里是3.14159265358979323846
5. 换行符
利用这个技巧,我们可以声明unsigned
、size_t
和double
常量的文本替换。实际上,已经定义了size_t
,size_MAX
的实现限制,以及我们已经看到的许多其他系统特性:EXIT_SUCCESS
、false
、true
、not_eq
、bool
、complex
等在这本书的彩色电子版中,这样的C标准宏都是用深红色打印的。
C标准中这些示例的写法不能代表在大多数软件项目中通常使用的规范。它们中的大多数都有相当严格的规则,使得宏在视觉上相比周围环境显得更突出。
要点5.45 宏名全部大写。
只有当你有充分的理由,特别是在你达到第3级之后,才可以偏离这条规则。
5.6.4 复合字面量
对于没有描述其常量的字面量的类型,事情会变得更加复杂。我们必须在宏的替换端使用复合字面量C。这样的复合字面量具有这种形式

也就是说,类型在括号内,后跟初始值设定。这里有一个例子:

这样,我们就可以省去bird
数组并重写for
循环:

虽然宏定义中的复合字面量可以帮助我们声明某些东西,其行为类似于所选类型的常量,但在狭义的C语言中,它不是一个常量。
要点5.46 复合字面量定义一个对象。
总的来说,这种形式的宏有一些缺陷:
- 复合字面量不适合ICE。
- 在这里,为了声明命名常量,类型
T
应该是常量限定C的。这将确保优化器有更多的空闲时间来为这样的宏替换生成好的二进制代码。 - 宏名和复合字面量的
()
之间必须有空格,这里用/**/
注释表示。否则,这将被解释为类似函数的宏定义的开始。我们稍后会看到这些。 - 行尾的退格字符
\
可用于将宏定义延续到下一行。 - 在宏定义的最后不能有
;
。记住,这只是文本替换。
要点5.47 不要在宏中隐藏结束的分号。
另外,为了宏的可读性,请考虑到偶尔才会读你代码的人:
要点5.48 宏的右缩进延续标记到同一列。
正如你在示例中所看到的,这有助于轻松地可视化宏定义的整个扩展。
[1]它使用char const*const
类型的指针来指向字符串。稍后我们将看到这种特殊的技术是如何工作的。
[2]存在第三种只读对象:临时对象。我们稍后将在13.2.2节中看到它们。
[3]我们将在12.7节和12.1节中处理后两个概念。