學習目標

讀完本章後,你應該能夠:

  • 學會宣告與初始化變數,理解四種初始化形式的差異
  • 認識 C++ 的基本資料型別(整數、浮點數、字元、布林)
  • 理解固定寬度整數型別(<cstdint>)與何時該用它們
  • 搞懂 signed / unsigned 的差別與常見陷阱
  • 了解浮點數的 IEEE 754 表示法,以及為什麼 0.1 + 0.2 != 0.3
  • 掌握型別轉換規則(整數提升、一般算術轉換)與四種 cast
  • 認識位元運算子(&|^~<<>>
  • 區分 constconstexpr
  • 理解作用域(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
大小寫敏感 Scorescore 是不同的變數
建議使用有意義的名稱 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;

建議:除非有特殊記憶體或效能考量,浮點運算預設用 doublefloat 精度較低,容易累積誤差。

字元與布林

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.10.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),簡化記法:

  1. int 小的型別(charshort)會先「整數提升(integer promotion)」成 int
  2. 接著往「範圍較大 / 浮點」的一方靠攏:intunsignedlonglong longfloatdoublelong 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 / 2static_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;       // 前置遞減

對於迴圈計數器這類「不在乎回傳值」的場合,++ii++ 結果相同,但 ++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 章節深入。先建立「變數有作用域與壽命」的直覺即可。


重點整理

  1. 變數使用前必須宣告型別,推薦使用 {} 列表初始化(可防窄化),且一定要初始化
  2. C++ 提供豐富的整數與浮點數型別;標準只保證最小大小,需精確位元數時用 <cstdint>int32_t 等。
  3. unsigned 容易踩雷(回繞、混合比較、倒數迴圈),非必要少用。
  4. 浮點數用 IEEE 754 近似儲存,0.1 + 0.2 != 0.3;比較浮點數要用 epsilon 而非 ==
  5. const 是執行期唯讀,constexpr 是編譯期常數;能用 constexpr 就用。
  6. 型別轉換遵循整數提升與一般算術轉換規則;明確轉型優先用 static_cast
  7. 位元運算子(& | ^ ~ << >>)對二進位位元操作,別跟邏輯運算子混淆,且優先順序偏低,多加括號。
  8. 變數有作用域(看得到的範圍)與儲存期(存活的時間)。
  9. 整數除法會截斷小數;運算子有優先順序,不確定時加括號。

常見錯誤與陷阱

錯誤 說明
未初始化的變數 區域變數若未初始化,其值是未定義的(垃圾值),讀取是 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>,輸出 charintlong longint32_tdouble 的大小與(整數型別的)最小 / 最大值。

  • 提示:#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.20.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 陷阱並輸出結果,再用註解說明為什麼:

  1. unsigned int x = 0; x--; 的值
  2. int a = -1; unsigned b = 1; 比較 a < b 的結果
  3. for (unsigned i = 3; i >= 0; i--) 為何是無窮迴圈(請只跑前幾次就 break,避免真的卡死)
  • 提示:用 g++ -Wall -Wextra 編譯,觀察編譯器給的警告。

對應程式碼檔案