对const声明变量的奇异行为的探讨

本文通过一个具体的C++代码示例探讨了const变量的行为特点,尤其是当const变量被非法修改时,编译器如何处理这一情况。通过对编译后的汇编代码进行分析,揭示了编译器对const变量和普通变量的不同处理方式。

对const声明变量的奇异行为的探讨
----------------------------------------------------------------

The information in this article applies to:

- C/C++

----------------------------------------------------------------

奇异的现象:
我把这个试验的源代码列出来:

int main(int argc, char* argv[])

{

const int x=10000;

int *y=0;

y=(int*)&x;

*y=10;

printf("%d/n", x);

printf("%d/n", *y);

return 0;

}

首先我们声明了一个const变量x,初始化为10000。然后让一个int指针y指向x。通过给*y赋值,从而改变了x的实际值!

虽然在Watch窗口中你明明看到x的值确实是10,但是printf出来的x的值却偏偏是10000!!

可是,这个已经被彻底抹去的10000,又是从哪里被找回来的呢?


我的解释:
这样的代码经过VC编译器的Debug版本的编译,最后生成的完整的汇编代码为(我做了注释,可以参考一下):

11: int main(int argc, char* argv[])

12: {

00401250 push ebp

// 第一步,将基址寄存器(EBP) 压入堆栈

00401251 mov ebp,esp

// 第二步,把当前的栈顶指针(ESP)拷贝到EBP,做为新的基地址

00401253 sub esp,48h

// 第三步,把ESP减去一个数值,用来为本地变量留出一定空间。这里减去48h,也就是

// 72 .

// 这里对前面的三步说明一下:ESP和EBP寄存器是堆栈专用的。堆栈基址指针(EBP)寄

// 存器确定堆栈帧的起始位置,而堆栈指针(ESP)寄存器执行当前堆栈顶。在函数的入口处,

// 当前堆栈基址指针被压到了堆栈中,并且当前堆栈指针成为新的堆栈基址指针。局部变

// 量的存储空间、函数使用的各种需要保存的寄存器的存储空间在函数入口处也被预留出

// 来。

// 所以也就有了下面的三个压栈行为。


// 下面是连续三个压栈,第4步:

00401256 push ebx

// 将ebx寄存器压栈;EBX寄存器是段寄存器的一种,为基址 DS 数据段;

00401257 push esi

// 将esi寄存器压栈;ESI寄存器是指针寄存器的一种。是内存移动和比较操作的源地址寄

// 存器;

00401258 push edi

// 将edi寄存器压栈;EDI寄存器是指针寄存器的一种。是内存移动和比较操作的目标地址

// 寄存器

// 以上四步执行完之后,函数入口处的堆栈帧结构如下所示:

 

 

 

// 值得注意的是,上面所说的对于Debug版本才是正确的,对于Release版本可不一定对。

// Release 版本也许已经把堆栈基址指针优化掉了。


00401259 lea edi,[ebp-48h]

// 第5步,lea指令装入有效地址,用来得到局部变量和函数参数的指针。这里[ebp-48h]就是基地址再向下偏移48h,就是前面说的为本地变量留出的空间的起始地址;将这个值装载入edi寄存器,从而得到局部变量的地址;


// 下面的这第六步可是非常的重要,请记住:

// 第六步,给段寄存器预先赋值:

0040125C mov ecx,12h

// ECX寄存器是段寄存器的一种,为计数器 SS 堆栈段。设为12h。

00401261 mov eax,0CCCCCCCCh

// EAX寄存器是段寄存器的一种,为累加器 CS 代码段;设为0CCCCCCCCh。


00401266 rep stos dword ptr [edi]

// 这句话是干吗的?


// 下面开始我们的代码了:

13: const int x=10000;

00401268 mov dword ptr [ebp-4], 2710h

// 第一步,在基地址向下偏移4个字节所指向的地址,将10000这个DWORD数值放进去;

// 可以看出的是,对于一个普通的int z = 10000;汇编代码依然是这个样子。说明从这句话

// 是无法分清楚局部const变量的初始化和普通变量的初始化的!这一点很重要!就是说编译器

// 从表面上是无法分清楚一个局部const变量和一个普通变量的。


14: int *y=0;

0040126F mov dword ptr [ebp-8],0


15: y=(int*)&x;

00401276 lea eax,[ebp-4]

00401279 mov dword ptr [ebp-8],eax

// 第2步,将x的地址装载到EAX寄存器;

// 第3步,再把这个地址作为一个数值导到y的地址,这样y就指向了x!

// 这是局部const变量声明的情况!

// 而对于全局const变量声明的情况,这句y=(int*)&x;的汇编却是:

// 00401276 mov dword ptr [ebp-8],offset x (0043101c)

// 一个很显著的区别!


16: *y=10;

0040127C mov ecx,dword ptr [ebp-8]

0040127F mov dword ptr [ecx],0Ah

// 第4步,通过ECX寄存器倒手,将y所指向的地址的数值修改为0Ah,也就是10!

// 编译器之所以允许这种修改const变量值的非法情况,是因为编译器并不知道这是一个

// const变量,它实在是和普通的变量太像了!


17:

18: printf("%d/n", x);

00401285 push 2710h

// 第5步,将10000数值压栈!按照惯例,这个2710h会被存在当前栈顶指针前4个字节

// 处。原来ESP指向0012FF2C,所以现在指向0012FF28了。

// 编译器为什么会直接push一个常量入栈呢?

// 我觉得可能是这样:制定C++编译器规则的人想反正都是const变量了,它的值肯定不

// 能变。printf一个普通变量是倒手两个寄存器后把EAX寄存器的内容压栈,多影响效率呀。

// 还不如直接将这个const变量的值压栈呢。


0040128A push offset string "%d/n" (0042f01c)

// 再把格式化压栈;

// 这样,printf函数将取栈顶的内容打印,当然是按照%d/n来打印的,所以只会再取栈顶的

// 0x0012FF28指向的内容;所以打印出来的就是上面压栈的常量2710h!

// 这就是我给出的解释。请高手们指正。


0040128F call printf (004082f0)

00401294 add esp,8

19:

20: printf("%d/n", *y);

00401297 mov edx,dword ptr [ebp-8]

0040129A mov eax,dword ptr [edx]

0040129C push eax

// 看,对于一个普通变量的printf,就不一样了!


0040129D push offset string "%d/n" (0042f01c)

004012A2 call printf (004082f0)

004012A7 add esp,8

21:

22:

23: return 0;

004012AA xor eax,eax

24:

 

 
<think>嗯,用户想知道C++const声明变量的作用和使用场景。我之前学过一些C++,但可能有些细节记不太清了。先回忆一下const的基本概念。const是常量,声明后不能修改。那作用的话,应该是保护变量不被意外修改,提高代码的安全性。然后使用场景可能有常量定义、函数参数、指针修饰、类成员等等。 记得引用里提到过,引用[1]说const限制变量的作用域和可见性,不过作用域应该是static控制的,可能这里的描述不太准确?或者可能指const变量在文件内的链接性?比如const全局变量默认内部链接,所以限制作用域到文件内。这点可能需要确认。 然后引用[2]提到const用于声明常量或指定只读性,用在变量、函数、指针等。比如const int myConst=42。这种情况下,变量初始化后不能修改,编译器会检查。 引用[3]的例子是用const修饰函数参数,防止在函数内部修改结构体的内容。比如void printPerson(const Person *person),这样函数里不能修改person指向的数据,避免误操作。 所以用户的问题需要总结const声明变量的作用,包括保护数据不被修改、提高代码可读性、编译器优化可能等。使用场景的话,比如定义常量、函数参数中的只读参数、指针或引用的const修饰、类中的常成员变量等等。 可能还需要注意不同位置const的作用,比如const在指针的左边还是右边,比如const int* p和int* const p的区别。前者指向的内容不可变,后者指针本身不可变。这也是常见的考点。 另外,类中的const成员变量必须在构造函数初始化列表中初始化,不能在函数体内赋值。这点也要提到。 可能用户还不太清楚这些细节,所以需要分点说明,结构清晰。比如作用分几点,使用场景再分不同情况举例。同时结合引用的例子,比如函数参数中的const,结构体的指针等。 需要检查是否有错误,比如const变量的链接性是否正确。比如在C++中,全局const变量默认内部链接,所以不同文件可以定义同名的const变量而不会冲突。而如果要用外部链接的话,需要加extern。 但用户的问题主要是声明变量的作用和使用场景,可能不需要深入链接性,但可以简单提到作用域相关的内容。 另外,constexpr和const的区别可能是一个进阶问题,但用户的问题可能不需要涉及,除非必要。 总结起来,回答应该包括: 作用: 1. 防止修改变量值,保证数据安全 2. 提高代码可读性,明确变量为常量 3. 帮助编译器优化,如常量折叠 4. 作为函数参数时,避免意外修改传入的数据 使用场景: 1. 定义常量替代宏 2. 函数参数修饰,特别是指针或引用,防止修改 3. 类中的常成员变量 4. 修饰指针,区分指向常量和常量指针 5. 常量引用,如函数参数避免复制 同时,每个部分给出例子,比如代码示例,用LaTeX格式包裹。例如: 定义常量:const int MAX_SIZE = 100; 函数参数:void func(const int* ptr); 类成员变量:在构造函数的初始化列表初始化。 指针的不同情况:const int* ptr和int* const ptr。 可能还需要提到const在成员函数后的用法,比如常成员函数,但用户的问题是关于声明变量,所以可能不需要展开,但如果使用场景包括类成员变量的话,需要说明。 最后,根据系统指令生成相关问题,比如constconstexpr的区别,如何用const修饰类成员,或者指针的const用法区别等。</think>### C++中`const`声明变量的作用与使用场景 #### 一、`const`的作用 1. **防止变量被修改** 通过`const`声明变量不可被重新赋值,编译器会直接阻止修改操作。例如: ```cpp const int MAX_SIZE = 100; MAX_SIZE = 200; // 编译错误:表达式必须是可修改的左值 ``` 这种特性可以避免程序运行时因意外修改关键数据导致的逻辑错误[^2]。 2. **提高代码可读性** `const`明确标识变量的常量属性,使代码意图更清晰。例如: ```cpp const double PI = 3.14159; // 明确表示PI是常量 ``` 3. **编译器优化支持** 常量可能在编译阶段被直接替换为字面值(常量折叠),从而提升运行效率。 4. **作为函数参数的限制** 在函数参数中使用`const`修饰指针或引用,可防止函数内部修改外部数据。例如: ```cpp void printData(const std::string& str) { // str[0] = 'A'; // 错误:str是常量引用 std::cout << str; } ``` --- #### 二、`const`的使用场景 1. **定义常量替代宏** 用`const`取代`#define`定义常量,具备类型检查且更安全。例如: ```cpp const int BUFFER_SIZE = 1024; // 替代 #define BUFFER_SIZE 1024 ``` 2. **函数参数保护** 当函数不需要修改传入的数据时,使用`const`修饰指针或引用参数。例如: ```cpp void calculate(const int* array, int size) { // array[0] = 1; // 错误:array指向的数据是常量 } ``` 3. **类中的常成员变量** 类的`const`成员变量必须在构造函数初始化列表中初始化,且后续不可修改。例如: ```cpp class Circle { public: Circle(double r) : radius(r) {} // 初始化列表赋值 private: const double radius; // 常量成员变量 }; ``` 4. **指针的`const`修饰** - **指向常量的指针**:`const int* ptr`(指针指向的值不可变) - **常量指针**:`int* const ptr`(指针本身不可变) ```cpp int a = 10; const int* p1 = &a; // 不能通过p1修改a的值 int* const p2 = &a; // p2不能再指向其他地址 ``` 5. **常量引用** 避免拷贝大型对象,同时防止修改原始数据。例如: ```cpp void processData(const std::vector<int>&
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值