浅谈C++ const
引入
分别考虑以下代码:
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
#include <bits/stdc++.h>
int main() {
const int a = std::rand();
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
请问两次代码分别会输出什么?
运行后不难发现,前者会输出42,后者则输出1。事实上,两者逻辑几乎一致:
- 定义一个常量a并初始化
- 强制修改a的值
- 输出a
那为什么行为上存在差异呢?
汇编分析
我们通过Compiler Explorer查看二者的汇编代码(省略部分代码),如下:
main:
; ...
lea rdi, [rip + .L.str] ; 传递printf第一个参数
mov esi, 1 ; 传递printf的第二个参数
xor eax, eax ; 将eax寄存器清零,便于printf调用
call printf ; 调用printf
; 以上代码相当于printf("%d\n", 1);
; ...
.L.str:
.asciz "%d\n"
main:
; ...
call rand ; 调用rand函数
lea rdi, [rip + .L.str] ; 传递printf第一个参数
mov esi, 42 ; 传递printf的第二个参数
xor eax, eax ; 将eax寄存器清零,便于printf调用
call printf ; 调用printf
; 以上代码相当于printf("%d\n, 42);
; ...
.L.str:
.asciz "%d\n" ; 定义格式化字符串
观察到,编译器忽略了a的内存分配,并直接使用Magic Number作为A的值。
符号表替换
我们先分析代码A。通过查阅资料可知,编译器会进行符号表替换优化,具体来说,会将所有编译期常量替换为Magic Number,如以下代码:
const int a = 114514;
int b[a];
会被优化为
int b[114514];
这一优化发生在AST阶段,位于预处理之后,汇编之前。那么回到刚才的代码,
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
显然,按照刚才的逻辑,程序会被优化成这样:
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", 1);
}
那么,此时显然
const int a = 1;
const_cast<int &>(a) = 42;
已经没有任何意义,那么编译器会根据as-if原则(编译器可以自由地改变程序,只要可观察行为与原始程序一致。),这段代码就会被优化。
最终被优化为:
#include <bits/stdc++.h>
int main() {
std::printf("%d\n", 1);
}
代码B分析
接下来考虑代码B。我们知道,符号表替换适用于编译期常量,显然对于代码B不适用。接下来,编译器会考虑将变量a分配至.rodata段(只读数据段)。然而很不幸,上述方法不适用于局部变量。因此,编译器只能像普通变量一样处理a,只不过在编译器进行检查。
但是,const_cast会拒绝编译器检查,相当于告诉编译器“我保证这段代码是安全的”。于是,编译器检查通过后,源代码
#include <bits/stdc++.h>
int main() {
const int a = std::rand();
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
会被处理为像这样:
#include <bits/stdc++.h>
int main() {
int a = std::rand();
a = 42;
std::printf("%d\n", a);
}
此时,编译器会进行数据流分析最后发现:
唯一一次获取a的值,即printf时,a的值是确定的,为42。因此,编译器会认为a是没有意义的,优化成这样:
#include <bits/stdc++.h>
int main() {
std::rand();
std::printf("%d\n", 42);
}
请注意,此处的std::rand是有副作用的,也就是说,执行std::rand会改变程序状态。因此,编译器不会删除std::rand的调用,但是会忽略其返回值。
写在最后
永远不要尝试修改一个常量!这在C++中是未定义行为,也就是说,这种操作的结果是不确定的,编译器可以对未定义行为进行任何处理。上述分析只是当前主流编译器的普遍优化方法。
参考
- ISO/IEC 14882:2024
- https://godbolt.org/
- https://chat.deepseek.com/
浅谈C++ const的更多相关文章
- 浅谈JS中 var let const 变量声明
浅谈JS中 var let const 变量声明 用var来声明变量会出现的问题: 1. 允许重复的变量声明:导致数据被覆盖 2. 变量提升:怪异的数据访问.闭包问题 3. 全局变量挂载到全局对象:全 ...
- 浅谈在ES5环境下实现const
最近看到一个面试题--用ES5实现const.作为JS初学者的笔者知道在ES6中有const命令,可以用来声明常量,一旦声明,常量的值就不可改变.例如: 1234567891011 const Pi ...
- 浅谈Hybrid技术的设计与实现第三弹——落地篇
前言 接上文:(阅读本文前,建议阅读前两篇文章先) 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 根据之前的介绍,大家对前端与Native的交互应该有一些简单的认识了,很多 ...
- 浅谈Linux中的信号处理机制(二)
首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...
- 浅谈 Linux 内核无线子系统
浅谈 Linux 内核无线子系统 本文目录 1. 全局概览 2. 模块间接口 3. 数据路径与管理路径 4. 数据包是如何被发送? 5. 谈谈管理路径 6. 数据包又是如何被接收? 7. 总结一下 L ...
- 浅谈iOS视频开发
浅谈iOS视频开发 这段时间对视频开发进行了一些了解,在这里和大家分享一下我自己觉得学习步骤和资料,希望对那些对视频感兴趣的朋友有些帮助. 一.iOS系统自带播放器 要了解iOS视频开发,首先我们从 ...
- 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树
http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...
- 浅谈Android系统进程间通信(IPC)机制Binder中的Server和Client获得Service Manager接口之路
文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6627260 在前面一篇文章浅谈Service ...
- C++ STL中的常用容器浅谈
STL是C/C++开发中一个非常重要的模板,而其中定义的各种容器也是非常方便我们大家使用.下面,我们就浅谈某些常用的容器.这里我们不涉及容器的基本操作之类,只是要讨论一下各个容器其各自的特点.STL中 ...
- [UWP]浅谈按钮设计
一时兴起想谈谈UWP按钮的设计. 按钮是UI中最重要的元素之一,可能也是用得最多的交互元素.好的按钮设计可以有效提高用户体验,构造让人眼前一亮的UI.而且按钮通常不会影响布局,小小的按钮无论怎么改也不 ...
随机推荐
- VsCode+DeepSeek的AI编程助手初体验
前言 最近随着AI编程助手的兴起,我这个重度码农也想试着尝下鲜,看看他究竟有多厉害,会不会把我们都给取代了.Github Copilot大名鼎鼎,和微软全家桶重度绑定,但是使用价格不菲,并且使用它有一 ...
- QPlainTextEdit获取鼠标选中内容
QPlainTextEdit获取鼠标选中内容 m_plainTextEdit是一个 QPlainTextEdit * 获取选中内容 QString selectStr = m_plainTextEdi ...
- OAuth2密码模式:信任的甜蜜陷阱与安全指南
title: OAuth2密码模式:信任的甜蜜陷阱与安全指南 date: 2025/05/29 14:56:19 updated: 2025/05/29 14:56:19 author: cmdrag ...
- 面试题:java Runnable与Callable 的区别
相同点 都是接口:(废话,当然是接口了) 都可用来编写多线程程序: 都需要调用Thread.start()启动线程. Callable是类似于Runnable的接口,实现Callable接口的类和实现 ...
- Django Web应用开发实战第一章
一.常见域名后缀 .com:商业性的机构或公司. .net:从事Internet相关的网络服务的机构或公司. .org:非营利的组织.团体. .gov:政府部门. .cn:中国国内域名. .com.c ...
- java客户端发送socket消息到指定服务并接收响应
做个笔记 /** * 发送socket到指定服务 * 接收有6位报文头长度的响应,支持读取分包 * * @param host IP * @param port 端口 * @param msg 消息内 ...
- java netty socket实例:报文长度+报文内容,springboot
前言 说实话,java netty方面的资料不算多,尤其是自定义报文格式的,少之又少 自己写了个简单的收发:报文长度+报文内容 发送的话,没有写自动组装格式,自己看需求吧,需要的话,自己完善 服务端启 ...
- WPF与WinForm的对比
WPF与WinForm的对比 本文同时为b站WPF课程的笔记,相关示例代码 创建新项目 在vs2022中,这两者分别叫做WPF应用和Windows窗体应用. 渲染引擎和设计 WPF使用DirectX作 ...
- 洛谷 P6625 [省选联考 2020 B 卷] 卡牌游戏
洛谷 P6625 [省选联考 2020 B 卷] 卡牌游戏 题目传送门 Solution 每次操作的得分都是一个前缀和,即每次的得分为\(p=\sum_\limits{i=1}^ka_i(2\le k ...
- etcd详细介绍
一.etcd介绍 etcd是一个分布式.可靠的key-value存储的分布式系统,它不仅仅可以用于存储,还提供共享配置和服务发现.这里提供配置共享和服务发现的系统较多,比较常用的有zookeeper. ...