《C++ Primer》學習筆記(四):表達式
左值和右值
當一個對象被用作右值的時候,用的是對象的值(內容);當對象被用作左值的時候,用的是對象的身份(在內存中的位置)。
使用關鍵字decltype
的時候,如果表達式的求值結果是左值,decltype
作用於該表達式(不是變量)得到一個引用類型。舉個例子,假定p
的類型是int *
,由於解引用運算符生成左值,所以decltype(*p)
的結果是int&
。另一方面,由於取地址運算符生成右值,所以decltype(&p)
的結果是int**
。
求值順序
對於那些沒有指定執行順序的運算符來說,如果表達式指向並修改了同一個對象,將會引發錯誤併產生未定義的行爲。例如<<
運算符沒有明確規定何時以及如何對運算對象求值,因此下面的輸出表達式是未定義的:
int i = 0;
cout << i << " " << ++i << endl; // 未定義的
編譯器可能先求++i
的值再求i
的值,此時輸出1 1
;也可能先求i
的值再求++i
的值,輸出結果0 1
。因爲此表達式的行爲不可預知,因此無論編譯器生成什麼樣的程序代碼都是錯誤的。
有4 種運算符明確規定了運算對象的求值順序:邏輯與(&&
)運算符、邏輯或(||
)運算符、條件 ( ? :
) 運算符、逗號 (,
) 運算符。
算數運算符
21 % 6; /* 結果是3 */ 21 / 6; /* 結果是3 */
21 % 7; /* 結果是0 */ 21 / 7; /* 給果是3 */
-21 % -8; /* 結果是-5 */ -21 / -8; /* 結果是2 */
21 % -5; /* 結果是1 */ 21 / -5; /* 結果是-4 */
在除法運算中,C++語言的早期版本允許結果爲負數的商向上或向下取整,C++11則規定商一律向0取整(即直接去除小數部分)。
邏輯和關係運算符
邏輯與 運算符 &&
和 邏輯或 運算符||
都是先計算左側運算對象的值再計算右側運算對象的值,當且僅當左側運算對象無法確定表達式的結果時纔會去計算右側運算對象的值,這種策略稱爲 短路求值(short-circuit evaluation)。
- 對於邏輯與運算符來說,當且僅當左側運算對象爲真時纔對右側運算對象求值。
- 對於邏輯或運算符來說,當且僅當左側運算對象爲假時纔對右側運算對象求值。
關係運算符比較運算對象的大小關係並返回布爾值,關係運算符都滿足左結合律。
賦值運算符
C++11標準允許使用花括號括起來的初始值列表作爲賦值語句的右側運算對象。
int i = 0, j = 0, k = 0; // 初始化而非賦值
const int ci = i; // 初始化而非賦值
1024 = k ; // 錯誤:字面值是右值
i + j = k ; // 錯誤:算術表達式是右值
ci = k; // 錯誤:ci是常量(不可修改的)左值
k = {3.14}; // 錯誤:窄化轉換
vector<int> vi; // 初始爲空
vi = {0,1,2,3,4,5,6,7,8,9}; // vi現在含有10個元素了,值從0到9
遞增和遞減運算符
遞增( ++
)和遞減(--
)運算符是爲對象加1或減1的簡潔書寫形式。這兩個運算符還可應用於迭代器,因爲很多迭代器本身不支持算術運算。
遞增和遞減運算符有兩種形式,前置版本和後置版本:
- 前置版本:首先將運算對象加1(或減1),然後將改變後的對象作爲求值結果。
- 後置版本:也會將運算對象加1(或減1),但求值結果是運算對象改變前的值的副本。
除非必須,否則不應該使用遞增或遞減運算符的後置版本。因爲後置版本需要將原始值存儲下來以便於返回修改前的內容,如果我們不需要這個值,那麼後置版本的操作就是一種浪費。
int i = 0, j;
j = ++i; // j = 1, i = 1: 前置版本得到遞增之後的值
j = i++; // j = 1, i = 2: 後置版本得到遞增之前的值
auto pbeg = v.begin();
// 輸出元素直至遇到第一個負值爲止
while (pbeg != v.end() && *beg >= 0)
// 輸出當前值並將pbeg向前移動一個元素
cout << *pbeg++ << endl;
後置遞增運算符的優先級高於解引用運算符,因此*pbeg++
等價於*(pbeg++)
。pbeg++
把 pbeg
的值加1, 然後返回 pbeg
的初始值的副本作爲其求值結果,此時解引用運算符的運算對象是 pbeg
未增加之前的值。最終,這條語句輸出pbeg
開始時指向的那個元素,並將指針向前移動一個位置。這種寫法要更加簡潔。
成員訪問運算符
ptr->mem
等價於(*ptr).mem
。
string s1 = "a string", *p = &s1;
auto n = s1.size(); // 運行string對象s1的size成員
n = (*p).size(); // 運行p所指對象的size成員
n = p->size(); // 等價於(*p).size()
條件運算符
cond ? expr1 : expr2;
條件運算符的執行過程是:首先求條件cond
的值,如果條件爲真則對expr1求值並返回該值,否則對expr2
求值並返回該值。
string finalgrade = (grade < 60) ? "fail" : "pass";
條件運算符可以嵌套,但是考慮到代碼的可讀性,運算的嵌套層數最好不要超過兩到三層。
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
條件運算符的優先級非常低,因此當一個長表達式中嵌套了條件運算子表達式時,通常需要在它兩端加上括號。
cout << ((grade < 60) ? "fail" : "pass"); // 正確:輸出pass或者fail
cout << (grade < 60) ? "fail" : "pass"; // 輸出1或者0
// 等價於下面幾個式子,故上式是錯誤的
// cout << (grade < 60); // 輸出1或者0
// cout ? "fail" : "pass"; // 根據cout的值是true還是false產生對應的字面值
cout << grade < 60 ? "fail" : "pass"; // 錯誤:試圖比較cout和60
// 等價於下面幾個式子,故上式是錯誤的
// cout << grade; // 小於運算符的優先級低於移位運算符,所以先輸出grade
// cout < 60 ? "fail" : "pass"; // 然後比較cout和60
位移運算符
位運算符作用於整數類型的運算對象,並把運算對象看成是二進制位的集合。
如果運算對象是帶符號的,那麼位運算符如何處理運算對象的“符號位”依賴於機器。而且,此時的左移操作可能會改變符號位的值,因此是一種未定義的行爲。關於符號位如何處理沒有明確的規定,所以強烈建議僅將位運算符用於處理無符號類型。
移位運算符
左移運算符 <<
在運算對象右側插入值爲0的二進制位,右移運算符 >>
的行爲依賴於其左側運算對象的類型:如果該運算對象是無符號類型,在其左側插入值爲0的二進制位;如果是帶符號類型,在其左側插入符號位的副本或者值爲0的二進制位,如何選擇視具體環境而定。
位求反運算符
位求反運算符( ~
)將運算對象逐位求反而生成一個新值,將1置爲0、將0置爲1。
char 類型的運算對象首先提升成 int 類型,提升時運算對象原來的位保持不變, 往 高位(high order position)
添0即可。
位與、位或、位異或運算符
sizeof運算符
sizeof
運算符返回一個表達式或一個類型名字所佔的字節數,返回值是 size_t
類型。
Sales_data data, *p;
sizeof(Sales_data); // 存儲Sales_data類型的對象所佔的空間大小
sizeof data; // data的類型的大小,即
sizeof p; // 指針所佔的空間大小
sizeof *p; // p所指類型的空間大小,即
sizeof data.revenue; // Sales_data的revenue成員對應類型的大小
sizeof Sales_data::revenue; // 另一種獲取revenue大小的方式
sizeof
運算符的結果部分依賴於其作用的類型:
- 對
char
或者類型爲char
的表達式執行sizeof
運算,返回值爲1。 - 對引用類型執行
sizeof
運算得到被引用對象所佔空間的大小。 - 對指針執行
sizeof
運算得到指針本身所佔空間的大小。 - 對解引用指針執行
sizeof
運算得到指針指向的對象所佔空間的大小,指針不需要有效。 - 對數組執行
sizeof
運算得到整個數組所佔空間的大小。 - 對
string
或vector
對象執行sizeof
運算只返回該類型固定部分的大小,不會計算對象中元素所佔空間的大小。
//可以用數組的大小除以單個元素的大小獲得數組ia中元素的個數
constexpr size_t sz = sizeof(ia)/sizeof(*ia);
int arr2[sz];
逗號運算符
逗號運算符 ,
含有兩個運算對象,按照從左向右的順序依次求值,最後返回右側表達式的值。逗號運算符經常用在 for
循環中。
vector<int>::size_type cnt = ivec.size();
// 將把從size到1的值賦給ivec的元素
for(vector<int>::size_type ix = 0;
ix != ivec.size(); ++ix, --cnt)
ivec[ix] = cnt;
類型轉換
在下面這些情況下, 編譯器會自動地轉換運算對象的類型:
- 在大多數表達式中,比
int
類型小的整型值首先提升爲較大的整數類型。 - 在條件中,非布爾值轉換成布爾類型。
- 在初始化過程中,初始值轉換成變量的類型;在賦值語句中,右側運算對象轉換成左側運算對象的類型。
- 如果算術運算或關係運算的運算對象有多種類型,需要轉換成同一種類型。
- 函數調用時也會發生類型轉換。
算術轉換
把一種算術類型轉換成另一種算術類型叫做 算術轉換(arithmetic conversion),其中運算符的運算對象將被轉換成最寬的類型。例如如果一個運算對象的類型是long double
,那麼另外一個對象不管是什麼類型都將轉換爲long double
。當表達式中既有浮點類型也有整數類型時,整數值將轉換成相應的浮點類型。
整型提升負責把小整數類型轉換爲較大的整數類型。對於bool
、char
、signed char
、unsigned char
、short
和unsigned short
等類型來說,只要它們所有可能的值都能存在int
裏,它們就會提升成int
類型,否則提升爲unsigned int
類型。例如熟知的布爾值false
提升爲0、true提升爲1。
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'; // 'a'提升成int,然後該int值轉換成long double
dval + ival; // ival轉換成double
dval + fval; // fval轉換成double
ival = dval; // dval轉換成(切除小數部分後)int
flag = dval; // 如果dval是0,則flag是false,否則flag是true
cval + fval; // cval提升成int,然後該int值轉換成float
sval + cval; // sval和cval都提升成int
cval + lval; // cval轉換成long
ival + ulval; // ival轉換成unsigned long
usval + ival; // 根據unsigned short和int所佔空間的大小進行提升
uival + lval; // 根據unsigned int和long所佔空間的大小進行轉換
其他隱式類型轉換
- 數組轉換成指針:在大多用到數組的表達式中,數組名字自動轉換成指向數組首元素的指針。但是當數組用作
decltype
關鍵字的參數,或者作爲取地址符(&
)、sizeof
及typeid
等運算符的運算對象時,上述轉換不會發生。 - 指針的轉換:常量整數值
0
或字面值nullptr
能轉換成任意指針類型;指向任意非常量的指針能轉換成void*
;指向任意對象的指針能轉換成const void*
。 - 轉換成布爾類型:任意一種算術類型或指針類型都能轉換成布爾類型。如果指針或算術類型的值爲0,轉換結果是
false
,否則是true
。
char *cp = get_string();
if (cp) /* ... */ // 如果指針cp不是0,條件爲真
while (*cp) /* ... */ // 如果*cp不是空字符,條件爲真
- 轉換成常量:允許將指向非常量類型的指針轉換成指向相應的常量類型的指針,對於引用也是這樣。
int i;
const int &j = i; // 非常量轉換成const int的引用
const int *p = &i; // 非常量的地址轉換成const的地址
int &r = j, *q = p; // 錯誤:不允許const轉換成非常量
顯示轉換
命名的強制類型轉換(named cast) 形式如下:
cast-name<type>(expression);
其中 type
是轉換的目標類型,expression
是要轉換的值。如果 type
是引用類型,則轉換結果是左值。cast-name
是 static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
中的一種,用來指定轉換的方式。
dynamic_cast
支持運行時類型識別。- 任何具有明確定義的類型轉換,只要不包含底層
const
,都能使用static_cast
。當需要把一個較大的算術類型賦值給較小的類型時,static cast
非常有用。static cast
對於編譯器無法自動執行的類型轉換也非常有用。 const_cast
只能改變運算對象的底層const
,同時也只有const_cast
能改變表達式的常量屬性。const_cast
常常用於函數重載。reinterpret_cast
通常爲運算對象的位模式提供底層上的重新解釋。reinterpret_cast
本質上依賴於機器。要想安全使用reinterpret_cast
必須對涉及的類型和編譯器實現轉換的過程都非常瞭解。
static_cast、dynamic_cast、const_cast和reinterpret_cast總結
static_cast
主要用於非多態類型之間的轉換,不提供運行時的檢查來確保轉換的安全性。主要在以下幾種場合中使用:
- 用於類層次結構中,基類和子類之間指針和引用的轉換;
當進行上行轉換,也就是把子類的指針或引用轉換成父類表示,這種轉換是安全的;
當進行下行轉換,也就是把父類的指針或引用轉換成子類表示,這種轉換是不安全的,也需要程序員來保證; - 用於基本數據類型之間的轉換,如把
int
轉換成char
,把int
轉換成enum
等等,這種轉換的安全性需要程序員來保證; - 把
void
指針轉換成目標類型的指針,是及其不安全的;
注:static_cast
不能轉換掉expression
的const
、volatile
和__unaligned
屬性。
dynamic_cast
dynamic_cast<type>(expression);
使用dynamic_cast
時,type
必須是類的指針、類的引用或者是void *
;如果type
是指針類型,那麼expression
也必須是一個指針;如果type
是一個引用,那麼expression
也必須是一個引用。
dynamic_cast
主要用於類層次間的上行轉換和下行轉換,還可以用於類之間的交叉轉換。在類層次間進行上行轉換時,dynamic_cast
和static_cast
的效果是一樣的;在進行下行轉換時,dynamic_cast
具有類型檢查的功能,比static_cast
更安全。在多態類型之間的轉換主要使用dynamic_cast
,因爲類型提供了運行時信息。
const_cast
const_cast
用來將類型的const
、volatile
和__unaligned
屬性移除。常量指針被轉換成非常量指針,並且仍然指向原來的對象;常量引用被轉換成非常量引用,並且仍然引用原來的對象。
注:你不能直接對非指針和非引用的變量使用const_cast
操作符去直接移除它的const
、volatile
和__unaligned
屬性。
reinterpret_cast
允許將任何指針類型轉換爲其它的指針類型;聽起來很強大,但是也很不靠譜。它主要用於將一種數據類型從一種類型轉換爲另一種類型。它可以將一個指針轉換成一個整數,也可以將一個整數轉換成一個指針,在實際開發中,先把一個指針轉換成一個整數,在把該整數轉換成原類型的指針,還可以得到原來的指針值;特別是開闢了系統全局的內存空間,需要在多個應用程序之間使用時,需要彼此共享,傳遞這個內存空間的指針時,就可以將指針轉換成整數值,得到以後,再將整數值轉換成指針,進行對應的操作。
運算符優先級表
練習
- 寫出一條表達式用於確定一個整數是奇數還是偶數
if (num % 2 == 0) /* ... */
if (num & 0x1) /* ... */
- 解釋在下面的 if 語句中條件部分的判斷過程。
const char *cp = "Hello World";
if (cp && *cp)
cp
是指向字符串的指針,因此上式的條件部分含義是首先檢查指針 cp 是否有效。
- 如果
cp
爲空指針或無效指針,則條件不滿足。 - 如果
cp
有效,即 cp 指向了內存中的某個有效地址,繼續解引用指針cp
並檢查所指的對象是否爲空字符\0
,如果cp
所指的對象不是空字符則條件滿足,否則不滿足。
在本例中,顯然初始狀態下 cp
指向了字符串的首字符,是有效地;同時當前 cp
所指的對象是字符H
,不是空字符,所以 if
的條件部分爲真。
- 說明前置遞增運算符和後置遞增運算符的區別。
遞增和遞減運算符有兩種形式:前置版本和後置版本。
- 前置版本首先將運算對象加1(或減1),然後把改變後的對象作爲求值結果;
- 後置版本也將運算對象加1(或減1),但是求值結果是運算對象改變之前那個值的副本。
這兩種運算符必須作用於左值運算對象。前置版本將對象本身作爲左值返回;後置版本則將對象原始值的副本作爲右值返回。
我們的建議是,除非必須,否則不用遞增(遞減)運算符的後置版本。
- 前置版本的遞增運算符避免了不必要的工作,它把值加1後直接返回改變了的運算對象。
- 與之相比,後置版本需要將原始值存儲下來以便於這個未修改的內容。如果我們不需要修改之前的值,那麼後置版本的操作就是一種浪費。
對於整數和指針類型來說,編譯器可能對這種額外的工作進行了一定的優化;但是對於相對複雜的迭代器類型來說,這種額外的工作就消耗巨大了。建議養成使用 前置版本 的習慣,這樣不僅不需要擔心性能問題,而且更重要的是寫出的代碼會更符合編程人員的初衷。
- 編寫一段程序,輸出每一種內置類型所佔空間的大小。
#include <iostream>
using namespace std;
int main()
{
cout << "bool:\t\t" << sizeof(bool) << " bytes" << endl << endl;
cout << "char:\t\t" << sizeof(char) << " bytes" << endl;
cout << "wchar_t:\t" << sizeof(wchar_t) << " bytes" << endl;
cout << "char16_t:\t" << sizeof(char16_t) << " bytes" << endl;
cout << "char32_t:\t" << sizeof(char32_t) << " bytes" << endl << endl;
cout << "short:\t\t" << sizeof(short) << " bytes" << endl;
cout << "int:\t\t" << sizeof(int) << " bytes" << endl;
cout << "long:\t\t" << sizeof(long) << " bytes" << endl;
cout << "long long:\t" << sizeof(long long) << " bytes" << endl << endl;
cout << "float:\t\t" << sizeof(float) << " bytes" << endl;
cout << "double:\t\t" << sizeof(double) << " bytes" << endl;
cout << "long double:\t" << sizeof(long double) << " bytes" << endl << endl;
system("pause");
return 0;
}
5. 說明下面這條表達式的含義。
someValue ? ++x, ++y : --x, --y
條件運算符的優先級高於逗號運算符,所以 someValue ? ++x, ++y : --x, --y
實際上等價於 (someValue ? ++x, ++y : --x), --y
。它的求值過程是:
首先判斷 someValue 是否爲真,
- 如果爲真,依次執行 ++x 和 ++y,最後執行 --y;
- 如果爲假,執行 --x 和 --y。
- 假設有如下的定義:
char cval;
int ival;
unsigned int ui;
float fval;
double dval;
請回答在下面的表達式中發生了隱式類型轉換嗎?如果有,指出來。
(a) cval = 'a' + 3;
(b) fval = ui - ival * 1.0;
(c) dval = ui * fval;
(d) cval = ival + fval + dval;
( a )字符 'a'
轉換爲int
,然後與 3 相加的結果再轉換爲 char
並賦給 cval
。
( b )ival
轉換爲 double
,與1.0
相乘的結果也是 double
類型,ui
轉換爲 double
後與乘法得到的結果相減,最終的結果轉換爲 float
並賦給fval
。
( c )ui
轉換爲 float
,與 fval
相乘的結果轉換爲 double
類型並賦給 dval
。
( d )ival
轉換爲float
,與 fval
相加後的結果轉換爲double
類型,再與 dval
相加後結果轉換爲 char
類型。