☰
C 语言字符串与内存函数:从字符处理到字节级操作
2026/9/26 4:48:33 网站建设 项目流程

C 语言字符串与内存函数:从字符处理到字节级操作

C 语言没有独立的字符串类型。我们平时写下的字符串,要么是存放在只读区域中的字符串字面量,要么是以字符数组形式保存在内存中的一段连续空间。字符串函数和内存函数的本质,都是按照某种约定读取、复制、比较或修改这段空间。

理解这些函数,不能只记住函数名和参数顺序,更要明确三个问题:

  1. 数据是否以 \0 作为结束标志。
  2. 目标空间是否足够大、是否允许修改。
  3. 操作的对象是字符串,还是任意字节序列。

前两个问题决定字符串函数是否安全,第三个问题决定应该使用字符串函数还是内存函数。

一、字符串在 C 语言中的表示

字符串字面量可以直接写在程序中:

constchar*message="hello";

这里的指针保存字符串首字符的地址,字符串内容通常位于不可修改的存储区域。需要修改内容时,应使用字符数组:

charmessage[]="hello";message[0]='H';

字符数组中的字符串必须以 ‘\0’ 结尾。对字符串来说,\0 不是普通的可见字符,而是表示内容结束的标记:

charword[]={'c','o','d','e','\0'};

上面的数组有 5 个元素,但字符串长度是 4。字符串函数通常从首地址开始逐个读取字符,直到遇到 ‘\0’。

如果字符数组没有结尾的 ‘\0’,就不能把它当作 C 字符串传给 strlen、strcpy 或 printf(“%s”, …)。函数可能越过数组边界继续读取,造成未定义行为。

二、获取字符串长度:strlen

strlen 用于计算字符串中 ‘\0’ 之前的字符数量,不包含结束标志:

#include<stdio.h>#include<string.h>intmain(void){constchar*text="hello";printf("%zu\n",strlen(text));return0;}

函数原型是:

size_tstrlen(constchar*str);

参数必须指向以 ‘\0’ 结尾的有效字符串。返回值类型是 size_t,它是无符号整数类型,因此进行减法或比较时要特别小心:

size_ta=strlen("abc");size_tb=strlen("abcdef");if(a-b>0){/* 这里不一定代表 a 比 b 长 */}

因为无符号数不能表示负数,a - b 可能发生回绕。比较两个字符串的长度时,可以直接写 a > b,不要通过两个 size_t 相减后判断正负。

手动实现 strlen

计数器版本:

#include<stddef.h>size_tmy_strlen(constchar*str){size_tcount=0;while(str[count]!='\0'){++count;}returncount;}

指针相减版本:

#include<stddef.h>size_tmy_strlen(constchar*str){constchar*end=str;while(*end!='\0'){++end;}return(size_t)(end-str);}

递归版本也可以表达同样的逻辑,但字符串很长时可能产生较深的调用栈,因此实际工程中通常使用循环版本。

三、复制字符串:strcpy

strcpy 从源字符串复制字符到目标数组,并且会把源字符串结尾的 ‘\0’ 一起复制:

char*strcpy(char*destination,constchar*source);

使用时必须满足以下条件:

  • source 必须指向以 ‘\0’ 结尾的字符串。
  • destination 必须有足够空间保存源字符串和结尾的 ‘\0’。
  • destination 必须是可写空间。
  • 源区域和目标区域不能重叠。

示例:

#include<stdio.h>#include<string.h>intmain(void){chardestination[32];constchar*source="C language";strcpy(destination,source);puts(destination);return0;}

目标数组的容量不能只看字符串的可见字符数量,还要为 ‘\0’ 预留一个字节。对于外部输入,直接使用 strcpy 很容易造成缓冲区溢出,应该先检查长度,或使用更明确的长度限制策略。

手动实现 strcpy

#include<assert.h>char*my_strcpy(char*destination,constchar*source){char*result=destination;assert(destination!=NULL);assert(source!=NULL);while((*destination++=*source++)!='\0'){/* copy including the terminator */}returnresult;}

返回目标地址是标准库函数的一个常见设计,这样可以把复制函数放进表达式中使用。

四、拼接字符串:strcat

strcat 会找到目标字符串末尾的 ‘\0’,从那里开始追加源字符串,并在新内容末尾写入新的 ‘\0’:

char*strcat(char*destination,constchar*source);

使用时需要确保:

  • destination 本身已经是一个合法字符串。
  • source 是以 ‘\0’ 结尾的字符串。
  • destination 的剩余空间足够容纳追加内容和新的 ‘\0’。
  • 两个区域不能重叠。
#include<stdio.h>#include<string.h>intmain(void){charmessage[32]="hello";strcat(message," world");puts(message);return0;}

不要让字符串自己追加到自己,例如 strcat(buffer, buffer)。源字符串和目标字符串重叠时,标准并不保证结果,应该改用更明确的临时缓冲区或内存移动方案。

手动实现 strcat

#include<assert.h>char*my_strcat(char*destination,constchar*source){char*result=destination;assert(destination!=NULL);assert(source!=NULL);while(*destination!='\0'){++destination;}while((*destination++=*source++)!='\0'){/* append including the terminator */}returnresult;}

五、比较字符串:strcmp

strcmp 按字典序比较两个字符串,从第一个字符开始逐个比较。遇到字符不同,或者某个字符串先结束时,比较停止:

intstrcmp(constchar*left,constchar*right);

返回值只需要关注符号:

  • 返回值小于 0,表示 left 小于 right。
  • 返回值等于 0,表示两个字符串内容相同。
  • 返回值大于 0,表示 left 大于 right。

不要把返回值当成固定的 -1、0 或 1。标准只保证正负关系,不保证具体数值。

字符串比较的是字符编码顺序,而不是字符串长度。比如 “z” 的长度小于 “apple”,但按常见字符编码顺序,“z” 可能大于 “apple”。

手动实现 strcmp

#include<assert.h>intmy_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 的函数可以限制本次处理的最大字符数,但它们并不自动解决所有安全问题。

strncpy

char*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’ 结尾的字符串。

strncat

char*strncat(char*destination,constchar*source,size_tcount);

strncat 最多从 source 追加 count 个字符,并在结果末尾写入一个 ‘\0’。目标数组必须有足够空间容纳原字符串、实际追加内容和结束标志。

charmessage[32]="hello ";strncat(message,"world",5);

需要注意,count 是允许从源字符串读取的最大字符数,不是目标数组的总容量。

strncmp

intstrncmp(constchar*left,constchar*right,size_tcount);

strncmp 最多比较 count 个字符。如果在此之前发现不同字符、遇到字符串结束,或者已经比较完 count 个字符,函数就会停止:

if(strncmp(command,"quit",4)==0){puts("exit");}

它适合比较固定前缀,但如果要判断两个完整字符串是否相同,必须同时考虑长度和结束符。

七、查找字符串:strstr

strstr 在一个字符串中查找另一个字符串第一次出现的位置:

char*strstr(constchar*text,constchar*pattern);

找到时返回匹配位置的指针,找不到时返回 NULL。如果 pattern 是空字符串,标准行为是返回 text 本身:

#include<stdio.h>#include<string.h>intmain(void){charmessage[]="C makes memory visible";char*match=strstr(message,"memory");if(match!=NULL){printf("found: %s\n",match);}return0;}

返回的指针仍然指向原字符串。如果原字符串是字符数组,可以通过它修改匹配位置;如果原字符串是字符串字面量,则不能写入。

基础实现可以从主串的每一个位置开始逐字符比较:

#include<stddef.h>char*my_strstr(constchar*text,constchar*pattern){if(*pattern=='\0'){return(char*)text;}for(constchar*start=text;*start!='\0';++start){constchar*a=start;constchar*b=pattern;while(*a!='\0'&&*b!='\0'&&*a==*b){++a;++b;}if(*b=='\0'){return(char*)start;}}returnNULL;}

这种实现易于理解,但在大量文本匹配时可能效率较低。对性能要求较高的场景,可以进一步研究 KMP 等模式匹配算法。

八、切分字符串:strtok

strtok 根据分隔符集合把一个字符串拆成多个标记:

char*strtok(char*str,constchar*separators);

第一次调用时传入待切分字符串,之后继续调用时传入 NULL,函数会从上一次保存的位置继续查找:

#include<stdio.h>#include<string.h>intmain(void){charinput[]="user@example.com";constchar*separators=".@";for(char*token=strtok(input,separators);token!=NULL;token=strtok(NULL,separators)){puts(token);}return0;}

strtok 会直接修改输入字符串,把分隔符替换成 ‘\0’,因此不能把字符串字面量作为第一个参数。需要保留原始内容时,应先复制到可修改的字符数组。

传统 strtok 使用内部状态保存切分位置,不适合交错切分多个字符串,也不适合需要重入性的场景。具体平台如果提供 strtok_r 或其他可重入版本,可以根据项目需求选择。

九、错误信息:strerror 与 errno

许多库函数失败时会设置 errno,它是一个错误码。strerror 可以把错误码转换为可读的说明:

#include<errno.h>#include<stdio.h>#include<string.h>intmain(void){FILE*file=fopen("missing.txt","r");if(file==NULL){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:大小写转换。
#include<ctype.h>#include<stdio.h>intmain(void){chartext[]="C Language 2025!";for(size_ti=0;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);

它可以复制字符数组、结构体或任意对象的字节表示:

#include<string.h>structPerson{charname[32];intage;};structPersonsource={"Ada",36};structPersoncopy;memcpy(&copy,&source,sizeofcopy);

memcpy 要求源区域和目标区域不能重叠。如果存在重叠,复制结果是未定义的,此时应该使用 memmove。

十二、处理重叠区域:memmove

void*memmove(void*destination,constvoid*source,size_tcount);

memmove 与 memcpy 的主要区别是能够正确处理重叠区域。它会根据源地址和目标地址的关系,选择从前往后或从后往前复制:

#include<stdio.h>#include<string.h>intmain(void){chartext[]="abcdef";memmove(text+2,text,4);puts(text);return0;}

这里目标区域与源区域发生重叠,使用 memcpy 并不安全,而 memmove 会保证每个源字节在被覆盖前已经完成读取。

手动实现 memmove

#include<stddef.h>void*my_memmove(void*destination,constvoid*source,size_tcount){unsignedchar*dst=destination;constunsignedchar*src=source;if(dst==src||count==0){returndestination;}if(dst<src||dst>=src+count){for(size_ti=0;i<count;++i){dst[i]=src[i];}}else{for(size_ti=count;i>0;--i){dst[i-1]=src[i-1];}}returndestination;}

真实库实现会针对平台进行优化,但“无重叠时从前往后,有重叠且目标在后方时从后往前”的思路是理解它的关键。

十三、设置内存:memset

memset 把目标区域的每一个字节设置为同一个 unsigned char 值:

void*memset(void*destination,intvalue,size_tcount);
#include<string.h>charbuffer[16];memset(buffer,0,sizeofbuffer);

需要注意,memset 按字节填充,而不是把一个整数整体写入每个元素。下面的代码会把每个字节设置为 1,因此整型数组中的每个元素通常会变成 0x01010101,而不是整数 1:

intvalues[4];memset(values,1,sizeofvalues);

如果想把整型数组初始化为 1,应使用循环或显式赋值;把内存清零则通常可以使用 memset(values, 0, sizeof values)。

十四、按字节比较:memcmp

memcmp 比较两个内存区域前 count 个字节:

intmemcmp(constvoid*left,constvoid*right,size_tcount);

返回值同样只需要关注正负和零。它比较的是字节序列,不是 C 字符串,也不会因为遇到 ‘\0’ 停止:

#include<stdio.h>#include<string.h>intmain(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’,适合文本;内存函数依赖显式字节数,适合二进制数据、结构体和包含零字节的缓冲区。把二者混用,是很多内存错误的根源。

十六、实现和使用时的安全检查

编写字符串和内存操作代码时,可以遵循这些习惯:

  1. 明确源数据和目标数据的容量,给字符串结束符预留空间。
  2. 不把字符串字面量传给会修改内容的函数。
  3. 需要处理重叠区域时使用 memmove,不要使用 memcpy。
  4. 使用 strncpy 后检查目标是否有 ‘\0’,必要时手动补上。
  5. 不要用字符串函数处理没有结束符的字符数组。
  6. 使用 strlen 的返回值时注意 size_t 是无符号类型。
  7. 调用 ctype.h 函数前将普通字符转换为 unsigned char。
  8. 使用 memset 时记住它填充的是字节,不是完整整数。
  9. 用 memcmp 比较二进制数据时传入准确的字节数。
  10. 模拟实现库函数时,先写清参数顺序、停止条件、返回值和重叠规则。

字符串函数让文本处理更方便,内存函数则提供了更底层、更通用的字节操作能力。真正可靠的 C 代码,不是简单地把一个函数名换成另一个函数名,而是先弄清楚数据的表示方式、生命周期、容量和重叠关系,再选择与问题匹配的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询