news 2026/7/12 2:37:43

第八章:字符串与字符串函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第八章:字符串与字符串函数

欢迎来到第八章!

在上一章中,我们学习了指针。从这一章开始,我们将把指针和数组结合起来,深入学习 C 语言中最常用的数据之一 ——字符串

字符串在编程中无处不在:用户输入、文件内容、网络数据、界面文字…… 几乎所有和 "文字" 相关的东西,本质上都是字符串。

但 C 语言比较特别 —— 它没有专门的 "字符串类型",而是用字符数组 + 结束符来表示字符串。这种设计既灵活又容易出错,是新手踩坑的重灾区。

本章我们将系统学习:

  • 字符串的本质是什么?'\0'到底是干嘛的?
  • 字符串怎么输入、怎么输出?
  • 常用的字符串函数有哪些?怎么用?
  • 字符数组和字符指针有什么区别?
  • 字符串操作有哪些常见的坑?

学完本章,你就能熟练处理各种字符串操作了~

准备好了吗?让我们开始!


一、字符串的本质

1.1 什么是字符串?

C 语言中,字符串就是以 '\0' 结尾的字符数组

两个关键点:

  1. 它是一个字符数组—— 每个元素是一个 char
  2. 末尾必须有一个结束标志 '\0'

'\0'是一个特殊字符,叫空字符,它的 ASCII 码值是 0。它不算字符串的内容,只是用来标记 "字符串到这里结束了"。

比如字符串"Hello",看起来是 5 个字符,实际在内存中占 6 个字节:

'H' 'e' 'l' 'l' 'o' '\0' ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ 72 │ 101 │ 108 │ 108 │ 111 │ 0 │ └─────┴─────┴─────┴─────┴─────┴─────┘

⚠️重要提醒'\0'是必须的!没有它就不是合法的字符串。很多字符串相关的 bug,都是因为少了这个结束符导致的。

1.2 字符串的两种写法

写法一:逐个字符初始化(不推荐)
char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

要手动加'\0',麻烦,容易忘。

写法二:用双引号(推荐)
char str[] = "Hello";

用双引号括起来的叫字符串字面量。编译器会自动在末尾加上'\0',不用你手动写。

💡注意:用双引号初始化时,数组大小要记得给结束符留位置。比如 "Hello" 有 5 个字符,数组至少要 6 个字节。写成char str[5] = "Hello"是不对的,会越界。

1.3 字符串字面量

用双引号括起来的"Hello"就是一个字符串字面量

它有几个特点:

  • 存储在只读内存区,不能修改
  • 类型是const char[]
  • 直接写"Hello"代表的是这个字符串首字符的地址
char *p = "Hello"; // p指向字符串字面量的首地址

⚠️注意:字符指针指向字符串字面量时,不要尝试修改内容,否则会崩溃!

char *p = "Hello"; p[0] = 'h'; // ❌ 错误!字符串字面量是只读的,修改会崩溃

但如果是字符数组就可以改:

char arr[] = "Hello"; arr[0] = 'h'; // ✅ 没问题,数组在栈上,可以修改

这是字符数组和字符指针的一个重要区别,后面还会详细讲。


二、字符串的输入输出

2.1 输出字符串:printf + % s

printf%s格式符可以输出字符串。

#include <stdio.h> int main() { char str[] = "Hello World"; printf("%s\n", str); // 输出:Hello World return 0; }

%s的工作原理:从你给的地址开始,一个字符一个字符地输出,直到遇到'\0'为止。

💡 所以如果你的字符串没有'\0'%s就会一直往后打,打出一堆乱码,直到碰巧遇到一个 0 为止 —— 这就是越界了。

2.2 输入字符串:scanf + % s

scanf%s可以读入字符串。

#include <stdio.h> int main() { char name[20]; printf("请输入你的名字:"); scanf("%s", name); // 注意!数组名前面不用加& printf("你好,%s!\n", name); return 0; }

⚠️scanf 的 % s 有两个大坑:

坑 1:遇到空格就停止

输入:Zhang San 输出:你好,Zhang!

scanf("%s")遇到空格、回车、Tab 就会停止,所以它读不了带空格的句子。

坑 2:不检查数组长度,容易越界如果你的数组只有 20 个字节,用户输入了 100 个字符,scanf照样往里写,直接越界 —— 这就是著名的缓冲区溢出问题,非常危险。

2.3 gets 和 puts(了解即可)

puts:输出一行字符串
char str[] = "Hello"; puts(str); // 输出字符串,自动加换行

printf("%s\n")差不多,更简单,但只能输出字符串。

gets:读入一行字符串(不推荐!)
char str[100]; gets(str); // 读一整行,包括空格

gets可以读一整行(遇到回车才停止),但它完全不检查缓冲区长度,比scanf还危险,已经被 C 标准废弃了。

💡安全建议:实际开发中更推荐用fgets来读字符串,可以指定最大读多少,更安全。初学阶段先用scanf没问题,知道有这个坑就行。


三、字符数组 vs 字符指针

很多新手搞不清这两个的区别:

char arr[] = "Hello"; // 字符数组 char *p = "Hello"; // 字符指针

它们看起来都能当字符串用,但区别很大:

对比项char arr[] = "Hello"char *p = "Hello"
存储位置栈上(局部变量时)只读数据区
内容可修改吗?✅ 可以修改❌ 不能修改
arr/p 是什么?常量指针(地址不能改)指针变量(可以改指向)
大小整个数组的大小(6 字节)指针本身的大小(4 或 8 字节)

3.1 修改内容

char arr[] = "Hello"; arr[0] = 'h'; // ✅ 没问题,数组内容可以改 char *p = "Hello"; p[0] = 'h'; // ❌ 崩溃!字符串字面量是只读的

3.2 修改指向

char arr[] = "Hello"; arr = "World"; // ❌ 错误!数组名是常量,不能改 char *p = "Hello"; p = "World"; // ✅ 没问题,p可以指向别的地方

3.3 简单总结

  • 想修改字符串内容→ 用字符数组
  • 只是读字符串→ 两种都可以
  • 需要改变指向不同的字符串→ 用字符指针

💡新手建议:初学阶段,如果你不确定用哪个,就用字符数组,更安全,不容易出问题。


四、常用字符串函数

C 语言标准库提供了很多处理字符串的函数,都在<string.h>头文件里。下面我们一个个来讲。

4.1 strlen:求字符串长度

size_t strlen(const char *s);

作用:计算字符串的长度(不包括末尾的'\0')。

#include <stdio.h> #include <string.h> int main() { char str[] = "Hello"; printf("长度:%zu\n", strlen(str)); // 输出 5 return 0; }

注意:

  • 返回值是size_t类型(无符号整数),用%zu格式化输出
  • 长度不包括'\0',"Hello" 长度是 5 不是 6

4.2 strcpy:字符串复制

char *strcpy(char *dest, const char *src);

作用:把src字符串复制到dest中,包括'\0'

#include <stdio.h> #include <string.h> int main() { char src[] = "Hello"; char dest[20]; strcpy(dest, src); // 把src复制到dest printf("%s\n", dest); // 输出:Hello return 0; }

⚠️注意

  • dest必须有足够的空间,否则会越界
  • 会覆盖dest原来的内容

4.3 strcat:字符串拼接

char *strcat(char *dest, const char *src);

作用:把src拼接到dest的末尾。

#include <stdio.h> #include <string.h> int main() { char dest[20] = "Hello "; char src[] = "World"; strcat(dest, src); // 把src拼到dest后面 printf("%s\n", dest); // 输出:Hello World return 0; }

原理:先找到dest末尾的'\0',把src从那里开始覆盖过去,最后加'\0'

⚠️ 同样要注意:dest空间要足够大。

4.4 strcmp:字符串比较

int strcmp(const char *s1, const char *s2);

作用:比较两个字符串的大小(按 ASCII 码逐位比较)。

返回值:

  • s1 == s2 → 返回0
  • s1 > s2 → 返回正数
  • s1 < s2 → 返回负数
#include <stdio.h> #include <string.h> int main() { printf("%d\n", strcmp("abc", "abc")); // 0,相等 printf("%d\n", strcmp("abc", "abd")); // 负数,abc < abd printf("%d\n", strcmp("abd", "abc")); // 正数,abd > abc return 0; }

⚠️新手常错:比较字符串不能用==

if (s1 == s2) { ... } // ❌ 这比的是地址,不是内容! if (strcmp(s1, s2) == 0) { ... } // ✅ 这样才是比内容

==比的是两个指针的值(地址),而不是字符串内容。比较内容一定要用strcmp

4.5 带 n 的安全版本

上面的strcpystrcatstrcmp都有一个 "安全版本"—— 可以指定最多操作多少个字符,防止越界。

普通版安全版区别
strcpystrncpy最多复制 n 个字符
strcatstrncat最多拼接 n 个字符
strcmpstrncmp最多比较前 n 个字符

比如strncpy

char dest[10]; strncpy(dest, "Hello World", 9); // 最多复制9个字符,防止越界 dest[9] = '\0'; // 注意:strncpy不一定自动加'\0',最好手动补

虽然用起来麻烦一点,但更安全。实际开发中推荐优先用带 n 的版本。

4.6 strchr:查找字符

char *strchr(const char *s, int c);

作用:在字符串s中查找字符c第一次出现的位置。

  • 找到了 → 返回该位置的指针
  • 没找到 → 返回 NULL
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello World"; char *p = strchr(str, 'o'); if (p != NULL) { printf("找到了,位置:%td\n", p - str); // 4,第5个字符 printf("从这里开始:%s\n", p); // o World } return 0; }

4.7 strstr:查找子串

char *strstr(const char *haystack, const char *needle);

作用:在字符串haystack中查找子串needle第一次出现的位置。

  • 找到了 → 返回子串起始位置的指针
  • 没找到 → 返回 NULL
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello World"; char *p = strstr(str, "World"); if (p != NULL) { printf("找到了:%s\n", p); // World } return 0; }

五、自己实现字符串函数

光会用还不够,最好能理解它们是怎么实现的。我们来手写几个,加深理解。

5.1 实现 strlen

思路:从开头往后数,直到遇到'\0'

// 自己实现strlen size_t my_strlen(const char *s) { size_t len = 0; while (*s != '\0') // 没到结束符就继续 { len++; s++; } return len; }

更简洁的版本:

size_t my_strlen(const char *s) { const char *p = s; while (*p) p++; // 一直走到'\0' return p - s; // 两个指针相减,就是长度 }

5.2 实现 strcpy

思路:一个字符一个字符地复制,直到把'\0'也复制过去。

// 自己实现strcpy char *my_strcpy(char *dest, const char *src) { char *p = dest; while ((*dest++ = *src++)) ; // 空循环体,赋值表达式的值就是复制的字符,遇到'\0'就结束 return p; }

这个写法很经典 ——*dest++ = *src++一步完成了取值、赋值、两个指针后移,循环条件就是赋值的结果,遇到'\0'(值为 0)循环就结束。

5.3 实现 strcmp

思路:逐位比较,遇到不同的或者结束符就停下来。

// 自己实现strcmp int my_strcmp(const char *s1, const char *s2) { while (*s1 && *s2 && *s1 == *s2) { s1++; s2++; } return *s1 - *s2; }

💡建议:自己动手实现一遍这些函数,你对字符串和指针的理解会上一个台阶。这也是面试常考题哦~


六、字符串常见的坑

字符串是新手踩坑的重灾区,我们来盘点一下最常见的几个。

坑 1:忘记 '\0'

char str[5] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0' printf("%s\n", str); // ❌ 会输出乱码,直到遇到0才停

解决:用双引号初始化,或者手动加'\0'

坑 2:数组空间不够

char str[5] = "Hello"; // ❌ "Hello"要6个字节(含'\0'),5个不够

解决:数组大小要留够,或者直接写char str[] = "Hello"让编译器自己算。

坑 3:修改字符串字面量

char *p = "Hello"; p[0] = 'h'; // ❌ 崩溃!字面量是只读的

解决:要修改就用字符数组,不要用指针指向字面量然后改。

坑 4:用 == 比较字符串

char s1[] = "abc"; char s2[] = "abc"; if (s1 == s2) { ... } // ❌ 比的是地址,不是内容!

解决:用strcmp比较内容。

坑 5:越界写入

char str[5]; strcpy(str, "Hello World"); // ❌ 严重越界!

解决:确保目标数组足够大,或者用strncpy限制长度。


七、综合示例

示例 1:统计字符出现次数

#include <stdio.h> #include <string.h> int main() { char str[100]; char ch; int count = 0; printf("请输入一个字符串:"); scanf("%s", str); getchar(); // 吃掉回车 printf("请输入要统计的字符:"); scanf("%c", &ch); for (int i = 0; str[i] != '\0'; i++) { if (str[i] == ch) { count++; } } printf("字符'%c'出现了%d次\n", ch, count); return 0; }

示例 2:字符串反转

#include <stdio.h> #include <string.h> int main() { char str[100]; printf("请输入字符串:"); scanf("%s", str); int len = strlen(str); int left = 0; int right = len - 1; while (left < right) { // 交换左右两个字符 char temp = str[left]; str[left] = str[right]; str[right] = temp; left++; right--; } printf("反转后:%s\n", str); return 0; }

八、本章小结

本章我们深入学习了 C 语言中的字符串:

1. 字符串的本质

  • 字符串 = 字符数组 + 末尾的'\0'
  • '\0'是结束标志,不算内容,但占一个字节
  • 用双引号初始化时,编译器自动加'\0'

2. 字符数组 vs 字符指针

  • 字符数组:内容可修改,地址不可改
  • 字符指针指向字面量:内容不可修改,指针可以改指向
  • 需要修改内容就用数组,只读可以用指针

3. 常用字符串函数(都在 <string.h>)

函数作用
strlen求字符串长度(不含 '\0')
strcpy/strncpy复制字符串
strcat/strncat拼接字符串
strcmp/strncmp比较字符串
strchr查找字符
strstr查找子串

4. 常见坑点

  • 忘记加'\0'
  • 数组空间不够
  • 修改字符串字面量
  • ==比较字符串内容
  • 越界写入

📌学习建议:字符串是编程的基本功,一定要多练。建议你把常用的字符串函数都自己实现一遍,既能加深对指针的理解,又能记住函数的用法 —— 一举两得。


九、写在最后

"Talk is cheap. Show me the code."

——Linus Torvalds(林纳斯・托瓦兹,Linux 之父)

翻译成中文就是:"废话少说,放码过来。"

这句话是 Linux 之父林纳斯的名言,也成为了程序员圈的经典梗。道理很简单:说得再多,不如直接看代码。

学字符串也是一样。看十遍讲解,不如自己动手写一遍。写代码、调 bug、踩坑、爬坑 —— 这个过程才是真正的学习。


⚠️ 关于本文

由于笔者水平有限,文中难免存在疏漏、错误或表述不够准确的地方。如果你在阅读过程中发现了任何问题 —— 无论是概念错误、代码 bug、还是表述不清 —— 都欢迎在评论区留言指正,我会及时修正。

技术之路,道阻且长,行则将至。我们互相学习,共同进步 💪

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 2:36:15

TC78H651AFNG与PIC18F97J94的直流有刷电机驱动方案

1. 项目背景与核心器件选型在工业自动化和精密控制领域&#xff0c;直流有刷电机驱动器一直是运动控制系统的核心部件。TC78H651AFNG与PIC18F97J94的组合方案&#xff0c;代表了当前中高功率直流有刷驱动的最新设计趋势。这套方案特别适合需要精确速度控制、高扭矩输出和智能保…

作者头像 李华
网站建设 2026/7/12 2:36:12

Photoshop 动作录制:1分钟自动化生成1寸/2寸证件照排版(附动作文件)

Photoshop动作录制&#xff1a;1分钟自动化生成证件照排版终极指南每次处理证件照排版时&#xff0c;你是否厌倦了重复执行裁剪、加白边、定义图案等机械操作&#xff1f;作为从业十年的影像处理专家&#xff0c;我发现90%的证件照制作时间都消耗在重复性操作上。本文将彻底改变…

作者头像 李华
网站建设 2026/7/12 2:35:43

Vulnhub 靶机 RickdiculouslyEasy:3种非常规端口利用与2个隐蔽后门分析

Vulnhub靶机RickdiculouslyEasy&#xff1a;非常规端口攻防与隐蔽后门深度剖析当大多数渗透测试者将注意力集中在常见的22、80、443等标准端口时&#xff0c;RickdiculouslyEasy靶机却为我们打开了一扇新窗口——那些被忽视的非标准端口往往隐藏着最致命的攻击路径。本文将带您…

作者头像 李华
网站建设 2026/7/12 2:33:42

PyTorch深度学习3小时入门:从动态计算图到图像识别实战

这次我们来深入探讨PyTorch深度学习框架的完整学习路径。作为一个由Facebook开发的开源机器学习库&#xff0c;PyTorch凭借其动态计算图和直观的接口设计&#xff0c;已经成为学术界和工业界最受欢迎的深度学习框架之一。对于刚接触深度学习的新手来说&#xff0c;最关心的往往…

作者头像 李华
网站建设 2026/7/12 2:32:24

Linux Bridge 与 veth-pair 实战:3步构建K8s Pod网络命名空间隔离与通信

Linux Bridge 与 veth-pair 实战&#xff1a;3步构建K8s Pod网络命名空间隔离与通信在容器化技术盛行的今天&#xff0c;Kubernetes已经成为企业级容器编排的事实标准。而理解Pod网络背后的实现机制&#xff0c;对于运维工程师和开发者来说至关重要。本文将带你深入Linux网络虚…

作者头像 李华