具有引用同一对象的不同值的两个指针是不确定的行为吗?

时间:2016-03-07 19:39:52

标签: c++ operating-system undefined-behavior memory-address

注意:如果在阅读完这个问题之后,你会想,"怎么可能发生这种情况",这没关系。如果你想保持开放的心态,那么在你可以遵循的问题后面有一些要点,这些要点表明了这种情况如何发生以及为什么这有用。请记住,这只是一个问题,而不是任何这些主题的教程。评论已经有足够的噪音,很难遵循。如果您对这些主题有疑问,如果您将其作为问题发布在SO而不是评论中,我将不胜感激。

问题:如果我在int指向的地址存储了c类型的对象

int* c = /* allocate int (returns unique address) */;
*c = 3;

由两个指针ab引用:

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指向内存分配函数,该函数返回唯一地址(newmalloc,...) 。使用不同值创建这些指针的方法非常特定于平台,但在大多数unix系统中,可以使用mmap完成,而在Windows上可以使用VirtualAlloc完成。

背景:大多数操作系统(具有不在0环上的用户空间的操作系统)在虚拟内存上运行其进程,并具有从虚拟内存页面到物理内存页面的映射。其中一些系统(Linux / MacOS / BSD / Unix和64位窗口)提供了一些系统调用(如mmapVirtualAlloc),可用于将两个虚拟内存页映射到同一物理内存页。当进程执行此操作时,它实际上可以从两个不同的虚拟内存地址访问同一页物理内存。也就是说,这两个指针将具有不同的值,但它们将访问相同的物理内存存储。 google for的关键字:mmap,虚拟内存,内存页面。使用此功能获取利润的数据结构是魔术环缓冲区(即技术术语),以及非重新分配动态大小的向量(即不需要的向量)当他们成长时重新分配记忆)。谷歌提供的信息比我在这里所能适用的更多。

非常小的可能非工作示例(仅限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有点不同。

7 个答案:

答案 0 :(得分:5)

首先让我们看一下标准对象的含义

  

<强> [intro.object]

     

C ++程序中的构造创建,销毁,引用,访问和操作对象。对象是存储区域。 [注意:函数不是对象,无论它是否以对象的方式占用存储。 -end note]一个对象由定义(3.1),new-expression(5.3.4)或实现(12.2)在需要时创建。创建对象时确定对象的属性。对象可以有一个名称(第3条)。对象的存储持续时间(3.7)会影响其生命周期(3.8)。对象具有类型(3.9)。术语对象类型是指用于创建对象的类型。有些对象是多态的(10.3);该实现生成与每个这样的对象相关联的信息,使得可以在程序执行期间确定该对象的类型。对于其他   对象,其中发现的值的解释取决于用于访问它们的表达式(第5条)的类型。

然后我们

  

除非对象是零字段或零大小的基类子对象,否则该对象的地址是它占用的第一个字节的地址。如果一个是另一个的子对象,或者如果至少一个是零大小的基类子对象并且它们是不同类型的,则不是位字段的两个对象可以具有相同的地址;否则,他们应有不同的地址。

所以我们知道一个对象有一个地址,它是它使用的存储的第一个字节。如果我们看一下我们有什么字节

  

<强> [intro.memory] ​​

     

C ++内存模型中的基本存储单元是字节。一个字节至少足以包含基本执行字符集(2.3)的任何成员和Unicode UTF-8编码形式的八位代码单元,并由连续的位序列组成,其数量为implementationdefined。最低有效位称为低位;最重要的位称为高位。 C ++程序可用的内存由一个或多个连续字节序列组成。 每个字节都有一个唯一的地址。

强调我的

因此,如果我们有一个指向对象的指针,则指针将保存唯一值(地址)。如果我们有另一个指向同一个对象的指针,那么它也必须具有相同的值(地址)。未定义的行为甚至不会输入等式,因为您根本不能有两个指向具有不同值的同一对象的指针。

答案 1 :(得分:3)

C ++标准没有定义a或任何其他映射内存的方法。 C ++标准只关注查看内存的一种方法。如果系统使用虚拟内存,则标准仅关注虚拟内存。据我所知,虚拟和物理内存之间没有关系。

关于记忆的标准是什么:

  

C ++程序可用的内存由一个或多个连续字节序列组成。每个字节都有一个唯一的地址。

关于对象的标准是什么:

  

除非对象是零字段或零大小的基类子对象,否则该对象的地址是它占用的第一个字节的地址。如果一个是另一个的子对象,或者如果至少一个是零大小的基类子对象并且它们是不同类型的,则不是位字段的两个对象可以具有相同的地址;否则,应具有不同的地址

所以,当你问:

  

指向同一对象的不同值的两个指针是不确定的行为

这两个前提是矛盾的。你永远不会有两个指向同一个对象的不同值的指针。从标准的角度来看,你所拥有的是两个不同的对象。即使两个虚拟地址都映射到相同的物理内存。

如果我们在下面的代码中假设指针bint *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是不同的对象。他们必须是,因为他们有不同的地址。编译器可以自由地优化*a = 1的读数并使用常数1,因为*a和读*b之间的任何点都不是1修改的,这是一个无关的对象。

因此,如果编译器选择优化并使用常量,则输出将为2。但是,如果实际读取内存,并且虚拟地址实际映射到刚刚写入{{1}}的物理内存,则输出可能不同。我不知道它是否有明确的未定义行为,但至少它没有明确规定。

内存映射由实现指定,因此,实现指定了内存映射对象的行为方式。

答案 2 :(得分:1)

完全允许有两个不同的指针指向同一个对象,条件是它们的类型相同 原始对象。什么都没有阻止这一点,它肯定不是未定义的行为。

什么是未定义的行为是当你不尊重strict aliasing rule时,即你有两个不同类型的指针引用同一个对象。这在标准3.10 / 10节中说明。但事实并非如此。

现在你问题的难点部分:你能有两个指向同一个对象的不同值的指针吗?

  • 指针管理是实现定义的。在一些较旧的CPU架构中,编译器使用了使用segment registers和偏移寄存器的内存模型。然后将它们组合起来以找出它们所引用的存储器中的唯一地址。根据指针的存储方式,例如,如果将它们存储为段和偏移的并置,您确实可以有两个指向同一对象的不同值的指针。
  • 但是,通过定义等于运算符(标准,第5.10 / 3节),指向同一对象的两个指针是相等的。无论编译器如何实现指针,这都应该成立(即使按位值不同,如果它们引用相同的对象,则比较应返回true)

答案 3 :(得分:1)

是的,可以使用多重继承,如下所示:

#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

现在,您可以封装机制以在子类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 faq What special considerations do I need to know about when I use virtual inheritance?上的对象地址的信息

答案 4 :(得分:1)

您的假设实施问题实际上是assert(a != b);

一个简单的思想实验说明了原因。在经典8086上,两个指针0000:00100001:0000相等,因为两个指针都指向同一个对象。实现必须确保这两个指针不按位进行比较。

通常,如果您的实现允许两个唯一的 bitpatterns 引用同一个对象,那么这些位模式(解释为指针)必须比较相等。

但是,您会发现很少有包含mmap函数的C ++实现。这通常是OS功能,OS不受C ++规则的约束。无论如何,调用操作系统功能往往是UB。

答案 5 :(得分:1)

您不必诉诸MMU技巧来弄清楚如何构建指向同一内存区域的两个地址。存在分段存储器架构,ARM Cortex位带等.C编译器本身不允许构造具有两个不同地址的对象(根据语言定义诸如“地址”和“对象的术语的方式,这实际上是无意义的“)但标准是以这样一种方式编写的,即预期写入某些对象会导致事件作为副作用发生,并且除了程序写入对象之外的任意其他事物可能会在读取之间改变其值。但是,您应该知道这些并将它们标记为volatile

所以你的情况与未定义的行为无关; UB是程序员和编译器之间合同的一部分,它描述了程序员如何使用该语言来约束(或不编译)编译器。如果你外面语言,如果你不知道你在做什么,你可以任意搞砸,但这与编译器对你的标准义务无关。在这种情况下,您只需创建两个恰好通过副作用链接的volatile个对象来包含相同的数据。在设备驱动程序和内存映射寄存器的世界里,这根本不是一种奇怪的情况!

答案 6 :(得分:1)

指向识别相同存储区域的两个或多个地址中的每一个的指针,并使用其中任何一个修改对象,将创建一个等同于实现修改存储器后面的对象的存储的情况&# 39;因任何其他原因回来了。除非两个指针都是volatile - 限定类型,否则不应指望任何特定行为,但如果两个指针都是合格的行为,那么在大多数实现中应该如预期的那样。标准没有明确说明volatile的效果应该是什么,但也没有定义您描述的情况可能存在的任何方法。您描述的情况可能存在的实现通常会记录一种方法,即确保易失性读取或写入将物理访问由指针标识的地址空间区域,并且所有此类访问将按给定的顺序执行,但标准不要求volatile限定符就足够了。

请注意,即使volatile次访问相对于彼此进行排序,实现也可能不会相对于非volatile访问对它们进行排序。如果有一种方法可以告诉编译器以任何顺序写一堆信息,然后在所有其他写操作完成后只写入volatile位置,那将会很有帮助,但是没有这样做的标准方法除了制作所有数据volatile之外(这将限制优化机会,无论如何可能也可能不够)。