11

当我创建对象的 std::vector 时,并不总是调用这些对象的构造函数。

#include <iostream>
#include <vector>
using namespace std;

struct C {
    int id;
    static int n;
    C() { id = n++; }   // not called
//  C() { id = 3; }     // ok, called
};

int C::n = 0;


int main()
{
    vector<C> vc;

    vc.resize(10);

    cout << "C::n = " << C::n << endl;

    for(int i = 0; i < vc.size(); ++i)
        cout << i << ": " << vc[i].id << endl;  
}

这是我得到的输出:

C::n = 1
0: 0
1: 0
2: 0
...

这就是我想要的:

C::n = 10
0: 0
1: 1
2: 2
...

在这个例子中,我是否被迫调整向量的大小,然后“手动”初始化它的元素?
原因可能是向量的元素没有以有序的方式初始化,从第一个到最后一个,所以我无法获得确定性的行为?

我想做的是轻松计算在程序中创建的对象的数量,在不同的容器中,在代码的不同点,并为每个对象提供一个 id。

谢谢!

4

4 回答 4

21

这些对象的构造函数并不总是被调用。

是的,它是,但它不是你认为的构造函数。成员函数resize()实际上是这样声明的:

void resize(size_type sz, T c = T());

第二个参数是要复制到向量的每个新插入元素中的对象。如果省略第二个参数,它默认构造一个类型的对象,T然后将该对象复制到每个新元素中。

在您的代码中,构造了一个临时C构造函数并调用了默认构造函数;id设置为 0。然后调用隐式声明的复制构造函数十次(将十个元素插入向量中),并且向量中的所有元素都具有相同的 id。

【有兴趣的注意:在C++03中,resize()( c)的第二个参数是取值;在 C++0x 中,它由 const 左值引用(参见LWG 缺陷 679)]。

在这个例子中,我是否被迫调整向量的大小,然后“手动”初始化它的元素?

您可以(并且可能应该)将元素单独插入向量中,例如,

std::vector<C> vc;
for (unsigned i(0); i < 10; ++i)
    vc.push_back(C());
于 2010-07-17T16:42:41.923 回答
5

原因是vector::resize通过调用自动提供的复制构造函数来插入副本,而不是您在示例中定义的构造函数。

为了得到你想要的输出,你可以显式定义复制构造函数:

struct C {
//....
C(const C& other) {
    id = n++;
    // copy other data members
}
//....
};

由于 vector::resize 的工作方式(它有第二个可选参数用作它创建的副本的“原型”,在您的情况下具有默认值C()),这在您的示例中创建了 11 个对象(“原型' 和 10 个副本)。

编辑(在许多评论中包含一些好的建议)

该解决方案有几个值得注意的缺点,以及一些可能产生更可维护和更合理的代码的选项和变体。

  • 这种方法确实增加了维护成本和一定的风险。每当您添加或删除类的成员变量时,您必须记住修改您的复制构造函数。如果您依赖默认的复制构造函数,则不必这样做。解决这个问题的一种方法是将计数器封装在另一个类中(像这样),这也可以说是更好的 OO 设计,但是当然你还必须记住多重继承可能会出现的许多问题

  • 它会使其他人更难理解,因为副本不再是大多数人所期望的。同样,处理您的类(包括标准容器)的其他代码可能行为不端。解决这个问题的一种operator==方法是为您的类定义一个方法(可能有人认为,即使您不使用该方法,在覆盖复制构造函数时这是一个好主意),以保持它在概念上“合理”并且作为一种内部文档。如果您的类得到大量使用,您最终可能还会提供一个operator=,以便您可以将自动生成的实例 id 与应该在此运算符下进行的类成员分配保持分离。等等 ;)

  • 如果您对程序有足够的控制权以使用动态创建的实例(通过新实例)并使用指向容器内实例的指针,它可能会消除“副本的不同 id 值”的整个问题。这确实意味着您需要在某种程度上“手动”初始化元素 - 但是编写一个函数来返回一个充满指向新的初始化实例的指针的函数并不是很多工作。如果您在使用标准容器时始终如一地处理指针,您就不必担心标准容器会在“幕后”创建任何实例。

如果您了解所有这些问题,并相信您可以应对后果(这当然高度依赖于您的特定上下文),那么覆盖复制构造函数是一个可行的选择。毕竟,语言功能的存在是有原因的。显然,它并不像看起来那么简单,你应该小心。

于 2010-07-17T16:42:50.690 回答
3

该向量正在使用 c++ 为您生成的复制构造函数,而无需询问。一个“C”被实例化,其余的从原型中复制。

于 2010-07-17T16:42:55.747 回答
0

@James:假设我必须能够区分每个对象,即使多个对象可以(暂时)具有相同的值。由于向量的重新分配,我不太信任它的地址。此外,不同的对象可以在不同的容器中。您提到的问题是否仅与遵循的约定有关,或者此类代码是否存在真正的技术问题?我做的测试效果很好。
这就是我的意思:

#include <iostream>
#include <vector>
#include <deque>
using namespace std;

struct C {
    int id;
    static int n;
    int data;

    C() {               // not called from vector
        id = n++;
        data = 123;
    }

    C(const C& other) {
        id = n++;
        data = other.data;
    }

    bool operator== (const C& other) const {
        if(data == other.data)      // ignore id
            return true;
        return false;
    }
};

int C::n = 0;


int main()
{
    vector<C> vc;
    deque<C> dc;

    vc.resize(10);

    dc.resize(8);

    cout << "C::n = " << C::n << endl;

    for(int i = 0; i < vc.size(); ++i)
        cout << "[vector] " << i << ": " << vc[i].id << ";  data = " << vc[i].data << endl;

    for(int i = 0; i < dc.size(); ++i)
        cout << "[deque] " << i << ": " << dc[i].id << ";  data = " << dc[i].data << endl;
}

输出:

C::n = 20
[vector] 0: 1;  data = 123
[vector] 1: 2;  data = 123
[vector] 2: 3;  data = 123
[vector] 3: 4;  data = 123
[vector] 4: 5;  data = 123
[vector] 5: 6;  data = 123
[vector] 6: 7;  data = 123
[vector] 7: 8;  data = 123
[vector] 8: 9;  data = 123
[vector] 9: 10;  data = 123
[deque] 0: 12;  data = 123
[deque] 1: 13;  data = 123
[deque] 2: 14;  data = 123
[deque] 3: 15;  data = 123
[deque] 4: 16;  data = 123
[deque] 5: 17;  data = 123
[deque] 6: 18;  data = 123
[deque] 7: 19;  data = 123
于 2010-07-18T00:27:53.787 回答