C++ 變數、型別與輸入輸出:讓編譯器替你把關
從靜態型別、const 到 cin/cout 與型別轉換,看 C++ 如何在程式執行前就抓住錯誤,以及 Python 使用者最容易踩的雷
從一張收據開始:讓編譯器在你跑程式前先抓錯
假設你要寫一支小程式:讀進商品「單價」與「數量」,算出總價。在 Python 裡你大概不會多想,直接 price * qty 就好。但在 C++ 裡,你得先回答一個問題——「單價是整數還是小數?數量呢?」——而且這個答案必須在寫程式時就講清楚,寫進變數宣告裡。
這聽起來像是多此一舉的麻煩,卻正是 C++ 與 Python 最根本的分野。Python 是動態型別(dynamic typing):變數的型別在執行時才確定,一個 x 今天裝整數、明天裝字串都沒人攔你。C++ 是靜態型別(static typing):每個變數從出生那一刻就綁定一個固定型別,且這件事由編譯器(compiler)在程式真正執行之前就檢查完畢。打錯型別?程式根本編譯不過,連跑都跑不起來。
這篇文章會帶你認識 C++ 的型別系統、變數宣告、cin/cout 輸入輸出,以及那些 Python 使用者轉過來最容易踩的雷。我們的目標不是「把 Python 換成 C++ 語法重講一次」,而是理解 C++ 為什麼要這樣設計,以及這個設計給了你什麼。

變數宣告:型別寫在最前面
C++ 變數的基本形式是「型別 名稱 = 初始值;」。型別在前,這是靜態型別語言的招牌。
#include <iostream>
int main() {
int qty = 3; // 整數
double price = 49.9; // 雙精度浮點數
char grade = 'A'; // 單一字元(注意是單引號)
bool passed = true; // 布林值:true 或 false
std::cout << qty << " " << price << " " << grade << " " << passed << "\n";
// 輸出:3 49.9 A 1
return 0;
}
幾個立刻要記住的點:
bool印出來是1與0,不是true/false(除非你用std::boolalpha)。char用單引號'A',字串用雙引號"A",兩者是完全不同的東西。'A'本質上是一個整數(它的 ASCII 值是 65)。- 每個敘述句結尾要有分號
;,這點 Python 使用者要適應。
相較於 Python 的 qty = 3,C++ 多寫的那個 int 不是廢話。它是一份契約:你和編譯器約定好 qty 永遠是整數,編譯器就能在後續每一次使用時替你檢查、並產生最有效率的機器碼。
四個基本內建型別
| 型別 | 用途 | 範例字面值 | 典型大小 |
|---|---|---|---|
int |
整數 | 42、-7 |
4 bytes(約 ±21 億) |
double |
浮點數(小數) | 3.14、2.0 |
8 bytes |
char |
單一字元 | 'x'、'\n' |
1 byte |
bool |
真假值 | true、false |
1 byte |
這裡藏著 C++ 與 Python 的一大差異:整數有上限。Python 的整數可以無限大(自動升級成大數),但 C++ 的 int 是固定 4 bytes,超過範圍會發生「溢位(overflow)」,得到莫名其妙的結果。需要更大的範圍時改用 long long。
cout 與 cin:C++ 的輸入輸出
C++ 透過 <iostream> 標準函式庫做輸入輸出。輸出用 std::cout,搭配 <<(插入運算子);輸入用 std::cin,搭配 >>(擷取運算子)。
#include <iostream>
int main() {
int age;
std::cout << "請輸入你的年齡:";
std::cin >> age;
std::cout << "明年你就 " << age + 1 << " 歲了\n";
return 0;
}
<< 可以串接,由左往右把東西「推」進輸出串流。"\n" 是換行字元。你也會看到有人用 std::endl,它除了換行還會強制清空緩衝區(flush),通常較慢;除非你真的需要立刻刷新輸出,平常用 "\n" 就好。
那一長串 std:: 是什麼?std 是 standard library 的命名空間(namespace)。許多教材會在開頭寫 using namespace std; 來省略它,但這在大型專案中容易造成名稱衝突,是業界普遍不建議的反模式。比較好的折衷是只引入你要的:
#include <iostream>
using std::cout;
using std::cin;
本文後續為清楚起見保留 std:: 前綴。
const:宣告「這個值不會變」
如果某個值在程式執行期間不該被改動——例如圓周率、稅率、班級人數上限——就用 const 把它鎖起來。
const double TAX_RATE = 0.05;
const int MAX_STUDENTS = 50;
// TAX_RATE = 0.07; // 編譯錯誤!不能修改 const 變數
const 不只是「給人看的提醒」,它是編譯期強制的。一旦你試圖修改它,編譯器直接報錯。這呼應了 C++ 的核心哲學:把錯誤盡量提前到編譯期。相較於 Python 靠命名慣例(全大寫變數「約定俗成」不要改,但語言其實攔不住你),C++ 的 const 是真有牙齒的保證。養成習慣:只要一個值邏輯上不該變,就加 const。
auto:讓編譯器幫你推導型別
寫多了 int、double,你可能會想:既然初始值已經透露了型別,能不能省下那個字?可以,用 auto:
auto qty = 3; // 編譯器推導為 int
auto price = 49.9; // 推導為 double
auto name = 'K'; // 推導為 char
注意:auto 不是動態型別。它只是叫編譯器「看右邊的初始值,幫我把型別填上」。一旦推導完成,qty 就永遠是 int,跟你親手寫 int qty 完全一樣,之後想塞字串照樣編譯不過。這跟 Python 的 qty = 3 看似神似,本質卻天差地遠:Python 的 qty 隨時能換型別,C++ 的 auto qty 是「型別被推導後就定終身」。
auto 真正發光的場合是型別名稱很長的時候(例如迭代器、複雜的範本型別),這在你之後學到容器與範本時會深刻體會。對基本型別,初學階段我建議:簡單時把型別寫清楚有助於閱讀,等你熟練了再依場合用 auto。
整數與浮點:那個會咬人的除法
這是初學者最常中招的地方。在 C++ 中,兩個整數相除得到的是整數,小數部分直接被截掉(無條件捨去,不是四捨五入)。
#include <iostream>
int main() {
int a = 7, b = 2;
std::cout << a / b << "\n"; // 輸出:3 ←整數除法!
std::cout << 7.0 / 2 << "\n"; // 輸出:3.5 ←只要有一個是浮點
std::cout << a % b << "\n"; // 輸出:1 ←取餘數
return 0;
}
7 / 2 給你 3 而不是 3.5,因為兩個運算元都是 int,C++ 認定你要的是整數運算。想要小數結果,至少要讓其中一個是浮點數,例如 7.0 / 2,或把變數轉型(下一節談)。
這與 Python 3 的行為不同:Python 3 的 / 永遠回傳浮點(7 / 2 得 3.5),整數除法要用 //。從 Python 轉來的人請特別小心這個差異。
浮點數還有另一個無關語言、但 C++ 不會替你藏起來的坑——精度。double 用二進位表示小數,有些十進位小數無法精確表示:
std::cout << (0.1 + 0.2 == 0.3) << "\n"; // 輸出:0(也就是 false!)
所以永遠不要用 == 直接比較兩個浮點數,而要檢查它們的差是否小於一個很小的容許誤差(如 1e-9)。
型別轉換:隱式與顯式
當不同型別混在一起運算,C++ 會做隱式轉換(implicit conversion),通常把「小」的型別自動升級成「大」的:
int n = 5;
double result = n / 2.0; // n 被隱式轉成 double,得 2.5
但你也可以顯式轉換,明確表達意圖。現代 C++ 慣用 static_cast<目標型別>(值):
int a = 7, b = 2;
double exact = static_cast<double>(a) / b; // 先把 a 變成 7.0,再除
std::cout << exact << "\n"; // 輸出:3.5
你可能在舊教材看過 C 風格的 (double)a,能動,但 static_cast 更清楚、更安全(編譯器會做更多檢查),是 C++ 推薦寫法。記住一條原則:寧可顯式,不要依賴隱式。當轉換是你刻意要的,就寫出來給讀者看。
動手寫一段:BMI 計算機
把上面的東西串起來。這支程式讀進身高(公尺)與體重(公斤),算出 BMI,並用 const 鎖住判斷門檻。
#include <iostream>
int main() {
const double NORMAL_UPPER = 24.0; // 正常範圍上限
double height, weight;
std::cout << "請輸入身高(公尺):";
std::cin >> height;
std::cout << "請輸入體重(公斤):";
std::cin >> weight;
double bmi = weight / (height * height);
std::cout << "你的 BMI 是:" << bmi << "\n";
bool overweight = bmi > NORMAL_UPPER;
std::cout << "是否超過正常上限:" << std::boolalpha << overweight << "\n";
return 0;
}
假設輸入身高 1.7、體重 70,預期輸出:
請輸入身高(公尺):1.7
請輸入體重(公斤):70
你的 BMI 是:24.2215
是否超過正常上限:true
注意三件事:我用 double 而非 int 來存身高體重(否則 1.7 會被截成 1,整個算錯);用 const 標示門檻是固定值;用 std::boolalpha 讓 bool 印成 true 而非 1。
常見錯誤
初學 C++ 型別系統,這幾個雷幾乎人人踩過一次,先認得它們:
- 整數除法吃掉小數:
int total = 7 / 2;得到3。要小數結果,記得讓運算元帶浮點,或用static_cast<double>。這是最高頻的邏輯錯誤。 - 忘了初始化:
int x;之後直接使用x,它的值是未定義的垃圾值(不像 Python/Java 會給預設值,也不像它們會報錯)。養成宣告即初始化的習慣:int x = 0;。 char與字串搞混:'A'是字元、"A"是字串,不能互換。char c = "A";會編譯失敗。- 用
==比較浮點數:因為精度問題0.1 + 0.2 != 0.3。改用std::abs(a - b) < 1e-9。 - 整數溢位無聲無息:
int超過約 21 億會回繞成負數,且不會報錯。處理大數請用long long。
重點回顧
- C++ 是靜態型別:型別寫在變數宣告裡,編譯器在執行前就檢查完畢。
- 四大基本型別:
int、double、char、bool;整數有大小上限,浮點有精度限制。 const鎖住不可變的值,是編譯期強制的保證,不只是慣例。auto是編譯期型別推導,不是動態型別;推導完就定終身。cin >>讀輸入、cout <<印輸出;型別轉換優先用static_cast,寧可顯式。
深入探討(研究所視角)
靜態型別的真正價值:把錯誤鎖在編譯期
我們一直說 C++「在執行前就檢查型別」,這句話的份量值得展開。型別檢查可以發生在兩個時間點:編譯期(compile time)或執行期(run time)。Python 把它推遲到執行期——一個型別錯誤的程式碼路徑,只有當程式真的跑到那一行、那一筆資料時才會炸開。如果那條路徑藏在某個罕見的 if 分支裡,你可能上線數週後才在使用者面前出包。
靜態型別把這類錯誤前移到編譯期。std::string s = 42; 這種錯誤不可能逃過編譯,程式根本生不出來。從軟體工程角度,這是一種「左移(shift left)」:缺陷發現得越早,修復成本越低。型別系統在這裡扮演的是一個輕量級、永遠在跑、零執行成本的證明器——它在編譯期證明了「所有對這個變數的使用都符合它的型別」這條性質。
而且這份保證是零執行期成本的。型別資訊在編譯後大多被「抹除」進機器碼,執行時 CPU 不需要再帶著型別標籤到處跑、不需要在每次運算前查表確認型別。這正是 C++「零成本抽象(zero-overhead abstraction)」哲學的一個面向:你享受到型別安全的好處,卻不為它付出執行期的代價。相較之下,動態語言每個值都得在執行期攜帶型別資訊、每次操作都要動態分派(dynamic dispatch),這份彈性是有速度與記憶體代價的。
隱式轉換的陷阱
靜態型別不是萬靈丹。C++ 為了相容 C、為了書寫方便,保留了大量隱式轉換規則,而這些「貼心」的自動轉換正是 bug 的溫床。
考慮無號與有號整數的混用:
unsigned int u = 3;
int s = -1;
if (s < u) {
std::cout << "s 比較小\n";
} else {
std::cout << "u 比較小\n"; // 實際印出這行!
}
直覺上 -1 < 3 應該成立,但這裡輸出竟是「u 比較小」。原因是當有號數 s 與無號數 u 比較時,C++ 會把 s 隱式轉成無號數,-1 在無號數的世界裡會回繞成一個很大的正數(約 42 億),於是 s < u 變成 false。這類「無號數比較」陷阱在迴圈邊界、容器 .size()(回傳無號型別)時特別常見,是 C++ 老手都會多看兩眼的地方。
另一個經典是窄化轉換造成的精度與資訊流失:把 double 指定給 int 會默默截斷小數,把大的 int 塞進 char 會溢位。編譯器對這些多半只給警告、甚至不吭聲。所以開發時請務必開啟嚴格警告旗標(-Wall -Wextra),讓編譯器把這些可疑轉換攤在你眼前。
窄化(narrowing)與 C++11 的補救
C++11 引入了大括號初始化(brace initialization),順帶提供了一道對付窄化的防線。用 {} 初始化時,禁止窄化轉換——會把資訊流失的隱式轉換從「警告」升級為「編譯錯誤」:
int a = 3.9; // 合法,但默默截斷成 3(傳統初始化容忍窄化)
int b{3.9}; // 編譯錯誤!大括號初始化禁止窄化轉換
int c{3}; // OK
int b{3.9} 直接編譯失敗,因為 double 到 int 會流失小數,屬於窄化。這正是大括號初始化被現代 C++ 推崇的理由之一:它把一類靜默的資料流失錯誤,提升為編譯期就攔得住的硬錯誤,徹底貫徹「錯誤越早發現越好」的精神。許多現代 C++ 風格指南因此建議:優先使用 {} 初始化。
把這幾點串起來,你會看見 C++ 型別系統的設計張力:它一方面用靜態型別、const、窄化檢查把大量錯誤鎖在編譯期;另一方面又因歷史包袱保留了隱式轉換這類危險彈性。當一名稱職的 C++ 開發者,意味著你既要善用前者給的保護,也要熟知後者埋的地雷——而這份「知道編譯器何時會替你做主」的意識,正是從寫得出程式到寫得出可靠程式之間的距離。