C++:矢量分配器行为、内存分配和智能指针

C++ : Vector Allocator behavior, memory allocation and smart pointers

本文关键字:分配 内存 智能 指针 分配器 C++      更新时间:2023-10-16

>请参考以下代码片段。

据我了解:

a) 'p1' 和 'p2' 对象在堆栈中创建,并在 getPoints() 方法结束时被销毁。

b) 当使用 push_back() 将p1p2添加到向量时,默认分配器会创建新的点实例 并将P1P2的值 (x,y) 复制到这些新创建的实例中。

我的问题是:

1)我的理解正确吗?

㞖;

2) 如果分配器创建新的点对象,为什么我只看到两行"已创建点"?

因为我希望看到p1p2的两行,以及分配器新创建对象的两行。

3) 分配器如何将原始值分配给新创建的对象的 x,y 字段?它是否使用原始内存副本?

4) 共享指针是从方法返回向量的推荐方法吗?

#include <iostream>
#include <vector>
using namespace std;
struct Point {
Point() {
std::cout<< "Point createdn";
x=0;
y=0;
}
int x;
int y;
};

std::shared_ptr< vector<Point> > getPoints() {
std::shared_ptr< vector<Point> > ret =  std::make_shared< vector<Point> >();
Point p1;
p1.x=100;
p1.y=200;
Point p2;
p2.x = 1000;
p2.y = 2000;
ret->push_back(p1);
ret->push_back(p2);
return ret;
}
int main(int argc, char** argv)
{
std::shared_ptr< vector<Point> > points = getPoints();
for(auto point : *(points.get())) {
std::cout << "Point x "<<point.x << " "<< point.y<<"n";
}
}

问:我的理解正确吗?

答:你的理解是部分正确的。

  • P1 和 P2 是使用您定义的默认无参数构造函数在堆栈上创建的。
  • 调用 push_back() 时,默认分配器可用于为 p1 和 p2 分配更多内存,但并不总是这样做。不过,它永远不会创建默认构造 Point 的新实例。

问:如果分配器创建新的点对象,为什么我只看到两行"已创建点"?

答:分配器不会创建新对象 - 分配器仅在需要时分配更多内存。插入到矢量中的对象是复制构造的。由于您尚未创建复制构造函数,因此编译器已为您生成一个。

问:分配器如何将原始值分配给新创建对象的 x,y 字段?它是否使用原始内存副本?

答:如上一个问题所述,分配器仅分配内存,不会创建或销毁对象。复制字段的行为由复制构造函数完成,该构造函数在执行push_back时被调用。自动生成的复制构造函数将对类的每个成员进行成员级复制构造。在您的情况下,xy是基元类型,因此它们只是复制的原始内存。如果成员是复杂对象,则将调用其复制构造函数。

问:共享指针是否是从方法返回矢量的推荐方法?

答:这取决于您的用例,并且是基于意见的。我个人的建议,适用于所有对象是:

  • 如果您的用例允许,请按值返回(即std::vector<Point> getPoints())
  • 如果需要动态分配的存储,或者要返回的对象可以什么都没有,因为构造失败,请按std::unique_ptr返回。这几乎适用于您可能想要创建的所有工厂函数。即使您以后想共享所有权(见第 3 点),您也可以通过从unique_ptr(std::shared_ptr<T> shared = std::move(unique))移动来构建shared_ptr;
  • 避免使用shared_ptr,除非您确实需要 ptr 的共享所有权shared_ptr推理起来更复杂,可能会创建难以调试的周期,导致内存泄漏,并且在性能方面更重(因为与其引用计数和为控制块分配的额外内存相关的原子操作)。如果你认为你需要一个shared_ptr,重新考虑你的设计,并考虑是否可以使用unique_ptr

工作原理:

在内部,std::vector 使用堆上的默认分配器(或提供的自定义用户,如果您提供了)分配的内存。这种分配发生在幕后,与向量的大小和向量中的元素数量无关(但始终为>=size())。您可以使用capacity()函数获取矢量为其分配了多少个元素。当您呼叫push_back()时,会发生什么:

  1. 如果有足够的存储空间(由capacity()确定)来容纳另一个元素,则传递给 push_back 的参数是复制构造的,如果使用push_back( const T& value )变体,则使用复制构造函数,如果使用push_back( T&& value )从 移动构造函数移动。
  2. 如果没有更多的内存(即新的 size()>容量),则会分配更多的内存,足以容纳新元素。将分配多少内存由实现定义。一种常见的模式是将矢量以前拥有的容量增加一倍,直到达到阈值,之后在存储桶中分配内存。您可以在插入元素之前使用reserve(),以确保您的向量有足够的容量来容纳至少同样多的元素,而无需新的分配。分配新内存后,矢量将所有现有元素重新分配到新存储中,方法是复制它们,或者在它们不可复制插入时移动它们。这种重新分配将使向量中的所有迭代器和对元素的引用无效(警告:当重新分配稍微复杂一些时,将使用确切复制与移动的规则,但这是一般情况)

1)我的理解正确吗?

[答]是的,部分正确。对象 p1 和 p2 是在堆栈中创建的,但是当推送到向量时,它会调用复制构造函数来创建和初始化新对象。

2) 如果分配器创建新的点对象,为什么我只看到两行"已创建点"? 因为我希望看到 p1 和 p2 的两行,以及分配器新创建对象的两行。

[答]使用复制构造函数。请添加一个复制构造函数,您将看到差异。

3) 分配器如何将原始值分配给新创建的对象的 x,y 字段?它是否使用原始内存副本? [答]使用复制构造函数和向量本身是一个动态数组,它根据需要重新分配内存。

4) 共享指针是从方法返回向量的推荐方法吗? [答]这也取决于您的用例。您可以将对向量的引用作为参数传递并返回相同的参数。

将复制构造函数添加到Point类以查看发生了什么。

Point(const Point& p) {
std::cout<< "Point copiedn";
this->x = p.x;
this->y = p.y;
}

如果您使用的是 GCC 编译器,您将看到该语句打印了五次。在getPoints函数中,一次用于第一个push_back,两次用于下一个push_back,因为矢量被调整大小并再次插入所有元素。 第四次和第五次将是main内的for循环。

您可以使用reserve消除三个副本,以在getPoints功能中设置vector的容量,

ret->reserve(2);

并通过在mainfor循环中使用引用。

for(auto& point : *(points.get())) {
std::cout << "Point x "<<point.x << " "<< point.y<<"n";
}