【问题标题】:Could someone explain this C++ union example?有人可以解释这个 C++ 联合示例吗?
【发布时间】:2018-03-03 03:33:00
【问题描述】:

我在 cppreference.com 上找到了这段代码。这是我见过的最奇怪的 C++,对此我有几个问题:

union S
{
    std::string str;
    std::vector<int> vec;
    ~S() {}  
};          

int main()
{
    S s = { "Hello, world" };
    // at this point, reading from s.vec is undefined behavior
    std::cout << "s.str = " << s.str << '\n';
    s.str.~basic_string<char>();
    new (&s.vec) std::vector<int>;
    // now, s.vec is the active member of the union
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';
    s.vec.~vector<int>();
}

我想确保我做对了一些事情。

  1. 联合强制您通过删除默认构造函数来初始化联合成员之一,在这种情况下,他使用 Hello World 初始化了字符串。
  2. 在他初始化字符串之后,从技术上讲,向量还不存在?我可以访问它,但它尚未构建?
  3. 他通过调用其析构函数显式销毁字符串对象。在这种情况下,当 S 超出范围时,会调用 ~S() 析构函数吗?如果有,在哪个物体上?如果他没有在字符串上显式调用析构函数是内存泄漏吗?我倾向于不,因为字符串会自行清理,但对于我不知道的工会。他自己调用了字符串和向量的析构函数,所以 ~S() 析构函数似乎没用,但是当我删除它时,我的编译器不会让我编译它。
  4. 这是我第一次看到有人使用 new 运算符将对象放在堆栈上。在这种情况下,这是唯一可以使用向量的方法吗?
  5. 当您像他对向量一样使用placement new 时,您不应该对其调用delete,因为尚未分配新内存。通常如果你将 new 放在堆上,你必须 free() 内存以避免泄漏,但在这种情况下,如果他让向量和联合超出范围而不调用析构函数会发生什么?

我觉得这真的很混乱。

【问题讨论】:

  • 得说...以前从未见过带有析构函数的union。好,坏,我不知道。只是没见过。
  • 在实践中,您的班级应该知道它使用哪个工会成员,例如成为tagged union;见std::variant;阅读所有union reference 页面,尤其是最后一个标记的联合示例
  • 每个帖子一个问题。而且您需要阅读good c++ book 而不是随机询问互联网人
  • @passerby 这一切都如此紧密地结合在一起,几乎是从不同方面看待一个问题。
  • (2) 访问联合的非活动成员是未定义的行为。当前活动成员是写入的联合的最后一个成员。

标签: c++ class destructor unions


【解决方案1】:
  1. 是的,没错。
  2. 因为向量和字符串使用相同的底层存储(unions 就是这样工作的),并且该存储当前包含一个字符串,所以没有地方可以放置 vertor 并且尝试访问它是未定义的.不是还没建好;是它不能被构造,因为有一个字符串在路上。
  3. 只要S 超出范围,就会调用它的析构函数。在这种情况下,这就是 union 的析构函数,它被明确定义为什么都不做(因为 union 不知道哪个成员是活动的,所以它实际上不能做它应该做的事情) .因为联合不知道它的哪个成员是活跃的,如果你不显式调用字符串的析构函数,它就无法知道那里有一个字符串,字符串也不会被清理。当存在具有非平凡析构函数的联合成员时,编译器会让您编写自己的析构函数,因为它不知道如何清理它并希望您这样做;在这个例子中,你也不知道如何清理它,所以你在联合的析构函数中什么也不做,让使用S的人手动调用正确元素的析构函数。
  4. 这称为“放置新”,是在现有内存位置构造对象而不是分配新位置的典型方法。除了联合之外,它还有其他用途,但我相信这是在不使用未定义行为的情况下将向量放入此联合的唯一方法。
  5. 如第 3 部分所述),当s 超出范围时,它不知道它是否包含字符串或向量。 ~S 析构函数什么都不做,所以你需要用它自己的析构函数来销毁向量,就像字符串一样。

要了解联合不能自动知道调用哪个析构函数的原因,请考虑以下替代函数:

int maybe_string() {
    S s = {"Hello, world"};
    bool b;
    std::cin >> b;
    if (b) {
        s.str.~basic_string<char>();
        new (&s.vec) std::vector<int>;
    }
    b = false;
    // Now there is no more information in the program for what destructor to call.
}

在函数结束时,编译器无法知道s 是包含字符串还是向量。如果您不手动调用析构函数(假设您有办法判断,我认为您在这里不会这样做),它必须安全行事,不要破坏任何一个成员。 C++ 的创建者并没有制定关于编译器何时能够销毁活动成员以及何时不会销毁任何东西的复杂规​​则,而是决定保持简单,永远不要自动销毁联合的活动成员,而是强制程序员手动完成。

【讨论】:

  • 但是如果 S 没有显式调用析构函数,我认为如果 S 超出范围,字符串不会泄漏。当然 ~S 析构函数没有做任何事情,但是普通类中的字符串对象会自行清理,对吗?我认为向量也应该如此?
  • @Zebrafish,在普通类中,您知道哪个对象存在于空间中。在联合中,两个对象占据相同的空间,实际上只有一个对象存在。编译器不知道要销毁哪一个,所以不,在联合中省略任一对象的析构函数调用是不行的。
  • 哦,我现在明白了。所以它强迫你有一个析构函数,除非在这种情况下它没有做任何特别的事情。但我想你可以记录下哪个成员处于活动状态,并据此调用它的析构函数。
  • @Zebrafish 我添加了一个示例,编译器无法知道要调用什么析构函数,因此您可以了解为什么它让程序员这样做。
  • @Zebrafish 堆栈上没有泄漏(将自动回收内存),但 string 对象很可能会进行 动态 分配。如果没有适当的处理,程序将不知道它应该被删除并泄漏它。
【解决方案2】:

联合强制你通过删除默认构造函数来初始化联合成员之一,在这种情况下,他用 Hello World 初始化了字符串。

正确

在他初始化字符串之后,从技术上讲,向量还不存在?我可以访问它,但它尚未构建?

好吧,即使它可以访问并不意味着您可以访问。由于它不是访问它的活动项目,因此是未定义的行为。原因是它的生命周期还没有开始,因为它的构造函数还没有被调用。

会调用~S() 析构函数吗?

不,s 只会在超出范围时被销毁。

如果他没有在字符串上显式调用析构函数,是不是内存泄漏?

是的,但实际上是未定义的行为。你不能在不破坏活动成员的情况下更改成员,因为析构函数不是微不足道的。如果您在创建向量之前不销毁字符串,那么您将丢失字符串的状态,其中包括它所持有的内存(如果它持有任何 - 请参阅small string optimizations,了解它是如何做到的)。

所以 ~S() 析构函数似乎没用,但是当我删除它时,我的编译器不会让我编译它。

你说的没用,但确实是你能做的。联合必须有一个析构函数,并且编译器提供的析构函数被删除,因为std::stringstd::vector 有非平凡的析构函数。

在这种情况下,这是唯一可以使用向量的方法吗?

是的。您必须使用placement new 才能构建对象。如果你没有尝试做类似的事情

s.vec = std::vector<int>{};

然后,您将分配给一个从未构造过的未定义行为的对象。

vector 和 union 超出范围而不调用析构函数?

好吧,如果他们没有手动销毁矢量,那么您将泄漏矢量所包含的内容,因为不会被破坏。只要您在联合超出范围之前销毁活动成员,就可以了。

【讨论】:

    猜你喜欢
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-20
    • 2020-01-31
    相关资源
    最近更新 更多