文章目录
字符串函数
strlen
我们知道strlen这个库函数的工作原理是从传入的字符地址向后寻找直至找到结束标志‘\0’来计算字符串的长度的,因此想要复现这个库函数并不困难,常见的复现方式包括以下三种:
计数器实现strlen
#include<assert.h>
int my_strlen(const char* str)
{
assert(str);
int count = 0;
while (*str)
{
count++;
str++;
}
return count;
}
这种方式先判断当前指针所指内容是否为0,若不为零则向后移动指针并使count加一,否则循环结束并返回count值得到字符串长度。
递归方法实现strlen
#include<assert.h>
int my_strlen(const char* str)
{
assert(str);
if (*str == '\0')
return 0;
else
return 1 + my_strlen(str + 1);
}
递归方法省去了创建临时变量的步骤,最终返回的值就是此函数的调用次数,同时也是待测量字符串的长度。
指针计算实现strlen
#include<assert.h>
int my_strlen(char* s)
{
assert(s);
char* p = s;
while (*p != ‘\0’)
p++;
return p - s;
}
这种方法利用指针的偏移量来计算字符串长度,与第一种方法思路相同,但代码更加简洁。
strcpy
strcpy这个库函数解决了c语言中字符串之间不能相互赋值的问题,它将来源字符串中的内容覆盖到目标字符串所在的地址处,会覆盖掉目标字符串中原有内容,并且会把结束标志’\0’也拷贝过去,具体复现方式如下:
#include<assert.h>
char* my_strcpy(char* destination, char* source)
{
assert(source && destination);
char* ret = destination;
while (*destination++ = *source++)
{
;
}
*destination = *source;
return ret;
}
因为在拷贝的过程中传进去的目标地址和源头地址会移动,因此二者无需const进行修饰;此外为了能够返回传入的目标地址的首地址,开始时需要进行备份,其余就是正常地在遇到结束标志之前进行逐个字节的赋值,当跳出循环时,source指针指向的是字符串结束标志,要将其赋值给当前destination指针指向的内存以完成拷贝。
strcat
strcat函数全称为string catenate,也就是字符串连接的意思。其复现思路是找到前置字符串的末尾,从此处开始将后置字符串的每个字节拷贝过来。其复现方式如下:
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (*dest)
{
dest++;
}
while ((*dest++ = *src++))
{
;
}
return ret;
}
由于改变了传入参数也就是目标字符串的地址,因此为了保证返回值无误,也需要进行备份。
strstr
strstr函数的作用是判断一个字符串是否为另一个字符串的子串。当然,kmp算法是解决这个问题的首选,但这里我们选择一个更基础、更容易理解的方法。
通常,判断字串包含以下两种情况:
情况一:
字符串①:abcdefg
字符串②:abc
情况二:
字符串①:abbbbcdefg
字符串②:bbc
在情况一中,我们在字符串①的某个位置开始与字符串②进行逐个比较,刚好在第一次遇到和字符串②的首字符相同的情况下就完成了匹配,这种情况比较容易解决,因为无需回头在母串中继续错位查找。
在情况二中,我们在字符串①中连续遇到了三次相同的首字符,但前两次里判断到第三个字符时就会发现并不匹配,此时我们就必须回头找到下一个还没有进行判断的位置重新匹配,这种情况比较复杂,但同时更常见,所以我们要在每次判断的同时记录好匹配开始位置,若匹配失败,马上回头从失败位置的下一个位置继续尝试。
复现代码如下:
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
const char* s1 = str1;
const char* s2 = str2;
const char* p = str1;
while (*p != '\0')
{
s1 = p;
s2 = str2;
while (*s1 != '\0' && *s2 != '\0' && * s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return p;
}
p++;
}
return NULL;
}
代码中的p就是一个标兵,能够在匹配失败时找到下一个“爬起来”的地方,当p指针未指向字符串结束位置时,将p的位置赋值给指向母串的指针,然后进行循环匹配,当所指内容不匹配时跳出匹配循环,并且判断此时字串是否已经匹配完毕(字串指针指向其结束标志),若匹配完毕,则说明匹配成功,返回p所指向的位置,也就是母串中字串的起始位置,否则使指针p向后移动一个位置继续进行匹配。
strcmp
strcmp为字符串比较函数,其作用原理是逐个比较两个字符串中每个字符的大小,若出现不同字符,则进行判断并返回,若字符串①对应字符ASCII码大于字符串②中的对应字符,则返回正数,否则返回负数,若判断到两个字符串结束时每个字符均相同,则返回0。其复现方法如下:
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
return *str1 - *str2;
}
只要对应位置字符相同,就继续循环,如果在循环中遇到了某个字符已经是0了,说明两字符串完全相同,直接返回0;若跳出了循环,说明二者出现了不同的字符,则返回该不同字符的差值。
strncat
strncat加上数字n的同时函数也多了一个参数,也就是要拷贝来源函数的多少个字符,这使得字符串可以进行自我拷贝,不会像strcat那样陷入死循环。复现代码如下:
char* My_strncat(char* dest, const char* src, size_t n)
{
assert(dest);
assert(src);
char* ret = dest;
while (*dest)
{
dest++;
}
while (n--)
{
*dest++ = *src++;
}
*dest = '\0';
return ret;
}
strncpy
strncpy多出的参数决定了拷贝长度,与此同时该函数并不会在拷贝结束后添加’\0’,复现代码如下:
int StrToInt(char *str)
{
long number = 0;
int flag = 1;
if (NULL == str)
{
printf("str is NULL");
return 0;
}
while (*str == ' ')
{
str++;
}
if (*str == '-')
{
flag = -1;
str++;
}
while ((*str >= '0') && (*str <= '9'))
{
number = number * 10 + *str - '0';
str++;
}
return flag*number;
}
atoi
atoi函数会扫描参数字符串,先跳过前面的空白字符(例如空格,tab缩进)等,然后将扫描到的连续数字转换为十进制数字,如果字符串指针不能转换成int或者指针指向的是空字符串,那么将返回 0 。. 代码复现如下:
int my_atoi(char *str)
{
long number = 0;
int flag = 1;
if (NULL == str)
{
printf("str is NULL");
return 0;
}
while (*str == ' ')
{
str++;
}
if (*str == '-')
{
flag = -1;
str++;
}
while ((*str >= '0') && (*str <= '9'))
{
number = number * 10 + *str - '0';
str++;
}
return flag*number;
}
内存函数
memcpy
memcpy全称为memory copy,意味内存拷贝,和strcpy类似,但也有不同之处。这个函数共包含三个参数:目的地址,源地址,以及要拷贝的字节数,因此它也可以用来拷贝数组、结构体等其他的数据结构。其复现方式如下:
void* my_memcpy(void* dest, void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
因为char类型是c中的最小单位,所以这里我们将void*类型都强转为字符指针类型再进行逐位操作。此外,因为改变了目标内存的地址,因此也需要进行拷贝以保证返回值。
正常来讲,memcpy是不能够进行自我拷贝的。比如数组123456,如果想把123三个数字拷贝至234的位置,正常想要得到的结果应该是112356,而实际得到的结果是111156,因为在逐位拷贝的过程中改变了拷贝源头的内容,导致了拷贝出错,所以我们导入了memmove函数来解决这个问题。
memmove
由于我们发现,当字符串或者数组对于自己进行拷贝(拷贝内容有重叠)时会出现因为源头改变而导致事与愿违,所以我们对这种问题进行了仔细分析:
注:由于数组或者字符串随着其位置的后移,地址大小是不断升高的,所以后面当比较来源地址和目的地址的大小的时候能够直观地体现二者在数组或字符串本身中的相对位置。
情况一:当目的地址小于源地址时
此时我们可以采用从前向后依次赋值的方法,这样并不会改变源地址中的值,所以能够顺利完成任务:
第一次赋值1 4 3 4 5 6 7 8 9 10;
第二次赋值1 4 5 4 5 6 7 8 9 10;
第一次赋值1 4 5 6 5 6 7 8 9 10;
但是反过来从后向前赋值,就无法达到想要的效果:
第一次赋值1 2 3 6 5 6 7 8 9 10;
第二次赋值1 2 5 6 5 6 7 8 9 10;
第三次赋值1 6 5 6 5 6 7 8 9 10;
情况二:当目的地址大于源地址时
此时我们就要采用从后向前依次赋值的方法,才能够顺利完成任务:
第一次赋值1 2 3 4 5 6 7 6 9 10;
第二次赋值1 2 3 4 5 6 5 6 9 10;
第一次赋值1 2 3 4 5 4 5 6 9 10;
但是反过来从前向后赋值,就无法达到想要的效果:
第一次赋值1 2 3 4 5 4 7 8 9 10;
第二次赋值1 2 3 4 5 4 5 8 9 10;
第三次赋值1 2 3 4 5 4 5 4 9 10;
其复现代码如下:
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
if (dest < src)
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
memmove和memcpy不同之处在于拷贝之前进行了情况分流,保证自身有重叠拷贝时不会发生错误。
结束语
本篇博客进行了部分c语言库函数的讲解和复现,如有不足或遗漏之处还请大家指正,笔者感激不尽;同时也欢迎大家在评论区进行讨论,一起学习,共同进步!