hello各位小伙伴,在C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中,而字符串常量适用于那些对它不做修改的字符串函数。
因此小风觉得了解一些在C语言中经常使用的字符串处理库函数的原理和使用是非常有必要,这不仅能使我们的代码变得简洁,而且在处理某些复杂的问题是发挥着十分重要的作用,这也是小风创作这篇文章的初衷。
一、strlen计算字符串长度函数
1.函数介绍
相信大家对着个函数一定并不陌生,它的作用是计算字符串长度.
size_t strlen ( const char * str );//函数原型
- 字符串已经 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。
- 参数指向的字符串必须要以 '\0' 结束。
- 注意函数的返回值为size_t,是无符号的( 易错 )
在这里希望大家能够注意上述第三点的内容,例如下面这段代码
#include <stdio.h>
int main()
{
const char* str1 = "abcdef";
const char* str2 = "bbb";
if (strlen(str2) - strlen(str1) > 0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
在这里小风相信汗多小伙伴们都觉得难以置信,这是因为 strlen函数的计算结果是一个无符号整型的数据,而两个同类型的数据运算的结果仍是同类型的,因此无论两个strlen的数是何种的大小关系,系统都将最后得出的结果认为是一个无符号数(也即非负数)。
2.函数模拟实现
通过上述对该函数的描述,我们不妨自己实现一下该函数的功能吧!
方法一(普通方式)
//计数器方式
unsigned int my_strlen(const char* str)
{
assert(str);//断言一下,防止字符串为NULL,需添加头文件<assert.h>
int count = 0;
while (*str)
{
count++;
str++;
}
return count;
}
方法二(递归实现)
//不创建临时变量计数器
int my_strlen(const char * str)
{
assert(str);
if(*str == '\0')
return 0;
else
return 1+my_strlen(str+1);
}
方法三(使用指针减指针)
之所以可以使用这种方法,是因为同类型的指针计算的是单位距离是按照本身指向大小空间来进行换算的(即:char*将每一个字节算作是一个单位距离,int*将每四个字节算作是一个单位距离等等其他指针同样递推)。
//指针-指针的方式
int my_strlen(char* s)
{
assert(s);
char* p = s;
while (*p != ‘\0’)
p++;
return p - s;
}
二、strcpy复制函数
1.函数介绍
char* strcpy(char * destination, const char * source );//函数原型
- 源字符串(source)必须以 '\0' 结束。
- 会将源字符串中的 '\0' 拷贝到目标空间(destination)。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
2.函数模拟实现
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);
while (*dest++ = *src++)
{
;
}
return ret;
}
值得注意的上述代码中的while判断内容,不仅进行了赋值操作,同时也判断了复制过程是否继续下去的双向操作,整段代码精简而又凝练,值得我们大家好好理解其中的原理并运用于实践中。
三、strcat拼接字符串函数
1.函数介绍
strcat函数的作用是将源字符串拼接到目标字符串的末尾。
char * strcat ( char * destination, const char * source );//代码框架
- 源字符串必须以 '\0' 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
2.函数实现
在实现该函数的过程中我们应该注意函数的返回参数是指针(即地址),在使用该函数时由于可以直接打印整个字符串的内容,所以该函数返回的应该是字符串的首元素地址,在函数的模拟过程中我们应该记录这一位置。
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);
while (*dest)
{
dest++;
}
while (*dest++ = *src++)
{
;
}
return ret;
}
在这里小风想给大家留下一个思考题:其实这段代码也存在一个小小的问题,那就是如果拼接的字符串是其本身时,程序将会进入一个死循环之中,那么我们应该如何解决这个问题呢?
小风在自己的电脑上使用VS编译器调用原函数并没有出现该类问题,这应该是编译器另外做了一些修饰,但这并不是说我们上面的这段代码是错误的,因为我们是按照C语言标准中规定的功能来实现的,所以希望大家在使用这个函数的时候应尽量避免出现调用自身的情况吧!
四、strstr查找子串函数
1.函数介绍
该函数的功能是查找字符串中是否含有我们想要查找的字符串。
如果包含,则返回指向首次出现位置的指针;否则则返回空指针。
char * strstr ( const char *str1, const char * str2);//函数原型
例如下面这段实例代码
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "This is a simple string";
char* pch;
pch = strstr(str, "simple");
puts(pch);
return 0;
}
2.函数模拟实现
下面这段代码采用的时暴力求解法,如果有兴趣的小伙伴们可以采用更为高效的KMP算法来进行实现。
char* strstr(const char* str1, const char* str2)
{
char* cp = (char*)str1;
char* s1, * s2;
if (!*str2)
return((char*)str1);
while (*cp)
{
s1 = cp;
s2 = (char*)str2;
while (*s1 && *s2 && !(*s1 - *s2))
s1++, s2++;
if (!*s2)
return(cp);
cp++;
}
return(NULL);
}
小风考虑到上述的这段代码相较于之前可能比较复杂,比较难以理解整个实现的思路。在这里小风打算为大家进行比较细致的讲解:
- 首先是准备工作,除了参数传递的str1和str2指针,我们还另外创建了cp、s1、s2三个指针,具体作用如下面图解所示
- 指针s1与s2在扫描的过程中是同步变化的,它们的起始位置都是指向各自字符串的首元素地址
- cp是一个控制指针,初始位置也是自字符串的首元素地址,每当s1与s2指向的内容不匹配时,此时cp指针便相后移动一位,并且s1指向的位置回调指向cp指针
- 当s2指向'\0'时,说明str1包含str2,则返回cp指针;如果当cp指向'\0'时,则说明str1不包含str2,此时则返回NULL
五、strcmp字符串比较函数
1.函数介绍
该函数适用于连个字符串的比较大小(ASCII)的函数,逐个对字符串中的每个符号比较,如果比较的两个字符相同则向后继续遍历直至一方结束为止。
int strcmp ( const char * str1, const char * str2 );//函数原型
标准规定:
- 第一个字符串大于第二个字符串,则返回大于0的数字
- 第一个字符串等于第二个字符串,则返回0
- 第一个字符串小于第二个字符串,则返回小于0的数字
2.函数模拟实现
int my_strcmp(const char* src, const char* dst)
{
int ret = 0;
assert(src && dst);
//开始遍历比较
while (!(ret = *(unsigned char*)src - *(unsigned char*)dst) && *dst)
++src, ++dst;
if (ret < 0)
ret = -1; //注意这里并没有明确规定具体的大小,只需小于0即可
else if (ret > 0)
ret = 1; //同上
return ret;
}
六、memcpy内存拷贝函数
1.函数介绍
如果细心的小伙伴和内容发现,该函数与strcpy函数长得有点像哈,而且好像功能也好像有点类似。想必大家心中都有一些疑问这两者之间有什么区别和联系呢?
首先memcpy和strcpy都是对索要操作的对象实现复制的一个功能,但二者之间的一个不同点在于strcpy函数它的操作对象只能是字符串,而memcpy的功能更加强大,他可以对任意数据类型的数组进行操作。
而接收任意参数类型的功能最重要的试因为它的形式参数采用的是void*进行接收的。
以下是对于空类型指针(void*)的介绍:
- 它能用于接收任意类型的数据
- 再使用空类型的指针进行加减运算时,一定要提前将他们强制类型转换为字符指针(char*)
void * memcpy ( void * destination, const void * source, size_t num );//函数原型
标准规定:
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到 '\0' 的时候并不会停下来。
- 如果source和destination有任何的重叠,复制的结果都是未定义的。
希望大家特别重视一下标准规定中的最后一点,因为重叠的时候可能重复的现象,如果要解决这个问题,可以使用memmove函数,可以说该函数是对memcpy的进一步完善。
下面一段代码展示的是对memcpy函数应用
/* memcpy example */
#include <stdio.h>
#include <string.h>
struct {
char name[40];
int age;
} person, person_copy;
int main()
{
char myname[] = "Pierre de Fermat";
/* using memcpy to copy string: */
memcpy(person.name, myname, strlen(myname) + 1);
person.age = 46;
/* using memcpy to copy structure: */
memcpy(&person_copy, &person, sizeof(person));
printf("person_copy: %s, %d \n", person_copy.name, person_copy.age);
return 0;
}
2.函数模拟实现
void* memcpy(void* dst, const void* src, size_t count)
{
void* ret = dst;
assert(dst);
assert(src);
/*
* copy from lower addresses to higher addresses
*/
while (count--) {
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
return(ret);
}
七、memmove平移复制函数
1.函数介绍
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
void * memmove ( void * destination, const void * source, size_t num );//函数原型
2.函数模拟实现
那么memmove函数是如何解决memcpy留下的这个问题呢?
请看下面这段代码
void* memmove(void* dst, const void* src, size_t count)
{
void* ret = dst;
//应对重叠机制
//1.dst不在src+count范围内(不包括边界),则选择从前缀开始赋值
if (dst <= src || (char*)dst >= ((char*)src + count)) {
while (count--) {
*(char*)dst = *(char*)src;
dst = (char*)dst + 1;
src = (char*)src + 1;
}
}
//2.dst在src+count范围内,则选择从末尾开始赋值
else {
dst = (char*)dst + count - 1;
src = (char*)src + count - 1;
while (count--) {
*(char*)dst = *(char*)src;
dst = (char*)dst - 1;
src = (char*)src - 1;
}
}
return ret;
}
总的来说这一块的内容还是稍微有一点抽象的,希望大家能对比参照一下memcpy函数模拟的代码进行理解,同时不妨和小风一样学着画图一起理解,这样能加深我们对函数功能的印象和理解。