【初识C语言】字符 / 字符串函数 + 内存函数(详解+模拟实现+避坑)
本文系统整理了C语言中字符与字符串操作的核心函数,包括字符分类/转换函数(如isdigit、toupper等)和基础字符串函数(strlen、strcpy、strcmp等)。详细讲解了各函数的功能、使用注意事项,并提供了strlen的三种模拟实现方法(计数器法、递归法、指针减法)。此外还介绍了安全字符串函数(如strncpy)以避免越界风险。文章包含代码示例和实现细节,适合C语言初学者系统学习字符
系列文章目录
学习系列文章:
【初识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'停止。 - 注意事项:
-
- 源字符串必须以
'\0'结束;
- 源字符串必须以
-
- 目标空间足够大且可修改;
-
- 会拷贝
'\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’位置开始。
- 注意事项:
-
- 目标字符串需包含
'\0'(否则无法确定追加起始位置);
- 目标字符串需包含
-
- 目标空间需足够大;
-
- 源字符串必须以
'\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'。 - 返回值:
-
- 0:str1 > str2;
-
- =0:str1 == str2;
-
- <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个字符到目标空间。 - 关键区别:
-
- 源字符串长度 <
num:拷贝完源字符串(含'\0')后,剩余位置补'\0';
- 源字符串长度 <
-
- 源字符串长度 ≥
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')。 - 使用规则:
-
- 首次调用:传入待分割字符串和分隔符;
-
- 后续调用:传入NULL和相同分隔符,继续分割;
-
- 连续分隔符视为单个。
代码示例:
#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一致,但支持源和目标内存重叠(核心区别)。 - 实现逻辑:
-
- 目标地址 < 源地址:从前向后拷贝;
-
- 目标地址 > 源地址:从后向前拷贝。
模拟实现:
#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 |
总结
熟练使用库函数能够让我们解决问题时事半功倍,希望这篇文章对大家有所帮助。
更多推荐


所有评论(0)