C++编译器如何实现 const(常量)?

C++编译器如何实现 const(常量)?

表面上看,我们在讨论 “编译器怎么保证一个常量不会被程序员强行改变呢?”;其实,我们说的是:如果你表明自己就是要强行修改一个常量,那么,C++编译器也会支持你。


1. 问题

某乎网友 Shinnku 问:
我(提问者)最近在写C++程序时发现编译器并不是单纯的靠类型检查来实现const的类型安全的。我之前认为只有下面一种方式:

#include <iostream>
#include <stdio.h>
const char name[] = "Hello World";
int main() {
	(char*)name[2] = '3';
        //编译器直接报错,const char[13]不能转成(char*)
	std::cout << name << std::endl;
	return 0;
}

但是请看下面的代码:

#include <iostream>
#include <stdio.h>
const char name[] = "Hello World"; //const char[13]
template<typename _Ty> void f(_Ty param) {
	std::cout << param << std::endl;
	printf("%p\n", ¶m[2]);
	param[2] = '3';
};
int main() {
	printf("%p\n", &name[2]);
	f((char*)name);
	std::cout << name << std::endl;
	return 0;
}

成功编译,但却仍然无法在那块区域写入内容,在VC中运行上面代码,有如下图的报错:

请问是什么原因能让C++保持在运行之后占有那块内存却无法写入的?

编者注:

提问者后面的代码逻辑是:
(1) name 是常量字符串(字符指针),其内容按理不允许被修改;
(2) 但在 main() 中调用f,通过C风格强制转换 (char*)name,将它转成非常量字符指针;
(3) 于是等真执行到 f() 里头,第11行修改 param[2],相当于是要修改 name 的内容(而name在根源上可是常量,于是运行出错,发生如图中的“写入访问权限冲突”;
(4) 提问者想问:程序是不是具备某种机,所以能够及时发现我们的非法企图(试图修改常量),然后及时“甩脸”(中止程序);
(5) 另外,对比第一段测试程序,为什么前者能直接在编译期就报错,而后者却是在运行时才报错?

2 南老师的回答

哈哈哈哈,好多人批你(却不知自己中招你的障眼法),并没说到点子上啊,所以我猜你可能会郁闷。

我可能也会说得比较凶狠或过头,望能理解。

一、你的第一个错:基础不扎实

//你写的代码:
(char*)name[2] = '3';    

//它的正确理解:
(char*)(name[2]) = '3';  //正: 把 name里面的第3个 char 强制转换为 char*

//这似乎是你的理解:
((char*)name)[2] = '3';  //误: 先强制去除 name 的const修饰,其后修改它的第3个字符

二、你的第二个错:做事不认真

有意思的是,你看了编译错误,并且将它翻译成中文(或者VC本来就能报中文),下面这话是你问题中的原文:

//编译器直接报错,const char[13] 不能转成(char*)

我试了一下使用GCC编译的情况,应该是一个警告和一个错误:

warning: cast to pointer from integer of different size
error : lvalue required as left operand of assignment

附加指出的错误位置是:

(char *)name[2] = 'M';
                  ^

里面可有出现一个“const”不? “cast to pointer from integer ……”中的“pointer/指针”及“integer/整数”丝毫不能引发你的好奇心?

你的第一个错误,本有机会在此刻得到纠正,但你没有,你看似先入为主,其实就是没有认真过大脑地,继续往错误的人生道路前行,就差掉进犯罪的深渊。结合你的上下文,此时你应该是得出: “编译器可以在这 种 代 码 形 式 ”下正确识别及实现“const”的相关限制。

或许: VC真的报“const char[13]不能转换成(char *)”?

三、你的第三个错:甩锅太随意

然后,你开始测试另一种代码形式的测试。那个形式有模板、有强制转换,复杂度略高,并且结果与你事先认为的不同,于是你开始“屈(甩)服(锅):嗯,一定是C++太复杂,所以它出现这种奇怪的,前后不一致的行为也是正常的。于是判定,这应该要从更复杂层面(编译器实现)才能理解的了……

铺垫已足,强行插入:
很多小白看问题看到这里,也该差不多进一步强化了C++的印象:“这玩意太复杂,复杂到语言自己都能自己打架,也就一堆老人会去看它的底层编译原理实现才能明白这门语言的脾气的……还是早早放弃C++保我一头青丝吧”

其实,第二个案例中,全部“精华”就在这一句:

f((char*)name);

再精一点,就是:

(char *)name

既然是要做“对比”分析,那就再对比一次,字面上的对比:

甲组

乙组

疑惑

奇怪,C++编译器会检查以下const

奇怪,C++编译器不会检查以下const

代码

(char *)name[2]

(char *)name

然后有“百思不得其解”:

“请问是什么原因能让C++保持在运行之后占有那块内存却无法写入的?”

这是一个好问题,但这不是初始问题应该衍生出来的好问题。因为如此轻轻松松跳过编译期的警告和错误,直接杀入运行期更深层次问题的思考——无助于原始问题理解,还会误导很多人,前面说的初学者,还有——可怜,辛苦一个个技术大牛恨不得从CPU的电路实现帮你说起……

以下是用来欢乐的:

女:“抱歉,我们真的不合适。”
男:“为什么!为什么你会怀疑结婚以后我们之间会在某些事情上不和谐?”
女:“我没有说结婚以后的事啊??”
男:“那你为什么要说我们不合适呢?!”

对应——

编译器:“喂,警告你哦,你这里要把一个整数转强制换成指针,它俩的尺寸都不一样呢。这样合适吗?”
程序员:“你丫的,你是怎么做到常量一会儿可改一会儿不可改的?!”。
编译器:“我没提到常量啊。”
程序员:“那你为什么要提‘cast to’!!!”


考虑到一些答案(可能是受你误导)基本答错了,下是正面回答。

首先,“在C语言中,强制转换基本是能成功的”——但这只是上半句,还有下半句非常重要: “如果一次强制转换不能成功,那就再来一次”。

按照这一原则,再做次对比:

甲组

(char *)name[2] ,就属于下半句:name[2] 是一个字符类型,char在C/C++归属于“整数 ”类系,即,它基本可以视为一个取值范围很小的 int ——就算不知道这些,直观上也应该很清楚:把一个 值 转换成一个地址(指针),编译器不会轻易放行的。于是报错。

编译器为什么在这里要报错?因为编译器认为你很有可能在这里是写错了。

又,编译器为什么会怀疑你在这里很有可能写错了?

因为制定语法的人认为,人类在这里很可能犯一种错:忘记各类操作符的优先级。比如这里name之前的“强制转换操作(type)”和后面的下标操作 [ ],到底哪个优先级或结合率高呢?

制定语法的人是不是太小看俺们C程序员的水平呢?

至少我在这个问题和这个问题之下,看到了好些个人犯了这个错。这无关你编程高低,只要是人,就容易犯这种错——这种低级错误;优秀的编译器,从他们的祖爷爷那一代开始,就知道的自身背负的一个重大责任:纠出人类写代码时容易犯的低级错误。

乙组

(char *)name;属于上半句,可以成功。为什么?因为你都已经这样写的,而且这样写就算让一块砖头来阅读,它也可以清楚的知道你的意图就是要强制去除name的const属性。编译器是程序,而你是人。程序可以帮人纠正低级错误,但不能阻止人类的意义明确的高(复)级(杂)行为啊。(回忆一下C++的原则:永远相信一个程序员可以做出多好的事的重要性,可比永远怀疑他会做出什么错误的事重要……)

意义明确,语法正确就不阻止这个程序员。因为,搞不好这个程序员就是想在程序运行时看看什么叫段错误,比如:他是一个教编程的老师,这位老师想给学生演示一下叫段错误。我不能拦着他。编译器,特别是C/C++编译器就是这么想的。结合当前例子,假设它看到程序员将甲组那行强制转换代码改成如下:

((char*)((long)(name[2])))[0] = '3';

有哪个程序员会因为犯“低级错误”而写出这样的代码?代码写成这样子,只能是故意的。

看的人更不会犯错,因为不管是谁看到这代码,都会一边骂骂咧咧,一边打开头脑里的那个CPU水冷系统,开始全力人肉分析这是在干嘛。

这时候,C++和C就开始体现语言个性上的不同了。C++觉得这样的代码太为难程序员了,完全可以在保持代码“丑”的同时,还保持直观。注意,这里又涉及C++的两个原则:一、当你干的事情很丑,那么就应该让对应的代码也很丑。二、就算丑,也应保持直观。功能相同,但是完美符合这两条原则的写法是:

reinterpret_cast<char *>(name[2])[0] = '3';

“reinterpret”是"重新解释",并且括号就一层,转换就一次,所以看这行代码时,程序员的大脑CPU基本上保持波澜不惊,一眼即可人肉解析出代码的意图,同时嘴角露出一丝不屑:好丑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/572725.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日一题:托普利茨矩阵

给你一个 m x n 的矩阵 matrix 。如果这个矩阵是托普利茨矩阵&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果矩阵上每一条由左上到右下的对角线上的元素都相同&#xff0c;那么这个矩阵是 托普利茨矩阵 。 示例 1&#xff1a; 输入&#xff1a;matrix…

【原创教程】EPLAN如何制作专属的封面

想要给EPLAN制作专属封面吗?没问题,我来给你支个招。在EPLAN设计电气图纸时,封面就是第一印象,得好好弄。咱们以口罩机项目为例,来看看怎么做吧! 首先,得新建个封面。在项目属性里找到表格名称,点那个数值下拉菜单,选择“查找”。在弹出的表格里挑个你喜欢的模版,点击…

【IC设计】边沿检测电路(上升沿、下降沿、双沿,附带源代码和仿真波形)

文章目录 边沿检测电路的概念上升沿检测电路下降沿检测电路双边沿检测电路代码和仿真RTL代码Testbench代码仿真波形 参考资料 边沿检测电路的概念 边沿检测指的是检测一个信号的上升沿或者下降沿&#xff0c;如果发现了信号的上升沿或下降沿&#xff0c;则给出一个信号指示出来…

OurBMC开源大赛高校获奖队伍专访来啦!

精彩纷呈的 OurBMC 开源大赛已告一段落&#xff0c;经历为期四个月的实战&#xff0c;各个参赛队伍也积淀了丰富的实践经验与参赛心得。本期&#xff0c;社区特别邀请 OurBMC 开源大赛获奖高校团队分享「走进OurBMC开源大赛&#xff0c;共同践行开放包容、共创共赢的开源精神」…

【春秋云境】文件上传漏洞合集

CVE-2022-30887 1.题目简介 2.CVE-2022-30887简介 使用工具&#xff1a; 蚁剑 burpsuite 一句话木马 3.渗透测试 输入用户名密码进行抓包 猜测账号密码 无有用信息&#xff0c;根据页面现有信息找到作者邮箱&#xff1a; mayuri.infospacegmail.com&#xff0c;猜测密码为&a…

每日一题:跳跃游戏II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最…

YOLOv3没有比这详细的了吧

YOLOv3&#xff1a;目标检测基于YOLOv2的改进 在目标检测领域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;系列以其出色的性能和速度而闻名。YOLOv3作为该系列的第三个版本&#xff0c;不仅继承了前身YOLOv2的优势&#xff0c;还在多个方面进行了创新和改进。…

机器学习理论基础—支持向量机的推导(一)

机器学习理论基础—支持向量机的推导 算法原理 SVM:从几何角度&#xff0c;对于线性可分数据集&#xff0c;支持向量机就是找距离正负样本都最远的超平面&#xff0c;相比于感知机&#xff0c;其解是唯一的&#xff0c;且不偏不倚&#xff0c;泛化性能更好。 超平面 n维空间…

如何拿取 macOS 系统中的图标文件

如何拿取 macOS 系统中的图标文件 比如在 Finder 中看到这个文件夹图标很好看&#xff0c;想用一下&#xff0c;就是不知道它在什么位置&#xff0c;我来告诉你。 它在系统中的位置是 /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/如何打开这个位置&am…

计算机网络物理层思维导图+大纲笔记

大纲笔记&#xff1a; 物理层的基本概念 解决如何在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是具体的传输媒体 主要任务 确定与传输媒体接口有关的一些特性 机械特性 电气特性 功能特性 规程特性信道上传送的信号 基带信号 来自信源的信号&#xff0c;直接表…

【CLI命令行接口和Java连接openLooKeng查询数据 】

CLI命令行接口和Java连接openLooKeng查询数据 一、摘要二、正文0. 环境说明1. CLI命令行工具的使用2. Java API 的使用三、小结一、摘要 通过CLI命令行接口工具连接openLooKeng,可帮助初学者能够使用SQL语句的方式快速操作openLooKeng,任何只要熟悉SQL的人都可以快速切换到op…

解决 uniapp uni.getLocation 定位经纬度不准问题

【问题描述】 直接使用uni.getLocation获取经纬度不准确&#xff0c;有几百米的偏移。 【解决办法】 加偏移量 //加偏移 let x longitude let y latitude let x_pi (3.14159265358979324 * 3000.0) / 180.0 let z Math.sqrt(x * x y * y) 0.00002 * Math.sin(y * x_pi)…

ArcGIS Pro专题地图系列教程

专题地图系列是ArcGIS Pro3.2的新功能。之前&#xff0c;如果要做8张相同区域的专题图&#xff0c;可能需要新建8个布局&#xff0c;分别进行排版&#xff0c;再导出。现在&#xff0c;一幅地图&#xff0c;一个布局&#xff0c;就可以完成这个流程。 原理是&#xff0c;根据单…

Swift-24-集合对象

概述 在了解正式内容之前可以先回顾下objectiveC中提供的集合特性。 它的特点是&#xff0c;拿NSArray举例&#xff0c;包含NSArray 和 NSMutableArray两个API&#xff0c;前者是不可变数组&#xff0c;一旦创建其值和数量就不能改变了&#xff1b;NSMutableArray是可变数组&…

tableau基础学习——添加标靶图、甘特图、瀑布图

标靶图 添加参考线 添加参考分布 甘特图 创建新的字段 如设置延迟天数****计划交货日期-实际交货日期 为正代表提前交货&#xff0c;负则代表延迟交货 步骤&#xff1a;创建——计算新字段 把延迟天数放在颜色、大小里面就可以 瀑布图 两个表按照地区连接 先做个条形图&…

工业4.0的基石:探索工业级光模块的力量

引言 工业4.0代表着智能制造的新时代&#xff0c;而工业级光模块则是这一革命性转变的基石。这些高科技组件不仅是现代通信网络的核心&#xff0c;更是连接智能工厂、智慧城市和远程服务的关键。本文将深入探讨工业级光模块的技术特性、应用领域以及它们如何塑造未来工业的面貌…

公司网页制作需要多少钱

公司网页制作需要多少钱&#xff1f;这是一个非常常见的问题。答案取决于您需要的功能和设计。一些小型企业网站可能只需要一些基本的功能&#xff0c;花费可能低至几百美元&#xff0c;而一些大型企业网站可能需要高级功能和设计&#xff0c;可能需要几万美元。 以下是一些考虑…

js如何获取对象的属性值

获取对象的属性值&#xff0c;有两种方式。 方式一&#xff1a; 对象.属性名 let obj {name:张三,age:23 }; console.log(obj.name); //张三方式二&#xff1a; 对象[属性名] let obj {name:张三,age:23 }; console.log(obj[name]); //张三 两种方式有什么不同&am…

Mac安装telnet

一、安装Homebrew 1、打开官网&#xff1a;Homebrew — The Missing Package Manager for macOS (or Linux) 2、打开终端输入&#xff1a; /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 二、安装Telnet bre…

内容策略的精准定位:Kompas.ai的目标受众分析

在这个信息爆炸的时代&#xff0c;内容营销已经成为品牌与消费者沟通的重要桥梁。然而&#xff0c;随着内容的海量增长&#xff0c;品牌如何从众多信息中脱颖而出&#xff0c;成为营销人员面临的巨大挑战。精准定位目标受众&#xff0c;不仅能够帮助品牌更有效地传达信息&#…