學習目標
讀完本章後,你應該能夠:
- 學會宣告與初始化變數,理解四種初始化形式的差異
- 認識 C++ 的基本資料型別(整數、浮點數、字元、布林)
- 理解固定寬度整數型別(
<cstdint>)與何時該用它們 - 搞懂 signed / unsigned 的差別與常見陷阱
- 了解浮點數的 IEEE 754 表示法,以及為什麼
0.1 + 0.2 != 0.3 - 掌握型別轉換規則(整數提升、一般算術轉換)與四種 cast
- 認識位元運算子(
&、|、^、~、<<、>>) - 區分
const與constexpr - 理解作用域(scope)與儲存期(storage duration)的概念
- 熟悉各種運算子及其優先順序
1. 變數(Variables)
變數是程式中用來儲存資料的命名記憶體空間。使用前必須先宣告其型別與名稱。可以把變數想成一個貼了標籤、規定了大小的盒子:標籤是名稱,盒子大小由型別決定,裡面放的東西是值。
初始化的四種形式
// 宣告(僅保留空間,值未定義)
int x;
int a = 10; // (1) 複製初始化(copy initialization)
int b(20); // (2) 直接初始化(direct initialization)
int c{30}; // (3) 列表初始化(list / brace initialization,C++11 推薦)
int d = {40}; // (4) 複製列表初始化(copy-list initialization)
| 形式 | 寫法 | 特性 |
|---|---|---|
| 複製初始化 | int a = 10; |
最常見、易讀 |
| 直接初始化 | int b(20); |
呼叫建構子的語意較明確 |
| 列表初始化 | int c{30}; |
防止窄化轉換,現代 C++ 推薦 |
| 複製列表初始化 | int d = {40}; |
同列表初始化,也防窄化 |
為什麼推薦 {}?防止窄化轉換(Narrowing)
「窄化」是指把一個可能裝不下的值塞進較小的型別。{} 會在編譯期就攔截這種危險:
int e = 3.14; // 可以編譯(但靜默截斷成 3,危險!)
int f{3.14}; // ❌ 編譯錯誤:narrowing conversion,幫你擋下來
C++ Core Guidelines(ES.23):優先使用
{}初始化語法,因為它在編譯期就能抓出窄化錯誤,避免靜默的資料遺失。
一定要初始化!
int score; // 區域變數未初始化 → 內含「垃圾值」
std::cout << score; // 未定義行為(Undefined Behavior, UB)
未初始化的區域變數其值是不確定的(可能每次執行都不同)。讀取它是未定義行為——程式可能正常、可能崩潰、可能給出莫名其妙的結果。
C++ Core Guidelines(ES.20):永遠初始化你的物件(Always initialize an object)。最簡單的安全寫法是
int score{};,會初始化為 0。
多個變數宣告
int i = 1, j = 2, k = 3; // 同時宣告多個同型別變數
// int m = 1, double n = 2; // 錯誤!不能混合不同型別
命名規則
| 規則 | 範例 |
|---|---|
| 由字母、數字、底線組成 | score, player_1, _count |
| 不能以數字開頭 | 1st_place ❌ → first_place ✅ |
| 不能使用保留字 | int, return, class 等 |
| 大小寫敏感 | Score 和 score 是不同的變數 |
| 建議使用有意義的名稱 | studentAge 優於 sa |
命名風格建議
int studentAge; // camelCase(常見於變數)
int student_age; // snake_case(C++ 標準函式庫風格)
const int MAX_SIZE; // SCREAMING_SNAKE_CASE(常見於常數)
2. 基本資料型別
整數型別
| 型別 | 最小大小 | 典型大小 | 值範圍(典型 64 位元平台) |
|---|---|---|---|
short |
16 位元 | 2 bytes | -32,768 ~ 32,767 |
int |
16 位元 | 4 bytes | -2,147,483,648 ~ 2,147,483,647 |
long |
32 位元 | 4/8 bytes | 依平台而定 |
long long |
64 位元 | 8 bytes | ±9.2 × 10¹⁸ |
⚠️ 重要觀念:C++ 標準只保證每種型別的最小大小與相對關係(
short ≤ int ≤ long ≤ long long),不保證確切的位元組數。int在某些嵌入式平台可能只有 2 bytes!想要可攜的精確大小,請用下節的固定寬度型別。
每種整數型別都有 unsigned 版本(僅非負數,正值範圍加倍):
unsigned int positiveOnly = 42; // 0 ~ 4,294,967,295
unsigned short small = 100; // 0 ~ 65,535
固定寬度整數型別(<cstdint>,C++11)
當你需要「在任何平台上都是 32 位元」時,用這些別名:
#include <cstdint>
int8_t a = 127; // 剛好 8 位元有號
int16_t b = 32767; // 16 位元
int32_t c = 2000000000; // 32 位元(最常用來取代 int)
int64_t d = 9000000000LL; // 64 位元
uint8_t e = 255; // 無號版本,前面加 u
uint32_t f = 4000000000U;
| 型別 | 位元數 | 用途 |
|---|---|---|
int8_t / uint8_t |
8 | 處理位元組、二進位資料 |
int16_t / uint16_t |
16 | 省記憶體的小整數 |
int32_t / uint32_t |
32 | 一般整數運算(跨平台一致) |
int64_t / uint64_t |
64 | 大數值、時間戳、檔案大小 |
何時用? 撰寫網路協定、檔案格式、需要精確控制位元數時,固定寬度型別是首選。一般日常運算用
int即可。 ⚠️ 注意:int8_t/uint8_t通常其實是char,用std::cout輸出時會被當成「字元」而非數字,需要static_cast<int>()轉一下才會印出數字。
浮點數型別
| 型別 | 大小 | 精度 |
|---|---|---|
float |
4 bytes | ~7 位有效數字 |
double |
8 bytes | ~15 位有效數字 |
long double |
8/16 bytes | ≥ double 精度 |
float pi_f = 3.14159f; // 注意 f 後綴
double pi_d = 3.14159265358979;
建議:除非有特殊記憶體或效能考量,浮點運算預設用
double。float精度較低,容易累積誤差。
字元與布林
char letter = 'A'; // 單一字元,用單引號
char newline = '\n'; // 跳脫字元
bool isReady = true; // 布林值:true 或 false
bool isFinished = false;
char 本質上是一個小整數(通常 1 byte),存的是字元的編碼(如 ASCII)。'A' 其實就是數字 65。
字面值後綴與分隔符
auto big = 10000000000LL; // LL → long long
auto ufull = 42U; // U → unsigned
auto money = 19.99f; // f → float
auto large = 1'000'000; // C++14:' 數字分隔符,方便閱讀
int hex = 0xFF; // 十六進位 → 255
int oct = 0o17; // (註:八進位實際寫法是 017)
int bin = 0b1010; // 二進位(C++14)→ 10
使用 sizeof 查看型別大小
std::cout << "int 大小:" << sizeof(int) << " bytes" << std::endl;
std::cout << "double 大小:" << sizeof(double) << " bytes" << std::endl;
// 典型輸出:int 大小:4 bytes / double 大小:8 bytes
使用 <limits> 查看型別範圍
#include <limits>
std::cout << "int 最大值:" << std::numeric_limits<int>::max() << std::endl;
std::cout << "int 最小值:" << std::numeric_limits<int>::min() << std::endl;
// int 最大值:2147483647 / int 最小值:-2147483648
3. signed 與 unsigned 的陷阱
unsigned 型別只能存非負數,看似省範圍,卻是 bug 的溫床。
陷阱一:減到負數會「回繞」
unsigned int x = 0;
x = x - 1;
std::cout << x << '\n'; // 4294967295(不是 -1!回繞到最大值)
無號數沒有負數的概念,0 - 1 會「繞回」到最大值。這是有明確定義的行為,但常常不是你要的。
陷阱二:signed 與 unsigned 混合比較
int a = -1;
unsigned int b = 1;
if (a < b) { // 你以為會成立(-1 < 1)
std::cout << "a 比較小\n";
} else {
std::cout << "居然走到這!\n"; // 實際上會印這行!
}
當有號與無號比較時,有號數會被「轉成」無號數:-1 變成一個超大的正數,於是 a < b 變成 false。這就是為什麼 -Wall -Wextra 會對 signed/unsigned 比較發出警告。
陷阱三:迴圈用 unsigned 倒數
for (unsigned int i = 5; i >= 0; i--) { // ❌ 無窮迴圈!
std::cout << i << " ";
}
// i 永遠 >= 0(因為無號數不會變負),i 從 0 再減 1 會回繞成超大正數
C++ Core Guidelines(ES.106、ES.107):除非真的需要位元操作或與系統 API 對接,避免無謂地使用 unsigned。迴圈索引若可能需要做減法或倒數,用 signed 的
int比較安全。
4. 浮點數的真相:IEEE 754
電腦用 IEEE 754 標準以二進位的「科學記號」儲存浮點數,大致拆成三部分:
符號位 指數 尾數(小數部分)
┌──┬───────────────┬──────────────────────────┐
│ S│ exponent │ mantissa │ ← double:1 + 11 + 52 位元
└──┴───────────────┴──────────────────────────┘
關鍵在於:二進位無法精確表示所有十進位小數,就像十進位無法精確寫出 1/3 = 0.333... 一樣。0.1 在二進位是一個無限循環小數,只能存「最接近的近似值」。
double sum = 0.1 + 0.2;
std::cout << sum << '\n'; // 印出 0.3(預設精度看不出問題)
std::cout << (sum == 0.3) << '\n'; // 0(false!)
std::cout << std::setprecision(17) << sum; // 0.30000000000000004
0.1 與 0.2 各自存的都是近似值,相加後的誤差讓結果不等於同樣是近似值的 0.3。
正確比較浮點數
永遠不要用 == 比較浮點數,改用「差的絕對值小於一個容忍值(epsilon)」:
#include <cmath> // std::abs / std::fabs
bool nearlyEqual(double a, double b, double eps = 1e-9) {
return std::fabs(a - b) < eps;
}
if (nearlyEqual(0.1 + 0.2, 0.3)) {
std::cout << "視為相等\n"; // 會印這行
}
實務建議:金額、貨幣等需要「精確」的場合,不要用
double!改用整數(以「分」為單位)或專門的定點數 / 大數函式庫,避免浮點誤差累積。
5. const 與 constexpr
const — 執行期常數
const 表示「這個變數初始化後就不能被修改」:
const double PI = 3.14159265358979;
const int MAX_STUDENTS = 50;
// PI = 3.14; // ❌ 編譯錯誤!不能修改 const 變數
const 的值可以在執行期才決定:
int n;
std::cin >> n;
const int size = n; // OK:size 之後不可改,但它的值來自執行期輸入
constexpr — 編譯期常數(C++11)
constexpr 更強:要求值在編譯期就能算出來。適合用於陣列大小、模板參數等必須是編譯期常數的地方。
constexpr int BUFFER_SIZE = 1024; // 編譯期就確定
constexpr double TWO_PI = 2 * 3.14159265; // 編譯期計算
int buffer[BUFFER_SIZE]; // OK,陣列大小需編譯期常數
// constexpr int x = n; // ❌ 錯誤:n 是執行期才知道的值
const |
constexpr |
|
|---|---|---|
| 何時確定值 | 可執行期 | 必須編譯期 |
| 能否當陣列大小 | 不一定 | 可以 |
| 語意 | 「唯讀」 | 「編譯期常數」 |
C++ Core Guidelines(Con.1、Con.5):能用
constexpr就用constexpr,否則用const。預設讓東西不可變,需要修改時才拿掉 const,能避免許多意外修改的 bug。
6. auto — 自動型別推導(C++11)
auto x = 42; // 推導為 int
auto y = 3.14; // 推導為 double
auto name = "Hello"; // 推導為 const char*
auto flag = true; // 推導為 bool
auto讓編譯器根據初始值自動判斷型別,簡化程式碼(尤其是型別名稱很長的迭代器)。初學時建議明確寫出型別以加深理解,熟悉後再多用auto。注意:auto變數必須初始化,否則編譯器無從推導。
7. 型別轉換(Type Conversion)
隱式轉換與「一般算術轉換」
當不同型別一起運算時,編譯器會自動把它們轉成「共同型別」,這套規則叫一般算術轉換(usual arithmetic conversions),簡化記法:
- 比
int小的型別(char、short)會先「整數提升(integer promotion)」成int。 - 接著往「範圍較大 / 浮點」的一方靠攏:
int→unsigned→long→long long→float→double→long double。
char c = 10;
short s = 20;
auto r = c + s; // c、s 都先提升為 int,r 的型別是 int
int i = 5;
double d = 2.0;
auto r2 = i + d; // i 轉成 double,r2 是 double(= 7.0)
可能造成精度遺失的方向:
int i = 42;
double d = i; // int → double(安全,無精度遺失)
int j = 3.99; // double → int(危險!小數被截斷,j = 3)
四種明確轉換(Cast)
C++ 提供四種具名的轉型運算子,比 C 風格的 (int)x 更安全、意圖更清楚:
| Cast | 用途 | 初學階段重要性 |
|---|---|---|
static_cast<T>(x) |
一般、合理的型別轉換(數值、相關型別) | ⭐⭐⭐ 最常用 |
const_cast<T>(x) |
移除 / 加上 const 限定 |
⭐ 少用,謹慎 |
reinterpret_cast<T>(x) |
重新詮釋位元(指標型別間) | ⭐ 低階,危險 |
dynamic_cast<T>(x) |
多型階層的安全向下轉型(執行期檢查) | 學到繼承時再深入 |
日常 99% 的情況用 static_cast 就夠了:
double pi = 3.14159;
int truncated = static_cast<int>(pi); // 明確轉換,truncated = 3
int a = 7, b = 2;
double result = static_cast<double>(a) / b; // 3.5(先轉再除)
C++ Core Guidelines(ES.48、ES.49):避免 C 風格轉型
(int)x,改用具名 cast。具名 cast 不僅檢查更嚴格,也讓讀者一眼看出「這裡正在做哪種轉換」,方便日後搜尋與審查。
8. 運算子
算術運算子
| 運算子 | 說明 | 範例 | 結果 |
|---|---|---|---|
+ |
加法 | 5 + 3 |
8 |
- |
減法 | 5 - 3 |
2 |
* |
乘法 | 5 * 3 |
15 |
/ |
除法 | 7 / 2 |
3(整數除法) |
% |
取餘數 | 7 % 2 |
1 |
注意:整數除法會截斷小數部分!
7 / 2 = 3,若要得到3.5,需將至少一個運算元轉為浮點數(例如7.0 / 2或static_cast<double>(7) / 2)。%只能用於整數;-17 % 5的結果是-2(餘數的正負號跟隨被除數)。
比較運算子
| 運算子 | 說明 | 範例 | 結果 |
|---|---|---|---|
== |
等於 | 5 == 5 |
true |
!= |
不等於 | 5 != 3 |
true |
< |
小於 | 3 < 5 |
true |
> |
大於 | 5 > 3 |
true |
<= |
小於等於 | 5 <= 5 |
true |
>= |
大於等於 | 3 >= 5 |
false |
邏輯運算子
| 運算子 | 說明 | 範例 | 結果 |
|---|---|---|---|
&& |
且(AND) | true && false |
false |
|| |
或(OR) | true || false |
true |
! |
非(NOT) | !true |
false |
邏輯運算子具有短路求值:&& 左邊為 false 就不算右邊,|| 左邊為 true 就不算右邊(詳見 Ch03)。
位元運算子(Bitwise Operators)
位元運算子直接對整數的二進位位元逐位操作,常用於旗標、遮罩、低階最佳化。
| 運算子 | 說明 | 範例(以 8 位元示意) | 結果 |
|---|---|---|---|
& |
位元 AND | 0b1100 & 0b1010 |
0b1000(8) |
| |
位元 OR | 0b1100 | 0b1010 |
0b1110(14) |
^ |
位元 XOR(互斥或) | 0b1100 ^ 0b1010 |
0b0110(6) |
~ |
位元 NOT(取反) | ~0b00001100 |
0b...11110011 |
<< |
左移 | 1 << 4 |
16(= 2⁴) |
>> |
右移 | 32 >> 2 |
8(= 32 / 4) |
unsigned int flags = 0b0000;
flags |= 0b0010; // 設定第 1 個位元(開啟某旗標)→ 0b0010
bool isOn = flags & 0b0010; // 檢查該位元是否為 1 → true
flags &= ~0b0010; // 清除第 1 個位元 → 0b0000
int x = 5;
std::cout << (x << 1) << '\n'; // 10(左移 1 位 = 乘以 2)
std::cout << (x >> 1) << '\n'; // 2 (右移 1 位 = 除以 2 取整)
逐位對照(AND 為例):
1100 (12)
& 1010 (10)
-------
1000 (8) ← 只有「兩邊都是 1」的位元才是 1
⚠️ 別把位元運算子
&、|跟邏輯運算子&&、||搞混!&&/||回傳 true/false 且會短路;&/|對每個位元運算且不會短路。
複合指定運算子
int x = 10;
x += 5; // x = x + 5 → 15
x -= 3; // x = x - 3 → 12
x *= 2; // x = x * 2 → 24
x /= 4; // x = x / 4 → 6
x %= 4; // x = x % 4 → 2
x <<= 1; // 位元左移後賦值
x &= 0xF; // 位元 AND 後賦值
遞增 / 遞減運算子
int a = 5;
a++; // 後置遞增:先使用 a 的值,再加 1
++a; // 前置遞增:先加 1,再使用 a 的值
a--; // 後置遞減
--a; // 前置遞減
對於迴圈計數器這類「不在乎回傳值」的場合,
++i與i++結果相同,但++i對複雜型別(如迭代器)效率略高,是慣用寫法。
三元運算子
int age = 20;
std::string status = (age >= 18) ? "成年" : "未成年";
運算子優先順序(由高到低,簡化版)
| 優先順序 | 運算子 |
|---|---|
| 1 | () 括號 |
| 2 | ++ --(前置)、!、~、一元 - |
| 3 | * / % |
| 4 | + - |
| 5 | << >>(位移) |
| 6 | < <= > >= |
| 7 | == != |
| 8 | &(位元 AND) |
| 9 | ^(位元 XOR) |
| 10 | |(位元 OR) |
| 11 | && |
| 12 | || |
| 13 | ?:(三元) |
| 14 | = += -= … |
⚠️ 位元運算子的優先順序比比較運算子低,這是常見陷阱!
if (x & 1 == 0)其實是if (x & (1 == 0)),幾乎一定不是你要的。不確定優先順序時,善用括號()讓意圖更清晰。
9. 作用域與儲存期
作用域(Scope)— 名稱「在哪裡看得到」
int g = 1; // 全域作用域:整個檔案都看得到
int main() {
int x = 10; // 區塊作用域:只在 main 內有效
{
int y = 20; // 更內層的區塊
std::cout << x << y; // OK:內層看得到外層
}
// std::cout << y; // ❌ 錯誤:y 已離開作用域
}
內層可以宣告與外層同名的變數,會「遮蔽(shadowing)」外層的——這通常容易造成混淆,建議避免。
儲存期(Storage Duration)— 變數「活多久」
| 儲存期 | 何時建立 / 銷毀 | 範例 |
|---|---|---|
| 自動(automatic) | 進入區塊建立,離開區塊銷毀 | 一般區域變數 |
| 靜態(static) | 程式開始到結束都存在 | 全域變數、static 區域變數 |
| 動態(dynamic) | 由 new/delete 手動控制 |
堆積(heap)配置(後面章節) |
| 執行緒(thread) | 隨執行緒生滅 | thread_local(進階) |
static 區域變數只初始化一次,跨呼叫保留值:
void counter() {
static int count = 0; // 只初始化一次
int x = 0;
x ++;
count++;
std::cout << x << " ";
std::cout << count << " ";
}
// 連續呼叫三次 counter() → 輸出 1 2 3
這些只是概念導入,動態記憶體與生命週期管理會在指標與 RAII 章節深入。先建立「變數有作用域與壽命」的直覺即可。
重點整理
- 變數使用前必須宣告型別,推薦使用
{}列表初始化(可防窄化),且一定要初始化。 - C++ 提供豐富的整數與浮點數型別;標準只保證最小大小,需精確位元數時用
<cstdint>的int32_t等。 unsigned容易踩雷(回繞、混合比較、倒數迴圈),非必要少用。- 浮點數用 IEEE 754 近似儲存,
0.1 + 0.2 != 0.3;比較浮點數要用 epsilon 而非==。 const是執行期唯讀,constexpr是編譯期常數;能用 constexpr 就用。- 型別轉換遵循整數提升與一般算術轉換規則;明確轉型優先用
static_cast。 - 位元運算子(
& | ^ ~ << >>)對二進位位元操作,別跟邏輯運算子混淆,且優先順序偏低,多加括號。 - 變數有作用域(看得到的範圍)與儲存期(存活的時間)。
- 整數除法會截斷小數;運算子有優先順序,不確定時加括號。
常見錯誤與陷阱
| 錯誤 | 說明 |
|---|---|
| 未初始化的變數 | 區域變數若未初始化,其值是未定義的(垃圾值),讀取是 UB |
| 整數除法陷阱 | 7 / 2 結果是 3 而非 3.5 |
| 整數溢位 | 有號 int 超過最大值是 UB;無號則明確回繞 |
= vs == |
= 是賦值,== 是比較,搞混會造成難以發現的 bug |
浮點數用 == |
0.1 + 0.2 != 0.3,應改用 epsilon 比較 |
| unsigned 倒數迴圈 | for (unsigned i = n; i >= 0; i--) 會變成無窮迴圈 |
| signed/unsigned 混合比較 | -1 < 1u 結果可能是 false,務必開 -Wextra |
| 位元 vs 邏輯運算子 | &/` |
用 double 算錢 |
浮點誤差會累積,金額請用整數(分)處理 |
練習題
練習 1(基礎):圓的面積與周長
宣告一個 double 變數儲存半徑,並用 constexpr 定義圓周率 PI,計算並輸出圓的面積(πr²)與周長(2πr)。
- 提示:
constexpr double PI = 3.14159265358979;,注意運算用double避免整數除法。
練習 2(基礎):溫度轉換
撰寫程式將攝氏溫度轉換為華氏溫度。公式:F = C × 9 / 5 + 32。請特別小心整數除法陷阱!
- 提示:把
9.0 / 5.0寫成浮點,或讓攝氏溫度是double。
練習 3(基礎):型別探索
使用 sizeof、<limits> 的 numeric_limits 與 <cstdint>,輸出 char、int、long long、int32_t、double 的大小與(整數型別的)最小 / 最大值。
- 提示:
#include <limits>與<cstdint>;輸出int8_t的值要static_cast<int>()。
練習 4(中級):交換變數
宣告兩個整數變數,分別用「加減法」與「XOR(^)」兩種方式,在不使用第三個變數的情況下交換它們的值,並驗證結果。
- 提示:XOR 交換:
a ^= b; b ^= a; a ^= b;。想想為什麼會成立。
練習 5(中級):浮點數比較
寫一個函式 nearlyEqual(double a, double b),用 epsilon(如 1e-9)判斷兩個浮點數是否「幾乎相等」。用它驗證 0.1 + 0.2 與 0.3 是否相等,並對照直接用 == 的結果。
- 提示:
#include <cmath>,用std::fabs(a - b) < eps。
練習 6(中級):位元旗標
用一個 unsigned int 當作 4 個開關的旗標集合。實作「開啟第 n 個開關」「關閉第 n 個開關」「查詢第 n 個開關」三個操作,並用二進位印出最終狀態。
- 提示:開啟用
flags |= (1u << n);關閉用flags &= ~(1u << n);查詢用flags & (1u << n)。
練習 7(挑戰):unsigned 陷阱實驗
寫一段程式,故意製造下列三種 unsigned 陷阱並輸出結果,再用註解說明為什麼:
unsigned int x = 0; x--;的值int a = -1; unsigned b = 1;比較a < b的結果for (unsigned i = 3; i >= 0; i--)為何是無窮迴圈(請只跑前幾次就break,避免真的卡死)
- 提示:用
g++ -Wall -Wextra編譯,觀察編譯器給的警告。
對應程式碼檔案
- variables.cpp — 變數宣告與初始化示範
- data_types.cpp — 資料型別、sizeof、型別轉換、固定寬度型別
- operators.cpp — 各種運算子示範
- bitwise.cpp — 位元運算子、位元旗標、constexpr 與固定寬度型別示範