ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C/C++字符与字符串函数详解:从strlen到std::string的实战指南

C/C++字符与字符串函数详解:从strlen到std::string的实战指南 字符函数和字符串函数是每个C/C新手都绕不过去的一关。你会发现课上讲了半天写代码的时候还是经常懵strlen和sizeof到底啥区别strcpy为什么总说危险strcmp到底怎么比较字符串还有转义字符那堆\0、\n看的时候明白用的时候全乱套。这篇文章我直接把这些函数掰开揉碎讲清楚从最基础的字符判定到字符串拷贝、拼接、比较、查找再到C里的std::string和“函数返回字符串”的经典坑一次性给你捋顺。内容适合刚学完语法、开始刷练习的初学者也适合准备面试想快速回顾基础的人基本都是实战里天天碰到的点。1. 先搞清楚字符函数和字符串函数到底差在哪1.1 字符和字符串的本质区别很多新手上来就混着用“字符函数”和“字符串函数”这俩词其实它们在C语言里是完全不同的两码事。**字符char**就是一个单字节的整数在内存里占1个字节本质上存储的是ASCII码值。比如字符A它在内存里实际存的是整数65。字符串则是一个以\0空字符ASCII码0结尾的字符数组。数组本身不存长度信息字符串的长度是靠遍历到那个\0来确定的。所以字符函数处理的是“单个字符”比如判断它是不是数字、是不是字母、是大写还是小写而字符串函数处理的是“一串字符组成的数组”比如拷贝、连接、比较长度。这个区别直接决定了使用场景你拿到的是char a;还是char s[];要处理的是一个字符还是一整个句子选用的函数库完全不同。字符函数主要在ctype.hC里是cctype头文件里字符串函数主要在string.hC里是cstring头文件里。1.2 为什么C语言的字符串设计得这么“原始”先说个背景理解了你会写得更从容。C语言诞生的时候内存和磁盘资源都极其宝贵设计哲学就是“让程序员完全掌控内存”所以语言本身不提供字符串类型。字符串就是普通数组数组就是裸内存块语言层面不检查越界。你能用char *指针随便指来指去能按字节精确操作。这套设计的优点是很灵活、性能开销极低几乎所有高级语言底层的字符串实现都有C语言的影子缺点是太容易出错了——越界写、忘加终止符、指针悬空每个坑都是新手常见的程序崩溃来源。现代C提供了std::string这个封装好的类能动态管理内存、自动处理终止符。但如果你学的是C语言或者在做C但需要和C接口交互比如Linux系统调用、嵌入式开发、一些第三方库API必须还得会C风格字符串这套东西。这篇内容主要按C语言体系来讲最后单独讲C里更省心的用法。1.3 学习字符串函数的正确姿势我给新手的建议是不要死记每个函数的签名而是要建立一个“场景→函数”的映射思维。你要处理“字符串长度”时想到strlen要“拷贝”时想到strcpy/strncpy要“比较”时想到strcmp要“找字符/找子串”时想到strchr/strstr。平时练习的时候先想清楚输入是什么、输出是什么、要求的安全级别是什么再去查函数慢慢就熟了。另外强烈建议新手在写字符串代码时先画一下内存布局。一个字符串数组在内存里长什么样、指针指向哪里、终止符在哪图画出来问题自然就清楚了。2. 字符函数核心用法判断和转换单个字符2.1 分类判断函数让代码更专业ctype.h里提供了一组字符分类函数输入一个字符返回布尔值。新手最常用的是isalpha、isdigit、isalnum。直接看例子#include stdio.h #include ctype.h int main() { char c 5; char d a; char e ; if (isdigit(c)) { printf(%c 是数字\n, c); } if (isalpha(d)) { printf(%c 是字母\n, d); } if (isspace(e)) { printf(空格被检测到了\n); } return 0; }这个函数族还有isupper大写字母、islower小写字母、isxdigit十六进制数字字符、ispunct标点符号、isprint可打印字符等。实际开发中很典型的应用就是写一个登录界面校验用户名只能包含字母和数字或者写配置文件解析器跳过所有空格和换行。用这些函数比手写ASCII码范围判断要清晰得多而且不用自己去记A是65、a是97这些数字。2.2 大小写转换注意返回值这个坑toupper和tolower是最常用的字符转换函数。用法很简单#include stdio.h #include ctype.h int main() { char c a; char upper toupper(c); // 得到 A char lower tolower(Z); // 得到 z printf(%c %c\n, upper, lower); return 0; }这里有个很多新手踩过的坑toupper和tolower的返回值是int不是char。如果你把一个超过unsigned char范围的负数传进去行为是未定义的。因为char在有些平台上尤其ARM平台默认是有符号类型一个中文编码的字节或\xff传进去直接就能得到异常结果。安全写法是先把参数转成unsigned char再传char c \xff; // 假设char有符号此时c是-1 char result toupper((unsigned char)c); // 先转成unsigned char这个细节看起来很细但Windows上跑没问题、Linux程序崩掉的情况不少就出在这种隐式类型转换上。2.3 自己实现判断大多数时候没必要新手喜欢“锻炼自己”手写isalpha比如if (c a c z || c A c Z)。自己写本身没问题但要注意第一ASCII码范围判断不代表能覆盖所有语言环境下的字符第二标准库函数内部的实现是查表或者位运算优化过的比你一串if判断快得多。实际项目里我建议直接用标准库自己写反而容易埋坑。3. 字符串函数核心用法C语言最重要的函数库3.1 strlen算长度但别被size_t骗了strlen走遍历遇到\0停下返回字符串长度不含终止符。看这个#include stdio.h #include string.h int main() { char s1[] hello; char s2[] {h, e, l, l, o}; // 注意没有 \0 printf(%zu\n, strlen(s1)); // 正确输出 5 // printf(%zu\n, strlen(s2)); // 危险会一直向后扫描直到碰巧遇到0 return 0; }第二个数组如果不加\0它不是真正意义上的C字符串strlen会越过数组边界继续扫描内存结果是不确定的可能直接Segmentation fault。新手最常犯的错就是忘记为字符数组留终止符位置。另外strlen返回值类型是size_t这是一个无符号整数类型。这意味着你写if (strlen(s) -1)这种判断时永远成立无符号数和-1比较时-1会被转成极大值。所以跟长度比较时别用负数概念去比老老实实用 0、 0这种形式。3.2 strcpy和strncpy拷贝字符串的两个极端strcpy(dest, src)很简单把src的内容拷贝到dest包括末尾的\0。但这是极其危险的一个函数因为目标缓冲区必须足够大否则就缓冲区溢出——这是C语言历史上无数安全漏洞的根源。#include stdio.h #include string.h int main() { char dest[5]; char src[] hello world; // 12个字符 // strcpy(dest, src); // 灾难会写穿dest破坏栈上相邻数据 strncpy(dest, src, sizeof(dest) - 1); // 是比较安全的替代 dest[sizeof(dest) - 1] \0; printf(%s\n, dest); return 0; }注意上面这个安全写法strncpy拷贝n个字符但它不保证一定会以\0结尾。如果源字符串长度超过n它拷贝前n个字符不补终止符所以你必须自己手动在最后一位补上\0。这是strncpy最坑的地方你以为安全了其实还差一步。很多老代码里的bug就是这么造成的。我的实际建议是优先用平台提供的安全版本函数比如strcpy_sWindows下或者自己写一个带长度检查的封装。虽然在纯C的标准库层面strncpy是主流替代品但永远记得自己补终止符。3.3 strcat和strncat字符串连接没那么简单strcat(dest, src)把src追加到dest末尾同样的问题——它不知道dest缓冲区有多少剩余空间很可能直接写越界。如果用strncat请注意它的第三个参数含义和strncpy不一样它最多追加n个字符然后会自动在末尾补一个\0所以不会出现strncpy那种忘补终止符的问题。#include stdio.h #include string.h int main() { char dest[32] Hello, ; char src[] world!; size_t remaining sizeof(dest) - strlen(dest) - 1; // -1 给 \0 留位置 strncat(dest, src, remaining); printf(%s\n, dest); // Hello, world! return 0; }这里有个细节计算剩余空间时strlen(dest)得到已用字符数缓冲区总大小减去已用再减1就是还能安全追加的字符上限。这个“减1”是最容易漏的漏了就多写1个字节的坑。3.4 strcmp和strncmp比较两个字符串的规则strcmp(str1, str2)按字典序逐个比较字符的ASCII码返回值是一个整数小于0表示str1在前str1 str2等于0表示完全相同大于0表示str1在后str1 str2。要注意两点第一返回值不保证是-1和1有的实现返回的是第一个不同字符的ASCII差值比如a和d可能返回-3。所以判等时用 0千万别写成 -1第二strcmp能正确比较字符串内容直接用比较两个char *指针比较的是指针本身即地址不是内容永远是“不同”。新手最常见的错误就是写if (str1 str2)。对于只比较前n个字符的需求用strncmp。比如判断一个字符串是否以http开头就写strncmp(url, http, 4) 0非常实用。3.5 strchr和strstr查找字符和子串strchr(str, ch)在str中查找字符ch第一次出现的位置返回指向该位置的指针找不到返回NULL。strstr(str, substr)则是在str中查找子串首次出现的完整位置也是返回指针。#include stdio.h #include string.h int main() { char email[] userexample.com; char *pos strchr(email, ); if (pos ! NULL) { printf(邮箱分隔符位于第 %ld 个字符: %s\n, pos - email, pos); } char *sub strstr(email, example); if (sub ! NULL) { printf(找到子串: %s\n, sub); } return 0; }用法上注意一点strchr返回的是指向原字符串内部的指针你可以通过指针相减pos - email得到偏移量也可以直接把它当作子串起点截取但不能修改这个指针指向的只读字符串字面量。如果字符串是const修饰的返回的指针最好也赋给const char *。3.6 sprintf与snprintf格式化输出的一进一出字符串处理中经常要把数字转成字符串、拼装日志、构造SQL语句等这时候snprintf比strcpy strcat的组合更省心一步到位#include stdio.h int main() { int id 42; char name[] alice; char buffer[128]; snprintf(buffer, sizeof(buffer), 用户%s的ID是%d, name, id); printf(%s\n, buffer); return 0; }sprintf老版本是安全重灾区没有缓冲区长度限制snprintf多了第二个参数传缓冲区总大小系统会自动在末尾补\0最多写size-1个有效字符。实际项目中我禁用sprintf一律用snprintf这个习惯能帮你避开非常多的内存崩溃问题。4. C字符串std::string和函数返回字符串的几种方式4.1 为什么C里最好用std::string如果你写的是C代码又有得选我强烈建议优先用std::string而不是C风格字符串。std::string自动管理内存往大了随便增长拷贝、连接直接用运算符可以直接比较内容不用调strcmp。比如#include iostream #include string int main() { std::string s1 Hello; std::string s2 World; std::string s3 s1 , s2 !; if (s3 Hello, World!) { std::cout 相等 std::endl; } std::cout 长度: s3.size() std::endl; return 0; }新手在实际编程中除非要跟C接口对接比如open()、read()这类POSIX函数只接受const char*参数否则完全没必要自己维护字符数组。4.2 函数返回字符串的四种方式对比“C函数返回字符串”是网上问得特别多的高频问题主要有四种套路各有适用场景。方式一返回std::string最推荐#include string std::string make_greeting(const std::string name) { return Hello, name !; }这里按值返回看似拷贝但现代编译器有返回值优化RVO和移动语义实际上不会有额外深拷贝开销非常安全。所有“返回一个新拼接字符串”的需求都应该这么做。方式二返回const char*并指向静态存储区const char* get_version() { return v1.0.0; }字符串字面量有静态生命周期程序启动到结束都存在返回这种指针是安全的但注意返回内容必须有固定生命周期。方式三返回const char*但指向局部数组——大坑禁止const char* bad_func() { char buffer[64] temp; return buffer; // 局部数组函数一结束内存就失效 }这是经典错误局部数组在栈上函数退出后生命周期结束返回的指针是悬垂指针解引用是未定义行为。但很多新手就是想玩指针踩得头破血流。方式四传入输出参数void fill_name(char* out, size_t size) { snprintf(out, size, hello); }调用者提供缓冲区和大小函数往里面写。这是C风格最稳妥的做法安全性由调用者保证适合模块间接口。4.3 std::string常用成员函数速查std::string的功能远不止拼字符串新手常用这几个#include iostream #include string int main() { std::string s Hello, World!; size_t pos s.find(World); // 从开头找子串找不到返回 std::string::npos if (pos ! std::string::npos) { std::cout 找到位置: pos std::endl; } std::string sub s.substr(0, 5); // 截取子串 std::cout sub std::endl; // Hello // 与C字符串互转 const char* cstr s.c_str(); // 转换成C风格字符串只读 std::string a abc; int cmp a.compare(abd); // 比较 std::cout cmp std::endl; // 负值 return 0; }这里有个关键陷阱c_str()返回的指针指向std::string内部缓冲区后续对字符串的任何修改包括追加字符、重新赋值都可能导致原内部缓冲区被销毁或重新分配之前得到的指针就没用了。所以别把c_str()的返回值存起来长期用随用随取。5. 新手常见问题与排查技巧实录5.1 典型错误速查表我把C/C字符串操作最常见的错误和对应解决方案整理成一张表遇到问题先翻这里错误代码/场景问题原因正确做法char s[] hello; printf(%d, sizeof(s) strlen(s) ? 1 : 0);sizeof含\0strlen不含明确区分计算数组空间用sizeof计算逻辑长度用strlenchar *a abc; char *b abc; if (a b)比较的是指针地址不是内容strcmp(a, b) 0strcpy(dest, src);dest太小缓冲区溢出可能破坏栈strncpy手动补\0或用std::stringchar buf[10]; strcat(buf, hello);buf未初始化strcat需要buf中已有合法字符串保证buf[0] \0或初始化为空串return 局部字符串;函数返回局部数组名返回了悬垂指针返回std::string或传入输出缓冲区分printf(%s, c);c是char类型%s需要C字符串指针不是字符用%c或构造包含字符的字符串strlen(str) -1来判错size_t无符号-1转成极大值用strlen(str) 0判断空串字符串字面量被修改如str[0] Astr指向字面量修改只读存储区崩溃用char[]数组不用char*指向字面量5.2 排查字符串问题的实操技巧一旦程序因为字符串操作崩溃我的排查顺序是这样的先开-Wall -fsanitizeaddress编译选项地址消毒器能直接报出缓冲区越界的精确位置比自己加print日志快一百倍。比如gcc -Wall -g -fsanitizeaddress main.c -o test终端会直接打印出类似ERROR: AddressSanitizer: stack-buffer-overflow on address...的信息告诉你哪一行越界了越界了几个字节。这是我现在排查C字符串内存问题的第一工具。第二步打印字符数组的十六进制内容确认终止符和边界。比如for (int i 0; i sizeof(buf); i) { printf(%02x , (unsigned char)buf[i]); }如果发现buf该有\0的地方是一串随机数就说明之前某步写入没正常收尾。想一下源头八成是strncpy没补终止符或strcat的位置不对。第三步如果还是没头绪检查是否对只读字符串字面量做了修改。比如char *s abc; s[0] A; // 写只读存储区崩这种写法在部分编译器上静默通过到运行时才爆炸。想修正就把声明改成char s[] abc;。5.3 给新手的三个实战习惯第一个习惯定义字符数组时缓冲区大小永远比你要存的内容多留至少1个字节用于存\0。宁可多申请不要少一个。第二个习惯所有字符串相关的“长度”自己心里要有数。拷贝前先算一下dest剩余空间能不能装下src拼接前先算一下已用长度和剩余容量。这些计算都用sizeof和strlen配合着来千万别靠手感估算。第三个习惯能用C标准库就优先用std::string。只有在模块边界、系统调用、嵌入式裸机环境这些必须用C接口的场景才手写C风格字符串函数。别在能用工具的地方硬造轮子把精力花在真正有难度的逻辑上。我个人这几年写字符串代码最大的体会是C语言的字符串函数设计得确实原始但理解它们是理解内存、指针、数组之间的关系的绝佳催化剂。很多新手怕这些函数本质上是怕指针和内存模型。你只要肯花一个下午把上面这些函数逐个敲一遍、故意制造几次缓冲区溢出、再用address sanitizer看看现场基本上以后谈起来就顺了。遇到不懂的字符串问题先画一块内存标出指向、边界、终止符的位置答案往往自己就浮现出来了。
返回列表