记录学习过程中的点点滴滴
面试题

c++中const变量问题的简单分析
十二 5th
今天同事问了关于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:%d\n" 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类型参数进行验证)
参考资料:
1. http://stackoverflow.com/questions/6212665/why-is-eax-zeroed-before-a-call-to-printf
2. http://yaronspace.cn/blog/archives/1347
关于const与volatile笔试题目的分析
十 17th
昨天笔试遇到了关于c++中关于const与const_cast的题目,大概如下:
int main(int argc, char* argv[]) { const int a = 10; int * p = const_cast<int *>(&a); *p = 1; printf("%d %d\n", a, *p); return 0; }
求上述程序的输出结果,正确答案是:10 1
分析如下:
首先可以确定是p和&a的地址是指向同一片内存区域的,理论上来说最后的输出结果应该是1 1
但是为什么会输出a的值为10呢? 猜测应该是const关键字的问题,可能编译器看到a为const型变量,所以在编译期就将所有的a直接替换为10了,这个是编译器做的一个优化,
下面简单的验证下:
直接使用下面的命令来看下编译后的汇编代码,关键部分的汇编如下:
movl %edi, -20(%rbp) movq %rsi, -32(%rbp) movl $10, -12(%rbp) leaq -12(%rbp), %rax movq %rax, -8(%rbp) movq -8(%rbp), %rax movl $1, (%rax) movq -8(%rbp), %rax movl (%rax), %edx movl $10, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave ret
显然%rax中存放的指针p的值,(%rax)代表间接寻址
在调用printf函数之前,将10放入%esi, (%rax)放入到%edx中,显然验证了上述的猜想
关于volatile关键字
如何避免编译器做这方面的优化呢?
一个常用的方法是将变量a加上关键字volatile,代表是”易变,每次都需要从内存中读取,这样上述程序的运行结果就是1 1了
当然修改常量变量的值不是好的编程习惯,尽量还是少用上述用法
register、volatile、restrict 三关键字的用法[转载]
十 6th
原文地址:register、volatile、restrict 三关键字的用法 – RaymondAmos的技术专栏 – CSDN博客.
register
使用修饰符register声明的变量属于寄存器存储类型。该类型与自动存储类型相似,具有自动存储时期、代码块作用域和内连接。声明为register 仅仅是一个请求,因此该变量仍然可能是普通的自动变量。无论哪种情况,用register修饰的变量都无法获取地址。如果没有被初始化,它的值是未定的。
volatile
volatile告诉编译器该被变量除了可被程序修改外,还可能被其他代理、线程修改。因此,当使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,而不使用寄存器中的缓存的值。比如,
val1=x; val2=x;
如 果没有声明volatile,系统在给val2赋值的时候可能直接从寄存器读取x,而不是从内存的初始位置读取。那么在两次赋值之间,x完全有可能被被某 些编译器未知的因素更改(比如:操作系统、硬件或者其它线程等)。如果声明为volatile,编译器将不使用缓存,而是每次都从内存重新读取x。
restrict
restrict是c99引入的,它只可以用于限定指针,并表明指针是访问一个数据对象的唯一且初始的方式,考虑下面的例子:
int ar[10]; int * restrict restar=(int *)malloc(10*sizeof(int)); int *par=ar;
这里说明restar是访问由malloc()分配的内存的唯一且初始的方式。par就不是了。那么:
for(n=0;n<10;n++) { par[n]+=5; restar[n]+=5; ar[n]*=2; par[n]+=3; restar[n]+=3; }
因 为restar是访问分配的内存的唯一且初始的方式,那么编译器可以将上述对restar的操作进行优化:restar[n]+=8;。而par并不是访 问数组ar的唯一方式,因此并不能进行下面的优化:par[n]+=8;。因为在par[n]+=3前,ar[n]*=2进行了改变。使用了关键字 restric,编译器就可以放心地进行优化了。这个关键字据说来源于古老的FORTRAN。
总结
两个关键字:volatile和restrict,两者都是为了方便编译器的优化。
今天EMC笔试题目
九 25th
两道的题目:
1. dup(int fd)和dup2(int fd1, int fd2)函数的区别:详细请见http://blog.donews.com/mutecat/archive/2007/09/20/1212178.aspx
2. 有关一致性哈希的算法设计
其他的是 多项选择题目,涉及的范围比较广,linux和语言层次的题目偏多一些吧
包括堆栈缓冲区溢出以及linux内核中container_of宏的实现,以及spinlock和虚拟内存的相关知识
关于container_of这个请参考我的前一篇blog: http://yaronspace.cn/blog/index.php/archives/1026
近期评论