news 2026/10/8 15:35:39

超详细的c语言字符串操作函数教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
超详细的c语言字符串操作函数教程

前言

C 语言没有字符串类型,只有「以'\0'结尾的char数组」这一个约定。<string.h>里的那一组函数全部建立在这个约定之上,它们有一个共同特点:除了少数几个带n的函数,其余都不做任何边界检查。函数不知道你的缓冲区有多大,越界了也不会报错,只会安静地踩坏旁边的内存——这是 C 程序里缓冲区溢出(buffer overflow)最主要的来源。

最常见的一个误解是把strncpy当成「安全的strcpy」。它不是:strncpy(dst, src, n)在src长度达到或超过n时不会写结束符'\0',于是dst变成了一个没有结尾的字节数组,之后任何按字符串处理的函数都会一路读过头。还有一个高频误解是认为字符串操作函数会告诉你缓冲区够不够——只有snprintf会(通过返回值),其他的都不会。

本文按「模型 → 拷贝与连接 → 比较与查找 → 内存与格式化」的顺序,把 ISO C 标准里的字符串函数过一遍,重点是每个函数的边界行为和结束符行为。文中函数原型均取自 ISO C 标准(<string.h>/<stdio.h>),凡是非标准扩展都会明确标注来源。示例用 C99 编译:gcc -std=c99 -Wall -Wextra。

一、C 字符串的模型:以 '\0' 结尾的字符数组

长度 N 的字符串占用 N+1 个字节,最后一个是值为 0 的字符'\0'。所有<string.h>里的函数都靠这个终止符判断结束,没有任何一个函数会去读缓冲区的大小。

#include <stdio.h> #include <string.h> int main(void) { char arr[] = "hello"; /* 数组:sizeof 为 6,含结尾的 '\0' */ const char *ptr = "hello"; /* 指针:sizeof 为指针本身的大小(64 位上是 8)*/ const char *lit = "hello"; /* 字符串字面量 */ printf("%zu %zu %zu\n", sizeof(arr), /* 6 */ sizeof(ptr), /* 8(64 位平台)*/ strlen(arr)); /* 5:不含 '\0' */ /* 字符串字面量的静态存储期,同一个字面量在多处出现可能被合并成一份, 所以下面这两个指针的比较结果由实现决定,不要依赖 */ printf("%d\n", ptr == lit ? 1 : 0); return 0; }

两点必须记住:


  • sizeof(数组)是缓冲区总大小,strlen(指针)是字符串长度,两者永远不是一回事。一旦数组名传给函数、退化成指针,sizeof就再也拿不到缓冲区大小了——这就是「把缓冲区大小单独作为参数传进去」这一惯例的由来。

  • 字符串字面量的内容不可修改。在 C 里它的类型是char[N](这一点和 C++ 不同,C++ 里是const char[N]),编译器不会因为你写lit[0] = 'x'而报错,但这是未定义行为:字面量通常放在只读段,写它多半直接崩溃,而标准并不保证崩溃——它什么都不保证。


二、长度、拷贝与连接

size_t strlen(const char *s); char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); char *strcat(char *dest, const char *src); char *strncat(char *dest, const char *src, size_t n);

各函数的边界行为差异,是这一节的全部重点:

函数拷贝/追加多少是否保证写入'\0'dest需要多大
strcpysrc全部是strlen(src) + 1
strncpy至多n个不保证(strlen(src) >= n时不写)n字节,且需要手动补'\0'
strcatsrc全部是strlen(dest) + strlen(src) + 1
strncat至多n个是(总是补)strlen(dest) + n + 1

strncpy还有一个人人都会踩的细节:当src比n短时,它会把剩下的字节全部用'\0'填满。也就是说strncpy(dst, "a", 1000)会老老实实写 1000 字节。这个行为和「按需拷贝」的直觉相反,也是strncpy在性能敏感的代码里被诟病的原因。

下面是一段可以直接编译的对照示例:

/* strings_copy.c 编译:gcc -std=c99 -Wall -Wextra strings_copy.c */ #include <stdio.h> #include <string.h> int main(void) { const char *src = "hello, world"; char dst[8]; /* ❌ 危险:src 有 12 个字符,dst 只有 8 字节,strcpy 会越界写,行为未定义 */ /* strcpy(dst, src); */ /* ✅ 截断后必须自己补 '\0',否则 dst 不是一个合法的字符串 */ strncpy(dst, src, sizeof(dst) - 1); dst[sizeof(dst) - 1] = '\0'; printf("[%s]\n", dst); /* [hello, ] */ /* ✅ 更省心的做法:snprintf 自动截断,并保证以 '\0' 结尾 */ char dst2[8]; int n = snprintf(dst2, sizeof(dst2), "%s", src); printf("[%s] 完整长度=%d\n", dst2, n); /* [hello, ] 完整长度=12 */ /* ✅ 拼接:先确认空间再拼 */ char path[32] = "/usr"; if (strlen(path) + strlen("/local") + 1 <= sizeof(path)) { strcat(path, "/local"); } printf("%s\n", path); /* /usr/local */ return 0; }

注意strncpy的第二个参数写法sizeof(dst) - 1:留出最后一格手工放'\0'。这是strncpy的标准用法模板,记不住它就不要用strncpy,直接用snprintf。

三、比较、查找与切分

int strcmp(const char *s1, const char *s2); int strncmp(const char *s1, const char *s2, size_t n); char *strchr(const char *s, int c); char *strrchr(const char *s, int c); char *strstr(const char *haystack, const char *needle); size_t strspn(const char *s, const char *accept); size_t strcspn(const char *s, const char *reject); char *strpbrk(const char *s, const char *accept); char *strtok(char *str, const char *delim);

strcmp的返回值只保证符号:小于 0、等于 0、大于 0。不要写成if (strcmp(a, b) == -1),也不要直接当布尔用——if (strcmp(a, b))表达的是「不相等」,这是最常见的反向逻辑 bug。

查找函数的返回值是指针,不是下标。strchr的第二个参数类型是int,传入的char会被提升;另外strchr(s, '\0')是合法的,返回指向结尾终止符的指针。

strtok是这一组里最需要小心的。

/* strtok_demo.c 编译:gcc -std=c99 -Wall -Wextra strtok_demo.c */ #include <stdio.h> #include <string.h> int main(void) { /* 必须是可写的数组:strtok 会改写原串,把分隔符换成 '\0' */ char text[] = "a,,b"; char *save = NULL; /* strtok_r 是 POSIX 函数,不属于 ISO C。 在严格 -std=c99 下可能需要 #define _POSIX_C_SOURCE 200809L */ for (char *tok = strtok_r(text, ",", &save); tok != NULL; tok = strtok_r(NULL, ",", &save)) { printf("[%s]\n", tok); } return 0; }

输出是:

[a] [b]

注意中间那个空字段被跳过了——strtok会把连续的分隔符合并处理,因此它无法解析出空字段,这是它的硬限制,不是配置问题。此外:


  • strtok内部用静态变量保存位置,不是线程安全的,也不能嵌套使用。标准 C 库里没有可重入版本;POSIX 提供strtok_r(char *str, const char *delim, char **saveptr),MSVC 提供的strtok_s与之同形(三个参数)。需要留意的是 C11 附录 K 里定义的同名函数strtok_s是四个参数的版本,附录 K 又是可选的、很多实现根本不提供——跨平台时以目标平台的文档为准,不要凭名字猜签名。

  • strtok会修改原串,所以不能对字符串字面量使用,那是对只读存储的写入,属于未定义行为。


四、内存操作与格式化

void *memcpy(void *dest, const void *src, size_t n); void *memmove(void *dest, const void *src, size_t n); void *memset(void *s, int c, size_t n); int memcmp(const void *s1, const void *s2, size_t n); int snprintf(char *str, size_t size, const char *format, ...); int sprintf(char *str, const char *format, ...);

最需要强调的是memcpy与memmove的分工:源和目标内存区域重叠时用memcpy是未定义行为,标准不保证任何结果(某些实现从低地址往高地址复制,另一些可能用向量指令按块复制,行为完全不可预测)。有重叠就用memmove。

/* mem_demo.c 编译:gcc -std=c99 -Wall -Wextra mem_demo.c */ #include <stdio.h> #include <string.h> int main(void) { char buf[16] = "abcdef"; /* 把索引 1 起的 3 个字节前移到索引 0,源和目标重叠 */ memmove(buf, buf + 1, 3); /* ✅ 重叠必须用 memmove */ printf("%s\n", buf); /* bcddef */ char dst[16]; memcpy(dst, "0123456789", 11); /* 不重叠时用 memcpy,语义更直接 */ printf("%s\n", dst); /* 0123456789 */ char small[8]; int n = snprintf(small, sizeof(small), "%s-%d", "abcdefgh", 42); if (n < 0) { printf("编码错误\n"); } else if ((size_t)n >= sizeof(small)) { /* 返回值是"假如空间足够会写多少个字符(不含 '\0')" */ printf("被截断,完整长度=%d,实际存入=[%s]\n", n, small); /* 输出:被截断,完整长度=11,实际存入=[abcdefg] */ } return 0; }

snprintf的返回值规则值得单独记住:它返回「假如缓冲区足够大,本应写入的字符数(不含结尾'\0')」,而不是实际写入的字符数;返回负值表示发生了编码错误。因此「是否被截断」的判断方法就是n >= size,这也是它相比sprintf唯一的安全优势——sprintf完全不检查边界,缓冲区不够就是越界写,属未定义行为。

memset(s, c, n)的第二个参数虽然是int,但只有低 8 位会被使用——想用memset把int数组置为 1 是无效的,它会把每个字节都设成 1,得到0x01010101这样的值,而不是 1。

数字与字符串互转不属于<string.h>:atoi/atol/atof在<stdlib.h>,它们无法报告错误(输入非法时返回 0,和输入真的是 "0" 无法区分)。需要区分这两种情况就用strtol/strtod,它们提供endptr指向第一个未解析的字符,并把溢出情况写入errno(ERANGE)。

常见坑点

场景❌ 错误写法✅ 正确写法
修改字符串字面量char *p = "abc"; p[0] = 'x';(UB,标准不保证任何行为)char p[] = "abc"; p[0] = 'x';
用sizeof求字符串长度size_t len = sizeof(p);(p是指针,得到 8)size_t len = strlen(p);
无边界拷贝strcpy(dst, src);,dst可能不够大用snprintf(dst, sizeof(dst), "%s", src);
strncpy后忘记补结束符strncpy(dst, src, n); printf("%s", dst);strncpy(dst, src, n); dst[n] = '\0';
strncat空间算错按n + 1分配,忽略了dest已有的内容预留strlen(dest) + n + 1
strcmp当布尔用if (strcmp(a, b)) { /* 以为是相等 */ }if (strcmp(a, b) == 0) { /* 相等 */ }
对字面量用strtokstrtok("a,b", ",")(UB:写只读存储)先复制到数组:char t[] = "a,b"; strtok(t, ",");
重叠内存用memcpymemcpy(p, p + 1, n);(UB)memmove(p, p + 1, n);

有两个坑需要额外提一句。一是漏包含<string.h>:在 64 位平台上这尤其危险,因为 C89 会把未声明的函数隐式假定为返回int,而strlen返回的size_t是 64 位无符号数,被当成int截断后可能得到错误的长度(现代编译器已把隐式函数声明从警告升级为错误,但老代码里仍会见到)。二是多线程里用strtok:它把位置保存在内部静态变量里,两个线程同时切分不同字符串会互相踩踏,必须换成 POSIX 的strtok_r。

另外两个跨平台注意事项:strdup(复制字符串并malloc)是POSIX 函数,不属于 ISO C 标准,在严格模式下需要_POSIX_C_SOURCE;strlcpy/strlcat来自 BSD,glibc 长期不提供(较新的版本才补上),跨平台代码不要依赖它们。

总结

函数是否检查目标缓冲区大小结束符'\0'行为典型用途
strlen——不计入长度求长度(每次调用都遍历,别放进循环条件)
strcpy❌ 不检查总是写确认目标够大时的整体拷贝
strncpy部分(限源长度)源过长时不写定长字段拷贝,须手工补'\0'
strcat❌ 不检查总是写确认空间足够时的拼接
strncat部分(限追加长度)总是写限制追加长度,注意dest已有内容也要算空间
strtok——把分隔符改写成'\0'简单切分;不可重入、会改原串
memcpy❌ 不检查不涉及不重叠的字节块拷贝
memmove❌ 不检查不涉及允许重叠的字节块拷贝
snprintf✅ 通过size参数总是写(size > 0时)格式化输出与安全拼接

用这三条规则可以避开绝大多数字符串 bug:第一,任何写入目标缓冲区的调用,先问「目标多大、源多长」;第二,凡是带n的函数,先确认它对结束符的处理(strncpy不补、strncat补);第三,重叠内存一律memmove。至于日常拼接,最省心的组合是snprintf加检查返回值,它在安全性和可读性上都比手工算strncpy/strncat的长度更可靠。

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

North 2企业智能体平台:可控、可审计、可嵌入IT治理的生产级AI架构

1. 项目概述&#xff1a;North 2 不是又一个“AI玩具”&#xff0c;而是企业级智能体落地的工程分水岭Cohere 发布 North 2 企业智能体平台这件事&#xff0c;我第一时间在 Slack 上看到内部消息时&#xff0c;手里的咖啡杯差点没拿稳。不是因为名字多酷&#xff0c;也不是因为…

作者头像 李华
网站建设 2026/10/8 15:35:22

细调Ornith-9B:用Unsloth从加载到LoRA微调的5步上手指南

细调Ornith-9B&#xff1a;用Unsloth从加载到LoRA微调的5步上手指南 【免费下载链接】Ornith-1 项目地址: https://gitcode.com/gh_mirrors/or/Ornith-1 本文是一份面向新手的大模型微调教程&#xff1a;Ornith-9B 是开源 Agent 编程模型家族 Ornith&#xff08;MIT 协…

作者头像 李华
网站建设 2026/10/8 15:35:08

IDEA中Git回退分支的三种Reset模式详解与安全实践

简介&#xff1a;本资源是一份面向Java开发者与Git初学者的实操指南&#xff0c;聚焦IntelliJ IDEA环境下安全、可控地回退Git分支至指定历史版本这一高频痛点问题。内容系统对比Revert&#xff08;推荐&#xff09;与Reset Head两种核心策略&#xff0c;涵盖操作原理、适用场景…

作者头像 李华
网站建设 2026/10/8 15:35:01

Codex CLI daemon启动失败:OS Error 5拒绝访问详解

1. 问题本质与典型场景还原“codex cli 启动报错&#xff1a;failed to open daemon process: 拒绝访问。(os error 5)”——这行错误不是冷门异常&#xff0c;而是 Windows 环境下 codex CLI 用户在首次启动、升级后重启或权限变更后高频遭遇的“拦路虎”。它表面是操作系统级…

作者头像 李华
网站建设 2026/10/8 15:35:00

AI芯片软硬件协同设计:脉动阵列、FP8与编译器映射全链路解析

1. 从一次流片返工说起&#xff1a;AI芯片软硬件协同到底难在哪去年冬天&#xff0c;我参与的一颗边缘推理芯片在流片回来后跑ResNet-50&#xff0c;实测吞吐只有仿真预估的六成&#xff0c;功耗却高出将近四成。团队连着排查了两周&#xff0c;最后发现问题不在RTL代码&#x…

作者头像 李华
网站建设 2026/10/8 15:32:13

揭秘“妻子机器人”走红背后:从硅胶外壳到AI交互的技术真相

1. “妻子机器人”彻底走红背后&#xff1a;一场被营销放大的技术现实我必须先泼一盆冷水&#xff1a;你很可能在短视频和自媒体标题里看到的“日本妻子机器人”&#xff0c;和你今天能买到的、实验室里真实存在的机器人&#xff0c;是两码事。过去几个月&#xff0c;这个关键词…

作者头像 李华