繁体   English   中英

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

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

请参考以下代码段。

据我了解:

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

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

我的问题是:

1)我的理解正确吗?

如果是这样的话 ;

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

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

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

}

问:我的理解正确吗?

答:您的理解部分正确。

  • p1和p2是使用已定义的默认无参数构造函数在堆栈上创建的。
  • 默认分配器用于P1和P2,当你调用的push_back(分配更多的内存),但不会一直做下去。 它永远不会创建默认构造Point的新实例。

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

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

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

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

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

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

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

工作原理:

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

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

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

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)我的理解正确吗?

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM