首先,雙目運算符在運算之前,會對操作數進行類型提升,以使兩者的類型相同。同時,該類型也將會是最後結果的類型(5-10)
1、如果任何一個操作數是long double、double或者float,那麼兩者都會提升到該類型
2、否則,會對操作數進行integral promotion,在此之後
3、如果兩者都是有符號或者無符號,以rank大的類型爲準。否則
4、如果無符號類型的rank大於等於有符號類型,則以無符號類型爲準。否則
5、如果有符號類型能表達無符號類型所有的數值,則以有符號類型爲準。否則
6、轉換成該有符號類型對應的無符號類型
例如:
struct Dummy {};
void Fun(const Dummy &);
int main()
{
Fun(10.0l+10.0); //1
Fun(10.0+10.0f); //1
Fun(10.0f+10LL); //1
Fun(10L+10); //3
Fun(10U+10); //4
Fun(10LL+10U); //5
Fun(10L+10U); //6
}
換言之,任何雙目運算的結果不可能是char,short或者相應的無符號類型,因爲integral promotion至少會將整型類型提升到int。
其次,對於無符號整數而言,所有的運算都是在模2n的數域上進行的(n是相應類型的位數)(3.9.1-4)
另外標準允許,對涉及浮點的運算,在內部使用更高的精度(譬如,Intel CPU中用於浮點計算的寄存器都是80位的)
5.6 * / %
三者分別表示乘法,除法和取餘。前兩者允許浮點、整型和枚舉操作數。後者只允許整型和枚舉操作數
/和%要求除數不能爲0,否則會導致未定義的行爲。
下面的等式在b不爲0的時候恆成立
(a/b)*b+a%b=a
在Intel架構的CPU上,鑑於除法運算比較慢,當除數是常數時,編譯器可以將除法優化成乘法,從而大大提高效率。
而取餘操作無法直接優化成乘法,所以編譯器有時會利用上面的公式先將a%b轉化成a-(a/b)*b,再進行計算。
如果被除數和除數都是非負數,那麼餘數也是非負的;否則,結果是實現相關的。
比如說:
5%3必然等於2,而不會等於-1
(-5)%3和5%(-3)的結果則隨編譯器的不同而不同。不過在VC和GCC中,餘數的符號都是由被除數決定的,所以結果都是-2和2
注:標準傾向於建議使用rounded toward zero的算法來計算商。這和VC&GCC採用的算法是一致的。
5.7 + -
沒什麼好說的 :-)
5.8 << >>
首先,移位的次數不能爲負或者超過被移位操作數的位數,否則行爲是未定義的,比如說1<<32。即使是1>>32,雖然理論上不會溢出,但同樣會導致未定義的行爲
左移E1<<E2,在語義等價於下面的操作:把E1看成無符號整數,假設該整數類型有n位,那麼結果是
(E1*2E2)%2n
右移E1>>E2,當E1是無符號整型或者E1的值非負,等價於E1/2E2。如果E1<0,那麼結果是實現相關的。
因此一般來說,E1>>E2的結果和E1/2E2未必相同。比如在VC和GCC中,(-5)/4=-1,而(-5)>>2=-2。所以當你想優化除法的時候,不能想當然的用移位來代替。大多數現代編譯器都能夠幫你完成這一優化,因此程序員就不必畫蛇添足了。
5.9~5.10 < > == !=
注意,浮點數最好不要直接進行比較,因爲它的表達是不精確的。可以設一個誤差限再比較
5.11~5.13 & ^ |
沒什麼好說的,用於位運算
5.14~5.15 && ||
短路算法,兩邊的表達式會隱式轉換成bool。
需要注意的是,
如果第二個表達式需要被計算,那麼在運算之前,第一個表達式的所有運算和副作用必須首先完成。
所以,i++ || i++的行爲是確定的
但是,表達式中產生的臨時對象要到整個&&或者||表達式都計算完成之後纔會析構。比如說:
#include <iostream>
using namespace std;
struct A1
{
A1() {cout<<"A1 ctor"<<endl;}
~A1() {cout<<"A1 dtor"<<endl;}
operator bool() const {return false;}
};
struct A2
{
A2() {cout<<"A2 ctor"<<endl;}
~A2() {cout<<"A2 dtor"<<endl;}
operator bool() const {return true;}
};
int main()
{
if (A1() || A2()) ;
return 0;
}