5

我试图了解 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++ 函数/方法返回对象的最常用方法。我对吗?
4

3 回答 3

7

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

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

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

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

不。考虑复制省略,正如我在上面描述的那样。所有当代的主要编译器都实现了这种优化,并且做得很好。因此,如果您可以(至少)像按指针复制一样有效地按值复制,那么按指针复制在性能方面有什么好处吗?

答案是不。如今,您通常希望按值返回,除非您有迫切的需要。在这些引人注目的需求中,您需要返回的对象比创建它的“范围”更长——但性能不在其中。事实上,动态分配在时间方面可能比自动(即“堆栈”)分配更昂贵。

于 2013-10-17T15:09:18.950 回答
3

是的,makeNewCarCopy很安全。理论上,函数退出时会生成一个副本,但是由于返回值优化,编译器可以删除该副本。

实际上,这意味着makeNewCarCopy将有一个隐藏的第一个参数,它是对未初始化的引用,Car并且内部的构造函数调用makeNewCarCopy实际上将初始化Car驻留在函数堆栈框架之外的实例。

至于你的第二个问题:返回一个必须被释放的指针不是首选方式。这是不安全的,因为函数分配Car实例的实现细节被泄露了,调用者有责任清理它。如果您需要动态分配,那么我建议您返回一个std::shared_ptr<Car>

于 2013-10-17T15:08:05.817 回答
1
  • makeNewCarCopy的很安全。在大多数情况下,它是有效的,因为编译器可以进行某些优化,例如复制省略(以及您看不到赋值运算符或调用复制 ctor 的原因)和/或移动C++11 添加的语义
  • makeNewCarPointer可以很有效,但同时也很危险。问题是你可以很容易地忽略返回值并且编译器不会产生任何警告。所以至少你应该像std::unique_ptror一样返回智能指针std::shared_ptr。但恕我直言,以前的方法更受欢迎,至少不会慢。如果您必须出于不同的原因在堆上创建对象,则情况不同。
于 2013-10-17T15:11:09.613 回答