C++ 入門指南 4.01
單元 20 - 型態轉換問題
C++ 是強型態 (strong typing) 的程式語言 (programming language) ,每一個變數 (variable) 都必須宣告所屬的型態 (type) ,如果變數屬於基本內建型態 (primitive built-in type) ,而在運算式 (expression) 中遇到型態不相符的情況,基本內建型態的變數會自動進行隱性型態轉換
轉換方向就是儲存範圍小的型態自動轉換成儲存範圍大的型態,字元 (character) char 或布林 (Boolean) bool 依需要會轉換成整數 (integer) ,整數型態則會依序由 short 到 long long ,如果含有浮點數 (floating-point number) 型態的運算元 (operand) ,就會轉換成 float 或 double 。
可是編譯器 (compiler) 默認反過來的情況,這是說像是最後計算結果是 int 的話,同樣可放到 short 裡,例如
#include <iostream> using namespace std; int main() { int a = 65536; cout << a <<endl; short b = a; cout << b <<endl; int c = b; cout << c <<endl; return 0; } /* 《程式語言教學誌》的範例程式 http://kaiching.org/ 檔名:type_demo.cxx 功能:示範型態轉換的問題 作者:張凱慶 */
此例將 int 的變數值指派給 short 的變數
short b = a;
然後再將此 short 變數值重新指派給另一個 int 變數
int c = b;
先來編譯執行看看結果囉
變數 b 是 0 ?這是因為 short 型態可以儲存的最大正整數為 65535 ,而 65536 超過 1 ,編譯器就讓 b 歸 0 了,以二進位來看就像是
因為對 short 型態而言,最左端的 1 並不在儲存範圍,因此編譯器自動略去導致只有儲存右邊 15 個 0
當然這是特意舉的例子,實際上從儲存範圍大的型態轉換到儲存範圍小的型態都容易造成資料遺失,編譯器的處理可能莫衷一是,無法預期。
我們可以在編譯時加上 -Wconversion 的旗標 (flag) ,看看編譯器對於這方面有什麼建議
GCC 有很多 -W 開頭的旗標,都是編譯器對程式檢查後的警告訊息。
簡單講就是會遺失精確度 (precision) ,最好的解決辦法是自己寫程式用心點,遇到型態不相符的情況都自己做好型態轉換,舊式的強制型態轉換就是用小括弧標出運算式最後計算出的型態,如上例的第 9 行應改成
short b = (short) a;
第 12 行應改成
int c = (int) b;
強制轉型並不會保留精確度,也就是 int 的 65536 到 short 變數仍是會變成 0 ,如果不希望編譯器代為處理轉換後的數字,就得自己寫關於轉型的程式設定。
這麼一來編譯重新加上 -Wconversion 旗標,就不會有警告訊息出現了
同樣的型態轉換問題發生在單元 15 裡 set_code_array() 的原始版本上,在 encrypt01.cxx 的第 25 行
s += m + 97;
完整的 encrypt01.cxx 程式碼,請參考單元 15 。
因為 s 的型態為 string ,而 m + 97 的結果卻是 int , string 的 += 對字元型態 char 多載過,可以將字元接在 string 後,也由於 char 與 int 可互通,因此這裡是先把 int 的轉換成 char ,再由 string 與 char 進行連接。
我們的範例並沒有出錯,因為 char 是從 -128 到 127 之間的整數, 'a' 的值等於 97 ,最多加 25 到 122 ,並不會超過 127 的範圍。
雖說是不會出錯,不過為了後續維護上的方便,像是要改寫成使用其他程式庫的字串型態等等,因此我們在 encrypt.cxx 的最終版本一樣加上強制型態轉換
s += (char) m + DIFF;
完整的實作檔案可以參考「範例程式碼」的 encrypt.cxx 。
這是舊式的型態轉換,稱為舊式是因為這是從 C 語言沿襲而來的強制型態轉換, C++ 還有加入用關鍵字 static_cast 等的型態轉換。
對了,最終版本的 encrypt.cxx 也把 97 換成了 DIFF ,這是用前置處理器指令 (preprocessor directive) #define 定義的常數。
前置處理是編譯器在實際編譯 (compile) 前所處理的工作,接下來我們繼續討論前置處理吧!
中英文術語對照 | |
---|---|
布林 | Boolean |
字元 | character |
編譯 | compile |
編譯器 | compiler |
運算式 | expression |
旗標 | flag |
浮點數 | floating-point number |
整數 | integer |
運算元 | operand |
精確度 | precision |
前置處理器指令 | preprocessor directive |
基本內建型態 | primitive built-in type |
程式語言 | programming language |
型態 | type |
變數 | variable |
重點整理 |
---|
1. C++ 屬於強型態的程式語言,每一個變數在使用前都必須宣告所屬的型態。 |
2. 基本內建型態的變數會在運算式中自動進行隱性型態轉換,轉換方向是由儲存範圍小的型態往儲存範圍大的型態進行,反之會損失精確度。 |
3. GCC 編譯時加上 -Wconversion 旗標可顯示轉換警告訊息。 |
4. 舊式強制型態轉換是用小括弧加上型態名稱。 |
問題與討論 |
---|
1. 為什麼變數都要有型態?為什麼使用變數前都要先宣告所屬的型態? |
2. 為什麼基本內建型態會自動進行隱性型態轉換? |
3. 什麼是舊式強制型態轉換,跟新式有什麼不同? |
練習 |
---|
1. 標準程式庫中的 set 是不具有重複元素的集合體型態,字面常數同樣用大括弧,寫一個程式 exercise2001.cxx ,利用字面常數宣告建立正數型態的 set 變數,然後利用迴圈印出 set 變數的內容。 參考程式碼 |
2. 承上題, set 的 empty() 可以判斷 set 是否為空, size() 回傳元素個數,寫一個程式 exercise2002.cxx ,先以字面常數建立 set 變數,然後用 empty() 判斷 set 變數是否為空,若不為空則印出 size() 回傳值。 參考程式碼 |
3. 承上題, set 的 insert() 可以替 set 插入元素,寫一個程式 exercise2003.cxx ,先宣告空的 set 變數,然後用 insert() 增加元素。 參考程式碼 |
4. 承上題, set 的 clear() 可以清空 set 內容,寫一個程式 exercise2004.cxx ,先用字面常數建立 set 變數,然後用 clear() 清空內容,接著判斷 set 如果有內容就印出元素,如果沒有就印出 size() 回傳值。 參考程式碼 |
5. 承上題, set 的 erase() 可以移除迭代器形式的 set 元素,寫一個程式 exercise2005.cxx ,先用字面常數建立 set 變數,然後用迭代器移除其中兩個元素。 參考程式碼 |
6. 標準程式庫中 cmath 的 abs() 回傳參數的絕對值,參數可以是整數或浮點數,寫一個程式 exercise2006.cxx ,以 -200 、 -100.0 、 -0 、 100 、 200 當參數,印出回傳值。 參考程式碼 |
7. 承上題, cmath 的 fmod() 回傳第一個參數除以第二個三數的餘數,寫一個程式 exercise2007.cxx ,印出 200 除以 19 、 20.5 除以 1.6 、 -22 除以 -5 等餘數。 參考程式碼 |
8. 承上題, cmath 的 exp2() 回傳 2 的指數,寫一個程式 exercise2008.cxx ,印出 2 的 6 次方到 10 次方。 參考程式碼 |
9. 承上題, cmath 的 pow() 計算指數,第一個參數為底數,第二個參數為次方,寫一個程式 exercise2009.cxx ,印出 2 的 6 次方到 10 次方。 參考程式碼 |
10. C++17 在 numeric 新增了 gcd() , gcd() 用來計算兩個整數參數的最大公因數,寫一個程式 exercise2010.cxx ,印出 20 與 6 到 10 之間整數的最大公因數。 參考程式碼 |
相關教學影片