系列文章目录

学习系列文章:
【初识C语言】选择结构(if语句和switch语句)详细解答
【初识C语言】循环结构(while语句、do…while语句和for语句)详细解答
【初识C语言】C语言指针从入门到进阶详细解答

理解函数文章:
【初识C语言】qsort 函数保姆级教程,搞定各种数据类型的排序

实战项目文章:
【初识C语言】经典扫雷C语言实战(原码+解析),看完就能上手拆解与修改



前言

在 C 语言编程中,字符操作、字符串处理和内存管理是核心基础。C 语言标准库提供了一系列专门的函数来简化这些操作,本文系统整理了字符与字符串函数内存函数的功能、使用场景、模拟实现及常见易错点,希望对大家有帮助。


一、字符与字符串函数(ctype.h + string.h)

字符串函数是 C 语言操作文本数据的核心工具,需包含头文件 <string.h>;字符分类与转换函数需包含 <ctype.h>

1. 字符分类与转换函数

这类函数主要用于判断字符类型(如是否为数字、字母)或转换大小写,参数均为int类型(实际传入字符的 ASCII 值),返回非 0 表示 “真”,0 表示 “假”。

函数 功能描述
isdigit(c) 判断字符c是否为十进制数字('0'-'9')
islower(c) 判断字符c是否为小写字母('a'-'z')
isupper(c) 判断字符c是否为大写字母('A'-'Z')
isalpha(c) 判断字符c是否为字母(大小写均可)
isspace(c) 判断字符c是否为空白字符(空格、\t、\n、\r 等)
toupper(c) 将小写字母转换为大写,非小写字母返回原字符
tolower(c) 将大写字母转换为小写,非大写字母返回原字符

代码示例:字符串小写转大写

#include <stdio.h>
#include <ctype.h> // 必须包含此头文件

int main() {
    char str[] = "Test String 123!\n";
    int i = 0;
    while (str[i]) { // 遍历字符串直到'\0'
        if (islower(str[i])) { // 判断是否为小写字母
            str[i] = toupper(str[i]); // 转换为大写
        }
        putchar(str[i]);
        i++;
    }
    // 输出:TEST STRING 123!
    return 0;
}

2. 基础字符串函数

(1)strlen:字符串长度统计

  • 功能:统计字符串中'\0'之前的字符个数(不包含’\0’)。
  • 返回值size_t(无符号整数)

代码示例:

#include <stdio.h>
#include <string.h>

int main() {
    const char* str1 = "abcdef";
    const char* str2 = "bbb";
    // 注意:size_t是无符号,strlen(str2)-strlen(str1)结果仍为无符号(正数)
    if (strlen(str1) > strlen(str2)) {
        printf("str1更长\n");
    }
    return 0;
}

模拟实现(计数器法)

定义一个计数器变量,遍历字符串,每遇到一个非'\0'的字符,计数器加 1,直到遇到’\0’停止,最终返回计数器的值。

  • 优点:直观,容易理解,效率高(时间复杂度 O (n),空间复杂度 O (1))
  • 缺点:需要额外定义计数器变量
#include <assert.h>
// 方式1:计数器法(最直观)
size_t my_strlen(const char* str) {
    assert(str != NULL); // 断言指针非空
    size_t count = 0;
    while (*str) { // 等价于*str != '\0'
        count++;
        str++;
    }
    return count;
}

模拟实现(递归法)

利用递归思想:若当前字符是'\0',返回 0;否则返回1 + 下一个字符的strlen结果(递归调用自身)。

  • 优点:代码简洁,不需要使用临时变量
  • 缺点栈溢出风险,效率低

栈溢出风险:递归深度等于字符串长度,若字符串过长(如长度 > 10000),会超出栈空间(默认栈大小通常为几 MB,支持递归深度约几千层),触发段错误

#include <assert.h>
// 方式2:递归法(无临时变量)
size_t my_strlen(const char* str) {
    assert(str != NULL); // 断言指针非空
    // 递归终止条件:遇到'\0',长度为0
    if (*str == '\0') {
        return 0;
    }
    // 递归调用:当前字符长度1 + 后续字符串的长度
    return 1 + my_strlen(str + 1);
}

模拟实现(指针减指针法)

利用 C 语言 “指针相减结果为元素个数” 的特性:定义一个指针保存字符串起始地址,另一个指针遍历到'\0',最终两个指针的差值即为字符串长度。

  • 优点:代码简洁,无需额外计数器变量,效率高(时间复杂度 O (n),空间复杂度 O (1))
  • 缺点:对新手不容易理解
#include <assert.h>
// 方式3:指针减指针法(高效)
size_t my_strlen(const char* str) {
    assert(str != NULL); // 断言指针非空
    const char* start = str; // 保存字符串起始地址
    // 遍历到'\0'
    while (*str != '\0') {
        str++;
    }
    // 指针相减:结束地址 - 起始地址 = 字符个数
    return str - start;
}

(2)strcpy:字符串拷贝

  • 功能:将源字符串(含'\0')拷贝到目标空间,直到遇到'\0'停止。
  • 注意事项
    1. 源字符串必须以'\0'结束;
    1. 目标空间足够大且可修改;
    1. 会拷贝'\0'到目标空间。

模拟实现:

#include <assert.h>
char* my_strcpy(char* dest, const char* src) {
    assert(dest && src); // 双断言,确保指针非空
    char* ret = dest; // 记录目标空间起始地址,用于返回
    // 循环拷贝:*dest++ = *src++ 等价于先赋值再自增
    while ((*dest++ = *src++)) {
        ; // 空语句,逻辑在条件中完成
    }
    return ret; // 返回目标地址
}

(3)strcat:字符串追加

  • 功能:将源字符串追加到目标字符串末尾,从目标字符串的’\0’位置开始。
  • 注意事项
    1. 目标字符串需包含'\0'(否则无法确定追加起始位置);
    1. 目标空间需足够大;
    1. 源字符串必须以'\0'结束。

模拟实现:

#include <assert.h>
char* my_strcat(char* dest, const char* src) {
    assert(dest && src);
    char* ret = dest;
    // 1. 找到目标字符串的'\0'
    while (*dest) {
        dest++;
    }
    // 2. 拷贝源字符串(含'\0')
    while ((*dest++ = *src++)) {
        ;
    }
    return ret;
}

(4)strcmp:字符串比较

  • 功能:逐字符比较 ASCII 值,直到遇到不同字符或'\0'
  • 返回值
    1. 0:str1 > str2;
    1. =0:str1 == str2;
    1. <0:str1 < str2。

模拟实现:

#include <assert.h>
int my_strcmp(const char* str1, const char* str2) {
    assert(str1 && str2);
    // 逐字符比较,直到不同或遇到'\0'
    while (*str1 == *str2) {
        if (*str1 == '\0') {
            return 0; // 同时到'\0',相等
        }
        str1++;
        str2++;
    }
    // 返回ASCII差值(标准推荐用unsigned char避免负数异常)
    return (unsigned char)*str1 - (unsigned char)*str2;
}

3. 安全字符串函数:规避越界风险

基础函数(如strcpy)无长度限制,易导致内存越界,因此有了如strncpy的安全版本。

(1)strncpy:指定长度拷贝

  • 功能:最多拷贝num个字符到目标空间。
  • 关键区别
    1. 源字符串长度 < num:拷贝完源字符串(含'\0')后,剩余位置补'\0'
    1. 源字符串长度 ≥ num:仅拷贝num个字符,不追加'\0'

模拟实现:

#include <assert.h>
char* my_strncpy(char* dest, const char* src, size_t num) {
    assert(dest && src);
    char* ret = dest;
    // 第一步:拷贝源字符(直到源结束或num用完)
    while (num > 0 && *src != '\0') {
        *dest++ = *src++;
        num--;
    }
    // 第二步:剩余位置补'\0'
    while (num > 0) {
        *dest++ = '\0';
        num--;
    }
    return ret;
}

(2)strncat:指定长度追加

  • 功能:最多追加num个字符,追加后自动补'\0'(区别于strncpy)。

模拟实现:

#include <assert.h>
char* my_strncat(char* dest, const char* src, size_t num) {
    assert(dest && src);
    char* ret = dest;
    // 找到目标字符串的'\0'
    while (*dest) {
        dest++;
    }
    // 追加num个字符或直到源结束
    while (num > 0 && *src != '\0') {
        *dest++ = *src++;
        num--;
    }
    *dest = '\0'; // 强制补结束符
    return ret;
}

(3)strncmp:指定长度比较

  • 功能:最多比较num个字符,其余规则同strcmp

模拟实现:

#include <assert.h>
int my_strncmp(const char* str1, const char* str2, size_t num) {
    assert(str1 && str2);
    while (num > 0) {
        if (*str1 != *str2) {
            return (unsigned char)*str1 - (unsigned char)*str2;
        }
        if (*str1 == '\0') {
            return 0; // 同时结束
        }
        str1++;
        str2++;
        num--;
    }
    return 0; // 前num个字符相等
}

4. 实用工具函数:字符串查找与错误处理

(1)strstr:子字符串查找

  • 功能:在str1中查找str2第一次出现的位置,找到返回起始指针,否则返回NULL。

模拟实现(暴力查找):

#include <assert.h>
char* my_strstr(const char* str1, const char* str2) {
    assert(str1 && str2);
    // 特殊情况:str2为空字符串,直接返回str1
    if (*str2 == '\0') {
        return (char*)str1;
    }
    const char* cp = str1; // 记录str1的当前起始位置
    while (*cp) {
        const char* s1 = cp;
        const char* s2 = str2;
        // 匹配连续字符
        while (*s1 && *s2 && *s1 == *s2) {
            s1++;
            s2++;
        }
        if (*s2 == '\0') {
            return (char*)cp; // 找到,返回起始位置
        }
        cp++;
    }
    return NULL; // 未找到
}

(2)strtok:字符串分割

  • 功能:按分隔符拆分字符串,会修改原字符串(分隔符替换为'\0')。
  • 使用规则
    1. 首次调用:传入待分割字符串和分隔符;
    1. 后续调用:传入NULL和相同分隔符,继续分割;
    1. 连续分隔符视为单个。

代码示例:

#include <stdio.h>
#include <string.h>

int main() {
    char arr[] = "192.168.6.123";
    const char* sep = ".";
    char buf[30];
    strcpy(buf, arr); // 拷贝原字符串,避免修改原数据
    // 循环分割
    for (char* str = strtok(buf, sep); str != NULL; str = strtok(NULL, sep)) {
        printf("%s\n", str);
    }
    // 输出:192 → 168 → 6 → 123
    return 0;
}

(3)strerror:错误信息转换

  • 功能:将错误码(如errno)转换为可读的错误信息字符串。

代码示例:

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    // 尝试打开不存在的文件
    FILE* pFile = fopen("test.txt", "r");
    if (pFile == NULL) {
        // errno是全局错误码变量,记录最近的错误
        printf("错误信息:%s\n", strerror(errno));
        // 输出:No such file or directory
        return 1;
    }
    fclose(pFile);
    return 0;
}

二、内存操作函数(直接操控内存)

内存函数不关心数据类型,按字节操作,适用于任意数据(int、结构体等),需包含 <string.h>头文件。

1. memcpy:内存块拷贝(不处理重叠内存块)

  • 功能:从源地址拷贝num个字节到目标地址。
  • 注意:源和目标内存重叠时,结果就是未定义。

模拟实现:

#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t num) {
    assert(dest && src);
    void* ret = dest;
    // 强制转换为char*,按字节拷贝
    while (num--) {
        *(char*)dest = *(char*)src;
        dest = (char*)dest + 1;
        src = (char*)src + 1;
    }
    return ret;
}

2. memmove:内存块拷贝(处理重叠内存块)

  • 功能:与memcpy一致,但支持源和目标内存重叠(核心区别)。
  • 实现逻辑
    1. 目标地址 < 源地址:从前向后拷贝;
    1. 目标地址 > 源地址:从后向前拷贝。

模拟实现:

#include <assert.h>
void* my_memmove(void* dest, const void* src, size_t num) {
    assert(dest && src);
    void* ret = dest;
    char* dst = (char*)dest;
    const char* s = (const char*)src;

    if (dst < s) {
        // 前→后拷贝(无重叠或不影响)
        while (num--) {
            *dst++ = *s++;
        }
    } else {
        // 后→前拷贝(避免重叠覆盖源数据)
        dst += num - 1;
        s += num - 1;
        while (num--) {
            *dst-- = *s--;
        }
    }
    return ret;
}

3. memset:内存块设置

  • 功能:将num个字节的内存空间设置为指定值(按字节设置)。
  • 注意:设置的是字节值,非任意整数(如给 int 数组设 1 会出错)。

代码示例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "hello world";
    memset(str, 'x', 6); // 前6个字节设为'x'
    printf("%s\n", str); // 输出:xxxxxxworld

    int arr[5] = {1,2,3,4,5};
    memset(arr, 0, sizeof(arr)); // 数组所有字节设为0(正确结果)
    // memset(arr, 1, sizeof(arr)); // 错误:每个字节设为1,int值为0x01010101=16843009
    return 0;
}

4. memcmp:内存块比较

  • 功能:按字节比较num个字节的内存内容,返回值规则同strcmp。
  • 区别:strcmp到’\0’停止,memcmp严格比较num个字节。

代码示例:

#include <stdio.h>
#include <string.h>

int main() {
    char buf1[] = "DWgaOtP12df0";
    char buf2[] = "DWGAOTP12DF0";
    int ret = memcmp(buf1, buf2, sizeof(buf1));
    if (ret > 0) {
        printf("%s > %s\n", buf1, buf2);
    } else if (ret < 0) {
        printf("%s < %s\n", buf1, buf2); // 输出(小写ASCII值大于大写)
    } else {
        printf("相等\n");
    }
    return 0;
}

三、易混淆函数对比

函数对比 核心区别 适用场景
strcpy vs strncpy 前者无长度限制,后者指定num,需补'\0' 安全场景用strncpy
strcat vs strncat 前者无长度限制,后者指定num,自动补'\0' 安全场景用strncat
memcpy vs memmove 前者不处理重叠,后者处理重叠 内存可能重叠用memmove
strcmp vs memcmp 前者按字符串(到'\0'),后者按字节 非字符串数据用memcmp

总结

熟练使用库函数能够让我们解决问题时事半功倍,希望这篇文章对大家有所帮助。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐