在C语言编程中,处理字符和内存块是基本功。标准库提供了丰富的函数,但如果不了解其原理和边界,很容易写出不安全的代码。
本篇博客将系统讲解 <ctype.h><string.h> 和 <string.h> 中的内存函数,从使用到模拟实现,并给出优化建议,帮你写出更健壮的程序。


📚 目录

  1. 字符分类与转换函数

  2. 字符串函数

    • 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. 内存函数

    • 3.1 memcpy —— 内存拷贝(不重叠)

    • 3.2 memmove —— 内存拷贝(可重叠)

    • 3.3 memset —— 内存填充

    • 3.4 memcmp —— 内存比较

  4. 模拟实现与安全性优化

  5. 总结


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 为什么需要优化?

标准库函数如 strcpystrcat 不检查目标空间大小,容易导致缓冲区溢出。优化思路:

  • 使用 strncpystrncat 等带长度限制的版本。

  • 在模拟实现中加入 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 下编译运行。
如有疑问,欢迎留言讨论!

Logo

一站式 AI 云服务平台

更多推荐