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

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

参考以下代码片段。

据我了解:

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

b)p1p2被添加到向量中使用push_back(),默认的Allocator创建Point的新实例 并将 p1p2 的值(x,y)复制到这些新创建的实例中。

我的问题是:

1)我的理解正确吗?

如果是;

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

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

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

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

#include <iostream>
#include <vector>

using namespace std;

struct Point {
    Point() {
        std::cout<< "Point created\n";
        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 分配更多内存,但并非总是如此。它永远不会创建默认构造点的新实例。

问:如果Allocator创建了新的Point对象,为什么我只看到两行"Points created"?

A: 分配器未创建新对象 - 分配器仅分配更多内存,如果需要。您插入向量中的对象是复制构造的。因为你还没有创建拷贝构造函数,编译器已经帮你生成了。

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

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

问:共享指针是从方法 return 向量的推荐方式吗?

A: 这取决于您的用例,并且是基于意见的。我个人的建议是:

  • 如果您的用例允许,return 按值(即 std::vector<Point> getPoints()
  • 如果你需要动态分配存储,或者你想要return的对象可以什么都没有,因为构造失败,return被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 的参数是 copy constructed ,如果使用 push_back( const T& value ) 变体,则使用复制构造函数;如果使用 push_back( T&& value ),则使用移动构造函数从中移动。
  2. 如果没有更多内存(即新的 size() > capacity),将分配更多内存以足以容纳新元素。将分配多少内存是实现定义的。一个常见的模式是将 vector 之前拥有的容量加倍,直到达到阈值,然后将内存分配到桶中。您可以在插入元素之前使用 reserve() ,以确保您的向量有足够的容量来容纳至少尽可能多的元素而无需新的分配。分配新内存后,向量将所有现有元素重新分配到新存储中,方法是复制它们,或者如果它们不可复制插入则移动它们。这种重新分配将使所有迭代器和对向量中元素的引用无效(警告:当重新分配有点复杂时,将使用何时精确复制与移动的规则,但这是一般情况)

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

Point(const Point& p) {
    std::cout<< "Point copied\n";
    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";
}

1)我的理解正确吗?

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

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

[Ans]使用复制构造函数。请添加一个复制构造函数,您将看到不同之处。

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

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