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的区别

这两者在连接生成可执行文件后将不复存在,在二进制编码中也没有这两种类型存在。在实际分析中,读者需要根据自身的经验进行还原。