有人可以解释这个C ++联合示例吗?

时间:2017-09-21 17:18:18

标签: c++ class destructor unions

我在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. union通过删除默认构造函数强制您初始化其中一个union成员,在这种情况下,他使用Hello World初始化了该字符串。
  2. 在他初始化字符串之后,这个向量在技术上还不存在吗?我可以访问它,但它还没有构建?
  3. 他通过调用析构函数显式地销毁字符串对象。在这种情况下,当S超出范围时,是否会调用~S()析构函数?如果是这样,在哪个对象上?如果他没有在字符串上显式调用析构函数,那是内存泄漏吗?我倾向于不,因为弦乐自我清理,但对于工会,我不知道。他自己调用了字符串和向量的析构函数,所以~S()析构函数似乎没用,但是当我删除它时,我的编译器不会让我编译它。
  4. 这是我第一次看到有人使用new运算符将对象放在堆栈上。在这种情况下,这是现在可以使用向量的唯一方法吗?
  5. 当您使用向量时使用贴图时,您不应该在其上调用删除,因为尚未分配新内存。通常,如果你在堆上放置new,你必须释放()内存以避免泄漏,但在这种情况下,如果他让vector和union超出范围而不调用析构函数会发生什么?
  6. 我发现这真令人困惑。

2 个答案:

答案 0 :(得分:7)

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

    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 ++的创建者决定保持简单并且永远不会自动破坏联合的活动成员而不是强制执行,而不是关于编译器何时能够销毁活动成员以及什么时候不会破坏任何东西的复杂规​​则。程序员手动完成。

答案 1 :(得分:3)

  

union会强制您通过删除默认构造函数来初始化其中一个union成员,在这种情况下,他使用Hello World初始化了该字符串。

正确

  

在他对字符串进行初始化之后,这个向量在技术上还不存在吗?我可以访问它,但它还没有构建?

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

  

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

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

  

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

是的,但它确实是未定义的行为。由于析构函数不是微不足道的,因此您无法在不破坏活动成员的情况下更改成员。如果你在创建向量之前没有销毁字符串,那么你就会丢失字符串的状态,包括它所持有的内存(如果它持有任何内容 - 请参阅small string optimizations如何不能)。 / p>

  

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

如你所说的那样没用,但你真的可以做到。联合必须有一个析构函数,并且提供的编译器会被删除,因为std::stringstd::vector具有非平凡的析构函数。

  

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

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

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

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

  

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

好吧,如果他们没有手动破坏矢量,那么你就会泄漏矢量所持有的东西,因为什么都不会被破坏。只要在联盟超出范围之前销毁活动成员,那么你就可以了。