前两天朋友叫我帮忙做几道题(阿里的笔试题?),遇到这样一道题:
const int a = 10;
int *p = (int*)(&a);
*p = 20;
cout << &a << " " << a << " " << *p << " " << p << endl;
乍一看,似乎很明显,const关键字表示常量,不允许修改,那就应该会出现编译错误喽?哪有这么简单的事!我运行了一下,发现竟然可以正常编译运行,但是结果是a=10,*p=20.看起来有些不可思议,难道为了避免const变量的值被修改,将a的地址赋值给指针p时又重新分配了一个int型变量的空间?但是,用C语言改写一下这段代码:
const int a = 10;
int *p = (int*)(&a);
*p = 20;
printf("%d %d", a, *p);
发现运行结果为a=20,*p=20.为什么?const变量被改变了!其实在C/C++语言里,const的目的(似乎)并不是为了防止变量被修改,而是为了防止变量被“无意间”修改,这个关键字并没有我之前以为的能为变量加上访问权限的功能,只需要像以上代码做显示类型转换或者使用const_cast或者mutable关键字,就可以修改const变量。在Dev-C++上,甚至可以不用做显示类型转换,
const int a = 10;
int *p = &a;
*p = 30;
这段代码是可以正确编译运行的,编译器仅仅是给出警告而已:
[Warning] initialization discards 'const' qualifier from pointer target type [enabled by default]
在Stroustrup的《C++程序设计语言》里有关于const的内容,const更多地是用来替代C语言中的宏即#define的作用,因为宏是由预处理器(preprocessor)在编译之前开展的,不属于编译器职责范围,因此相关的类型检查等语言本身的安全机制都无法发挥作用;同时,宏的展开常常是简单的文本替换,在上下文比较复杂的情况下,无法预料一段宏展开后会与上下文结合产生什么样的代码,例如:
#define add(x,y) x+y
int a = 2*add(1, 3);
我想大部分人是知道a=5的。这个宏并不能按照程序员的本意工作,预处理过程后,代码实际上是
int a = 2*1+3;
add(1, 3)被文本替换为1+3,而放到程序中,因为运算符的优先级问题,这个宏并不能按照我们当初预想的那样工作。解决方法通常是加上括号,然而这只是一个很简单的例子,在更复杂的例子里,需要更为复杂的处理,例如放在do{}while(0)语句里,经验不足的程序员很难写出精炼而能正确工作的宏;更糟糕的是,宏为代码加上了一层间接性,我们在定义宏时很难预见宏会在何时何地被展开,与上下文发生何种作用,调试代码时如果仅仅观察源代码而不查看预处理后的代码,常常很难发现某些隐晦的错误,这就使调试的难度加大了。宏是独立于编译系统之外的,享受不到编译器带给我们的种种便利,事实上,现代编译器对源代码做了许多工作,游荡在编译器的职责范围外是很危险的。详细内容仍然可以参阅《C++程序设计语言》等各种著作。
好了,到目前为止,C的部分说完了,但是第一个C++程序仍然没有解释,为什么a的值保持不变,难道C++关于const的规定和C语言不同,即使进行显示类型转换,也不能改变const变量的值?非也。变量a的值其实改变了,但编译器在这里做了一个优化:常量折叠。编译器检测到变量a被声明为常量,认为a的值保持不变,因此每次调用a的值时直接用常量取代变量a,而不从内存中读取a的值,所以int b = a;被编译器优化为int b = 10;这样可以节省一次读取内存的操作。怎么证明呢?可以在变量前加上volatile关键词,禁止编译器对此变量进行优化,结果就对了。可以查看汇编代码:
未加volatile关键词
mov DWORD PTR [ebp-0xc],0xa
mov DWORD PTR [esp],0xa
直接用常量0xa赋值
加volatile
mov DWORD PTR [ebp-0xc],0xf
mov eax,DWORD PTR [ebp-0xc]
mov DWORD PTR [esp],eax
就要从相应的地址取变量的值了。(其实我汇编学得本来就渣,又好久没碰了,这些代码具体是啥意思我也不甚清楚,瞎蒙的,呵呵~:)
还有一个问题,在C++中,如果用volatile修饰变量,再输出这个变量的地址,结果为1.为什么呢?根据网上的信息,ostream的operator<< 并没有重载支持volatile修饰的变量,因此cout << 一个volatile变量,实际上是调用以bool类型为参数的操作符,也就是说,volatile变量的地址被作为一个bool类型,一般地址都是非0的,非0值的bool值当然就是1了。
好了,这篇文章就差不多写完了,本来标题可以取得再吸引眼球一点,什么“阿里笔试题”,呵呵。
这几年没怎么写过博客,在CSDN论坛发帖的次数也不多。既不懂得利用网络资源,也静不下心来踏踏实实地学点东西,说到底,还是太懒散了啊。希望以后能有点长进,欢迎指点~