常量是一个有名字、并且承诺不会改变的值。C 语言为此提供了三种机制,它们在底层确实是不同的东西:一种是变量,一种是文本替换,还有一种是整数类型。
为什么要给值命名
对比下面两行:
double area = 3.14159 * r * r;
double area = PI * r * r;
第二行更好,原因有三个,而且迟早都会体现出来。它说明了这个数字的含义。它只出现在一个地方,所以修改精度只需改一处,不用到处搜索。而且把 PI 拼错是编译错误,把 3.14195 写错却只会得到错误的结果。
散落在代码中、没有名字的数字被称为魔法数字,消除它们是改进 C 程序代价最小的方法之一。
const
const 关键字把变量标记为只读:
取消那行赋值的注释,编译器就会拒绝:assignment of read-only variable 'PI'。在构建时报出这个错误,正是 const 的意义所在。
const 变量是真正的变量。它有类型,所以编译器会检查你的用法。它遵守作用域规则,所以函数内的 const 不会泄漏到外面。它有地址,所以你可以取一个指向它的指针。调试器也能显示它的名字和值。
const 必须在声明时初始化,没有第二次机会:
const int limit; /* error (or garbage forever): nothing can assign to it later */
const int limit = 50; /* correct */
#define
预处理器提供的另一种方式是宏:
这不是变量。在编译器运行之前,预处理器会把源代码中出现的每一个 PI 都替换成 3.14159265358979 这串字符。编译器根本看不到这个名字,这正是 #define 没有类型、也无法出现在调试器中的原因。
注意这里没有分号,也没有 =。宏是原始文本,所以下面这个常见错误:
#define MAX_USERS 100; /* note the semicolon */
int limit = MAX_USERS + 1; /* expands to: int limit = 100; + 1; -> error */
会在一行看起来和定义毫不相干的代码上报错。宏使用大写命名的约定,正是为了让读者知道这里发生了替换。
宏按惯例写成 UPPER_CASE;当 const 变量表示同样的概念时,通常也沿用这个约定。
枚举常量
第三种形式给你一组有名字的整数:
如果不手动赋值,值从 0 开始,依次加一,所以 MON 是 0,FRI 是 4。匿名的 enum { ... } 是定义普通整数常量的常见写法。
与另外两种方式相比,枚举常量有一个实实在在的优势:它们是编译器能理解的编译期整数常量,而且调试器也能看到。完整的内容见 C 语言枚举。
该用哪一种
| 需求 | 使用 |
|---|---|
| 有类型、有作用域、可调试的常量 | const |
数组大小、case 标签、位域宽度 | enum 或 #define |
| 字符串常量 | #define 或 const char * |
| 浮点常量 | const double |
| 一组相关的整数值 | enum |
预处理器必须能看到的内容(#if、头文件保护) | #define |
默认应该用 const。当这个值必须在编译之前就存在时,用 #define;需要编译期整数或一组有名字的值时,用 enum。
真正容易出问题的地方是数组大小:
在 C 语言中,const int 是一个只读变量,而不是编译期常量表达式。在函数内部,C99 会把 int local[const_size] 当作变长数组来接受;在文件作用域、struct 中或作为 case 标签时,它会被直接拒绝。enum 和 #define 在任何地方都能用。
(这是 C 和 C++ 的真正区别之一:在 C++ 中,const int 就是常量表达式。在两种语言之间迁移的代码常常在这里栽跟头。)
const 与指针
const 和指针有三种组合方式,它们的含义不能互换。从变量名开始向外阅读声明:
const char *p; /* p points at chars that must not be modified */
char * const q; /* q itself cannot be re-pointed; the chars can change */
const char * const r; /* neither the pointer nor the chars can change */
const char * 是你最常写的形式,因为对于只读取字符串、不修改它的函数参数来说,这才是正确的类型:
给只读参数加上 const 没有任何代价,还能以编译器强制执行的方式写明这份约定。值得在所有地方这样做。
字符串字面量实际上是 const
一个 const 正好能抓住的隐蔽陷阱:
char *s = "hello"; /* legal C, but s points into read-only memory */
s[0] = 'H'; /* undefined behavior - usually a crash */
字符串字面量存放在可执行文件的只读区域中。通过那个指针写入,通常会导致段错误。把指针声明为 const char *s = "hello";,编译器就会拒绝这次赋值,而不是让它在运行时崩溃。
如果你需要一个可以修改的字符串,就把它复制到数组里:
常见问题
C 语言怎么定义常量?
在声明前加上 const 并赋值:const double PI = 3.14159;。之后编译器会拒绝任何对它赋值的尝试。较老的另一种方法是预处理宏 #define PI 3.14159,它做的是文本替换。
C 语言中 const 和 #define 有什么区别?
const 创建的是编译器知道的、有类型的真正变量,所以会进行类型检查,能在调试器中看到,也遵守作用域规则。#define 是编译前进行的纯文本替换:没有类型,没有作用域,调试时也无法查看。优先使用 const,除非你需要在要求编译期常量的地方使用这个值。
C 语言中 const 变量可以作为数组大小吗?
不能用于文件作用域中的定长数组。const int N = 10; int arr[N]; 在 C99 中是一个变长数组(在函数内部允许,在文件作用域和结构体中不允许)。要得到真正的编译期大小,请使用 #define N 10 或 enum { N = 10 };。
const char *p 是什么意思?
它是一个指向常量 char 的指针:你可以改变 p 指向哪里,但不能修改它指向的字符。char * const p 正好相反:一个指向可修改 char 的常量指针。const char * const p 则两者都锁定。从变量名开始向外阅读声明。