常变量: const 类型说明符 变量名
常引用: const 类型说明符 &引用名
常对象: 类名 const 对象名
常成员函数: 类名::fun(形参) const
常数组: 类型说明符 const 数组名[大小]
常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名
首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。
今天同事问了关于const 值被更改的问题,这个问题在面试中也会经常被问到,所以专门写篇博客总结下。
代码如下
输出结果:a:10 p:20
简单分析:
其实针对该问题,原因比较简单,在printf调用中,由于a是常量,编译器做了优化,将a的值直接替换为10,所以在后续对a的内存位置进行更改为20时,并未对printf的a参数影响。当然,同事并未相信我的分析,我只能调用汇编后的代码进行分析。
深度分析:
1. 生成汇编代码:
g++ -S test.cpp # 输出的汇编代码位于test.s中
2. 汇编代码如下:
根据第26行,验证了我上述的分析,此时同事想让我讲下整个汇编代码的逻辑(我心里暗喜,刚好最近在上@林士鼎 的课程,顺便复习下)
3. 函数调用的知识
理解函数调用栈,有几个概念需要事先明确:
a. 栈的增长方式由高到低(大学课本里早就会背了,但是要真正理解)
b. 参数传递方式:寄存器 + 栈
c. %rsp : 栈头指针 , push pop会更改对应的指针值
d. %rbp : 帧指针, 这个概念非常重要,当前函数的栈的起始位置,
使用%rbp + offset访问局部变量及传递的参数,下面这张图是@林士鼎 老师的课件中某个图
4. 汇编分析:
main:
.LFB2:
pushq %rbp # 将原来的帧指针保存
.LCFI0:
movq %rsp, %rbp # 将当前%rsp 保存至%rbp, 当前帧
.LCFI1:
subq $32, %rsp # 将栈下移32Bytes, 这32字节保存了main的参数和main中局部变量
.LCFI2:
movl %edi, -4(%rbp) # 取 argc的值
movq %rsi, -16(%rbp) # 取argv的值, 为何会占12Bytes呢? 后续分析
movl $10, -20(%rbp) # 将10赋值给a
leaq -20(%rbp), %rax #取a的地址到%rax
movq %rax, -32(%rbp) #将a的地址赋值给p(int *), 32的偏移有待分析
movq -32(%rbp), %rax # 去p的值到%rax
movl $20, (%rax) # 对*p赋值
movq -32(%rbp), %rax
movl (%rax), %edx #这两步相当于*p,同时将其赋值到寄存器作为printf的参数
movl $10, %esi #传递10参数(其实是a)
movl $.LC0, %edi # "a:%d p:%dn"
movl $0, %eax #这个作用稍后分析
call printf
movl $0, %eax #main函数的返回值
leave
ret
5. 其他问题分析:
a. 为何argv 和 p在栈的位置偏移异常,这里主要是因为argc 和a 的大小为4Bytes, 而系统是64Bits, 所以存在8Bytes对齐,
这里可以验证,在a后面单独声明一个b变量,发现其在栈的空间刚好为间隙的位置
b. movl $0, %eax 这句话的作用:当对于变参的参数传递时,%eax标示的使用vector register的数目(可以向printf传递float类型参数进行验证)