---
# System prepended metadata

title: 第四章 運算式 筆記

---

# 第四章 運算式 筆記

## 基礎知識

- 單元運算子（unary operator）：只能作用在一個運算元上（operand），如取址（address-of）和解參考（dereference）
- 二元運算子（binary operator）：可作用在兩個運算元上，如加法以及減法
- 還有三元運算子（?: ternary operator）以及可接受無限至數量的運算元的函式呼叫運算子（function call），但大致上通常使用單元以及二元運算子
- 某些符號，如 *，可能會因當下狀況變成單元運算子或二元運算子，如：

```cpp
  int n = 5 * 6;
  int *ptr = &n;
```

- 運算元轉換（type conversion）：小整數型別會被提升（promoted）為較大的整數類型
- 重載的運算子（overloaded operators）：當運算子作用在類別型別的物件時，使用者可以自行定義其含意
- 左值和右值（lvalue and rvalue）：
    - C++ 中的每個運算式（expression）不是左值就是右值
    - 當一個物件被用作右值的時候，用的是物件的值（内容）
    - 被用做左值時，用的是物件的身份（identity 指在記憶體中的位置）
    - 在一個需要 rvalue 的運算式中，lvalue 可以被轉為 rvalue（除了在第 13 章會提到的移動物件以及移動參考），但在需要 lvalue 的運算式中，rvalue 就不能被轉為 lvalue

### 優先序（precedence）與結合性（associativity）

- 帶有兩個或更多個運算子的運算式被稱作複合運算式（compound expression）
- 估算（evaluated）一個複合運算式涉及了運算元相對於運算子的歸組（grouping）動作。優先序（precedence）與結合性（associativity）決定了運算元如何歸組
- 優先序依據各運算元的優先順序歸組，結合性則決定優先序相同時運算元歸組順序
  - 依據優先序，運算式 3 + 4 * 5 是 23，而非 35
  - 依據結合性，運算式 20 - 15 - 3 是 2，而非 8
- 括弧（parentheses）會複寫優先序和結合性
- 優先序及結合性雖然指出運算元如何歸組，但並沒有說明運算元被估算（evaluated）的順序，在多數情況下，順序都是未定的，如：int i = f1() * f2(); 我們知道 f1() 和 f2() 必須在乘法運算進行前被呼叫，但我們無法知道 f1() 和 f2() 哪個會先被估算
- 對於沒有指明估算順序的運算式，運算式參考並更改相同的物件會是一種錯誤，如：

### 估算順序、優先序和結合性

以上三種易混淆，以下觀念務必釐清，舉例 f() + g() * h() + j()
- 優先序保證 g() 和 h() 的結果會相乘
- 結合性保證 f() 的結果會被加到 g() 與 h() 的乘積，而加法運算的結果會被加到 j() 的值
- 這些函式被呼叫的順序則沒有保證

```cpp
int i = 0;
cout << i << " " << i + 1 << endl;
```

## 算數運算子（arithmetic operators）

- 溢位（overflow）：當計算的結果超出該型別所能表示的範圍時就會發生溢位（overflow）
- bool 型別不應該參與計算

  ```cpp
  bool b = true;
  bool b2 = -b;  // 仍然為 true
  // b 為 true，提升為對應 int = 1，-b = -1
  // b2 = (-1 != 0)，所以 b2 仍為 true
  ```
- 取餘數運算（modulus）m % n，结果的正負符號與 m 相同
- 整數與指標可以做加法二元運算，但是指標與指標不能，此外，指標可以與單元運算正號做運算，但是不能與單元運算負號做運算

## 邏輯運算子（logical operators）

- 短路估算（short-circuit evaluated）：邏輯 And（&&）運算子與邏輯 Or（||）運算子都是先求左邊的值再求右邊的值，且當左側的運算結果無法確定該表達式（expression）的結果時才會計算右側的值

## 指定運算子（assignment operator）

- 指定運算的回傳结果為它左側的物件，且是一個左值，型別當然也就是左側物件的型別
- 如果指定運算的左右兩側的運算物件的類型不同，則右側的物件會被嘗試轉換（conversion）成左側物件的類型
- 在新標準之下，我們可以在右邊使用大括號圍起的一個初始器串列（initializer_list）

```cpp
int k = 0;
k = {5};
vector<int> vec;
vec = {4,5,6,7,8,9};
```

- 指定運算子屬於右结合律，這點和其它的二元運算子（binary operator）不一樣，因此 ival = jval = 0; 等價於 ival = (jval = 0);
- 指定運算的優先級相較其他的運算子優先度（precedence）較低，使用其運算作為條件判斷時應加上括號

```cpp
  int i;
  while ((i = get_value()) != 100)
    ;
```
- 複合指定運算子（compound assignment operator）只求值一次 **（如 a += 5），但如果使用一般的指定運算子（如 a = a + 5）的做法，則會需要求值兩次（可能會對性能有一點影響，但通常較流行的編譯器會優化）
- 任意複合指定運算子 op= 基本上都等同於 a = a op b

## 遞增與遞減運算子（increment and decrement operators）

* 前綴（prefix）版本 j = ++i，先遞增後指定
* 後綴（postfix）版本 j = i++，先指定後遞增

優先使用前置版本，因為後綴版本會需要多一步回傳原本儲存的原始值（除非當下情況有需要使用變化前的值），不過這可能會被編譯器優化

### 同時使用解參考（dereference）與遞增運算子（increment operator）

\*iter++ 等價於 \*(iter++)，遞增運算子的優先級比較高（precedence）

```c++
auto iter = vi.begin();
while (iter != vi.end() && *iter >= 0)
  cout << *iter++ << endl;  // 輸出當前值，指針向後移動 1
```

## 成員存取運算子（member access operator）

- 點號與箭號運算子用於成員存取（member access）
- 點號運算子從類別型別（class type）的一個物件擷取一個成員
- 箭號運算子被定義為讓 ptr->mem 成為 (*ptr).mem 的同義詞
- ptr->mem 等同於 (*ptr).mem，注意 . 運算子優先級大於 *，所以要記得加上括號
- 箭號運算子可運算子重載（operator overloading），但點號運算子則無法

## 條件運算子（conditional ternary operator，ternary operator 又稱為三元運算子）

- 條件運算子（?:）允許我們把簡單的 if else 嵌入到單一個運算式（expression）之中，按照如下形式：cond ? expr1 : expr2
- 該運算子可以嵌套使用，且為右结合律，從右向左順序結合

```cpp
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
// 等同於
finalgrade = (grade > 90) ? "high pass" : ((grade < 60) ? "fail" : "pass");
```

- 在運算式（expression）之中使用時記得是否該加上括號，因為條件運算子的優先級（precedence）太低
- 雖然說條件運算子可以嵌套，但基於可讀性，內嵌不可超過二或三層

## 位元運算子（bitwise operators）

用於測試或設定個別位元等等的操作

- 位元運算子（bytewise operators）作用於整數型別的物件，不能作用於浮點數物件
- 可進行二進位的左移（<< right shift）或者右移（>> left shift），而移出邊界外的位元會被捨棄掉
- 所有的位元運算子種類：Not（~）、And（&）、Or（|）、Xor（^）
- 因為正負號位元的處理方式沒有一定的保證，因此強烈建議只將位元運算子用於 unsigned 型別
位元運算的操作範例：

```cpp
unsigned long quiz1 = 0;    // 每一位學生代表是否有通過考試
1UL << 12;  // 代表第 12 個學生通過了考試
quiz1 |= (1UL << 12);   // 將第 12 個學生修改為有通過
quiz1 &= ~(1UL << 12);  // 將第 12 個學生修改為未通過
bool stu12 = quiz1 & (1UL << 12);   // 判斷第 12 個學生是否有通過
```
- 位元運算子（bitwise operator）使用的機會較少，但是運算子重載（operator overloading）後作為輸入輸出運算子，拿來操作 cout、cin 大家都用過
- 位元運算子（bitwise operator）是左結合律，優先度（precedence）介於中間，使用時盡量加上括號

## sizeof 運算子（sizeof operator）

- 回傳一個運算式的結果或一個類別物件（可使用類別名稱）所占用的位元組數（byte）
- 回傳的型別是 size_t 的常數表達式（constant expression）
- sizeof 並不會實際計算其運算物件的值
- 使用以下兩種形式：
  - sizeof (type)，使用類別名稱
  - sizeof (expr)，使用表達式（expression）
- sizeof 對一個陣列型別使用時不會轉換成指標

```cpp
int ia[10];
// sizeof(ia) 回傳整個陣列所佔用空間的大小（byte）
constexpr size_t sz = sizeof(ia)/sizeof(*ia); // 回傳陣列中含有的元素個數
int arr[sz];
```

## 逗號運算子（comma operator）

- 會從左向右依次求值，且回傳右邊的值，而左側的结果會被丟棄
- 逗號運算子（comma operator）回傳的结果是右側表達式的值，且結果如果是左值就回傳左值，如果是右值就回傳右值
- 為左結合性的運算子

## 型別轉換（type conversion）

### 隱含的轉換（implicit conversion）
- 設計為盡可能避免損失精度，而轉換為更精細的型別
- 比 int 小的整數值會先被提升（promoted）為較大的整數型別
- 在條件中，非 bool 運算式會被轉為 bool
- 在初始化中，初始器（initializer）會被轉為變數的型別；在指定中，右運算元會轉為左運算元的型別
- 在混合了不同型別運算元的算術（arithmetic expression）和關係運算式（relational expression）中，型別會被轉換為共通型別
- 在函式呼叫（function call）的過程中也會發生轉換

### 算術轉換（arithmetic conversions）

整數提升（integral promotions）：
- 常見的 bool、char、short、unsigned char、unsigned short 等能存在 int 之中都會被提升（promotion）成 int，否則提升為 unsigned int
- 較大的 char 型別（wchar_t、char16_t、char32_t）會被提升（promoted）為整數型別中 int、unsigned int、long、unsigned long、long long、unsigned long long ... 最小的，且能容纳原本型別所有可能值的類型

### 其它的隱含轉換

- 陣列至指標的轉換（array to pointer conversions）：在多數運算式中（不包含 sizeof、decltype、& 以及 typeid），當我們使用一個陣列，該陣列會被自動轉換為一個指標，指向該陣列中的第一個元素
- 指標轉換（pointer conversion）：一個常數值 0（只能是 0，不能是其他數字）和字面值 nullptr 可以被轉換為任何指標型別；一個指向任何非 const 型別的指標可以被轉換為 void*，而對任何型別的指標都可以被轉換為一個 const void*
- 轉換為 bool（conversion to bool）：算術或指標型別有對 bool 的自動轉換，如果指標或算術值為 0，轉換就會產出 false，任何其他的值都會產出 true
- 轉換為 const（conversion to const）：我們可以將對某個非 const 型別的指標轉為指向對應的 const 型別的一個指標，類似的轉換也適用於參考。也就是說，如果 T 是一個型別，我們可以將指向 T 的一個指標或參考分別轉為指向 const T 的指標或參考

```cpp
int main() {
  int i;
  const int &j = i;   // 將一個非 const 轉為對 const int 的參考
  const int *p = &i;  //將一個非 const 的位址轉為一個 const 的位址
  int &r = j, *q = p;  // 錯誤：從 const 到非 const 的隱性轉換是不被允許的
}
```

- 類別型別所定義的轉換（conversions defined by class types）：類別型別可以定義編譯器會自動套用的轉換。編譯器一次只會套用一個類別型別的轉換，若需要多次轉換，會被駁回

我們已經使用過類別型別的轉換了，也就是 cin >> n，cin >> n 這個運算式會回傳原本的 cin 作為其結果，但條件述句預期型別為 bool 的一個值，但實際上 cin 是一個 istream 的值。但其實在 IO 程式庫定義了 istream 對 bool 的轉換，那個轉換會自動用來將 cin 轉換為 bool，而這個 bool 的值取決於資料流的狀態。如果最後的讀取成功，那麼轉換就會產出 true。如果上一次的嘗試失敗，那麼對 bool 的轉換就會產出 false

### 明確的轉換（explicit conversion）

具名的強制轉型（named cast）共有下列四種：

- static_cast：任何定義良好的型別轉換，除了涉及低階 const 的那些，都可以使用 static_cast 來請求，如：
```cpp
// cast 用來強制執行浮點除法
double slope = static_cast<double>(j) / i;
```

- dynamic_cast：支持執行時期的型態識別（run-time type identification），第 19 章才會再提到
- const_cast：只會變更其運算元中的一個底層的 const，一般可用於去除 const 的性質，且只有 const_cast 可以變更一個運算式的常數性質（constness）。試著以其他形式的具名強制轉型（named cast）改變一個運算式的 const 性質都會是編譯時期錯誤。同樣地，我們無法使用一個 const_cast 來變更一個運算式的型別，以下為 const_cast 的使用範例：

```cpp
const char *pc;
char *p = const_cast<char *>(pc)
```

- reinterpret_cast：會對其運算元的位元模式（bit pattern）進行低階的重新解讀，如：

```cpp
int *ip;
char *pc = reinterpret_cast<char*>(ip);
```

#### 舊式強制轉型（old-style casts）

- type (expr)：函式形式的強制轉型記號
- (type) expr：C 語言式的強制轉型記號

- 舊式強制轉型比具名強制轉型（named cast）更不容易被察覺。因為它們很容易被漏看，要追查出錯的強制轉型就更為困難了
- 建議：避免強制轉型，強制轉型會干擾正常的型別檢查動作。所以我們強烈建議程式設計師避免強制轉型。這個建議特別適用於 reinterpret_cast。這種強制轉型幾乎總是多災多難的。const_cast 在重載函式的情境中可能會有用處。而其它的 const_cast 使用經常代表設計上有問題。其他的強制轉型，static_cast 和 dynamic_cast，應該要很少用到。每次你編寫強制轉型時，就應該認真思考是否能以其他方式達到同樣效果。如果強制轉型無法避免，為了減少錯誤發生的機會，我們應該限制強制轉型後的值被使用的範圍，並以說明文件記錄對於所涉及型別的所有假設