在C++中先声明对象再初始化的问题

89

在C++中是否可能声明变量而不实例化它?我想要做类似这样的事情:

Animal a;
if( happyDay() ) 
    a( "puppies" ); //constructor call
else
    a( "toads" );

基本上,我只是想在条件语句外声明一个变量 a,以便它获得正确的作用域。

有没有不使用指针并在堆上分配a的方法?也许可以使用引用实现巧妙的解决方案吗?


1
请看RAII(资源获取即初始化) - newacct
2
如果它是非静态的全局/命名空间范围,那么值得注意的是你可以声明而不初始化:extern Animal a; ... Animal a(stuff); - Johannes Schaub - litb
@newacct:提供一个链接会更好 https://dev59.com/WHE95IYBdhLWcg3watM3 - spinkus
11个回答

58

这里不能使用引用,因为一旦超出作用域,引用将指向一个已被删除的对象。

实际上,你有两个选择:

1- 使用指针:

Animal* a;
if( happyDay() ) 
    a = new Animal( "puppies" ); //constructor call
else
    a = new Animal( "toads" );

// ...
delete a;
或者使用智能指针。
#include <memory>

std::unique_ptr<Animal> a;
if( happyDay() ) 
    a = std::make_unique<Animal>( "puppies" );
else
    a = std::make_unique<Animal>( "toads" );

2- 为 Animal 添加一个初始化方法:

class Animal 
{
public:
    Animal(){}
    void Init( const std::string& type )
    {
        m_type = type;
    }
private:
    std:string m_type;
};

Animal a;
if( happyDay() ) 
    a.Init( "puppies" );
else
    a.Init( "toads" );

我个人会选择选项2。


4
我会选择选项#1。谢谢! - Neurotransmitter
4
如果选择选项1,应该使用unique_ptr - Chronial
我喜欢第二个选项。非常感谢 :) - herr_azad

48

你不能在不调用构造函数的情况下声明一个变量。但是,在你的例子中,你可以这样做:

Animal a(happyDay() ? "puppies" : "toads");

41

在C++中,由于对象是在使用默认构造函数定义时构造的,所以您无法直接这样做。

但是,您可以通过运行带参数的构造函数来开始:

Animal a(getAppropriateString());
或者你可以使用类似于 ?:运算符 的东西来确定正确的字符串。(更新:@Greg 给出了语法。请查看该答案)

2
这是解决方案的一般形式 - 将其包装在函数内部。(正如您所说,?:通常可以完成工作,并且在它能够完成工作时更加方便,但编写单独的函数将始终起作用。) - j_random_hacker
然而,如果您的构造函数需要接受多个参数,您会为每个参数编写一个函数吗? - newacct
有一些研究表明,最好不要使用具有多个参数的构造函数,而是使用默认值创建对象,然后使用setter方法。话虽如此,你可以为每个参数编写一个函数,甚至更好的做法是,如果这些参数相关,则创建一个中间结构体来表示构成参数的相关元素。 - Uri

18

我更倾向于Greg的答案,但你也可以这样做:

char *AnimalType;
if( happyDay() ) 
    AnimalType = "puppies";
else
    AnimalType = "toads";
Animal a(AnimalType);

我提出这个建议是因为我曾经工作过的某些地方禁止使用条件运算符。(叹气!)此外,这种方法可以非常容易地扩展到两个以上的选择。


8

如果您想避免垃圾回收,可以使用智能指针。

auto_ptr<Animal> p_a;
if ( happyDay() )
    p_a.reset(new Animal( "puppies" ) );
else
    p_a.reset(new Animal( "toads" ) );

// do stuff with p_a-> whatever.  When p_a goes out of scope, it's deleted.

如果你仍然希望使用.语法而不是->,那么你可以在上面的代码之后这样做:

Animal& a = *p_a;

// do stuff with a. whatever

这需要更改为auto_ptr<Animal> p_a(new Animal);否则auto_ptr只有一个空指针。虽然我喜欢第二个想法,因为它不会复制它 - 但您必须注意生命周期在该范围内。 - Natalie Adams
3
@NathanAdams,在这种情况下使用初始化为null的auto_ptr是可以的,因为它稍后将变成“puppies”或“toads”。多出来的“new Animal”是多余的。 - fxam

7
除了Greg Hewgill的回答外,还有其他几个选择:
将代码的主体提取到一个函数中:
void body(Animal & a) {
    ...
}

if( happyDay() ) {
  Animal a("puppies");
  body( a );
} else {
  Animal a("toad");
  body( a );
}

滥用placement new:

struct AnimalDtor {
   void *m_a;
   AnimalDtor(void *a) : m_a(a) {}
   ~AnimalDtor() { static_cast<Animal*>(m_a)->~Animal(); }
};

char animal_buf[sizeof(Animal)]; // still stack allocated

if( happyDay() )
  new (animal_buf) Animal("puppies");
else
  new (animal_buf) Animal("toad");

AnimalDtor dtor(animal_buf); // make sure the dtor still gets called

Animal & a(*static_cast<Animal*>(static_cast<void*>(animal_buf));
... // carry on

你知道在C++11版本之前,有没有一种方法可以确保使用placement new时正确的对齐方式吗? - enobayram

7
自C++17以来,现在有一种无额外开销的方法可以实现此操作:std::optional。在这种情况下,代码如下:
#include <optional>

std::optional<Animal> a;
if (happyDay()) {
    a.emplace("puppies");
} else {
    a.emplace("toads");
}

这可能是一个不错的解决方案。虽然它并不完全符合 optional 的预期用法(标记可能已经初始化或未初始化的值),但它确实避免了调用 a 的默认构造函数。 - Quantum7
请注意,optional确实有一个微小的开销来存储布尔值(在我的系统上为8字节)并初始化自身。 - Quantum7
以下是关于此(以及其他)解决方案的探讨,详见链接:https://gist.github.com/sbliven/359d180753febc4777ac79bb97685b5b - Quantum7
1
@Quantum7 一个好的编译器将完全优化掉可选项:https://godbolt.org/z/x9gncT - Chronial

3
最佳解决方案是使用指针。
Animal a*;
if( happyDay() ) 
    a = new Animal( "puppies" ); //constructor call
else
    a = new Animal( "toads" );

3
当使用 new 时,变量存储在堆中,应稍后删除。 - nathanfranke

1

有一种方法可以在不使用指针/堆内存的情况下完成这个操作,但这种语法有点晦涩难懂。以下是使用std::string的示例。除非您确实需要性能,否则我不建议这样做。

uint8_t object[sizeof(std::string)];

int main() {
    if(true)
        new(&object) std::string("Your arguments");
    else
        new(&object) std::string("Your other arguments");

    (*(std::string*)(&object)).append("");
    std::cout << (*(std::string*)(&object));
    return 0;
}

这个让人不爽的部分是每次你想使用它时都必须将对象转换为字符串:
(*(std::string*)(&object))

0
你也可以使用 std::move:
class Ball {
private:
        // This is initialized, but not as needed
        sf::Sprite ball;
public:
        Ball() {
                texture.loadFromFile("ball.png");
                // This is a local object, not the same as the class member.
                sf::Sprite ball2(texture);
                // move it
                this->ball=std::move(ball2);
        }
...

std::move在这里确实有潜力使用,但是这段代码并没有展示如何解决问题中的双重初始化。您能否重新编写您的答案以更紧密地跟随我的问题(例如实现一个Animal(char *)构造函数)?您还应该声明这需要C++11。 - Quantum7

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