第4章 表達式
基礎(Fundamentals)
表達式(expression)由一個或多個運算對象(operand)組成,對表達式求值將得到一個結果(result)。字面值和變量是最簡單的表達式,其結果就是字面值和變量的值。
基礎概念(Basic Concepts)
C++定義了一元運算符(unary operator)和二元運算符(binary operator)。除此之外,還有一個作用於三個運算對象的三元運算符。函數調用也是一種特殊的運算符,它對運算對象的數量沒有限制。
表達式求值過程中,小整數類型(如bool
、char
、short
等)通常會被提升(promoted)爲較大的整數類型,主要是int
。
C++定義了運算符作用於內置類型和複合類型的運算對象時所執行的操作。當運算符作用於類類型的運算對象時,用戶可以自定義其含義,這被稱作運算符重載(overloaded operator)。
C++的表達式分爲右值(rvalue)和左值(lvalue)。當一個對象被用作右值的時候,用的是對象的值(內容);當對象被用作左值時,用的是對象的地址。需要右值的地方可以用左值代替,反之則不行。
- 賦值運算符需要一個非常量左值作爲其左側運算對象,返回結果也是一個左值。
- 取地址符作用於左值運算對象,返回指向該運算對象的指針,該指針是一個右值。
- 內置解引用運算符、下標運算符、迭代器解引用運算符、
string
和vector
的下標運算符都返回左值。 - 內置類型和迭代器的遞增遞減運算符作用於左值運算對象。前置版本返回左值,後置版本返回右值。
如果decltype
作用於一個求值結果是左值的表達式,會得到引用類型。
優先級與結合律(Precedence and Associativity)
複合表達式(compound expression)指含有兩個或多個運算符的表達式。優先級與結合律決定了運算對象的組合方式。
括號無視優先級與結合律,表達式中括號括起來的部分被當成一個單元來求值,然後再與其他部分一起按照優先級組合。
求值順序(Order of Evaluation)
對於那些沒有指定執行順序的運算符來說,如果表達式指向並修改了同一個對象,將會引發錯誤併產生未定義的行爲。
int i = 0; cout << i << " " << ++i << endl; // undefined
處理複合表達式時建議遵循以下兩點:
- 不確定求值順序時,使用括號來強制讓表達式的組合關係符合程序邏輯的要求。
- 如果表達式改變了某個運算對象的值,則在表達式的其他位置不要再使用這個運算對象。
當改變運算對象的子表達式本身就是另一個子表達式的運算對象時,第二條規則無效。如*++iter
,遞增運算符改變了iter的值,而改變後的iter又是解引用運算符的運算對象。類似情況下,求值的順序不會成爲問題。
算術運算符(Arithmetic Operators)
算術運算符(左結合律):
在除法運算中,C++語言的早期版本允許結果爲負數的商向上或向下取整,C++11新標準則規定商一律向0取整(即直接去除小數部分)。
邏輯和關係運算符(Logical and Relational Operators)
關係運算符作用於算術類型和指針類型,邏輯運算符作用於任意能轉換成布爾值的類型。邏輯運算符和關係運算符的返回值都是布爾類型。
邏輯與(logical AND)運算符&&
和邏輯或(logical OR)運算符||
都是先計算左側運算對象的值再計算右側運算對象的值,當且僅當左側運算對象無法確定表達式的結果時纔會去計算右側運算對象的值。這種策略稱爲短路求值(short-circuit evaluation)。
- 對於邏輯與運算符來說,當且僅當左側運算對象爲真時纔對右側運算對象求值。
- 對於邏輯或運算符來說,當且僅當左側運算對象爲假時纔對右側運算對象求值。
進行比較運算時,除非比較的對象是布爾類型,否則不要使用布爾字面值true
和false
作爲運算對象。
賦值運算符(Assignment Operators)
賦值運算符=
的左側運算對象必須是一個可修改的左值。
C++11新標準允許使用花括號括起來的初始值列表作爲賦值語句的右側運算對象。
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
賦值運算符滿足右結合律。
int ival, jval; ival = jval = 0; // ok: each assigned 0
因爲賦值運算符的優先級低於關係運算符的優先級,所以在條件語句中,賦值部分通常應該加上括號。
不要混淆相等運算符==
和賦值運算符=
。
複合賦值運算符包括+=
、-=
、*=
、/=
、%=
、<<=
、>>=
、&=
、^=
和|=
。任意一種複合運算都完全等價於a = a op b。
遞增和遞減運算符(Increment and Decrement Operators)
遞增和遞減運算符是爲對象加1或減1的簡潔書寫形式。很多不支持算術運算的迭代器可以使用遞增和遞減運算符。
遞增和遞減運算符分爲前置版本和後置版本:
- 前置版本首先將運算對象加1(或減1),然後將改變後的對象作爲求值結果。
- 後置版本也會將運算對象加1(或減1),但求值結果是運算對象改變前的值的副本。
int i = 0, j; j = ++i; // j = 1, i = 1: prefix yields the incremented value j = i++; // j = 1, i = 2: postfix yields the unincremented value
除非必須,否則不應該使用遞增或遞減運算符的後置版本。後置版本需要將原始值存儲下來以便於返回修改前的內容,如果我們不需要這個值,那麼後置版本的操作就是一種浪費。
在某些語句中混用解引用和遞增運算符可以使程序更簡潔。
cout << *iter++ << endl;
成員訪問運算符(The Member Access Operators)
點運算符.
和箭頭運算符->
都可以用來訪問成員,表達式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()
條件運算符(The Conditional Operator)
條件運算符的使用形式如下:
cond ? expr1 : expr2;
其中cond是判斷條件的表達式,如果cond爲真則對expr1求值並返回該值,否則對expr2求值並返回該值。
只有當條件運算符的兩個表達式都是左值或者能轉換成同一種左值類型時,運算的結果纔是左值,否則運算的結果就是右值。
條件運算符可以嵌套,但是考慮到代碼的可讀性,運算的嵌套層數最好不要超過兩到三層。
條件運算符的優先級非常低,因此當一個長表達式中嵌套了條件運算子表達式時,通常需要在它兩端加上括號。
位運算符(The Bitwise Operators)
位運算符(左結合律):
在位運算中符號位如何處理並沒有明確的規定,所以建議僅將位運算符用於無符號類型的處理。
左移運算符<<
在運算對象右側插入值爲0的二進制位。右移運算符>>
的行爲依賴於其左側運算對象的類型:如果該運算對象是無符號類型,在其左側插入值爲0的二進制位;如果是帶符號類型,在其左側插入符號位的副本或者值爲0的二進制位,如何選擇視具體環境而定。
sizeof運算符(The sizeof Operator)
sizeof
運算符返回一個表達式或一個類型名字所佔的字節數,返回值是size_t
類型。
在sizeof
的運算對象中解引用一個無效指針仍然是一種安全的行爲,因爲指針實際上並沒有被真正使用。
sizeof
運算符的結果部分依賴於其作用的類型:
- 對
char
或者類型爲char
的表達式執行sizeof
運算,返回值爲1。 - 對引用類型執行
sizeof
運算得到被引用對象所佔空間的大小。 - 對指針執行
sizeof
運算得到指針本身所佔空間的大小。 - 對解引用指針執行
sizeof
運算得到指針指向的對象所佔空間的大小,指針不需要有效。 - 對數組執行
sizeof
運算得到整個數組所佔空間的大小。 - 對
string
或vector
對象執行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;
類型轉換(Type Conversions)
無須程序員介入,會自動執行的類型轉換叫做隱式轉換(implicit conversions)。
算術轉換(Integral Promotions)
把一種算術類型轉換成另一種算術類型叫做算術轉換。
整型提升(integral promotions)負責把小整數類型轉換成較大的整數類型。
其他隱式類型轉換(Other Implicit Conversions)
在大多數表達式中,數組名字自動轉換成指向數組首元素的指針。
常量整數值0或字面值nullptr
能轉換成任意指針類型;指向任意非常量的指針能轉換成void*
;指向任意對象的指針能轉換成const void*
。
任意一種算術類型或指針類型都能轉換成布爾類型。如果指針或算術類型的值爲0,轉換結果是false
,否則是true
。
指向非常量類型的指針能轉換成指向相應的常量類型的指針。
顯式轉換(Explicit Conversions)
顯式類型轉換也叫做強制類型轉換(cast)。雖然有時不得不使用強制類型轉換,但這種方法本質上是非常危險的。建議儘量避免強制類型轉換。
命名的強制類型轉換(named cast)形式如下:
cast-name<type>(expression);
其中type是轉換的目標類型,expression是要轉換的值。如果type是引用類型,則轉換結果是左值。cast-name是static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
中的一種,用來指定轉換的方式。
dynamic_cast
支持運行時類型識別。- 任何具有明確定義的類型轉換,只要不包含底層
const
,都能使用static_cast
。 const_cast
只能改變運算對象的底層const
,不能改變表達式的類型。同時也只有const_cast
能改變表達式的常量屬性。const_cast
常常用於函數重載。reinterpret_cast
通常爲運算對象的位模式提供底層上的重新解釋。
早期版本的C++語言中,顯式類型轉換包含兩種形式:
type (expression); // function-style cast notation (type) expression; // C-language-style cast notation