第2章 表示式

N1rv2na發表於2024-07-08

第2章 表示式

2.1 基礎

2.1.1 基本概念

  • 一元運算子(unary operator):作用於一個運算物件,如取地址符(&)和解引用符(*);
  • 二元運算子(binary operator):作用於兩個個運算物件,如相等運算子(==)和乘法運算子(*);
  • 三元運算子:作用於三個運算物件(?😃

組合運算子和運算物件

要理解含有多個運算子的複雜表示式,首先要理解運算子的優先順序結合律以及運算物件的求值順序

運算物件轉換

在表示式求值過程中,運算物件常常由一種型別轉換為另外一種型別,一般二元運算子要求兩個運算物件的型別相同,但很多時候即使兩個運算物件的型別不同也沒有關係,只要它們能轉換成同一種型別即可。

過載運算子

C++定義了運算子作用於內建型別和複合型別的運算物件所執行的操作,當運算子作用於類型別的運算物件時,程式設計師可以自定義運算子的含義,稱之為過載運算子(overloaded operator)

左值與右值

C++的表示式要麼是右值(rvalue),要麼是左值(lvalue)。左值可以位於賦值語句的左側,右值則不能。

當一個物件被用作右值的時候,用的是物件的值(內容);當物件被用作左值的時候,用的是物件的身份(在記憶體中的位置)

不同的運算子對運算物件的要求各不相同,有的需要左值運算物件,有的需要右值運算物件,返回值也有差異,有的返回左值結果,有的返回右值結果。

在需要右值的時候可以用左值來替代,但是不能把右值當成左值使用。當一個左值被當成右值使用時,實際使用的是它的內容

  • 賦值運算子需要一個左值作為其左側的運算物件,得到的結果也仍然是一個左值;
  • 取地址符作用於一個左值運算物件,返回一個指向該運算物件的指標,這個指標是一個右值;
  • 內建解引用符、下標運算子的求值結果都是一個左值;
  • 內建型別和迭代器的遞增遞減運算子作用於左值運算物件,所得的結果也是一個左值;

使用decltype(expression)的時候,expression是左值或右值返回的型別結果也有所不同,如果表示式的求值結果是一個左值,decltype將返回一個引用型別。

int *p, a = 42;
decltype(*p) ref = a;  // 解引用符返回一個左值,因此decltype(*p)返回int&。
decltype(&p) ptr = nullptr;  // 取地址符返回一個右值,因此decltype(&p)返回int**。

2.1.2 優先順序與結合律

求複合表示式的值首先需要將運算子和運算物件合理地組合在一起,優先順序與結合律決定了運算物件的組合方式。表示式中的括號無視上述規則,可以使用括號將表示式的區域性括起來使其得到優先運算。

高優先順序運算子的運算物件要比低優先順序運算子的運算物件更為緊密地結合在一起,如果優先順序相同,則由結合律決定。算術運算子滿足左結合律,如果算術運算子優先順序相同將按照從左往右的順序組合運算物件。

6 + 3 * 4 / 2 + 2
// 上述表示式的運算順序為:((6 + ((3 * 4) / 2) + 2)    

括號無視優先順序與結合律

括號無視普通的組合規則,表示式中被括起來的部分被當成一個單元來求值,然後再與其他部分一起按照優先順序組合。

// 不同括號組合將導致不同的組合結果
cout << (6 + 3) * (4 /2 + 2) << endl;   // 輸出36
cout << ((6 + 3) * 4) /2 + 2) << endl;  // 輸出20
cout << 6 + 3 * 4 / (2 + 2) << endl;    // 輸出9

優先順序與結合律會影響程式的正確性

int ia[] = {0, 2, 4, 6, 8};
int last = *(ia + 4);  // 把last初始化成8,也就是ia[4]的值
last = *ia + 4;  // last = 4,等價於ia[0] + 4

2.1.3 求值順序

優先順序規定了運算物件的組合方式,但是沒有說明運算物件按什麼順序求值,大多數情況下,都不會明確指定求值的順序.

int i = f1() * f2();
// f1() 和 f2() 函式一定會在執行乘法操作之前被呼叫,但是並不能確定f1()和f2()兩者誰先被呼叫。

如果一個運算子沒有指定執行順序,但是表示式指向並修改了同一個物件,就會引發錯誤併產生未定義行為!

int i = 0;
cout << i << ++i << endl;
// << 運算子沒有規定求值順序,因此上述表示式的輸出是不確定的,可能是 0 1,也可能是 1 1。

有四種運算子明確規定了運算物件的求值順序:

  • 邏輯與(&&)運算子:先求左側運算物件的值,只有左側運算物件的值為真時才繼續求右側運算物件的值
  • 邏輯與(||)運算子:先求左側運算物件的值,只有左側運算物件的值為假時才繼續求右側運算物件的值
  • 條件(?:)運算子
  • 逗號運算子

建議:處理複合表示式

  1. 不確定運算子的優先順序時最好使用括號來強制讓表示式的組合關係符合設想的邏輯。
  2. 如果表示式改變了某個運算物件的值,那麼在該表示式的其他地方不要再使用這個運算物件。

2.2 算術運算子

image-20240130195034712

算術運算子都滿足左結合律,當優先順序相同時,按照從左到右的順序進行組合。

算術運算子的運算物件和求值結果都是右值

2.3 邏輯與關係運算子

關係運算子作用於算術型別或指標型別,邏輯運算子作用於任意能轉換成布林值的型別,邏輯運算子和關係運算子的返回值都是布林型別,值為0的運算物件表示假,值為非0的運算物件表示真。

對於邏輯運算子和關係運算子來說,運算物件和求值結果都是右值。

image-20240219200136029

邏輯與和邏輯或運算子

對於邏輯與運算子(&&)來說,當且僅當兩個運算物件都為真時結果為真;對於邏輯或運算子(||)來說,只要兩個運算子中的一個為真結果就為真。

短路求值

  • 對於邏輯與運算子來說,當且僅當左側運算物件為真時,才對右側運算物件求值
  • 對於邏輯或運算子來說,當且僅當左側運算物件為假時,才對右側運算物件求值

邏輯非運算子

邏輯非運算子(!)將運算物件的值取反後返回

關係運算子

關係運算子比較運算物件的大小關係並返回布林值。關係運算子都滿足左結合律。

C++並不支援鏈式比較!

int i = 1, j = 2, k = 3;
// i < j < k的運算結果實際上是先計算i < j的結果然後再與k作比較
// i < j的結果為真即為1,1 < 3,結果為真
if (i < j < k) {
    ...
} 

// 若想要實現鏈式比較,應該使用&&:
if (i < j && j < k) {
    ...
}

相等性測試與布林字面值

如果想要測試一個算術物件或指標物件的真值,最簡單的方法是將其作為if語句的條件:

if (val) {  // 如果val是任意的非0值,條件為真
    
}
if (!val) {  // 如果val是0,則條件為真
    
}

進行比較運算時除非比較的物件是布林型別,否則不要使用布林字面值true或false作為運算物件

不推薦if (val == true) {}這樣的形式。

2.4 賦值運算子

賦值運算子的左側運算物件必須是一個可修改的左值

int i = 0, j = 0, k = 0;  // 初始化變數,不是賦值
const int ci = i; // 初始化變數,不是賦值

1024 = k;   // 錯誤,1024是字面值,是右值
i + j = k;  // 錯誤,i + j是表示式,是右值
ci = k;     // 錯誤,ci是常量左值,不可修改

賦值運算子的結果是它的左側的運算物件,並且是一個左值。相應的,結果的型別就是左側運算物件的型別。如果賦值運算子的左右兩側的兩個運算物件型別不同,則右側運算物件將換成左側運算物件的型別。

k = 0;     // 結果:型別是int,值為0
k = 3.14;  // 結果:型別是int,值為3

C++11允許使用花括號括起來的初始值列表作為賦值運算子的右側運算物件:

k = {3.14}  // 錯誤:當使用列表初始化時,如果型別轉換時存在窄化風險,編譯器會報錯
vector<int> vi;
vi = {0, 1, 2};

賦值運算子滿足右結合律

相關文章