天天看點

c++類臨時對象、複制構造函數、析構函數 VS2017 與gcc 編譯器對比

在看《C和C++程式員面試秘籍》的時候,看到第6章28與29條,有些疑惑:

  • 作者說函數調用時,參數傳遞進來,以及傳回時,都會構造臨時對象。

    也就是說,會有兩個臨時對象被建立,一個是在參數傳遞進來的時候,一個是在傳回的時候。

先說實驗結論:

  • 書中的代碼在 VS2017中實測沒有問題,輸出結果完全一緻;
  • 但在 GCC 下,代碼有問題,輸出結果有一些不一緻(即:g++ 編譯器在函數傳回時,不一定建立臨時變量;且不接受臨時建構的類對象作為複制構造函數的實參)。

表述的可能不嚴謹,實驗如下:

先寫一個用來測試的類,其所有的構造函數,指派operator等,隻做基本的資料指派,和輸出資訊:

myclass.h

内容:

#ifndef _MY_CLASS_
#define _MY_CLASS_

#include <iostream>
#include <string>
namespace myclass {

class A {
public:
  // 預設構造函數
  A() : data_(), name_("default") {
    count_++;
    std::cout << "default constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }
  // 含參構造函數
  A(int data, std::string name = "default") : data_(data), name_(name) {
    count_++;
    std::cout << "param constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }
  // 複制構造函數
  A(A &other) {
    count_++;
    data_ = other.data_;
    name_ = other.name_ + "_copy";
    std::cout << "copy constructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }

  // 下面是右值複制構造函數,沒有這個函數,面試題28在 g++ 編譯器下無法編譯通過,會輸出如下錯誤資訊:
  // error: invalid initialization of non-const reference of type 'myclass::A&' from an rvalue of type 'myclass::A'
  //   A t1 = Play(5);
  // 意思是臨時類對象(右值)無法用來初始化類的左值引用(即:A&)
  // 但在VS2017下,沒有這個右值複制構造函數,是可以編譯通過的,會調用上面定義的複制構造函數

  A(A &&other) {
    count_++;
    data_ = other.data_;
    name_ = other.name_ + "_copy";
    std::cout << "copy [rvalue] constructor, data: " << data_
              << ", name: " << name_ << ", count: " << count_ << std::endl;
  }

  A &operator=(const A &a);

  // name_ setter and getter
  const std::string &name() { return name_; }
  void set_name(const std::string &name) { name_ = name; }
  // destructor
  virtual ~A() {
    std::cout << "destructor, data: " << data_ << ", name: " << name_
              << ", count: " << count_ << std::endl;
  }

  static int count_;

private:
  int data_;
  std::string name_;
};

int A::count_ = ;

A &A::operator=(const A &a) {
  std::cout << "operator = function, before assignment data: " << data_
            << ", name:" << name_ << std::endl;
  if (this == &a) {
    std::cout << "                   afeter assignment[same obj] data: "
              << data_ << ", name:" << name_ << std::endl;
    return *this;
  }

  data_ = a.data_;
  name_ = a.name_;
  std::cout << "                     afeter assignment data: " << data_
            << ", name:" << name_ << std::endl;
  return *this;
}

} // namespace myclass

#endif
           

A

裡面的資料隻有一個

data_

和一個

name_

,和一個靜态變量

count_

, 它們主要用來輸出的時候檢視是哪一個類被建立和析構。

面試題28:

主函數(

main_28.cpp

):

#include <iostream>
#include <string>
#include "myclass.h"

using namespace std;
using namespace myclass;

A Play(A a) { return a; }

int main(int argc, char *argv[]) {
  A t1 = Play();
  cout << "t1.name = " << t1.name() << endl;
  t1.set_name("t1");

  A t2 = Play(t1);
  cout << "t2.name = " << t2.name() << endl;
  t2.set_name("t2");

  return ;
}
           

運作結果如下:

  • VS2017 輸出

param constructor, data: 5, name: default, count: 1

copy [rvalue] constructor, data: 5, name: default_copy, count: 2

destructor, data: 5, name: default, count: 2

t1.name = default_copy

copy constructor, data: 5, name: t1_copy, count: 3

copy [rvalue] constructor, data: 5, name: t1_copy_copy, count: 4

destructor, data: 5, name: t1_copy, count: 4

t2.name = t1_copy_copy

destructor, data: 5, name: t2, count: 4

destructor, data: 5, name: t1, count: 4

  • g++ 6.2.0 輸出:

    param constructor, data: 5, name: default, count: 1

    copy [rvalue] constructor, data: 5, name: default_copy, count: 2

    destructor, data: 5, name: default, count: 2

    t1.name = default_copy

    copy constructor, data: 5, name: t1_copy, count: 3

    copy [rvalue] constructor, data: 5, name: t1_copy_copy, count: 4

    destructor, data: 5, name: t1_copy, count: 4

    t2.name = t1_copy_copy

    destructor, data: 5, name: t2, count: 4

    destructor, data: 5, name: t1, count: 4

可以看到,結果完全一樣。

其中,第2行輸出說明:臨時變量在傳入Play() 函數時,确實利用copy構造函數構造了臨時類對象(name:

default_copy

),但是在函數傳回時,并沒有構造另一個臨時變量

default_copy_copy

(也就是說這次函數調用,隻有在參數傳入時,有臨時變量生成,函數傳回并沒有生成另一個臨時變量)。用t1指派給t2時,也是如此。

面試題29:

我們再看面試題29,情況就不一樣了,在函數傳回時,VS2017确實生成了一個臨時類對象,而g++依然沒有生成。

主函數(

main_29.cpp

):

#include <iostream>
#include "myclass.h"

using namespace std;
using namespace myclass;

A fun() {
  A a;
  return a;
}

int main(int argc, char *argv[]) {
  {
    A a();
    a = fun();
  }
  return ;
}
           
  • VS2017 輸出

    param constructor, data: 5, name: default, count: 1

    default constructor, data: 0, name: default, count: 2

    copy [rvalue] constructor, data: 0, name: default_copy, count: 3

    destructor, data: 0, name: default, count: 3

    operator = function, before assignment data: 5, name:default

    afeter assignment data: 0, name:default_copy

    destructor, data: 0, name: default_copy, count: 3

    destructor, data: 0, name: default_copy, count: 3

  • g++ 6.2.0 輸出:

    param constructor, data: 5, name: default, count: 1

    default constructor, data: 0, name: default, count: 2

    operator = function, before assignment data: 5, name:default

    afeter assignment data: 0, name:default

    destructor, data: 0, name: default, count: 2

    destructor, data: 0, name: default, count: 2

從VS2017 的第3-4行輸出可以看到,VS2017在函數

fun()

傳回時構造了臨時變量

default_copy

, 而 g++ 編譯器并沒有,還是挺有意思的。

總結一下:

在這個實驗裡,g++編譯器 在函數調用結束後,并沒有生成類的臨時拷貝用來傳回;而VS2017有時候生成,有時候不生成。

  • 猜想:當 return 的變量本身就是臨時變量(即右值)

    A

    時,不再生成臨時變量

    B

    ,而是直接将

    A

    傳回;如果 return 的變量為左值,則函數調用傳回時将其copy,生成的臨時變量并傳回。

繼續閱讀