C++物件模型:constructor

sgqmax發表於2024-11-01

建構函式constructor

explicit的引入,是為了能夠制止“單一引數的constructor”被當作一個conversion運算子

帶有預設建構函式的物件成員

若一個類中包含物件成員,且該物件有預設建構函式,此時:

  • 若該類沒有建構函式
    則編譯器會合成一個預設建構函式,且發生在真正呼叫時
  • 若該類有建構函式,但沒有初始化物件成員
    則在已有建構函式中按宣告順序生成構造物件成員的程式碼,且發生在構造最開始的時候

被合成的預設建構函式,只滿足編譯器的需要,而不是程式的需要
如初始化類物件成員是編譯器的需要,初始化其他類成員變數則是程式的需要

編譯器是如何避免合成多個預設建構函式的呢?如在不同檔案中構造同一個類
編譯器會將合成的預設建構函式,複製建構函式,解構函式,賦值運算子過載以inline的方式完成,inline函式有靜態連結static linkage,不會被檔案以外者看到,若函式太複雜,則會合成出一個explicit non-inline static例項

帶有預設建構函式的父類

  • 若子類沒有建構函式,則會合成預設建構函式呼叫父類建構函式
  • 若子類已有建構函式,則會生成呼叫父類建構函式的程式碼,且發生在初始化物件成員之前

帶有虛擬函式的類

還有兩種情況也需要合成預設建構函式

  • 類中宣告瞭虛擬函式
  • 類的繼承鏈中,包含虛繼承關係
class Widget{
public:
  virtual void flip()=0;
};

void flip(const Widget& w){ w.flip(); }

會生成虛擬函式表vtbl和其指標vptr

w.flip()的虛呼叫操作會被改寫,以滿足多型

(*w.vptr[1])(&w); // &w 為當前物件的this指標

在初始化時,編譯器會給每個物件的vptr設定初值,放置適當的vtbl地址
此時會合成,或在已有建構函式中生成相關初始化程式碼

帶有虛基類的類

class X{ public: int i; };
class A:public virtual X{ public: int j; };
class B:public virtual X{ public: int n; };
class C:public A,public B{ public: int m; };

void foo(const A* pa){ pa->i=1024; }

編譯器無法知道X::i的實際偏移量(經由pa存取),因為pa的真正型別是可改變的

編譯器必須改變“執行存取操作”的程式碼,使X::i可以延遲至執行期決定
cfront的做法是在子類物件中安插虛基類指標
pa->_vbcX->i=1024;,其中_vbcX是編譯器產生的指標,指向虛基類X

此時會合成,或在已有建構函式中生成相關程式碼,來初始化該指標

為什麼虛擬函式和虛基類只有在執行時才能確定
因為編譯器不解析賦值操作

複製建構函式copy constructor

如下場景,會呼叫類的複製建構函式

物件的顯式初始化操作

X xx=x;

物件作為引數傳遞給函式
extern void foo(X x);
foo(xx);

物件作為函式返回值
X foo_bar(){
X xx;
return xx;
}

default memberwise initialization

若一個類沒有提供顯式的複製建構函式,則在複製構造時,內部以default memberwise initialization方式完成
即複製原生型別的成員變數,但對於成員物件,會採用遞迴的方式施行memberwise initialization
(在同類物件間複製構造時)

決定一個類是否生成預設的複製建構函式,在於該類是否展現出bitwise copy semantics
(不展現才會生成)

位逐次複製bitwise copy semantics

以下宣告展現了bitwise copy semantics

class Word{
public:
  Word(const char*);
  ~Word(){ delete[] str; }
private:
  int cnt;
  char* str;
};

這種情況下,不需要合成一個default copy constructor,因為上述宣告展現了default copy semantics
而物件的複製初始化操作也就不需要以一個函式呼叫收場

什麼時候一個類不展現bitwise copy semantics呢?
1.成員變數中包含成員物件,且物件的型別有複製建構函式(包括編譯器生成)
2.繼承自父類,且父類有複製建構函式(包括編譯器生成)
3.類中宣告瞭虛擬函式
4.類的繼承鏈中,包含虛繼承關係

前兩種情況,會在編譯器預設生成的複製建構函式中,呼叫相關成員物件或父類的複製建構函式
第三種情況,以子類物件初始化父類物件時,需要保證vptr的操作安全,此時生成的父類的複製建構函式會設定vptr的值,而不是直接從子類中複製,這也解釋了上一章程式碼

ZooAnimal za= b;
za.rotate();

呼叫的是ZooAnimal::rotate()

第四種情況

class Raccoon:public virtual ZooAnimal{
public:
  Raccoon(){}
  Raccoon(int val){}
private:
};

class RedPanda:public Raccoon{
public:
  RedPanda(){}
  RedPanda(int val){}
private:
};

若以一個Raccoon object作為另一個Raccoon object的初值,則bitwise copy綽綽有餘
若以RedPanda object作為Raccoon object的初值,編譯器則需要生成複製建構函式,並初始化virtual base class pointer/offset

下面這種情況,編譯器無法知道bitwise copy semantics是否還保持,因為無法知道Raccoon指標是指向Raccoon object還是derived class object

Raccoon *ptr;
Raccoon little_critter= *ptr;

程式轉化program transformation

顯式初始化

T t1(t0);
T t2= t0;
T t1= T(t0);

會轉化成:先宣告,再複製構造

T t1;
T t2;
T t3;

t1.T::T(t0);
t2.T::T(t0);
t3.T::T(t0);

引數初始化
將一個class object當作函式實參,或函式返回值
引數傳遞時,會以memberwise方式進行
在編譯器實現計算上,有以下兩種轉化策略
策略一
引入臨時性物件,並用複製建構函式初始化,再以bitwise方式傳遞給形參
函式形參也必須被轉化,需要以引用方式宣告
策略二
將實參物件實際複製構造在函式堆疊中

返回值初始化
cfront中採用雙階段轉化

  1. 宣告一個class object的引用__result
  2. 在return前,使用返回值來複製構造傳入的引用

對於函式指標

X (*pf)();
pf= bar;

轉化為

void (*pf)(X&);
pf= bar;

在使用者層面最佳化
定義一個建構函式constructor,可以直接計算返回值,而不是呼叫複製建構函式

在編譯器層面最佳化
將返回值直接使用__result代替,稱為Named Return Value(NRV)最佳化
NRV最佳化現在被認為是C++編譯器義不容辭的最佳化操作
如下程式碼

class Test{
  friend Test foo(double);
public:
  Test(){
    memset(arr, 0, 100*sizeof(double));
  }
private:
  double arr[100];
};

此時,編譯器不會做NRV最佳化,因為沒有複製建構函式,如下加上inline copy constructor

inline Test(const Test& t){
  memcpy(this, &t, sizeof(test));
}

是否需要複製建構函式

沒有任何理由要提供一個複製建構函式,因為編譯器自動實施了最好的行為

若一個class要大量memberwise初始化操作,則提供一個copy constructor的explicit inline函式例項是合理的(在編譯器提供NRV的前提下)

若使用更有效率的memset()memcpy()作為複製建構函式的實現,則需要在class中不含任何由編譯器產生的內部成員

成員的初始化

必須使用成員初始化列表的情況
1.初始化引用reference成員
2.初始化const成員
3.呼叫基類建構函式,且其有一組引數
4.呼叫成員的建構函式,且其有一組引數

class Word{
  String name_;
  int cnt_;
public:
  Word(){
    name_= 0;
    cnt_= 0;
  }
};

編譯器會生成一個臨時物件,可能的轉化如下

public:
  Word(){
    // 預設構造
    name_.String::String();

    // 臨時物件
    String temp= String(0);
    // memberwise複製
    name_.String::operator=(temp);
    temp.String::~String();

    cnt_= 0;
  }

若使用列表初始化

Word::Word():name_(0){
  cnt_= 0;
}

編譯器可能的轉化如下

Word::Word() {
  // 直接呼叫建構函式
  name_.String::String(0);
  cnt_= 0;
}

成員初始化列表到底做了什麼,是不是簡單的函式呼叫
可以回答,當然不是簡單的函式呼叫

編譯器會在建構函式中生成程式碼,將初始化列表中的變數按在類中的宣告順序初始化
如下程式碼

class X{
  int i;
  int j;
  X(int val):j(val),i(j){}
};

由於宣告順序的緣故,i會比j先執行,會導致問題,這種情況,GNU C++編譯器g++會做出告警
建議做出如下調整

class X{
  int i;
  int j;
  X(int val):j(val){
    i= j;
  }
};

能否呼叫成員函式,以初始化資料成員

X::X(int val):i(xfoo(val)),j(val){}

成員函式的使用是合法的,因為此時this指標已經被構造
編譯器可能的生成程式碼如下

X::X(int val){
  i= this->xfoo(val);
  j= val;
}

能否使用子類成員函式返回值,作為父類建構函式的實參

class FooBar:public X{
  int fval_;
public:
  int fval(){ return fval_; }
  FooBar(int val):fval_(val),X(fval()){}
};

編譯器可能的生成程式碼如下

FooBar::FooBar(int val){
  X::X(this, this->fval());
  fval_= val
}

可知,這不是一個好主意

相關文章