接下來的兩篇分別介紹變數型別及運算子,初學者只需大略了解相關概念,必要時再查詢相關資料即可。
前面我們介紹如何宣告整數型別的變數,但其實變數不只有整數,其實整數也有不少種。C++ 中的基本資料型別可以分為整數、浮點數、字元及布林等等,其中因為 C++ 算是弱型別語言,字元及布林是能直接和整數轉換的。
整數#
標準中的整數關鍵字計有:short
, int
, long
, long long
, … 為何要有那麼種多整數呢??以 ZeroJudge a002 為例,這題要求兩整數的和,相信對各位十分容易,肯定能輕易地寫出以下的程式:
現在請 F5 執行這個程式。輸入 \(87\ 69\), \(9487\ 666\), \(92\ 89\) 之類的顯然不會有問題,但現在試著輸入很大很大的數字。要多大呢??\(1000000000\ 1000000000\) 還是正確的,但 \(10000000000\ 10000000000\) 就出事惹阿伯!!怎麼跑出一個奇怪的負數,是電腦壞惹嗎??
對的沒錯你的電腦壞惹 50 收 先別急著砸爛電腦。事實上,每種基本型別都有其上下限。以 int
為例,在現代電腦中通常佔據 4 個位元組 (bytes), 即 32 位元 (bits), 而其中一個位元用來表達正負,因此 int
的值域為 \([-2^{31}, 2^{31})\), 請注意這是一個左閉右開的區間,並不包含 \(2^{31}\), 原因是區間中含有 \(0\).
當我們計算的值超出當前型別的最大值,稱為 Overflow. 這是一個很容易犯的錯誤,也常造成不少損失,知名案例包括波音 787, 2038 年問題、好幾年前的「江南 Style」點閱次數超過 \(2^{31} - 1\) 迫使 YouTube 採用 64-bit 整數等等。
long
, 長整數,顧名思義值域應該要比 int
長吧??如果你這麼想很正確但是錯的,這就是 C/C++ 中一些因為歷史的沉痾而肇致的討人厭共業。現今我們的電腦多數為 32-bit 或 64-bit (mostly x86_64), 然而在 C 出生的年代主流是 8-bit 或 16-bit, 因此實際上標準中對 int
, long
的定義分別是至少 2 bytes (16 bits), 4 bytes (32 bits). 也就是說,夠古老的 C 教材會告訴你 int
的值域為 \([-2^{15}, 2^{15})\). 而在 32-bit OS int
通常是 4 bytes (32 bits), 64-bit 為惹相容性也是蕭規曹隨沿襲下去。
麻煩的就在這個 long
喇。上面說道標準中 long
至少 4 bytes (32 bits), 對於 64-bit OS 主要有兩種 data model, LLP64 與 LP64, 其中 long
分別為 4 bytes (32 bits), 8 bytes (64 bits). 大部分的 OS 如 Linux, macOS, BSD 等等都是採取 LP64, 就微軟搞怪用 LLP64, 也就是說 64-bit Windows 上 long
的值域相當於 int
只有 \([-2^{31}, 2^{31})\).
因此,標準才改版新增惹 long long
這個整數型別,保證其至少為 8 bytes (64 bits).
回過頭來看這題說輸入的數字 \(a, b \Rightarrow |a| < 10^6, |b| < 10^6\), 因此我們用 int
是沒有問題的。程式競賽與日常生活中的問題一樣,首先我們必須定義問題,問題一定有條件、範圍,然後我們據此選擇解決問題的方法。
至於 short
就是短整數,除非要壓空間不然應該很少用。題外話是在 32-bit OS 同樣有 LP32, ILP32 之爭,真的很討厭不要理它吧。
另外上面有提到整數型別需要使用 1 bit 來記錄正負,而如果不需要管負數的話可以在整數前面加個 unsigned
, 比如說 unsigned int
, 則值域會變為 \([0, 2^{32})\). 現在電腦儲存負數的方式為 2-補數,以後有機會結合位元運算再加以說明。
類題演練#
這兩題都會用到 if-else
, 晚點記得回過頭來看看!!
浮點數#
如果說程式語言中的 int
是對於數論中 \(\mathbb{Z}\) 的近似,那麼浮點數就是 \(\mathbb{R}\) 實數的近似。既然說是近似惹,那就一定存在誤差。因為我們目前還未學到 if-else 條件判斷所以就不示範,事實上在電腦中 \(0.1 + 0.2 = 0.30000000000000004 \neq 0.3\), 下面是個 meme 博君一笑。

原因的話很容易理解:十進位制的有限小數轉到二進位制往往變成循環小數。目前主流關於浮點數的規範是 IEEE 754, 大一程設通常會教,這邊就不多說惹。
注意到現在編譯器預設字面常量都是 double
, 不要再用 float
喇,當然還有 long double
更精準。
使用浮點數務必小心謹慎,分享幾個因為不察浮點數誤差造就的悲劇:
- Intel Pentium FDIV bug (US$ 470M)
- Ariane 5 火箭發射意外(2 人死亡)
- 愛國者防空飛彈意外,攔不到伊拉克飛毛腿飛彈(28 人死亡、98 人受傷)
成大教授好文推介。
型別轉換#
看看這段程式會輸出什麼??
#include <iostream>
using namespace std;
int main()
{
cout << "87 / 69 = " << 87 / 69 << '\n';
return 0;
}
結果是 \(1\) 而非 \(1.26086…\) 前面提到過 C/C++ 中,對於兩整數使用 /
是取其商,那如果要求有小數的結果呢??我們可以使用以下語法來顯式地轉型:
// 略 ...
cout << double(87) / 69 << '\n';
// 略 ...
而以下方法則是隱式地轉型:
// 略 ...
cout << 87 * 1.0 / 69 << '\n';
// 略 ...
當兩個不同型別的變數運算時,預設會轉為比較廣泛的型別。而 C++ 還有 static_cast, const_cast, reinterpret_cast, dynamic_cast 等轉型方式,有興趣可以自行深入了解。
類題演練#
攝氏溫度 ℃ 與華氏溫度 ℉ 的轉換,剛好各一題。
字元#
前面我們多次使用字面常量的字串 "This is a string."
事實上,字串的組成單元是字元;準確來說,字串其實是字元陣列,單個字元我們用單引號夾住。一個字元就代表一個西方語系符號,中日韓共同表意文字 (CJK Ideograph) 則對應二或三個字元,一個跳脫序列也會對應到一個特殊字元。
電腦只懂二進位制,要記錄字元我們需要將之對映為數字,這個過程稱作編碼 (encoding), 知名編碼表包括 ANSI, ASCII, Unicode, UTF-8, … 舉例而言,'0'
的值是 \(48\), 'A'
的值是 \(65\), 'a'
的值是 \(97\), '\n'
的值是 \(10\), ' '
的值是 \(32\). 有趣的是,大寫字母與小寫字母剛好差 \(32\), 就是第五位元,而 \(32\) 又是空白字元 ' '
的值,因此在做大小寫轉換時可以應用位元運算。
關於字串及陣列,往後會詳盡解釋。這裡似乎寫得有點凌亂,總之:
MISC#
最後補一些雜七雜八的東西。
const & constexpr#
const
是 constant 的縮寫,就是常數的意思,在宣告變數時於型別前面加上 const
修飾,就可以宣告常數,之後其值就無法更改。常數仍然儲存在堆疊或堆積等記憶體中,換言之它不能節省記憶體,但可以在執行時期決定。
constexpr
則是 C++ 11 的新關鍵字,可以修飾變數或函數,其值在編譯時期就必須決定,或可優化程式效能。
sizeof()#
上面談到 int
, long
, long long
的大小是否令你一頭霧水??其實,我們可以使用 sizeof()
運算子得到各型別的大小:
#include <iostream>
using namespace std;
int main()
{
cout << sizeof(short) << '\n'
<< sizeof(int) << '\n'
<< sizeof(long) << '\n'
<< sizeof(long long) << '\n'
<< sizeof(float) << '\n'
<< sizeof(double) << '\n'
<< sizeof(long double) << '\n'
<< sizeof(char) << '\n'
#ifdef __SIZEOF_INT128__
<< sizeof(__int128_t) << '\n'
#endif
;
return 0;
}
上面的程式碼輸出八種基本型別的大小,單位是 byte. 而 __int128_t
是 gcc 提供的黑魔法,macOS 如果使用蘋果的 clang++ 則無法使用喔 o’_‘o
最後補充 <cstdint> _(<stdint.h>)_
有提供 int8_t
, int16_t
, int32_t
, int64_t
, … 這種定義。
Comments