目錄
第一性問題:計算機如何表示文字?
ASCII:最早的字符編碼標準(美國人寫的)
Unicode:解決全球語言的編碼方案
字符(Character)
?編輯?為什么字符常量必須加上單引號 ' '?
如果我想表示一個“漢字”怎么辦?
字符數組(character array)
?字符串(Strings)
?聲明字符串的方式
?為什么一定要有 '\0'?
?? 注意事項
第一性問題:計算機如何表示文字?
計算機的本質是一個只能理解 “0”和“1” 的電子設備,它不懂漢字、字母,也不懂你說的“蘋果”、“Hello”。所以我們有個根本的問題:
問題:如何讓計算機識別和存儲“文字”?
答案就是:把文字 → 轉換成數字(編碼) → 再轉換成 0/1(二進制)
這就是 字符編碼(Character Encoding) 的起源!
ASCII:最早的字符編碼標準(美國人寫的)
1. ASCII 是什么?
ASCII(American Standard Code for Information Interchange)是1963年美國制定的最早的字符編碼標準。
-
每個字符被映射成一個 7位二進制數(后來擴展成8位)。
-
一共能表示 128 個字符(2? = 128)
-
后來為兼容計算機的存儲單元,擴展為 8 位(即 1 字節)因此,在 ASCII 及其兼容的單字節編碼中,一個字符通常占用 1 字節內存
ASCII的結構邏輯:
類別 | 范圍 | 例子 |
---|---|---|
控制字符 | 0–31 | 回車(13)、換行(10) |
可打印字符 | 32–126 | 空格(32)、A(65)、z(122) |
字符 | ASCII十進制 | ASCII二進制 |
---|---|---|
A | 65 | 01000001 |
B | 66 | 01000010 |
a | 97 | 01100001 |
0 | 48 | 00110000 |
空格 | 32 | 00100000 |
?2. 特點:
-
優點:簡單,早期英文系統夠用。
-
缺點:只能表示英文字符、數字和常用符號,不支持漢字、西班牙語、阿拉伯語等多語言。
Unicode:解決全球語言的編碼方案
1. 為什么需要 Unicode?
ASCII 只能表示128個字符,不適合全球。于是各國開始“自己發明編碼”:
-
中國:GB2312、GBK、GB18030
-
日本:Shift-JIS
-
韓國:EUC-KR
?問題出現了:一個文件在中文電腦上正常,放到英文電腦就亂碼。同樣的二進制,在不同系統下解釋為不同字符。
于是——我們能不能制定一個“全世界統一的字符編碼”?
?Unicode(Unified Code,統一碼)誕生于1991年,目標:為所有文字分配統一的編號(碼點)!
2. Unicode 的核心思想
-
給世界上所有字符(漢字、泰文、emoji等)分配唯一的“碼點”(code point),例如:
-
A:U+0041
-
中:U+4E2D
-
😃:U+1F603
-
?注意:Unicode 只是“編號表”,它沒有規定用多少個字節去存儲這些字符。于是出現了不同的“實
現方式”,即只定義 “字符 ? 碼點”映射,但沒有規定如何把碼點存儲在內存里!
Unicode的存儲實現:UTF-8、UTF-16、UTF-32
UTF全稱:Unicode Transformation Format(Unicode 轉換格式)
我們來解決另一個第一性問題:
“U+4E2D”這個碼點怎么放進內存?需要幾個字節?怎么轉成二進制?
這就誕生了多種“Unicode編碼實現方式”
編碼方式 | 特點 | 字節數 | 優點 | 缺點 |
---|---|---|---|---|
UTF-8 | 可變長編碼 | 1~4字節 | 英文節省空間,兼容 ASCII | 漢字需要3字節 |
UTF-16 | 可變長編碼 | 2或4字節 | 常用于Windows | 英文占2字節,不節省 |
UTF-32 | 定長編碼 | 4字節 | 編碼簡單 | 空間浪費 |
UTF-8 是目前最常用的編碼方式(尤其在網頁、Python中)
舉例:同一個字如何編碼
字符 | Unicode碼點 | UTF-8編碼(二進制) |
---|---|---|
A | U+0041 | 01000001(1字節) |
中 | U+4E2D | 11100100 10111000 10101101(3字節) |
😃 | U+1F603 | 11110000 10011111 10011000 10000011(4字節) |
現在把之前講的字符編碼知識,落地到 C/C++ 語言中,理解字符變量的本質和如何使用。
字符(Character)
核心第一性問題:
在 C/C++ 中,我們如何存儲一個字符?它在內存里是什么樣子?和 ASCII / Unicode 有什么關系?
char
類型(C語言的基礎字符類型)
char c = 'A';
-
char
本質上是一個 1字節(8位)整數。 -
它存的是字符的 ASCII碼的整數值。
舉個例子
char ch = 'A'; // 實際上等價于 char ch = 65;
printf("%c\n", ch); // 輸出字符A
printf("%d\n", ch); // 輸出65(對應ASCII)
也就是說:
-
'A'
是 字符常量,它的 ASCII 值是 65。 -
C語言內部:字符就是整數,只不過默認以字符形式解釋。
?為什么字符常量必須加上單引號 ' '
?
在 C/C++ 中:
字符是一個整數,本質上就是它的 ASCII 值(或 Unicode 碼點)
但我們不希望寫程序時天天記這些數字,所以語言提供了一個“簡寫”方式:
char ch = 'A'; // 實際等價于:char ch = 65;
所以:
-
'A'
是字符常量(character literal) -
單引號告訴編譯器:你要存的是字符的整數值,不是變量或字符串
單引號的作用 = 區分不同類型的常量
在 C/C++ 中,有很多類型的常量,你要用不同方式告訴編譯器它是什么類型:
代碼形式 | 含義 | 類型 |
---|---|---|
'A' | 單個字符 → 65 | char (字符常量) |
"A" | 字符串常量(含 \0 ) | char[2] (字符串數組) |
A | 錯誤(變量名或未定義) | - |
65 | 數值常量 → 65 | int (整型常量) |
所以可以理解為:'A'
是一種語法糖,是給你寫代碼時的“語義提示”,它會轉成整數。?
總結:為什么要用 ' '
來寫字符?
理由 | 解釋 |
---|---|
1?? 區分字符和變量 | 'A' 是字符常量,A 是變量名 |
2?? 區分字符和字符串 | 'A' 是一個字符,"A" 是一個字符串 |
3?? 讓編譯器知道你是想用字符的 ASCII 值 | 'B' → 66 |
4?? 和字符串數組不同(char[] vs char ) | 單引號用于單個字符 |
如果我想表示一個“漢字”怎么辦?
漢字在 Unicode 中的碼點遠大于 127(ASCII之外),所以不能用 char
存。
? 錯誤做法:
char h = '中'; // 錯誤,漢字需要多個字節,char 只存1字節
? 正確做法:使用 多字節編碼(如 UTF-8)+ 字符串處理
UTF-8 示例(多字節漢字):
char* s = "中"; // UTF-8編碼:0xE4 0xB8 0xAD
-
然聲明的是
char*
,但實際上:-
s[0] = 0xE4
-
s[1] = 0xB8
-
s[2] = 0xAD
-
s[3] = '\0'
-
打印每個字節:
for (int i = 0; s[i] != '\0'; ++i) {printf("%02x ", (unsigned char)s[i]);
}
// 輸出:e4 b8 ad
字符數組(character array)
一個 char
類型的數組,用來存儲一串字符(文本)。
開辟一段長度為 5個字節 的連續內存空間,每個元素是 char
類型(1字節),但不一定表示字符串!
重點來了:?? 字符數組 ≠ 字符串,除非你手動加 \0
示例1:
char arr[5] = {'H', 'e', 'l', 'l', 'o'};
-
初始化時指定了5個字符,數組大小為 5。
-
并沒有加
\0
,所以這只是一個 字符數組,不是C字符串。 -
如果你執行
printf("%s", arr);
→ ?可能輸出亂碼,因為沒有終止符。
?示例2:
char arr[] = {'H', 'e', 'l', 'l', 'o'};
-
數組長度由編譯器自動推導為 5。
-
同樣沒有加
\0
,仍然不是字符串。 -
用于數據處理完全OK,
for (int i = 0; i < 5; ++i)
這樣訪問是安全的。
示例3:
char arr[5] = {72,101,108,108,111};
-
97 和 98 是 ASCII:分別對應
'a'
和'b'
。 -
所以
arr[0] = 'a'
,arr[1] = 'b'
?示例4:
char arr[5] = {'H', 'e'};
-
數組長度是固定的 5,但只初始化了前兩個元素。
-
后面三個元素會默認補0(
'\0'
)(這是C初始化規則) -
實際上這是一種 手動初始化前兩項,后面自動置0 的數組。
-
這個數組其實可以當字符串用了,因為正好加上了 null terminator(
arr[2] = 0
),所以printf("%s", arr);
是安全的,會輸出 "He"
?字符串(Strings)
前面我們說過:
char arr[5] = {'H', 'e', 'l', 'l', 'o'}; // 只是字符數組,不是字符串
為什么不是字符串呢?
因為它沒有以 '\0'
結尾!
C語言中的字符串 = 一個以 null 結尾(即 '\0'
)的字符數組
也就是說:
char str[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 這是字符串
或者用簡寫:
char str[] = "Hello"; // 編譯器會自動補 '\0'
字符串的內存結構
char str[] = "Hi";
等價于:
char str[3] = {'H', 'i', '\0'};
?聲明字符串的方式
char name[10] = {'J','o','h','n','\0'};
-
顯式指定數組長度為
10
-
只初始化前5項,其余自動填
0
(C語言的初始化規則) -
多出空間可用于后續拼接等操作
char name[] = {'J','o','h','n','\0'};
-
編譯器自動推斷數組長度為5
-
和上一種不同,沒有額外空間(剛好放下這5個字符)
-
沒有多余空間(不能 strcat 附加)
char name[] = "John";
-
這是最常見、推薦的字符串聲明方式
-
字符串字面量會自動轉換為字符數組并添加
'\0'
-
編譯器推斷數組長度為 5(4個字符 + 1個
\0
)
char* n = "John";
-
"John"
是一個字符串常量,存在只讀常量區(Read-only memory segment) -
n
是一個指針,指向這塊常量內存
📦 內存示意:?
[Stack] [Read-Only Data]
+--------+ +--------+--------+--------+--------+--------+
| n --> | -----> | 'J' | 'o' | 'h' | 'n' | '\0' |
+--------+ +--------+--------+--------+--------+--------+棧變量 只讀常量區,不能修改
?? 特點:
-
內容不可修改(例如
n[0] = 'M';
是未定義行為,可能崩潰) -
節省空間(不用復制字面量)
為什么不能修改內容??
因為字符串字面量是 只讀的!它們放在 .rodata
(read-only data section)?
n[0] = 'M'; // 非法訪問只讀內存,行為未定義(Undefined Behavior)
正確做法(可改內容):
char s[] = "abc"; // 數組副本,可修改
s[0] = 'z'; // OK,現在 s = "zbc"
?為什么不用復制字面量?
因為 "John"
本身已經是存儲在內存中的一段文字,編譯器在編譯期就把它放在了常量段,它有了
地址,我們只要“拿來用”就行了。
所以不會把 "John"
的內容復制到棧,而是讓 n
直接指向編譯器分配的靜態內存。
好處:
-
節省空間(不重復復制)
-
快(不需要運行時構造)
代價:
-
不能修改內容(是只讀區域)
?為什么一定要有 '\0'
?
因為 C語言中字符串函數(如 printf, strlen, strcat 等)全靠 '\0'
判斷字符串結束位置。
它沒有 string.length()
這樣的成員變量。只能一位一位讀,直到遇到:
00000000 // 即 '\0',ASCII = 0
printf("%s", str)
的工作原理
?如何打印一個字符串?
char name[] = "John";
printf("%s", name);
實際發生的事情(模擬代碼邏輯):
void my_print_string(char* s) {while (*s != '\0') {putchar(*s); // 打印一個字符s++; // 移動到下一個字符}
}
-
%s
會觸發printf
調用字符串打印邏輯 -
從傳入的地址開始,一個字符一個字符地讀,直到遇到
\0
停止
如果沒有 \0
會怎樣??
沒有 '\0'
,printf
會一直往后讀,直到遇到某個隨機內存中的 0 → 結果:亂碼或程序崩潰?
scanf("%s", str)
的工作原理
?如何從鍵盤讀入一個字符串?
char name[100];
scanf("%s", name);
?實際發生的事情(偽代碼):
void my_scan_string(char* s) {char ch;while (ch = getchar()) {if (ch == ' ' || ch == '\n') break;*s++ = ch;}*s = '\0'; // 手動添加結尾符!
}
-
自動以空格、回車為結束
-
自動加
'\0'
到末尾(你才可以繼續用printf("%s", name)
) -
所以你傳入的數組必須足夠大(要容得下
\0
)
如果你忘了預留空間給 \0
:
char str[4];
scanf("%s", str); // 如果輸入 "John",會寫入 5 字節,越界!
?正確做法:總長度 = 最大字符數 + 1(for \0
)
為什么 \0
是必須的終止符?
原因 | 說明 |
---|---|
字符數組不存長度 | C語言的數組不記錄“當前長度” |
函數需要知道結束位置 | printf , strlen , strcpy 都必須知道“何時停止” |
\0 = ASCII 值 0 | 在內存中表示 “結束”,不會與正常字符沖突 |
所有字符串函數都依賴它 | 沒有它你什么都做不了 |
?
?? 注意事項
1. 不能用空格分隔字符串!
char str = 'H' 'i'; // 錯誤語法,不能寫兩個字符連在一起
2. scanf("%s", str)
遇到空格會提前終止!
char name[100];
scanf("%s", name); // 輸入 "John Smith" → 只讀到 "John"
📌 scanf 的工作機制
執行后:
-
scanf
會從標準輸入緩沖區(stdin)讀取字符 -
它遇到的第一個非空白字符 → 開始填入
str
-
一直讀,直到遇到空白字符(空格
' '
、制表符'\t'
、換行'\n'
) -
添加
\0
,停止寫入字符串 -
空白字符保留在緩沖區中,用于下一個
scanf
設計目的:
-
%s
處理的是“一個單詞” → 所以它默認把空格視為詞與詞的分隔符 -
它并不是專門為“讀取整行”設計的!
所以 scanf("%s")
讀取到第一個空格就會停止!
這就是為什么 C語言早期提供了 gets()
函數!
gets()
的目標是:一次讀一整行,連空格都讀進來!?
char line[100];
gets(line); // 輸入:John Smith → 全部讀入
字符 | 索引 |
---|---|
'J' | 0 |
'o' | 1 |
'h' | 2 |
'n' | 3 |
' ' | 4 |
'S' | 5 |
... | ... |
'\0' | N |
-
它會一直讀取,直到遇到換行符
\n
-
然后把換行符“吃掉”,用
\0
結尾 -
空格、制表符都能保留
📛 但 gets()
被淘汰了,為什么?
因為 gets 無法限制輸入長度 → 極度不安全!
char buf[10];
gets(buf); // 用戶輸入超過10字節,就會溢出
后果:
-
內存溢出(buffer overflow)
-
棧破壞(stack smashing)
-
造成安全漏洞(攻擊者可利用)
C11 標準中,gets()
被正式移除。?
? 現代安全替代品:fgets()
char line[100];
fgets(line, sizeof(line), stdin);
-
它會讀取整行,包括空格
-
最多讀取
sizeof(line) - 1
個字符,自動加\0
-
如果緩沖區不夠大,會保留未讀內容
?? 注意:fgets()
會保留換行符 \n
,如果你不想要它,要手動去掉:
line[strcspn(line, "\n")] = '\0';