当前位置: 首页 > news >正文

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

欢迎来到第八章!

在上一章中,我们学习了指针。从这一章开始,我们将把指针和数组结合起来,深入学习 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、还是表述不清 —— 都欢迎在评论区留言指正,我会及时修正。

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

http://www.cnnetsun.cn/news/3301800.html

相关文章:

  • TC78H651AFNG与PIC18F97J94的直流有刷电机驱动方案
  • Photoshop 动作录制:1分钟自动化生成1寸/2寸证件照排版(附动作文件)
  • Vulnhub 靶机 RickdiculouslyEasy:3种非常规端口利用与2个隐蔽后门分析
  • PyTorch深度学习3小时入门:从动态计算图到图像识别实战
  • Tomasulo 算法模拟器实验:3种数据相关(RAW/WAW/WAR)的消除过程与状态追踪
  • Linux Bridge 与 veth-pair 实战:3步构建K8s Pod网络命名空间隔离与通信
  • 计算机视觉完整学习路径:从CNN到Transformer的实战指南
  • 基于TC78H651AFNG和STM32的直流电机驱动方案
  • J-Link V8.50 驱动与 Keil MDK 5.36 版本冲突:2种排查与修复方案
  • 软件测试人员在AI相关领域最常被问到的20个高频面试题
  • WarcraftHelper:魔兽争霸3终极优化指南,突破60帧限制的完整教程
  • 如何快速部署KASandbox平台:5步搭建容器编排基础设施
  • 谷歌SEO值得长期投入吗?2026年依然值得,但方法已经变了
  • 主流 OpenJDK 发行版下载安装
  • YOLO与GPT结合:高效完成目标检测学术论文的实践指南
  • CTF Misc 实战:5类隐写术(PNG/音频/流量/压缩包/SUID)从原理到解题脚本
  • MAX77654与TM4C123GH6PZ的嵌入式电源管理方案
  • 软考软件设计师 2026 备考:操作系统 5 大核心考点(PV操作、死锁、缺页中断)实战解析
  • 脉冲雷达与FMCW雷达对比:5大核心指标实测与选型指南
  • AI绘画提示词工程实战:从情感化角色生成到叙事构建
  • NLP多任务分类模型评估:雷达图与堆叠柱状图组合可视化实战
  • Nginx 1.24 配置 TLS 1.3 实战:3步升级,Chrome 兼容性提升 100%
  • 缠论分析神器ChanlunX:3步实现股票走势可视化智能识别
  • 浏览器资源嗅探的终极解决方案:猫抓(cat-catch)让网页媒体下载变得简单高效
  • 2026年 水晶头自动穿线铆压机哪家强?TOP 5品牌大揭秘
  • 高端制造|半导体与集成电路|设计EDA赛道 技术管理线HR招聘工具包 总监/VP/CTO 顶层高管竞业风险核查清单(七维标准完整版)
  • 国家数据集管理平台:终结AI训练数据饥荒的基础设施
  • Zotero 7.0 插件生态实战:3款必备插件提升文献管理效率 200%
  • 基于TC78H653FTG和PIC18F46K42的直流有刷电机控制方案
  • ESXi 7.0 嵌套虚拟化与显卡直通冲突:3种配置方案实测与稳定性对比