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

时间:2018-10-04 08:13:00

标签: c++ stl shared-ptr stdvector allocator

请参阅以下代码段。

根据我的理解:

a)'p1'和'p2'对象在堆栈中创建,并在getPoints()方法的末尾销毁。

b)使用push_back()将 p1 p2 添加到矢量中时,默认分配器将创建Point的新实例 并将 p1 p2 的值(x,y)复制到这些新创建的实例中。

我的问题是:

1)我的理解正确吗?

如果是;

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

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

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

4)推荐使用共享指针从方法中返回向量吗?

#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";
    }

}

3 个答案:

答案 0 :(得分:4)

问:我的理解正确吗?

A:您的理解部分正确。

    使用已定义的默认无参数构造函数在堆栈上创建
  • p1和p2。
  • 在调用push_back()时,可以使用默认分配器 为p1和p2分配更多的内存,但并非总是如此。但是,它将永远不会创建默认构造Point的新实例。

问:如果分配器创建了新的Point对象,为什么只看到两行“ Points created”?

A:分配器未创建新对象-分配器仅分配更多内存,如果需要,则分配 。您插入到向量中的对象是复制构造的。由于尚未创建副本构造函数,因此编译器已为您生成了一个副本构造函数。

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

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

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

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

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

这是如何工作的:

内部,std :: vector正在使用通过堆上的默认分配器(或提供的自定义用户,如果提供了自定义用户)分配的内存。这种分配发生在幕后,并且与向量的大小以及向量中元素的数量无关(但始终> = {size())。您可以使用capacity()函数获得向量为存储分配了多少元素。当您致电push_back()时,会发生什么:

  1. 如果有足够的存储空间(由capacity()确定,可以容纳一个以上的元素,则传递给push_back的参数是 copy构建,如果使用{ {1}}是变量,还是使用push_back( const T& value )通过移动构造器从中移出。
  2. 如果没有更多的内存(即new size()>容量),则会分配更多的内存,足以容纳新元素。实现将分配多少内存。一种常见的模式是将向量以前具有的容量增加一倍,直到达到阈值为止,然后在存储桶中分配内存。您可以在插入元素之前使用push_back( T&& value ),以确保向量将具有足够的容量来容纳至少相同数量的元素而无需进行新分配。分配新内存后,向量将通过复制所有现有元素或将其移动到无法复制插入的方式将它们重新分配到新存储中。重新分配将使所有迭代器和对向量中元素的引用无效(注意:重新分配时将使用何时精确复制与移动的规则,但这有点复杂,但这是一般情况) < / li>

答案 1 :(得分:1)

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

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

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

您可以使用reservevector函数中设置getPoints的容量,消除三份副本

ret->reserve(2);

并通过在for的{​​{1}}循环中使用引用。

main

答案 2 :(得分:1)

1)我的理解正确吗?

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

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

[Ans]使用副本构造函数。请添加一个副本构造函数,您将看到区别。

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

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

相关问题