const修饰的变量不一定是常量
前言
很多C/C++方面的书中都介绍到“当变量由const
修饰时,这个变量就成为了常量,其值不能再修改”。果真如此吗?在之前的一篇文章C/C++中已初始化/未初始化全局/静态/局部变量/常量在内存中的位置对const
修饰的局部变量是否是常量就提出过怀疑,这里利用一些例子来确认一下const
修饰的变量究竟是不是常量。
还是首先上结论:const
修饰的全局变量、静态局部变量以及数组(包括字符串)的值都不能再更改,const
修饰的基本类型局部变量的值都可以更改。
注意:对const
修饰的变量重新赋值属未定义行为,其影响因编译器而异。
开发环境
- OS X El Captian (10.11.6)
- Apple LLVM 7.3.0 (clang-703.0.31)
- gcc 6.1.0
全局变量、静态变量与数组
这三种类型的变量,其值在编译时就放在只读的__const
段或__cstring
段内,运行时重新赋值会出现“段错误”。
const int global_int = 1;
int main(int argc, const char *argv[]) {
const static char static_char = 'a';
const int local_int_array[] = {1, 2, 3};
const char local_str1[] = "Hello";
char *local_str2 = "World";
int *global_int_ptr = &global_int;
*global_int_ptr = 2;
char *static_char_ptr = &static_char;
*static_char_ptr = 'b';
int *local_int_array_ptr = &local_int_array[0];
*local_int_array_ptr = 3;
char *local_str1_ptr = &local_str1[0];
*local_str1_ptr = 'W';
char *local_str2_ptr = local_str2;
*local_str2_ptr = 'H';
return 0;
}
如上代码使用C编译器编译,运行时无一例外都会提示出错(自己试试注释掉部分语句?),如clang提示
Thread 1: EXC_BAD_ACCESS (code=2, address=0x100000fa0)
我们来看看这些变量所在的内存位置(这里就简单点直接列出来,具体的方法请参考前面的文章):
变量名 | 内存地址 | 所在段 |
---|---|---|
global_int | 0x100000f90 | __const |
static_char | 0x100000f94 | __const |
local_int_array | 0x100000f98 | __const |
local_str1 | 0x100000fa4 | __const |
*local_str2 | 0x100000faa | __cstring |
无一例外,由于const
的修饰,这些变量在编译时全都放在了只读的__const
段或__cstring
段内,在运行时由操作系统保护这些段为只读,对其重新赋值会提示出错。
有没有发现上面例子中的local_str2
有点不一样,没有const
修饰?这种情况在上一篇C中字符串在内存中的位置及读写等分析中已经讨论过,采用指针方式初始化的字符串,其字符串常量本身被放在__cstring
段中,而指针则被放在栈中,所以对此字符串重新赋值也会提示出错。
基本类型局部变量
基本类型的局部变量加不加const
修饰都是放在栈中的,这里不再验证。这里主要关注的是:在栈中的const
修饰的局部变量,是否有如上述全局变量等的操作系统提供的写保护?即这些const
修饰的局部变量是只读的吗?
验证的方法很简单啦,找个const
修饰的局部变量仍然可以重新赋值的例子就好了。由于C和C++的处理方法不一样,这里分开讨论。
C
我们看这段代码:
#include <stdio.h>
int main(int argc, const char *argv[]) {
const int local_const_a = 10;
int *ptr = &local_const_a;
*ptr = 20;
printf("local_const_a=%d\n", local_const_a);
printf("*ptr=%d\n", *ptr);
return 0;
}
编译输出:
local_const_a=10
*ptr=20
同一个变量有两个不同的值?为谨慎起见我们还是看看内存里local_const_a
的值:local_const_a
所在内存地址为0x7fff5fbff93c
,此地址下内存值为14 00 00 00
,即20。
首先我们能确认的是这个const
修饰的变量的值已经被改变了。接下来我们再来分析为什么local_const_a
仍然会输出10。
看看反汇编代码:
0x100000f00 <+0>: pushq %rbp
0x100000f01 <+1>: movq %rsp, %rbp
0x100000f04 <+4>: subq $0x30, %rsp
0x100000f08 <+8>: leaq 0x7f(%rip), %rax ; "local_const_a=%d\n"
0x100000f0f <+15>: movl $0xa, %ecx
0x100000f14 <+20>: leaq -0x14(%rbp), %rdx
0x100000f18 <+24>: movl $0x0, -0x4(%rbp)
0x100000f1f <+31>: movl %edi, -0x8(%rbp)
0x100000f22 <+34>: movq %rsi, -0x10(%rbp)
0x100000f26 <+38>: movl $0xa, -0x14(%rbp)
0x100000f2d <+45>: movq %rdx, -0x20(%rbp)
0x100000f31 <+49>: movq -0x20(%rbp), %rdx
0x100000f35 <+53>: movl $0x14, (%rdx)
0x100000f3b <+59>: movq %rax, %rdi
0x100000f3e <+62>: movl %ecx, %esi
0x100000f40 <+64>: movb $0x0, %al
0x100000f42 <+66>: callq 0x100000f6c ; symbol stub for: printf
0x100000f47 <+71>: leaq 0x52(%rip), %rdi ; "*ptr=%d\n"
0x100000f4e <+78>: movq -0x20(%rbp), %rdx
0x100000f52 <+82>: movl (%rdx), %esi
0x100000f54 <+84>: movl %eax, -0x24(%rbp)
0x100000f57 <+87>: movb $0x0, %al
0x100000f59 <+89>: callq 0x100000f6c ; symbol stub for: printf
0x100000f5e <+94>: xorl %ecx, %ecx
0x100000f60 <+96>: movl %eax, -0x28(%rbp)
0x100000f63 <+99>: movl %ecx, %eax
0x100000f65 <+101>: addq $0x30, %rsp
0x100000f69 <+105>: popq %rbp
0x100000f6a <+106>: retq
想必没几个人会耐着性子看汇编,我们把注意力集中在两个printf
上。因为涉及到函数调用,这里说明下64位系统下函数调用参数传递的方式,callq
函数调用前,第一个参数放在rdi
寄存器中,第二个参数放在rsi
寄存器中,其中rsi
的低32位称为esi
。
第一个printf
:
printf("local_const_a=%d\n", local_const_a);
其相关的汇编代码为:
0x100000f08 <+8>: leaq 0x7f(%rip), %rax ; "local_const_a=%d\n"
0x100000f0f <+15>: movl $0xa, %ecx
0x100000f3b <+59>: movq %rax, %rdi
0x100000f3e <+62>: movl %ecx, %esi
0x100000f42 <+66>: callq 0x100000f6c ; symbol stub for: printf
第一个参数rdi
为指向"local_const_a=%d\n"
的指针,第二个参数esi
为0xa
,也就是说调用printf
时并没有从local_const_a
的内存地址获取最新的值20,而是直接拿const
声明时的值10,即在编译时相当于对local_const_a
作了宏替换。
也就是说,因为local_const_a
有const
修饰且其值确定,所以编译时将所有出现local_const_a
的地方直接替换为其值,而不考虑运行时值会不会改变。即可以理解为执行的是
printf("local_const_a=%d\n", 10);
第二个printf
:
printf("*ptr=%d\n", *ptr);
其相关的汇编代码为:
0x100000f26 <+38>: movl $0xa, -0x14(%rbp)
0x100000f14 <+20>: leaq -0x14(%rbp), %rdx
0x100000f2d <+45>: movq %rdx, -0x20(%rbp)
0x100000f47 <+71>: leaq 0x52(%rip), %rdi ; "*ptr=%d\n"
0x100000f4e <+78>: movq -0x20(%rbp), %rdx
0x100000f52 <+82>: movl (%rdx), %esi
0x100000f59 <+89>: callq 0x100000f6c ; symbol stub for: printf
第一个参数rdi
为指向"*ptr=%d\n"
的指针,第二个参数esi
为以ptr
为地址的内存的值,即local_const_a
的最新值。
也就是说,由于是通过指针间接访问,因此编译也是按照这个步骤从指针获取内存地址,然后再从内存地址获取内存值,从而得到了local_const_a
的真实值。
C++
C++不同于C是因为C++封堵了常量指针向指针的转换,所以上述C代码用C++编译器直接编译不过去。不过还是有办法通过指针改变const
修饰的变量的值得,下面介绍两种,第一种使用C++提供的const_cast
转换符来去除变量的const
限定;第二种则采用间接的方式来“制造”一个指向const
修饰的变量的指针。
方法一
#include <stdio.h>
int main(int argc, const char *argv[]) {
const int local_const_a = 10;
int *ptr = const_cast<int *>(&local_const_a);
*ptr = 20;
printf("local_const_a=%d\n", local_const_a);
printf("*ptr=%d\n", *ptr);
return 0;
}
通过const_cast
将常量指针转换为指针,与上面的C版本实际是一样的,输出
local_const_a=10
*ptr=20
方法二
#include <stdio.h>
int main(int argc, const char *argv[]) {
const int local_const_a = 10;
int temp = 1;
int *ptr = &temp;
ptr += 1;
*ptr = 20;
printf("local_const_a=%d\n", local_const_a);
printf("*ptr=%d\n", *ptr);
const int *const_ptr = &local_const_a;
printf("*const_ptr=%d\n", *const_ptr);
return 0;
}
这种方法实际是利用到了栈,local_const_a
进栈后,随机将占位的temp
进栈,此时这两个变量在内存中紧邻;然后创造一个指向temp
的指针,给这个指针一个int
长度的偏移,这个指针就实际指向local_const_a
啦!这时通过这个指针进行赋值,修改的就是local_const_a
的值。输出
local_const_a=10
*ptr=20
*const_ptr=20
上述这两种方法了C版本一样,直接输出local_const_a
为声明时的值,而通过指针输出的是实际值,但都做到了对const
变量的重新赋值。
总结
const
修饰的全局变量、静态局部变量以及数组(包括字符串)由于保存在只读的段中,其值受操作系统保护,所以这些const
修饰的变量就是实际意义上的常量。
但const
修饰的基本类型局部变量则保存在栈中,运行时并没有任何形式的写保护,其值可以自由修改。这些const
修饰只在编译时起作用,编译器会将这些变量当做类似于宏处理;或者说,编译器对这些变量只作“道义上”的保护,但并不能“防小人”。需要注意的一点是,任何对const
修饰的变量重新赋值,都是未定义行为(Undefined Behavior),不同编译器可以有不同的处理方法。
One More Thing
宏替换是需要值已经确定是吧?如果这个const
变量的值只能在运行时才能确定呢?
这里就不做实验了,结果就是不会出现上述一个变量两种值的问题,因为哪种情况下运行时都得先得到变量的内存地址,然后再获取最新的值。
参考
- 俞甲子, 潘爱民. 程序员的自我修养: 链接, 装载与库[M]. 电子工业出版社, 2009.
Thanks. I like this!
试了一下用C编译这个代码,编译出来的结果是都是20,有一条警告,但最后的结果仍旧是修改了const修饰的变量的值
C++和C一样,都用强制类型转换就可以了:
int ptr = (int )&local_const_a;