有两个具有不同值的指针引用同一个对象是未定义的行为吗?
Is it undefined behavior to have two pointers with different values referring to the same object?
注意:如果看完这道题你觉得,"how can that even happen",那就可以了。如果您想保持开放的心态,您可以遵循问题后的一些要点,这些要点说明了这是如何发生的以及为什么这样做有用。请记住,这只是一个问题,而不是关于这些主题的教程。这些评论已经有足够的噪音,而且很难跟上。如果您对这些主题有疑问,如果您 post 在 SO 中而不是在评论中将它们作为问题,我将不胜感激。
问题:如果我有一个int
类型的对象存储在c
指向的地址
int* c = /* allocate int (returns unique address) */;
*c = 3;
由两个指针引用 a
和 b
:
int* a = /* create pointer to (*c) */;
int* b = /* create pointer to (*c) */;
这样:
assert(a != b); // the pointers point to a different address
assert(*b == 3);
*a = 2;
assert(*b == 2); // but they refer to the same value
这是未定义的行为吗?如果是,C++ 标准的哪一部分不允许这样做?如果不允许,C++ 标准的哪些部分允许这样做?
注意:c
指向的内存是用一个内存分配函数分配的,returns一个唯一的地址(new
,malloc
, ...)。创建具有不同值的这些指针的方法是非常特定于平台的,尽管在大多数 unix 系统中它可以用 mmap
完成,而在 windows 上它可以用 VirtualAlloc
.[=33 完成=]
背景:大多数操作系统(那些用户空间不在 ring 0 上的操作系统)运行 它们在虚拟内存上的进程,并且有来自虚拟内存的映射内存页到物理内存页。其中一些系统(Linux/MacOS/BSDs/Unixes 和 64 位 windows)提供了一些系统调用(如 mmap
或 VirtualAlloc
)可用于将两个虚拟内存页映射到同一个物理内存页内存页。当进程执行此操作时,它实际上可以从两个不同的虚拟内存地址访问同一页物理内存。也就是说,这两个指针将具有不同的值,但它们将访问相同的物理内存存储。 google 的关键字用于:mmap
、虚拟内存、内存页。使用此功能获利的数据结构是 "magic ring buffer"s(这是技术术语)和非重新分配动态大小的向量(即,在增长时不需要重新分配内存的向量)。 Google 提供了比我在这里所能容纳的更多的信息。
非常小的可能无法运行的示例(仅限 unix):
我们先在堆上分配一个int。以下请求一个匿名的、非文件支持的虚拟内存映射。这里必须至少请求一个完整的内存页,但为了简单起见,我只请求 int
的大小(mmap
无论如何都会分配一个完整的内存页):
int* c= mmap(NULL, sizeof(int), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE,-1, 0);
现在我们需要将它映射到两个独立的内存位置,所以我们将它映射到同一个内存映射文件,两次,例如,两个相邻的内存位置。我们不会真正使用这个文件,但我们仍然需要创建它并打开它:
mmap(c, sizeof(int), PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, some_fd, 0);
mmap(c + 1, sizeof(int), PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, some_fd, 0);
现在我们快完成了:
int* a = c;
int* b = c + 1;
这些明显不同虚拟地址:
assert(a != b);
但它们指向相同的、非文件支持的、物理内存页:
*a = 314;
assert(*b == 314);
好了。使用 VirtualAlloc
可以在 Windows 上完成相同的操作,但 API 有点不同。
完全允许有两个不同的指针指向同一个对象,条件是它们的类型相同
原始对象。没有什么可以阻止这种情况,这当然不是未定义的行为。
什么是未定义的行为是当你不遵守 strict aliasing rule 时,即你有两个不同类型的指针引用同一个对象。这在标准第 3.10/10 节中有说明。但这不是你的例子的情况。
现在是您问题的难点部分:是否可以有两个指向同一个对象的不同值的指针?
- 指针管理是实现定义的。在一些较旧的 CPU 架构上,编译器使用内存模型,利用 segment registers 和偏移寄存器。然后将两者结合起来找出它们引用的内存中的哪个唯一地址。根据指针的存储方式,e.g.if 它们被存储为段和偏移量的并置,您确实可以有两个指向同一对象的不同值的指针。
- 然而,根据相等运算符的定义(标准,第 5.10/3 节),指向同一对象的两个指针是相等的。无论编译器如何实现指针,这都是正确的(即,即使按位值不同,如果它们引用同一对象,比较应该 return 为真)
是的,可以使用多重继承,如下所示:
#include <iostream>
using namespace std;
class A { int a; };
class B { int b; };
class C : public A, public B { };
void f(A &a) { cout << &a << endl; }
void g(B &b) { cout << &b << endl; }
int main() {
C c;
f(c);
g(c);
}
产生类似的东西:
0x7fff5aba2878
0x7fff5aba287c
现在你可以封装机制来在子进程中获得相同的共享值class C:
class A {
int a;
public:
virtual int getValue() { return a; }
virtual void setValue(int v) { a = v; }
};
class B {
int b;
public:
virtual int getValue() { return b; }
virtual void setValue(int v) { b = v; }
};
class C : public A, public B {
int c;
public:
virtual int getValue() { return c; }
virtual void setValue(int v) { c = v; }
};
void f(A &a) {
cout << &a << endl;
cout << a.getValue() << endl;
a.setValue(5);
cout << a.getValue() << endl;
}
void g(B &b) {
cout << &b << endl;
cout << b.getValue() << endl;
}
int main() {
C c;
c.setValue(3);
f(c);
g(c);
}
在那种情况下你可以观察到:
0x7fff51063860
3
5
0x7fff51063870
5
看起来有两个对象(实际上一个对象有 两个地址)但共享相同的值。
请注意,在 ISO 常见问题解答 What special considerations do I need to know about when I use virtual inheritance?
上有一些关于您应该如何仔细考虑对象地址的信息
C++ 标准未定义 mmap
或任何其他映射内存的方法。 C++ 标准只关注一种查看内存的方法。如果系统使用虚拟内存,那么标准只关注虚拟内存。据我所知,没有指定虚拟内存和物理内存之间的关系。
标准对内存的描述:
The memory available to a C++ program consists of one or more sequences of contiguous bytes. Every byte has a unique address.
标准对对象的描述:
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.
所以,当你问:
Is it undefined behavior to have two pointers with different values referring to the same object?
这两个前提是矛盾的。永远不能有两个具有不同值的指针指向同一个对象。从标准的角度来看,您拥有的是两个不同的对象。即使两个虚拟地址都映射到相同的物理内存。
如果我们假设在下面的代码中,指针 a
和 b
被神奇地映射到相同的物理内存:
int *a, *b; // initialize with magic mapping of your choice
*a = 1;
if(a != b) {
*b = 2;
std::cout << *a; // what is the value of *a?
}
就标准而言,*a
和*b
是不同的对象。他们一定是,因为他们有不同的地址。编译器可以自由地优化 *a
的读取并使用常量 1,因为在 *a = 1
和读取 *a
之间的任何点都没有修改 *b
以外的任何内容,这是一个不相关的对象。
因此,如果编译器选择优化并使用常量,则输出将为 1
。但是,如果实际读取内存,并且虚拟地址实际映射到刚刚写入 2
的物理内存,则输出可能会有所不同。我不知道它是否是明确未定义的行为,但至少它肯定是未指定的。
内存映射由实现指定,因此,实现指定内存映射对象的行为方式。
首先让我们看看标准对对象的看法
[intro.object]
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is a region of storage. [ Note: A function is not an object, regardless of whether or not it occupies storage in the way that objects do. —end note ] An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed. The properties of an object are determined when the object is created. An object can have a name (Clause 3). An object has a storage duration (3.7) which influences its lifetime (3.8). An object has a type (3.9). The term object type refers to the type with which the object is created. Some objects are polymorphic (10.3); the implementation generates information associated with each such object that makes it possible to determine that object’s type during program execution. For other
objects, the interpretation of the values found therein is determined by the type of the expressions (Clause 5) used to access them.
然后我们有
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.
所以我们知道一个对象有一个地址,它是它使用的存储空间的第一个字节。如果我们看看我们有什么字节
[intro.memory]
The fundamental storage unit in the C++ memory model is the byte. A byte is at least large enough to contain any member of the basic execution character set (2.3) and the eight-bit code units of the Unicode UTF-8 encoding form and is composed of a contiguous sequence of bits, the number of which is implementationdefined. The least significant bit is called the low-order bit; the most significant bit is called the high-order bit. The memory available to a C++ program consists of one or more sequences of contiguous bytes. Every byte has a unique address.
强调我的
因此,如果我们有一个指向对象的指针,该指针将保存一个唯一值(地址)。如果我们有另一个指向同一个对象的指针,那么它也必须具有相同的值(地址)。未定义的行为甚至不会出现在等式中,因为您根本无法让两个指针指向具有不同值的同一对象。
你假设的实现的问题实际上是assert(a != b);
一个简单的思想实验说明了原因。在经典的8086上,0000:0010
和0001:0000
这两个指针等于,因为这两个指针指向同一个对象。实现必须确保不按位比较这两个指针。
一般来说,如果您的实现允许两个唯一的 位模式 引用同一个对象,那么这些位模式(解释为指针)必须比较相等。
不过,您会发现很少有 C++ 实现包含 mmap
函数。这通常是一个 OS 函数,并且 OS 不受 C++ 规则的约束。无论如何调用 OS 函数往往是 UB。
您不必求助于 MMU 技巧来弄清楚如何构造指向同一内存区域的两个地址。有分段内存架构、ARM Cortex 位带等。C 编译器不允许自己构造具有两个不同地址的对象(根据语言定义 "address" 和 "address" 等术语的方式,这实际上是无意义的"object") 但标准的编写方式是预期写入某些对象会导致事情发生作为副作用,并且除了您的程序写入对象之外的任意其他事情可能会改变它读取之间的值。不过,您应该了解这些并将它们标记为 volatile
。
所以你的情况与未定义行为无关; UB 是程序员和编译器之间契约的一部分,它说明编译器如何受到(或不受)程序员使用该语言的约束。如果你在语言之外,如果你不知道自己在做什么,你可以任意搞砸事情,但这与编译器对你的义务无关标准。在这种情况下,您只是创建了两个 volatile
对象,它们恰好通过副作用链接在一起以包含相同的数据。这在设备驱动程序和内存映射寄存器的世界里一点也不奇怪!
拥有指向标识相同存储区域的两个或更多地址中的每一个的指针,并使用其中任何一个修改对象,将产生等同于让实现修改对象背后的存储的情况编译器出于任何其他原因返回。人们不应该期待任何特定的行为,除非两个指针都是 volatile
限定类型,但如果两个指针都是限定类型,那么大多数实现中的行为应该是预期的。该标准没有具体说明 volatile
的效果,但也没有定义您描述的情况可能存在的任何方式。您所描述的情况可能存在的实现通常会记录一种方法,以确保易失性读取或写入将物理访问指针标识的地址 space 区域,并且所有此类访问都将按照给定的顺序执行,但标准并不要求 volatile
限定符就足够了。
请注意,即使 volatile
访问相对于彼此排序,实现也可能不会相对于非 volatile
访问对它们进行排序。如果有一种方法可以告诉编译器以任何顺序写入一堆信息,然后在所有其他写入完成后只写入 volatile
位置,那将会很有帮助,但是没有这样做的标准方法除了可能使所有数据 volatile
(这将限制优化机会,并且可能足够也可能不够)。
注意:如果看完这道题你觉得,"how can that even happen",那就可以了。如果您想保持开放的心态,您可以遵循问题后的一些要点,这些要点说明了这是如何发生的以及为什么这样做有用。请记住,这只是一个问题,而不是关于这些主题的教程。这些评论已经有足够的噪音,而且很难跟上。如果您对这些主题有疑问,如果您 post 在 SO 中而不是在评论中将它们作为问题,我将不胜感激。
问题:如果我有一个int
类型的对象存储在c
int* c = /* allocate int (returns unique address) */;
*c = 3;
由两个指针引用 a
和 b
:
int* a = /* create pointer to (*c) */;
int* b = /* create pointer to (*c) */;
这样:
assert(a != b); // the pointers point to a different address
assert(*b == 3);
*a = 2;
assert(*b == 2); // but they refer to the same value
这是未定义的行为吗?如果是,C++ 标准的哪一部分不允许这样做?如果不允许,C++ 标准的哪些部分允许这样做?
注意:c
指向的内存是用一个内存分配函数分配的,returns一个唯一的地址(new
,malloc
, ...)。创建具有不同值的这些指针的方法是非常特定于平台的,尽管在大多数 unix 系统中它可以用 mmap
完成,而在 windows 上它可以用 VirtualAlloc
.[=33 完成=]
背景:大多数操作系统(那些用户空间不在 ring 0 上的操作系统)运行 它们在虚拟内存上的进程,并且有来自虚拟内存的映射内存页到物理内存页。其中一些系统(Linux/MacOS/BSDs/Unixes 和 64 位 windows)提供了一些系统调用(如 mmap
或 VirtualAlloc
)可用于将两个虚拟内存页映射到同一个物理内存页内存页。当进程执行此操作时,它实际上可以从两个不同的虚拟内存地址访问同一页物理内存。也就是说,这两个指针将具有不同的值,但它们将访问相同的物理内存存储。 google 的关键字用于:mmap
、虚拟内存、内存页。使用此功能获利的数据结构是 "magic ring buffer"s(这是技术术语)和非重新分配动态大小的向量(即,在增长时不需要重新分配内存的向量)。 Google 提供了比我在这里所能容纳的更多的信息。
非常小的可能无法运行的示例(仅限 unix):
我们先在堆上分配一个int。以下请求一个匿名的、非文件支持的虚拟内存映射。这里必须至少请求一个完整的内存页,但为了简单起见,我只请求 int
的大小(mmap
无论如何都会分配一个完整的内存页):
int* c= mmap(NULL, sizeof(int), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE,-1, 0);
现在我们需要将它映射到两个独立的内存位置,所以我们将它映射到同一个内存映射文件,两次,例如,两个相邻的内存位置。我们不会真正使用这个文件,但我们仍然需要创建它并打开它:
mmap(c, sizeof(int), PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, some_fd, 0);
mmap(c + 1, sizeof(int), PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, some_fd, 0);
现在我们快完成了:
int* a = c;
int* b = c + 1;
这些明显不同虚拟地址:
assert(a != b);
但它们指向相同的、非文件支持的、物理内存页:
*a = 314;
assert(*b == 314);
好了。使用 VirtualAlloc
可以在 Windows 上完成相同的操作,但 API 有点不同。
完全允许有两个不同的指针指向同一个对象,条件是它们的类型相同 原始对象。没有什么可以阻止这种情况,这当然不是未定义的行为。
什么是未定义的行为是当你不遵守 strict aliasing rule 时,即你有两个不同类型的指针引用同一个对象。这在标准第 3.10/10 节中有说明。但这不是你的例子的情况。
现在是您问题的难点部分:是否可以有两个指向同一个对象的不同值的指针?
- 指针管理是实现定义的。在一些较旧的 CPU 架构上,编译器使用内存模型,利用 segment registers 和偏移寄存器。然后将两者结合起来找出它们引用的内存中的哪个唯一地址。根据指针的存储方式,e.g.if 它们被存储为段和偏移量的并置,您确实可以有两个指向同一对象的不同值的指针。
- 然而,根据相等运算符的定义(标准,第 5.10/3 节),指向同一对象的两个指针是相等的。无论编译器如何实现指针,这都是正确的(即,即使按位值不同,如果它们引用同一对象,比较应该 return 为真)
是的,可以使用多重继承,如下所示:
#include <iostream>
using namespace std;
class A { int a; };
class B { int b; };
class C : public A, public B { };
void f(A &a) { cout << &a << endl; }
void g(B &b) { cout << &b << endl; }
int main() {
C c;
f(c);
g(c);
}
产生类似的东西:
0x7fff5aba2878
0x7fff5aba287c
现在你可以封装机制来在子进程中获得相同的共享值class C:
class A {
int a;
public:
virtual int getValue() { return a; }
virtual void setValue(int v) { a = v; }
};
class B {
int b;
public:
virtual int getValue() { return b; }
virtual void setValue(int v) { b = v; }
};
class C : public A, public B {
int c;
public:
virtual int getValue() { return c; }
virtual void setValue(int v) { c = v; }
};
void f(A &a) {
cout << &a << endl;
cout << a.getValue() << endl;
a.setValue(5);
cout << a.getValue() << endl;
}
void g(B &b) {
cout << &b << endl;
cout << b.getValue() << endl;
}
int main() {
C c;
c.setValue(3);
f(c);
g(c);
}
在那种情况下你可以观察到:
0x7fff51063860
3
5
0x7fff51063870
5
看起来有两个对象(实际上一个对象有 两个地址)但共享相同的值。
请注意,在 ISO 常见问题解答 What special considerations do I need to know about when I use virtual inheritance?
上有一些关于您应该如何仔细考虑对象地址的信息C++ 标准未定义 mmap
或任何其他映射内存的方法。 C++ 标准只关注一种查看内存的方法。如果系统使用虚拟内存,那么标准只关注虚拟内存。据我所知,没有指定虚拟内存和物理内存之间的关系。
标准对内存的描述:
The memory available to a C++ program consists of one or more sequences of contiguous bytes. Every byte has a unique address.
标准对对象的描述:
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.
所以,当你问:
Is it undefined behavior to have two pointers with different values referring to the same object?
这两个前提是矛盾的。永远不能有两个具有不同值的指针指向同一个对象。从标准的角度来看,您拥有的是两个不同的对象。即使两个虚拟地址都映射到相同的物理内存。
如果我们假设在下面的代码中,指针 a
和 b
被神奇地映射到相同的物理内存:
int *a, *b; // initialize with magic mapping of your choice
*a = 1;
if(a != b) {
*b = 2;
std::cout << *a; // what is the value of *a?
}
就标准而言,*a
和*b
是不同的对象。他们一定是,因为他们有不同的地址。编译器可以自由地优化 *a
的读取并使用常量 1,因为在 *a = 1
和读取 *a
之间的任何点都没有修改 *b
以外的任何内容,这是一个不相关的对象。
因此,如果编译器选择优化并使用常量,则输出将为 1
。但是,如果实际读取内存,并且虚拟地址实际映射到刚刚写入 2
的物理内存,则输出可能会有所不同。我不知道它是否是明确未定义的行为,但至少它肯定是未指定的。
内存映射由实现指定,因此,实现指定内存映射对象的行为方式。
首先让我们看看标准对对象的看法
[intro.object]
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is a region of storage. [ Note: A function is not an object, regardless of whether or not it occupies storage in the way that objects do. —end note ] An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed. The properties of an object are determined when the object is created. An object can have a name (Clause 3). An object has a storage duration (3.7) which influences its lifetime (3.8). An object has a type (3.9). The term object type refers to the type with which the object is created. Some objects are polymorphic (10.3); the implementation generates information associated with each such object that makes it possible to determine that object’s type during program execution. For other objects, the interpretation of the values found therein is determined by the type of the expressions (Clause 5) used to access them.
然后我们有
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects that are not bit-fields may have the same address if one is a subobject of the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they shall have distinct addresses.
所以我们知道一个对象有一个地址,它是它使用的存储空间的第一个字节。如果我们看看我们有什么字节
[intro.memory]
The fundamental storage unit in the C++ memory model is the byte. A byte is at least large enough to contain any member of the basic execution character set (2.3) and the eight-bit code units of the Unicode UTF-8 encoding form and is composed of a contiguous sequence of bits, the number of which is implementationdefined. The least significant bit is called the low-order bit; the most significant bit is called the high-order bit. The memory available to a C++ program consists of one or more sequences of contiguous bytes. Every byte has a unique address.
强调我的
因此,如果我们有一个指向对象的指针,该指针将保存一个唯一值(地址)。如果我们有另一个指向同一个对象的指针,那么它也必须具有相同的值(地址)。未定义的行为甚至不会出现在等式中,因为您根本无法让两个指针指向具有不同值的同一对象。
你假设的实现的问题实际上是assert(a != b);
一个简单的思想实验说明了原因。在经典的8086上,0000:0010
和0001:0000
这两个指针等于,因为这两个指针指向同一个对象。实现必须确保不按位比较这两个指针。
一般来说,如果您的实现允许两个唯一的 位模式 引用同一个对象,那么这些位模式(解释为指针)必须比较相等。
不过,您会发现很少有 C++ 实现包含 mmap
函数。这通常是一个 OS 函数,并且 OS 不受 C++ 规则的约束。无论如何调用 OS 函数往往是 UB。
您不必求助于 MMU 技巧来弄清楚如何构造指向同一内存区域的两个地址。有分段内存架构、ARM Cortex 位带等。C 编译器不允许自己构造具有两个不同地址的对象(根据语言定义 "address" 和 "address" 等术语的方式,这实际上是无意义的"object") 但标准的编写方式是预期写入某些对象会导致事情发生作为副作用,并且除了您的程序写入对象之外的任意其他事情可能会改变它读取之间的值。不过,您应该了解这些并将它们标记为 volatile
。
所以你的情况与未定义行为无关; UB 是程序员和编译器之间契约的一部分,它说明编译器如何受到(或不受)程序员使用该语言的约束。如果你在语言之外,如果你不知道自己在做什么,你可以任意搞砸事情,但这与编译器对你的义务无关标准。在这种情况下,您只是创建了两个 volatile
对象,它们恰好通过副作用链接在一起以包含相同的数据。这在设备驱动程序和内存映射寄存器的世界里一点也不奇怪!
拥有指向标识相同存储区域的两个或更多地址中的每一个的指针,并使用其中任何一个修改对象,将产生等同于让实现修改对象背后的存储的情况编译器出于任何其他原因返回。人们不应该期待任何特定的行为,除非两个指针都是 volatile
限定类型,但如果两个指针都是限定类型,那么大多数实现中的行为应该是预期的。该标准没有具体说明 volatile
的效果,但也没有定义您描述的情况可能存在的任何方式。您所描述的情况可能存在的实现通常会记录一种方法,以确保易失性读取或写入将物理访问指针标识的地址 space 区域,并且所有此类访问都将按照给定的顺序执行,但标准并不要求 volatile
限定符就足够了。
请注意,即使 volatile
访问相对于彼此排序,实现也可能不会相对于非 volatile
访问对它们进行排序。如果有一种方法可以告诉编译器以任何顺序写入一堆信息,然后在所有其他写入完成后只写入 volatile
位置,那将会很有帮助,但是没有这样做的标准方法除了可能使所有数据 volatile
(这将限制优化机会,并且可能足够也可能不够)。