2013-10-17 22 views
4

我想了解C++的某些方面。在C++中返回自动局部对象

我写了这个短节目表现出用C从函数返回对象的不同方式++:

#include <iostream> 

using namespace std; 

// A simple class with only one private member. 
class Car{ 
    private: 
     int maxSpeed; 
    public: 
     Car(int); 
     void print(); 
     Car& operator= (const Car &); 
     Car(const Car &); 
}; 

// Constructor 
Car::Car(int maxSpeed){ 
    this -> maxSpeed = maxSpeed; 
    cout << "Constructor: New Car (speed="<<maxSpeed<<") at " << this << endl; 
} 

// Assignment operator 
Car& Car::operator= (const Car &anotherCar){ 
    cout << "Assignment operator: copying " << &anotherCar << " into " << this << endl; 
    this -> maxSpeed = anotherCar.maxSpeed; 
    return *this; 
} 

// Copy constructor 
Car::Car(const Car &anotherCar) { 
    cout << "Copy constructor: copying " << &anotherCar << " into " << this << endl; 
    this->maxSpeed = anotherCar.maxSpeed; 
} 

// Print the car. 
void Car::print(){ 
    cout << "Print: Car (speed=" << maxSpeed << ") at " << this << endl; 
} 

// return automatic object (copy object on return) (STACK) 
Car makeNewCarCopy(){ 
    Car c(120); 
    return c; // object copied and destroyed here 
} 

// return reference to object (STACK) 
Car& makeNewCarRef(){ 
    Car c(60); 
    return c; // c destroyed here, UNSAFE! 
    // compiler will say: warning: reference to local variable ‘c’ returned 
} 

// return pointer to object (HEAP) 
Car* makeNewCarPointer(){ 
    Car * pt = new Car(30); 
    return pt; // object in the heap, remember to delete it later on! 
} 

int main(){ 
    Car a(1),c(2); 
    Car *b = new Car(a); 
    a.print(); 
    a = c; 
    a.print(); 

    Car copyC = makeNewCarCopy(); // safe, but requires copy 
    copyC.print(); 

    Car &refC = makeNewCarRef(); // UNSAFE 
    refC.print(); 

    Car *ptC = makeNewCarPointer(); // safe 
    if (ptC!=NULL){ 
     ptC -> print(); 
     delete ptC; 
    } else { 
     // NULL pointer 
    } 
} 

的代码似乎并没有崩溃,我得到下面的输出:

Constructor: New Car (speed=1) at 0x7fff51be7a38 
Constructor: New Car (speed=2) at 0x7fff51be7a30 
Copy constructor: copying 0x7fff51be7a38 into 0x7ff60b4000e0 
Print: Car (speed=1) at 0x7fff51be7a38 
Assignment operator: copying 0x7fff51be7a30 into 0x7fff51be7a38 
Print: Car (speed=2) at 0x7fff51be7a38 
Constructor: New Car (speed=120) at 0x7fff51be7a20 
Print: Car (speed=120) at 0x7fff51be7a20 
Constructor: New Car (speed=60) at 0x7fff51be79c8 
Print: Car (speed=60) at 0x7fff51be79c8 
Constructor: New Car (speed=30) at 0x7ff60b403a60 
Print: Car (speed=30) at 0x7ff60b403a60 

现在,我有以下问题:

  • 是makeNewCarCopy安全吗?本地对象是否在函数结束时被复制和销毁?如果是这样,为什么不调用重载赋值操作符?它是否调用默认的拷贝构造函数?
  • 我的胆量告诉我使用makeNewCarPointer作为从C++函数/方法返回对象的最常用方式。我对吗?
+2

1)你得到复制elision。 2)不,最自然的方式是按价值回报。 – juanchopanza

回答

5

makeNewCarCopy是否安全?在本函数结束时是否将本地对象复制并销毁为 ?如果是这样,为什么不调用超载 赋值运算符?它是否调用默认的拷贝构造函数?

重要这里的问题是“是makeNewCarCopy安全吗?”这个问题的答案是,“是的”。您正在制作对象的副本并按值返回该副本。你不会试图返回一个本地自动对象的引用,这是新手们常见的陷阱,这很好。

这个问题的其他部分的答案在哲学上不太重要,尽管一旦你知道如何安全地做到这一点,它们可能在生产代码中变得至关重要。你可能会也可能不会看到本地对象的构建和破坏。事实上,你可能不会,特别是在开启优化编译时。原因是因为编译器知道你正在创建一个临时的并返回它,而这又被复制到其他地方。在某种意义上,临时变得毫无意义,因此编译器跳过了整个令人烦恼的创建 - 复制 - 破坏步骤,并简单地在其最终预期的变量中构建新的副本直接。这被称为copy elision。即使复制构造函数具有副作用(请参阅:Return Value Optimization),只要可观察行为与未做任何更改(请参阅:As-If Rule)相同,编译器就可以对程序进行任何和所有更改。

我的胆量告诉我使用makeNewCarPointer作为从C++函数/方法返回对象的最常用方式。我对吗?

否。考虑复制elision,如上所述。所有当代主要的编译器都实现了这种优化,并且做得非常好。因此,如果您可以像副本指针那样有效地(至少)按值副本进行副本复制,那么在性能方面复制副指针是否有任何好处?

答案是否定的。现在,除非您有引人注意,不需要,否则您通常希望按值返回。在那些令人信服的需求中,当你需要返回的对象比它创建时的“范围”更大时 - 但其中不是,其中就是性能。事实上,动态分配在时间上可能比自动分配(即“堆栈”)分配显着更为昂贵。

+0

“返回值优化”实际上是允许编译器打破“asf”规则的一种情况。考虑一下'Car x = makeNewCarCopy();'这是一个明确的拷贝,“as if”规则不允许删除。 – Fozi

+0

@Fozi:呃,我不确定。 as-if规则规定编译器可以对程序进行任何更改,只要结果程序的可观察行为与未做任何更改相同即可。一般来说,缺乏复制并不是一种可观察到的行为。 –

+0

是的,但这里的预期行为是复制构造函数被调用。返回值优化明确地将编译器从这个义务中解放出来。请参阅我的答案中链接的Wiki文章。 – Fozi

1

是的,makeNewCarCopy是安全的。从理论上讲,当函数退出时将会创建一个副本,但是由于return value optimization允许编译器删除副本。

在实践中,这意味着makeNewCarCopy将具有是基准的隐藏第一参数的未初始化Car和内部makeNewCarCopy构造函数调用实际上将初始化驻留在函数的堆栈帧外的Car实例。

至于你的第二个问题:返回一个必须被释放的指针不是首选的方法。这是不安全的,因为函数如何分配Car实例的实现细节被泄漏出去,并且调用者负担清理工作。如果您需要动态分配,那么我建议您返回std::shared_ptr<Car>

+1

'std :: unique_ptr '通常比'std :: shared_ptr '更好。 – Yakk

+0

@Yakk我发现'unique_ptr'是不必要的限制,它唯一更好的是性能。我从来没有遇到过这样的情况:“我真的希望我在这里使用'unique_ptr'!”。如果它永远不会离开类/模块,我只使用'unique_ptr',这在这里不是这种情况。 – Fozi

+0

'shared_ptr'的问题是,这个生命周期和所有权现在不清楚。在几乎所有情况下,一个对象应该由另一个数据结构拥有:当一个对象拥有多于一个数据结构时,您倾向于获得与意大利面代码相同的资源管理。 'shared_ptr'表示对象的生命周期的共享管理和所有权,这是一个特例,不应该是你的默认值。是的,它可以让你做更多的事情比'unique_ptr',但其中许多是坏习惯! – Yakk

1
  • 是的makeNewCarCopy是安全的。并且在大多数情况下,它是有效的,因为编译器可以执行某些优化,如copy elision(以及您未看到赋值运算符或复制命令的原因)和/或由C++ 11添加的可能非常有效,但在同一时间是非常危险的。问题是你可以很容易地忽略返回值,编译器不会产生任何警告。所以至少你应该返回智能指针,如std::unique_ptrstd::shared_ptr。但恕我直言之前的方法是更受欢迎的,至少不会更慢。不同的故事,如果你不得不根据不同的原因在堆上创建对象。