-
表達式由一個或多個運算對象組成,對表達式求值將得到一個結果。字面值和變量是最簡單的表達式,其結果就是字面值和變量的值。把一個運算符和一個或多個運算對象組合起來可以生成較復雜的表達式。
-
重載運算符包括運算對象的類型和返回值的類型,都是由該運算符定義的;但是運算對象的個數、運算符的優先級和結合律都是無法改變的
-
當一個對象被用作右值的時候,用的是對象的值(內容);當對象被用作左值的時候,用的是對象的身份(在內存中的位置)。需要右值的地方可以用左值來代替,但是不能把右值當成左值使用。
- 賦值運算符需要一個(非常量)左值作為其左側運算對象,得到的結果也仍然是一個左值
- 取地址符作用于一個左值運算對象,返回一個指向該運算對象的指針,這個指針是一個右值
- 內置解引用運算符、下標運算符、迭代器解引用運算符、
string
和vector
的下標運算符的求值結果都是左值 - 內置類型和迭代器的遞增遞減運算符作用于左值運算對象,其前置版本所得到的結果也是左值
-
如果表達式的求值結果是左值,
decltype
作用于該表達式得到一個引用類型int a = 1; int *p = nullptr; decltype(*p) b = a; //b是int &類型 decltype(&p) c = nullptr; //c是int **類型
-
優先級規定了運算對象的組合方式,但是沒有說明運算對象按照什么順序求值,在大多數情況下,不會明確指定求值的順序。對于表達式
int i = f1() * f2()
,我們知道f1()
和f2()
一定在執行乘法之前被調用,但是我們無法直到到底f1
和f2
的執行先后順序。對于那些沒有指定執行順序的運算符來說,如果表達式指向并修改了同一個對象將會引發錯誤并產生未定義行為(UB
)。因為<<
運算符沒有明確規定何時以及如何對運算對象求值,因此cout << i << " " << ++i << endl
是未定義的。 -
有四種運算符明確規定了運算對象的求值順序:邏輯與
&&
、邏輯或||
、條件(三元)運算符?:
、逗號運算符,
-
運算對象的求值順序與優先級和結合律無關,對每個運算對象的運算結果的計算順序是通過優先級和結合律決定,但是對運算對象的運算順序是不確定的。如果在一個表達式中有多個運算對象涉及對同一個對象的運算,那么很容易產生未定義的行為。例如
f() + g() * h() + j()
中,對這些函數的返回值的運算順序是確定的,但是對這些函數的運算順序是不確定的。 -
書寫復合表達式的準則:
- 拿不準的時候最好使用括號來強制讓表達式的組合關系復合程序邏輯的要求
- 如果改變了某個對象的值,在表達式的其他地方不要再使用這個運算對象。這個其他地方是不包括當改變運算對象的子表達式本身就是另外一個子表達式的運算對象。例如:
*++iter
-
算數運算符的運算對象和求值結果都是右值。
-
一元正號運算符、加法運算符和減法運算符都能作用于指針。當一元正號運算符作用于一個指針或者算術值時,返回運算對象的一個(提升后的)副本
bool b = true; bool b2 = -b; //相當于b2 = -1,所以b2為真
-
整數相除結果還是整數,參與取余運算的運算對象必須是整數類型
-
C++11新標準規定商一律向0取整(即直接切除小數部分)
-
根據取余運算的定義,如果
m
和n
是整數且n
非0,則表達式(m/n)*n+m%n
的值和m
相同。在C++11新標準中,除了-m
導致溢出的特殊情況,其他時候(-m)/n
和m/(-n)
都等于-(m/n)
,m%(-n)
等于m%n
,(-m)%n
等于-(m%n)
-
邏輯運算符作用于任何能轉換成布爾值的類型。邏輯運算符和關系運算符的返回值都是布爾類型。運算對象和求值結果都是右值
-
邏輯與與邏輯或都采用短路求值。可以用左側運算對象來保證右側運算對象求值過程的正確性和安全性
-
使用范圍
for
循環時盡可能聲明成引用類型,能夠避免對元素的拷貝 -
進行比較運算時除非比較的對象是布爾類型,否則不要使用布爾字面值
true
和false
作為運算對象 -
char *cp; if (cp && *cp) {}
表示判斷指針cp
所指向的數組是否為空,如果C字符串數組為空,則數組中只有一個空字符\0
-
復制運算的結果是它左側的運算對象,并且是一個左值。如果賦值運算符左右兩個運算對象類型不同,則右側運算對象將轉換成左側運算對象的類型。
-
C++11新標準允許使用花括號括起來的初始值列表作為賦值語句的右側運算對象。如果左側運算對象是內置類型,那么初始值列表最多只能包含一個值,而且該值不能有丟失信息的風險(所占用的空間不應該大于目標類型的空間)。對于類類型來說,賦值運算的細節由類本身決定。
vector
模板重載了賦值運算符而且可以接收初始值列表,當賦值發生時用右側運算對象的元素替換左側運算對象的元素。無論左側運算對象的類型是什么,初始值列表都可以為空,此時編譯器創建一個值初始化的臨時量并將其賦給左側運算對象(我認為沒有默認構造函數的類可能不行)vector<int> a; a = {1, 2, 3, 4}; a = {1}; a = {1, 2, 3, 4, 5, 6, 7};
-
賦值運算滿足右結合律。因為賦值運算符的優先級低于關系運算符的優先級,所以在條件語句中,賦值部分通常應該加上括號
int i; while(1 == (i = getValue())) {// }
-
使用復合運算符只求值一次,使用普通的運算符則求值兩次(一次計算一次賦值)。因此盡量使用賦值運算符。
-
++
和--
可以用于迭代器,很多迭代器本身不支持算術運算。前置版本將對象本身作為左值返回,后置版本則將對象原始值的副本作為右值返回。如果不需要保存未修改版本的值,盡量使用前置版本不使用后置版本。cout << *iter++ << endl;
是一種被廣泛使用的有效的寫法。C++程序追求簡潔、摒棄冗長,因此C++程序員應該習慣于這種寫法。 -
因為大多數的運算符都沒有規定求值順序,因此同一條表達式中最好在一個地方修改對象的值(且在改變這個對象以后就不要再進行使用),否則很容易產生未定義的行為(
&& || , ?:
除外) -
ptr->item
等價于(*ptr).item
,.
運算符的優先級更高,因此括號必不可少。箭頭運算符作用于一個指針, 結果是一個左值。點運算符作用于左值則結果是左值,作用于右值則結果是右值 -
條件運算符
cond ? expr1 : expr2
,其中cond
是判斷條件的表達式,而expr1
和expr2
是兩個類型相同或者可能轉換為某個公共類型的表達式。條件運算符是有求值順序的,而且條件運算符只會對expr1
和expr2
中的一個求值。當條件運算符的兩個表達式都是左值或者能夠轉換成同一種左值類型時,運算的結果是左值,否則運算結果是右值 -
條件運算符滿足右結合律,意味著運算對象一般按照從右往左的順序結合。條件運算的嵌套最好別超過二到三層。
sting final_grade = (grade > 90) ? "high pass": (grade < 60) ? "fail" : "pass";
條件運算符的優先級比較低,因此最好在復合表達式中使用條件運算符的時候加上括號
-
位運算符作用于整數類型的運算對象,并把運算對象看成是二進制位的集合(
bitset
可以表示任意大小的二進制位集合,也可以使用位運算符)~ 位求反 ~expr << 左移 expr1 << epxr2 >> 右移 & 位與 ^ 位異或 | 位或
如果運算對象是“小整型”,則它的值會被自動提升成較大的整數類型,如果運算對象是帶符號的,有可能產生未定義的行為(如果有可能為負數),因此建議將位運算用于處理無符號類型
-
移位運算符將經過移動的(可能進行了提升)左側運算對象的拷貝作為球直接過。其中右側的運算對象一定不能為負,而且值必須嚴格小于結果的位數,否則就會產生未定義的行為。二進制位或者向左移(
<<
)或者向右移(>>
),移出邊界外的位就被舍棄掉了。<<
在右側插入值為0的二進制位,>>
對無符號數來講是 在左側添加0,但是對帶符號類型依賴環境 -
unsigned char
在位運算中會被提升為unsigned int
,unsigned long
在任何機器上都至少擁有32位。1UL << x
制造一個第x
為1,其他位都為0的數字,對這個數字取反可以得到第x
位為0,其他位都為1的數字。然后通過|
或&
進行操作 -
sizeof
運算符返回一條表達式或一個類型名字所占的字節數。sizeof
運算符滿足又結合律,得到的是一個size_t
類型的常量表達式。運算符的運算對象有兩種形式:sizeof (type) sizeof expr
在第二種類型中,
sizeof
返回的是表達式結果類型的大小,與眾不同的一點是sizeof
并不實際計算其運算對象的值。因此解引一個無效指針仍然是一種安全的行為,因為指針實際上沒有被真正使用。sizeof
不需要真的解引用指針也能知道它所指向對象的類型。Sales_data data, *p; sizeof *p; //返回Sales_data的空間大小 sizeof data.revenue sizeof Sales_data::revenue //同上
在C++11新標準勻速我們使用作用域運算符來獲取類成員的大小。通常情況下只有通過類的對象才能訪問到類的成員,但是
sizeof
運算符無需我們提供一個具體的對象 -
對數組執行
sizeof
運算得到整個數組所占空間的大小,等價于對數組中所有元素執行一次sizeof
運算并將所得的結果求和。注意,sizeof
運算符不會把數組轉換成指針來處理。因此可以用數組的大小除以單個元素的大小得到數組中元素的個數constexpr size_t sz = sizeof(ia)/sizeof(*ia); int arr2[sz]; //arr2的大小和ia一樣
-
對
string
對象或vector
對象執行sizeof
運算只返回該類型固定部分的大小,不會計算對象中的元素占用了多少空間。實測了一下發現對vector
(string
應該也一樣)使用sizeof
返回固定的大小,對于vector<int>
返回24 -
逗號運算符有兩個運算對象,按照從左往右的順序依次求值(同
&& || ?:
一樣規定了運算順序)。先對左側的運算對象求值,然后將求值結果拋棄,真正的運算結果是右側表達式的值,如果右側運算對象是左值,則最終的求值結果也是左值。 -
隱式類型轉換:
- 大多數表達式中,比
int
類型小的整型值首先提升為較大的整數類型 - 在條件中,非布爾值轉換為布爾值
- 初始化和賦值語句中,右側運算對象轉換成左側運算對象的類型
- 如果算術運算或關系運算的運算對象有多種類型,需要轉換為同一種類型
- 大多數表達式中,比
-
整型提升負責把小整數類型轉換成較大的整數類型,整數類型中比
int
小的類型如果參與運算,如果可以放到int
里就會提升成int
,否則提升成unsigned int
-
隱式類型轉換:
- 數組轉換成指針:在大多數用到數組的表達式中,數組自動轉換成指向數組首元素的指針。當數組被用作
decltype
關鍵字的參數,或者作為取地址符&
、sizeof
以及typeid
等運算符的運算對象時,以及用一個引用來初始化數組時,上述轉換不會發生 - 常量整數值
0
或者字面值nullptr
能夠轉換為任意指針類型,指向任意非常量的指針能夠轉化成void*
,指向任意對象的指針能夠轉化為const void*
- 數組轉換成指針:在大多數用到數組的表達式中,數組自動轉換成指向數組首元素的指針。當數組被用作
-
如果有的轉換不會隱式自動轉換就需要強制類型轉換,雖然有時候不得不使用強制類型轉換,但是這種方法本質上是非常危險的。
-
一個命名的強制類型轉換具有如下形式:
cast-name<type>(expression);
type
是要轉換的類型,expression
是要轉換的值,如果type
是引用類型,則結果是左值。cast-name
是static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
中的一種-
static_cast
:任何具有明確定義的類型轉換,只要不包含底層const
,就可以使用static_cast
。-
把較大的算數類型賦給較小的類型
double slope = static_cast<double>(j)
-
無法自動執行的類型轉換
void *p = &d; double *dp = static_cast<double*>(p);
-
-
const_cast
:只能改變運算對象的底層const
,用于將常量對象轉換為非常量對象const char *pc; char *p = const_cast<char*>(pc);
只有
const_cast
可以改變表達式的常量屬性const_cast
常用于有函數重載的上下文中 -
reinterpret_cast
:通常為運算對象的位模式提供較低層次上的重新解釋,不太好用,不建議使用
-
-
應該盡量避免使用強制類型轉換,且不推薦使用舊式類型轉換:與命名的強制類型轉換相比,舊式的強制類型轉換從表現形式上來說不那么清晰明了,容易被看漏。
int x; char y = (char)x;