当我们引用非静态数据成员时,分段错误是否实际上是未定义的行为?

11

我已经阅读了下面的规则,并一直在尝试编写一个反映该规则的示例。 该规则来自于3.8/5 N3797:

在对象的存储空间被分配但对象的生命周期开始之前,或者在对象的生命周期结束并且对象占用的存储空间被重新使用或释放之前,可以使用指向对象将要或曾经占用的存储位置的任何指针,但是仅限于有限的方式。对于正在构建或销毁的对象,请参见12.7。否则,这样的指针引用已分配的存储(3.7.4.2),并且将指针用作类型为“void *”是可以定义的。通过这样的指针间接寻址是允许的,但得到的左值只能以限定的方式使用,如下所述。如果程序出现以下情况,则具有未定义行为:

[...]

— 使用指针访问对象的非静态数据成员或调用非静态成员函数,或

[...]

我所编写的示例:

#include <iostream>
#include <typeinfo>

using std::cout;
using std::endl;

struct A
{
    int b = 5;
    static const int a = 5;
};

int main()
{
    A *p = (A*)0xa31a3442;
    cout << p -> a; //1, Well-fromed, there is no compile-time error
    cout << p -> b; //2, Segmentation fault is producing
}

//1是良好形式并且不会导致任何UB,但//2却导致了分段错误,这是否属于UB


3
我不确定您所说的“实际”未定义行为是什么意思。标准非常明确:(1) 不是未定义行为,(2) 是未定义行为。 - David Titarenco
@DavidTitarenco 标准提供了一个示例,展示了在内存重用后出现未定义行为。但并非我所提供的那个示例。 - user2953119
2
实际上,标准规定了“将会”或“已经”。请注意分离和“将会”。由于我们尚未完善时间旅行技术,您无法知道对象的内存位置将在哪里,因此它与选择任意内存位置在功能上是等效的。 - David Titarenco
3个回答

25

未定义行为指的是在符合标准的实现中,可以发生任何事情。真的是任何事情。(而您的第2点是UB)

实现可能会:

  • 炸毁您的计算机并对您造成身体伤害
  • 产生一个吞噬整个太阳系的黑洞
  • 没有发生严重的事情
  • 点亮键盘上的某些LED灯
  • 进行时间旅行并在您的父母出生之前杀死您的祖父母
  • 等等......

并且这也是符合标准的 (在 UB 的情况下); 同时了解更为熟悉的鼻子恶魔的概念。

因此,在未定义的行为中发生的事情是不可预测和不可重复的(一般而言)。

更严肃地说,想一想 UB 在连接到车辆 ABS 制动系统的计算机中、在一些人工心脏中或在驱动某个核电站的计算机中可能会产生什么后果。

特别地,它有时可能会正常工作。由于大多数操作系统具有地址空间布局随机化(ASLR),因此您的代码有很小的机会正常工作(例如,如果0xa31a3442恰好指向某个有效位置,例如堆栈上的位置,但您不能在下一次运行中重现这种情况!)

UB是给实现者(例如编译器或操作系统的实现者)和计算机自由发挥的一种方式,使它们可以做任何它们“想要”做的事情,换句话说,不用关心后果。这使得一些巧妙的优化或良好的实现技巧成为可能。但是,应该关心(如果你正在编写嵌入式飞行控制系统、仅仅是编写一个在Linux上运行的某个C++课程示例,还是只是用树莓派点亮LED灯这样的“hacky demo”,因为在这些情况下后果是不同的)。

需要注意的是,语言标准甚至不要求在实现中使用计算机(或硬件):你可以用一个人类奴隶团队“运行”你的C++代码,但这是高度不道德的(而且昂贵,也不可靠)。

更多参考请见这里。您至少应该阅读Lattner的博客: Undefined Behavior (他写的大部分内容适用于C++和许多其他具有UB的语言)。


(于2015年12月和2016年6月添加)

NB. Valgrind 工具和各种针对最近的 GCCClang/LLVM 调试选项(例如 -fsanitize=)非常有用。此外,启用编译器中的所有警告和调试信息(例如 g++ -Wall -Wextra -g),并使用适当的插桩选项,例如 -fsanitize=undefined。请注意,在编译时无法静态和全面地检测到所有 UB 的情况(这相当于停机问题)。

PS. 上述答案不仅适用于 C++,也适用于 C!


1
但是,如果使用时间旅行,他们在你的父母诞生之前就被杀死了,这就是一个经典的悖论。这就是我所指的问题。 - Basile Starynkevitch
@Destructor:那我就会对女性在StackOverflow上发帖有偏见。我尽量避免这种偏见。 - Basile Starynkevitch

1

规则3.8/5是关于对象构造/析构之外但在分配/释放内存的时间,此时对象所在内存中的点。以下演示了对象生命周期之外的点:

void *buffer = malloc(sizeof(A));
// outside of lifetime of a
// a->b is undefined
A* a = new (buffer) A();
// within lifetime of a
// a->b is valid
a->~A();
// outside of lifetime of a
// a->b is undefined
free(buffer);

从技术上讲,您的帖子实际上并没有反映规则3.8 / 5,因为您没有在对象超出其生命周期的情况下访问该对象。您只是将随机内存强制转换为实例。

1
您的问题是:
“如果//1格式良好且不会导致任何未定义行为,这是真的吗?”
您引用的标准部分没有提到这一点。
您还问道:
“但//2产生了分段错误,这是UB?”
您引用的标准部分与这种特定行为无关。您看到UB是因为指针p指向不包含有效对象的内存。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接