算術運算、型態轉換


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

這是因為 ab 都是 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 風格的方式一律住嘴。