- 表達式由一個或多個運算對象(operand)組成,對表達式求值將得到一個結果(result)
- 字面值和變量是最簡單的表達式(expression),其結果就是字面值和變量的值。把一個運算符(operator)和一個或多個運算對象組合起來可以生成較復雜的表達式
4.1基礎
- 有幾個基礎概念對表達式的求值過程有影響,它們涉及大多數(甚至全部)表達式。本節先簡要介紹這幾個概念,后面的小節將做更詳細的討論。
4.1.1基本概念
- C++定義了一元運算符和二元運算符。作用于一個運算對象的運算符是一元運算符,如取地址符(&)和解引用符(*);作用于兩個運算對象的運算符是二元運算符,如相等運算符(=)和乘法運算符(*)。除此之外,還有一個作用于三個運算對象的三元運算符。
- 函數調用也是一種特殊的運算符,它對運算對象的數量沒有限制。
- 一些符號既能作為一元運算符也能作為二元運算符。以符號*為例,作為一元運算符時執行解引用操作,作為二元運算符時執行乘法操作。一個符號到底是一元運算符還是二元運算符由它的上下文決定。對于這類符號來說,它的兩種用法互不相干,完全可以當成兩個不同的符號。
組合運算符和運算對象
- 對于含有多個運算符的復雜表達式來說,要想理解它的含義首先要理解運算符的優先級(precedence),結合律(associativity)以及運算對象的求值順序
- 例如,下面這條表達式的求值結果依賴于表達式中運算符和運算對象的組合方式:
- 5+10*20/2;
- 乘法運算符(*)是一個二元運算符,它的運算對象有4種可能:10和20、10和20/2、15和20、15和20/2。下一節將介紹如何理解這樣一條表達式。
運算對象轉換
- 在表達式求值的過程中,運算對象常常由一種類型轉換成另外一種類型。例如,盡管一般的二元運算符都要求兩個運算對象的類型相同,但是很多時候即使運算對象的類型不相同也沒有關系,只要它們能被轉換(參見2.1.2節,第32頁)成同一種類型即可。類型轉換的規則雖然有點復雜,但大多數都合乎情理、容易理解。例如,整數能轉換成浮點數,浮點數也能轉換成整數,但是指針不能轉換成浮點數。讓人稍微有點意外的是,小整數類型(如bool、char,short等)通常會被提升(promoted)成較大的整數類型,主要是int。4.11節(第141頁)將詳細介紹類型轉換的細節。
重載運算符
- C++語言定義了運算符作用于內置類型和復合類型的運算對象時所執行的操作。當運算符作用于類類型的運算對象時,用戶可以自行定義其含義。因為這種自定義的過程事實上是為已存在的運算符賦予了另外一層含義,所以稱之為重載運算符。IO庫的>和<運算符以及string對象、vector對象和迭代器使用的運算符都是重載的運算符。
- 我們使用重載運算符時,其包括運算對象的類型和返回值的類型,都是由該運算符定義的;但是運算對象的個數、運算符的優先級和結合律都是無法改變的。
左值和右值
- C++的表達式要不然是右值,要不然就是左值。這兩個名詞是從C語言繼承過來的,原本是為了幫助記憶:左值可以位于賦值語句的左側,右值則不能。
- std::move 將右值轉化為左值進行計算
- 在C++語言中,二者的區別就沒那么簡單了。一個左值表達式的求值結果是一個對象或者一個函數,然而以常量對象為代表的某些左值實際上不能作為賦值語句的左側運算對象。此外,雖然某些表達式的求值結果是對象,但它們是右值而非左值。可以做一個簡單的歸納:當一個對象被用作右值的時候,用的是對象的值(內容);當對象被用作左值的時候,用的是對象的身份(在內存中的位置)。
- 不同的運算符對運算對象的要求各不相同,有的需要左值運算對象、有的需要右值運算對象;返回值也有差異,有的得到左值結果、有的得到右值結果。一個重要的原則(參見13.6節,第470頁將介紹一種例外的情況)是在需要右值的地方可以用左值來代替,但是不能把右值當成左值(也就是位置)使用。當一個左值被當成右值使用時,實際使用的是它的內容(值)。到目前為止,已經有幾種我們熟悉的運算符是要用到左值的。
- 賦值運算符需要一個(非常量)左值作為其左側運算對象,得到的結果也仍然是一個左值。
- 取地址符(參見2.3.2節,第47頁)作用于一個左值運算對象,返回一個指向該運算對象的指針,這個指針是一個右值。
- 內置解引用運算符、下標運算符(參見2.3.2節,第48頁;參見3.5.2節,第104頁)、迭代器解引用運算符、string和vector的下標運算符(參見341節,第95頁;參見3.2.3節,第83頁;參見3.3.3節,第91頁)的求值結果都是左值。
- 內置類型和迭代器的遞增遞減運算符(參見1.4.1節,第11頁:參見3.4.1節,第96頁)作用于左值運算對象,其前置版本(本書之前章節所用的形式)所得的結果也是左值。
- 接下來在介紹運算符的時候,我們將會注明該運算符的運算對象是否必須是左值以及其求值結果是否是左值。
- 使用關鍵字decltype(參見2.5.3節,第62頁)的時候,左值和右值也有所不同。如果表達式的求值結果是左值,decltype作用于該表達式(不是變量)得到一個引用類。舉個例子,假定p的類型是int*,因為解引用運算符生成左值,所以decltype(*p)的結果是int&。另一方面,因為取地址運算符生成右值,所以decltype(&p)的結果是int**,也就是說,結果是一個指向整型指針的指針。
4.1.2優先級與結合律
- 復合表達式(compoundexpression)是指含有兩個或多個運算符的表達式。求復合表達式的值需要首先將運算符和運算對象合理地組合在一起,優先級與結合律決定了運算對象組合的方式。也就是說,它們決定了表達式中每個運算符對應的運算對象來自表達式的哪一部分。表達式中的括號無視上述規則,程序員可以使用括號將表達式的某個局部括起來使其得到優先運算。
- 一般來說,表達式最終的值依賴于其子表達式的組合方式。高優先級運算符的運算對象要比低優先級運算符的運算對象更為緊密地組合在一起。如果優先級相同,則其組合規則由結合律確定。例如,乘法和除法的優先級相同且都高于加法的優先級。因此,乘法和除法的運算對象會首先組合在一起,然后才能輪到加法和減法的運算對象。算術運算符滿足左結合律,意味著如果運算符的優先級相同,將按照從左向右的順序組合運算對象:
- 根據運算符的優先級,表達式3+4*5的值是2 3 ,不是35。
- 根據運算符的結合律,表達式20 -1 5 -3 的值是2 , 不是8。
括號無視優先級與結合律
- 括號無視普通的組合規則,表達式中括號括起來的部分被當成一個單元來求值,然后再與其他部分一起按照優先級組合。例如,對上面這條表達式按照不同方式加上括號就能得到4種不同的結果:
先級與結合律有何影響
- 由前面的例子可以看出,優先級會影響程序的正確性,這一點在3.5.3節(第107頁)介紹的解引用和指針運算中也有所體現:
- 如果想訪問ia+4位置的元素,那么加法運算兩端的括號必不可少。一旦去掉這對括號,*ia就會首先組合在一起,然后4再與*ia的值相加。結合律對表達式產生影響的一個典型示例是輸入輸出運算,4.8節(第138頁)將要介紹IO相關的運算符滿足左結合律。這一規則意味著我們可以把幾個IO運算組合在一條表達式當中:
- cin>>vl>>v2;//先讀入vl,再讀入v2
- 4.12節(第147頁)羅列出了全部的運算符,并用雙橫線將它們分割成若干組。同一組內的運算符優先級相同,組的位置越靠前組內的運算符優先級越高。例如,前置遞增運算符和解引用運算符的優先級相同并且都比算術運算符的優先級高。表中同樣列出了每個運算符在哪一頁有詳細的描述,有些運算符之前已經使用過了,大多數運算符的細節將在本章剩余部分逐一介紹,還有幾個運算符將在后面的內容中提及。
4.1.3求值順序
- 優先級規定了運算對象的組合方式,但是沒有說明運算對象按照什么順序求值。在大多數情況下,不會明確指定求值的順序。對于如下的表達式inti=fl()*f2();我們知道fl和f2-定會在執行乘法之前被調用,因為畢竟相乘的是這兩個函數的返回值。但是我們無法知道到底fl在f2之前調用還是f2在fl之前調用。對于那些沒有指定執行順序的運算符來說,如果表達式指向并修改了同一個對象,將會引發錯誤并產生未定義的行為(參見2.1.2節,第33頁)。舉個簡單的例子,<<運算符沒有明確規定何時以及如何對運算對象求值,因此下面的輸出表達式是未定義的:
- inti=0;cout ? i << " ” << ++i << endl; // 未定義的
- 因為程序是未定義的,所以我們無法推斷它的行為。編譯器可能先求++i的值再求i的值,此時輸出結果是11;也可能先求i的值再求++i的值,輸出結果是01;甚至編譯器還可能做完全不同的操作。因為此表達式的行為不可預知,因此不論編譯器生成什么樣的代碼程序都是錯誤的。
- 有4種運算符明確規定了運算對象的求值順序。第一種是323節(第85頁)提到的邏輯與(&&)運算符,它規定先求左側運算對象的值,只有當左側運算對象的值為真時才繼續求右側運算對象的值。另外三種分別是邏輯或(||)運算符(參見4.3節,第126頁)、條件(?:)運算符(參見4.7節,第134頁)和逗號(,)運算符(參見4.10節,第140頁)。
求值順序、優先級、結合律
- 運算對象的求值順序與優先級和結合律無關,在一條形如f()+g()*h()+j()的表達式中:
- 優先級規定,g()的返回值和h()的返回值相乘。
- 結合律規定,f()的返回值先與g()和h()的乘積相加,所得結果再與j()的返回值相加。
- 對于這些函數的調用順序沒有明確規定。如果f、g、h和j是無關函數,它們既不會改變同一對象的狀態也不執行IO任務,那么函數的調用順序不受限制。反之,如果其中某幾個函數影響同一對象,則它是一條錯誤的表達式,將產生未定義的行為。
注意事項
- 1,拿不準的時候最好用括號來強制讓表達式的組合關系符合程序邏輯的要求。
- 2,如果改變了某個運算對象的值,在表達式的其他地方不要再使用這個運算對象。
- 第2條規則有一個重要例外,當改變運算對象的子表達式本身就是另外一個子表達式的運算對象時該規則無效。例如,在表達式*++iter中,遞增運算符改變iter的值,iter(已經改變)的值又是解引用運算符的運算對象。此時(或類似的情況下),求值的順序不會成為問題,因為遞增運算(即改變運算對象的子表達式)必須先求值,然后才輪到解引用運算。顯然,這是一種很常見的用法,不會造成什么問題。先求值再解引用
4 . 2 算術運算符
- 表4.1(以及后面章節的運算符表)按照運算符的優先級將其分組。一元運算符的優先級最高,接下來是乘法和除法,優先級最低的是加法和減法。優先級高的運算符比優先級低的運算符組合得更緊密。上面的所有運算符都滿足左結合律,意味著當優先級相同時按照從左向右的順序進行組合。除非另做特殊說明,算術運算符都能作用于任意算術類型(參見2.1.1節,第30頁)以及任意能轉換為算術類型的類型。算術運算符的運算對象和求值結果都是右值。如4.11節(第141頁)描述的那樣,在表達式求值之前,小整數類型的運算對象被提升成較大的整數類型,所有運算對象最終會轉換成同一類型。
- 一元正號運算符、加法運算符和減法運算符都能作用于指針。3.5.3節(第106頁)已經介紹過二元加法和減法運算符作用于指針的情況。當一元正號運算符作用于一個指針或者算術值時,返回運算對象值的一個(提升后的)副本。一元負號運算符對運算對象值取負后,返回其(提升后的)副本:
- 在2.1.1節(第31頁),我們指出布爾值不應該參與運算,-b就是一個很好的例子。對大多數運算符來說,布爾類型的運算對象將被提升為int類型。如上所示,布爾變量b的值為真,參與運算時將被提升成整數值1(參見2.1.2節,第32頁),對它求負后的結果是-1。將-1再轉換回布爾值并將其作為b2的初始值,顯然這個初始值不等于0,轉換成布爾值后應該為1。所以,b2的值是真!
- 整數相除結果還是整數,也就是說,如果商含有小數部分,直接棄除
- 運算符%俗稱“取余”或 “取模”運算符,負責計算兩個整數相除所得的余數,參與取余運算的運算對象必須是整數類型
- C++11新標準則規定商一律向0取整(即直接切除小數部分)
4 .3 邏輯和關系運算符
- 關系運算符作用于算術類型或指針類型,邏輯運算符作用于任意能轉換成布爾值的類型。邏輯運算符和關系運算符的返回值都是布爾類型。值為0的運算對象(算術類型或指針類型)表示假,否則表示真。對于這兩類運算符來說,運算對象和求值結果都是右值
邏輯與和邏輯或運算符
- 對于邏輯與運算符(&&)來說,當且僅當兩個運算對象都為真時結果為真;對于邏輯或運算符(||)來說,只要兩個運算對象中的一個為真結果就為真。邏輯與運算符和邏輯或運算符都是先求左側運算對象的值再求右側運算對象的值,當且僅當左側運算對象無法確定表達式的結果時才會計算右側運算對象的值。這種策略稱為短路求值(short-circuitevaluation)
- 對于邏輯與運算符來說,當且僅當左側運算對象為真時才對右側運算對象求值。
- 對于邏輯或運算符來說,當且僅當左側運算對象為假時才對右側運算對象求值
- 第3章中的幾個程序用到了邏輯與運算符,它們的左側運算對象是為了確保右側運算對象求值過程的正確性和安全性。例如85頁的循環條件:
- index!=s.size()&&!isspace(s[index])
- 首先檢查index是否到達string對象的末尾,以此確保只有當index在合理范圍之內時才會計算右側運算對象的值。
- 舉一個使用邏輯或運算符的例子,假定有一個存儲著若干string對象的vector對象,要求輸出string對象的內容并且在遇到空字符串或者以句號結束的字符串時進行換行。使用基于范圍的for循環(參見3.2.3節,第81頁)處理string對象中的每個元素:
- 輸出當前元素后檢查是否需要換行。if語句的條件部分首先檢查S是否是一個空string,如果是,則不論右側運算對象的值如何都應該換行。只有當string對象非空時才需要求第二個運算對象的值,也就是檢查string對象是否是以句號結束的。在這條表達式中,利用邏輯或運算符的短路求值策略確保只有當s非空時才會用下標運算符去訪問它。
- 值得注意的是,s被聲明成了對常量的引用(參見2.5.2節,第61頁)。因為text的元素是string對象,可能非常大,所以將s聲明成引用類型可以避免對元素的拷貝;又因為不需要對string對象做寫操作,所以s被聲明成對常量的引用。
邏輯非運算符
- 邏輯非運算符(!)將運算對象的值取反后返回,之前我們曾經在3.2.2節(第79頁)使用過這個運算符。下面再舉一個例子,假設vec是一個整數類型的vector對象,可以使用邏輯非運算符將empty函數的返回值取反從而檢查vec是否含有元素:
關系運算符
- 顧名思義,關系運算符比較運算對象的大小關系并返回布爾值。關系運算符都滿足左結合律。
- 因為關系運算符的求值結果是布爾值,所以將幾個關系運算符連寫在一起會產生意想不到的結果:
- 但是這種寫法存在兩個問題:首先,與之前的代碼相比,上面這種寫法較長而且不太直接(盡管大家都認為縮寫的形式對初學者來說有點難理解);更重要的一點是,如果val不是布爾值,這樣的比較就失去了原來的意義。
- 如果val不是布爾值,那么進行比較之前會首先把true轉換成val的類型。也就是說,如果val不是布爾值,則代碼可以改寫成如下形式:if(val==1){/*...*/}
- 正如我們已經非常熟悉的那樣,當布爾值轉換成其他算術類型時,false轉換成0而true轉換成1(參見2.1.2節,第32頁)。如果真想知道val的值是否是1,應該直接寫出1這個數值來,而不要與true比較。
- 進行比較運算時除非比較的對象是布爾類型,否則不要使用布爾字面值true 和 false作為運算對象。
4 . 4 賦值運算符
- 賦值運算的結果是它的左側運算對象,并且是一個左值。相應的,結果的類型就是左側運算對象的類型。如果賦值運算符的左右兩個運算對象類型不同,則右側運算對象將轉換成左側運算對象的類型:
- 如果左側運算對象是內置類型,那么初始值列表最多只能包含一個值,而且該值即使轉換的話其所占空間也不應該大于目標類型的空間(參見2.2.1節,第39頁)。
- 對于類類型來說,賦值運算的細節由類本身決定。對于vector來說,vector模板重載了賦值運算符并且可以接收初始值列表,當賦值發生時用右側運算對象的元素替換左側運算對象的元素。無論左側運算對象的類型是什么,初始值列表都可以為空。此時,編譯器創建一個值初始化(參見3.3.1節,第88頁)的臨時量并將其賦給左側運算對象。
賦值運算滿足右結合律
- 賦值運算符滿足右結合律,這一點與其他二元運算符不太一樣:
- int ival, jval; ival = jval = 0; / / 正確:都被賦值為0
- 因為賦值運算符滿足右結合律,所以靠右的賦值運算jval=0作為靠左的賦值運算符的右側運算對象。又因為賦值運算返回的是其左側運算對象,所以靠右的賦值運算的結果(即jval)被賦給了ival。對于多重賦值語句中的每一個對象,它的類型或者與右邊對象的類型相同、或者可由右邊對象的類型轉換得到(參見4.11節,第141頁):
- 因為ival和pval的類型不同,而且pval的類型(int*)無法轉換成ival的類型(int),所以盡管這個值能賦給任何對象,但是第一條賦值語句仍然是非法的。與之相反,第二條賦值語句是合法的。這是因為字符串字面值可以轉換成string對象并賦給s2,而s2和si的類型相同,所以s2的值可以繼續賦給si。
賦值運算優先級較低
- 賦值語句經常會出現在條件當中。因為賦值運算的優先級相對較低,所以通常需要給賦值部分加上括號使其符合我們的原意。下面這個循環說明了把賦值語句放在條件當中有什么用處,它的目的是反復調用一個函數直到返回期望的值(比如42)為止:
- 這個版本的while條件更容易表達我們的真實意圖:不斷循環讀取數據直至遇到42為止。其處理過程是首先將get_value函數的返回值賦給i,然后比較i和42是否相等。如果不加括號的話含義會有很大變化,比較運算符!=的運算對象將是get_value函數的返回值及42,比較的結果不論真假將以布爾值的形式賦值給i,這顯然不是我們期望的結果。
- 因為賦值運算符的優先級低于關系運算符的優先級,所以在條件語句中,賦值部分通常應該加上括號。
復合賦值運算符
- 唯一的區別是左側運算對象的求值次數:使用復合運算符只求值一次,使用普通的運算符則求值兩次。這兩次包括:一次是作為右邊子表達式的一部分求值,另一次是作為賦值運算的左側運算對象求值。其實在很多地方,這種區別除了對程序性能有些許影響外幾乎可以忽略不計。
4 .5 遞增和遞減運算符
- 遞增運算符(++)和遞減運算符(一)為對象的加1和減1操作提供了一種簡潔的書寫形式。這兩個運算符還可應用于迭代器,因為很多迭代器本身不支持算術運算,所以此時遞增和遞減運算符除了書寫簡潔外還是必須的。遞增和遞減運算符有兩種形式:前置版本和后置版本。到目前為止,本書使用的都是前置版本,這種形式的運算符首先將運算對象加1(或減1),然后將改變后的對象作為求值結果。后置版本也會將運算對象加1(或減1),但是求值結果是運算對象改變之前那個值的副本:
- 這兩種運算符必須作用于左值運算對象。前置版本將對象本身作為左值返回,后置版本則將對象原始值的副本作為右值返回。
在一條語句中混用解引用和遞增運算符
- 如果我們想在一條復合表達式中既將變量加1或減1又能使用它原來的值,這時就可以使用遞增和遞減運算符的后置版本。
- 舉個例子,可以使用后置的遞增運算符來控制循環輸出一個vector對象內容直至遇到(但不包括)第一個負值為止:
- 對于剛接觸C++和C的程序員來說,*pbeg++不太容易理解。其實這種寫法非常普遍,所以程序員一定要理解其含義。
- 后置遞增運算符的優先級高于解引用運算符,因此*pbeg++等價于*(pbeg++)。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作為其求值結果,此時解引用運算符的運算對象是pbeg未增加之前的值。最終,這條語句輸出pbeg開始時指向的那個元素,并將指針向前移動一個位置。
- 這種用法完全是基于一個事實,即后置遞增運算符返回初始的未加1的值。如果返回的是加1之后的值,解引用該值將產生錯誤的結果。不但無法輸出第一個元素,而且更糟糕的是如果序列中沒有負值,程序將可能試圖解引用一個根本不存在的元素。
運算對象可按任意順序求值
- 大多數運算符都沒有規定運算對象的求值順序(參見4.1.3節,第123頁),這在一般情況下不會有什么影響。然而,如果一條子表達式改變了某個運算對象的值,另一條子表達式又要使用該值的話,運算對象的求值順序就很關鍵了。因為遞增運算符和遞減運算符會改變運算對象的值,所以要提防在復合表達式中錯用這兩個運算符。
- 為了說明這一問題,我們將重寫3.4.1節(第97頁)的程序,該程序使用for循環將輸入的第一個單詞改成大寫形式:
4.6成員訪問運算符
- 點運算符(參見1.5.2節,第21頁)和箭頭運算符(參見3.4.1節,第98頁)都可用于訪問成員,其中,點運算符獲取類對象的一個成員;箭頭運算符與點運算符有關,表達
- 因為解引用運算符的優先級低于點運算符,所以執行解引用運算的子表達式兩端必須加上括號。如果沒加括號,代碼的含義就大不相同了:
- / / 運 行 p 的 size成員,然后解引用size的結果
- *p.size() ; / / 錯誤:p 是一個指針,它沒有名為size的成員
- 這條表達式試圖訪問對象P的size成員,但是p本身是一個指針且不包含任何成員,所以上述語句無法通過編譯。箭頭運算符作用于一個指針類型的運算對象,結果是一個左值。點運算符分成兩種情況:如果成員所屬的對象是左值,那么結果是左值;反之,如果成員所屬的對象是右值,那么結果是右值。