# 第四章 運算式 筆記 ## 基礎知識 - 單元運算子(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,應該要很少用到。每次你編寫強制轉型時,就應該認真思考是否能以其他方式達到同樣效果。如果強制轉型無法避免,為了減少錯誤發生的機會,我們應該限制強制轉型後的值被使用的範圍,並以說明文件記錄對於所涉及型別的所有假設