目录
[编辑 为什么字符常量必须加上单引号 ' '?](#编辑 为什么字符常量必须加上单引号 ' '?)
[字符数组(character array)](#字符数组(character array))
[为什么一定要有 '\0'?](#为什么一定要有 '\0'?)
[⚠️ 注意事项](#⚠️ 注意事项)
第一性问题:计算机如何表示文字?
计算机的本质是一个只能理解 "0"和"1" 的电子设备,它不懂汉字、字母,也不懂你说的"苹果"、"Hello"。所以我们有个根本的问题:
问题:如何让计算机识别和存储"文字"?
答案就是:把文字 → 转换成数字(编码) → 再转换成 0/1(二进制)
这就是 字符编码(Character Encoding) 的起源!
ASCII:最早的字符编码标准(美国人写的)
- 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 |
- 特点:
-
优点:简单,早期英文系统够用。
-
缺点:只能表示英文字符、数字和常用符号,不支持汉字、西班牙语、阿拉伯语等多语言。
Unicode:解决全球语言的编码方案
- 为什么需要 Unicode?
ASCII 只能表示128个字符,不适合全球。于是各国开始"自己发明编码":
-
中国:GB2312、GBK、GB18030
-
日本:Shift-JIS
-
韩国:EUC-KR
问题出现了:一个文件在中文电脑上正常,放到英文电脑就乱码。同样的二进制,在不同系统下解释为不同字符。
于是------我们能不能制定一个"全世界统一的字符编码"?
Unicode**(Unified Code,统一码)**诞生于1991年,目标:为所有文字分配统一的编号(码点)!
- 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语言的基础字符类型)
cpp
char c = 'A';
-
char本质上是一个 1字节(8位)整数。 -
它存的是字符的 ASCII码的整数值。
举个例子
cpp
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 码点)
但我们不希望写程序时天天记这些数字,所以语言提供了一个"简写"方式:
cpp
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 存。
❌ 错误做法:
cpp
char h = '中'; // 错误,汉字需要多个字节,char 只存1字节
✅ 正确做法:使用 多字节编码(如 UTF-8)+ 字符串处理
UTF-8 示例(多字节汉字):
cpp
char* s = "中"; // UTF-8编码:0xE4 0xB8 0xAD
-
然声明的是
char*,但实际上:-
s[0] = 0xE4 -
s[1] = 0xB8 -
s[2] = 0xAD -
s[3] = '\0'
-
打印每个字节:
cpp
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:
cpp
char arr[5] = {'H', 'e', 'l', 'l', 'o'};
-
初始化时指定了5个字符,数组大小为 5。
-
并没有加
\0,所以这只是一个 字符数组,不是C字符串。 -
如果你执行
printf("%s", arr);→ 可能输出乱码,因为没有终止符。
示例2:
cpp
char arr[] = {'H', 'e', 'l', 'l', 'o'};
-
数组长度由编译器自动推导为 5。
-
同样没有加
\0,仍然不是字符串。 -
用于数据处理完全OK,
for (int i = 0; i < 5; ++i)这样访问是安全的。
示例3:
cpp
char arr[5] = {72,101,108,108,111};
-
97 和 98 是 ASCII:分别对应
'a'和'b'。 -
所以
arr[0] = 'a',arr[1] = 'b'
示例4:
cpp
char arr[5] = {'H', 'e'};
-
数组长度是固定的 5,但只初始化了前两个元素。
-
后面三个元素会默认补0(
'\0')(这是C初始化规则) -
实际上这是一种 手动初始化前两项,后面自动置0 的数组。
-
这个数组其实可以当字符串用了,因为正好加上了 null terminator(
arr[2] = 0),所以printf("%s", arr);是安全的,会输出 "He"

字符串(Strings)
前面我们说过:
cpp
char arr[5] = {'H', 'e', 'l', 'l', 'o'}; // 只是字符数组,不是字符串
为什么不是字符串呢?
因为它没有以 '\0' 结尾!
C语言中的字符串 = 一个以 null 结尾(即 '\0')的字符数组
也就是说:
cpp
char str[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 这是字符串
或者用简写:
cpp
char str[] = "Hello"; // 编译器会自动补 '\0'
字符串的内存结构
cpp
char str[] = "Hi";
等价于:
cpp
char str[3] = {'H', 'i', '\0'};
声明字符串的方式
cpp
char name[10] = {'J','o','h','n','\0'};
-
显式指定数组长度为
10 -
只初始化前5项,其余自动填
0(C语言的初始化规则) -
多出空间可用于后续拼接等操作
cpp
char name[] = {'J','o','h','n','\0'};
-
编译器自动推断数组长度为5
-
和上一种不同,没有额外空间(刚好放下这5个字符)
-
没有多余空间(不能 strcat 附加)
cpp
char name[] = "John";
-
这是最常见、推荐的字符串声明方式
-
字符串字面量会自动转换为字符数组并添加
'\0' -
编译器推断数组长度为 5(4个字符 + 1个
\0)
cpp
char* n = "John";
-
"John"是一个字符串常量,存在只读常量区(Read-only memory segment) -
n是一个指针,指向这块常量内存
📦 内存示意:
cpp
[Stack] [Read-Only Data]
+--------+ +--------+--------+--------+--------+--------+
| n --> | -----> | 'J' | 'o' | 'h' | 'n' | '\0' |
+--------+ +--------+--------+--------+--------+--------+
栈变量 只读常量区,不能修改
⚠️ 特点:
-
内容不可修改(例如
n[0] = 'M';是未定义行为,可能崩溃) -
节省空间(不用复制字面量)
为什么不能修改内容?
因为字符串字面量是 只读的!它们放在 .rodata(read-only data section)
cpp
n[0] = 'M'; // 非法访问只读内存,行为未定义(Undefined Behavior)
正确做法(可改内容):
cpp
char s[] = "abc"; // 数组副本,可修改
s[0] = 'z'; // OK,现在 s = "zbc"
为什么不用复制字面量?
因为 "John" 本身已经是存储在内存中的一段文字,编译器在编译期就把它放在了常量段,它有了
地址,我们只要"拿来用"就行了。
所以不会把 "John" 的内容复制到栈,而是让 n 直接指向编译器分配的静态内存。
好处:
-
节省空间(不重复复制)
-
快(不需要运行时构造)
代价:
- 不能修改内容(是只读区域)

为什么一定要有 '\0'?
因为 C语言中字符串函数(如 printf, strlen, strcat 等)全靠 '\0' 判断字符串结束位置。
它没有 string.length() 这样的成员变量。只能一位一位读,直到遇到:
cpp
00000000 // 即 '\0',ASCII = 0
printf("%s", str) 的工作原理
❓如何打印一个字符串?
cpp
char name[] = "John";
printf("%s", name);
实际发生的事情(模拟代码逻辑):
cpp
void my_print_string(char* s) {
while (*s != '\0') {
putchar(*s); // 打印一个字符
s++; // 移动到下一个字符
}
}
-
%s会触发printf调用字符串打印逻辑 -
从传入的地址开始,一个字符一个字符地读,直到遇到
\0停止
如果没有 \0 会怎样?
没有 '\0',printf 会一直往后读,直到遇到某个随机内存中的 0 → 结果:乱码或程序崩溃
scanf("%s", str) 的工作原理
❓如何从键盘读入一个字符串?
cpp
char name[100];
scanf("%s", name);
实际发生的事情(伪代码):
cpp
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:
cpp
char str[4];
scanf("%s", str); // 如果输入 "John",会写入 5 字节,越界!
正确做法:总长度 = 最大字符数 + 1(for \0)
为什么 \0 是必须的终止符?
| 原因 | 说明 |
|---|---|
| 字符数组不存长度 | C语言的数组不记录"当前长度" |
| 函数需要知道结束位置 | printf, strlen, strcpy 都必须知道"何时停止" |
\0 = ASCII 值 0 |
在内存中表示 "结束",不会与正常字符冲突 |
| 所有字符串函数都依赖它 | 没有它你什么都做不了 |

⚠️ 注意事项
1. 不能用空格分隔字符串!
cpp
char str = 'H' 'i'; // 错误语法,不能写两个字符连在一起
2. scanf("%s", str) 遇到空格会提前终止!
cpp
char name[100];
scanf("%s", name); // 输入 "John Smith" → 只读到 "John"
📌 scanf 的工作机制
执行后:
-
scanf会从标准输入缓冲区(stdin)读取字符 -
它遇到的第一个非空白字符 → 开始填入
str -
一直读,直到遇到空白字符(空格
' '、制表符'\t'、换行'\n') -
添加
\0,停止写入字符串 -
空白字符保留在缓冲区中,用于下一个
scanf
设计目的:
-
%s处理的是"一个单词" → 所以它默认把空格视为词与词的分隔符 -
它并不是专门为"读取整行"设计的!
所以 scanf("%s") 读取到第一个空格就会停止!
这就是为什么 C语言早期提供了 gets() 函数!
gets()的目标是:一次读一整行,连空格都读进来!
cpp
char line[100];
gets(line); // 输入:John Smith → 全部读入
| 字符 | 索引 |
|---|---|
'J' |
0 |
'o' |
1 |
'h' |
2 |
'n' |
3 |
' ' |
4 |
'S' |
5 |
| ... | ... |
'\0' |
N |
-
它会一直读取,直到遇到换行符
\n -
然后把换行符"吃掉",用
\0结尾 -
空格、制表符都能保留
📛 但 gets() 被淘汰了,为什么?
因为 gets 无法限制输入长度 → 极度不安全!
cpp
char buf[10];
gets(buf); // 用户输入超过10字节,就会溢出
后果:
-
内存溢出(buffer overflow)
-
栈破坏(stack smashing)
-
造成安全漏洞(攻击者可利用)
C11 标准中,gets() 被正式移除。
✅ 现代安全替代品:fgets()
cpp
char line[100];
fgets(line, sizeof(line), stdin);
-
它会读取整行,包括空格
-
最多读取
sizeof(line) - 1个字符,自动加\0 -
如果缓冲区不够大,会保留未读内容
⚠️ 注意:fgets() 会保留换行符 \n,如果你不想要它,要手动去掉:
cpp
line[strcspn(line, "\n")] = '\0';