高质量程序设计指南:C++/C语言
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

5.1 认识常量

5.1.1 字面常量

字面常量也许是在程序中使用得最多的常量了,例如,直接出现的各种进制的数字、字符(''括住的单个字符)或字符串(""括住的一系列字符)等。实际上,只存在基本数据类型的字面常量。示例5-1是一些字面常量的例子。

示例5-1

            x = -100.25f;
            #define OPEN_SUCCESS  0x00000001
            char c = 'a';
            char*pChar="abcdef";   //取字符串常量的地址
            int *pInt = NULL;

由于字面常量只能引用,不能修改,所以语言实现一般把它保存在程序的符号表里而不是一般的数据区中。符号表是“只读”的,其实它是一种访问保护机制,千万不要理解为只读存储器(ROM)。除了字符串外,你无法取一个字面常量的地址,例如,int *p = &5;这类语句是错误的。当你试图通过常量字符串的地址修改其中的字符时就会报告“只读”错误。例如:

                *(pChar+2)='k';  // 错误,不能修改字面常量的内存单元

如果程序中到处都充斥着各种各样的字面常量,那么就可能存在相同的常量,我们应该把相同的常量合并,以减少内存消耗。有些连接器自动执行常量合并,而有的连接器则提供了常量合并的开关,以优化程序的效率。如果你的编译链接环境支持常量合并,那么请将它打开。

5.1.2 符号常量

存在两种符号常量:用#define定义的宏常量和用const定义的常量。由于#define是预编译伪指令,它定义的宏常量在进入编译阶段前就已经被替换为所代表的字面常量了,因此宏常量在本质上是字面常量。在C语言中,用const定义的常量其实是值不能修改的变量,因此会给它分配存储空间(外连接的)。但是在C++中,const定义的常量要具体情况具体对待:对于基本数据类型的常量,编译器会把它放到符号表中而不分配存储空间,而ADT/UDT的const对象则需要分配存储空间(大对象)。还有一些情况下也需要分配存储空间,例如,强制声明为extern的符号常量或取符号常量的地址等操作,都将强迫编译器为这些常量分配存储空间,以满足用户的要求。

你可以取一个const符号常量的地址:对于基本数据类型的const常量,编译器会重新在内存中创建它的一个拷贝,你通过其地址访问到的就是这个拷贝而非原始的符号常量;而对于构造类型的const常量,实际上它是编译时不允许修改的变量,因此如果你能绕过编译器的静态类型安全检查机制,就可以在运行时修改其内存单元,见示例5-2。

示例5-2

            const long lng = 10;
            long*pl=(long*)&lng;          // 取常量的地址
            *pl=1000;                     // “迂回修改”
            cout<<*pl<<endl;              //1000,修改的是拷贝内容!
            cout<<lng<<endl;              //10,原始常量并没有变!
            class Integer {
            public:
              Integer() : m_lng(100) { }
              long m_lng;
            }
            const Integer int_1;
            Integer*pInt=(Integer*)&int_1;// 去除常数属性
            pInt->m_lng = 1000;
            cout<<pInt->m_lng<<endl;      //1000,修改const对象
            cout<<int_1.m_lng<<endl;      //1000,“迂回修改”成功

这个例子说明:const符号只是编译时(源代码级或语言层面)强类型安全检查机制的一种手段,以帮助程序员发现无意中要修改它们的代码并进行纠正,而在运行时(二进制层面)无法阻止恶意的修改。也可以说就是“防君子不防小人”。

【提示5-1】: 从理论上讲,只要你手中握有一个对象的指针(内存地址),你就可以设法绕过编译器随意修改它的内容,除非该内存受到操作系统的保护。也就是说,C++并没有提供对指针有效性的静态检查,而是把它丢给了操作系统,这正是使用指针的危险所在。

【提示5-2】: 在标准C语言中,const符号常量默认是外连接的(分配存储),也就是说你不能在两个(或两个以上)编译单元中同时定义一个同名的const符号常量(重复定义错误),或者把一个const符号常量定义放在一个头文件中而在多个编译单元中同时包含该头文件。但是在标准C++中,const符号常量默认是内连接的,因此可以定义在头文件中。当在不同的编译单元中同时包含该头文件时,编译器认为它们是不同的符号常量,因此每个编译单元独立编译时会分别为它们分配存储空间,而在连接时进行常量合并。

5.1.3 契约性常量

契约性const对象的定义并未使用const关键字,但被看作是一个const对象,见示例5-3。

示例5-3

            void ReadValue(const int& num)
            {
              cout << num;
            }
            int main(void)
            {
              int n = 0;
              ReadValue(n);  // 契约性const,n被看作是const
            }

5.1.4 枚举常量

C++/C的构造类型enum实际上常用来定义一些相关常量的集合。标准C++/C规定枚举常量的值是可以扩展的,并非受限于一般的整型数的范围(见示例5-4)。

示例5-4

            enum Gigantic
            {
              SMALL = 10,
              GIGANTIC = 300000000000
            };

至于底层如何实现,则依赖于具体的环境和编译器厂商,可能会有不同的语义,请查看编译器文档。