C++ 基础概念: 未定义行为(Undefined Behavior)
文章目录
- Intro
- 如何正确认识 UB
- 有多少未定义行为?
- 对 UB 的误解
- C++ 标准定义的几种行为
- 1. 定义的行为 (defined behavior)
- 2. 实现定义的行为 (implementation defined behavior)
- 3. 未指定的行为 (unspecified behavior)
- 4. 未定义行为 (undefined behavior)
- 揭晓答案
- C++ 中如何定义 UB
- UB 不是错误
- 常见 C++ UB 的部分列表
- 软件设计理念
- 编译器选项对 UB 的影响
- 当关闭编译器优化时
- 通常会启用优化
- 如何消除 UB
- UB 举例
- 带符号的整型溢出
- 缺失 `return` 语句
- 迭代器在使用时被破坏
- 修改`const reference`类型值
- 在`std`命名空间中增加代码
- 求值顺序
- 语法歧义
- 总结
- 视频链接
- 源码链接
Intro
在编程中我们会听到或者看到一个概念:“未定义行为(Undefined Behavior, 简称 UB)”. 什么是所谓的未定义行为, 会产生什么后果, 如何能避免? 本文将系统地探讨未定义行为的含义, 后果及规避方法
如何正确认识 UB
对"未定义行为"的字面理解就是: 这个行为没有被具体说明. 举个例子, 如果读到了 std::vector<T> 的末尾会发生什么?
可能得结果有:
- 读取操作可能返回完全有效的
T - 或者可能返回非
T的值 - 程序可能在运行时崩溃
- 读取操作可能会被编译器优化掉(删除掉)
因为根据 C++ 标准:
reading past the end of
std::vectoris undefined behavior
读取超过
std::vector末尾的内容是未定义的行为
因为没有做具体说明, 所以编译器可以做很多选择. 然而这些选择不一定是编写代码的程序员所期望的.
有多少未定义行为?
举个例子, 用 26 个英语字母作组合可以产生非常多单词. 排除掉字典里面定义的单词, 其他的单词可以被认为是"未定义的", 可想而知这些未定义的情况非常多的.
同样的, C++语言中的 UB 的具体 case 也会非常多, 不可枚举.
对 UB 的误解
一些常见的误解如下:
- 良好的测试将捕获 UB: 尽管测试可以帮助发现部分问题, 但由于测试覆盖率不足或测试环境与实际运行环境的差异, UB 可能在测试中未被触发. 例如, 某些 UB 仅在特定输入或硬件平台上显现, 因此良好的测试并不能保证捕获所有 UB.
- 更好的编译器会将 UB 报告为错误: 编译器主要在静态分析范围内工作, 能检测的未定义行为是有限的. 许多 UB 需要在运行时动态触发, 例如特定输入条件或程序状态下才会显现, 这超出了编译器的检测能力.
- 经验丰富的开发人员永远不会遇到糟糕的 UB
- 调试 UB 只需要一点练习
后续的例子中我们将会消除这些迷思.
C++ 标准定义的几种行为
C++标准中定义了如下几种行为:
1. 定义的行为 (defined behavior)
具有明确或精确含义的代码, 比如:
int sum = 17 + 8;printf("Welcome to CppCon 2021");auto [first, second] = getPair();
2. 实现定义的行为 (implementation defined behavior)
代码可以有多重含义, 但编译器必须选定一种并始终保持该选择.
请看下面的代码:
if ( sizeof(int) < sizeof(long) ) { }
C++ 标准中规定了int最小要有 16bit, long最小要有 32bit. 具体 bit 位数会因为编译器/操作系统而有所不同, 常见的编译器 GCC, Clang, MSVC 指定了 sizeof(int) == 4, sizeof(long)
3. 未指定的行为 (unspecified behavior)
代码可能有多种含义, 编译器可以随机选择一个.
比如比较字符串字面量:
#include <iostream>void fun(const char* key) {if (key == "name") {std::cout << "get name\n";} else {std::cout << "something else\n";}
}
int main() {std::string name = "name";fun("name"); // output: get namefun(name.c_str()); // output: something elsereturn 0;
}
比较字面量在实际中被实现为比较指针. 而程序员预期的应该是字符串比较.
4. 未定义行为 (undefined behavior)
毫无意义的代码, 比如:
- 两次调用对象的析构函数
- 按负值进行位移位
- 当值太大时将双精度数转换为浮点数
阅读下面的代码思考一下这两个问题:
- 下面的代码能通过编译吗?
- 是否有 UB, 有的话指出具体行数.
#include <iostream>int main() {int* p = nullptr; // line 1*p = 42; // line 2int b; // line 3p = &b; // line 4std::cout << *p; // line 5std::cout << b; // line 6
}
揭晓答案
- 能通过编译
- 有下面这些 UB
- ( line 2 ) 解析空指针是 UB
- ( line 5 和 line 6) 访问一个未初始化的变量 UB
C++ 中如何定义 UB
- 所谓 UB 就是尝试去执行那种没有被 C++标准明确说明其行为的代码.
- 只有当源代码没有 UB 时, 程序会按源代码所写的执行
- 如果你的代码有 UB, 那么 C++ 标准将对其执行结果不做任何保证
- 编写没有 UB 的代码是程序员的责任
UB 不是错误
- UB 和错误(Error)之间没有重叠
- 被定义为错误的东西不是 UB
- UB 不是你的代码可以测试的东西
常见 C++ UB 的部分列表
- 访问
std::vector末尾以外的元素 - 解引用空指针
- 使用未初始化的变量
- 从构造函数或析构函数调用纯虚函数
- 在对象被销毁后使用它(释放后使用)
- 将指针转换为不兼容的类型, 然后使用
- 无副作用的无限循环
- 修改字符串文字或任何其他
const对象 - 无法从值返回函数返回值
- 任何竞争条件
- 整数除以零
- 有符号整数溢出
软件设计理念
- 既然编译器可以做任何事情, 你不妨想象它会做一些坏事
- 如果你的代码适用于所有当前的编译器, 那么你所做的任何事情都可能成为标准的一部分
- 让人们以自己的方式尝试, 直到代码在测试期间崩溃
- 对于那些关心速度的人来说, UB 应该只作为一种可选功能存在
- 最终委员会将完成他们的工作并摆脱 UB
- 程序员应该提供在他们的代码库中使用 UB 的合理理由
编译器选项对 UB 的影响
当关闭编译器优化时
- 几乎不会对您的代码进行任何特殊处理
- 尽可能将您的代码翻译得接近字面意思
- 未定义的行为可能会按照您的预期执行, 因此您的代码似乎按预期运行
通常会启用优化
- 可以删除无法访问的代码
- 编译器无需诊断未定义的行为
- 代码可以"内联", 然后进行优化
- 当程序具有未定义的行为时可能会产生意外结果
如何消除 UB
-
借助工具
Address SanitizerMemory SanitizerUndefined Behavior SanitizerThread Sanitizer
-
代码审查, 制定专门检查 UB 的政策
-
注意编译器警告
-
使用多个编译器构建代码
-
测试极端情况
-
将 UB 视为严重错误
UB 举例
带符号的整型溢出
- 有符号整数运算: 如果结果超出可表示值的范围, 则会发生"有符号整数溢出", 这是未定义的行为
- 无符号整数运算: 根据标准, 此操作永远不会溢出, 并且是定义的行为
#include <iostream>template <typename T>
T cubic(T len) {return len * len * len;
}int main() {std::cout << "cubic signed: " << cubic(3000) << std::endl; // UBstd::cout << "cubic unsigned: " << cubic(3000u) << std::endl; // OKreturn 0;
}
缺失 return 语句
一些编译器会发出警告, 一些清理程序会在运行时检测到. 程序执行过程中的常见结果
- 可能导致崩溃
- 每次都可能返回 true
- 可能会继续执行可执行文件中的"下一个函数"
#include <iostream>bool baz() { return true; }
bool foo(int a, int b) { a == b; }
bool bar() { return false; }int main() {int a = 1;int b = 2;std::cout << "a == b: " << foo(a, b) << std::endl;std::cout << bar() << baz() << std::endl;return 0;
}
迭代器在使用时被破坏
容器上的某些操作会使迭代器无效, std::vector::insert() 使所有迭代器无效.
- 基于范围的
for循环中的迭代器被隐藏 - 当前迭代器在
insert之后被破坏
#include <iostream>
#include <vector>int main() {std::vector<int> vec = {1, 2, 3, 5, 6};for (auto &item : vec) {if (item == 3) {vec.insert(vec.begin(), 4);}std::cout << item << std::endl;}return 0;
}
修改const reference类型值
关键字 const_cast 删除对象的"常量性", 如果传递的参数最初被声明为 const, 则修改输入是未定义的行为
#include <iostream>
#include <string>const std::string global = "Hello";void fun(const std::string &input) {std::string &v = const_cast<std::string &>(input);v = "fun";
}int main() {const std::string local = "World";fun(local);std::cout << local << std::endl;fun(global);std::cout << global << std::endl;return 0;
}
在std命名空间中增加代码
偏特化 std 命名空间中存在的类型特征是 UB. 编写自己的类型特征是完全可以接受的, 它们可以
位于除 std:: 之外的任何命名空间中.
#include <iostream>
#include <type_traits>namespace std {
template <>
struct is_pointer<int> : public std::true_type // defines a type trait as true
{};
} // namespace stdint main() {bool var2 = std::is_pointer<int>::value;std::cout << std::boolalpha << std::is_pointer<int>::value << std::endl;return 0;
}
求值顺序
#include <iostream>int main() {int a = 5;a = ++a + 2; // C++03, undefined behaviora == 8; // C++11 and newer, definedstd::cout << "a: " << a << std::endl;int b = 3;b = b++ + 2; // C++03 and C++11, undefined behaviorb == 5; // C++17 and newer, definedstd::cout << "b: " << b << std::endl;
}
语法歧义
这个函数有未定义的行为吗?
#include <iostream>template <typename T1, typename T2>
void fun(T1 &x, T2 &y) {x << y;
}int main() {int a = 1;int b = 1000;fun(a, b); // UB: 左移操作的移动位数超过了类型的宽度fun(std::cout, "cat"); // OK
}
我们看到这个例子中是否有 UB 取决于入参, 以及入参的具体值.
C++ 为了更明确模板的行为, 推出了新特性"Concept"概念, 用来约束模板参数. 方便开发者避免此类问题.
总结
- UB 不能被视为错误
- 处理 UB 并非是一个间歇性工作, 需要一直坚持
- UB 不是一个简单的话题
- 项目可以选择关闭 C++ 特性(如异常), 但你不能关闭 UB
- 处理 UB 是每个开发人员的责任, 选择 C++ 就等于接受了它
视频链接
- Back To Basics: Undefined Behavior - Ansel Sermersheim & Barbara Geller - CppCon 2021
源码链接
源码链接
相关文章:
C++ 基础概念: 未定义行为(Undefined Behavior)
文章目录 Intro如何正确认识 UB有多少未定义行为?对 UB 的误解 C 标准定义的几种行为1. 定义的行为 (defined behavior)2. 实现定义的行为 (implementation defined behavior)3. 未指定的行为 (unspecified behavior)4. 未定义行为 (undefined behavior)揭晓答案 C 中如何定义…...
Rad Studio 11.3 Alexandria 3236a(DELPHI 11.3)官方ISO/百度云盘 下载地址
Embarcadero很高兴地宣布RAD Studio 11 Alexandria Release 3的发布,也被称为RAD Studio 11.3,同时发布的还有Delphi 11.3和CBuilder 11.3。这个版本专注于质量和改进,建立在RAD Studio 11 Alexandria三个前版本的伟大的新功能上。 RAD Studi…...
vue3-watchEffect异步依赖收集
当 b 更新时 a 并不会更新,因为watchEffect的依赖收集在该案例中停止于await asyncFn(),也就是只会收集同步代码的依赖,await 之后的异步代码的依赖并不会收集到 <template> <div>a: {{ a }} <br>b: {{ b }} <br>&l…...
微信小程序中 “页面” 和 “非页面” 的区别
微信小程序中 “页面” 和 “非页面” 的区别,并用表格进行对比。 核心概念: 页面 (Page): 页面是微信小程序中用户可以直接交互的视图层,也是小程序的基本组成部分。每个页面都有自己的 WXML 结构、WXSS 样式和 JavaScript 逻辑…...
【蓝桥杯】43709.机器人繁殖
题目描述 X 星系的机器人可以自动复制自己。它们用 1 年的时间可以复制出 2 个自己,然后就失去复制能力。 每年 X 星系都会选出 1 个新出生的机器人发往太空。也就是说,如果 X 星系原有机器人 5 个,1 年后总数是:5 9 14…...
【机器学习】机器学习的基本分类-自监督学习(Self-supervised Learning)
自监督学习是一种机器学习方法,介于监督学习和无监督学习之间。它通过数据本身生成标签,创建训练任务,从而学习数据的表征,而不需要人工标注的标签。这种方法在减少标注数据依赖、提高模型通用性等方面具有重要意义。 自监督学习的…...
R shiny app | 网页应用 空格分隔的文本文件在线转csv
shiny 能快速把R程序以web app的形式提供出来,方便使用,降低技术使用门槛。 本文提供的示例:把空格分隔的txt文件转为逗号分隔的csv文件。 前置依赖:需要有R环境(v4.2.0),安装shiny包(v1.9.1)。括号内是我使用的版本…...
三天速成微服务
微服务技术栈 总结 微服务技术对比 技术栈 SpringCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud Springboot和SpringCould兼容性 代码目录结构如下 用于远程调用Bean 代码 package cn.itcast.order.config;//import …...
【踩坑记录】uni-app 微信小程序调试不更新问题解决指南
uni-app 微信小程序调试不更新问题解决指南 在使用 uni-app 开发微信小程序时,可能会遇到代码修改后无法更新或者不生效的问题。这种现象常见于调试阶段,通常与缓存、编译或代码错误有关。 本文将详细分析调试过程中常见的“不更新”问题,并…...
【Adobe Acrobat PDF】Acrobat failed to connect to a DDE server.是怎么回事?
【Adobe Acrobat PDF】Acrobat failed to connect to a DDE server.是怎么回事? 【Adobe Acrobat PDF】Acrobat failed to connect to a DDE server.是怎么回事? 文章目录 【Adobe Acrobat PDF】Acrobat failed to connect to a DDE server.是怎么回事&…...
PyTorch 中 coalesce() 函数详解与应用示例
PyTorch 中 coalesce() 函数详解与应用示例 coalesce: 美 [ˌkoʊəˈlɛs] 合并;凝聚;联结,注意发音 引言 在 PyTorch 中,稀疏张量(Sparse Tensor)是一种高效存储和操作稀疏数据的方式。稀疏…...
ubuntu进行C++的调试
方法一:gdb调试 作用: GDB 是 GNU 调试器,用于调试 C/C 程序。它可以在命令行中使用,提供强大的调试功能。 集成: GDB 可以独立于 VSCode 使用,你可以在终端中直接运行 GDB 来调试程序。 使用示例:编译程序时使用 -g 选项以包含调…...
【U8+】用友U8软件中,出入库流水输出excel的时候提示报表输出引擎错误。
【问题现象】 通过天联高级版客户端登录拥有U8后, 将出入库流水输出excel的时候,提示报表输出引擎错误。 进行报表输出时出现错误,错误信息:找不到“fd6eea8b-fb40-4ce4-8ab4-cddbd9462981.htm”。 如果您正试图从最近使用的文件列…...
NoSQL简介
NoSQL 的定义及特点 NoSQL(Not Only SQL)是一种非关系型数据库,设计之初为解决关系型数据库在扩展性、性能和多样化数据处理方面的局限性。NoSQL 支持多种数据模型,包括键值对、文档、列族和图形结构,广泛应用于大规模…...
XIAO Esp32 S3 网络摄像头——3音视频监控
1、介绍 之前分别介绍了音频和视频的接收,本文是整合了前2篇文章,实现了音视频的同时获取。 效果: 用xiao esp35 s3自制一个网络摄像头 2、适用场景广泛 家庭安防 无论是门前监控,还是室内安全,自制摄像头可以让你轻松把握每个角落,实时查看视频流,防止任何潜在风险。…...
题目解析与代码实现:You‘re Given a String
引言 本文将详细解读一道字符串处理题目 “You’re Given a String”,并用 Python 实现该题的解决方案,同时解析其核心算法逻辑。本文适合有一定基础的程序员,希望通过字符串算法提升能力的读者。 1. 题目描述 问题背景 题目给出了一个字符…...
Understanding the Lomb–Scargle Periodogram
本文目的:了解Lomb–Scargle Periodogram的原理 (用来估算不均匀采样数据的周期)参考文献Understanding the Lomb–Scargle Periodogram思路: 连续傅里叶变换 --> 离散傅里叶变换(均匀采样–> Classifical perio…...
解决Linux切换用户后的命令提示符为-bashxx$的问题
1、问题描述 切换用户时,命令提示符为-bashxx$ 比如: [rootlocalhost ~]# su zhouxingchi bash-4.2$ ### 显示看着不正常的命令提示符 2、PS1变量 PS1变量就是我们的命令提示符的内容,当我们登录时会加载该变量,从而显示提…...
AMP 混合精度训练中的动态缩放机制: grad_scaler.py函数解析( torch._amp_update_scale_)
AMP 混合精度训练中的动态缩放机制 在深度学习中,混合精度训练(AMP, Automatic Mixed Precision)是一种常用的技术,它利用半精度浮点(FP16)计算来加速训练,同时使用单精度浮点(FP32…...
Oracle数据库如何找到 Top Hard Parsing SQL 语句?
有一个数据库应用程序存在过多的解析问题,因此需要找到产生大量硬解析的主要语句。 什么是硬解析 Oracle数据库中的硬解析(Hard Parse)是指在执行SQL语句时,数据库需要重新解析该SQL语句,并创建新的执行计划的过程。这…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...
【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...
多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...
【深尚想】TPS54618CQRTERQ1汽车级同步降压转换器电源芯片全面解析
1. 元器件定义与技术特点 TPS54618CQRTERQ1 是德州仪器(TI)推出的一款 汽车级同步降压转换器(DC-DC开关稳压器),属于高性能电源管理芯片。核心特性包括: 输入电压范围:2.95V–6V,输…...
