观察以下代码:

vector<int> X, Y, A, val;
inline int ls(int p) { return p << 1; }
inline int rs(int p) { return p << 1 | 1; }
int solve(int i, int l, int r) {
if (l == r) return val[i] = A[l];
int mid = (l + r) >> 1, p = X.size();
X.push_back(0), Y.push_back(0);
X[p] = solve(ls(i), l, mid);
Y[p] = solve(rs(i), mid + 1, r);
// do something
return val[i];
}

这是一份标准的线段树分治代码,其中数组 \(A\) 是给定的,\(val\) 在 \(solve\) 函数调用之前已经分配好了内存,而 \(X\) 和 \(Y\) 的内存空间则是动态分配的。

当我在本地测试完整的代码时,不会出现任何的异常。当我将代码提交到学校的 OJ 上时,却发现输出的结果不符合预期,而且对于同样的输入,输出却和本地有所出入。

经过艰难的排查,我最终发现问题出现在了 \(solve\) 函数中,即上述代码的第 \(8\) 至 \(9\) 行。我尝试将这两行替换为下面的代码:

int lp = solve(ls(i), l, mid);
X[p] = lp;
int rp = solve(rs(i), mid + 1, r);
Y[p] = rp;

这时 \(X[p]\) 与 \(Y[p]\) 的值就从错误的 \(0\) 变成了正确的答案。

我不禁陷入沉思,为何看似逻辑完全相同的代码,产生的效果却大相径庭?直到我发现第 \(7\) 行代码中的操作:

X.push_back(0), Y.push_back(0);

有没有可能,在第 \(8\) 行和第 \(9\) 行的赋值过程中,编译器先对等号左边的表达式进行计算,得到 \(X[p]\) 和 \(Y[p]\) 的左值引用,然后再计算了等号右边的表达式,调用了 \(solve\) 函数呢?

这样一切就解释得通了,\(X[p]\) 和 \(Y[p]\) 的引用先被取出,然后在递归调用 \(solve\) 函数的过程中,执行到了第 \(7\) 行的 \(push\_back\) 函数,使得 \(vector\) 重新分配了堆空间,导致 \(X[p]\) 和 \(Y[p]\) 的引用失效。于是,在赋值的过程中,我们对一个已经被释放掉的空间进行了修改,且不说有没有访问到不该访问的位置,当前 \(vector\) 中真实的 \(X[p]\) 和 \(Y[p]\) 也没能被赋为正确的值。

现在我们弄清楚发生 UB 的过程了。在这之后,我又进行了一些测试,目的在于弄清楚产生两种不同情况的本质原因。继续观察以下代码:

#include <bits/stdc++.h>
using namespace std;
int func1() {
cout << "func1" << endl;
return 1;
}
int func2() {
cout << "func2" << endl;
return 2;
}
int func3() {
cout << "func3" << endl;
return 3;
}
struct node {
int arr[100];
int& operator[](int i) {
func1();
return arr[i];
}
};
int main() {
node a;
(a[0] = func2()) = func3();
return 0;
}

当我使用 g++ 作为编译器,输出结果如下:

func1
func2
func3

当我使用 clang 作为编译器,输出结果如下:

func3
func2
func1

归根结底,产生这两种区别的原因还是在于编译器的实现。从上面的例子可以看出,g++ 在执行赋值语句的过程中,会从左往右进行运算,而 clang 则是从右往左。

在我的本机上,常用的编译器是 apple-clang,因此上文中线段树分治的代码从右往左执行赋值操作,不会产生引用失效的问题。而学校 OJ 的默认编译器为 g++,自然就出现与预期相违的情况了。

个人认为,对于这两种执行顺序,应当是从右往左更加符合正常人的逻辑,毕竟如 A = B = C 这样的连续赋值语句也是从右往左执行的。

总而言之,为了不触发此类未定义行为,在写代码时还需要多注意一下。对于本文开头的例子,最好还是在调用 \(solve\) 函数之前先对 \(X\) 和 \(Y\) 的内存空间进行 \(reserve\),这样就不会在 \(push\_back\) 时出现引用失效的问题了。

记一个难以发现的 UB的更多相关文章

  1. 记一个社交APP的开发过程——基础架构选型(转自一位大哥)

    记一个社交APP的开发过程——基础架构选型 目录[-] 基本产品形态 技术选型 最近两周在忙于开发一个社交App,因为之前做过一点儿社交方面的东西,就被拉去做API后端了,一个人头一次完整的去搭这么一 ...

  2. Entity Framework学习笔记——记一个错误解决方式及思路

    继续之前设定的学习目标前,先来一篇小小的外篇.按照第一篇里的配置方式配置好的工程前两天还能正常工作,昨天却突然无法通过Add-Migration命令进行数据库的升级.错误信息如下: System.Da ...

  3. hosts文件的一个小发现

    今天才发现原来同一个ip可以在hosts文件中配置多个域名.之间老是换一个网站就改一下,现在终于不用这么麻烦了 127.0.0.1 gg.pclady.com.cn 127.0.0.1 gg.pcon ...

  4. 记一个界面刷新相关的Bug

    今天遇到一个比较有意思的bug, 这里简单记录下. Bug的症状是通过拖拉边框把我们客户端主窗口拖小之后,再最大化,会发现窗口显示有问题, 看起来像是刷新问题, 有些地方显示的不对了. 这里要说明的是 ...

  5. 记一个同时支持模糊匹配和静态推导的Atom语法补全插件的开发过程: 序

    简介 过去的一周,都睡的很晚,终于做出了Atom上的APICloud语法提示与补全插件:apicloud_autocomplete.个中滋味,感觉还是有必要记录下来的.代码基于 GPL-3.0 开源, ...

  6. 记一个dynamic的坑

    创建一个控制台程序和一个类库, 在控制台创建一个匿名对象,然后再在类库中访问它,代码如下: namespace ConsoleApplication1 { class Program { static ...

  7. 记一个奇怪的python异常处理过程

    我的一个程序, 总是在退出时报异常, Exception TypeError: "'NoneType' object is not callable" in <functio ...

  8. 记一个python+sqlalchemy+tornado的一个高并发下,产生重复记录的bug

    场景:在用户通过支付通道支付完成返回时,发现我收到的处理数据记录中有两条同样的数据记录, 也就是同一笔钱,我数据库中记为了两条一样的记录. tornado端代码 from tornado import ...

  9. 彷徨中的成长-记一个文科生的IT成长过程

    纠结了许久,要不要写这篇文章,然而最终还是写了.就权当总结与呻吟吧..当然,呻吟最开始还是发在自己的站点的,忍不住手贱,还是想发博客园. 1 剧透 人算不如天算:时隔多年,我竟然搞起了前端. 2 发端 ...

  10. 记一个菜鸟在Linux上部署Tomcat的随笔

    以前都只是在园子里找各种资料.文档.各种抱大腿,今天是第一次进园子里来添砖加瓦,实话说,都不知道整些啥东西上来,就把自己在Linux上搭建Tomcat的过程记录下来,人笨,请各位大虾们勿喷. 虽然做开 ...

随机推荐

  1. CompareTest

    一.说明:Java中的对象,正常情况下,只能进行比较:== 或 != .不能使用 > 或 < 的 但是在开发场景中,我们需要对多个对象进行排序,言外之意,就需要比较对象的大小. 如何实现? ...

  2. Linux基础_3_文件/文件夹权限管理

    注:权限遮罩码: 控制用户创建文件和文件夹的默认安全设置,文件默认权限为666-umask的值,文件夹默认权限为777-umask的值. root默认0022,普通用户默认0002. 文件的默认权限不 ...

  3. 后端框架学习-----mybatis(使用mybatis框架遇到的问题)

    1.配置文件没有注册(解决:在核心配置文件中注册mapper,注册有三种形式.资源路径用斜杆,包和类用点) <mappers> <!--每一个mapper.xml文件都需要在myba ...

  4. Linux进程间通信(一)

    进程间通信 概念:进程是一个独立的资源分配单位,不同进程之间有关联,不能在一个进程中直接访问另一个进程的资源. 进程和进程之间的资源是相互独立的,一个进程不能直接访问另外一个进程的资源,但是进程和进程 ...

  5. 1.WEB应用模式

    1. Web应用模式 在开发Web应用中,有两种应用模式: 前后端不分离[客户端看到的内容和所有界面效果都是由服务端提供出来的.] 前后端分离[把前端的界面效果(html,css,js分离到另一个服务 ...

  6. springboot整合项目-商城个人头像上传功能

    上传头像的功能 持久层 1.sql语句的规划 avatar varchar(50) str - 字节流 将对象文件保存在操作系统上,然后在把这个文件的路径个记录下来,保存在avatar中,因为相比于字 ...

  7. Spring Boot 中使用 Swagger

    前后端分离开发,后端需要编写接⼝说明⽂档,会耗费⽐较多的时间. swagger 是⼀个⽤于⽣成服务器接⼝的规范性⽂档,并且能够对接⼝进⾏测试的⼯具. 作用 ⽣成接⼝说明⽂档 对接⼝进⾏测试 使用步骤 ...

  8. ui自动化测试数据复原遇到的坑——1、hibernate输出完整sql

    公司老项目使用SSH+informix+weblogic+IE开发,我们要做ui自动化测试,其中的测试数据复原,我打算通过hibernate输出sql,然后把插入.更新的sql改为delete或upd ...

  9. cmd唤醒windows设置,并配置opsshd

    1. 从cmd唤起windows设置 这个东西很有意思,大部分在运行窗口输入的内容,从cmd或powershell都能唤起,如:control控制面板,但偶尔有些操作就不能通用, 如: ms-sett ...

  10. python调用程序路径中包空格,及包含特殊字符问题

    解决办法 import os s = r'"C:\Program Files\Google\Chrome\Application\chrome.exe"' print(s) os. ...