《C++ Primer 5th》笔记(4 / 19):表达式
文章目錄
- 基礎
- 基本概念
- 組合運算符和運算對象
- 運算對象轉換
- 重載運算符
- 左值和右值
- 優先級與結合律
- 括號無視優先級與結合律
- 優先級與結合律有何影響
- 求值順序
- 求值順序、優先級、結合律
- 建議:處理復合表達式
- 算術運算符
- 一元正負號
- 加、減、乘、除、求余
- 提示:溢出和其他算術運算異常
- 邏輯和關系運算符
- 邏輯與和邏輯或運算符
- 邏輯非運算符
- 關系運算符
- 相等性測試與布爾字面值
- 賦值運算符
- 賦值運算滿足右結合律
- 賦值運算優先級較低
- 切勿混淆相等運算符和賦值運算符
- 復合賦值運算符
- 遞增和遞減運算符
- 建議:除非必須,否則不用遞增遞減運算符的后置版本
- 在一條語句中混用解引用和遞增運算符
- 建議:簡潔可以成為一種美德
- 運算對象可按任意順序求值
- 成員訪問運算符
- 條件運算符
- 嵌套條件運算符
- 在輸出表達式中使用條件運算符
- 位運算符
- 移位運算符
- 位求反運算符
- 位與、位或、位異或運算符
- 使用位運算符
- 移位運算符(又叫IO運算符)滿足左結合律
- sizeof運算符
- 逗號運算符
- 類型轉換
- 何時發生隱式類型轉換
- 算術轉換
- 整型提升
- 無符號類型的運算對象
- 理解算術轉換
- 其他隱式顯示轉換
- 顯示轉換
- 命名的強制類型轉換
- static_cast
- const_cast
- reinterpret_cast
- 建議:避免強制類型轉換
- 舊式的強制類型轉換
- 運算符優先級表
表達式(expression)由一個或多個運算對象(operand)組成,對表達式求值將得到一個結果(result)。
-
字面值和變量是最簡單的表達式,其結果就是字面值和變量的值。
-
把一個運算符( operator)和一個或多個運算對象組合起來可以生成較復雜的表達式。
基礎
有幾個基礎概念對表達式的求值過程有影響,它們涉及大多數(甚至全部)表達式。
基本概念
C++定義了一元運算符(unary operator)和二元運算符(binary operator)。
- 作用于一個運算對象的運算符是一元運算符,如取地址符(&)和解引用符(*);
- 作用于兩個運算對象的運算符是二元運算符,如相等運算符(==)和乘法運算符(*)。
除此之外,還有一個作用于三個運算對象的三元運算符(?:)。函數調用也是一種特殊的運算符,它對運算對象的數量沒有限制。
(一符多意)一些符號既能作為一元運算符也能作為二元運算符。以符號*為例,作為一元運算符時執行解引用操作,作為二元運算符時執行乘法操作。一個符號到底是一元運算符還是二元運算符由它的上下文決定。對于這類符號來說,它的兩種用法互不相干,完全可以當成兩個不同的符號。
組合運算符和運算對象
對于含有多個運算符的復雜表達式來說,要想理解它的含義首先要理解運算符的:
- 優先級(precedence)
- 結合律(associativity)
- 運算對象的求值順序(order of evaluation)
例如,下面這條表達式的求值結果依賴于表達式中運算符和運算對象的組合方式:
5 + 10 * 20 / 2;乘法運算符(*)是一個二元運算符,它的運算對象有4種可能: 10和20、10和20/2、15和20、15和20/2。下一節將介紹如何理解這樣一條表達式。
運算對象轉換
在表達式求值的過程中,運算對象常常由一種類型轉換成另外一種類型。
例如,盡管一般的二元運算符都要求兩個運算對象的類型相同,但是很多時候即使運算對象的類型不相同也沒有關系,只要它們能被轉換成同一種類型即可。
類型轉換的規則雖然有點復雜,但大多數都合乎情理、容易理解。
例如,整數能轉換成浮點數,浮點數也能轉換成整數,但是指針不能轉換成浮點數。讓人稍微有點意外的是,小整數類型(如bool、char、short等)通常會被提升(promoted)成較大的整數類型,主要是int。接下來將會詳細介紹類型轉換的細節。
重載運算符
C++語言定義了運算符作用于內置類型和復合類型的運算對象時所執行的操作。
當運算符作用于類類型的運算對象時,用戶可以自行定義其含義。因為這種自定義的過程事實上是為已存在的運算符賦予了另外一層含義,所以稱之為重載運算符( overloaded operator)。IO庫的>>和<<運算符以及string對象、vector對象和迭代器使用的運算符都是重載的運算符。
我們使用重載運算符時,其包括運算對象的類型和返回值的類型,都是由該運算符定義的。但是運算對象的個數、運算符的優先級和結合律都是無法改變的。
左值和右值
C++的表達式要不然是右值(rvalue,讀作“are-value"),要不然就是左值(lvalue,讀作“ell-value”)。這兩個名詞是從C語言繼承過來的,原本是為了幫助記憶:左值可以位于賦值語句的左側,右值則不能。
在C++語言中,二者的區別就沒那么簡單了。
一個左值表達式的求值結果是一個對象或者一個函數,然而以常量對象為代表的某些左值實際上不能作為賦值語句的左側運算對象。此外,雖然某些表達式的求值結果是對象,但它們是右值而非左值。
可以做一個簡單的歸納:
- 當一個對象被用作右值的時候,用的是對象的值(內容);
- 當對象被用作左值的時候,用的是對象的身份(在內存中的位置)。
不同的運算符對運算對象的要求各不相同,
- 有的需要左值運算對象、
- 有的需要右值運算對象;
返回值也有差異,
- 有的得到左值結果、
- 有的得到右值結果。
一個重要的原則是在需要右值的地方可以用左值來代替,但是不能把右值當成左值(也就是位置)使用。當一個左值被當成右值使用時,實際使用的是它的內容(值)。到目前為止,已經有幾種我們熟悉的運算符是要用到左值的。(右能被左替,對換不能替)
-
賦值運算符需要一個(非常量nonconst)左值作為其左側運算對象,得到的結果也仍然是一個左值。(a = b = 1)
-
取地址符作用于一個左值運算對象,返回一個指向該運算對象的指針,這個指針是一個右值。(int a = 1;int *p = &a;)
-
內置解引用運算符、下標運算符、迭代器解引用運算符、string和 vector的下標運算符的求值結果都是左值。
-
內置類型和迭代器的遞增遞減運算符作用于左值運算對象,其前置版本(本書之前章節所用的形式)所得的結果也是左值。
接下來在介紹運算符的時候,我們將會注明該運算符的運算對象是否必須是左值以及其求值結果是否是左值。
使用關鍵字decltype的時候,左值和右值也有所不同。如果表達式的求值結果是左值,decltype作用于該表達式(不是變量)得到一個引用類型。
舉個例子,假定p的類型是int*,因為解引用運算符生成左值,所以decltype(p)的結果是int&。另一方面,因為取地址運算符生成右值,所以 decltype (&p)的結果是int*,也就是說,結果是一個指向整型指針的指針。
優先級與結合律
復合表達式(compound expression)是指含有兩個或多個運算符的表達式。求復合表達式的值需要首先將運算符和運算對象合理地組合在一起,優先級與結合律決定了運算對象組合的方式。也就是說,它們決定了表達式中每個運算符對應的運算對象來自表達式的哪一部分。表達式中的括號無視上述規則,程序員可以使用括號將表達式的某個局部括起來使其得到優先運算。
一般來說,表達式最終的值依賴于其子表達式的組合方式。高優先級運算符的運算對象要比低優先級運算符的運算對象更為緊密地組合在一起。如果優先級相同,則其組合規則由結合律確定。例如,乘法和除法的優先級相同且都高于加法的優先級。因此,乘法和除法的運算對象會首先組合在一起,然后才能輪到加法和減法的運算對象。算術運算符滿足左結合律,意味著如果運算符的優先級相同,將按照從左向右的順序組合運算對象:
-
根據運算符的優先級,表達式3+4*5的值是23,不是35。
-
根據運算符的結合律,表達式20-15-3的值是2,不是8。
舉一個稍微復雜一點的例子,如果完全按照從左向右的順序求值,下面的表達式將得到20:
6 + 3 * 4 / 2 + 2也有一些人會計算得到9、14或者36,然而在C++語言中真實的計算結果應該是14。這是因為這條表達式事實上與下述表達式等價:
// parentheses in this expression match default precedence and associativity ((6 + ((3 * 4) / 2)) + 2)(Note:如果優先級相同,則其組合規則由結合律確定。)
(Note:左結合律 -> 按照從左向右的順序組合運算對象)
括號無視優先級與結合律
括號無視普通的組合規則,表達式中括號括起來的部分被當成一個單元來求值,然后再與其他部分一起按照優先級組合。例如,對上面這條表達式按照不同方式加上括號就能得到4種不同的結果:
// parentheses result in alternative groupings cout << (6 + 3) * (4 / 2 + 2) << endl; // prints 36 cout << ((6 + 3) * 4) / 2 + 2 << endl; // prints 20 cout << 6 + 3 * 4 / (2 + 2) << endl; // prints 9優先級與結合律有何影響
由前面的例子可以看出,優先級會影響程序的正確性,這一點在介紹的解引用和指針運算中也有所體現:
int ia[] = {0,2,4,6,8}; // array with five elements of type int int last = *(ia + 4); // initializes last to 8, the value of ia [4] last = *ia + 4; // last = 4, equivalent to ia [0] + 4如果想訪問ia+4位置的元素,那么加法運算兩端的括號必不可少。一旦去掉這對括號,*ia就會首先組合在一起,然后4再與*ia的值相加。
結合律對表達式產生影響的一個典型示例是輸入輸出運算,將要介紹IO相關的運算符滿足左結合律。這一規則意味著我們可以把幾個IO運算組合在一條表達式當中:
cin >> v1 >> v2; // read into v1 and then into v2最后部分會羅列出了全部的運算符,并用雙橫線將它們分割成若干組。
同一組內的運算符優先級相同,組的位置越靠前組內的運算符優先級越高。例如,前置遞增運算符和解引用運算符的優先級相同并且都比算術運算符的優先級高。
求值順序
優先級規定了運算對象的組合方式,但是沒有說明運算對象按照什么順序求值。在大多數情況下,不會明確指定求值的順序。對于如下的表達式:
int i = f1() * f2();我們知道f1和f2一定會在執行乘法之前被調用,因為畢竟相乘的是這兩個函數的返回值。但是我們無法知道到底f1在f2之前調用還是f2在f1之前調用。
對于那些沒有指定執行順序的運算符來說,如果表達式指向并修改了同一個對象,將會引發錯誤并產生未定義的行為。舉個簡單的例子,<<運算符沒有明確規定何時以及如何對運算對象求值,因此下面的輸出表達式是未定義的:
int i = 0; cout << i << " " << ++i << endl; // undefined因為程序是未定義的,所以我們無法推斷它的行為。
-
編譯器可能先求++i的值再求i的值,此時輸出結果是1 1;
-
也可能先求i的值再求++i的值,輸出結果是0 1;
甚至編譯器還可能做完全不同的操作。因為此表達式的行為不可預知,因此不論編譯器生成什么樣的代碼程序都是錯誤的。
有4種運算符明確規定了運算對象的求值順序。
求值順序、優先級、結合律
運算對象的求值順序與優先級和結合律無關,在一條形如f( )+g( ) *h()+j()的表達式中:
- 優先級規定,g()的返回值和h ()的返回值相乘。
- 結合律規定,f()的返回值先與g ()和 h ()的乘積相加,所得結果再與j()的返回值相加。
- 對于這些函數的調用順序沒有明確規定。
如果f、g、h和j是無關函數independent functions ,它們既不會改變同一對象的狀態也不執行IO任務,那么函數的調用順序不受限制。反之,如果其中某幾個函數影響同一對象,則它是一條錯誤的表達式,將產生未定義的行為。
建議:處理復合表達式
以下兩條經驗準則對書寫復合表達式有益:
第2條規則有一個重要例外,當改變運算對象的子表達式本身就是另外一個子表達式的運算對象時該規則無效。
例如,在表達式*++iter中,遞增運算符改變 iter的值,iter(已經改變)的值又是解引用運算符的運算對象。此時(或類似的情況下),求值的順序不會成為問題,因為遞增運算(即改變運算對象的子表達式)必須先求值,然后才輪到解引用運算。顯然,這是一種很常見的用法,不會造成什么問題。
算術運算符
左結合律
| + - | 一元正號 一元負號 | + expr - expr |
| * / % | 乘法 除法 求余 | expr * expr expr / expr expr % expr |
| + - | 加法 減法 | expr + expr expr - expr |
上表(以及后面章節的運算符表)按照運算符的優先級將其分組。
一元運算符的優先級最高,接下來是乘法和除法,優先級最低的是加法和減法。優先級高的運算符比優先級低的運算符組合得更緊密。上面的所有運算符都滿足左結合律,意味著當優先級相同時按照從左向右的順序進行組合。
除非另做特殊說明,算術運算符都能作用于任意算術類型以及任意能轉換為算術類型的類型。
算術運算符的運算對象和求值結果都是右值。(當一個對象被用作右值的時候,用的是對象的值(內容);)
在表達式求值之前,小整數類型的運算對象被提升成較大的整數類型,所有運算對象最終會轉換成同一類型。
一元正負號
一元正號運算符、加法運算符和減法運算符都能作用于指針。上一章節已經介紹過二元加法和減法運算符作用于指針的情況。當一元正號運算符作用于一個指針或者算術值時,返回運算對象值的一個(提升后的)副本。
(TODO:一元正號運算符作用于一個指針?查查有哪些用途。)
一元負號運算符對運算對象值取負后,返回其(提升后的)副本:
int i = 1024; int k = -i; // i is -1024 bool b = true; bool b2 = -b; // b2 is true!我們指出布爾值不應該參與運算,-b就是一個很好的例子。
對大多數運算符來說,布爾類型的運算對象將被提升為 int 類型。如上所示,布爾變量b的值為真,參與運算時將被提升成整數值1,對它求負后的結果是-1。將-1再轉換回布爾值并將其作為b2的初始值,顯然這個初始值不等于0轉換成布爾值后應該為1。所以,b2的值是真!(似非而是的結果)
加、減、乘、除、求余
當作用于算術類型的對象時,算術運算符+、-、*、/的含義分別是加法、減法、乘法和除法。整數相除結果還是整數,也就是說,如果商含有小數部分,直接棄除:
int ival1 = 21/6; // ival1 is 3; result is truncated; remainder is discarded int ival2 = 21/7; // ival2 is 3; no remainder; result is an integral value運算符%俗稱“取余”或“取?!边\算符,負責計算兩個整數相除所得的余數,參與取余運算的運算對象必須是整數類:
int ival = 42; double dval = 3.14; ival % 12; // ok: result is 6 ival % dval; // error: floating-point operand在除法運算中,如果兩個運算對象的符號相同則商為正(如果不為0的話),否則商為負。C++語言的早期版本允許結果為負值的商向上或向下取整,C++11新標準則規定商一律向0取整(即直接切除小數部分)。
根據取余運算的定義,如果m和 n是整數且n非0,則表達式(m/n)*n+m%n的求值結果與m相等。隱含的意思是,如果m%n不等于0,則它的符號和m相同。C++語言的早期版本允許 m%n 的符號匹配n的符號,而且商向負無窮一側取整,這一方式在新標準中已經被禁止使用了。除了-m 導致溢出的特殊情況,其他時候(-m)/n和 m/(-n)都等于- (m/ n),m% (-n)等于m%n,(-m)%n等于-(m%n)。具體示例如下:
21 % 6; /* result is 3 */ 21 / 6; /* result is 3 */21 % 7; /* result is 0 */ 21 / 7; /* result is 3 */-21 % -8; /* result is -5 */ -21 / -8; /* result is 2 */21 % -5; /* result is 1 */ c21 / -5; /* result is -4 */提示:溢出和其他算術運算異常
算術表達式有可能產生未定義的結果。
- 一部分原因是數學性質本身例如除數是0的情況;
- 另外一部分則源于計算機的特點:例如溢出,當計算的結果超出該類型所能表示的范圍時就會產生溢出。
假設某個機器的short類型占16位,則最大的short數值是32767。在這樣一臺機器上,下面的復合賦值語句將產生溢出:
short short_value = 32767; // max value if shorts are 16 bits short_value += 1; // this calculation overflows cout << "short_value: " << short_value << endl;給short_value賦值的語句是未定義的,這是因為表示一個帶符號數32768需要17位,但是short類型只有16位。很多系統在編譯和運行時都不報溢出錯誤,像其他未定義的行為一樣,溢出的結果是不可預知的。在我們的系統中,程序的輸出結果是:
short_value: -32768該值發生了“環繞(wrapped around)”,符號位本來是0,由于溢出被改成了1,于是結果變成一個負值。在別的系統中也許會有其他結果,程序的行為可能不同甚至直接崩潰。
邏輯和關系運算符
關系運算符作用于算術類型或指針類型,邏輯運算符作用于任意能轉換成布爾值的類型。
邏輯運算符和關系運算符的返回值都是布爾類型。
值為0的運算對象(算術類型或指針類型)表示假,否則表示真。
對于這兩類運算符來說,運算對象和求值結果都是右值。
| 右 | ! | 邏輯非 | !expr |
| 左 左 左 左 | < <= > >= | 小于 小于等于 大于 大于等于 | expr < expr expr <= expr expr > expr expr >= expr |
| 左 左 | == != | 相等 不相等 | expr == expr expr != expr |
| 左 | && | 邏輯與 | expr &&expr |
| 左 | || | 邏輯或 | expr||expr |
邏輯與和邏輯或運算符
對于邏輯與運算符(&&)來說,當且僅當兩個運算對象都為真時結果為真。對于邏輯或運算符(||)來說,只要兩個運算對象中的一個為真結果就為真。
邏輯與運算符和邏輯或運算符都是先求左側運算對象的值再求右側運算對象的值,當且僅當左側運算對象無法確定表達式的結果時才會計算右側運算對象的值。這種策略稱為短路求值(short-circuit evaluation)。
-
對于邏輯與運算符來說,當且僅當左側運算對象為真時才對右側運算對象求值。
-
對于邏輯或運算符來說,當且僅當左側運算對象為假時才對右側運算對象求值。
上一章的幾個程序用到了邏輯與運算符,它們的左側運算對象是為了確保右側運算對象求值過程的正確性和安全性。如:
index != s.size() && !isspace(s[index])首先檢查index是否到達string對象的末尾,以此確保只有當index在合理范圍之內時才會計算右側運算對象的值。
舉一個使用邏輯或運算符的例子,假定有一個存儲著若干string對象的vector對象,要求輸出string對象的內容并且在遇到空字符串或者以句號結束的字符串時進行換行。使用基于范圍的for循環處理string對象中的每個元素:
// note s as a reference to const; the elements aren't copied and can't be changed for (const auto &s : text) { // for each element in textcout << s; // print the current element// blank lines and those that end with a period get a newlineif (s.empty() || s[s.size() - 1] == '.')cout << endl;elsecout << " "; // otherwise just separate with a space }輸出當前元素后檢查是否需要換行。if語句的條件部分首先檢查s 是否是一個空string,如果是,則不論右側運算對象的值如何都應該換行(短路求值體現)。只有當string對象非空時才需要求第二個運算對象的值,也就是檢查string對象是否是以句號結束的。在這條表達式中,利用邏輯或運算符的短路求值策略確保只有當s非空時才會用下標運算符去訪問它。
值得注意的是,s被聲明成了對常量的引用。因為 text的元素是string對象,可能非常大,所以將s聲明成引用類型可以避免對元素的拷貝。又因為不需要對string對象做寫操作,所以s被聲明成對常量的引用。
邏輯非運算符
邏輯非運算符(!)將運算對象的值取反后返回。下面再舉一個例子,假設vec是一個整數類型的vector對象,可以使用邏輯非運算符將empty函數的返回值取反從而檢查vec是否含有元素:
// print the first element in vec if there is one if (!vec.empty())cout << vec[0];子表達式
!vec.empty()//vec.empty()==false的簡寫當empty函數返回假時結果為真。
關系運算符
顧名思義,關系運算符比較運算對象的大小關系并返回布爾值。關系運算符都滿足左結合律。
因為關系運算符的求值結果是布爾值,所以將幾個關系運算符連寫在一起會產生意想不到的結果:
// oops! this condition compares k to the bool result of i < j if (i < j < k) // true if k is greater than 1! errorif語句的條件部分首先把i、j和第一個<運算符組合在一起,其返回的布爾值再作為第二個<運算符的左側運算對象。也就是說,k 比較的對象是第一次比較得到的那個或真或假的結果!要想實現我們的目的,其實應該使用下面的表達式:
// ok: condition is true if i is smaller than j and j is smaller than k if (i < j && j < k) { /* ... */ }相等性測試與布爾字面值
如果想測試一個算術對象或指針對象的真值,最直接的方法就是將其作為if語句的條件:
if (val) { /* ... */ } // true if val is any nonzero value if (!val) { /* ... */ } // true if val is zero在上面的兩個條件中,編譯器都將val轉換成布爾值。如果val非0則第一個條件為真,如果val的值為О則第二個條件為真。
有時會試圖將上面的真值測試寫成如下形式:
if (val == true) { /* ... */ } // true only if val is equal to 1!但是這種寫法存在兩個問題:首先,與之前的代碼相比,上面這種寫法較長而且不太直接(盡管大家都認為縮寫的形式對初學者來說有點難理解);更重要的一點是,如果val不是布爾值,這樣的比較就失去了原來的意義。
如果val不是布爾值,那么進行比較之前會首先把 true 轉換成val的類型。也就是說,如果val不是布爾值,則代碼可以改寫成如下形式:
if (val == 1) { /* ... */ }正如我們已經非常熟悉的那樣,當布爾值轉換成其他算術類型時,false轉換成0而true轉換成1。如果真想知道val的值是否是1,應該直接寫出1這個數值來,而不要與true 比較。
進行比較運算時除非比較的對象是布爾類型,否則不要使用布爾字面值true和false作為運算對象。
賦值運算符
賦值運算符的左側運算對象必須是一個可修改的左值。如果給定
int i = 0, j = 0, k = 0; // initializations, not assignment const int ci = i; // initialization, not assignment則下面的賦值語句都是非法的:
1024 = k; // error: literals are rvalues i + j = k; // error: arithmetic expressions are rvalues ci = k; // error: ci is a const (nonmodifiable) lvalue賦值運算的結果是它的左側運算對象,并且是一個左值。相應的,結果的類型就是左側運算對象的類型。如果賦值運算符的左右兩個運算對象類型不同,則右側運算對象將轉換成左側運算對象的類型:
k = 0; // result: type int, value 0 k = 3.14159; // result: type int, value 3C++11新標準允許使用花括號括起來的初始值列表作為賦值語句的右側運算對象:
k = {3.14}; // error: narrowing conversion vector<int> vi; // initially empty vi = {0,1,2,3,4,5,6,7,8,9}; // vi now has ten elements, values 0 through 9如果左側運算對象是內置類型,那么初始值列表最多只能包含一個值,而且該值即使轉換的話其所占空間也不應該大于目標類型的空間。
對于類類型來說,賦值運算的細節由類本身決定。對于vector來說,vector模板重載了賦值運算符并且可以接收初始值列表,當賦值發生時用右側運算對象的元素替換左側運算對象的元素。
無論左側運算對象的類型是什么,初始值列表都可以為空。此時,編譯器創建一個值初始化的臨時量并將其賦給左側運算對象。
賦值運算滿足右結合律
賦值運算符滿足右結合律,這一點與其他二元運算符不太一樣:
int ival, jval; ival = jval = 0; // ok: each assigned 0因為賦值運算符滿足右結合律,所以靠右的賦值運算jval=0 作為靠左的賦值運算符的右側運算對象。又因為賦值運算返回的是其左側運算對象,所以靠右的賦值運算的結果(即jval)被賦給了ival。
對于多重賦值語句中的每一個對象,它的類型或者與右邊對象的類型相同、或者可由右邊對象的類型轉換得到:
int ival, *pval; // ival is an int; pval is a pointer to int ival = pval = 0; // error: cannot assign the value of a pointer to an int string s1, s2; s1 = s2 = "OK"; // string literal "OK" converted to string因為ival和pval的類型不同,而且 pval 的類型(int*)無法轉換成ival 的類型(int),所以盡管0這個值能賦給任何對象,但是第一條賦值語句仍然是非法的。
與之相反,第二條賦值語句是合法的。這是因為字符串字面值可以轉換成string對象并賦給s2,而s2和s1的類型相同,所以s2的值可以繼續賦給s1。
賦值運算優先級較低
賦值語句經常會出現在條件當中。因為賦值運算的優先級相對較低,所以通常需要給賦值部分加上括號使其符合我們的原意。下面這個循環說明了把賦值語句放在條件當中有什么用處,它的目的是反復調用一個函數直到返回期望的值(比如42)為止:
// a verbose and therefore more error-prone way to write this loop int i = get_value(); // get the first value while (i != 42) {// do something ...i = get_value(); // get remaining values }在這段代碼中,首先調用get_value函數得到一個值,然后循環部分使用該值作為條件。在循環體內部,最后一條語句會再次調用get_value函數并不斷重復循環。可以將上述代碼以更簡單直接的形式表達出來:
int i; // a better way to write our loop---what the condition does is now clearer while ((i = get_value()) != 42) {// do something ... }這個版本的while條件更容易表達我們的真實意圖:不斷循環讀取數據直至遇到42為止。其處理過程是首先將get_value函數的返回值賦給i,然后比較i和42是否相等。
如果不加括號的話含義會有很大變化,比較運算符!=的運算對象將是get_value函數的返回值及42,比較的結果不論真假將以布爾值的形式賦值給i,這顯然不是我們期望的結果。
因為賦值運算符的優先級低于關系運算符的優先級,所以在條件語句中,賦值部分通常應該加上括號。
切勿混淆相等運算符和賦值運算符
C++語言允許用賦值運算作為條件,但是這一特性可能帶來意想不到的后果:
if (i = j)此時,if語句的條件部分把j的值賦給i,然后檢查賦值的結果是否為真。如果j不為0,條件將為真。然而程序員的初衷很可能是想判斷i和j是否相等:
if (i == j)程序的這種缺陷顯然很難被發現,好在一部分編譯器會對類似的代碼給出警告信息。
復合賦值運算符
我們經常需要對對象施以某種運算,然后把計算的結果再賦給該對象。舉個例子,考慮1.4.2節(第11頁)的求和程序:
int sum = 0; // sum values from 1 through 10 inclusive for (int val = 1; val <= 10; ++val)sum += val; // equivalent to sum = sum + val這種復合操作不僅對加法來說很常見,而且也常常應用于其他算術運算符或者將要介紹的位運算符。每種運算符都有相應的復合賦值形式:
+= -= *= /= %= // arithmetic operators <<= >>= &= ^= |= // bitwise operators任意一種復合運算符都完全等價于
a = a op b;唯一的區別是左側運算對象的求值次數:使用復合運算符只求值一次,使用普通的運算符則求值兩次。這兩次包括:一次是作為右邊子表達式的一部分求值,另一次是作為賦值運算的左側運算對象求值。其實在很多地方,這種區別除了對程序性能有些許影響外幾乎可以忽略不計。
遞增和遞減運算符
遞增運算符(++)和遞減運算符(–)為對象的加1和減1操作提供了一種簡潔的書寫形式。這兩個運算符還可應用于迭代器,因為很多迭代器本身不支持算術運算,所以此時遞增和遞減運算符除了書寫簡潔外還是必須的。
遞增和遞減運算符有兩種形式:前置版本和后置版本。
- 到目前為止,之前的程序使用的都是前置版本,這種形式的運算符首先將運算對象加1(或減1),然后將改變后的對象作為求值結果。
- 后置版本也會將運算對象加1(或減1),但是求值結果是運算對象改變之前那個值的副本:
這兩種運算符必須作用于左值運算對象。
- 前置版本將對象本身作為左值返回,
- 后置版本則將對象原始值的副本作為右值返回。
建議:除非必須,否則不用遞增遞減運算符的后置版本
有C語言背景的讀者可能對優先使用前置版本遞增運算符有所疑問,其實原因非常簡單:前置版本的遞增運算符避免了不必要的工作,它把值加1后直接返回改變了的運算對象。與之相比,后置版本需要將原始值存儲下來以便于返回這個未修改的內容。如果我們不需要修改前的值,那么后置版本的操作就是一種浪費。
對于整數和指針類型來說,編譯器可能對這種額外的工作進行一定的優化,但是對于相對復雜的迭代器類型,這種額外的工作就消耗巨大了。建議養成使用前置版本的習慣,這樣不僅不需要擔心性能的問題,而且更重要的是寫出的代碼會更符合編程的初衷。
在一條語句中混用解引用和遞增運算符
如果我們想在一條復合表達式中既將變量加1或減1又能使用它原來的值,這時就可以使用遞增和遞減運算符的后置版本。
舉個例子,可以使用后置的遞增運算符來控制循環輸出一個vector對象內容直至遇到(但不包括)第一個負值為止:
auto pbeg = v.begin(); // print elements up to the first negative value while (pbeg != v.end() && *beg >= 0)cout << *pbeg++ << endl; // print the current value and advance pbeg對于剛接觸C++和C的程序員來說,*pbeg++不太容易理解。其實這種寫法非常普遍,所以程序員一定要理解其含義。
后置遞增運算符的優先級高于解引用運算符,因此*pbeg++等價于*(pbeg++)。pbeg++把pbeg 的值加1,然后返回pbeg的初始值的副本作為其求值結果,此時解引用運算符的運算對象是pbeg未增加之前的值。最終,這條語句輸出pbeg開始時指向的那個元素,并將指針向前移動一個位置。
這種用法完全是基于一個事實,即后置遞增運算符返回初始的未加1的值。如果返回的是加1之后的值,解引用該值將產生錯誤的結果。不但無法輸出第一個元素,而且更糟糕的是如果序列中沒有負值,程序將可能試圖解引用一個根本不存在的元素。
建議:簡潔可以成為一種美德
形如*pbeg++的表達式一開始可能不太容易理解,但其實這是一種被廣泛使用的、有效的寫法。當對這種形式熟悉之后,書寫
cout << *iter++ << endl;要比書寫下面的等價語句更簡潔、也更少出錯
cout << *iter << endl; ++iter;不斷研究這樣的例子直到對它們的含義一目了然。大多數C++程序追求簡潔、摒棄冗長,因此C++程序員應該習慣于這種寫法。而且,一旦熟練掌握了這種寫法后,程序出錯的可能性也會降低。
運算對象可按任意順序求值
大多數運算符都沒有規定運算對象的求值順序,這在一般情況下不會有什么影響。
然而,如果一條子表達式改變了某個運算對象的值,另一條子表達式又要使用該值的話,運算對象的求值順序就很關鍵了。因為遞增運算符和遞減運算符會改變運算對象的值,所以要提防在復合表達式中錯用這兩個運算符。
為了說明這一問題,使用for循環將輸入的第一個單詞改成大寫形式:
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)*it = toupper(*it); // capitalize the current character在上述程序中,我們把解引用it 和遞增it 兩項任務分開來完成。如果用一個看似等價的while循環進行代替
// the behavior of the following loop is undefined! while (beg != s.end() && !isspace(*beg))*beg = toupper(*beg++); // error: this assignment is undefined將產生未定義的行為。問題在于:賦值運算符左右兩端的運算對象都用到了beg,并且右側的運算對象還改變了beg 的值,所以該賦值語句是未定義的。
編譯器可能按照下面的任意一種思路處理該表達式:
*beg = toupper(*beg); // execution if left-hand side is evaluated first *(beg + 1) = toupper(*beg); // execution if right-hand side is evaluated first也可能采取別的什么方式處理它。
成員訪問運算符
點運算符和箭頭運算符都可用于訪問成員,其中,點運算符獲取類對象的一個成員,箭頭運算符與點運算符有關,表達式ptr->mem等價于(*ptr).mem:
string s1 = "a string", *p = &s1; auto n = s1.size(); // run the size member of the string s1 n = (*p).size(); // run size on the object to which p points n = p->size(); // equivalent to (*p).size()因為解引用運算符的優先級低于點運算符,所以執行解引用運算的子表達式兩端必須加上括號。如果沒加括號,代碼的含義就大不相同了:
// run the size member of p, then dereference the result! *p.size(); // error: p is a pointer and has no member named size這條表達式試圖訪問對象p的size成員,但是p本身是一個指針且不包含任何成員,所以上述語句無法通過編譯。
箭頭運算符作用于一個指針類型的運算對象,結果是一個左值。
點運算符分成兩種情況:
條件運算符
條件運算符(?:)允許我們把簡單的if-else邏輯嵌入到單個表達式當中,條件運算符按照如下形式使用:
cond ? expr1 : expr2;其中 cond是判斷條件的表達式,而expr1和expr2是兩個類型相同或可能轉換為某個公共類型的表達式。條件運算符的執行過程是:首先求cond 的值,如果條件為真對expr1求值并返回該值,否則對expr2求值并返回該值。舉個例子,我們可以使用條件運算符判斷成績是否合格:
string finalgrade = (grade < 60) ? "fail" : "pass";條件部分判斷成績是否小于60。如果小于,表達式的結果是"fail",否則結果是"pass"。有點類似于邏輯與運算符和邏輯或運算符( &&和||),條件運算符只對exprl和expr2中的一個求值。
當條件運算符的兩個表達式都是左值或者能轉換成同一種左值類型時,運算的結果是左值:否則運算的結果是右值。
嵌套條件運算符
允許在條件運算符的內部嵌套另外一個條件運算符。也就是說,條件表達式可以作為另外一個條件運算符的cond或expr。舉個例子,使用一對嵌套的條件運算符可以將成績分成三檔:優秀(high pass)、合格( pass)和不合格( fail ):
finalgrade = (grade > 90) ? "high pass": (grade < 60) ? "fail" : "pass";第一個條件檢查成績是否在90分以上,如果是,執行符號?后面的表達式,得到"highpass";如果否,執行符號:后面的分支。這個分支本身又是一個條件表達式,它檢查成績是否在60分以下,如果是,得到"fail";否則得到"pass"。
條件運算符滿足右結合律,意味著運算對象(一般)按照從右向左的順序組合。因此在上面的代碼中,靠右邊的條件運算(比較成績是否小于60)構成了靠左邊的條件運算的:分支。
隨著條件運算嵌套層數的增加,代碼的可讀性急劇下降。因此,條件運算的嵌套最好別超過兩到三層。
在輸出表達式中使用條件運算符
條件運算符的優先級非常低,因此當一條長表達式中嵌套了條件運算子表達式時,通常需要在它兩端加上括號。例如,有時需要根據條件值輸出兩個對象中的一個,如果寫這條語句時沒把括號寫全就有可能產生意想不到的結果:
cout << ((grade < 60) ? "fail" : "pass"); // prints pass or fail cout << (grade < 60) ? "fail" : "pass"; // prints 1 or 0! cout << grade < 60 ? "fail" : "pass"; // error: compares cout to 60在第二條表達式中, grade和60的比較結果是<<運算符的運算對象,因此如果grade<60為真輸出1,否則輸出0。<<運算符的返回值是cout,接下來cout作為條件運算符的條件。也就是說,第二條表達式等價于
cout << (grade < 60); // prints 1 or 0 cout ? "fail" : "pass"; // test cout and then yield one of the two literals// depending on whether cout is true or false因為第三條表達式等價于下面的語句,所以它是錯誤的:
cout << grade; // less-than has lower precedence than shift, so print grade first cout < 60 ? "fail" : "pass"; // then compare cout to 60!位運算符
位運算符作用于整數類型的運算對象,并把運算對象看成是二進制位的集合。位運算符提供檢查和設置二進制位的功能,將會要介紹的,一種名為bitset的標準庫類型也可以表示任意大小的二進制位集合,所以位運算符同樣能用于bitset類型。
左結合律
| ~ | 位求反 | expr |
| << >> | 左移 右移 | expr1 << expr2 expr1 >> expr2 |
| & | 位與 | expr & expr |
| ^ | 位異或 | expr ^ expr |
| | | 位或 | expr | expr |
一般來說,如果運算對象是“小整型”,則它的值會被自動提升成較大的整數類型。運算對象可以是帶符號的,也可以是無符號的。如果運算對象是帶符號的且它的值為負,那么位運算符如何處理運算對象的“符號位”依賴于機器。而且,此時的左移操作可能會改變符號位的值,因此是一種未定義的行為。(不像Java那樣,用>>>和>>來處理負號位)
關于符號位如何處理沒有明確的規定,所以強烈建議僅將位運算符用于處理無符號類型。
移位運算符
之前在處理輸入和輸出操作時,我們已經使用過標準IO庫定義的<<運算符和>>運算符的重載版本。
這兩種運算符的內置含義是對其運算對象執行基于二進制位的移動操作,
其中,右側的運算對象一定不能為負,而且值必須嚴格小于結果的位數,否則就會產生未定義的行為。二進制位或者向左移(<<)或者向右移(>>),移出邊界之外的位就被舍棄掉了:
在下面的例子中右側為最低位并且假定char占8位、int占32位。
unsigned char bits = 0233;//0233是八進制的字面值bits << 8 //bits提升成int類型,然后向左移動8位
bits << 31 //向左移動31位,左邊超出邊界的位丟棄掉了
bits >> 3 //向右移動3位,最右邊的3位丟棄掉了
左移運算符(<<)在右側插入值為0的二進制位。右移運算符(>>)的行為則依賴于其左側運算對象的類型:
- 如果該運算對象是無符號類型,在左側插入值為0的二進制位;
- 如果該運算對象是帶符號類型,在左側插入符號位的副本或值為0的二進制位,如何選擇要視具體環境而定。
位求反運算符
位求反運算符(~)將運算對象逐位求反后生成一個新值,將1置為0、將0置為1:
unsigned char bits = 0233;//0233是八進制的字面值~bits
char類型的運算對象首先提升成int類型,提升時運算對象原來的位保持不變,往高位(high order position)添加0即可。因此在本例中,首先將bits提升成int類型,增加24個高位0,隨后將提升后的值逐位求反。
位與、位或、位異或運算符
與(&)、或(|)、異或(^)運算符在兩個運算對象上逐位執行相應的邏輯操作:
| unsigned char b1 = 0145; | - | 0 1 1 0 0 1 0 1 |
| unsigned char b2 = 0257; | - | 1 0 1 0 1 1 1 1 |
| b1 & b2 | 24個高階位都是0 | 0 0 1 0 0 1 0 1 |
| b1 | b2 | 24個高階位都是0 | 1 1 1 0 1 1 1 1 |
| b1 ^ b2 | 24個高階位都是0 | 1 1 0 0 1 0 1 0 |
-
對于位與運算符(&)來說,如果兩個運算對象的對應位置都是1則運算結果中該位為1,否則為0。
-
對于位或運算符(|)來說,如果兩個運算對象的對應位置至少有一個為1則運算結果中該位為1,否則為0。
-
對于位異或運算符(^)來說,如果兩個運算對象的對應位置有且只有一個為1則運算結果中該位為1,否則為0。
有一種常見的錯誤是把位運算符和邏輯運算符搞混了,比如位與(&)和邏輯與(&&)、位或(I)和邏輯或(1l)、位求反(~)和邏輯非(!)
使用位運算符
(位圖法)
我們舉一個使用位運算符的例子:假設班級中有30個學生,老師每周都會對學生進行一次小測驗,測驗的結果只有通過和不通過兩種。為了更好地追蹤測驗的結果,我們用一個二進制位代表某個學生在一次測驗中是否通過,顯然全班的測驗結果可以用一個無符號整數來表示:
unsigned long quiz1 = 0; //我們把這個值當成是位的集合來使用定義quiz1的類型是unsigned long,這樣,quiz1在任何機器上都將至少擁有32位;給quiz1賦一個明確的初始值,使得它的每一位在開始時都有統一且固定的值。
教師必須有權設置并檢查每一個二進制位。例如,我們需要對序號為27的學生對應的位進行設置,以表示他通過了測驗。為了達到這一目的,首先創建一個值,該值只有第27位是1其他位都是0,然后將這個值與quiz1進行位或運算,這樣就能強行將quiz1的第27位設置為1,其他位都保持不變。
為了實現本例的目的,我們將quiz1的低階位賦值為0、下一位賦值為1,以此類推,最后統計quiz1各個位的情況。
使用左移運算符和一個unsigned long類型的整數字面值1就能得到一個表示學生27通過了測驗的數值:
1UL << 27 //生成一個值,該值只有第27位為1指定數位置1
1U 的低階位上有一個1,除此之外(至少)還有31個值為0的位。之所以使用unsignedlong類型,是因為int類型只能確保占用16位,而我們至少需要27位。上面這條表達式通過在值為1的那個二進制位后面添加0,使得它向左移動了27位。
接下來將所得的值與quiz1進行位或運算。為了同時更新quiz1的值,使用一條復合賦值語句:
quiz1 |= 1UL<< 27; //表示學生27通過了測驗|-運算符的工作原理和+=非常相似,它等價于
quiz1 = quiz1 | 1UL << 27; //等價于quiz1 l= 1UL << 27;指定數位置0
假定教師在重新核對測驗結果時發現學生27實際上并沒有通過測驗,他必須要把第27位的值置為0。此時我們需要使用一個特殊的整數,它的第27位是0、其他所有位都是1。將這個值與quiz1進行位與運算就能實現目的了:
quiz1 &= ~(1UL << 27); //學生27沒有通過測驗通過將之前的值按位求反得到一個新值,除了第27位外都是1,只有第27位的值是0。隨后將該值與quiz1進行位與運算,所得結果除了第27位外都保持不變。
查指定數位
最后,我們試圖檢查學生27測驗的情況到底怎么樣:
bool status = quiz1 & (1UL << 27);// 學生27是否通過了測驗?我們將quiz1和一個只有第27位是1的值按位求與,如果quiz1的第27位是1,計算的結果就是非0(真);否則結果是0。
小結
移位運算符(又叫IO運算符)滿足左結合律
盡管很多程序員從未直接用過位運算符,但是幾乎所有人都用過它們的重載版本來進行IO操作。重載運算符的優先級和結合律都與它的內置版本一樣,因此即使程序員用不到移位運算符的內置含義,也仍然有必要理解其優先級和結合律。
因為移位運算符滿足左結合律,所以表達式
cout<< "hi" << " there" << endl;的執行過程實際上等同于
((cout <<"hi") << " there" ) << endl;在這條語句中,運算對象"hi"和第一個<<組合在一起,它的結果和第二個<<組合在一起,接下來的結果再和第三個<<組合在一起。
移位運算符的優先級不高不低,介于中間:比算術運算符的優先級低,但比關系運算符、賦值運算符和條件運算符的優先級高。因此在一次使用多個運算符時,有必要在適當的地方加上括號使其滿足我們的要求。
cout << 42 + 10; //正確:+的優先級更高,因此輸出求和結果 cout << (10 < 42); //正確:括號使運算對象按照我們的期望組合在一起,輸出1 cout << 10 < 42; //錯誤:試圖比較cout和42!最后一個cout的含義其實是
(cout << 10) < 42;也就是“把數字10寫到cout,然后將結果(即 cout)與42進行比較”。
sizeof運算符
sizeof運算符返回一條表達式或一個類型名字所占的字節數。sizeof運算符滿足右結合律,其所得的值是一個size_t類型的常量表達式。運算符的運算對象有兩種形式:
sizeof (type) sizeof expr在第二種形式中,sizeof返回的是表達式結果類型的大小。與眾不同的一點是,sizeof并不實際計算其運算對象的值:
Sales_data data, *p; sizeof(Sales_data); // size required to hold an object of type Sales_data sizeof data; // size of data's type, i.e., sizeof(Sales_data) sizeof p; // size of a pointer sizeof *p; // size of the type to which p points, i.e., sizeof(Sales_data) sizeof data.revenue; // size of the type of Sales_data's revenue member sizeof Sales_data::revenue; // alternative way to get the size of revenue這些例子中最有趣的一個是sizeof *p。
-
首先,因為sizeof滿足右結合律并且與*運算符的優先級一樣,所以表達式按照從右向左的順序組合。也就是說,它等價于sizeof(*p)。
-
其次,因為sizeof不會實際求運算對象的值,所以即使p是一個無效(即未初始化)的指針也不會有什么影響。在sizeof的運算對象中解引用一個無效指針仍然是一種安全的行為,因為指針實際上并沒有被真正使用。sizeof不需要真的解引用指針也能知道它所指對象的類型。
C++ 11新標準允許我們使用作用域運算符來獲取類成員的大小。通常情況下只有通過類的對象才能訪問到類的成員,但是sizeof運算符無須我們提供一個具體的對象,因為要想知道類成員的大小無須真的獲取該成員。
sizeof運算符的結果部分地依賴于其作用的類型:
-
對char或者類型為char的表達式執行sizeof運算,結果得1。
-
對引用類型執行sizeof運算得到被引用對象所占空間的大小。
-
對指針執行sizeof運算得到指針本身所占空間的大小。
-
對解引用指針執行sizeof運算得到指針指向的對象所占空間的大小,指針不需有效。
-
對數組執行sizeof運算得到整個數組所占空間的大小,等價于對數組中所有的元素各執行一次sizeof運算并將所得結果求和。注意,sizeof運算不會把數組轉換成指針來處理。
-
對string對象或vector對象執行sizeof運算只返回該類型固定部分的大小,不會計算對象中的元素占用了多少空間。(這個要注意)
因為執行sizeof運算能得到整個數組的大小,所以可以用數組的大小除以單個元素的大小得到數組中元素的個數:
// sizeof(ia)/sizeof(*ia) returns the number of elements in ia constexpr size_t sz = sizeof(ia) / sizeof(*ia); int arr2[sz]; // ok sizeof returns a constant expression因為sizeof的返回值是一個常量表達式,所以我們可以用sizeof的結果聲明數組的維度。
逗號運算符
逗號運算符(comma operator)含有兩個運算對象,按照從左向右的順序依次求值。和邏輯與、邏輯或以及條件運算符一樣,逗號運算符也規定了運算對象求值的順序。
對于逗號運算符來說,首先對左側的表達式求值,然后將求值結果丟棄掉。逗號運算符真正的結果是右側表達式的值。如果右側運算對象是左值,那么最終的求值結果也是左值。
逗號運算符經常被用在for循環當中:
vector<int>::size_type cnt = ivec.size(); // assign values from size... 1 to the elements in ivec for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)ivec[ix] = cnt;這個循環在for語句的表達式中遞增ix、遞減cnt,每次循環迭代ix和cnt相應改變。只要ix滿足條件,我們就把當前元素設成cnt的當前值。
類型轉換
在C++語言中,某些類型之間有關聯。如果兩種類型有關聯,那么當程序需要其中一種類型的運算對象時,可以用另一種關聯類型的對象或值來替代。換句話說,如果兩種類型可以相互轉換(conversion),那么它們就是關聯的。
舉個例子,考慮下面這條表達式,它的目的是將ival初始化為6:
int ival = 3.541 + 3;//編譯器可能會警告該運算損失了精度加法的兩個運算對象類型不同:3.541的類型是double,3的類型是int。C++語言不會直接將兩個不同類型的值相加,而是先根據類型轉換規則設法將運算對象的類型統一后再求值。上述的類型轉換是自動執行的,無須程序員的介入,有時甚至不需要程序員了解。因此,它們被稱作隱式轉換(implicit conversion)。
算術類型之間的隱式轉換被設計得盡可能避免損失精度。很多時候,如果表達式中既有整數類型的運算對象也有浮點數類型的運算對象,整型會轉換成浮點型。在上面的例子中,3轉換成double類型,然后執行浮點數加法,所得結果的類型是double。
接下來就要完成初始化的任務了。在初始化過程中,因為被初始化的對象的類型無法改變,所以初始值被轉換成該對象的類型。仍以這個例子說明,加法運算得到的double類型的結果轉換成int類型的值,這個值被用來初始化ival。由 double向 int 轉換時忽略掉了小數部分,上面的表達式中,數值6被賦給了ival。
何時發生隱式類型轉換
在下面這些情況下,編譯器會自動地轉換運算對象的類型:
- 在大多數表達式中,比int類型小的整型值首先提升為較大的整數類型。
- 在條件中,非布爾值轉換成布爾類型。
- 初始化過程中,初始值轉換成變量的類型;在賦值語句中,右側運算對象轉換成左側運算對象的類型。
- 如果算術運算或關系運算的運算對象有多種類型,需要轉換成同一種類型。
- 函數調用時也會發生類型轉換。(未來會介紹)
算術轉換
算術轉換(arithmetic conversion)的含義是把一種算術類型轉換成另外一種算術類型,前些章節有介紹。算術轉換的規則定義了一套類型轉換的層次,其中運算符的運算對象將轉換成最寬的類型。
例如,
- 如果一個運算對象的類型是 long double,那么不論另外一個運算對象的類型是什么都會轉換成long double。
- 還有一種更普遍的情況,當表達式中既有浮點類型也有整數類型時,整數值將轉換成相應的浮點類型。
整型提升
整型提升(integral promotion)負責把小整數類型轉換成較大的整數類型。
對于bool、char、signed char、 unsigned char、short和 unsigned short等類型來說,只要它們所有可能的值都能存在 int 里,它們就會提升成int 類型;否則,提升成unsigned int類型。就如我們所熟知的,布爾值false提升成0、true提升成1。
較大的char類型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和 unsigned long long中最小的一種類型,前提是轉換后的類型要能容納原類型所有可能的值。
無符號類型的運算對象
如果某個運算符的運算對象類型不一致,這些運算對象將轉換成同一種類型。但是如果某個運算對象的類型是無符號類型,那么轉換的結果就要依賴于機器中各個整數類型的相對大小了。
像往常一樣,首先執行整型提升。如果結果的類型匹配,無須進行進一步的轉換。如果兩個(提升后的)運算對象的類型要么都是帶符號的、要么都是無符號的,則小類型的運算對象轉換成較大的類型。
如果一個運算對象是無符號類型、另外一個運算對象是帶符號類型:
-
其中的無符號類型不小于(>=)帶符號類型,那么帶符號的運算對象轉換成無符號的。
- 例如,假設兩個類型分別是unsigned int和 int,則 int類型的運算對象轉換成unsigned int類型。
- 需要注意的是,如果int型的值恰好為負值,其結果感人(具體轉換方法回看第二章筆記),并帶來該節描述的所有副作用。
-
剩下的一種情況是帶符號類型大于(>)無符號類型,此時轉換的結果依賴于機器。
- 如果無符號類型的所有值都能存在該帶符號類型中,則無符號類型的運算對象轉換成帶符號類型。
- 如果不能,那么帶符號類型的運算對象轉換成無符號類型。
- 例如,
- 如果兩個運算對象的類型分別是long和 unsigned int,并且int和 long的大小相同,則long類型的運算對象轉換成unsigned int 類型;(帶符號的運算對象轉換成無符號的)
- 如果 long類型占用的空間比 int更多,則unsigned int類型的運算對象轉換成long類型。
(Note:好麻煩)
理解算術轉換
要想理解算術轉換,辦法之一就是研究大量的例子:
bool flag; char cval; short sval; unsigned short usval; int ival; unsigned int uival; long lval; unsigned long ulval; float fval; double dval; 3.14159L + 'a'; //1. 'a' promoted to int, then that int converted to long double dval + ival; // ival converted to double dval + fval; // fval converted to double ival = dval; // dval converted (by truncation) to int flag = dval; // if dval is 0, then flag is false, otherwise true cval + fval; // cval promoted to int, then that int converted to float sval + cval; // sval and cval promoted to int cval + lval; // cval converted to long ival + ulval; // ival converted to unsigned long usval + ival; //2. promotion depends on the size of unsigned short and int uival + lval; // conversion depends on the size of unsigned int and long在第一個加法運算中,小寫字母’a’是char型的字符常量,它其實能表示一個數字值。到底這個數字值是多少完全依賴于機器上的字符集,在我們的環境中,'a’對應的數字值是97。當把’a’和一個 long double類型的數相加時,char類型的值首先提升成int類型,然后int類型的值再轉換成long double類型。最終我們把這個轉換后的值與那個字面值相加。
最后的兩個含有無符號類型值的表達式也比較有趣,它們的結果依賴于機器。(Note:有趣?Excuse me.)
其他隱式顯示轉換
除了算術轉換之外還有幾種隱式類型轉換,包括如下幾種。
數組轉換成指針:在大多數用到數組的表達式中,數組自動轉換成指向數組首元素的指針:
int ia[10]; // array of ten ints int* ip = ia; // convert ia to a pointer to the first element當數組被用作decltype關鍵字的參數,或者作為取地址符(&)、sizeof及typeid等運算符的運算對象時,上述轉換不會發生。同樣的,如果用一個引用來初始化數組,上述轉換也不會發生。在將來會看到,當在表達式中使用函數類型時會發生類似的指針轉換。
指針的轉換:C++還規定了幾種其他的指針轉換方式,包括
- 常量整數值0或者字面值nullptr能轉換成任意指針類型;
- 指向任意非常量的指針能轉換成void*;
- 指向任意對象的指針能轉換成const void*。
- 未來將要介紹,在有繼承關系的類型間還有另外一種指針轉換的方式。
轉換成布爾類型:存在一種從算術類型或指針類型向布爾類型自動轉換的機制。如果指針或算術類型的值為0,轉換結果是false;否則轉換結果是true:
char *cp = get_string(); if (cp) /* ... */ // true if the pointer cp is not zero while (*cp) /* ... */ // true if *cp is not the null character轉換成常量:允許將指向非常量類型的指針轉換成指向相應的常量類型的指針,對于引用也是這樣。也就是說,如果T是一種類型,我們就能將指向T的指針或引用分別轉換成指向const T的指針或引用:
int i; const int &j = i; // convert a nonconst to a reference to const int const int *p = &i; // convert address of a nonconst to the address of a const int &r = j, *q = p; // error: conversion from const to nonconst not allowed相反的轉換并不存在,因為它試圖刪除掉底層const。
類類型定義的轉換:類類型能定義由編譯器自動執行的轉換,不過編譯器每次只能執行一種類類型的轉換。在第7章,我們將看到一個例子,如果同時提出多個轉換請求,這些請求將被拒絕。
我們之前的程序已經使用過類類型轉換:一處是在需要標準庫string類型的地方使用C風格字符串;另一處是在條件部分讀入istream:
string s, t = "a value"; // character string literal converted to type string while (cin >> s) // while condition converts cin to bool條件 (cin>>s) 讀入cin的內容并將cin作為其求值結果。條件部分本來需要一個布爾類型的值,但是這里實際檢查的是istream類型的值。
幸好,IO庫定義了從istream向布爾值轉換的規則,根據這一規則,cin自動地轉換成布爾值。所得的布爾值到底是什么由輸入流的狀態決定,如果最后一次讀入成功,轉換得到的布爾值是 true;相反,如果最后一次讀入不成功,轉換得到的布爾值是false。
顯示轉換
有時我們希望顯式地將對象強制轉換成另外一種類型。例如,如果想在下面的代碼中執行浮點數除法:
int i,j; double slope = i/j;就要使用某種方法將i和/或j顯式地轉換成double,這種方法稱作強制類型轉換(cast)。
WARNING:雖然有時不得不使用強制類型轉換,但這種方法本質上是非常危險的。
命名的強制類型轉換
一個命名的強制類型轉換具有如下形式:
cast-name<type>(expression);其中,type是轉換的目標類型而expression是要轉換的值。如果type是引用類型,則結果是左值。cast-name是 static_cast、dynamic_cast、const_cast和reinterpret_cast中的一種。cast-name指定了執行的是哪種轉換。
dynamic_cast將在最后一章介紹。
static_cast
任何具有明確定義的類型轉換,只要不包含底層const,都可以使用static_cast。例如,通過將一個運算對象強制轉換成double類型就能使表達式執行浮點數除法:
//進行強制類型轉換以便執行浮點數除法 double slope = static_cast<double>(j) / i;當需要把一個較大的算術類型賦值給較小的類型時,static_cast非常有用。此時,強制類型轉換告訴程序的讀者和編譯器:我們知道并且不在乎潛在的精度損失。一般來說,如果編譯器發現一個較大的算術類型試圖賦值給較小的類型,就會給出警告信息;但是當我們執行了顯式的類型轉換后,警告信息就會被關閉了。
static_cast對于編譯器無法自動執行的類型轉換也非常有用。例如,我們可以使用static_cast找回存在于void*指針中的值:
void* p = &d;//正確:任何非常量對象的地址都能存入void* //正確:將void*轉換回初始的指針類型 double *dp = static_cast<double*>(p) ;當我們把指針存放在void*中,并且使用static_cast將其強制轉換回原來的類型時,應該確保指針的值保持不變。也就是說,強制轉換的結果將與原始的地址值相等,因此我們必須確保轉換后所得的類型就是指針所指的類型。
類型一旦不符,將產生未定義的后果。
(Note:Java的類轉換錯誤會拋出ClassCastException)
const_cast
const_cast只能改變運算對象的底層const(第二章內容):
const char *pc; char *p = const_cast<char*>(pc);// 正確:但是通過p 寫值是未定義的行為對于將常量對象轉換成非常量對象的行為,我們一般稱其為“去掉const性質(cast away the const)”。一旦我們去掉了某個對象的const性質,編譯器就不再阻止我們對該對象進行寫操作了。如果對象本身不是一個常量,使用強制類型轉換獲得寫權限是合法的行為。
然而如果對象是一個常量,再使用const_cast執行寫操作就會產生未定義的后果。
只有const_cast能改變表達式的常量屬性,使用其他形式的命名強制類型轉換改變表達式的常量屬性都將引發編譯器錯誤。同樣的,也不能用const_cast改變表達式的類型:
const char *cp; //錯誤: static_cast不能轉換掉const性質 char *q = static_cast<char*> (cp) ;static_cast<string>(cp);//正確:字符串字面值轉換成string類型const_cast<string>(cp);//錯誤:const_cast只改變常量屬性,下行才對 const_cast<char *>(cp);const_cast常常用于有函數重載的上下文中,關于函數重載將在第六章進行詳細介紹。
reinterpret_cast
reinterpret_cast通常為運算對象的位模式提供較低層次上的重新解釋。舉個例
子,假設有如下的轉換
我們必須牢記pc所指的真實對象是一個int而非字符,如果把pc當成普通的字符指針使用就可能在運行時發生錯誤。例如:
string str(pc);可能導致異常的運行時行為。
使用reinterpret_cast是非常危險的,用pc初始化str的例子很好地證明了這一點。其中的關鍵問題是類型改變了,但編譯器沒有給出任何警告或者錯誤的提示信息。當我們用一個int的地址初始化pc時,由于顯式地聲稱這種轉換合法,所以編譯器不會發出任何警告或錯誤信息。接下來再使用pc時就會認定它的值是char*類型,編譯器沒法知道它實際存放的是指向int的指針。
最終的結果就是,在上面的例子中雖然用pc初始化 str沒什么實際意義,甚至還可能引發更糟糕的后果,但僅從語法上而言這種操作無可指摘。查找這類問題的原因非常困難,如果將ip強制轉換成pc的語句和用pc初始化string對象的語句分屬不同文件就更是如此。Thus, the initialization of str with pc is absolutely correct—albeit盡管 in this case meaningless or worse! Tracking down the cause of this sort of problem can prove extremely difficult, especially if the cast of ip to pc occurs in a file separate from the one in which pc is used to initialize a string.
無可指摘
指沒有什么可以指責的。表示做得妥當。
link
WARNING:reinterpret_cast本質上依賴于機器,要想安全地使用reinterpret_cast必須對涉及的類型和編譯器實現轉換的過程都非常了解。
(Note:沒看懂reinterpret_cast作用,個人理解為"強人所難的強制類型轉換",TODO:查查它的作用)
建議:避免強制類型轉換
強制類型轉換干擾了正常的類型檢查,因此我們強烈建議程序員避免使用強制類型轉換。
這個建議對于reinterpret_cast尤其適用,因為此類類型轉換總是充滿了風險。
在有重載函數的上下文中使用const_cast無可厚非,關于這一點將在第六章中詳細介紹;但是在其他情況下使用const_cast也就意味著程序存在某種設計缺陷。其他強制類型轉換,比如 static_cast 和dynamic_cast,都不應該頻繁使用。
每次書寫了一條強制類型轉換語句,都應該反復斟酌能否以其他方式實現相同的目標。就算實在無法避免,也應該盡量限制類型轉換值的作用域,并且記錄對相關類型的所有假定,這樣可以減少錯誤發生的機會。
舊式的強制類型轉換
在早期版本的C++語言中,顯式地進行強制類型轉換包含兩種形式:
type (expr); // function-style cast notation (type) expr; // C-language-style cast notation根據所涉及的類型不同,舊式的強制類型轉換分別具有與const_cast 、static_cast或reinterpret_cast相似的行為。當我們在某處執行舊式的強制類型轉換時,如果換成const_cast和 static_cast也合法,則其行為與對應的命名轉換一致。
如果替換后不合法,則舊式強制類型轉換執行與reinterpret_cast類似的功能:
int *ip; char *pc = (char*) ip; // ip is a pointer to int的效果與使用reinterpret_cast一樣。
WARNING:與命名的強制類型轉換相比,舊式的強制類型轉換從表現形式上來說不那么清晰明了,容易被看漏,所以一旦轉換過程出現問題,追蹤起來也更加困難。
(Note:有新的就用新的,舊的擱置吧)
運算符優先級表
| 左 左 左 | :: :: :: | 全局作用域 類作用域 命名空間作用域 | ::name class::name namespace::name |
| 左 左 左 左 左 | . -> [] () () | 成員選擇 成員選擇 下標 函數調用 類型構造 | object.member pointer->member expr[expr] name(expr_list) type(expr_list) |
| 右 右 右 右 右 | +_ – typeid typeid explicit cast | 后置遞增運算 后置遞減運算 類型ID 運行時類型ID 類型轉換 | lvalue++ lvalue– typeid(type) typeid(expr) cast_name<type>(expr) |
| 右 右 右 右 右 右 右 右 右 右 右 右 右 右 右 右 右 | ++ – ~ ! - + * & () sizeof sizeof sizeof… new new[] delete delete[] noexcept | 前置遞增運算 前置遞減運算 位求反 邏輯非 一元負號 一元正號 解引用 取地址 類型轉換 對象的大小 類型的大小 參數包的大小 創建對象 創建數組 釋放對象 釋放數組 能否拋出異常 | ++lvalue –lvalue ~expr !expr -expr +expr *expr &lvalue (type)expr sizeof expr sizeof(type) sizeof–(name) new type new type[size] delete expr delete[] expr noexcept(expr) |
| 左 左 | ->* .* | 指向成員選擇的指針 指向成員選擇的指針 | ptr->*ptr_to_member obj.*ptr_to_member |
| 左 左 左 | * / % | 乘法 除法 取模(取余) | expr * expr expr / expr expr % expr |
| 左 左 | + - | 加法 減法 | expr + expr expr - expr |
| 左 左 | << >> | 向左移位 向右移位 | expr << expr expr >> expr |
| 左 左 左 左 | < <= > >= | 小于 小于等于 大于 大于等于 | expr < expr expr <= expr expr > expr expr >= expr |
| 左 左 | == != | 相等 不想等 | expr == expr expr != expr |
| 左 | & | 位與 | expr & expr |
| 左 | ^ | 位異或 | expr ^ expr |
| 左 | | | 位或 | expr | expr |
| 左 | && | 邏輯與 | expr && expr |
| 左 | || | 邏輯或 | expr || expr |
| 右 | ? : | 條件 | expr ? expr : expr |
| 右 | = | 賦值 | lvalue = expr |
| 右 右 右 右 | *=,/=,%= +=,-= <<=,>>= &=,|=,^= | 復合賦值 | lvalue += expr等 |
| 右 | throw | 拋出異常 | throw expr |
| 左 | , | 逗號 | expr, expr |
說明:
總結
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(4 / 19):表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《C++ Primer 5th》笔记(3
- 下一篇: 矩阵论-集合与映射,线性空间及其性质