
C 语言字符串与内存函数从字符处理到字节级操作C 语言没有独立的字符串类型。我们平时写下的字符串要么是存放在只读区域中的字符串字面量要么是以字符数组形式保存在内存中的一段连续空间。字符串函数和内存函数的本质都是按照某种约定读取、复制、比较或修改这段空间。理解这些函数不能只记住函数名和参数顺序更要明确三个问题数据是否以 \0 作为结束标志。目标空间是否足够大、是否允许修改。操作的对象是字符串还是任意字节序列。前两个问题决定字符串函数是否安全第三个问题决定应该使用字符串函数还是内存函数。一、字符串在 C 语言中的表示字符串字面量可以直接写在程序中constchar*messagehello;这里的指针保存字符串首字符的地址字符串内容通常位于不可修改的存储区域。需要修改内容时应使用字符数组charmessage[]hello;message[0]H;字符数组中的字符串必须以 ‘\0’ 结尾。对字符串来说\0 不是普通的可见字符而是表示内容结束的标记charword[]{c,o,d,e,\0};上面的数组有 5 个元素但字符串长度是 4。字符串函数通常从首地址开始逐个读取字符直到遇到 ‘\0’。如果字符数组没有结尾的 ‘\0’就不能把它当作 C 字符串传给 strlen、strcpy 或 printf(“%s”, …)。函数可能越过数组边界继续读取造成未定义行为。二、获取字符串长度strlenstrlen 用于计算字符串中 ‘\0’ 之前的字符数量不包含结束标志#includestdio.h#includestring.hintmain(void){constchar*texthello;printf(%zu\n,strlen(text));return0;}函数原型是size_tstrlen(constchar*str);参数必须指向以 ‘\0’ 结尾的有效字符串。返回值类型是 size_t它是无符号整数类型因此进行减法或比较时要特别小心size_tastrlen(abc);size_tbstrlen(abcdef);if(a-b0){/* 这里不一定代表 a 比 b 长 */}因为无符号数不能表示负数a - b 可能发生回绕。比较两个字符串的长度时可以直接写 a b不要通过两个 size_t 相减后判断正负。手动实现 strlen计数器版本#includestddef.hsize_tmy_strlen(constchar*str){size_tcount0;while(str[count]!\0){count;}returncount;}指针相减版本#includestddef.hsize_tmy_strlen(constchar*str){constchar*endstr;while(*end!\0){end;}return(size_t)(end-str);}递归版本也可以表达同样的逻辑但字符串很长时可能产生较深的调用栈因此实际工程中通常使用循环版本。三、复制字符串strcpystrcpy 从源字符串复制字符到目标数组并且会把源字符串结尾的 ‘\0’ 一起复制char*strcpy(char*destination,constchar*source);使用时必须满足以下条件source 必须指向以 ‘\0’ 结尾的字符串。destination 必须有足够空间保存源字符串和结尾的 ‘\0’。destination 必须是可写空间。源区域和目标区域不能重叠。示例#includestdio.h#includestring.hintmain(void){chardestination[32];constchar*sourceC language;strcpy(destination,source);puts(destination);return0;}目标数组的容量不能只看字符串的可见字符数量还要为 ‘\0’ 预留一个字节。对于外部输入直接使用 strcpy 很容易造成缓冲区溢出应该先检查长度或使用更明确的长度限制策略。手动实现 strcpy#includeassert.hchar*my_strcpy(char*destination,constchar*source){char*resultdestination;assert(destination!NULL);assert(source!NULL);while((*destination*source)!\0){/* copy including the terminator */}returnresult;}返回目标地址是标准库函数的一个常见设计这样可以把复制函数放进表达式中使用。四、拼接字符串strcatstrcat 会找到目标字符串末尾的 ‘\0’从那里开始追加源字符串并在新内容末尾写入新的 ‘\0’char*strcat(char*destination,constchar*source);使用时需要确保destination 本身已经是一个合法字符串。source 是以 ‘\0’ 结尾的字符串。destination 的剩余空间足够容纳追加内容和新的 ‘\0’。两个区域不能重叠。#includestdio.h#includestring.hintmain(void){charmessage[32]hello;strcat(message, world);puts(message);return0;}不要让字符串自己追加到自己例如 strcat(buffer, buffer)。源字符串和目标字符串重叠时标准并不保证结果应该改用更明确的临时缓冲区或内存移动方案。手动实现 strcat#includeassert.hchar*my_strcat(char*destination,constchar*source){char*resultdestination;assert(destination!NULL);assert(source!NULL);while(*destination!\0){destination;}while((*destination*source)!\0){/* append including the terminator */}returnresult;}五、比较字符串strcmpstrcmp 按字典序比较两个字符串从第一个字符开始逐个比较。遇到字符不同或者某个字符串先结束时比较停止intstrcmp(constchar*left,constchar*right);返回值只需要关注符号返回值小于 0表示 left 小于 right。返回值等于 0表示两个字符串内容相同。返回值大于 0表示 left 大于 right。不要把返回值当成固定的 -1、0 或 1。标准只保证正负关系不保证具体数值。字符串比较的是字符编码顺序而不是字符串长度。比如 “z” 的长度小于 “apple”但按常见字符编码顺序“z” 可能大于 “apple”。手动实现 strcmp#includeassert.hintmy_strcmp(constchar*left,constchar*right){assert(left!NULL);assert(right!NULL);while(*left!\0*left*right){left;right;}if((unsignedchar)*left(unsignedchar)*right){return-1;}if((unsignedchar)*left(unsignedchar)*right){return1;}return0;}转换为 unsigned char 可以避免某些平台上 char 为有符号类型时产生负值比较问题。六、带长度限制的字符串函数无长度限制的 strcpy、strcat 和 strcmp 使用方便但调用者必须自行保证空间和边界。带 n 的函数可以限制本次处理的最大字符数但它们并不自动解决所有安全问题。strncpychar*strncpy(char*destination,constchar*source,size_tcount);strncpy 最多复制 count 个字符。如果源字符串在 count 个字符之前遇到 ‘\0’目标区域剩余位置会补零如果源字符串长度大于或等于 count目标区域可能不会自动添加 ‘\0’。charname[8];strncpy(name,Alexander,sizeofname-1);name[sizeofname-1]\0;这种显式补终止符的写法更容易保证结果是合法字符串。不要简单地认为 strncpy 永远会返回以 ‘\0’ 结尾的字符串。strncatchar*strncat(char*destination,constchar*source,size_tcount);strncat 最多从 source 追加 count 个字符并在结果末尾写入一个 ‘\0’。目标数组必须有足够空间容纳原字符串、实际追加内容和结束标志。charmessage[32]hello ;strncat(message,world,5);需要注意count 是允许从源字符串读取的最大字符数不是目标数组的总容量。strncmpintstrncmp(constchar*left,constchar*right,size_tcount);strncmp 最多比较 count 个字符。如果在此之前发现不同字符、遇到字符串结束或者已经比较完 count 个字符函数就会停止if(strncmp(command,quit,4)0){puts(exit);}它适合比较固定前缀但如果要判断两个完整字符串是否相同必须同时考虑长度和结束符。七、查找字符串strstrstrstr 在一个字符串中查找另一个字符串第一次出现的位置char*strstr(constchar*text,constchar*pattern);找到时返回匹配位置的指针找不到时返回 NULL。如果 pattern 是空字符串标准行为是返回 text 本身#includestdio.h#includestring.hintmain(void){charmessage[]C makes memory visible;char*matchstrstr(message,memory);if(match!NULL){printf(found: %s\n,match);}return0;}返回的指针仍然指向原字符串。如果原字符串是字符数组可以通过它修改匹配位置如果原字符串是字符串字面量则不能写入。基础实现可以从主串的每一个位置开始逐字符比较#includestddef.hchar*my_strstr(constchar*text,constchar*pattern){if(*pattern\0){return(char*)text;}for(constchar*starttext;*start!\0;start){constchar*astart;constchar*bpattern;while(*a!\0*b!\0*a*b){a;b;}if(*b\0){return(char*)start;}}returnNULL;}这种实现易于理解但在大量文本匹配时可能效率较低。对性能要求较高的场景可以进一步研究 KMP 等模式匹配算法。八、切分字符串strtokstrtok 根据分隔符集合把一个字符串拆成多个标记char*strtok(char*str,constchar*separators);第一次调用时传入待切分字符串之后继续调用时传入 NULL函数会从上一次保存的位置继续查找#includestdio.h#includestring.hintmain(void){charinput[]userexample.com;constchar*separators.;for(char*tokenstrtok(input,separators);token!NULL;tokenstrtok(NULL,separators)){puts(token);}return0;}strtok 会直接修改输入字符串把分隔符替换成 ‘\0’因此不能把字符串字面量作为第一个参数。需要保留原始内容时应先复制到可修改的字符数组。传统 strtok 使用内部状态保存切分位置不适合交错切分多个字符串也不适合需要重入性的场景。具体平台如果提供 strtok_r 或其他可重入版本可以根据项目需求选择。九、错误信息strerror 与 errno许多库函数失败时会设置 errno它是一个错误码。strerror 可以把错误码转换为可读的说明#includeerrno.h#includestdio.h#includestring.hintmain(void){FILE*filefopen(missing.txt,r);if(fileNULL){fprintf(stderr,open failed: %s\n,strerror(errno));return1;}fclose(file);return0;}errno 只有在相关函数报告失败后才有意义。不要在成功调用后无条件读取它也不要把 errno 的数值直接当作用户可读的错误信息。十、字符分类与转换头文件 ctype.h 提供了一组字符分类函数和大小写转换函数isspace空白字符。isdigit十进制数字。isxdigit十六进制数字。islower、isupper小写或大写字母。isalpha字母。isalnum字母或数字。ispunct标点符号。isprint可打印字符。iscntrl控制字符。tolower、toupper大小写转换。#includectype.h#includestdio.hintmain(void){chartext[]C Language 2025!;for(size_ti0;text[i]!\0;i){unsignedcharch(unsignedchar)text[i];if(isupper(ch)){text[i](char)tolower(ch);}}puts(text);return0;}除了 EOF 之外传给这些函数的参数应该能够转换为 unsigned char。如果直接传入一个值为负的普通 char在某些平台上可能触发未定义行为。因此处理可能包含非 ASCII 字节的数据时先转换为 unsigned char 是稳妥的习惯。十一、按字节复制memcpy字符串函数以 ‘\0’ 为结束条件而 memcpy 完全按照字节数复制不会因为遇到零字节而提前停止void*memcpy(void*destination,constvoid*source,size_tcount);它可以复制字符数组、结构体或任意对象的字节表示#includestring.hstructPerson{charname[32];intage;};structPersonsource{Ada,36};structPersoncopy;memcpy(copy,source,sizeofcopy);memcpy 要求源区域和目标区域不能重叠。如果存在重叠复制结果是未定义的此时应该使用 memmove。十二、处理重叠区域memmovevoid*memmove(void*destination,constvoid*source,size_tcount);memmove 与 memcpy 的主要区别是能够正确处理重叠区域。它会根据源地址和目标地址的关系选择从前往后或从后往前复制#includestdio.h#includestring.hintmain(void){chartext[]abcdef;memmove(text2,text,4);puts(text);return0;}这里目标区域与源区域发生重叠使用 memcpy 并不安全而 memmove 会保证每个源字节在被覆盖前已经完成读取。手动实现 memmove#includestddef.hvoid*my_memmove(void*destination,constvoid*source,size_tcount){unsignedchar*dstdestination;constunsignedchar*srcsource;if(dstsrc||count0){returndestination;}if(dstsrc||dstsrccount){for(size_ti0;icount;i){dst[i]src[i];}}else{for(size_ticount;i0;--i){dst[i-1]src[i-1];}}returndestination;}真实库实现会针对平台进行优化但“无重叠时从前往后有重叠且目标在后方时从后往前”的思路是理解它的关键。十三、设置内存memsetmemset 把目标区域的每一个字节设置为同一个 unsigned char 值void*memset(void*destination,intvalue,size_tcount);#includestring.hcharbuffer[16];memset(buffer,0,sizeofbuffer);需要注意memset 按字节填充而不是把一个整数整体写入每个元素。下面的代码会把每个字节设置为 1因此整型数组中的每个元素通常会变成 0x01010101而不是整数 1intvalues[4];memset(values,1,sizeofvalues);如果想把整型数组初始化为 1应使用循环或显式赋值把内存清零则通常可以使用 memset(values, 0, sizeof values)。十四、按字节比较memcmpmemcmp 比较两个内存区域前 count 个字节intmemcmp(constvoid*left,constvoid*right,size_tcount);返回值同样只需要关注正负和零。它比较的是字节序列不是 C 字符串也不会因为遇到 ‘\0’ 停止#includestdio.h#includestring.hintmain(void){unsignedcharfirst[]{1,2,0,4};unsignedcharsecond[]{1,2,0,5};if(memcmp(first,second,sizeoffirst)0){puts(first is smaller);}return0;}对结构体直接使用 memcmp 判断是否相等要谨慎因为结构体可能包含填充字节而填充字节的内容未必相同。判断结构体逻辑是否相等通常应逐个比较有意义的成员。十五、字符串函数与内存函数的选择场景合适的函数计算以 ‘\0’ 结尾的字符串长度strlen复制完整字符串strcpy 或经过边界设计的替代方案追加字符串strcat 或长度受控的替代方案按字典序比较字符串strcmp、strncmp查找子串strstr按分隔符切分可修改字符串strtok报告 errno 对应的文字strerror复制任意字节序列memcpy复制可能重叠的内存区域memmove将字节填成同一个值memset比较任意字节序列memcmp字符串函数依赖 ‘\0’适合文本内存函数依赖显式字节数适合二进制数据、结构体和包含零字节的缓冲区。把二者混用是很多内存错误的根源。十六、实现和使用时的安全检查编写字符串和内存操作代码时可以遵循这些习惯明确源数据和目标数据的容量给字符串结束符预留空间。不把字符串字面量传给会修改内容的函数。需要处理重叠区域时使用 memmove不要使用 memcpy。使用 strncpy 后检查目标是否有 ‘\0’必要时手动补上。不要用字符串函数处理没有结束符的字符数组。使用 strlen 的返回值时注意 size_t 是无符号类型。调用 ctype.h 函数前将普通字符转换为 unsigned char。使用 memset 时记住它填充的是字节不是完整整数。用 memcmp 比较二进制数据时传入准确的字节数。模拟实现库函数时先写清参数顺序、停止条件、返回值和重叠规则。字符串函数让文本处理更方便内存函数则提供了更底层、更通用的字节操作能力。真正可靠的 C 代码不是简单地把一个函数名换成另一个函数名而是先弄清楚数据的表示方式、生命周期、容量和重叠关系再选择与问题匹配的工具。