std::vector 如何破坏其对象?
Posted
技术标签:
【中文标题】std::vector 如何破坏其对象?【英文标题】:How does std::vector destruct its objects? 【发布时间】:2016-08-10 15:47:47 【问题描述】:为了练习,我正在尝试实现自己的 std::vector。当前源码:http://pastebin.com/bE4kjzcb
这是我的课程大纲:
Array()
使用 malloc()
分配一些内存
push_back(const T &t)
添加一个元素,必要时调用realloc()
。
~Array()
调用free()
释放内存。
这个模型的主要问题是,free()
回收了内存,但它不调用T
的析构函数(当T
是一个类而不是标准数据类型时)。
当向量内的元素是对象时,这可能会导致严重的资源泄漏。我对这个问题的解决方案是,在我free()
内存之前调用~T()
明确。
我使用malloc()
的原因是,我正在尝试使用realloc()
。如果我使用new
和delete
,内存使用会在重新分配时达到峰值。 (新缓冲区和旧缓冲区都存在的时刻。)
问:
这是一个糟糕的设计吗? std::vector
如何解决这个问题?我的向量类还有其他缺陷吗?
PS:现在不谈malloc()
的多线程性能。
【问题讨论】:
如果你使用C++,不要使用malloc
和free
,使用new
和delete
如何在push_back
中添加元素?如果您使用的是placement new,那么显式调用析构函数就可以了。
您的实施总体上并不太安全。您的 push_back
将原始未初始化内存重新解释为对象,然后分配给它。尝试将std::string
存储在您的Array
中,灾难肯定会随之而来。
"我使用 malloc() 的原因是,我正在尝试使用 realloc()。" 您不能将 realloc
用于存储类型不可轻易复制。
std::vector
使用作为模板参数的 allocator,默认为 std::allocator
。从 C++17 及更高版本开始,它将通过std::allocator_traits
使用该分配器,调用std::allocator_traits:.construct
在指定存储中构造一个对象,并调用std::allocator_traits::destroy
来销毁一个对象。
【参考方案1】:
调用~T()
正是std::vector
处理问题的方式。
但是你确实有几个问题:
首先,push_back
需要使用placement new 将值复制构造到向量中。你不能只使用赋值。
其次,你不能调用realloc
——如果对象有内部指针,它们最终会指向它们自身之外。您必须再次调用malloc
,然后使用placement new 复制构造值,然后显式删除所有旧值,然后调用free
释放旧值。
(实际上,std::vector
本身并没有调用~T()
。相反,它调用了负责...分配和释放内存的分配器。不过在内部,这就是通用的用途分配器会这样做。)
【讨论】:
并非如此。vector
是一个分配器感知容器,所以没有主流实现会写 ~T()
。
@uhohsomebodyneedsaupper 但是默认分配器会。
OK 我应该如何使用placement new? new (buffer[length]) T()
?
如果我使用placement new,是否还需要将分配的对象分配给buffer[length]?(由于object对象是在buffer上分配的)
"如果我使用placement new,是否还需要将分配的对象分配给buffer[length]?"不,push_back
应该使用复制构造函数调用放置new
。没有任务。【参考方案2】:
push_back(const T &t) 添加一个元素,必要时调用realloc()。
只要T
是trivially copiable
就可以,例如,尝试推回双链表并在重新分配后取一个并向后迭代 - 应用程序可能会崩溃。解决方案是重载函数两次,一次用于可复制的类型,另一次用于不可复制的对象。
与其他人相反,我很抱歉标准容器不使用realloc
来表示可轻松复制的对象。至少在 Windows 上,realloc
首先检查当前块是否可以容纳新大小,如果可以,它只会更新堆条目,从而导致巨大的性能提升(无复制)。
在我 free() 内存之前明确调用 ~T()。
是的,标准分配器就是这样做的。假设size
是对象计数,你迭代每一个并手动销毁它:
for (auto i=0U;i<size;i++)
data[i].~T();
有趣的是,C++17 将添加 std::destruct
正是这样做的。
奖金:
在这里使用 new[]
和 delete[]
将无济于事。通常,动态数组比实现容量所需的空间节省更多空间,多余的空间不会被活动对象填充,只有垃圾。
new[]
将用对象填充整个内存。容量不能以这种方式实现。每当有人推回新元素时,数组就会移动/复制整个对象。所以在 1000 个push_back
之后,将有 1000 个重新分配。我们想要O(log (n))
的摊销时间。
即使是标准分配器也会调用new(size_t)
或malloc
而不是new[]
【讨论】:
【参考方案3】:与其调用malloc
和free
,不如使用new
和delete
。调用delete
将确保调用实例dtor。 =)
【讨论】:
我想过,但我担心new
和delete
是重新分配时的内存峰值。 (旧缓冲区和新缓冲区都必须存在片刻,以便我们可以复制数据。)
您将您的课程编写为模板。如果我给你的班级一个std::string
或基本上任何非 POD 类型,你的代码将使用malloc
失败。 C++ 不是 C。
@KelvinZhang 如果您想支持必须深度复制的类型,这就是您所需要的。
@KelvinZhang 所以你的问题实际上是关于vector如何实现重新分配,而不是malloc和free对吗?
您的印象似乎是 realloc 保留了原始分配的内存。事实并非如此。【参考方案4】:
如果默认构造函数是为刚刚分配的对象保留的并且如果 /move/copy 构造函数/赋值运算符和T 的析构函数传播刚刚分配的或用户对象的信息。 std::vector 中的解决方案及其默认分配器仍然是一个更好的设计。
建筑
buffer = new T[capacity];
而不是
buffer = (T*)malloc(capacity * sizeof(T));
和
delete [] buffer;
而不是
free(buffer);
将自动调用每个对象的析构函数,如示例所示
class A
public:
~A() std::cout << "ok" << std::endl;
;
int main()
A* a = new A[3];
delete [] a;
return 0;
此代码输出 3“ok”。那么 A 应该包含额外的字段和一个非默认构造函数来区分分配和用户构造。
【讨论】:
当您尝试在向量后面添加一个新值时,这将无法正常工作。您必须每次复制数组。 @MartinBonner 我认为长度可以演变直到容量,并且析构函数对默认构造函数创建的 T 对象(介于长度和容量之间的对象)不执行任何操作。对于重新分配,移动分配需要新的分配。是的 std::vector 完美地完成了这项工作。 但是关于容量的一点是,你没有在 size() 和 capacity() 之间构造元素——你只是分配了内存。 @MartinBonner 我同意你的评论。上面和 vector.tcc 中描述的解决方案更适合仅分配 size() 和 capacity() 之间的元素。【参考方案5】:这里有一个例子,它或多或少是如何工作的 std::vector:
#ifndef __STDVECTOR__
#define __STDVECTOR__
#include <iostream>
using namespace std;
template <typename T>
class StdVector
private:
T *buffer;
unsigned int capacity;
public:
//Constructor.
StdVector()
capacity=0;
buffer=new T[capacity];
//Copy constructor.
StdVector(const StdVector &asv)
int i;
capacity=asv.getCapacity();
buffer=new T[asv.getCapacity()];
for (i=0; i<capacity; i++)
buffer[i]=asv[i];
//Destructor.
~StdVector()
delete []buffer;
void push_back(T obj)
StdVector oldSV(*this);
int i;
capacity++;
delete []buffer;
buffer=new T[capacity];
for (i=0; i<oldSV.getCapacity(); i++)
buffer[i]=oldSV[i];
buffer[i]=obj;
;
T getBuffer() const
if (capacity==0)
throw exception();
return *buffer;
;
T &operator[](int index) const
if (index>=capacity)
//Out of range.
throw exception();
else
return buffer[index];
StdVector &operator=(const StdVector &obj)
capacity=obj.getCapacity();
delete []buffer;
buffer=new T[capacity];
buffer=obj.getBuffer();
return *this;
unsigned int getCapacity() const
return capacity;
;
;
#endif
int main()
try
StdVector<int> test;
StdVector<string> test2;
unsigned int i;
test.push_back(5);
test.push_back(4);
test.push_back(3);
test.push_back(2);
test.push_back(1);
test.push_back(0);
test.push_back(-1);
test.push_back(-2);
test.push_back(-3);
test.push_back(-4);
test.push_back(-5);
for (i=0; i<test.getCapacity(); i++)
cout << test[i] << endl;
test2.push_back("Hello");
test2.push_back(" ");
test2.push_back("World");
test2.push_back(".");
cout << "---------------" << endl;
for (i=0; i<test2.getCapacity(); i++)
cout << test2[i];
cout << endl;
catch(...)
cout << "Exception." << endl;
return 0;
它打印:
5
4
3
2
1
0
-1
-2
-3
-4
-5
---------------
你好世界。
也许我有一些错误。如果你知道,请告诉我。
【讨论】:
以上是关于std::vector 如何破坏其对象?的主要内容,如果未能解决你的问题,请参考以下文章
如何桥接 JavaScript(参差不齐)数组和 std::vector<std::vector<T>> 对象?
如何确定`range :: view`对象和`std :: vector`之间的等价?