文章目录


C++中可以使用下文介绍的大多数C标准库函数,函数功能和基本使用规则通常不变。 C++推荐将C风格标准头文件改为对应的C++头文件,例如 <ctype.h><string.h>改为<cctype><cstring>,并通过std::命名空间调用其中的函数 ,如std::isalpha()std::strlen()但这不是可以对所有内容机械套用的规则:宏、POSIX函数和非标准函数不一定使用std::,其头文件也不一定能通过去掉.h并添加c前缀得到;部分C代码还可能因C++更严格的类型检查而需要调整。

一. 字符分类函数

C语言提供了丰富的字符分类函数,这些函数都位于<ctype.h>头文件中,用于判断字符的类型。

1.1 字符分类函数概述。

这些函数的参数类型为int【是为了能表示EOF和所有可表示为unsigned char的字符值】,返回值为非0(表示条件满足)或0(表示条件不满足)。

函数名 说明 返回非0的条件
int isalpha(int c) 检查是否为字母 a~z 或 A~Z
int isalnum(int c) 检查是否为字母或数字 a~z, A~Z, 0~9
int isdigit(int c) 检查是否为十进制数字 0~9
int isxdigit(int c) 检查是否为十六进制数字 0~9, a~f, A~F
int islower(int c) 检查是否为小写字母 a~z
int isupper(int c) 检查是否为大写字母 A~Z
int isspace(int c) 检查是否为空白字符 空格、换页、换行、回车、水平制表符、垂直制表符
int ispunct(int c) 检查是否为标点符号 当前区域设置中除字母、数字和空格外的可打印字符
int isprint(int c) 检查是否为可打印字符 当前区域设置中的可打印字符(包括空格,不包括制表符、换行符等控制字符)
int isgraph(int c) 检查是否为图形字符 当前区域设置中的可打印字符(不包括空格)
int iscntrl(int c) 检查是否为控制字符 控制字符

上表中的字母和数字范围按默认的C区域设置描述;切换区域设置后,部分分类结果可能不同。

1.2 函数使用示例

字符分类基本示例

#include <stdio.h>
#include <ctype.h>
int main() {
    char c = 'A';
    
    printf("字符 '%c' 是否为字母: %d\n", c, isalpha(c));
    printf("字符 '%c' 是否为大写字母: %d\n", c, isupper(c));
    printf("字符 '%c' 是否为小写字母: %d\n", c, islower(c));
    printf("字符 '%c' 是否为数字: %d\n", c, isdigit(c));
    printf("字符 '%c' 是否为空白字符: %d\n", c, isspace(c)); 
    return 0;
}

1.3 字符分类函数的实现原理

这些函数的具体实现由C标准库决定,通常会结合当前区域设置进行判断,并不一定只是通过ASCII码范围判断。
下面的代码仅模拟C区域设置下英文字母和数字的部分判断:



// 自定义实现islower函数
int my_islower(int c) {
    return (c >= 'a' && c <= 'z');
}

// 自定义实现isdigit函数
int my_isdigit(int c) {
    return (c >= '0' && c <= '9');
}

// 自定义实现isupper函数
int my_isupper(int c) {
    return (c >= 'A' && c <= 'Z');
}

1.4 注意事项

  • 参数类型:这些函数的参数是int类型,但实际使用时应传入unsigned char或EOF。传入其他值可能导致未定义行为。
    如:
char c = (char)0x80; // 如果char为有符号类型,c可能是负值
if (islower(c)) { ... } // 如果c既不是EOF,也不能表示为unsigned char,则行为未定义

正确的做法:

char c = (char)0x80;
if (islower((unsigned char)c)) { ... }

通过先将c转换为unsigned char,再由整数提升转换为int,可以确保传给函数的值能够表示为unsigned char。

  • 区分大小写:C语言的字符分类函数是区分大小写的,例如’a’和’A’会被视为不同的字符。
  • 区域设置:这些函数的分类结果受当前区域设置影响;在默认的C区域设置中,英文字母和数字的行为与常见ASCII规则一致。
  • 字符串处理:在处理字符串时,通常需要配合strlen()函数(需要包含<string.h>)来获取字符串长度。

二.字符转换函数

C语言提供了两个主要的字符转换函数,用于在大小写字母之间进行转换。这些函数都位于<ctype.h>头文件中,是C语言字符处理的重要工具。

2.1 字符转换函数概述

函数 说明 参数类型 返回值
int tolower(int c) 将大写字母转换为小写字母 int 转换后的字符(如果c不是大写字母,则返回c本身)
int toupper(int c) 将小写字母转换为大写字母 int 转换后的字符(如果c不是小写字母,则返回c本身)

2.2 函数详细说明

  1. tolower(int c)功能:将大写字母转换为小写字母,若参数不是大写字母,则返回原值。
    使用示例:
#include <stdio.h>
#include <ctype.h>

int main() {
    char c = 'A';
    printf("大写字母 '%c' 转换为小写: '%c'\n", c, tolower(c));
    
    c = 'a';
    printf("小写字母 '%c' 转换为小写: '%c'\n", c, tolower(c));
    
    c = '1';
    printf("数字 '%c' 转换为小写: '%c'\n", c, tolower(c));
    
    return 0;
}
输出:
大写字母 'A' 转换为小写: 'a'
小写字母 'a' 转换为小写: 'a'
数字 '1' 转换为小写: '1'
  1. toupper(int c)功能:将小写字母转换为大写字母,若参数不是小写字母,则返回原值。
    使用示例:
#include <stdio.h>
#include <ctype.h>

int main() {
    char c = 'a';
    printf("小写字母 '%c' 转换为大写: '%c'\n", c, toupper(c));
    
    c = 'A';
    printf("大写字母 '%c' 转换为大写: '%c'\n", c, toupper(c));
    
    c = '1';
    printf("数字 '%c' 转换为大写: '%c'\n", c, toupper(c));
    
    return 0;
}
输出:
小写字母 'a' 转换为大写: 'A'
大写字母 'A' 转换为大写: 'A'
数字 '1' 转换为大写: '1'

2.3 使用注意事项

参数类型:这些函数的参数是int类型,但实际使用时应该传入unsigned char或EOF。传入其他值可能导致未定义行为。
正确用法:

char c = 'A';
printf("%c", tolower((unsigned char)c));

错误用法:

char c = (char)0x80; // 如果char为有符号类型,c可能是负值
printf("%c", tolower(c)); // 如果c既不是EOF,也不能表示为unsigned char,可能导致未定义行为

2.4 与ASCII码的关系

在默认的C区域设置且使用ASCII兼容编码时:
大写字母A-Z的编码范围是65-90(0x41-0x5A)
小写字母a-z的编码范围是97-122(0x61-0x7A)
这两个范围之间相差32(0x20)。但标准库中的tolower和toupper受区域设置影响,不能普遍理解为简单地加32或减32。

仅适用于ASCII兼容编码中英文字母的简化实现:

// 自定义tolower函数
int my_tolower(int c) {
    if (c >= 'A' && c <= 'Z') {
        return c + 32;
    }
    return c;
}

// 自定义toupper函数
int my_toupper(int c) {
    if (c >= 'a' && c <= 'z') {
        return c - 32;
    }
    return c;
}

三. strlen的使用和模拟实现

3.1 strlen函数基本介绍

  • 函数原型:size_t strlen(const char *str);
  • 头文件:<string.h>
  • 功能:计算字符串的长度(不包括终止符\0),直到遇到第一个\0字符为止。
  • 返回值:size_t类型(无符号整数类型,表示字符串长度)

3.2 strlen函数的使用

  1. 基本用法
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    size_t length = strlen(str);
    printf("The length of the string \"%s\" is %zu.\n", str, length);
    return 0;
}
输出:
The length of the string "Hello, World!" is 13.
  1. 使用注意事项
  • 必须以\0结尾:参数所指向的字符串必须以\0结束,否则strlen函数的行为是未定义的(会一直遍历直到找到\0)。
  • 不包含\0:strlen返回的长度不包括字符串末尾的\0。
  • 无符号整数:返回值是size_t类型(无符号整数),使用时需注意比较时的陷阱。
  • 指针有效性:传入的指针必须指向有效的字符串。
  • 头文件:使用strlen前必须包含<string.h>头文件。
  1. 常见错误示例
#include <stdio.h>
#include <string.h>

int main() {
    char str[4] = {'a', 'b', 'c', 'd'}; // 没有'\0'结尾
    printf("%zu\n", strlen(str)); // 未定义行为,可能输出4或更长
    return 0;
}

3.3 strlen函数的模拟实现

1. 计数器法(循环实现)
#include <assert.h>
#include <stddef.h>
size_t my_strlen(const char *str) {
    assert(str);  // 确保指针不为空
    size_t count = 0;
    while (*str != '\0') {
        count++;
        str++;
    }
    return count;
}
原理:遍历字符串,直到遇到\0,计数器记录字符个数。


2. 递归实现
#include <assert.h>
size_t my_strlen(const char *str) {
    assert(str);
    if (*str == '\0')
        return 0;
    else
        return 1 + my_strlen(str + 1);
}
原理:递归地将字符串长度分解为1(当前字符)+剩余字符串的长度。


3. 指针减指针法
#include <assert.h>
#include <stddef.h> // 定义size_t和ptrdiff_t

size_t my_strlen(const char *str) {
    assert(str);
    const char *p = str;
    while (*str) { // 等价于 *str != '\0'
        str++;
    }
    return (size_t)(str - p); // 显式转换避免警告
}
原理:通过指针运算,计算字符串首地址和终止符\0地址之间的差值。

3.4 strlen与sizeof的区别

特性 strlen sizeof
计算内容 字符串长度(不包括\0) 对象或类型占用的字节数;对数组为整个数组的大小
返回值 字符个数 字节数
使用头文件 <string.h> 无需头文件
示例 strlen(“Hello”) → 5 sizeof(“Hello”) → 6

示例:

#include <stdio.h>
#include <string.h>
int main() {
    char str[20] = "Hello";
    size_t len = strlen(str);       len = 5
    size_t size = sizeof(str);      size = 20
    printf("strlen: %zu, sizeof: %zu\n", len, size);
    return 0;
}
输出:
strlen: 5, sizeof: 20

3.5 易错点分析

1.无符号整数比较陷阱

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

int main() {
    char str[] = "abc";
    if (strlen(str) - 4 < 0) {
        printf("This will print!\n");
    }
    return 0;
}

原因:strlen(str)返回size_t无符号整数。这里strlen(str)为3,3-4会按无符号运算转换为一个很大的正数,所以比较结果为假。
正确写法:

if (strlen(str) < 4) {
    printf("This will print!\n");
}

2.未以\0结尾的字符串



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

int main() {
    char str[5] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0'结尾
    printf("%zu\n", strlen(str)); // 未定义行为,可能输出5或更长
    return 0;
}

3.6 strlen函数的内部实现原理

strlen函数的内部实现原理是遍历字符串,直到遇到\0,同时计数。这个过程是线性的,时间复杂度为O(n)。在实际C库实现中,strlen可能会使用一些优化技术(如利用CPU的字长优势),但基本原理与模拟实现相同。

strlen 的核心机制就是:给我一个内存地址(指针),我从这个地址开始往后数,直到遇到 \0 为止。所以在函数内部,哪怕数组已经退化成了指针(形参),strlen 依然能通过那个指针找到后面跟着的所有字符,直到结尾。

四. strcpy 的使用和模拟实现

4.1 strcpy函数概述

strcpy是C语言中用于字符串复制的标准库函数,定义在<string.h>头文件中。它的主要功能是将源字符串(包括终止符\0)复制到目标字符串空间中。

  • 函数原型:char *strcpy(char *dest, const char *src);
  • 头文件:<string.h>
  • 参数说明:
    dest:目标字符串数组,用于存储复制后的字符串(必须可修改)
    src:源字符串,即要被复制的字符串(必须以\0结尾
  • 返回值: 返回指向目标字符串dest的指针,方便链式调用。

4.2 strcpy函数的使用

1.基本用法示例

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

int main() {
    char source[] = "Hello, world!";
    char destination[20];
    
    // 使用strcpy函数将source复制到destination
    strcpy(destination, source);
    
    printf("复制后的字符串: %s\n", destination);
    
    return 0;
}
输出:
复制后的字符串: Hello, world!

2.使用注意事项

  • 源字符串必须以\0结尾:strcpy会一直复制直到遇到\0,如果源字符串没有\0,会导致未定义行为。
  • 目标空间必须足够大:目标数组必须能容纳源字符串(包括\0),否则会导致缓冲区溢出。
    char dest[5];
    strcpy(dest, “Hello”); // 错误!目标空间不足
  • 目标空间必须可修改:不能将字符串常量直接复制到字符串常量。
    char *dest = “Hello”; // 字符串常量
    strcpy(dest, “World”); // 错误!不能修改字符串常量
  • 避免重叠:如果源和目标地址重叠,行为未定义(应使用memmove处理重叠)。

解引用运算符 * 属于一元运算符,优先级比较高,但低于后缀运算符,例如 []、()、.、->、后置 ++。
后缀运算符 > 一元运算符 > 乘除 > 加减

4.3 strcpy函数的模拟实现

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

// 模拟实现strcpy函数
char *my_strcpy(char *dest, const char *src) {
    char *ret = dest;
    assert(dest != NULL);
    assert(src != NULL);
    
    while ((*dest++ = *src++) != '\0');
    
    return ret;
}

int main() {
    char source[] = "This is a test string";
    char dest[50];
    
    // 使用标准strcpy
    strcpy(dest, source);
    printf("Standard strcpy result: %s\n", dest);
    
    // 重置目标数组
    for (int i = 0; i < 50; i++) dest[i] = ' ';
    
    // 使用自定义my_strcpy
    my_strcpy(dest, source);
    printf("Custom my_strcpy result: %s\n", dest);
    
    return 0;
}

1.标准模拟实现

#include <assert.h>
char *my_strcpy(char *dest, const char *src) {
    char *ret = dest; // 保存目标字符串的起始地址
    assert(dest != NULL); // 确保目标指针不为空
    assert(src != NULL);  // 确保源指针不为空
    
    while ((*dest++ = *src++) != '\0'); // 复制字符,包括'\0'
    
    return ret; // 返回目标字符串的起始地址
}
/*
参数检查:使用assert确保指针不为空,避免空指针解引用。
保存起始地址:char *ret = dest; 保存目标字符串的起始地址,因为dest在复制过程中会移动。
核心复制逻辑:while ((*dest++ = *src++) != '\0')
先将src指向的字符赋值给dest指向的位置,然后dest和src指针都自增
判断赋值后的字符是否为\0,如果是则停止循环
返回值:返回ret,即目标字符串的起始地址,与标准库函数一致。

*/

2.简洁实现

char *my_strcpy(char *dest, const char *src) {
    char *ret = dest;
    while ((*dest++ = *src++) != '\0');
    return ret;
}

3.包含空指针检查的实现

char *my_strcpy(char *dest, const char *src) {
    if (dest == NULL || src == NULL) {
        return NULL;
    }
    
    char *ret = dest;
    while ((*dest++ = *src++) != '\0');
    return ret;
}

4.4 strcpy与strlen的区别

函数 功能 返回值 说明
strlen 计算字符串长度(不包括\0) size_t 不复制字符串,只计算长度
strcpy 复制字符串(包括\0) char* 实际复制字符串内容

4.5 常见错误与解决

错误1:目标空间不足
char dest[5];
strcpy(dest, “Hello”); // 缓冲区溢出!
解决:确保目标空间足够大
char dest[6]; // 必须包含终止符’\0’
strcpy(dest, “Hello”);

错误2:未以\0结尾的源字符串
char src[5] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’};
strcpy(dest, src); // 未以’\0’结尾,导致未定义行为
解决:确保源字符串以\0结尾
char src[6] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’};
strcpy(dest, src);

错误3:重叠内存复制
char str[20] = “Hello, World!”;
strcpy(str + 2, str); // 源和目标重叠,行为未定义
解决:使用memmove处理重叠内存,并确保目标空间足够
memmove(str + 2, str, strlen(str) + 1);

4.6 为什么需要复制\0?

strcpy不仅复制字符串内容,还会复制终止符\0。这是因为:C语言中字符串的结束标志是\0,没有\0的字符串不是有效的C字符串。复制\0后,目标字符串才能被其他字符串函数(如strlen、printf)正确处理。

五 . strcat 的使用和模拟实现

5.1 strcat函数基本介绍

  • 函数原型:char *strcat(char *destination, const char *source);
  • 头文件:<string.h>
  • 功能:将源字符串(source)追加到目标字符串(destination)的末尾。
    (目标字符串的终止符\0将被源字符串的第一个字符替换,源字符串的内容将被复制到目标字符串末尾,连接后的新字符串在末尾添加新的终止符\0)
  • 返回值:返回指向目标字符串destination的指针,方便链式调用。

5.2 strcat函数的使用

1.基本用法示例

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

int main() {
    char dest[50] = "Hello, ";
    char src[] = "world!";
    
    // 使用strcat函数连接字符串
    strcat(dest, src);
    
    printf("连接后的字符串: %s\n", dest);
    
    return 0;
}
输出:
连接后的字符串: Hello, world!

2.实际应用示例

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

int main() {
    char path[100] = "/home/user/";
    char filename[] = "document.txt";
    
    // 连接路径和文件名
    strcat(path, filename);
    
    printf("Full path: %s\n", path);
    
    return 0;
}
输出:
Full path: /home/user/document.txt

5.3 strcat函数的使用注意事项

1.必须满足的条件:
源字符串必须以\0结尾:
char src[5] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’}; // 没有’\0’结尾
strcat(dest, src); // 未定义行为
目标空间必须足够大:
char dest[6] = “Hello”; // 只能容纳原字符串和终止符,没有追加空间
char src[] = " World";
strcat(dest, src); // 缓冲区溢出!
目标空间必须可修改:
char *dest = “Hello”; // 字符串常量
char src[] = " World";
strcat(dest, src); // 错误!不能修改字符串常量
内存区域不能重叠:
char str[] = “Hello, World!”;
strcat(str + 2, str); // 源和目标重叠,行为未定义

2.常见错误示例

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

int main() {
    char dest[6] = "Hello"; // 目标空间不足,没有追加空间
    char src[] = " World";
    
    strcat(dest, src); // 缓冲区溢出!
    
    printf("%s\n", dest);
    return 0;
}
输出:可能导致程序崩溃或输出不正确

5.4strcat函数的模拟实现

1.标准模拟实现(清晰易懂)

#include <assert.h>

char *my_strcat(char *dest, const char *src) {
    char *ret = dest; // 保存目标字符串的起始地址
    
    // 确保指针不为空
    assert(dest != NULL);
    assert(src != NULL);
    
    // 移动dest指针到目标字符串末尾
    while (*dest != '\0') {
        dest++;
    }
    
    // 将源字符串复制到目标字符串末尾
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    
    // 在连接后的字符串末尾添加终止符
    *dest = '\0';
    
    return ret; // 返回目标字符串的起始地址
}

2.最简洁且严谨的实现



#include <assert.h>

char *my_strcat(char *destination, const char *source) {
    assert(destination != NULL);
    assert(source != NULL);
    
    char *ret = destination;
    
    // 找到目标字符串的'\0'
    while (*destination) {
        destination++;
    }
    
    // 追加源字符串
    while (*source) {
        *destination++ = *source++;
    }
    
    // 添加新的终止符
    *destination = '\0';
    
    return ret;
}

5.5 strcat与strcpy的区别

函数 功能 操作对象 结束符处理
strcpy 复制字符串 源字符串复制到目标字符串 复制源字符串包括\0
strcat 连接字符串 源字符串追加到目标字符串末尾 目标字符串的\0被覆盖,添加新的\0

5.6strcat的常见错误分析

  1. 目标空间不足
    char dest[5] = “Hi”;
    char src[] = " Hello";
    strcat(dest, src); // 目标空间不足,会导致缓冲区溢出
    解决方法:确保目标空间足够大
    char dest[20] = “Hi”; // 确保有足够空间
  2. 未以\0结尾的源字符串
    char src[5] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’}; // 没有’\0’结尾
    strcat(dest, src); // 未定义行为
    解决方法:确保源字符串以\0结尾
    char src[6] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\0’};
  3. 目标字符串为常量
    char *dest = “Hello”; // 字符串常量
    char src[] = " World";
    strcat(dest, src); // 错误!不能修改字符串常量
    解决方法:使用字符数组而非字符指针
    char dest[50] = “Hello”; // 字符数组

5.7 strcat的替代方案:strncat

  • 当需要限制最多追加的字符数量时,可以使用strncat函数,但它不会自动获知目标数组容量,也不能自动避免缓冲区溢出。

  • 安全建议:使用strncat前仍需根据目标数组容量、目标字符串当前长度和终止符所需空间,正确计算n。

六. strcmp 的使用和模拟实现

6.1strcmp函数概述

  • 函数原型:int strcmp(const char *str1, const char *str2);
  • 头文件:<string.h>
  • 功能:将字符按unsigned char解释后逐字符比较两个字符串,直到出现不同字符或遇到\0。
  • 返回值说明
    返回0:表示str1等于str2
    返回负数:表示str1小于str2
    返回正数:表示str1大于str2
  • 重要特性:
    区分大小写(例如,"Apple"和"apple"被视为不同字符串)
    在ASCII兼容编码中,‘a’的编码值大于’A’;一般情况下应只依赖返回值的正负,而不要依赖具体差值

6.2 strcmp函数的使用

1.基本用法示例



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

int main() {
    char str1[15] = "abcdef";
    char str2[15] = "ABCDEF";
    
    int ret = strcmp(str1, str2);
    
    if (ret < 0) {
        printf("str1 小于 str2\n");
    } else if (ret > 0) {
        printf("str1 大于 str2\n");
    } else {
        printf("str1 等于 str2\n");
    }
    
    return 0;
}
输出:
str1 大于 str2
解释:在常见的ASCII兼容编码中,字母'a'的编码值大于大写字母'A',所以"abcdef"大于"ABCDEF"

2.实际应用示例

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

int main() {
    char input[50];
    
    printf("请输入一个字符串: ");
    scanf("%49s", input);
    
    if (strcmp(input, "hello") == 0) {
        printf("您输入了'hello'\n");
    } else if (strcmp(input, "world") == 0) {
        printf("您输入了'world'\n");
    } else {
        printf("您输入的字符串是: %s\n", input);
    }
    
    return 0;
}

6.3 strcmp函数的模拟实现

1.标准模拟实现(清晰易懂)

#include <assert.h>

int my_strcmp(const char *str1, const char *str2) {
    // 确保指针不为空
    assert(str1 != NULL);
    assert(str2 != NULL);
    
    // 逐字符比较
    while (*str1 != '\0' && *str2 != '\0') {
        if (*str1 != *str2) {
            return (int)(*(const unsigned char *)str1 - *(const unsigned char *)str2);
        }
        str1++;
        str2++;
    }
    
    // 如果其中一个字符串提前结束
    return (int)(*(const unsigned char *)str1 - *(const unsigned char *)str2);
}

2.最简洁实现

#include <assert.h>

int my_strcmp(const char *str1, const char *str2) {
    assert(str1 != NULL);
    assert(str2 != NULL);
    
    while (*str1 == *str2++) {
        if (*str1++ == '\0') {
            return 0;
        }
    }
    
    return (int)(*(const unsigned char *)str1 - *(const unsigned char *)(--str2));
}

6.4strcmp函数的常见用法示例

比较字符串的大小关系
int result = strcmp(str1, str2);
if (result == 0) {
    printf("str1 == str2\n");
} else if (result < 0) {
    printf("str1 < str2\n");
} else {
    printf("str1 > str2\n");
}

6.5 strcmp函数的注意事项

  1. 必须以\0结尾的字符串
    strcmp函数依赖于字符串以\0结尾,如果字符串没有以\0结尾,会导致未定义行为。

  2. 空指针检查
    在使用strcmp前,应确保两个字符串指针都不为空,避免空指针解引用。

  3. 区分大小写
    strcmp是区分大小写的,如果需要不区分大小写的比较,需要先将字符串转换为相同大小写。

  4. 长度无关性
    strcmp只比较到第一个差异字符。例如,"abc"和"abcd"在比较第四个位置时,由’\0’与’d’确定大小关系。

  5. 使用返回值的正确方式
    比较时应使用== 0判断相等,而不是直接使用返回值,因为返回值可能是任意非零值。

七.strncpy 函数的使用

7.1strncpy函数概述

strncpy 是 C 标准库中用于定长复制和填充字符序列的函数。它会向目标区域写入恰好 n 个字符,因此不会自动检查目标空间,也不能自动防止缓冲区溢出。

  • 函数原型:char *strncpy(char *dest, const char *src, size_t n);
  • 头文件:#include <string.h>
  • 复制规则
    如果 src 的长度 < n:将 src 的所有字符(包括终止符 \0)复制到 dest,剩余空间用 \0 填充
    如果 src 的长度 ≥ n:只复制前 n 个字符到 dest,不会自动添加终止符 \0(这是最容易出错的地方)

7.2 关键注意事项

1. 终止符问题(最重要的注意事项)
当 src 长度 < n:strncpy 会自动在 dest 中添加终止符
当 src 长度 ≥ n:strncpy 不会自动添加终止符,dest 可能没有终止符
解决方法:如果目标数组容量为capacity且capacity大于0,可预留一个字符存放终止符

strncpy(dest, src, capacity - 1);
dest[capacity - 1] = '\0'; // 手动确保终止

2. 缓冲区大小
确保 dest 有足够的空间容纳 n 个字符
如果 dest 的大小小于 n,会发生缓冲区溢出,这是非常危险的
3. 与 strcpy 的区别

函数 复制行为 终止符处理
strcpy 复制整个字符串(包括 \0) 自动添加终止符
strncpy 复制指定长度 n 个字符 仅在 src 长度 < n 时自动添加终止符

7.3 正确使用示例

示例 1:安全复制字符串

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

int main() {
    char src[] = "Hello, World!";
    char dest[10]; // 目标缓冲区大小为10

    // 复制最多9个字符(留1位给'\0')
    strncpy(dest, src, sizeof(dest) - 1);
    dest[sizeof(dest) - 1] = '\0'; // 手动确保字符串终止

    printf("Copied string: %s\n", dest); // 输出 "Hello, Wo"
    return 0;
}

示例 2:使用 snprintf 作为更安全的替代

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

int main() {
    char src[] = "Hello, World!";
    char dest[10];
    
    // 更安全的替代方案:snprintf 自动处理终止符
    snprintf(dest, sizeof(dest), "%s", src);
    
    printf("Copied string: %s\n", dest); // 输出 "Hello, Wo"
    return 0;
}

7.4 常见错误示例

错误 1:忽略终止符
char src[] = “Hello, World!”;
char dest[10];
strncpy(dest, src, 9); // 只复制9个字符
// 没有添加终止符!dest 可能没有终止符
printf(“%s\n”, dest); // 可能导致未定义行为(崩溃、乱码等)
错误 2:缓冲区大小不足
char src[] = “Hello, World!”;
char dest[5]; // 缓冲区太小
strncpy(dest, src, 10); // 试图复制10个字符,但 dest 只有5个空间
// 会导致缓冲区溢出

7.5 总结

  • strncpy 的核心用途:定长复制并在源字符串较短时用\0填充目标区域;它不会自动防止缓冲区溢出

  • 关键注意事项:必须手动确保字符串以 \0 结尾,特别是在 src 长度 ≥ n 的情况下

  • 推荐用法(已知dest为数组、容量为capacity且capacity大于0):
    strncpy(dest, src, capacity - 1);
    dest[capacity - 1] = ‘\0’; // 确保字符串终止

  • 更安全的替代:使用 snprintf 或 strlcpy(如果可用)

     记住:strncpy 不是自动安全的字符串复制函数。开发者必须同时保证目标空间足够,并正确处理终止符;使用不当同样可能导致未定义行为。
    

八. strncat 函数的使用

8.1函数概述

  • 函数原型:char *strncat(char *dest, const char *src, size_t n);
  • 头文件:#include <string.h>
  • 功能说明 :strncat 是 C 语言标准库中用于字符串追加的函数,它将源字符串 src 的前 n 个字符追加到目标字符串 dest 的末尾。它不会检查目标数组容量,因此调用者仍需确保空间足够。
  • 工作原理 :
    寻找目标字符串末尾:函数首先找到 dest 字符串的终止符 \0 位置
    追加字符:从 src 字符串的开头开始,追加最多 n 个字符到 dest 末尾
    添加终止符:无论追加了多少字符,函数都会在新字符串末尾自动添加终止符 \0
  • 参数详解
    dest 目标字符串的指针,必须有足够的空间容纳追加后的字符串(包括终止符 \0)
    src 源字符串的指针,要追加的内容
    n 要追加的最大字符数(不包括终止符 \0)

8.2 关键注意事项

  1. 目标缓冲区大小(最重要!)
    必须确保 dest 有足够的空间:包括:dest 原有内容长度,要追加的字符数(最多 n 个),1 个字节用于终止符 \0
    错误示例:
    char dest[5] = “Hi”;
    strncat(dest, “World”, 5); // 会导致缓冲区溢出!
  2. 追加长度处理
    如果 src 长度 < n:追加整个 src,并在末尾添加 \0
    如果 src 长度 ≥ n:只追加前 n 个字符,并在末尾添加 \0
  3. 内存重叠问题
    dest 和 src 指向的内存区域不应重叠
    如果重叠,结果是未定义的

8.3 正确使用示例

示例 1:基本用法

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

int main() {
    char dest[50] = "Hello, ";
    char src[] = "world!";
    size_t n = 5; // 追加最多5个字符
    
    strncat(dest, src, n);
    printf("%s\n", dest); // 输出: Hello, world
    return 0;
}

示例 2:安全使用(推荐)

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

int main() {
    char dest[50] = "Hello, ";
    char src[] = "world!";
    
    // 计算可用空间(减去已有的内容和终止符)
    size_t space_left = sizeof(dest) - strlen(dest) - 1;
    
    if (space_left > 0) {
        // 确保不会超过可用空间
        strncat(dest, src, (space_left < strlen(src)) ? space_left : strlen(src));
    }
    
    printf("%s\n", dest); // 输出: Hello, world!
    return 0;
}

示例 3:使用计算空间的方法

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

int main() {
    char dest[50] = "Hello";
    const char src[] = "World!";
    
    // 计算可用空间
    size_t space_left = sizeof(dest) - strlen(dest) - 1;
    
    if (space_left > 0) {
        strncat(dest, src, space_left);
    }
    
    printf("%s\n", dest); // 输出: HelloWorld!
    return 0;
}

8.4 常见错误与解决方法

错误 1:目标缓冲区空间不足
char dest[5] = “Hi”;
strncat(dest, “Hello”, 5); // 错误!dest 只有 5 字节空间,但需要 8 字节
解决方法:确保目标缓冲区足够大
char dest[10] = “Hi”; // 足够容纳"HiHello\0"
strncat(dest, “Hello”, 5); // 安全
错误 2:忘记处理终止符
char dest[10];
strcpy(dest, “Hello”);
strncat(dest, “World”, 5); // 需要11字节,会发生缓冲区溢出
解决方法:strncat 会自动添加终止符,但必须为追加内容和终止符预留足够空间
错误 3:使用错误的 n 值
char dest[10] = “Hi”;
strncat(dest, “HelloWorld”, 100); // 源字符串较长且n超过剩余空间,会导致缓冲区溢出
解决方法:使用 space_left 计算可用空间

8.5 strncat 与 strcat 的对比

特性 strncat strcat
长度限制 有(n 参数)
安全性 可限制追加数量,但仍需正确计算剩余空间 不限制追加数量,必须确保空间足够
终止符处理 总是添加终止符 自动添加终止符
推荐使用 需要限制长度时 不推荐,除非确定空间足够

8.6 strncat 与 strncpy 的对比

特性 strncat strncpy
功能 追加字符串 复制字符串
终止符 总是添加终止符 仅在长度不足时添加
目标位置 从目标字符串末尾开始 从目标字符串开头开始
使用场景 字符串连接 字符串复制

九. strncmp函数的使用

9.1 函数概述

  • 函数原型:int strncmp(const char *str1, const char *str2, size_t n);
  • 头文件:#include <string.h>
  • 功能说明:strncmp 是 C 语言标准库中用于字符串比较的函数,它比较两个字符串的前 n 个字符,而不比较整个字符串。
  • 比较规则
    如果前 n 个字符完全相等:返回 0
    如果在前 n 个字符中出现不相等:根据第一个不同字符按unsigned char比较的结果返回负数或正数;具体数值不作保证
    如果 str1 中的字符 > str2 中的字符,返回正值
    如果 str1 中的字符 < str2 中的字符,返回负值
  • 重要特性
    区分大小写:在ASCII兼容编码中,小写字母的编码值大于对应的大写字母(例如 ‘a’ > ‘A’)
    考虑字符串终止符:最多比较 n 个字符,但任一字符串遇到终止符 \0 后即可确定结果
    有限比较:相比 strcmp,strncmp 可以限制最多比较的字符数,但传入的指针仍必须指向有效的字符串或具有足够可访问空间的字符序列
  • 参数详解
    str1 要进行比较的第一个字符串的指针
    str2 要进行比较的第二个字符串的指针
    n 要比较的最大字符数
  • 返回值说明
    < 0 str1 小于 str2 ; == 0 str1 等于 str2 ; > 0 str1 大于 str2

9.2 正确使用示例

示例 1:基本用法

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

int main() {
    char str1[] = "Hello";
    char str2[] = "Hello";
    char str3[] = "Hell";
    char str4[] = "Hella";
    char str5[] = "Hello";
    char str6[] = "hello"; // 区分大小写

    printf("str1 vs str2 (n=5): %d\n", strncmp(str1, str2, 5));  // 0
    printf("str1 vs str3 (n=4): %d\n", strncmp(str1, str3, 4));  // 0
    printf("str1 vs str3 (n=5): %d\n", strncmp(str1, str3, 5));  // 返回正数(因为 'o' > '\0')
    printf("str1 vs str4 (n=5): %d\n", strncmp(str1, str4, 5));  // 返回正数(因为 'o' > 'a')
    printf("str1 vs str6 (n=5): %d\n", strncmp(str1, str6, 5));  // 在ASCII兼容编码中返回负数(因为 'H' < 'h')
    
    return 0;
}

示例 2:实际应用场景

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

int main() {
    char filename[] = "example.txt";
    char extension[] = "txt";
    
    // 检查文件是否以 .txt 结尾
    if (strncmp(filename + strlen(filename) - 3, extension, 3) == 0) {
        printf("文件是 .txt 格式\n");
    } else {
        printf("文件不是 .txt 格式\n");
    }
    
    return 0;
}

9.3 常见错误与注意事项

  1. 传入的字符序列不是有效字符串
    char str1[3] = {‘a’, ‘b’, ‘c’};
    char str2[4] = {‘a’, ‘b’, ‘c’, ‘d’};
    printf(“%d\n”, strncmp(str1, str2, 5)); // 两个数组都没有终止符,且可访问空间不足,行为未定义
    正确做法:确保参数指向以\0结尾的字符串,或确保至少有n个字符可安全访问
    char str1[] = “abc”;
    char str2[] = “abcd”;
    printf(“%d\n”, strncmp(str1, str2, 5)); // 安全;比较到str1的’\0’时即可确定结果
  2. 混淆 strncmp 与 strcmp
    strcmp 比较整个字符串
    strncmp 只比较前 n 个字符
  3. 区分大小写
    strncmp 是区分大小写的,例如:
    strncmp(“Hello”, “hello”, 5) // 在ASCII兼容编码中返回负数(因为 ‘H’ < ‘h’)
  4. 正确理解 n
    对于两个正常以\0结尾的字符串,即使n较大,strncmp也会在遇到终止符后停止。
    只有当参数并非有效字符串,且其可访问字符数少于n时,才可能发生越界读取并导致未定义行为。

十.strstr 的使用和模拟实现

10.1函数概述

  • 函数原型:char *strstr(const char *str1, const char *str2);
  • 头文件:#include <string.h>
  • 功能说明:strstr 是 C 语言标准库中用于字符串查找的函数,它在字符串 str1 中查找子字符串 str2 的第一次出现位置。
  • 工作原理:
    查找子串:在 str1 中从头开始搜索,查找 str2 第一次出现的位置
    匹配规则:逐个字符比较,直到找到匹配的子串或遍历完 str1
    终止条件:当 str2 中的字符与 str1 中的字符完全匹配时,返回匹配开始的位置
    未找到处理:如果 str2 不是 str1 的子串,返回 NULL
  • 重要特性
    区分大小写:strstr 是区分大小写的
    不包含终止符:比较时不考虑字符串终止符 \0,但以 \0 作为结束标志
    *空字符串处理:如果 str2 为空字符串(str2 == ‘\0’),则返回 str1 的起始地址
  • 参数详解
    str1 被搜索的目标字符串(主串)
    str2 需要查找的子串
  • 返回值说明
    非NULL 返回指向 str2 在 str1 中第一次出现位置的指针
    NULL 没有找到 str2 作为 str1 的子串

10.2 使用示例

示例 1:基本用法

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

int main() {
    char str1[] = "Hello, world!";
    char str2[] = "world";
    
    char *result = strstr(str1, str2);
    
    if (result != NULL) {
        printf("子串 found at position: %td\n", result - str1);
        printf("Substring: %s\n", result);
    } else {
        printf("Substring not found\n");
    }
    
    return 0;
}
输出:
子串 found at position: 7
Substring: world!

示例 2:查找特定字符串



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

int main() {
    char cmd[] = "enable";
    char *p = strstr(cmd, "en");
    
    if (p != NULL) {
        printf("Found: %s\n", p); // 输出: enable
    } else {
        printf("Not found\n");
    }
    
    return 0;
}

示例 3:替换子串


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

int main() {
    char str[] = "This is a simple example.";
    char *p = strstr(str, "simple");
    
    if (p != NULL) {
        strncpy(p, "sample", 6); // 替换 "simple" 为 "sample"
        printf("%s\n", str); // 输出: This is a sample example.
    }
    
    return 0;
}

10.3 常见错误与注意事项

  1. 未包含头文件
    // 错误:未包含 string.h
    char *result = strstr(str1, str2);
    解决方法:添加 #include <string.h>
  2. 忽略返回值
    char *result = strstr(str1, str2); // 未检查返回值
    printf(“%s\n”, result); // 如果 result 为 NULL,将导致未定义行为
    解决方法:总是检查返回值
    char *result = strstr(str1, str2);
    if (result != NULL) {
    printf(“%s\n”, result);
    } else {
    printf(“Substring not found\n”);
    }
  3. 处理空字符串
    char str1[] = “Hello”;
    char str2[] = “”; // 空字符串
    char *result = strstr(str1, str2); // 会返回 str1 的起始地址
    正确理解:当 str2 为空字符串时,strstr 返回 str1 的起始地址

10.4 strstr 函数的模拟实现

实现方式 1:基于指针移动

#include <assert.h>
char *my_strstr(const char *str1, const char *str2) {
    assert(str1 && str2);

    // 处理空字符串的情况
    if (*str2 == '\0') {
        return (char *)str1;
    }
    
    const char *s1 = str1;
    const char *s2 = str2;
    const char *cur = str1;
    
    while (*cur != '\0') {
        s1 = cur;
        s2 = str2;
        
        // 比较子串
        while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) {
            s1++;
            s2++;
        }
        
        // 如果匹配完成,返回匹配起始位置
        if (*s2 == '\0') {
            return (char *)cur;
        }
        
        cur++;
    }
    
    return NULL;
}

实现方式 2:基于索引的循环

#include <assert.h>
char *my_strstr(const char *str1, const char *str2) {
    assert(str1 && str2);
    
    // 处理空字符串的情况
    if (*str2 == '\0') {
        return (char *)str1;
    }
    
    int i, j;
    
    for (i = 0; str1[i] != '\0'; i++) {
        // 检查是否可能匹配
        for (j = 0; str2[j] != '\0' && str1[i + j] == str2[j]; j++);
        
        // 如果匹配完成
        if (str2[j] == '\0') {
            return (char *)(str1 + i);
        }
    }
    
    return NULL;
}

实现方式 3:使用长度计算

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

char *my_strstr(const char *str1, const char *str2) {
    assert(str1 && str2);
    
    // 处理空字符串的情况
    if (*str2 == '\0') {
        return (char *)str1;
    }
    
    size_t str1_len = strlen(str1);
    size_t str2_len = strlen(str2);
    
    if (str2_len > str1_len) {
        return NULL;
    }
    
    for (size_t i = 0; i <= str1_len - str2_len; i++) {
        size_t j;
        for (j = 0; j < str2_len && str1[i + j] == str2[j]; j++);
        
        if (j == str2_len) {
            return (char *)(str1 + i);
        }
    }
    
    return NULL;
}

十一. strtok 函数的使用

11.1函数概述

  • 函数原型:char *strtok(char *str, const char *delim);
  • 头文件:#include <string.h>
  • 功能说明:strtok 是 C 语言标准库中用于字符串分割的函数,它将字符串按照指定的分隔符进行分割,返回分割后的子字符串指针。
  • 工作原理
    首次调用:传入待分割的字符串和分隔符
    后续调用:传入 NULL 作为第一个参数,继续从上次分割的位置往后处理
    修改字符串:strtok 会修改原始字符串,将分隔符替换为 \0 字符
  • 返回值:返回指向分割出的子字符串的指针,当没有更多子字符串可分割时返回 NULL
  • 参数详解
    str 第一次调用时为要分割的字符串指针,后续调用为 NULL
    delim 分隔符字符串,可以包含多个字符,任意一个字符都会被视为分隔符

11.2 使用示例

基本用法

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

int main() {
    char str[] = "hello,world;how are you";
    char *token;
    char *delim = ",;"; // 逗号和分号作为分隔符
    
    // 首次调用strtok
    token = strtok(str, delim);
    
    while (token != NULL) {
        printf("%s\n", token);
        // 继续分割字符串
        token = strtok(NULL, delim);
    }
    
    return 0;
}
输出:
hello
world
how are you

详细示例(IP 地址分割)

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

int main() {
    char arr[] = "192.168.6.111";//arr 是一个字符数组,不是指针;它具有自动存储期,数组中的字符串内容可以修改
    char* sep = ".";
    char* str = NULL;
    
    // 使用strtok分割IP地址
    for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep)) {
        printf("%s\n", str);
    }
    
    return 0;
}
输出:
192
168
6
111

11.3 重要注意事项

  1. 修改原始字符串
    strtok 会修改原始字符串,将分隔符替换为 \0
    示例:
    char str[] = “abc,def,ghi”;
    strtok(str, “,”);
    // 此时 str 变为 “abc\0def,ghi”
  2. 不能用于字符串字面量
    不能将字符串字面量作为可修改的 str 参数,因为strtok会尝试修改其中的内容
    错误示例:
    char *str = “abc,def,ghi”; // 指向字符串字面量
    strtok(str, “,”); // 行为未定义,可能导致程序崩溃
    正确做法:
    char str[] = “abc,def,ghi”; // 可修改的字符串
    strtok(str, “,”);
  3. 线程不安全
    strtok 使用内部静态状态记录后续扫描位置,因此不同分割过程会相互干扰,通常也不是线程安全的
    多线程环境下:使用 strtok_r(POSIX 标准)代替
  4. 分隔符可以是多个字符
    分隔符可以包含多个字符,任意一个字符都会被视为分隔符
    示例:
    char *delim = ",; ";
    // 逗号、分号和空格都会被视为分隔符

11.4 strtok 的工作原理

第一次调用:
从字符串开头开始查找分隔符,找到第一个分隔符后,将其替换为 \0,返回从开头到第一个分隔符的子字符串指针
后续调用:
传入 NULL,表示从上次分割的位置继续,继续查找下一个分隔符,找到后,将其替换为 \0,返回从上次分割位置到下一个分隔符的子字符串指针

十二.strerror 函数的使用

12.1函数概述

  • 函数原型:char *strerror(int errnum);
  • 头文件:#include <string.h>;如果需要使用errno和错误码宏,还应包含#include <errno.h>
  • 功能说明:
    strerror 是 C 语言标准库中的错误报告函数,用于将错误码转换为对应的错误信息字符串。许多库函数或系统调用在失败时会设置errno,strerror函数可以将这个错误码转换为人类可读的错误信息。只有在函数通过返回值报告失败后,errno的值才应被用于判断该次错误。
  • 工作原理
    错误码存储:许多库函数或系统调用执行失败时,会将错误码存入errno
    错误信息转换:strerror 函数根据 errno 的值,返回一个指向错误信息字符串的指针
    错误信息格式:返回的字符串描述了错误的具体原因
  • 参数详解:errnum 错误码,通常是 errno 的值

12.2 使用示例

基本用法

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

int main() {
    FILE *fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        printf("Error opening file: %s\n", strerror(errno));
    }
    return 0;
}
输出:
Error opening file: No such file or directory

打印所有错误码对应的错误信息

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

int main() {
    for (int i = 0; i <= 10; i++) {
        printf("Error code %d: %s\n", i, strerror(i));
    }
    return 0;
}
输出(某一实现上的示例,具体错误码和文本可能因系统而异):
Error code 0: Success
Error code 1: Operation not permitted
Error code 2: No such file or directory
Error code 3: No such process
Error code 4: Interrupted system call
Error code 5: Input/output error
Error code 6: No such device or address
Error code 7: Argument list too long
Error code 8: Exec format error
Error code 9: Bad file descriptor
Error code 10: No child processes

%s在C语言中用于输出字符串,它的行为是:
从指定的字符串指针开始,一直打印字符直到遇到空字符\0,不会因为遇到空格、制表符或换行符而停止

12.3与 perror 函数对比

perror 函数与 strerror 类似,但会直接打印错误信息:

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

int main() {
    FILE *fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
    }
    return 0;
}
输出:
Error opening file: No such file or directory

12.4 使用注意事项

  • 只读字符串:strerror 返回的字符串不应尝试修改,后续调用还可能覆盖或改变该字符串的内容
  • errno:errno是由<errno.h>提供的可修改左值宏,现代实现通常为每个线程分别保存
  • 系统差异:不同系统和C库实现中,错误码和对应的错误信息可能有所不同
  • 错误处理:strerror 通常与系统函数失败时的错误处理一起使用
  • 移植性:为确保移植性,最好根据标准文档中规定的错误码来使用 strerror 函数

12.5 实际应用场景

文件操作错误处理

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

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        printf("Failed to open file: %s\n", strerror(errno));
        return 1;
    }
    // 文件操作...
    fclose(fp);
    return 0;
}

网络编程错误处理

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <unistd.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        printf("Socket creation failed: %s\n", strerror(errno));
        return 1;
    }
    // 网络操作...
    close(sockfd);
    return 0;
}

12.6 常见错误

错误 1:未包含必要的头文件
// 错误:未包含 <errno.h>
char *error_msg = strerror(errno);
解决方法:添加 #include <errno.h>
错误 2:未检查系统函数返回值
FILE *fp = fopen(“file.txt”, “r”);
printf(“File opened successfully\n”); // 未检查是否成功打开
解决方法:总是检查系统函数的返回值
FILE *fp = fopen(“file.txt”, “r”);
if (fp == NULL) {
printf(“Error opening file: %s\n”, strerror(errno));
return 1;
}

十三. memcpy函数和模拟实现

memcpy 进行的是原始内存的字节复制。用于包含指针成员的对象时,它只复制指针值而不会复制指针所指向的对象,因此常被称为 “浅拷贝”

13.1 memcpy 函数概述

memcpy 是 C 语言标准库中的内存复制函数,用于将一块内存区域的内容复制到另一块内存区域。它是最基本的内存操作函数之一,提供高效的字节级复制功能。

  • 函数原型:void *memcpy(void *dest, const void *src, size_t n);
  • 头文件:#include <string.h>
  • 函数参数详解
    dest 目标内存地址指针,指向要存储复制内容的内存区域
    src 源内存地址指针,指向要复制的内存区域
    n 要复制的字节数
  • 返回值:返回指向目标存储区 dest 的指针(void* 类型)
  • 函数功能与特点:
    字节级复制:memcpy 不关心数据类型,只进行字节级复制,不检查数据内容
    不处理终止符:在复制过程中,不会因为遇到 ‘\0’ 而停止
    不处理重叠:如果源和目标内存区域重叠,memcpy 的行为是未定义的(应使用 memmove)
  • 使用注意事项:
    目标内存足够大:必须确保目标内存区域足够大以容纳要复制的数据量,否则会导致溢出错误
    不处理重叠:当源和目标内存区域可能重叠时,应使用 memmove 函数替代 memcpy
    错误检查:确保 dest 和 src 不为 NULL(虽然标准库函数通常不检查,但模拟实现中建议添加断言)

13.2 memcpy 与 memmove 的区别

特性 memcpy memmove
内存重叠处理 不处理,行为未定义 处理,确保数据正确复制
效率 在某些实现中可能略高 在某些实现中可能略低
使用场景 确定内存区域不重叠时 内存区域可能重叠时

13.3 memcpy 的模拟实现

#include <assert.h>
#include <stddef.h>

void* my_memcpy(void* destination, const void* source, size_t num)
{
    // 断言确保指针不为NULL
    assert(destination != NULL);
    assert(source != NULL);
    
    // 保存目标内存的起始地址
    void* ret = destination;
    
    // 将指针转换为char*类型,以便进行字节级操作
    char* dest = (char*)destination;
    const char* src = (const char*)source;
    
    // 逐字节复制
    while (num--)
    {
        *dest = *src;
        dest++;
        src++;
    }
    
    return ret;
}

模拟实现代码解释:
断言检查:确保目标和源指针不为 NULL
保存起始地址:ret 用于返回目标内存的起始地址
指针转换:将 void* 指针转换为 char* 类型,以便逐字节操作
字节复制:使用 while 循环,每次复制一个字节,然后移动指针
返回结果:返回目标内存的起始地址

13.4 使用示例

1.复制整型数组

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

int main() {
    int arr1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int arr2[10] = {0};
    
    // 复制前5个整型元素
    memcpy(arr2, arr1, 5 * sizeof(arr1[0]));
    
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr2[i]);
    }
    
    return 0;
}
输出:
1 2 3 4 5 0 0 0 0 0

2.复制字符串

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

int main() {
    char src[] = "Hello, World!";
    char dest[20];
    
    // 复制整个字符串数组(包括结尾的'\0')
    memcpy(dest, src, sizeof(src));
    
    printf("%s\n", dest);
    
    return 0;
}
输出:
Hello, World!

13.5 常见错误与解决方法

错误 1:未确保目标内存足够大
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[3];
memcpy(arr2, arr1, sizeof(arr1)); // 错误!目标内存太小
解决方法:确保目标内存足够大
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5]; // 目标内存足够大
memcpy(arr2, arr1, sizeof(arr1)); // 正确
错误 2:在内存重叠时使用 memcpy
char str[] = “abcdef”;
memcpy(str + 1, str, 3); // 错误!源和目标内存重叠
解决方法:使用 memmove
char str[] = “abcdef”;
memmove(str + 1, str, 3); // 正确!memmove可以处理重叠

十四. memmove函数和模拟实现

14.1 memmove 函数概述

memmove 是 C 语言标准库中的内存移动函数,用于将一块内存区域的内容复制到另一块内存区域。它的核心特点是能够处理源内存区域和目标内存区域重叠的情况,这是它与 memcpy 的主要区别。

  • 函数原型:void *memmove(void *dest, const void *src, size_t count);(count是字节数)
  • 头文件:#include <string.h>
  • 工作原理:
    C标准规定memmove的效果如同先将源区域复制到一个与源、目标都不重叠的临时数组,再从临时数组复制到目标区域。
    常见的高效实现会根据源地址和目标地址的相对位置选择复制方向:
    当 dest < src(目标区域在源区域之前):从前向后复制(避免覆盖尚未读取的源数据)
    例如:memmove(str, str+1, 3),将 str+1 中的 “bcd” 复制到 str 位置
    当 dest > src(目标区域在源区域之后):从后向前复制
    例如:memmove(str+1, str, 3),将 str 中的 “abc” 复制到 str+1 位置

14.2 memmove 与 memcpy 的区别

特性 memcpy memmove
内存重叠处理 不处理,行为未定义 处理,确保数据完整
工作原理 源和目标不得重叠 效果如同经过临时数组复制,常见实现会调整复制顺序
效率 在某些实现中可能略高 在某些实现中可能略低
适用场景 确定内存区域不重叠时 内存区域可能重叠时
 关键区别:当源区域和目标区域重叠时,memcpy 的行为未定义,而 memmove 会正确处理这种情况。

补充:地址间比较大小

比较类型 是否合法 说明
同一数组的指针 ✅ 合法 比较结果等价于数组下标比较
同一结构体的成员指针 ✅ 合法 后声明的成员指针较大
同一联合体的成员指针 ✅ 合法 指针相等
不同对象的指针 ❌ 不合法 比较结果未定义

14.3 memmove 模拟实现

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

void* my_memmove(void* dest, const void* src, size_t num)
{
    if (num == 0) {
        return dest;
    }

    assert(dest != NULL);
    assert(src != NULL);

    unsigned char* temp = (unsigned char*)malloc(num);
    if (temp == NULL) {
        return NULL; // 教学实现:临时缓冲区分配失败
    }

    unsigned char* destination = (unsigned char*)dest;
    const unsigned char* source = (const unsigned char*)src;

    for (size_t i = 0; i < num; i++) {
        temp[i] = source[i];
    }

    for (size_t i = 0; i < num; i++) {
        destination[i] = temp[i];
    }

    free(temp);
    return dest;
}

模拟实现代码解释:
零长度处理:当num为0时,直接返回dest
断言检查:当需要复制数据时,确保目标和源指针不为NULL
临时缓冲区:先把源区域的num个字节复制到temp
目标复制:再把temp中的字节复制到目标区域,因此能够正确处理重叠
资源释放:复制完成后释放临时缓冲区
返回结果:成功时返回目标内存的起始地址;该教学实现如果内存分配失败会返回NULL

标准库memmove通常不会通过动态分配实现;这里使用临时缓冲区是为了直接体现标准规定的“如同经过临时数组复制”的语义。

14.4 使用示例

1.处理内存重叠情况

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

int main() {
    char str[] = "abcdef";
    
    // 源区域和目标区域重叠
    // 从str位置复制3个字节到str+1位置
    memmove(str + 1, str, 3);
    
    printf("Result: %s\n", str);
    
    return 0;
}
输出:
Result: aabcef

2.正常复制情况

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

int main() {
    char src[] = "Hello, World!";
    char dest[20];
    
    // 复制整个字符串数组(包括结尾的'\0')
    memmove(dest, src, sizeof(src));
    
    printf("%s\n", dest);
    
    return 0;
}
输出:
Hello, World!

14.5 为什么需要 memmove?

  • 处理重叠内存:当源和目标内存区域重叠时,memcpy 的行为未定义,而 memmove 会正确处理这种情况
  • 数据完整性:确保在内存重叠的情况下,数据复制是完整的
  • 代码健壮性:使用 memmove 可以避免因内存重叠导致的难以调试的错误

14.6常见错误与解决方法

错误 1:在重叠内存区域使用 memcpy
char str[] = “abcdef”;
memcpy(str + 1, str, 3); // 错误!源和目标内存重叠
解决方法:使用 memmove
char str[] = “abcdef”;
memmove(str + 1, str, 3); // 正确!memmove处理重叠
错误 2:未确保目标内存足够大
char src[] = “Hello, World!”;
char dest[5];
memmove(dest, src, sizeof(src)); // 错误!目标内存太小
解决方法:确保目标内存足够大
char src[] = “Hello, World!”;
char dest[20]; // 目标内存足够大
memmove(dest, src, sizeof(src)); // 正确

14.7 总结

memmove 是 C 语言中用于内存移动的函数,其核心优势在于能够处理源内存区域和目标内存区域重叠的情况。C标准规定其效果如同经过临时数组复制,常见实现可能根据重叠情况选择合适的复制方向。

使用建议:
确定内存区域不重叠:可以使用 memcpy;是否更快取决于具体实现
内存区域可能重叠:如果存在内存重叠的可能性,必须使用 memmove 以确保数据安全
目标内存足够大:确保目标内存区域足够大,避免内存溢出

十五. memset函数

15.1 函数概述

  • 函数原型:void *memset(void *ptr, int value, size_t num);
  • 头文件:#include <string.h>
  • 参数说明
    ptr 指向要填充的内存块的指针
    value 要设置的值(以int形式传递,但实际使用unsigned char进行填充)
    num 要设置的字节数
  • 工作原理
    memset 以字节为单位进行赋值,从内存块的起始地址开始,依次将每个字节设置为指定的值,直到设置的字节数达到num指定的值。

关键点:
value 参数是按int类型传递,但函数内部将其转换为unsigned char进行填充
value会转换为unsigned char; 在CHAR_BIT为8的常见平台上,可理解为使用value的低8位
按字节填充,不是按元素填充

15.2 使用示例

1.清零内存

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

int main() {
    int arr[10];
    // 正确:将整个int数组清零
    memset(arr, 0, sizeof(arr));
    
    // 打印验证
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}
输出:
0 0 0 0 0 0 0 0 0 0

2.初始化结构体

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

struct Student {
    int id;
    char name[20];
    int score;
};

int main() {
    struct Student s;
    // 清空结构体
    memset(&s, 0, sizeof(s));
    
    // 验证结构体被清零
    printf("ID: %d, Name: %s, Score: %d\n", 
           s.id, s.name, s.score);
    return 0;
}
输出:
ID: 0, Name: , Score: 0

3.字符数组初始化

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

int main() {
    char buffer[10];
    // 前9个字节填充为'A',最后一个字节存放终止符
    memset(buffer, 'A', sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';

    printf("Buffer: %s\n", buffer);
    return 0;
}
输出:
Buffer: AAAAAAAAA

15.3 常见用法与注意事项

  1. 正确初始化
    清零字符数组或整数数组:
    memset(arr, 0, sizeof(arr)); // 对字符类型和整数类型可得到数值0
    初始化为-1(int类型,仅适用于采用二进制补码且无相关填充问题的常见实现):
    memset(arr, 0xFF, sizeof(arr)); // 常见平台上可得到-1
    初始化为0x3f(假设CHAR_BIT为8且int为4字节):
    memset(arr, 0x3f, sizeof(arr)); // 在这些假设下,每个int值为1061109567

  2. 常见错误
    错误1:错误地初始化int数组
    int arr[10];
    memset(arr, 1, sizeof(arr)); // 错误!
    解释:memset按字节填充。在CHAR_BIT为8且sizeof(int)=4的常见平台上,会将每个int的4个字节都设为1,结果int值变为0x01010101, 而不是1。
    正确做法:
    // 如果确实需要将int数组初始化为1,应使用循环
    for (int i = 0; i < 10; i++) {
    arr[i] = 1;
    }
    错误2:未正确计算字节数
    int arr[10];
    memset(arr, 0, 10); // 错误!10是元素个数,不是字节数
    正确做法:
    // 使用sizeof计算字节数
    memset(arr, 0, sizeof(arr)); // 正确

  3. 为什么不能把字节填充值直接当作int元素值
    memset是按字节填充,而int通常由多个字节组成。
    例如,在CHAR_BIT为8、sizeof(int)=4且采用二进制补码的常见平台上:
    memset(arr, 1, sizeof(arr)); → 每个int的4个字节都设为1 → int值 = 0x01010101 = 16843009
    memset(arr, 0xFF, sizeof(arr)); → 每个int的4个字节都设为0xFF → int值 = -1

  4. 按字节填充:
    memset不会检查数据类型,只按字节操作
    对于多字节数值类型,不能把填充值直接理解为元素值。整数数组清零通常可移植;对浮点数、指针以及其他复杂对象,应使用逐元素赋值或初始化语法

  5. 字节数计算:
    num参数必须是字节数,不是元素个数
    对于数组,应使用sizeof(array)计算总字节数

  6. 内存边界:
    memset不会检查ptr是否指向有效内存地址
    num不能大于目标内存区域的大小,否则会导致越界

  7. value参数:
    value可以是任意int值,调用时会转换为unsigned char
    在CHAR_BIT为8的常见平台上,例如300会转换为44

15.4 效率优势

memset通常由标准库和编译器进行高度优化,适合批量字节填充;是否比手动循环更快取决于编译器、平台和具体代码
适合批量字节填充:特别适合清空字符缓冲区,以及初始化适合全零位模式的数据结构

15.5 总结

memset是一个非常方便的内存初始化函数,但使用时必须注意:
它是按字节操作,不是按元素操作。num参数必须是字节数,通常应使用sizeof计算。不要把非零字节填充误认为对int、float等类型进行了对应数值的逐元素赋值
最佳实践:
清零字符缓冲区或整数数组:memset(ptr, 0, size);
初始化仅由整数和字符数组等适合全零位模式成员组成的结构体:memset(&object, 0, sizeof(object));
非零初始化:对int/float等类型,使用循环逐元素赋值
通过正确使用memset,可以提高代码的效率和可读性,避免常见的内存初始化错误。

十六. memcmp函数

16.1 函数概述

  • 函数原型:int memcmp(const void *str1, const void *str2, size_t n);
  • 头文件:#include <string.h>
  • 参数说明
    str1 指向第一个内存块的指针
    str2 指向第二个内存块的指针
    n 要比较的字节数
  • 工作原理
    memcmp 按字节比较两个内存区域的内容,从最低地址开始,逐字节比较,直到找到不同的字节或比较完 n 个字节。
  • 返回值
    返回 0:如果 str1 和 str2 所指向的内存内容在前 n 个字节内完全相同
    返回正数:如果 str1 在第一个不同字节处的值大于 str2 的对应值
    返回负数:如果 str1 在第一个不同字节处的值小于 str2 的对应值
    标准只保证返回值的符号;具体数值不作保证。常见模拟实现会返回第一个不同字节的差值
    关键点:
    比较时,将字节视为无符号字符(unsigned char)
    逐字节比较,不关心数据类型
    不会因遇到 \0 而停止比较(与 strncmp 不同)

16.2 使用示例

1.字符串比较

#include <stdio.h>
#include <string.h>
int main() {
    char str1[] = "hello";
    char str2[] = "world";
    
    // 比较前5个字节
    int result = memcmp(str1, str2, 5);
    
    if (result == 0) {
        printf("str1 and str2 are equal\n");
    } else if (result < 0) {
        printf("str1 is less than str2\n");
    } else {
        printf("str1 is greater than str2\n");
    }
    
    return 0;
}
输出(在ASCII兼容编码中):
str1 is less than str2

2.数组比较

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

int main() {
    unsigned char arr1[] = {1, 2, 3, 4};
    unsigned char arr2[] = {1, 2, 3, 5};
    
    // 比较整个数组
    int result = memcmp(arr1, arr2, sizeof(arr1));
    
    if (result == 0) {
        printf("arr1 and arr2 are equal\n");
    } else if (result < 0) {
        printf("arr1 is less than arr2\n");
    } else {
        printf("arr1 is greater than arr2\n");
    }
    
    return 0;
}
输出:
arr1 is less than arr2

3.比较部分内存

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

int main() {
    char arr1[] = "a2345";
    char arr2[] = "Abc4xxdefg";
    
    printf("%d\n", memcmp(arr1, arr2, 1));      // 在ASCII兼容编码中返回正数(因为 'a' > 'A')
    printf("%d\n", memcmp(arr1 + 3, arr2 + 3, 1)); // 返回0(因为 '4' == '4')
    printf("%d\n", memcmp(arr1 + 1, arr2 + 1, 1)); // 在ASCII兼容编码中返回负数(因为 '2' < 'b')
    
    return 0;
}

16.3 常见陷阱与注意事项

1. 陷阱:未正确计算比较长度
// 错误:在不知道str2可访问空间的情况下,直接使用sizeof(str1)
if (memcmp(str1, str2, sizeof(str1)) == 0) {
// …
}

// 正确:只比较两个内存区域都保证有效的字节数
size_t n = buffer1_size < buffer2_size ? buffer1_size : buffer2_size;
if (memcmp(buffer1, buffer2, n) == 0) {
// 前n个字节相同
}
2. 陷阱:比较结构体时
如果结构体包含填充字节(padding bytes),memcmp 会比较这些填充字节即使两个结构体各成员的值相等,填充字节也可能不同,因此通常应逐成员比较,而不要使用memcmp判断结构体值是否相等
3. 陷阱:与 strcmp 的混淆
memcmp 比较字节,不关心字符串终止符 \0
strcmp 在遇到 \0 时会停止比较
4. 陷阱:与 strncmp 的混淆
memcmp 比较前 n 个字节,不管是否遇到 \0
strncmp 在比较过程中遇到 \0 时会停止,即使 n 还未用完

16.4 与 strncmp 的区别

特性 memcmp strncmp
比较方式 比较前 n 个字节 比较前 n 个字节,但遇到 \0 会停止
与字符串终止符关系 不考虑 \0 考虑 \0,在遇到 \0 时停止
适用场景 比较原始内存块、二进制数据 比较字符串(C风格)

16.5 模拟实现

#include <assert.h>
#include <stddef.h>

int my_memcmp(const void *buf1, const void *buf2, size_t count) {
    assert(buf1 != NULL);
    assert(buf2 != NULL);
    
    const unsigned char *p1 = (const unsigned char *)buf1;
    const unsigned char *p2 = (const unsigned char *)buf2;

    while (count--) {
        // 按unsigned char逐字节比较
        unsigned char b1 = *p1;
        unsigned char b2 = *p2;
        
        if (b1 != b2) {
            // 返回两个字节的差值
            return (int)(b1 - b2);
        }
        
        // 移动指针到下一个字节
        p1++;
        p2++;
    }
    
    return 0; // 所有字节都相同
}

十七. strchr函数

功能描述

strchr 函数用于查找 字符在字符串中的首次出现位置。 该函数会将字符串末尾的 空字符('\0' 视为字符串的一部分。因此,可以通过此函数获取指向字符串末尾的指针。

函数原型

const char * strchr ( const char * str, int character );
      char * strchr (       char * str, int character );
  • C++:提供两个重载版本(支持 const char*char*)。
  • C:仅提供一个版本(char * strchr ( const char *, int ))。

参数说明

参数 说明
str 输入的 C 字符串(以 '\0' 结尾)。
character 要查找的目标字符。虽然以 int 类型传入,但实际比较时会转换为 char

返回值

  • 成功:返回指向字符串中第一个匹配字符的指针。
  • 失败:若未找到目标字符,返回 NULL(空指针)。

可移植性说明

  • C 语言:函数原型为 char *strchr(const char *str, int character);。参数允许传入const char *,但返回类型仍为char *
  • C++:支持两种重载版本(const char*char*)。

示例代码

/* strchr 示例 */
#include <stdio.h>
#include <string.h>
#include <stddef.h>

int main () {
  char str[] = "This is a sample string";
  char * pch;

  printf ("Looking for the 's' character in \"%s\"...\n",str);
  pch = strchr(str,'s');  // 查找第一个 's'

  while (pch != NULL) {
    printf ("found at %td\n", pch - str + 1);  // 输出位置(从 1 开始计数)
    pch = strchr(pch + 1, 's');  // 从当前位置后继续查找
  }

  return 0;
}

输出结果

Looking for the 's' character in "This is a sample string"...
found at 4   // 第4个字符(索引3)是 's'
found at 7   // 第7个字符(索引6)是 's'
found at 11  // 第11个字符(索引10)是 's'
found at 18  // 第18个字符(索引17)是 's'

关键点总结

项目 说明
查找目标 包括字符串末尾的空字符 '\0'
字符类型 characterint 类型传入,但内部转换为 char 比较。
返回值 返回指向匹配字符的指针;未找到时返回 NULL
使用场景 解析字符串、提取子串、验证字符存在性等。

注意事项

  1. 索引从 0 开始
    示例中 pch - str + 1 的输出是人为调整为从 1 开始的索引,实际内存地址偏移量从 0 开始。

  2. 空字符查找
    若传入 character'\0',函数会返回指向字符串末尾的指针(即 str + strlen(str))。

  3. 大小写敏感
    strchr("Hello", 'h') 会返回 NULL,因为目标字符是小写 'h',而字符串中是 'H'

  4. 线程安全
    strchr 是线程安全的(无内部状态保存)。

典型应用

  1. 文件路径解析
    查找路径中的斜杠 /\

  2. 字符串分割
    结合 strchrstrncpy 提取子串。

  3. 验证字符存在性
    检查密码是否包含特殊字符(如 strchr(password, '!'))。

  4. 动态调整
    修改字符串中首次出现的某个字符(需使用非 const 版本)。

通过 strchr,可以高效地定位字符在字符串中的位置,是字符串处理中的基础工具之一。

十八. 在C++中的使用

上述C标准库函数在C++中也可以使用。C++通常推荐使用对应的C++头文件,并通过std::命名空间调用:

  • 字符分类和转换函数:#include <cctype>,如std::isalphastd::tolower
  • 字符串和内存函数:#include <cstring>,如std::strlenstd::strcpystd::memcpystd::strchr
  • 错误码:#include <cerrno>
  • 输入输出与文件函数:#include <cstdio>
  • 断言:#include <cassert>
  • size_tptrdiff_t#include <cstddef>

C++中通常也兼容<ctype.h><string.h>等C风格头文件,但更推荐使用<cctype><cstring>等C++风格头文件。对含有构造函数、析构函数、虚函数或其他非平凡成员的C++对象,不应使用memcpymemmovememset代替正常的复制、移动和初始化操作。<sys/socket.h><unistd.h>属于POSIX系统接口,不是标准C++头文件。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐