【问题标题】:C++, how to correctly copy std::vector<Class *> in copy constructor?C++,如何在复制构造函数中正确复制 std::vector<Class *>?
【发布时间】:2023-03-31 13:04:01
【问题描述】:

我正在使用这两个类

// This is generic data structure containing some binary data
class A {
public:
    A();
    A(const A&);
    ~A();
}

// Main data container
class B {
public:
    B();
    B( const B&);
    ~B();
protected:
    std::vector<A *> data;
}

// Copy constructor for class b
B::B( const B& orig):data() {
    for( std::vector<A *>::const_iterator it = orig.data.begin();
        it < orig.data.end(); ++it){
        data.push_back( new A( *(*it)));
    }
}

我想这门课会做它的工作,但我正在寻找一种达到完美的方法。

首先,:data() - 这个初始化是否需要正确初始化空向量(是干净的代码)吗?

vector::iterator 在复制构造函数中是如何使用的?我发现的唯一方法是我已经写入代码的方法(复制构造函数应该强制使用 const)。

复制向量是否复制指针值而不是整个对象?

最后是新数据初始化...有没有办法用更少的代码替换整个循环和/或是否有任何标准如何为包含对象指针的 std::containers 编写复制构造函数?

子问题:我假设使用vector&lt;A *&gt;vector&lt;A&gt; 更适合和有效的各种原因(不是每次都复制,有权决定是否(不)复制对象......)这是假设正确吗?

【问题讨论】:

  • 您的意思是“子问题:我假设使用指针向量...”
  • 在初始化列表中预分配data。像这样使用push_back() 是非常无效的。
  • 子回答:我认为,如果你可以不使用指针,你不应该使用它们。
  • 我从未使用过 boost,但我确信它有解决这个问题的方法(它总是有解决看似常见问题的方法)。
  • 正如公认的答案所示,正确处理所有异常是一件痛苦的事情。智能指针向量或对象std::deque 通常更好。 (另外,对于小对象,指针向量通常比对象向量慢;太多的间接和对 new/delete 的调用)

标签: c++ vector containers copy-constructor


【解决方案1】:

data() 不是必需的,因为这将在输入构造函数之前自动对向量进行。您只需要初始化 POD(普通旧数据)类型或没有默认构造函数(或引用、常量等)的类型的成员。

您可以使用另一个向量的元素数来初始化向量,这样向量就不必在增长时自行调整大小。如果不这样做,您将从一个小向量开始,并通过分配和重新分配使其逐渐达到目标大小。这将使向量从一开始就具有正确的大小:

B::B(const B& orig) : data(orig.data.size()) {
    for (std::size_t i = 0; i < orig.data.size(); ++i)
        data[i] = new A(*orig.data[i]);
}

请注意,您不再使用push_back,因为向量已经充满了orig.data.size() 默认构造的元素数量(在指针的情况下为NULL)。

这也减少了代码,因为您可以使用整数而不是迭代器来迭代它。

如果你真的想使用迭代器,你可以这样做

B::B(const B& orig) : data(orig.data.size()) {
    // auto is preferable here but I don't know if your compiler supports it
    vector<A*>::iterator thisit = data.begin();
    vector<A*>::const_iterator thatit = orig.data.cbegin();

    for (; thatit != orig.data.cend(); ++thisit, ++thatit)
        *thisit = new A(**thatit);
}

这样做的好处是它可以与其他容器类型(如list)一起使用,只需更改迭代器的类型(当然,如果你有auto,这将消失)。

如果你想添加异常安全,你需要一个try/catch 块:

B::B(const B& orig) : data(orig.data.size()) {
    try {
        // auto is preferable here but I don't know if your compiler supports it
        vector<A*>::iterator thisit = data.begin();
        vector<A*>::const_iterator thatit = orig.data.cbegin();

        for (; thatit != orig.data.cend(); ++thisit, ++thatit)
            *thisit = new A(**thatit);
    } catch (...) {
        for (vector<A*>::iterator i = data.begin(); i != data.end(); ++i)
            if (!*i)
                break;
            else
                delete *i;

        throw;
    }
}

这样,如果new 调用之一引发异常,您将不会发生内存泄漏。当然,如果您愿意这样做,您可以使用 try/catch 以及不使用迭代器的方式。

【讨论】:

  • 你需要取消引用orig.data[i]
  • 要考虑的另一件事是错误处理:如果news 之一在循环中间失败会发生什么?程序可能会终止,但如果在调用堆栈中进一步处理内存不足错误,则会出现内存泄漏。
  • @EmilStyrke 请查看我的最新编辑,看看是否能处理您所说的情况。
  • @Seth Carnegie:如果*i 是一个空指针(在这种情况下你会跳出循环),你只需要添加一点检查:)
  • @someguy 实际上在空指针上调用delete 被定义为什么都不做。不知道为什么我将 *i 设置为 null,当时似乎是个好主意 :) 至少现在我可以摆脱花括号了。
【解决方案2】:

在 C++11 十年之后,您应该能够利用它的特性。 reserve() 预先分配足够的空间并且仍然能够push_backemplace_back,尽管对于普通指针这并不重要。和range-based for loop 避免显式迭代器

B::B(const B &orig) {
    data.reserve(orig.data.size());
    for (auto p : orig.data)
        data.push_back(new A(*p));
}

最后,您可以使用std::unique_ptr 来确保安全并避免内存泄漏和提供显式析构函数的需要

class B {
public:
    B();
    B(const B&);
protected:
    std::vector<std::unique_ptr<A> > data;
};

构造函数更改为

B::B(const B &orig) {
    data.reserve(orig.data.size());
    for (auto &p : orig.data)
        data.emplace_back(new A(*p));
}

【讨论】:

    猜你喜欢
    • 2012-11-03
    • 2018-03-12
    • 1970-01-01
    • 2020-08-06
    • 2011-04-27
    • 2020-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多