C++ 基础概念: 未定义行为(Undefined Behavior)

news2025/1/7 18:24:37

文章目录

    • 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::vector is 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 name
  fun(name.c_str());  // output: something else
  return 0;
}

比较字面量在实际中被实现为比较指针. 而程序员预期的应该是字符串比较.

4. 未定义行为 (undefined behavior)

毫无意义的代码, 比如:

  • 两次调用对象的析构函数
  • 按负值进行位移位
  • 当值太大时将双精度数转换为浮点数

阅读下面的代码思考一下这两个问题:

  1. 下面的代码能通过编译吗?
  2. 是否有 UB, 有的话指出具体行数.
#include <iostream>

int main() {
  int* p = nullptr;  // line 1
  *p = 42;           // line 2
  int b;             // line 3
  p = &b;            // line 4
  std::cout << *p;   // line 5
  std::cout << b;    // line 6
}
揭晓答案
  1. 能通过编译
  2. 有下面这些 UB
    • ( line 2 ) 解析空指针是 UB
    • ( line 5 和 line 6) 访问一个未初始化的变量 UB

C++ 中如何定义 UB

  1. 所谓 UB 就是尝试去执行那种没有被 C++标准明确说明其行为的代码.
  2. 只有当源代码没有 UB 时, 程序会按源代码所写的执行
  3. 如果你的代码有 UB, 那么 C++ 标准将对其执行结果不做任何保证
  4. 编写没有 UB 的代码是程序员的责任

UB 不是错误

  • UB 和错误(Error)之间没有重叠
  • 被定义为错误的东西不是 UB
  • UB 不是你的代码可以测试的东西

常见 C++ UB 的部分列表

  • 访问 std::vector 末尾以外的元素
  • 解引用空指针
  • 使用未初始化的变量
  • 从构造函数或析构函数调用纯虚函数
  • 在对象被销毁后使用它(释放后使用)
  • 将指针转换为不兼容的类型, 然后使用
  • 无副作用的无限循环
  • 修改字符串文字或任何其他 const 对象
  • 无法从值返回函数返回值
  • 任何竞争条件
  • 整数除以零
  • 有符号整数溢出

软件设计理念

  • 既然编译器可以做任何事情, 你不妨想象它会做一些坏事
  • 如果你的代码适用于所有当前的编译器, 那么你所做的任何事情都可能成为标准的一部分
  • 让人们以自己的方式尝试, 直到代码在测试期间崩溃
  • 对于那些关心速度的人来说, UB 应该只作为一种可选功能存在
  • 最终委员会将完成他们的工作并摆脱 UB
  • 程序员应该提供在他们的代码库中使用 UB 的合理理由

编译器选项对 UB 的影响

当关闭编译器优化时

  • 几乎不会对您的代码进行任何特殊处理
  • 尽可能将您的代码翻译得接近字面意思
  • 未定义的行为可能会按照您的预期执行, 因此您的代码似乎按预期运行

通常会启用优化

  • 可以删除无法访问的代码
  • 编译器无需诊断未定义的行为
  • 代码可以"内联", 然后进行优化
  • 当程序具有未定义的行为时可能会产生意外结果

如何消除 UB

  1. 借助工具

    • Address Sanitizer
    • Memory Sanitizer
    • Undefined Behavior Sanitizer
    • Thread Sanitizer
  2. 代码审查, 制定专门检查 UB 的政策

  3. 注意编译器警告

  4. 使用多个编译器构建代码

  5. 测试极端情况

  6. 将 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;     // UB
  std::cout << "cubic unsigned: " << cubic(3000u) << std::endl;  // OK
  return 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 std

int 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 behavior
  a == 8;       // C++11 and newer, defined
  std::cout << "a: " << a << std::endl;

  int b = 3;
  b = b++ + 2;  // C++03 and C++11, undefined behavior
  b == 5;       // C++17 and newer, defined
  std::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

源码链接

源码链接

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cursor 使用技巧

一、创建项目前期步骤 1.先给AI设定一个对应项目经理角色&#xff0c; 2.然后跟AI沟通项目功能&#xff0c;生成功能设计文件&#xff1a;README.md README.md项目功能 3.再让AI总结写出开发项目规则文件&#xff1a; .cursorrules 是技术栈进行限定&#xff0c;比如使用什…

xinput1_3.dll丢失修复方法。方法1-方法6

总结 xinput1_3.dll的核心作用 xinput1_3.dll作为Microsoft DirectX库的关键组件&#xff0c;对于游戏控制器的支持起着至关重要的作用。它不仅提供了设备兼容性、多控制器管理和反馈机制等核心功能&#xff0c;还通过XInput API简化了开发人员对控制器状态的检索和设备特性的…

【C++】P2550 [AHOI2001] 彩票摇奖

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述输入格式&#xff1a;输出格式&#xff1a;输入输出样例&#xff1a; &#x1f4af;题解思路1. 问题解析 &#x1f4af;我的实现实现逻辑问题分析 &#x1f4af;老…

01:C语言的本质

C语言的本质 1、ARM架构与汇编2、局部变量初始化与空间分配2.1、局部变量的初始化2.1、局部变量数组初始化 3、全局变量/静态变量初始化化与空间分配4、堆空间 1、ARM架构与汇编 ARM简要架构如下&#xff1a;CPU&#xff0c;ARM(能读能写)&#xff0c;Flash&#xff08;能读&a…

8086汇编(16位汇编)学习笔记10.寄存器总结

8086汇编(16位汇编)学习笔记10.寄存器总结-C/C基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net 寄存器 8086CPU有14个寄存器 它们的名称为&#xff1a; AX、BX、CX、DX、SI、DI、SP、BP、 IP**、CS、DS、ES、**SS、PSW。 8086CPU所有的寄存器都是16位的&#…

如何在 Ubuntu 22.04 上安装 Cassandra NoSQL 数据库教程

简介 本教程将向你介绍如何在 Ubuntu 22.04 上安装 Cassandra NoSQL 数据库。 Apache Cassandra 是一个分布式的 NoSQL 数据库&#xff0c;旨在处理跨多个普通服务器的大量数据&#xff0c;并提供高可用性&#xff0c;没有单点故障。Apache Cassandra 是一个高度可扩展的分布…

uni-app:实现普通选择器,时间选择器,日期选择器,多列选择器

效果 选择前效果 1、时间选择器 2、日期选择器 3、普通选择器 4、多列选择器 选择后效果 代码 <template><!-- 时间选择器 --><view class"line"><view classitem1><view classleft>时间</view><view class"right&quo…

centos,789使用mamba快速安装R及语言包devtools

如何进入R语言运行环境请参考&#xff1a;Centos7_miniconda_devtools安装_R语言入门之R包的安装_r语言devtools包怎么安装-CSDN博客 在R里面使用安装devtools经常遇到依赖问题&#xff0c;排除过程过于费时&#xff0c;使用conda安装包等待时间长等。下面演示centos,789都是一…

STM32第十一课:STM32-基于标准库的42步进电机的简单IO控制(附电机教程,看到即赚到)

一&#xff1a;步进电机简介 步进电机又称为脉冲电机&#xff0c;简而言之&#xff0c;就是一步一步前进的电机。基于最基本的电磁铁原理,它是一种可以自由回转的电磁铁,其动作原理是依靠气隙磁导的变化来产生电磁转矩&#xff0c;步进电机的角位移量与输入的脉冲个数严格成正…

kafka使用以及基于zookeeper集群搭建集群环境

一、环境介绍 zookeeper下载地址&#xff1a;https://zookeeper.apache.org/releases.html kafka下载地址&#xff1a;https://kafka.apache.org/downloads 192.168.142.129 apache-zookeeper-3.8.4-bin.tar.gz kafka_2.13-3.6.0.tgz 192.168.142.130 apache-zookee…

JSON结构快捷转XML结构API集成指南

JSON结构快捷转XML结构API集成指南 引言 在当今的软件开发世界中&#xff0c;数据交换格式的选择对于系统的互操作性和效率至关重要。JSON&#xff08;JavaScript Object Notation&#xff09;和XML&#xff08;eXtensible Markup Language&#xff09;是两种广泛使用的数据表…

Android14 CTS-R6和GTS-12-R2不能同时测试的解决方法

背景 Android14 CTS r6和GTS 12-r1之后&#xff0c;tf-console默认会带起OLC Server&#xff0c;看起来olc server可能是想适配ATS(android-test-station)&#xff0c;一种网页版可视化、可配置的跑XTS的方式。这种网页版ATS对测试人员是比较友好的&#xff0c;网页上简单配置下…

BurpSuite工具安装

BurpSuite介绍&#xff1a; BurpSuite是由PortSwigger开发的一款集成化的Web应用安全检测工具&#xff0c;广泛应用于Web应用的漏洞扫描和攻击模拟&#xff0c;主要用于抓包该包(消息拦截与构造) 一、Burp suite安装 windows系统需要提前配置好java环境&#xff0c;前面博客…

Win11+WLS Ubuntu 鸿蒙开发环境搭建(一)

参考文章 Windows11安装linux子系统 WSL子系统迁移、备份与导入全攻略 如何扩展 WSL 2 虚拟硬盘的大小 Win10安装的WSL子系统占用磁盘空间过大如何释放 《Ubuntu — 调整文件系统大小命令resize2fs》 penHarmony南向开发笔记&#xff08;一&#xff09;开发环境搭建 一&a…

flink cdc oceanbase(binlog模式)

接上文&#xff1a;一文说清flink从编码到部署上线 环境&#xff1a;①操作系统&#xff1a;阿里龙蜥 7.9&#xff08;平替CentOS7.9&#xff09;&#xff1b;②CPU&#xff1a;x86&#xff1b;③用户&#xff1a;root。 预研初衷&#xff1a;现在很多项目有国产化的要求&#…

和为0的四元组-蛮力枚举(C语言实现)

目录 一、问题描述 二、蛮力枚举思路 1.初始化&#xff1a; 2.遍历所有可能的四元组&#xff1a; 3.检查和&#xff1a; 4.避免重复&#xff1a; 5.更新计数器&#xff1a; 三、代码实现 四、运行结果 五、 算法复杂度分析 一、问题描述 给定一个整数数组 nums&…

某xx到家app逆向

去官网下载app即可 https://www.dongjiaotn.com/#/home查壳 360的壳子 直接脱壳即可 抓包 请求地址 https://api.gateway.znjztfn.cn/server/user/index 请求参数 {"lng": "xxxx","lat": "xxxx","city_id": "1376&…

docker搭建gitlab和jenkins

搭建gitlab 搭建gitlab首先需要一个gitlab的镜像 其次最好为他设置一个单独的目录 然后编写一个docker-compose文件 version: 3.1 services:gitlab:image: gitlab_zh:latest //此处为你的镜像名称container_name: gitlab //容器名称restart: always …

嵌入式linux中socket控制与实现

一、概述 1、首先网络,一看到这个词,我们就会想到IP地址和端口号,那IP地址和端口各有什么作用呢? (1)IP地址如身份证一样,是标识的电脑的,一台电脑只有一个IP地址。 (2)端口提供了一种访问通道,服务器一般都是通过知名端口号来识别某个服务。例如,对于每个TCP/IP实…

推荐系统重排:MMR 多样性算法

和谐共存&#xff1a;相关性与多样性在MMR中共舞 推荐系统【多样性算法】系列文章&#xff08;置顶&#xff09; 1.推荐系统重排&#xff1a;MMR 多样性算法 2.推荐系统重排&#xff1a;DPP 多样性算法 引言 在信息检索和推荐系统中&#xff0c;提供既与用户查询高度相关的文…