在 C 语言中,字符串并非内置数据类型,而是以'\0'(空字符)结尾的字符数组。这种设计使得字符串操作本质上就是对一段连续内存的访问与修改,而指针正是 C 语言中操作内存最直接、最高效的工具。
今天,我们通过模拟实现 5 个经典字符串函数,循序渐进地学习如何用指针这个 "内存书签" 来处理字符串,同时理解这些函数的设计逻辑。
基础知识回顾
| 写法 | 含义 | 新手提示 |
|---|---|---|
char *str | 指向字符的指针,通常表示字符串 | 指针指向字符串首字符,通过移动指针可访问整个字符串 |
*p | 解引用,获取指针 p 所指向的值 | 相当于 "打开书签指向的那一页" |
p++ | 指针自增,指向下一个内存位置 | 相当于 "书签往后翻一页" |
'\0' | 字符串结束标志(ASCII 值为 0) | 没有这个标志,函数就不知道字符串在哪里结束 |
const char* | 常量指针,表示指向的内容不可修改 | 用于保护源数据不被意外修改,增加代码安全性 |
重要:指针运算中,两个同类型指针相减的结果是它们之间的元素个数,这是计算字符串长度的关键原理。
阶段 1:单指针遍历 —— my_strlen(字符串长度计算)
需求
计算字符串中有效字符的个数(不包括结束符'\0')。
实现策略
用一个指针从字符串首字符开始遍历,直到遇到'\0',记录移动的步数即为长度。
代码实现
#include <stddef.h> // 包含size_t类型定义
size_t my_strlen(const char *str) {
const char *p = str; // 用临时指针p进行遍历,保留str的起始位置
while (*p != '\0') { // 当指针指向的字符不是结束符时继续循环
p++; // 指针向后移动一位
}
return p - str; // 两个指针的差值就是字符个数
}
代码解析
const char *str参数:使用const修饰是因为计算长度不需要修改字符串内容,同时允许传入常量字符串(如"hello")。- 临时指针
p:为什么不直接移动str?因为需要保留原始起始地址用于最后计算长度(p - str)。 - 循环条件
*p != '\0':*p获取当前指针指向的字符,直到遇到结束符停止遍历。 - 返回值
p - str:由于指针指向连续的字符数组,两者差值即为有效字符的数量。
参数与返回值设计逻辑
-
参数类型
const char *str:const确保函数内部不会修改输入字符串,增强安全性- 指针类型是因为 C 语言中字符串传参本质是传递首地址(数组会退化为指针)
-
返回值类型
size_t:size_t是标准的无符号整数类型(定义在<stddef.h>),专门用于表示长度- 不用
int是因为字符串长度不可能为负数,无符号类型语义更准确
阶段 2:双指针同步 —— 字符串复制与比较
2.1 my_strcpy(字符串复制)
需求
将源字符串src的内容(包括结束符'\0')完整复制到目标字符串dest。
实现策略
使用两个指针分别遍历源字符串和目标字符串,逐个字符复制,直到遇到源字符串的结束符。
代码实现
char* my_strcpy(char *dest, const char *src) {
char *ret = dest; // 保存目标字符串的起始地址
while ((*dest = *src) != '\0') { // 先赋值再判断
dest++; // 目标指针后移
src++; // 源指针后移
}
return ret; // 返回目标字符串的起始地址
}
代码解析
- 保存起始地址:
char *ret = dest记录dest的初始位置,因为后续dest指针会移动。 - 核心循环
(*dest = *src) != '\0':- 先执行
*dest = *src:将源字符复制到目标位置 - 再判断赋值结果是否为
'\0':如果是则结束循环(已复制结束符)
- 先执行
- 指针同步移动:
dest++和src++保证两个指针始终指向对应的位置
参数与返回值设计逻辑
-
参数类型
char *dest, const char *src:dest是普通指针:需要修改目标字符串内容src是常量指针:只读取源字符串,不允许修改- 必须传递两个指针:C 语言没有字符串整体赋值的语法,必须逐个字符复制
-
返回值类型
char*:- 返回
dest的起始地址(通过ret保存) - 支持链式操作,例如:
printf("%s", my_strcpy(dest, src))
- 返回
-
使用注意:
- 目标缓冲区
dest必须有足够大的空间,否则会导致缓冲区溢出 dest和src的内存区域不能重叠
- 目标缓冲区
2.2 my_strcmp(字符串比较)
需求
按字典序比较两个字符串,判断它们的大小关系。
实现策略
用两个指针同步遍历两个字符串,逐个比较对应位置的字符,直到找到不同字符或遇到结束符。
代码实现
int my_strcmp(const char *s1, const char *s2) {
while (*s1 && (*s1 == *s2)) { // 字符存在且相等时继续
s1++; // 同步后移
s2++;
}
// 返回两个字符的差值(转换为unsigned char避免符号问题)
return (unsigned char)*s1 - (unsigned char)*s2;
}
代码解析
- 循环条件
*s1 && (*s1 == *s2):*s1等价于*s1 != '\0',确保字符串未结束*s1 == *s2判断当前字符是否相等
- 指针同步移动:两个指针同时后移,保持位置对应
- 返回值计算:当循环结束时,两个指针指向的字符差值就是比较结果
参数与返回值设计逻辑
-
参数类型
const char *s1, const char *s2:- 两个参数都用
const修饰,因为比较操作不需要修改原字符串 - 指针类型使函数能高效访问字符串内容
- 两个参数都用
-
返回值类型
int:- 返回正数:表示
s1大于s2 - 返回负数:表示
s1小于s2 - 返回 0:表示两个字符串相等
- 使用
unsigned char转换:避免扩展 ASCII 字符(值 > 127)因符号位导致的比较错误
- 返回正数:表示
阶段 3:定位 + 操作 —— my_strcat(字符串拼接)
需求
将源字符串src追加到目标字符串dest的末尾,拼接后的字符串以'\0'结尾。
实现策略
分两步操作:
- 用指针找到
dest的末尾(即'\0'的位置) - 从该位置开始,将
src的内容复制到dest中
代码实现
char* my_strcat(char *dest, const char *src) {
char *ret = dest; // 保存目标字符串的起始地址
// 第一步:找到dest的末尾
while (*dest != '\0') {
dest++;
}
// 第二步:从末尾开始复制src
while ((*dest = *src) != '\0') {
dest++;
src++;
}
return ret; // 返回目标字符串的起始地址
}
代码解析
- 定位目标字符串末尾:第一个
while循环移动dest指针直到*dest == '\0' - 执行拼接操作:第二个
while循环与my_strcpy逻辑相同,从dest末尾开始复制 - 自动添加结束符:当
src的'\0'被复制后,循环结束,确保结果字符串正确终止
参数与返回值设计逻辑
- 参数类型:同
my_strcpy,dest为可修改指针,src为常量指针 - 返回值类型:同
my_strcpy,返回dest起始地址支持链式操作 - 关键限制:
dest必须有足够空间容纳原内容 +src内容 +'\0'- 两个字符串不能重叠,否则会导致未定义行为
阶段 4:嵌套遍历 —— my_strstr(子串查找)
需求
在字符串haystack中查找子串needle的首次出现位置,返回该位置的指针。
实现策略
使用三层指针逻辑:
start指针:记录haystack中每次尝试匹配的起始位置h指针:遍历haystack中当前匹配的字符n指针:遍历needle中的字符进行匹配检查
代码实现
char* my_strstr(const char *haystack, const char *needle) {
// 空指针检查:防御性编程
if (!haystack || !needle) return NULL;
// 特殊情况:空字符串匹配任何字符串的起始位置
if (*needle == '\0') return (char *)haystack;
const char *start = haystack; // 记录每次匹配的起点
while (*start) {
const char *h = start; // 当前haystack位置
const char *n = needle; // 当前needle位置
// 逐字符匹配
while (*h && *n && (*h == *n)) {
h++;
n++;
}
// 如果n到达结束符,说明匹配成功
if (*n == '\0') return (char *)start;
start++; // 否则从下一个位置继续尝试
}
return NULL; // 未找到匹配
}
代码解析
-
边界条件处理:
- 空指针检查:防止
*haystack或*needle操作导致崩溃 - 空子串处理:根据标准库定义,空串匹配任何字符串的起始位置
- 空指针检查:防止
-
嵌套匹配逻辑:
- 外层循环
while (*start):遍历haystack的每个可能起始位置 - 内层循环
while (*h && *n && (*h == *n)):检查从start开始是否匹配needle
- 外层循环
-
匹配成功判断:当
n指向'\0'时,说明needle已完全匹配
参数与返回值设计逻辑
- 参数类型:两个参数都用
const,因为查找操作不修改原字符串 - 返回值类型:
- 成功:返回
haystack中匹配子串的起始位置指针 - 失败:返回
NULL - 非
const返回值:允许通过返回的指针修改原字符串(如果原字符串本身是非 const 的)
- 成功:返回
为什么必须用指针?不用指针可以实现吗?
用数组下标模拟的 "非指针" 实现
表面上可以用数组下标实现字符串函数,例如my_strlen的下标版本:
size_t my_strlen(const char str[]) { // 数组形式参数
size_t len = 0;
while (str[len] != '\0') { // 用下标访问
len++;
}
return len;
}
但实际上,数组作为函数参数时会自动退化为指针,const char str[]在编译时会被解析为const char *str。而str[len]本质是*(str + len)的语法糖,仍然是指针运算。
指针的核心优势
-
更高效率:
- 指针直接操作内存地址,省去下标计算的额外开销
p++操作比len++后再计算str[len]更直接
-
代码更简洁:
- 例如
my_strcpy的(*dest++ = *src++) != '\0'一行代码完成赋值、移动、判断 - 复杂操作(如
my_strstr的多指针追踪)用指针实现更清晰
- 例如
-
贴合内存模型:
- 字符串本质是 "连续内存块 + 结束符",指针天然适合这种线性结构的遍历
- 直接反映了数据在内存中的存储方式
-
功能不可替代:
- 某些操作(如动态内存分配的字符串处理)必须使用指针
- 标准库字符串函数的设计依赖指针实现链式操作等特性
总结
通过这 5 个字符串函数的模拟实现,我们可以看到指针在 C 语言字符串操作中的核心地位:
- 指针提供了直接访问内存的能力,完美适配 C 语言字符串的内存模型
- 每个函数的参数和返回值类型设计都有明确目的:
const用于保护数据,char*支持链式操作,size_t确保长度语义正确 - 指针操作虽然看似复杂,但比下标方式更高效、更贴近底层实现
理解指针如何操作字符串,不仅能帮助你更好地使用 C 标准库,更能建立对内存操作的直观认识 —— 这正是 C 语言的精髓所在。建议你亲手调试这些代码,观察指针在每一步的变化,体会 "内存书签" 的真正威力。
C语言指针模拟实现典型字符处理库函数
1349

被折叠的 条评论
为什么被折叠?



