没有默认构造函数的对象数组初始化

93
#include <iostream>
class Car
{
private:
  Car(){};
  int _no;
public:
  Car(int no)
  {
    _no=no;
  }
  void printNo()
  {
    std::cout<<_no<<std::endl;
  }
};
void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i<length;i++)
         std::cout<<cars[i].printNo();
}

int main()
{
  int userInput = 10;
  Car *mycars = new Car[userInput];
  for(int i =0;i < userInput;i++)
         mycars[i]=new Car[i+1];
  printCarNumbers(mycars,userInput);
  return 0;
}    

我想创建一个汽车数组,但是却遇到了如下错误:

cartest.cpp: In function ‘int main()’:
cartest.cpp:5: error: ‘Car::Car()’ is private
cartest.cpp:21: error: within this context

有没有办法在不将Car()构造函数公开的情况下进行初始化?


operator new []总是调用默认构造函数。但是C++11有一个解决方案,如下所示:链接 - rustyx
15个回答

83
你可以这样使用placement-new:

class Car
{
    int _no;
public:
    Car(int no) : _no(no)
    {
    }
};

int main()
{
    void *raw_memory = operator new[](NUM_CARS * sizeof(Car));
    Car *ptr = static_cast<Car *>(raw_memory);
    for (int i = 0; i < NUM_CARS; ++i) {
        new(&ptr[i]) Car(i);
    }

    // destruct in inverse order    
    for (int i = NUM_CARS - 1; i >= 0; --i) {
        ptr[i].~Car();
    }
    operator delete[](raw_memory);

    return 0;
}

摘自《More Effective C++》 - Scott Meyers:
条款4 - 避免不必要的默认构造函数


13
因为它是按正序构建的,所以最后一个被构建的东西应该是第一个被销毁的。 - Thomas Eding
7
如何使这个解决方案具有异常安全性?例如,如果Car的构造函数对某个i抛出异常,那怎么办?我想必须捕获异常,在相反的顺序中解构已经构建的所有Car,并重新抛出... - ingomueller.net
7
如果我只是在结尾处执行 delete [] ptr;,会发生什么? - Youda008
1
根据对VS 2015的实验,这将导致无限循环。 - cqdjyy01234
2
malloc和operator new返回的内存位置保证适当对齐,以便可以将其转换为任何完整对象的指针并作为对象数组访问。换句话说,在这里没有对齐问题。 - vbraun
显示剩余10条评论

54

不行。

但是!如果你使用像应该做的那样 std::vector<Car>(绝对不要使用 new[]),那么你可以指定元素应该如何构造*。

*好吧,有点。你可以指定要复制的值。


就像这样:

#include <iostream>
#include <vector>

class Car
{
private:
    Car(); // if you don't use it, you can just declare it to make it private
    int _no;
public:
    Car(int no) :
    _no(no)
    {
        // use an initialization list to initialize members,
        // not the constructor body to assign them
    }

    void printNo()
    {
        // use whitespace, itmakesthingseasiertoread
        std::cout << _no << std::endl;
    }
};

int main()
{
    int userInput = 10;

    // first method: userInput copies of Car(5)
    std::vector<Car> mycars(userInput, Car(5)); 

    // second method:
    std::vector<Car> mycars; // empty
    mycars.reserve(userInput); // optional: reserve the memory upfront

    for (int i = 0; i < userInput; ++i)
        mycars.push_back(Car(i)); // ith element is a copy of this

    // return 0 is implicit on main's with no return statement,
    // useful for snippets and short code samples
} 

具有以下附加功能:

void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i < length; i++) // whitespace! :)
         std::cout << cars[i].printNo();
}

int main()
{
    // ...

    printCarNumbers(&mycars[0], mycars.size());
} 

请注意:printCarNumbers 函数应该设计成接受两个迭代器,表示一个范围。


3
这实际上满足我的需求,但使用向量不是选项。它必须是Car,因为我有另一个接受*Car而非std::vector<Car>的方法。 - Dan Paradox
4
@Dan:你可以使用&mycars[0]来获取指向底层数组的指针。虽然这通常只对传递给不使用std::vector的旧代码有用,但如果可能的话,你应该更新这个要求。 - GManNickG
13
嘿,伙计们,我们现在有了C++11,不要再用&mycars[0]了,让我们用等效的mycars.data()吧,这样更易读。 - Russell Greene
10
虽然我不是专家,但是我对“永远不要使用new[]”这个说法给出负面评价。它存在的原因是有其合理性的。 - Matias Chara
当你想要在堆栈中初始化数组时,它就无法正常工作。你可以使用一些黑科技来创建一个未初始化值的数组,然后手动初始化它们,但否则很难实现。 - JoaoBapt
显示剩余8条评论

26

你可以创建一个指针数组。

Car** mycars = new Car*[userInput];
for (int i=0; i<userInput; i++){
    mycars[i] = new Car(...);
}

...

for (int i=0; i<userInput; i++){
    delete mycars[i];
}
delete [] mycars;

Car() 构造函数不需要是 public 的。在你的类中添加一个静态方法来构建一个数组:

static Car* makeArray(int length){
    return new Car[length];
}

这通常比放置 new 选项更干净,但是如果代码需要 "Car *" 呢? - miniBill

9
在C++11的std::vector中,您可以使用emplace_back就地实例化元素:
  std::vector<Car> mycars;

  for (int i = 0; i < userInput; ++i)
  {
      mycars.emplace_back(i + 1); // pass in Car() constructor arguments
  }

看这里!

Car() 默认构造函数未被调用。

mycars 超出作用域时,将自动进行删除。


我遇到了与 OP 类似的问题,但是我的 mycars 包含一个引用,因此我无法使用 emplace_back(因为它可能需要使用赋值运算符,而引用是 const,所以不存在赋值运算符)。所以,回到 OP 的原始问题,有没有办法在不使用默认构造函数的情况下实例化数组中的每个元素? - mbrandalero
为什么你需要对引用数组进行构造函数调用? - PapaDiHatti

4

没有人评论过使用分配器来完成这个任务的可能性。

#include <iostream>
#include <memory>

class Car
{
private:
  Car(){};
  int _no;
public:
  Car(int no)
  {
    _no=no;
  }
  void printNo()
  {
    std::cout<<_no<<std::endl;
  }
};

void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i<length;i++)
        (cars+i)->printNo();
}

int main()
{
  int userInput = 10;

  std::allocator<Car> carAllocator;

  // reserves space in memory for 10 car objects, but not construct them
  Car *myCars = carAllocator.allocate(10); 
  Car *myCarsBegin = myCars; // begin of array myCars

  for(int i =0; i < userInput; i++ ){
      // effectively creates the class "Car" and initializes it
      // myCars now points to the first car created
      carAllocator.construct( myCars, i );
      ++myCars;    
  }
  
  printCarNumbers(myCarsBegin,userInput);

  // destroy the objects created
  for( Car *carIterator = myCarsBegin; carIterator != myCars; ++carIterator )
      carAllocator.destroy( carIterator );

  return 0;
}

1
我不知道为什么这里没有点赞,这是一个简单而好的答案。 - Holy semicolon

4

好问题。我曾经也有同样的疑问,并在这里找到了答案。实际上,@Dan-Paradox,没有标准的语法方式来解决这个问题。因此,所有这些答案都是绕过问题的各种替代方法。

我自己看了这些答案,没有特别完美符合我个人惯例的。我可能会采用使用默认构造函数和 set 方法的方法:

class MyClass
{
  int x,y,z;
public:
  MyClass(): x(0), y(0), z(0) {}
  MyClass(int _x,int _y,int _z): x(_x), y(_y), z(_z) {} // for single declarations
  void set(int _x,int _y,int _z)
  {
    x=_x;
    y=_y;
    z=_z;
  }
};

标准初始化构造函数仍然存在,因此如果我不需要多个初始化,则仍然可以正常进行初始化,但是如果需要更多,则有一个set方法可设置在构造函数中初始化的所有变量。因此,我可以像这样做:

int len = 25;
MyClass list = new MyClass[len];
for(int i = 0; i < len; i++)
  list[i].set(1, 2, 3);

这样做很好,自然流畅,不会使代码看起来混乱。


现在这就是我对于那些想声明一个需要初始化对象数组的人的答案。

针对你的情况,你正在尝试给汽车数组分配身份,我想你希望它们始终是唯一的。你可以使用我上面解释的方法,并在for循环中使用i+1作为传递给set方法的参数,但根据我阅读您的评论,似乎您希望id更多地被内部初始化,因此默认情况下每个Car都具有唯一的id,即使其他人使用您的类 Car 。

如果这是你想要的,你可以使用静态成员:

class Car
{
  static int current_id;
  int id;
public:
  Car(): id(current_id++) {}

  int getId() { return id; }
};
int Car::current_id = 1;

// ...

int cars=10;
Car* carlist = new Car[cars];

for(int i = 0; i < cars; i++)
  cout << carlist[i].getId() << " "; // prints "1 2 3 4 5 6 7 8 9 10"

这样一来,您完全不必担心发起身份,因为它们是在内部管理的。


2
你通过使用静态存储标识循环计数器使您的类非线程安全。 如果它们只需要在每个集合内保持唯一,则使用局部变量进行循环是更为合理和高效的选择。 在C ++中可能没有一个干净的方法来表达这一点,特别是在构造函数中隐藏增量的情况下。 - Peter Cordes

4

不,没有这样的方法。新表达式只允许默认初始化或完全不进行初始化。

解决方法是使用operator new[]分配原始内存缓冲区,然后使用带有非默认构造函数的placement-new在该缓冲区中构造对象。


2
当然,我们会使用std::vector来实现这个功能。但是对于正确性加一分。 - GManNickG
如何使用operator new[]来分配原始内存?有没有任何解释它的链接? - Dan Paradox
@Dan Paradox:Chan的回答中有一个例子。 - AnT stands with Russia
我认为new-expression的数组形式只允许默认初始化,A *a = new A(args);是可以的。此外,这个答案可能需要更新到C++11。 - M.M

3

您可以始终创建一个指向汽车对象的指针数组,然后按照需要在for循环中创建对象,并将它们的地址保存在数组中,例如:

#include <iostream>
class Car
{
private:
  Car(){};
  int _no;
public:
  Car(int no)
  {
    _no=no;
  }
  void printNo()
  {
    std::cout<<_no<<std::endl;
  }
};
void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i<length;i++)
         std::cout<<cars[i].printNo();
}

int main()
{
  int userInput = 10;
  Car **mycars = new Car*[userInput];
  int i;
  for(i=0;i<userInput;i++)
      mycars[i] = new Car(i+1);

注意新方法!!!

  printCarNumbers_new(mycars,userInput);


  return 0;
}    

在新方法中,你所需要改变的是处理汽车指针而不是静态对象作为参数,并且在调用printNo()方法时。

void printCarNumbers_new(Car **cars, int length)
{
    for(int i = 0; i<length;i++)
         std::cout<<cars[i]->printNo();
}

在 main 函数结束时,最好像这样删除所有动态分配的内存。
for(i=0;i<userInput;i++)
  delete mycars[i];      //deleting one obgject
delete[] mycars;         //deleting array of objects

希望我能帮到你,加油!

2

解决这个问题的一种方法是提供一个静态工厂方法来分配数组,如果出于某些原因您想将构造函数设置为私有。

static Car*  Car::CreateCarArray(int dimensions)

但是为什么要将一个构造函数设为公有,另一个设为私有呢?

但无论如何,还有一种方法是使用默认值声明公有构造函数。

#define DEFAULT_CAR_INIT 0
Car::Car(int _no=DEFAULT_CAR_INIT);

我将Car()设为私有,因为我想避免复制构造函数。 - Dan Paradox
3
@Dan:但那不是一个复制构造函数。 - GManNickG
@GMan 你说得对,但是我无论如何都想避免默认构造函数。所有的汽车都必须有一个id。 - Dan Paradox
@Dan,在这种情况下,您可以使用具有尺寸和初始化列表的工厂方法。工厂方法将负责创建数组并为每个对象初始化ID。初始化函数也可以是私有的。 - Neera

2
首先我想澄清一下,您的代码中在printCarNumbers函数中存在一个错误,您试图使用std::cout将空值发送到标准输出,如下所示:
void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i < length; i++)
         std::cout << cars[i].printNo();
}

既然printNo()函数是用来打印的,那么直接调用即可:

for(int i = 0; i < length; i++)
   cars[i].printNo();

让我们回到主题,你正在尝试使用new来分配一个对象数组,就像这样:

Car *mycars = new Car[userInput];

使用这种语法,实际上您正在尝试分配一个对象数组,其大小为userInput(这就是我们想要的),但问题在于它尝试调用每个对象的默认构造函数,并且默认构造函数被声明为私有,因此它找不到它,这就是为什么您会得到那个错误:

cartest.cpp:5: error: ‘Car::Car()’ is private

相反,你需要以这种方式执行:
Car *mycars = (Car*) ::operator new (sizeof(Car));
// allocates memory by calling: operator new (sizeof(Car))
// but does not call Car's constructor

如评论中所述,以这种方式调用new会为您分配内存,而不调用默认构造函数。有关更多详细信息,请查看new operator

现在,如果您想调用参数化构造函数,则需要为每个对象单独调用它,如下所示:

for(int i =0; i < userInput; i++)
    new (&mycars[i]) Car(i + 1);  // does not allocate memory -- calls: operator new (sizeof(Car), &mycars[i])
                                  // but constructs an object at mycars[i]

你可能会感到困惑,因为我们再次调用了 new,但是这种新的语法并没有分配任何内存,它只是调用了索引对象的构造函数。

以下是完整的可运行代码,供任何人测试:

#include <iostream>

class Car
{
    private:
        Car(){};
        int _no;
    public:
        Car(int no)
        {
          _no=no;
        }
        void printNo()
        {
          std::cout << _no << std::endl;
        }
};

void printCarNumbers(Car *cars, int length)
{
    for(int i = 0; i < length; i++)
        cars[i].printNo();
}



int main()
{
  int userInput = 10;

  Car *mycars = (Car*) ::operator new (sizeof(Car));

  for(int i =0;i < userInput;i++)
    new (&mycars[i]) Car(i+1);

  printCarNumbers(mycars,userInput);

  return 0;
}

我知道我来晚了,但也许有人会觉得这很有用。如果有任何错误的陈述,请随时纠正我。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接