C++ 提供算術相關的加(+
)、減(-
)、乘(*
)、除(/
) 以及餘除運算子(%)或稱模數(Modulus)運算子,這類以數學運算為主的運算子,稱為「算術運算子」(Arithmetic operator)。
這類運算子的使用基本上由左而右進行運算,遇到加減乘除的順序問題時,也是先乘除後加減,必要時加上括號表示運算的先後順序,例如這個程式碼會在主控台顯示 7:
cout << (1 + 2 * 3);
編譯器在讀取程式碼時,是由左往右讀取的,由於習慣將分子寫在上面,而分母寫在下面,使得初學者往往將之寫成了:
cout << 1 + 2 + 3 / 4;
這個程式事實上會是這樣運算的:1 + 2 + (3 / 4);為了避免錯誤,加上括號才是最保險的,例如:
cout << (1 + 2 + 3) / 4;
%
運算子是餘除運算子,它計算除法後的餘數,一個例子是要產生指定位數的亂數,可以使用 %
運算子,假設亂數產生函式為 rand
,可以產生正整數亂數,可以如下產生 0 到 99 的亂數:
cout << rand() % 100;
也可以利用 %
來作循環計數,例如由 0 計數至 9 不斷循環:
counter = (counter + 1) % 10;
算術運算子使用不難,但要注意型態轉換的問題,先看看這段程式會印出什麼結果?
int number = 10;
cout << number / 3;
答案不是 3.3333,而是 3,小數點之後的部份被自動消去了,這是因為 number
是整數,而除數 3 也是整數,運算出來的程式被自動轉換為整數了, 那下面這個程式呢?
double number = 10.0;
cout << number / 3;
這個程式的結果會顯示 3.3333,這是 C++ 做了型態的隱式轉換(Implicit conversion),在一個型態混雜的算式中,長度較長的資料型態會成為目標型態,較小的型態會自動提升,因而在上例中 3 會被提升為 3.0 再進行運算,結果就可以顯示無誤,這樣的轉換又稱算術轉換(Arithmetic conversion)。
在一個指定的動作中,左值會成為目標型態,當右值型態比左值型態長度小時,右值會自動提升為目標型態,例如:
int num = 10;
double number = num;
在上例中,number
的值最後會是 10.0,在指定的動作時,如果右值型態比左值型態型態長度大時,超出可儲存範圍的部份會被自動消去,例如將浮點數指定給整數變數,小數的部份會被自動消去,例子如下,程式會顯示 3 而不是 3.14:
int num = 0;
double number = 3.14;
num = number;
cout << num;
由於會失去精度,若想要編譯器在這類情況提出警訊,可以在編譯時加上 -Wconversion
引數。
算術運算必要時,得進行型態的顯式轉換(Explicitly conversion),例如底下會顯示 3:
int a = 10;
int b = 3;
cout << a / b; // 顯示 3
這是因為 a
與 b
都是 int
,計算結果也就是 int
,想得到小數的結果,必須顯式地轉換型態,方式之一是使用舊式的 C 轉型(cast)語法:
cout << (double) a / b; // 顯示 3.33333
或者是使用函式標示方式:
cout << double(a) / b; // 顯示 3.33333
顯式轉型的目的是提供編譯器資訊,就以上而言,就是告訴編譯器,將 a
的值提昇為 double
;類似地,如果編譯時加上了 -Wconversion
引數,若指定會失去精度就會發出警訊,若某些場合中,這確實就是你想要的,也可以顯式轉型,這樣編譯器就會住嘴了。例如:
int num = 0;
double number = 3.14;
num = int(number); // 編譯時加上 `-Wconversion` 引數也不會有警訊
cout << num;
對於基本型態來說,這樣就足夠了,不過這種轉型是強制性的,也就是加上以上的轉型語法,不管什麼情況,編譯器就都噤聲了,如果因為編譯時加上 -Wconversion
引數,有些開發者只是為了消除這類警訊,不管三七二十一都用這種方式強制轉型,執行時期就可能因為精度遺失而發生問題,而 C++ 中還有指標、類別等型態,無差別地強制轉換(例如將 Dog
類別指標轉為 Cat
類別指標),可能導致執行時期錯誤或不可預期的結果。
在 C++ 中為了避免這類問題,定義了四種用於不同場合的具名轉型(named casting):
static_cast
const_cast
reinterpret_cast
dynamic_cast
其中 static_cast
的一部份應用場合,就是算術運算時的顯式轉換,例如:
int a = 10;
int b = 3;
cout << static_cast<double>(a) / b; // 顯示 3.33333
或者是:
int num = 0;
double number = 3.14;
num = static_cast<int>(number); // 編譯時加上 `-Wconversion` 引數也不會有警訊
cout << num;
表面上看來,static_cast
也是單純叫編譯器住嘴,實際上不然,例如以下在編譯時會發生錯誤:
const double PI = 3.14159;
double *pi = &PI; // error: invalid conversion from 'const double*' to 'double*'
C 風格轉型語法加上後,編譯器會完全閉嘴:
const double PI = 3.14159;
double *pi = (double*) &PI; // 沒有錯誤也沒有警訊
然而,static_cast
會有編譯錯誤:
const double PI = 3.14159;
double *pi = static_cast<double*>(&PI); // error: invalid static_cast from type 'const double*' to type 'double*'
目前還沒談到指標,然而可以先知道的是,PI
是個 const
修飾過的變數,儲存的值是唯讀的,以上程式碼試圖將唯讀的記憶體空間位址指定給 pi
,如果之後試圖對 pi
位址處的資料做變動,執行時期會有不可預期的結果,為此編譯器不能通過編譯,若真要通過編譯,得使用 const_cast
:
const double PI = 3.14159;
double *pi = const_cast<double*>(&PI);
當然,這只是叫編譯器住嘴罷了,後續程式碼也是別對 pi
位址處的資料做變動,以避免執行時期不可預期的結果。
其他有關 C++ 具名轉型,後續在適當的地方還會談到,簡單來說,C++ 希望開發者可以依不同的場合選擇的具名轉型,以便在編譯時期提供不同粒度的檢查,而不是像 C 風格的方式一律住嘴。