【问题标题】:C++ Pointers break when changing a pointer更改指针时 C++ 指针中断
【发布时间】:2020-10-25 19:43:35
【问题描述】:

当我更改联合中的一个指针时,我的其他指针会中断并显示无效指针。

CustomDataType 示例类:

struct CustomDataTypeExample {
float x;
float y;
float z;
CustomDataTypeExample() = default;
CustomDataTypeExample(float x, float y, float z) {

    this->x = x;
    this->y = y;
    this->z = z;

};

// ...
};

ConfigCustomDataType示例类:

struct ConfigCustomDataTypeExample {
public:
    ConfigCustomDataTypeExample() = default;
    ConfigCustomDataTypeExample(CustomDataTypeExample values) {
        x = &values.x;
        y = &values.y;
        z = &values.z;
    }
    union {
        struct {

            CustomDataTypeExample* ex;
        };
        struct {

            float* x;
            float* y;
            float* z;
        };
    };
};

主要:

ConfigCustomDataTypeExample example({ 1.2f,3.4f,5.6f });
float value = 565;
example.x = &value;
std::cout << example.ex->x << ", " << example.ex->y << ", " << example.ex->z << "\n";
std::cout << *example.x << ", " << *example.y << ", " << *example.z << "\n";

输出:

565, -1.07374e+08, -1.07374e+08
565, 3.4, 5.6

到底发生了什么?如果我不将 example.x 更改为指向其他内容,它会正常工作,否则如果我更改它,则会破坏其他指针。

【问题讨论】:

  • 您好,欢迎来到 StackOverflow。你知道“工会”是做什么的吗?因为我怀疑它不像你认为的那样。有人把它比作酒店的房间:一次只能有一个租户入住。在您的情况下,这是结构或类。因此,在您分配给该类的那一刻,您将关闭该结构,反之亦然。只要您继续访问“您最后写的成员”,就可以定义行为。
  • 感谢您的回复。因此,您认为我试图通过使用联合来实现的目标是不可能的,或者是否有某种解决方法?
  • ConfigCustomDataTypeExample构造函数中,变量values是一个本地变量,它的生命周期在构造函数结束时结束。一旦values 不再存在,您保存的指针就会失效。稍后取消引用这些指针将导致未定义的行为
  • 你能解释一下你想要达到的目标吗?到目前为止,您描述的是您观察到的情况,而不是您想要做的事情。
  • @Someone 恐怕,我不确定你想要达到什么目的。

标签: c++ windows pointers unions invalid-pointer


【解决方案1】:

TL;DR: 三种不同类型的未定义行为:生命周期问题、访问联合的非活动成员(没有非标准扩展)以及通过example.ex(对声明的联合代表什么的误解)。

看起来你可以使用简单的引用。完整的解决方案在最后描述。

深入分析

这实际上是一个非常有趣的问题,因为这里发生了很多事情!三种不同类型的未定义行为。让我们一块一块地看。

首先,如 cmets 中所述,您将参数values 的地址分配给xyz(成员的地址)。参数values 具有自动存储持续时间,这意味着它在ConfigCustomDataTypeExample 的构造函数结束时被销毁。

struct ConfigCustomDataTypeExample {
public:
    ConfigCustomDataTypeExample() = default;
    ConfigCustomDataTypeExample(CustomDataTypeExample values) {
        x = &values.x;
        y = &values.y;
        z = &values.z;
    } // Pass this line x, y and z store invalid pointer values
      // (addresses to now destructed members of values).
      // Any indirection through these pointers is undefined behavior.
...

使用您的程序,您仍然能够读取yz 的值。这是未定义行为的本质:有时您可能会得到合理的结果,但没有任何保证。例如,当我尝试运行您的程序时,yz 得到了截然不同的结果。这是第一个明确的 UB。让我们检查下联合的声明,以了解它的真正含义。

类是由一系列成员组成的类型。 Union 是一种特殊类型的类,一次最多可以保存一个非静态数据成员。联合当前持有的对象称为活动成员。这意味着联合仅与其最大的数据成员一样大,如果内存使用是一个问题,这很有用。

union {
  struct {
      CustomDataTypeExample* ex;
  };
  struct {
      float* x;
      float* y;
      float* z;
  };
};

对于这个联合,成员是两个匿名结构(请注意,C++ 标准禁止匿名结构)。联合体的大小由最大的结构体决定,即float* 结构体。对于 64 位系统,指针类型的大小通常为 8 字节,因此对于 64 位系统,此联合的大小为 24 字节。

关于联合的使用,您显然不是为了减少内存消耗而使用联合。相反,您正在尝试做一些称为类型双关语的事情。类型双关语是当您尝试将一种类型的二进制表示解释为另一种类型时。根据 C++ 标准类型与联合的双关是未定义的行为(second),尽管许多编译器提供允许这样做的非标准扩展。让我们按照标准规则分析你的main程序:

ConfigCustomDataTypeExample example({1.2f, 3.4f, 5.6f});
// The anonymous struct holding 3 float* is now the active member.
// Though, all of the pointers are invalid, as already mentioned.

float value = 565;

example.x = &value;
// example.x is now a valid ptr value
 
std::cout 
    << example.ex->x << ", "  // UB: Accessing a non-active member
    << example.ex->y << ", "  // UB: non-active and invalid ptr (more on that later)
    << example.ex->z << "\n"; // UB: same as above

std::cout 
    << *example.x << ", "     // This is ok (active member and valid ptr)
    << *example.y << ", "     // UB: indirection to an invalid ptr
    << *example.z << "\n";    // UB: same as above

再一次,在取消引用 example.ex-&gt;x 时,未定义的行为足以打印 565。这是因为 float* xexample.ex-&gt;x 在联合的二进制表示中重叠,尽管这仍然是未定义的行为。

让我们首先通过更改 ConfigCustomDataTypeExample 以将引用作为参数来快速解决生命周期问题:ConfigCustomDataTypeExample(CustomDataTypeExample&amp; values) 并在 main 中声明一个 CustomDataTypeExample 变量。 我也在使用 gcc 进行编译,其中联合的类型双关语定义明确(非标准扩展):

CustomDataTypeExample data{1.0f, 2.0f, 3.0f};
ConfigCustomDataTypeExample example(data);
    
float value = 565;
example.x = &value;

std::cout 
    << example.ex->x << ", "  // This is now ok (using gcc's non-standard extension)
    << example.ex->y << ", "  // Something seems odd
    << example.ex->z << "\n"; // with these two lines
    
std::cout 
    << *example.x << ", "     // Now well defined
    << *example.y << ", "     // same
    << *example.z << "\n";    // same

这里什么都没有。我的一次运行的输出是:

565, 1961.14, 4.59163e-41
565, 2, 3

好的,至少现在 xyz 值是有效的,但是在取消引用 example.ex 的部分时我们仍然会得到垃圾值。是什么赋予了?让我们回到我们的联合声明,想想它是如何转换成它的二进制表示的。这是一个粗略的图表:

[float* x, float* y, float* z]

所以我们联合的内存布局是三个浮点指针,每个指针指向一个浮点值(相当于一个存储三个浮点指针的数组,例如float* arr[3])。 然而,对于 example.ex,我们试图将 float* x 解释为 3 个浮点数组。这是因为CustomDataTypeExample 的内存布局相当于一个包含 3 个浮点值的数组,而试图引用它的成员相当于数组索引。

我认为 gcc 的扩展基于 C90 标准第 6.5.2.2 节脚注 82 对 example-&gt;ex 的解释:

如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新类型中的对象表示为在 6.2.6 中描述(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。

我们还可以通过查看编译器如何将这三行转换为汇编来验证这一点:

example.x = &value;

std::cout 
    << example.ex->x << ", " 
    << example.ex->y << ", " 
    << example.ex->z << "\n";

使用godbolt 我们得到以下信息(我只取了相关的部分):

// Copies the value of rax to the memory pointed by QWORD PTR [rbp-48]
mov     QWORD PTR [rbp-48], rax  // example.x = &value;

// Copy a 32-bit value from memory address rax to eax.
// (eax register is used here to pass the value to std::cout)
// No surprises yet, as this address has a well defined floating point value (526).
mov     eax, DWORD PTR [rax]     // example.ex->x

// Not good, tries to copy a floating point value from memory address 
// [rax + 4 bytes]. Equivalent to *(&value + 1). This is gonna get 
// whatever random junk is in that part of memory.
mov     eax, DWORD PTR [rax+4]   // example.ex->y

我们可以很清楚地看到编译器是如何尝试将example.ex 指向的地址解释为内存中包含3 个浮点值的区域,即使它只包含一个。因此,第一次读取没问题,但第二次和第三次取消引用就大错特错了。

这段代码产生了极其相似的程序集,这并不奇怪,因为行为是等价的:

float* value_ptr = &value;

std::cout
    << *value_ptr << ", "    // equivalent to example.ex->x, OK
    << value_ptr[1] << ", "  // equivalent to example.ex->y, plain UB
    << value_ptr[2] << '\n'; // equivalent to example.ex->z, plain UB

这是未定义行为的情况与第一种情况非常相似。程序正在通过无效的指针值(third)执行间接寻址。

这三个未定义的行为结合在一起导致执行main 时出现奇怪的值。现在解决方案。

解决方案

首先让我们把小问题排除在外。 CustomDataTypeExample 显然是一个仅将数据包含在其中的聚合,因此无需为它显式声明特殊的成员函数(在这种情况下为构造函数)。特殊的成员函数被隐式声明(并且很简单):

struct CustomDataTypeExample {
    float x;
    float y;
    float z;
};

// Construct an instance of CustomDataTypeExample by aggregate initializing.
// This was also utilized earlier.
CustomDataTypeExample data{1.0f, 2.0f, 3.0f};

解决方案是什么,看起来您正试图为一个简单的问题提出一个额外的抽象层。简单的引用应该可以解决问题。没有理由进行复杂的联合设置,正如您可能已经注意到的那样,它很容易出错。在 C++ 中,联合只能真正用于减少内存是稀缺资源的系统上的内存消耗。

因此,我将摆脱 ConfigCustomDataTypeExample 并使用如下引用:

CustomDataTypeExample data{1.0f, 2.0f, 3.0f};
CustomDataTypeExample& data_ref = data;

// Modifies the contents of the existing data
data_ref.x = 565;

std::cout 
    << data_ref.x << ", " 
    << data_ref.y << ", " 
    << data_ref.z << '\n';

当您使用具有自动存储持续时间的变量时,参考是可行的方法。与指针相比,使用引用的生命周期问题更难创建,整体解决方案通常更简单。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-14
    • 2014-06-26
    • 2016-03-18
    • 1970-01-01
    • 2019-03-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多