一、回顾与本篇目标
上一篇你学会了指针——C 语言最核心的概念。你知道变量有内存地址,指针存地址,* 解引用可以访问指针指向的数据,数组名就是指向首元素的指针。
这一篇我们要学的是字符串。在 Python 和 JavaScript 中,字符串是一种内置的高级数据类型——你可以直接拼接、切片、替换,几乎感觉不到它的底层实现。但在 C 语言中,没有专门的字符串类型。字符串就是一个以 \0 结尾的字符数组。操作字符串就是操作数组和指针。
这种“简陋”的设计恰恰是 C 语言哲学的体现——给你最基础的工具,让你自己构建一切。理解了 C 语言的字符串,你会对字符串的底层存储有全新的认识。
本篇的目标:
- 理解 C 语言中字符串的本质——以
\0结尾的 char 数组 - 学会字符串的声明和初始化
- 掌握
printf和scanf处理字符串 - 学会常用字符串处理函数:
strlen、strcpy、strcmp、strcat - 理解字符串和指针的关系
二、C 语言中字符串的本质
在 Python 或 JavaScript 中,字符串是一个高级对象:
// JavaScript
let str = "Hello";
console.log(str.length); // 5
console.log(str.toUpperCase()); // "HELLO"
# Python
s = "Hello"
print(len(s)) # 5
print(s.upper()) # "HELLO"
在 C 语言中,没有专门的字符串类型。字符串就是一个 char 类型的数组,最后一个字符后面跟着一个特殊字符 \0(空字符,ASCII 码为 0),用来标记字符串的结束。
// C 语言中 "Hello" 在内存中的存储:
// 索引: 0 1 2 3 4 5
// 字符: 'H' 'e' 'l' 'l' 'o' '\0'
// 地址:1000 1001 1002 1003 1004 1005
\0 是字符串的“终止符”。C 语言中所有处理字符串的函数,都是通过这个 \0 来判断字符串在哪结束的。如果没有 \0,函数会一直向后读内存,直到碰巧遇到一个 0 为止——这会导致读取越界。
三、字符串的声明和初始化
C 语言中有两种方式创建字符串:
方式一:字符数组
// 逐个字符初始化(需要手动加 \0)
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 用字符串字面量初始化(编译器自动加 \0)
char str2[6] = "Hello"; // 数组大小至少为 6(5 个字符 + 1 个 \0)
// 不指定大小,编译器自动推导
char str3[] = "Hello"; // 自动分配 6 个字节
// 部分初始化
char str4[10] = "Hello"; // 前 6 个字节是 Hello\0,后面 4 个字节自动填 0
方式二:字符指针
// 指针指向一个字符串常量(存储在只读内存区)
char *str5 = "Hello";
// 注意:这种方式声明的字符串通常不能修改!
// str5[0] = 'h'; // 可能导致程序崩溃(取决于编译器和操作系统)
字符数组和字符指针的区别:
char str[] = "Hello" |
char *str = "Hello" |
|
|---|---|---|
| 存储位置 | 栈上(可读可写) | 指向只读数据段(通常不可写) |
| 是否可以修改内容 | ✅ 可以 | ❌ 通常不行(未定义行为) |
| sizeof 的结果 | 整个数组的大小(包括 \0) | 指针本身的大小(8 或 4 字节) |
| 是否可以指向其他字符串 | ❌ 数组名是常量指针 | ✅ 可以 |
如果你想创建一个可以修改的字符串,用字符数组。如果只是一个不需要修改的常量,用字符指针更省内存。
四、字符串的输入输出
用 printf 输出字符串
char name[] = "张三";
printf("你好,%s!\n", name); // 你好,张三!
%s 是字符串的格式占位符。printf 从传入的地址开始,逐个打印字符,直到遇到 \0 停止。
用 scanf 读取字符串
char name[50]; // 预留足够的空间
printf("请输入你的名字:");
scanf("%s", name); // 注意:name 不需要加 &,因为它本身就是地址!
printf("你好,%s!\n", name);
注意:scanf("%s", name) 中 name 前面没有 &。因为 name 是数组名,本身就是指向首元素的指针,不需要再取地址。这和我们之前学的 scanf("%d", &age) 不同——age 是普通变量,需要 & 才能得到地址。
scanf 读取字符串的三个问题:
- 遇到空格就停止:如果用户输入
张三 丰,scanf("%s")只会读到张三。空格、Tab、换行都被当作分隔符。 - 不检查数组大小:如果用户输入了超过 49 个字符(留 1 个给
\0),就会数组越界。这是 scanf 的安全隐患。 - 换行符留在缓冲区:用户输入后按的回车键会留在输入缓冲区中,可能影响后续的输入。
更安全的字符串输入:fgets
fgets 是更安全的读取字符串的函数,它限制了最大读取字符数:
#include <stdio.h>
int main() {
char name[50];
printf("请输入你的全名:");
fgets(name, sizeof(name), stdin); // 最多读 49 个字符
// fgets 会保留换行符,如果不需要可以去掉
// name[strcspn(name, "\n")] = '\0';
printf("你好,%s!\n", name);
return 0;
}
fgets 参数解释:
name:存储读取内容的数组。sizeof(name):最多读取的字符数(包括\0,实际读取sizeof - 1个字符)。stdin:标准输入(键盘)。
fgets 和 scanf 的区别:
fgets会读取空格,可以读入完整的带空格的字符串。fgets会保留用户输入的换行符\n(如果你不想要,需要手动去掉)。fgets限制了读取长度,不会越界。
五、字符串处理函数
C 语言标准库提供了一组字符串处理函数,声明在 <string.h> 头文件中。
strlen:获取字符串长度
#include <string.h>
#include <stdio.h>
int main() {
char str[] = "Hello World";
int len = strlen(str);
printf("字符串长度:%d\n", len); // 11(不包括 \0)
printf("sizeof:%d\n", (int)sizeof(str)); // 12(包括 \0)
return 0;
}
strlen 和 sizeof 的区别:
strlen(str):返回字符串的实际长度,不包含\0。sizeof(str):返回数组占用的总字节数,包含\0。
strcpy:复制字符串
#include <string.h>
#include <stdio.h>
int main() {
char source[] = "Hello";
char destination[20]; // 确保目标数组足够大
strcpy(destination, source); // 把 source 复制到 destination
printf("destination: %s\n", destination); // "Hello"
return 0;
}
注意:strcpy 不会检查目标数组的大小。如果 source 比 destination 长,会导致数组越界。更安全的版本是 strncpy:
strncpy(destination, source, sizeof(destination) - 1);
destination[sizeof(destination) - 1] = '\0'; // 确保以 \0 结尾
strcmp:比较两个字符串
#include <string.h>
#include <stdio.h>
int main() {
char str1[] = "apple";
char str2[] = "banana";
char str3[] = "apple";
int result1 = strcmp(str1, str2);
int result2 = strcmp(str2, str1);
int result3 = strcmp(str1, str3);
printf("apple vs banana: %d\n", result1); // 负数(str1 < str2)
printf("banana vs apple: %d\n", result2); // 正数(str2 > str1)
printf("apple vs apple: %d\n", result3); // 0(相等)
return 0;
}
strcmp 的返回值规则:
- 0:两个字符串相等。
- 正数:第一个字符串大于第二个(按字典序)。
- 负数:第一个字符串小于第二个(按字典序)。
不能用 == 比较字符串!
// 错误示范
if (str1 == str2) { // 这样比较的是地址,不是内容!
printf("相同\n");
}
// 正确写法
if (strcmp(str1, str2) == 0) {
printf("相同\n");
}
在 C 语言中,str1 == str2 比较的是两个数组的地址,而不是字符串的内容。这和 JavaScript/Python 不同——JS 和 Python 的 == 比较字符串内容,C 语言比较的是指针。
strcat:拼接字符串
#include <string.h>
#include <stdio.h>
int main() {
char str1[50] = "Hello"; // 目标数组必须足够大
char str2[] = " World";
strcat(str1, str2); // 把 str2 追加到 str1 末尾
printf("%s\n", str1); // "Hello World"
return 0;
}
注意:str1 必须有足够的空间容纳拼接后的完整字符串(包括 \0)。如果空间不足,就会越界。
更安全的版本是 strncat:
strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);
六、字符串和指针的关系
字符串本质上就是 char*。这意味着你可以用指针来遍历和处理字符串。
用指针遍历字符串
#include <stdio.h>
int main() {
char str[] = "Hello";
// 用指针遍历字符串的每个字符
for (char *p = str; *p != '\0'; p++) {
printf("%c ", *p); // H e l l o
}
printf("\n");
return 0;
}
循环条件是 *p != '\0'——只要指针指向的字符不是终止符,就继续。
字符串数组:存储多个字符串
#include <stdio.h>
int main() {
// 用二维数组存储多个字符串
char fruits[][10] = {"apple", "banana", "orange"};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]); // fruits[i] 是一维数组的数组名,即指针
}
// 用指针数组存储多个字符串(更灵活,每个字符串长度可以不同)
char *colors[] = {"red", "green", "blue", "yellow"};
for (int i = 0; i < 4; i++) {
printf("%s\n", colors[i]);
}
return 0;
}
二维字符数组 vs 字符指针数组:
char fruits[][10]:每个字符串固定占用 10 字节,即使只有 5 个字符也占 10 字节。浪费空间但可以修改内容。char *colors[]:每个元素是一个指针,指向不同长度的字符串常量。节省空间但内容通常不可修改。
七、综合演示:一个简单的用户管理系统
下面这段代码综合运用了字符串的声明、输入输出、处理函数和指针遍历:
#include <stdio.h>
#include <string.h>
int main() {
// 存储用户名和密码的数组
char username[50];
char password[50];
// 预设的用户名和密码
char correct_username[] = "admin";
char correct_password[] = "123456";
printf("===== 登录系统 =====\n");
printf("用户名:");
fgets(username, sizeof(username), stdin);
// 去掉 fgets 读取的换行符
username[strcspn(username, "\n")] = '\0';
printf("密码:");
fgets(password, sizeof(password), stdin);
password[strcspn(password, "\n")] = '\0';
// 验证用户名和密码
if (strcmp(username, correct_username) == 0 &&
strcmp(password, correct_password) == 0) {
printf("\n登录成功!欢迎,%s!\n", username);
// 用指针遍历用户名,统计字符数(演示指针用法)
int char_count = 0;
for (char *p = username; *p != '\0'; p++) {
char_count++;
}
printf("你的用户名有 %d 个字符。\n", char_count);
} else {
printf("\n登录失败:用户名或密码错误。\n");
}
return 0;
}
代码解析:
strcspn(str, "\n"):返回str中第一个匹配"\n"中任意字符的位置。如果用户输入了换行符,它会返回换行符的索引。我们把这个位置设为\0,就去掉了换行符。这是处理 fgets 换行符的经典写法。strcmp比较字符串内容,不能直接用==。- 最后用指针遍历用户名统计字符数——虽然
strlen也能做到,但这里是为了演示指针遍历。
八、字符串处理函数的源码实现
理解这些函数的内部实现,能帮你更深刻地理解指针和字符串。以下是 strlen 和 strcpy 的手动实现:
// 手动实现 strlen
int my_strlen(const char *str) {
int count = 0;
while (*str != '\0') {
count++;
str++; // 指针后移
}
return count;
}
// 手动实现 strcpy
char* my_strcpy(char *dest, const char *src) {
char *original = dest; // 保存目标起始地址
while (*src != '\0') {
*dest = *src; // 复制一个字符
dest++;
src++;
}
*dest = '\0'; // 别忘了加终止符
return original;
}
核心逻辑:遍历源字符串,把每个字符复制到目标数组,最后加上 \0。这两个函数的核心都是用指针遍历,以 \0 为终止条件。
九、本篇动手练习
练习 1:实现 strcmp
新建 practice6-1.c,手动实现 strcmp 函数。用指针同时遍历两个字符串,逐个字符比较,返回第一个不同字符的差值(或 0 表示相等)。
练习 2:字符串反转
新建 practice6-2.c,让用户输入一个字符串,用指针对这个字符串原地反转。例如输入 "hello",反转后变成 "olleh"。提示:用两个指针,一个指向头部,一个指向尾部,交换它们指向的字符后向中间移动。
练习 3:统计字符类型
新建 practice6-3.c,让用户输入一个字符串,统计其中大写字母、小写字母、数字、空格各有多少个。用指针遍历实现。
练习 4:简单加密
新建 practice6-4.c,让用户输入一个字符串,对每个字符的 ASCII 码加 1(如 'A' 变 'B','z' 变 '{'),输出加密后的字符串。再写一个解密函数把加密后的字符串还原。
十、本篇小结
这一篇你学会了 C 语言中字符串的核心知识:
- 字符串的本质:以
\0(空字符)结尾的字符数组。没有专门的字符串类型,字符串就是char[]或char*。 - 声明和初始化:
char str[] = "Hello"(可修改,在栈上)vschar *str = "Hello"(通常不可修改,指向只读区)。 - 输入输出:
printf("%s", str)输出,scanf("%s", str)输入(遇到空格停止,不安全),fgets(str, size, stdin)更安全。 - 常用字符串函数:
strlen(长度)、strcpy/strncpy(复制)、strcmp(比较,不能用==)、strcat/strncat(拼接)。永远注意目标数组的大小,防止越界。 - 字符串和指针:字符串就是
char*,可以用指针遍历字符串。char *p = str; while (*p != '\0') { ... p++; }是经典遍历模式。 - 字符串数组:二维数组
char arr[][N]固定长度但可修改,指针数组char *arr[]灵活但通常不可修改。
C 语言的字符串没有 Python/JavaScript 那样丰富的内置方法,但正因为它“简陋”,你能清楚地看到字符串在内存中是怎么存的、\0 是怎么起作用的。下一篇,我们学习 C 语言中的函数——如何定义函数、传值和传指针的区别、递归函数。
下一篇预告
下一篇——《函数——封装可复用的代码块》:函数的定义和声明、函数原型、传值调用 vs 传指针调用、递归函数、函数指针初探。同时对比 JavaScript 和 Python 中的函数,理解 C 语言函数的独特之处。
C/C++ 零基础入门,每周更新。












暂无评论内容