2013年11月15日 星期五

const 的使用 (5) - 參考篇

上一篇: const 的使用 (4) - 基礎測驗解答篇

參考 (reference) 是 C++ 相對於 C 的新功能。在介紹參考使用 const 時的問題前,先稍微複習一下參考的定義語法:

int a = 3;
int &b = a;  // 定義 b 為 a 的參考,也就是說『 b 就是 a 』
b = 5;       // 將 b 賦值為 5,也就是將 a 賦值為 5

定義參考跟定義指標一個很大的不同就在於定義參考時並不會進行任何的變數建構,但是定義指標時會建構一個變數來儲存指標。

我們可以在參考初始化的時候不指定參考的對象嗎?我們可以初始化後修改參考所參考的對象嗎?這兩個問題的答案都是否定的。所以參考與本身為 const 的指標在性質上有些類似:初始化時要指定參考對象且初始化之後不能改變要參考的對象。

同樣地,在處理指向變數的 const 性質轉換時,具有相似的規則:『一個非 const 變數可以被一個 const 變數的參考所參考』

const int c = a;

再來想想下面這段程式碼是不是合法的?

int **d;
int **&e = d;        // [編譯成功]
int ** const &f = d; // [編譯成功]
int * const *&g = d; // [編譯失敗] 為什麼?
int * const * h = d; // [編譯成功]
int * const * const &i = d; // [編譯成功] 為什麼?
const int ** &j = d; // [編譯失敗]
const int **  k = d; // [編譯失敗]

那參考跟指標在使用 const 時有什麼不同呢?不同點分為兩個方向:

  • 無法把參考本身設為 const 或非 const: 這可以想成參考預設就是無法改變參考對象,所以沒有 const 與否的問題。
  • 參考所參考的變數不能也是個參考型態: 換句話說不會有上一章類似巢狀指標轉型的巢狀參考問題。

int & const l = a;  // [編譯失敗]
int &&m = b;        // [編譯失敗]
int &n = b;         // [編譯成功] 等同於 int &n = a;

const 變數的參考

const 變數的參考 (reference-to-const) 具備一個特殊的用途:可用來參考右值。

我們先來看看下面的程式碼:

int &o = 3;     // [編譯失敗] 3 是個字面常數 (右值)
int &p = a + 2; // [編譯失敗] a+2 的結果是個暫時變數 (右值)

上述的兩行程式碼都是不合法的。

不合法的原因可以從比較簡單的角度來看:3 與 a+2 可以放在等號左邊嗎?

能放在等號左邊的表示式我們稱之為左值,反之則稱之為右值。而依照規定我們無法用非 const 變數的參考來參考右值。

為什麼呢?如果可以用非 const 變數的參考來參考右值的話,那就會發生下面的詭異情況:

o = 5;  // o 只是個參考,那是誰變成 5 了?
p = 10; // p 只是個參考,那是誰變成 10 了?

為了避免上面的情況,所以我們無法讓非 const 變數的參考來參考右值。

但是如果今天是一個 const 變數的參考時,情況就不同了:

const int &q = 3;     // [編譯成功]
const int &r = a + 2; // [編譯成功]

因為這樣就不會發生我們上面提到的詭異情況:

q = 5;  // [編譯失敗]
r = 10; // [編譯失敗]

聽起來事情好像很完美的解決:如果我們要參考一個右值的話,就用 const 變數的參考就好了。

不過事情沒有想的那麼好,因為你會發現雖然 3 跟 a+2 的生命週期都只活在該表示式內,但是 q 跟 r 是在 3 跟 a+2 生命週期結束之後作為一個 const 變數使用。照參考的意義來說:『q 就是 3』與『r 就是 a+2』這樣的說法就行不通。例如當我們要去讀取 r 的時候,r 所參考的 a+2 可能已經結束生命週期而釋放了。去讀取一個已經釋放的變數內容是個『未定義行為』。所以為了不造成未定義行為,C++ 額外規定讓 const 變數的參考可視需要將該 const 變數的生命週期延長直到該參考離開可視範圍!

舉例來說,當我們對 q 與 r 取址的時候,你會發現這個動作是合法的。但是我們卻不能對 3 與 (a+2) 取址:

&q;     // [編譯成功]
&r;     // [編譯成功]
&3;     // [編譯失敗]
&(a+2); // [編譯失敗]

這表示 q 並不就是 3, 而 r 並不就是 a+2。這些微的語意差異使得 const 變數的參考可視需要經過特殊處理去延長所參考變數的生命週期並因此具有特殊性。


讓 const 變數的參考可以參考右值的好處?

上一個章節我們提到 const 變數的參考可以參考右值。但是有一個很根本的問題是:我們為什麼要讓他可以這麼做?就讓所有的右值都不能被參考不就好了?

考慮下面這兩個函式:

int sum1(int lhs, int rhs) {
  return lhs+rhs;
}
int sum2(const int &lhs, const int &rhs) {
  return lhs+rhs;
}

這兩個函式的差別一個是傳值呼叫,而另一個是傳參考呼叫。

傳參考呼叫的好處是不用複製一份引數到函式內,有機會可以省下複製所需要的空間與時間成本。(雖然這個例子因為參考的是 int 型態所以不見得比較省,但是當我們的參數型態是大型類別或結構時則有機會比較省。)

如果我們不允許 const 變數的參考可以參考右值,那下面這些呼叫都是不合法的:

int s = 3;
sum2(s, 3);          
sum2(3, 4);
sum2(s+1, 5);
sum2(sum2(s, s), s); // 裡面的 sum2 是可以的,但是外面的不行

因此允許 const 變數的參考可以參考右值有其使用上的好處。


下一篇: const 的使用 (6) - 類別與結構篇 (撰寫中)

沒有留言: