std::vector 如何处理对析构函数的调用?

how does std::vector deal with the call to destructor?

提问人:sixsixqaq 提问时间:11/29/2022 最后编辑:sixsixqaq 更新时间:11/30/2022 访问量:480

问:

当 tring 实现时,我对析构函数的隐式调用感到困惑。std::vector

然后向量中的元素可能是:

  • T
    • 一个类对象,
  • T*,shared_ptr<T>
    • 指向类对象的智能/简单指针
  • int
    • 内置型
  • int *
    • 指向内置类型的指针

调用 、 或 时,可能会调用析构函数。resize()reserve()erase()pop_back()

我想知道 std::vector 如何处理对析构函数的调用。

我发现,只有当类型是内置指针时才会调用析构函数(当然,如果它有的话)。std::vector

std::vector 是否通过区分类型并确定是否调用析构函数来实现它?

以下是关于这个问题的一些实验:

示例 1,元素为 Object。

#include <vector>
#include <iostream>
using namespace std;

struct Tmp {
    ~Tmp() { cerr << "Destructor is called." << endl; }
};

int main (void)
{
    std::vector<Tmp>arr;
    Tmp tmp = Tmp();
    cerr << "Capacity:" << arr.capacity() << endl;//0
    arr.push_back (tmp);
    cerr << "Capacity:" << arr.capacity() << endl;//1
    arr.push_back (tmp);
    cerr << "Capacity:" << arr.capacity() << endl;//2
    cerr << "popback start------------" << std::endl;
    arr.pop_back();
    arr.pop_back();
    cerr << "popback end--------------" << endl;
}

输出为:

Capacity:0
Capacity:1
Destructor is called.
Capacity:2
popback start------------
Destructor is called.
Destructor is called.
popback end--------------
Destructor is called.

示例 2,该元素是指向 obecjt 的内置指针:

...
std::vector<Tmp>arr;
Tmp * tmp = new Tmp;
...

析构函数不会被自动调用:

Capacity:0
Capacity:1
Capacity:2
popback start------------
popback end--------------

示例 3,shared_ptr

std::vector<shared_ptr<Tmp>>arr;
auto tmp = make_shared<Tmp>();

... //after being copied, the references count should be 3.
tmp = nullptr; //References count reduced by 1

cerr << "popback start------------" << std::endl;
arr.pop_back();//References count reduced by 1
arr.pop_back();//References count reduced by 1
cerr << "popback end--------------" << endl;

将调用shared_ptr的析构函数。当引用减少到 0 时,将调用 Tmp 的析构函数:

Capacity:0
Capacity:1
Capacity:2
popback start------------
Destructor is called.
popback end--------------
C++ 哎呀 C++11 标准

评论

1赞 Some programmer dude 11/29/2022
为什么要打印容量而不是尺寸?容量表示为向量分配的内存量,但其中一些内存可能未初始化且不包含任何对象。
4赞 StoryTeller - Unslander Monica 11/29/2022
您的问题是将指针与其指向的对象混为一谈。它们是不同的对象,可以独立地“破坏”。
0赞 sixsixqaq 11/29/2022
我认为尺寸是肉眼可见的,而容量则不然。
1赞 Some programmer dude 11/29/2022
不同之处在于,大小是向量内实际构造的对象的数量,而容量不是。容量只能为 。这意味着向量中只有实际的、构造的对象。然后是更多对象的未初始化内存,但没有更多对象,因此这两个插槽中的任何内容都无法被破坏。6442
0赞 463035818_is_not_an_ai 11/29/2022
“当 tring 实现 std::vector 时,”您实际上是指要使用吗?std::vector

答:

2赞 Some programmer dude 11/29/2022 #1

假设您有由以下项定义的指针:

Tmp * tmp = new Tmp;

这可以这样说明:

+--------------+      +------------+
| variable tmp | ---> | Tmp object |
+--------------+      +------------+

当您有指针向量时:

std::vector<Tmp*> vec;

并添加指针:

vec.push_back(tmp);

然后你有这样的东西:

+--------------+
| variable tmp | --\
+--------------+   |     +------------+
                    >--> | Tmp object |
+--------+         |     +------------+
| vec[0] | --------/
+--------+

从这些插图中可以很容易地看出,向量不包含对象本身,而只是一个指向它的指针。Tmp

因此,当您从向量中删除指针时

vec.pop_back();

只有向量内部的指针被删除和销毁。物体本身仍然存在,我们再次有了第一个插图。Tmp

评论

0赞 sixsixqaq 11/29/2022
我知道你的意思。我想知道如何实现“删除”步骤。检查类型并确定是否调用析构函数似乎很麻烦。
1赞 Dmitry 11/29/2022
你不需要检查,你总是调用析构函数。Asm 表示 ~T(),其中 T 是微不足道的,只是给出了 noop。
1赞 Some programmer dude 11/29/2022
@sixsixqaq 向量内的所有对象都由向量本身管理。它处理其所有存储对象的生存期。你不应该在向量内的对象上“调用析构函数”。或者通常在任何对象上(除非您已使用 placement new 初始化对象)。
0赞 Some programmer dude 11/29/2022
@sixsixqaq 如果您有一个指向对象的指针向量,并且没有指向对象的任何其他存储指针,则应先删除对象,然后再从向量中删除指针。或者在管理生存期的地方使用智能指针。
2赞 Peter 11/29/2022
通常,C++ 代码(由 C++ 开发人员编写)不需要确定何时调用析构函数。所有代码需要做的就是结束对象的生命周期(例如,对于具有自动存储持续时间的对象,对于动态分配的对象,封闭范围结束,对于动态分配的对象,等等)。编译器负责实际销毁对象的机制 - 包括在必要时调用析构函数 - 因为它有信息(例如,关于每个对象的类型)来这样做。[某些特殊情况除外 - 但这些都是例外]。delete
3赞 Yakk - Adam Nevraumont 11/30/2022 #2

的析构函数不执行任何操作。的析构函数不执行任何操作。int*T*

您可能认为“destroy an int pointer”的意思是调用,但这并不是破坏指针。即销毁指针指向的内容,并回收为其分配的内存(这是 2 个不同的步骤)。delete ptr

所以 a 摧毁了其中的所有 s;然而,这种破坏是一个努普。vector<int*>int*

的破坏者也摧毁了其中的所有东西;析构函数递减引用计数,如果达到零,则销毁 .vector<shared_ptr<T>>shared_ptr<T>T

和 也是一样的——在这两种情况下,析构函数(逻辑上)是运行的,但 的析构函数是 noop,而析构函数是 。vector<T>vector<T*>T*TT::~T()


在 C++ 中,每个实例都是一个对象。一个是对象,一个是对象,一个是对象,是一个对象。intint*vector<int>struct Foo{}; Foo foo;

摧毁一个物体有时是一个麻烦。所有基元类型(包括指针)都是如此。

因为它是一个noop,所以人们会变得草率,谈论销毁指针,就像他们谈论销毁指针所指向的内容一样。但它们不是一回事。

struct Noisy {
  ~Noisy() { std::cout << "~Noisy\n"; }
};
using Ptr = Noisy*;

我可以这样做:

Ptr ptr;
ptr.~Ptr(); // compilers might complain here in non-generic code

这是一个noop;在这里,我尝试手动调用 的(伪)析构函数,这是一个指针。这个(伪)析构函数是一个 noop,因此不会运行任何代码。ptr

Ptr ptr = new Noisy();
delete ptr;

这实际上破坏了,而不是。然后它会回收我们用来存储的内存。*ptrptr*ptr

new Noisy()还做了两件事——它从“自由存储”中获取内存来存储 A,然后在它得到的内存中构造该对象。Noisy

您可以拆分这些操作。您可以将存储与创建对象分开分配(这称为“放置新”),也可以将销毁对象与回收存储分开。

这样做被认为是C++中的一种高级技术,这就是为什么没有人和你谈论它的原因。

void demo() {
  alignas(Noisy) char buffer[sizeof(Noisy)];
  Noisy* ptr = ::new( (void*)buffer ) Noisy();
  ptr->~Noisy();
}

这会在堆栈上创建一个缓冲区(而不是自动存储),在其中手动构造一个对象,然后手动销毁该对象。NoisyNoisy

template<class T>
void demo2() {
  alignas(T) char buffer[sizeof(T)];
  T* ptr = ::new( (void*)buffer ) T();
  ptr->~T();
}

这使得演示具有通用性。我可以做或演示与存储分开的建造/破坏。demo<int>()demo<Noisy>()

std::vector正在做这样的事情 - 它管理一个存储缓冲区(由 )和该缓冲区“前面”的一堆对象(由 )。它使用类似于 的技术手动构造和销毁其缓冲区中的对象。vec.capacity()vec.size()demo2

销毁原始指针不会导致销毁指向的对象。但对于实例或智能指针来说,情况并非如此。