在C++中,为了定义常量,大部分人都不是继承C的特征去写宏定义,而是使用常量定义const。并且const也遍布了函数的返回值声明和参数声明,以确保函数的返回值或者参数不被使用的时候修改。那么事实上,const究竟做了什么呢?
那么这样的话我们就分为三种不同的使用分析分析const:
一. const 常量定义的使用。先看一段代码:
#include <stdio.h>
int main(){
const int a = 100;
int * b =const_cast< int*>(&a);
*b = 1;
int& c = const_cast<int&>(a);
c = 2;
printf("a = %d, *b = %d, c = %d\n", a, *b, c);
printf("&a = %p, b = %p, &c = %p\n", &a, b, &c);
return 0;
}
上面的代码输出是什么?又是为什么?先从输出说起:
a= 100 , *b = 2 , c = 2
a= 0x7ffca83ec0e4 , *b = 0x7ffca83ec0e4 , c = 0x7ffca83ec0e4
看到这里,估计有人开始迷糊了。有以下几个问题:
1. a和c的地址与b所指向的地址都相同,为什么a输出的是100,但*b和c输出的都是2?
2. a作为常量,修改它的值是否能够成功修改?
对于第二个问题,或许从很多资料上来说,都是未定义操作。对于未定义操作的理解,可以单纯的理解为:你操作你的,我不保证正确性。由于是使用了const_cast操作,将a的const属性移除,从而修改了值。虽然这里可能也确实修改了a的值,但确实无法保证原有的a的正确性了呢。但是到底有没有真正的修改到a的值呢,这个问题比较容易验证,那么就简单的跟踪一下就可以看到:
哇唔,在这里修改之前*b的值也确实和a相同是100,因为也毕竟是指向a的地址啊。但当修改了*b的值之后,输出a的时候,也确实被修改了。那也就证明了通过以上的方式实际上是真正的修改a的值呢。那么,第一个问题就越发的奇怪了,我明明已经看到a的值被修改了呢,但这是几个意思呢?我们先暂且来个自我的思考分析:既然在输出a的值的时候确实没有修改,那么是不是在编译的时候将直接对于a的调用替换成了常数呢?const常量是在内存中分配了地址空间,因此应该不会像宏一样在预编译的时候被替换掉吧。为了验证这个问题,在Linux下使用g++ -E 就可以查看预编译的结果。实际如下(由于预编译的结果还是比较庞大,我就只选择main部分):
# 3 "test_const.cc"
int main(){
const int a = 100;
int * b =const_cast< int*>(&a);
*b = 1;
int& c = const_cast<int&>(a);
c = 2;
printf("a= %d , *b = %d , c = %d \n" , a , *b , c);
printf("a= %p , *b = %p , c = %p \n", &a, b, &c);
# 22 "test_const.cc"
return 0;
}
从上面的结果看,和源代码几乎没有区别。那么汇编后的结果呢?在Linux下可以使用g++ -S输出汇编后的代码:
.file "test_const.cc" .section .rodata .LC0: .string "a= %d , *b = %d , c = %d \n" .LC1: .string "a= %p , *b = %p , c = %p \n" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp movq %fs:40, %rax movq %rax, -8(%rbp) xorl %eax, %eax movl $100, -28(%rbp) leaq -28(%rbp), %rax movq %rax, -24(%rbp) movq -24(%rbp), %rax movl $1, (%rax) leaq -28(%rbp), %rax movq %rax, -16(%rbp) movq -16(%rbp), %rax movl $2, (%rax) movq -16(%rbp), %rax movl (%rax), %edx movq -24(%rbp), %rax movl (%rax), %eax movl %edx, %ecx movl %eax, %edx movl $100, %esi movl $.LC0, %edi movl $0, %eax call printf movq -16(%rbp), %rcx movq -24(%rbp), %rdx leaq -28(%rbp), %rax movq %rax, %rsi movl $.LC1, %edi movl $0, %eax movl $0, %eax call printf movl $0, %eax movq -8(%rbp), %rsi xorq %fs:40, %rsi je .L3 call __stack_chk_fail .L3: leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010" .section .note.GNU-stack,"",@progbits这看的时候都想哭了,贴出来一堆的汇编代码,估计有很多人都不愿意看了。但这里有重要信息啊,如下:
从上述圈出的地方,可以看到原来常量在汇编成汇编代码的时候被直接替换了啊,怪不得在输出的时候是原值呢。终于理解为什么是这种输出了。但似乎问题没有这么简答的就结束呢,如果把常量作为普通函数的参数传递会是怎么样的呢?还是会像这个输出的时候直接向栈中压入常量值吗?带着这个问题,我又写了下面的代码做尝试:
#include <stdio.h>
int fun_1(const int x){
return x + 1;
}
int fun_2(int x){
return x + 2;
}
int fun_3(const int &x){
return x + 3;
}
int fun_4(int &x){
return x + 4;
}
int main()
{
const int a = 100;
int& c = const_cast<int&>(a);
c = 3;
.
printf("fun_1:%d, fun_2:%d, fun_3:%d, fun_4:%d\n", fun_1(a), fun_2(a), fun_3(a), fun_4(const_cast<int&>(a)));
return 0;
}
这个问题的输出是什么?
fun_1:101, fun_2:102, fun_3:103, fun_4:104
这个结果说明了一个问题,在常数作为参数的情况下,传值和传引用在汇编的时候处理方式是不同的,如果是传值,则无论参数是const或者非const,都会在汇编期决定了传入参数的值。传引用的话就会按取当前内存中的值作为参数传递。这当然也是因为引用是对应变量的内存的别名的缘故吧。
二、const 作为参数的定义
代码就有使用上述的代码吧,有const参数传递的。直接看看汇编后的结果看看:
被红框圈住的是fun_2的汇编代码,没有被圈的是fun_1的汇编代码,仔细对照一下,发现啥变化都没有。之前还一直想过一个问题,const常量和非const变量,究竟在代码里是怎么标记的,从这里看来实际是没有任何标记的,其检查是否有const常量当作非const变量使用应该是由编译器检查。对于此假设,可以作为以后有空的时候的一个验证。
三、const作为函数返回值的声明
基于作为变量的分析,我有了这样一个猜想,在实际的函数定义中,返回非const和const是完全没有变化的。带着这个猜想,我写了以下三个函数声明,代码做分析测试:
const int sum_1(int x, int y){
return x + y;
}
int sum_2(int x, int y){
return x + y;
}
int sum_3(int x, int y){
. return x + y;
}
汇编结果如下:
.globl _Z5sum_1ii
.type _Z5sum_1ii, @function
_Z5sum_1ii:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size _Z5sum_1ii, .-_Z5sum_1ii
.globl _Z5sum_2ii
.type _Z5sum_2ii, @function
_Z5sum_2ii:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size _Z5sum_2ii, .-_Z5sum_2ii
.globl _Z5sum_3ii
.type _Z5sum_3ii, @function
_Z5sum_3ii:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size _Z5sum_3ii, .-_Z5sum_3ii
果然猜想是正确的,参数的限定也是有编译器去做检测的(这也是猜想)。那么,如果是以下调用的话会是怎么样的呢?
int main(){
const int a = sum_1(2, 3);
const int b = sum_2(4, 5);
int c = sum_3(6, 7);
int d = sum_1(8, 9);
int e = sum_2(10, 11);
const int f = sum_3(12, 13);
. printf("a = %d, b = %d, c = %d, d = %d, e = %d, f = %d\n", a, b, c, d, e, f);
return 0;
}
这个我就不做太多的赘述了,有兴趣的话可以自己做个简单的尝试分析分析。