C语言字符串函数与内存函数详解(含模拟实现与优化)
在C语言编程中,处理字符和内存块是基本功。标准库提供了丰富的函数,但如果不了解其原理和边界,很容易写出不安全的代码。
本篇博客将系统讲解<ctype.h>、<string.h>和<string.h>中的内存函数,从使用到模拟实现,并给出优化建议,帮你写出更健壮的程序。
📚 目录
-
字符分类与转换函数
-
字符串函数
-
2.1 strlen —— 求字符串长度
-
2.2 strcpy / strncpy —— 字符串拷贝
-
2.3 strcat / strncat —— 字符串追加
-
2.4 strcmp / strncmp —— 字符串比较
-
2.5 strstr —— 查找子串
-
2.6 strtok —— 字符串分割
-
2.7 strerror / perror —— 错误信息
-
-
内存函数
-
3.1 memcpy —— 内存拷贝(不重叠)
-
3.2 memmove —— 内存拷贝(可重叠)
-
3.3 memset —— 内存填充
-
3.4 memcmp —— 内存比较
-
-
模拟实现与安全性优化
-
总结
1. 字符分类与转换函数
头文件 <ctype.h> 提供了一系列判断字符类型的函数,以及大小写转换函数。
| 函数 | 功能(返回真) |
|---|---|
islower |
小写字母 a~z |
isupper |
大写字母 A~Z |
isdigit |
数字 0~9 |
isalpha |
字母 a~z 或 A~Z |
isalnum |
字母或数字 |
isspace |
空白字符(空格、换行、制表等) |
ispunct |
标点符号 |
isprint |
可打印字符 |
转换函数:
-
int tolower(int c):将大写字母转小写 -
int toupper(int c):将小写字母转大写
示例:将字符串中小写转大写(优化版)
c
#include <stdio.h>
#include <ctype.h>
void toUpperString(char* str) {
while (*str) {
if (islower(*str)) {
*str = toupper(*str);
}
str++;
}
}
int main() {
char s[] = "Hello, World!";
toUpperString(s);
printf("%s\n", s); // HELLO, WORLD!
return 0;
}
2. 字符串函数
所有字符串函数均声明在 <string.h>,操作以 \0 结尾的字符串。
2.1 strlen —— 求字符串长度
函数原型:
c
size_t strlen(const char* str);
-
返回
\0之前的字符个数,不包含\0。 -
返回值类型
size_t是无符号整数,注意减法陷阱。
安全性优化: 使用 assert 检查空指针。
模拟实现(三种方式):
c
#include <assert.h>
// 方式1:计数器
size_t my_strlen1(const char* str) {
assert(str != NULL);
size_t count = 0;
while (*str) {
count++;
str++;
}
return count;
}
// 方式2:指针 - 指针(效率高)
size_t my_strlen2(const char* str) {
assert(str != NULL);
const char* start = str;
while (*str) str++;
return str - start;
}
// 方式3:递归(不推荐,容易栈溢出)
size_t my_strlen3(const char* str) {
assert(str != NULL);
if (*str == '\0') return 0;
return 1 + my_strlen3(str + 1);
}
2.2 strcpy / strncpy —— 字符串拷贝
函数原型:
c
char* strcpy(char* dest, const char* src); char* strncpy(char* dest, const char* src, size_t num);
-
strcpy将src全部拷贝到dest,包括\0,不检查目标空间大小,不安全。 -
strncpy最多拷贝num个字符,若src长度不足则补\0,更安全。
模拟实现 strcpy(优化:使用断言,返回目标地址):
c
char* my_strcpy(char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
while (*dest++ = *src++) // 把 '\0' 也拷过去
;
return ret;
}
优化建议: 优先使用 strncpy,并确保目标数组足够大,最后手动添加 \0。
2.3 strcat / strncat —— 字符串追加
c
char* strcat(char* dest, const char* src); char* strncat(char* dest, const char* src, size_t num);
-
strcat从dest的\0处开始追加,同样不安全。 -
strncat最多追加num个字符,并总是添加\0。
模拟实现 strcat:
c
char* my_strcat(char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
while (*dest) dest++; // 找末尾
while (*dest++ = *src++) // 复制
;
return ret;
}
2.4 strcmp / strncmp —— 字符串比较
c
int strcmp(const char* s1, const char* s2); int strncmp(const char* s1, const char* s2, size_t num);
-
返回 >0、=0、<0 分别表示 s1 大于、等于、小于 s2。
-
strncmp只比较前num个字符。
模拟实现 strcmp:
c
int my_strcmp(const char* s1, const char* s2) {
assert(s1 && s2);
while (*s1 && *s1 == *s2) {
s1++;
s2++;
}
return *s1 - *s2;
}
2.5 strstr —— 查找子串
c
char* strstr(const char* str, const char* substr);
-
在
str中查找substr第一次出现的位置,返回指针或 NULL。
模拟实现(暴力匹配):
c
char* my_strstr(const char* str, const char* substr) {
assert(str && substr);
if (*substr == '\0') return (char*)str;
const char* cp = str;
const char* s1, * s2;
while (*cp) {
s1 = cp;
s2 = substr;
while (*s1 && *s2 && *s1 == *s2) {
s1++;
s2++;
}
if (*s2 == '\0') return (char*)cp;
cp++;
}
return NULL;
}
💡 更高效的 KMP 算法可自行研究,但暴力匹配在大多数情况下足够。
2.6 strtok —— 字符串分割
c
char* strtok(char* str, const char* delim);
-
首次调用传入待分割字符串,后续传入 NULL,用分隔符
delim分割。 -
会修改原字符串,将分隔符替换为
\0,如需保留原串,先拷贝。
使用示例:
c
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "192.168.1.100";
char buf[30];
strcpy(buf, src); // 拷贝一份
const char* sep = ".";
char* token;
for (token = strtok(buf, sep); token != NULL; token = strtok(NULL, sep)) {
printf("%s\n", token);
}
return 0;
}
2.7 strerror / perror —— 错误信息
c
char* strerror(int errnum); void perror(const char* s);
-
strerror返回错误码对应的错误信息字符串。 -
perror直接打印错误信息,前面可加自定义前缀。
示例:
c
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE* fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
// 或 printf("错误:%s\n", strerror(errno));
}
return 0;
}
3. 内存函数
内存函数操作的是内存块,不关心数据类型,以字节为单位。
3.1 memcpy —— 内存拷贝(不重叠)
c
void* memcpy(void* dest, const void* src, size_t num);
-
从
src复制num个字节到dest。 -
如果两块内存重叠,行为未定义,应使用
memmove。
模拟实现:
c
void* my_memcpy(void* dest, const void* src, size_t num) {
assert(dest && src);
void* ret = dest;
while (num--) {
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
3.2 memmove —— 内存拷贝(可重叠)
c
void* memmove(void* dest, const void* src, size_t num);
-
处理重叠情况:若
dest < src,从前向后拷贝;否则从后向前拷贝。
模拟实现:
c
void* my_memmove(void* dest, const void* src, size_t num) {
assert(dest && src);
void* ret = dest;
char* d = (char*)dest;
const char* s = (const char*)src;
if (d < s) {
// 从前向后
while (num--) {
*d++ = *s++;
}
} else {
// 从后向前
d += num - 1;
s += num - 1;
while (num--) {
*d-- = *s--;
}
}
return ret;
}
3.3 memset —— 内存填充
c
void* memset(void* ptr, int value, size_t num);
-
将
ptr开始的num个字节设置为value(转换为无符号字符)。 -
常用于数组清零:
memset(arr, 0, sizeof(arr));
3.4 memcmp —— 内存比较
c
int memcmp(const void* ptr1, const void* ptr2, size_t num);
-
比较两块内存前
num个字节,返回正/零/负。
4. 模拟实现与安全性优化
4.1 为什么需要优化?
标准库函数如 strcpy、strcat 不检查目标空间大小,容易导致缓冲区溢出。优化思路:
-
使用
strncpy、strncat等带长度限制的版本。 -
在模拟实现中加入
assert断言,确保指针非空。 -
使用
const修饰只读参数,增强代码健壮性。
4.2 优化后的综合示例
下面给出一个安全的字符串拷贝函数,类似于 strncpy 但保证结尾有 \0(C库的 strncpy 在源长度≥num时不自动添加 \0,需手动处理):
c
#include <stdio.h>
#include <assert.h>
char* safe_strncpy(char* dest, const char* src, size_t n) {
assert(dest && src);
char* ret = dest;
size_t i;
for (i = 0; i < n && src[i]; i++) {
dest[i] = src[i];
}
// 填充剩余位置为 '\0'
for (; i < n; i++) {
dest[i] = '\0';
}
return ret;
}
int main() {
char buf[10];
safe_strncpy(buf, "hello world", sizeof(buf));
printf("%s\n", buf); // 输出 "hello wor"(截断)
return 0;
}
5. 总结
| 类别 | 函数 | 注意事项 |
|---|---|---|
| 字符串长度 | strlen |
返回无符号,注意减法 |
| 拷贝 | strcpy / strncpy |
strncpy 不自动补 '\0',需小心 |
| 追加 | strcat / strncat |
strncat 总会添加 '\0' |
| 比较 | strcmp / strncmp |
逐字符比较 ASCII |
| 查找 | strstr |
暴力匹配足够,KMP 可优化 |
| 分割 | strtok |
会修改原串,记得备份 |
| 错误 | strerror / perror |
结合 errno 使用 |
| 内存拷贝 | memcpy / memmove |
重叠时用 memmove |
| 内存填充 | memset |
常用于清零 |
| 内存比较 | memcmp |
按字节比较 |
核心原则:
-
任何时候都要保证目标空间足够大。
-
使用带
n的长度限制函数更安全。 -
善用
assert和const提升代码质量。
多动手模拟实现,你将对底层操作有更深刻的理解.
📝 所有代码均可在 VS2022 / GCC 下编译运行。
如有疑问,欢迎留言讨论!
更多推荐




所有评论(0)