如何调用std :: vector中包含的对象的构造函数?

时间:2010-07-17 16:36:52

标签: c++ stl static constructor stdvector

当我创建一个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 个答案:

答案 0 :(得分:21)

  

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

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

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

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

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

[注意那些感兴趣的人:在C ++ 03中,resize()c)的第二个参数是按值获取的;在C ++ 0x中,它由const lvalue引用(参见LWG Defect 679)]。

  

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

您可以(也可能应该)将元素单独插入到矢量中,例如,

std::vector<C> vc;
for (unsigned i(0); i < 10; ++i)
    vc.push_back(C());

答案 1 :(得分:5)

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

为了获得所需的输出,您可以明确定义复制构造函数:

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

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

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

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

  • 此方法确实增加了维护成本和风险。每当添加或删除类的成员变量时,您都必须记住修改复制构造函数。如果依赖于默认的复制构造函数,则不必执行此操作。解决这个问题的一种方法是将计数器封装在另一个类(like this)中,这也可以说是更好的OO设计,但当然你还要记住the many issues that can crop up with multiple inheritance

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

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

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

答案 2 :(得分:3)

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

答案 3 :(得分:0)

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

#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