2.6 常量
前几节介绍的数据类型都是以变量形式进行演示的,在程序运行中可以修改其保存的数据。从字面上理解,常量是一个恒定不变的值,它在内存中也是不可修改的。在程序中出现的1、2、3这样的数字或“Hello”这样的字符串,以及数组名称,都属于常量,程序在运行中不可修改这类数据。
常量数据在程序运行前就已经存在,它们被编译到可执行文件中,当程序启动后,它们便会被加载进来。这些数据通常都会保存在常量数据区中,该区的属性没有写权限,所以在对常量进行修改时,程序会报错。试图修改常量数据都将引发异常,导致程序崩溃。
2.6.1 常量的定义
在C++中,可以使用宏机制#define来定义常量,也可以使用const将变量定义为一个常量。#define定义常量名称,编译器在对其进行编译时,会将代码中的宏名称替换成对应信息。宏的使用可以增加代码的可读性。const是为了增加程序的健壮性而存在的。常用字符串处理函数strcpy的第二个参数被定义为一个常量,这是为了防止该参数在函数内被修改,对原字符串造成破坏,宏与const的使用如代码清单2-8所示。
代码清单2-8 宏与const的使用
//C++源码 #include <stdio.h> #define NUMBER_ONE 1 //定义NUMBER_ONE为常量1 int main(int argc, char* argv[]) { const int n = NUMBER_ONE; //将常量NUMBER_ONE赋值给const常量n printf("const = %d #define = %d \r\n", n, NUMBER_ONE); //显示两者结果 return 0; }
代码清单2-8中,使用#define定义了常量1,并赋值给const的常量n。编译后,宏名称NUMBER_ONE将被替换成1。使用VS编译此段代码,依次选择菜单“项目”→“属性”→“C/C++”→“命令行”,添加“/P”选项,如图2-12所示。
图2-12 添加编译选项
VS也可以使用命令行添加编译器选项编程,gcc和clang可以使用-E编译选项生成预处理文件,命令如下。
cl /Fe:vs.i /P test.cpp clang -E -o clang.i test.cpp gcc -E -o gcc.i test.cpp
此编译选项的功能是将预处理文件生成到文件中,编译后,在对应的CPP文件夹中会产生一个“文件名.i”的文件。编译代码清单2-8中的代码,生成.i文件,打开该文件查看main函数中的代码信息。添加“/P”选项后,在连接过程中会产生错误,这是由于没有生成OBJ文件,而是将预处理信息写入了.i文件中,编译器找到不OBJ,无法进行连接。查看.i文件中的信息,如代码清单2-9所示。
代码清单2-9 预处理文件信息
//VS对应预处理文件 int main(int argc, char* argv[]) { const int n = 1; printf("const = %d #define = %d \r\n", n, 1); return 0; } //GCC对应预处理文件 int main(int argc, char* argv[]) { const int n = 1; printf("const = %d #define = %d \r\n", n, 1); return 0; } //Clang对应预处理文件 int main(int argc, char* argv[]) { const int n = 1; printf("const = %d #define = %d \r\n", n, 1); return 0; }
2.6.2 #define和const的区别
#define修饰的符号名称是一个真量数值,而const修饰的栈常量,是一个“假”常量。在实际中,使用const定义的栈变量,最终还是一个变量,只是在编译期间对语法进行了检查,发现代码有对const修饰的变量存在直接修改行为则报错。
被const修饰过的栈变量本质上是可以被修改的。我们可以利用指针获取const修饰过的栈变量地址,强制将const属性修饰去掉,就可以修改对应的数据内容,如代码清单2-10所示。
代码清单2-10 修改const常量
// C++ 源码 #include <stdio.h> int main(int argc, char* argv[]) { const int n1 = 5; int *p = (int*)&n1; *p = 6; int n2 = n1; return 0; } //x86_vs对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 sub esp, 0Ch 00401006 mov dword ptr [ebp-4], 5 ;n1 = 5 0040100D lea eax, [ebp-4] 00401010 mov [ebp-8], eax ;p = (int*)&n1 00401013 mov ecx, [ebp-8] 00401016 mov dword ptr [ecx], 6 ;*p = 6 0040101C mov dword ptr [ebp-0Ch], 5 ;n2 = n1 00401023 xor eax, eax 00401025 mov esp, ebp 00401027 pop ebp 00401028 retn //x86_gcc对应汇编代码讲解 00401510 push ebp 00401511 mov ebp, esp 00401513 and esp, 0FFFFFFF0h ;对齐栈 00401516 sub esp, 10h 00401519 call ___main ;调用初始化函数 0040151E mov dword ptr [esp+4], 5 ;n1 = 5 00401526 lea eax, [esp+4] 0040152A mov [esp+0Ch], eax ;p = (int*)&n1 0040152E mov eax, [esp+0Ch] 00401532 mov dword ptr [eax], 6 ;*p = 6 00401538 mov dword ptr [esp+8], 5 ;n2 = n1 00401540 mov eax, 0 00401545 leave 00401546 retn //x86_clang对应汇编代码讲解 00401000 push ebp 00401001 mov ebp, esp 00401003 push esi 00401004 sub esp, 18h 00401007 mov eax, [ebp+0Ch] 0040100A mov ecx, [ebp+8] 0040100D xor edx, edx 0040100F mov dword ptr [ebp-8], 0 00401016 mov dword ptr [ebp-0Ch], 5 ;n1 = 5 0040101D lea esi, [ebp-0Ch] 00401020 mov [ebp-10h], esi ;p = (int*)&n1 00401023 mov esi, [ebp-10h] 00401026 mov dword ptr [esi], 6 ;*p = 6 0040102C mov dword ptr [ebp-14h], 5 ;n2 = n1 00401033 mov [ebp-18h], eax 00401036 mov eax, edx 00401038 mov [ebp-1Ch], ecx 0040103B add esp, 18h 0040103E pop esi 0040103F pop ebp 00401040 retn //x64_vs对应汇编代码讲解 0000000140001000 mov [rsp+10h], rdx 0000000140001005 mov [rsp+8], ecx 0000000140001009 sub rsp, 18h 000000014000100D mov dword ptr [rsp], 5 ;n1 = 5 0000000140001014 lea rax, [rsp] 0000000140001018 mov [rsp+8], rax ;p = (int*)&n1 000000014000101D mov rax, [rsp+8] 0000000140001022 mov dword ptr [rax], 6 ;*p = 6 0000000140001028 mov dword ptr [rsp+4], 5 ;n2 = n1 0000000140001030 xor eax, eax 0000000140001032 add rsp, 18h 0000000140001036 retn //x64_gcc对应汇编代码讲解 0000000000401550 push rbp 0000000000401551 mov rbp, rsp 0000000000401554 sub rsp, 30h 0000000000401558 mov [rbp+10h], ecx 000000000040155B mov [rbp+18h], rdx 000000000040155F call __main ;调用初始化函数 0000000000401564 mov dword ptr [rbp-10h], 5 ;n1 = 5 000000000040156B lea rax, [rbp-10h] 000000000040156F mov [rbp-8], rax ;p = (int*)&n1 0000000000401573 mov rax, [rbp-8] 0000000000401577 mov dword ptr [rax], 6 ;*p = 6 000000000040157D mov dword ptr [rbp-0Ch], 5 ;n2 = n1 0000000000401584 mov eax, 0 0000000000401589 add rsp, 30h 000000000040158D pop rbp 000000000040158E retn //x64_clang对应汇编代码讲解 0000000140001000 sub rsp, 28h 0000000140001004 xor eax, eax 0000000140001006 mov dword ptr [rsp+24h], 0 000000014000100E mov [rsp+18h], rdx 0000000140001013 mov [rsp+14h], ecx 0000000140001017 mov dword ptr [rsp+10h], 5 ;n1 = 5 000000014000101F lea rdx, [rsp+10h] 0000000140001024 mov [rsp+8], rdx ;p = (int*)&n1 0000000140001029 mov rdx, [rsp+8] 000000014000102E mov dword ptr [rdx], 6 ;*p = 6 0000000140001034 mov dword ptr [rsp+4], 5 ;n2 = n1 000000014000103C add rsp, 28h 0000000140001040 retn
在代码清单2-10中,由于const修饰的变量n1被赋值一个数字常量5,编译器在编译过程中发现n1的初始值是可知的,并且被修饰为const。之后所有使用n1的地方都替换为这个可预知值,故int n2 = n1;对应的汇编代码没有将n1赋值给n2,而是用常量值5代替。如果n1的值为一个未知值,则编译器不会做此优化。在示例中使用指针能否将n1中的数据修改为6呢?我们先来看看图2-13。
图2-13 const常量的修改结果
图2-13中演示了const修饰的变量被修改后的情况。被const修饰后,变量本质上并没有改变,还是可以修改的。#define与const两者之间还是不同的,如表2-5所示。
表2-5 #define与const的区别
这两者在连接生成可执行文件后将不复存在,在二进制编码中也没有这两种类型存在。在实际分析中,读者需要根据自身的经验进行还原。