C 语言字符串库函数模拟实现

字符串检验

strlen

函数原型

/// @brief 返回给定空终止字符串的长度,不包含 '\0'
/// @param str 指向要检测的字符串
/// @return 字符串 str 的长度
size_t strlen(const char* str);

空终止字符串即 C 语言中以 ‘\0’ 作为终止符的字符串,strlen() 计算字符串的长度并返回,返回的长度不包含 ‘\0’。

char arr[] = "qgw";
// num 的值是 3
int num = strlen(arr);

模拟实现

下面的实现方式比较简单,采用遍历字符串的方式,遇到 ‘\0’ 就退出循环,返回结果。

size_t my_strlen(const char* str) {
  size_t cnt = 0;
  while (*str != '\0') {
    ++str;
    ++cnt;
  }
  return cnt;
}

效率陷阱

假设我们有一个函数,可以将给定字符串中大写字母转化为小写。

void lower(char* s) {
  for (size_t i = 0; i < strlen(s); ++i) {
    if (s[i] >= 'A' && s[i] <= 'Z') {
      s[i] += 'a' - 'A';
    }
  }
}

你能看出来哪里有问题吗?能的话知道为什么会有这个问题吗?

问题很明显出在 strlen(s) 这,它的复杂度是 O ( n ) O(n) O(n) 的,上面这个函数整体的复杂度也就变为 O ( n 2 ) O(n^2) O(n2) 了。

之前我一直以为这没有问题,因为字符串的长度不变,编译器应该会优化这段代码,调用一次 strlen() 然后将结果保存重复使用。

在理想的世界里,编译器会认出循环测试中对 strlen 的每次调用都会返回相同的结果,因此应该能够把这个调用移出循环。那问题出在哪了,为什么编译器不进行优化呢?

主要有两个方面的原因:

  1. strlen() 会检查字符串的元素,而随着 lower() 的进行,字符串会改变,编译器必须小心仔细的分析,知道即使字符串会改变,从 strlen() 获得的结果也不会改变,这在编译时期是很难得知的
  2. 编译器不知道该调用哪个版本的 strlen(),文件是单独编译的,链接的时候才确定使用哪个函数,有可能我们使用的是自己的 strlen(),它还会做一些额外的事情,这是编译器无法确定的

所以我们应该将上述代码改为:

void lower(char* s) {
  int n = strlen(s);
  for (size_t i = 0; i < n; ++i) {
    if (s[i] >= 'A' && s[i] <= 'Z') {
      // 'a' - 'A' 是个常量值,这个编译器是可以优化的
      s[i] += 'a' - 'A';
    }
  }
}

strcmp

函数原型

/// @brief 以字典序比较两个字符串
/// @param lhs rhs 要比较的两字符串
/// @return 相等返回 0
int strcmp(const char* lhs, const char* rhs);

返回值:

  • 两字符串字典序相同,返回 0
  • lhs 字典序大于 rhs,返回一个正数
  • lhs 字典序小于 rhs,返回一个负数

模拟实现

实现的思路是:遍历两个字符串,直到出现不同有字符串结束

res 存储两字符串比较的结果,如果等于 0 循环继续,不等于 0 直接退出,lhs 指向的字符大于 rhs 指向的字符就为正数,否则反之。

若 lhs 先结束,rhs 还没结束,此时 res 为负数,‘\0’ 的 ASCII 码为 0,是最小的。若 rhs 先结束,lhs 还没结束,此时 res 为正数。若同时结束,res 刚好为 0 返回。

int my_strcmp(const char* lhs, const char* rhs) {
  int res = 0;
  while ((res = *lhs - *rhs) == 0 && *lhs != '\0' && *rhs != '\0') {
    ++lhs;
    ++rhs;
  }
  return res;
}

strstr

函数原型

/// @brief 在 str 中查找 substr 子串
/// @param str 指向要检验的空终止字符串
/// @param substr 指向要查找的空终止字节字符串
/// @return 指向于 str 中找到的子串首字符,或若找不到该子串则为空指针
char* strstr(const char* str, const char* substr);

若 substr 指向空,则会返回 str。

模拟实现

下面实现的方法为暴力匹配子串,实际中可以使用 KMP 或 BM 等字符串搜索算法优化这一过程。

char* my_strstr(const char* str, const char* substr) {
  if (substr == NULL) {
    return str;
  }
  
  // 遍历 str 所有字符,看以其起始字符是否匹配
  while (*str != '\0') {
    const char* backStr = str;
    const char* backSub = substr;
    while (*backStr != '\0' && 
          *backSub != '\0' && 
          *backStr == *backSub) {
      ++backStr;
      ++backSub;
    }
    // 如果 substr 走完了,说明匹配成功了,返回此时的 str
    if (*backSub == '\0') {
      return str;
    }
    ++str;
  }
  return NULL;
}

字符串操作

strcpy

函数原型

/// @brief 复制 src 所指向的空终止字符串,包含空终止符,到首元素为 dest 所指的字符数组
/// @param dest 指向要写入的字符数组的指针
/// @param src 指向要复制的空终止字符串
/// @return 返回 dest 的副本
char* strcpy(char* dest, const char* src);

需要注意的是:

  • 若 dest 数组长度不足则行为未定义
    • 即 dest 数组不足以包含 src 中所有元素,此时大概率会因越届访问崩溃
  • 若字符串覆盖则行为未定义
    • 现在主流编译器的实现都可以处理有覆盖的情况,如:MSVC、GCC
  • 若 dest 不是指向字符数组的指针或 src 不是指向空终止字符串的指针则行为未定义

模拟实现

下面的方法先记录要返回的地址,最后遍历 src 遇到 ‘\0’,此时退出循环。

注意:该实现方法并不能解决字符串有覆盖的情况。

char* my_strcpy(char* dest, const char* src) {
	char* res = dest;
	while (*dest = *src) {
		++dest;
		++src;
	}
	return res;
}

strcat

函数原型

/// @brief 后附 src 所指向的空终止字符串的副本到 dest 所指向的空终止字符串的结尾
/// @param dest 指向要后附到的空终止字符串
/// @param src 指向作为复制来源的空终止字符串
/// @return 返回 dest 的副本
char* strcat(char* dest, const char* src);

需要注意的是:

  • 会用字符 src[0] 替换 dest 末尾的 ‘\0’
  • 若目标数组对于 src 和 dest 的内容以及空终止符不够大,则行为未定义
  • 若字符串重叠,则行为未定义
  • 若 dest 或 src 不是指向空终止字符串的指针,则行为未定义

模拟实现

因为会先用 src 第一个字符替换 dest 结尾的 ‘\0’,所以要先找到 dest 结尾 ‘\0’。然后再遍历 src,将其添加到 dest 结尾。

char* my_strcat(char* dest, const char* src) {
	char res = dest;
	// 先找 \0 的位置
	while (*dest != '\0') {
		++dest;
	}
	// 追加到 dest 后面
	while (*dest = *src) {
		++dest;
		++src;
	}
	return res;
}

内存操作

memcpy

函数原型

/// @brief 从 src 所指向的对象复制 count 个字节到 dest 所指向的对象
/// @param dest 指向要复制的对象
/// @param src 指向复制来源对象
/// @param count 复制的字节数
/// @return 返回 dest 的副本
void* memcpy(void* dest, const void* src, size_t count);

要注意的是:

  • 若访问发生在 dest 数组结尾后则行为未定义
  • 若对象重叠,则行为未定义
    • 也就是说在标准中 memcpy() 也不能处理对象重叠的情况
  • 若 dest 或 src 为非法或空指针则行为未定义

模拟实现

下面给出的实现方式与 strcpy() 相似,不过是改用 count 变量来控制循环次数。

memcpy() 是最快的内存到内存复制子程序。它通常比必须扫描其所复制数据的 strcpy(),或必须预防以处理重叠输入的 memmove() 更高效。

void* my_memcpy(void* dest, const void* src, size_t count) {
	void* res = dest;
	while (count--) {
		*(char*)dest = *(char*)src;
		++(char*)dest;
		++(char*)src;
	}
	return res;
}

memmove

函数原型

/// @brief 从 src 所指向的对象复制 count 个字节到 dest 所指向的对象
/// @param dest 指向要复制的对象
/// @param src 指向复制来源对象
/// @param count 复制的字节数
/// @return 返回 dest 的副本
void* memmove(void* dest, const void* src, size_t count);

memmove() 是不是看起来和 memcpy() 一模一样,但 memmove() 可以处理内存重叠的情况,这也就说明它要做一些检查,来保证能够处理内存重叠的情况。

  1. 无重叠
    • 直接调用更高效的 memcpy()
  2. 有重叠,dest 在 src 之前,正常正向复制

Forward replication

  1. 有重叠,dest 在 src 之后,需要反向复制

foreard copy

reverse copy

模拟实现

通过对上图的观察,我们可以发现:若 dest 在 src 的后面并且存在内存重叠,就需要反向复制。

我们可以简化这一函数,对无重叠的情况不去调用 memcpy() 而是包含在下面两种情况中。

  1. dest 在 src 之前,采用正向复制
  2. dest 在 src 之后,采用反向复制
void* my_memmove(void* dest, const void* src, size_t count) {
	void* res = dest;
	if (dest < src) {
		while (count--) {
			*(char*)dest = *(char*)src;
			++(char*)dest;
			++(char*)src;
		}
	} else {
		while (count--) {
			*((char*)dest + count) = *((char*)src + count);
		}
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值