妈呀,我终于写完博客了!!!
我们在学习c语言,学习编程的过程中,对于其中的许多算法,知识点既要知其然,也要知其所以然,既要知道用这些算法,知识点写程序,实现相应的功能,也要知道为什么要这样写,这样的优点是什么,原理是什么。我们在学习过程中学习的是思想,方法,而不是代码。c语言有许多库函数,方便程序员的使用,但对于初学者为了很好的使用这些库函数,一方面我们要查看c语言使用手册了解其用法,另一方面我们也可以通过模拟实现这些库函数或者看其源码,从原理上来看它们是怎么实现的。因此本篇博文的主要内容就是字符串库函数,内存操作函数的介绍及其模拟实现
函数原型说明
size_t strlen ( const char * str );
- 字符串已经以’\0’ 作为结束标志,strlen函数返回的是在字符串中’\0’ 前面出现的字符个数(不包含’\0’ )。
- 参数指向的字符串必须要以’\0’ 结束。 注意函数的返回值为size_t,是无符号的( 易错)
- 函数参数是字符指针,也就是传过要计算长度的字符串首字符的首地址,并用const修饰,表示这个参数只能读,不希望被修改,不能修改原来的字符串,使代码更安全。
库函数的实现
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{
char arr1[] = "abc";
char arr2[] = { 'a', 'b', 'c' };
char arr3[] = { 'a', 'b', 'c', '\0'};
int len1 = strlen(arr1);
int len2= strlen(arr2);
int len3 = strlen(arr3);
printf("%d\n", len1);
printf("%d\n", len2);
printf("%d\n", len3);
return 0;
}
C语言中对字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。 上面创建了三个字符数组,arr1里面存放了4个元素包括‘\0’,是一个标准的字符串,arr2分别放了a,b,c三个元素没有\0,strlen函数在统计完数组元素个数后,继续在后面的内存空间里面寻找,随机的找到\0结束,因此打印结果也是随机值,arr3里面四个元素,后面那个\0是主动添加来进行验证的。程序运行结果如图:
结下来我们来看下面的代码结果是什么的????小心陷阱哦。
#include
int main()
{
char str1 []= "hello";
char str2 []= "boy";
if (strlen(str2) - strlen(str1) > 0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
解释:
库里面的strlen的返回值为size_t,返回的是一个无符号数,也就是没有正负之分,因此对于数据的存储打印很像正数(原反补相同),之后原码打印。首先求得的字符个数相减,还是转换为补码相减,减得一个负数,最高位的符号位为1,之后转化为无符号的数字就是一个很大的数字了,大概为2的30次方左右,大于0,所以程序的结果为str2>str1。这里的借用这个代码其实是为强调strlen函数返回值的类型
模拟实现:
这里的模拟实现思路和库函数原理基本一样,就是找\0之前的字符,找到一个字符,计数器count的值加一,找到\0就结束寻找,之后count 的值就是字符的个数。
在这里插入代码片#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include
#include
size_t my_strlen(const char *str)
{
assert(NULL);//断言一下防止是空指针,为空指针程序自动崩溃
int count = 0;
while (*str)
{
count++;
str++;
}
return count;//返回值打印
}
int main()
{
int ret = 0;
char arr[] = "abcde";
ret = my_strlen(arr);//函数调用
printf("%d", ret);
return 0;
}
函数原型说明
char* strcpy(char * destination, const char * source );
Copies the C string pointed by source into the array pointed by
destination, including the terminating null character (and stopping at
that point).
- 源字符串必须以’\0’ 结束。
- 会将源字符串中的’\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
通过以下代码,我们可以简单的使用strcpy函数,并且拿捏一下细节,放开解释中文后面的代码,运行下,探讨一下小细节。
//#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{
//源字符串必须以'\0' 结束
//会将源字符串中的'\0' 拷贝到目标空间。
char arr[20] = "##########";
strcpy(arr, "hello");//string copy,通过调试观察字符串末尾的\0是否被拷贝到目标空间
//char arr2[] = { 'a', 'b', 'c' };//\0作为拷贝结束的标志,arr2里面没有\0,导致strcpy一直向arr2后面的内存空间拷贝字符
//拷贝了许多字符到arr里面,撑爆了arr数组,导致数组越界,程序崩溃
//strcpy(arr, arr2);//string copy
printf("%s\n", arr);
_______________________________________________________________________________________
//目标空间必须足够大,以确保能存放源字符串。
//char arr[5] = "####";
//char p[] = "hello world";//源字符串太大,不能拷贝到目标空间里面,强行可以烤进去,但程序也挂了
//strcpy(arr, p);//
//printf("%s\n", arr);
_______________________________________________________________________________________
//目标空间必须可变
//char str [15]="$$$$$$$$$$$$$$$"//字符数组,创建字符变量,正常运行
//char *str="$$$$$$$$$"//常量字符串,不可以修改(常量),复制过去,如果运行程序奔溃
//char p[] = "hello world";
//strcpy(str, p);
//
//
// return 0;
//}
如放开横线上第一段代码,调试结果如下;其余的大家自己去试试吧,说多少不练都是扯蛋。
模拟实现
基本思路就是定义两个指针变量,一个指向源字符串首地址,一个指向目标字符串\0的位置,从源字符串,通过指针方式,拷贝一个字符到目标地址,之后两颗指针都向后移指向后面的一个位置,继续拷贝,直到把源字符串的\0拷过去为止。形成一个标准的字符串。
#include
#include
char* my_strcpy(char* dest, const char* src)//两个指针分别接收两个数组首元素的地址,
{
assert(dest != NULL);
assert(src != NULL);
char* p = dest;
//方法一:
//while (*src != '\0')
//{
// *dest = *src;
// dest++;
// src++;
//}
//*dest = *src;
//方法二:
while (*dest++ = *src++)//后置++用再加,拷完\0结束
{
;
}
return p;
}
int main()
{
char arr1[] = "##########";
char arr2[] = "hello";
my_strcpy(arr1, arr2);//模拟实现strcpy函数
printf("%s", arr1);
return 0;
}
函数原型说明: char * strcat ( char * destination, const char * source );
- 源字符串必须以’\0’ 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
库函数实现代码及细节探究
#include
#include
int main()
{
char arr1[30] = "hello";
//arr1空间需要足够大来接收追加过来的内容
//否则会造成越界访问
char arr2[] = "world";
//将字符串arr2内容追加给arr1
strcat(arr1, arr2);
printf("%s", arr1);
return 0;
}
是否追加\0探究代码
在这里插入代码片#include<stdio.h>
#include
int main()
{
char arr1[30] = "hello\0########";//调试看是否追加\0, 因为追加是从\0的位置开始的,主动放个\0之后调试好观察,
//否则从最后一个#追加你如何观察呢?
char arr2[] = "world";
strcat(arr1, arr2);
printf("%s", arr1);
return 0;
}
strcat函数进行字符串追加的算法分析及my_strcat的模拟实现:
1.定义两个指针变量分别指向,目标数组,源数组
2.dest指针找到目的空间中的’\0地址’
3.追加源字符串,包含\0 拷贝成功,返回的目标空间的起始地址,即可打印拷贝的字符串了。
模拟实现代码:
#include
#include
char* my_strcat(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* dest_start = dest;
//1.找到目的空间中的'\0'
while (*dest != '\0')//跳过不是'\0'的字符
{
dest++;
}
//2追加源字符串,包含\0
while (*dest++ = *src++)
{
;
}
return dest_start;//3返回的目标空间的起始地址
}
int main()
{
char arr1[30] = "hello";
char arr2[] = "world";
my_strcat(arr1, arr2);//模拟实现strcat
printf("%s", arr1);
return 0;
}
函数原型说明:
int strcmp ( const char * str1, const char * str2 ); This function
starts comparing the first character of each string. If they are equal
to each other, it continues with the following pairs until the
characters differ or until a terminating null-character is reached.
标准规定: 第一个字符串大于第二个字符串,则返回大于0的数字 第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
vs编译器里面strcmp函数的使用,vs编译器里面设计的是,第一个字符串大于第二个字符串,则返回1的数字,第一个字符串等于第二个字符串则返回0,第一个字符串小于第二个字符串,则返-1数字,不同编译器有所差异,我们知道就行,之后写代码最好按得标准写就行。下面代码的结果为-1,0,1。
在这里插入代码片#include<stdio.h>
#include
int main()
{
//if ("obc" > "abcdef")//不能这样比较,实际在比较两个字符的地址
//{
//}
//strcmp - 字符串比较大小的
int ret1 = strcmp("abbb", "abq");//<0
int ret2 = strcmp("aaa", "aaa");//=0
int ret3 = strcmp("aab", "aaa");//>0
printf("%d\n", ret1);
printf("%d\n", ret2);
printf("%d\n", ret3);
return 0;
}
my_strcmp的模拟实现计算法分析:
方法:
1.传过两个指针s1,s2分别指向两个字符串的首地址字符,从首字符向后一一比较,比较的是字符的ASCALL码值,以第一个字符串为比较标准。
2.两个字符要么相等,要么不等,不等,很好可以比较大小了,两个字符相减得到一个数字判断字符串的大小,如果某一字符为\0,结束比较(此时\0和一个字符也可以比较大小),如果不为\0两个指针分别向后移动比较,如此循环下去。
#include
#include
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 == *s2)
{
if (*s1 == '\0')
{
return 0;
}
s1++;
s2++;
}
return *s1 - *s2;//两个字符不相等肯定有大小之分,根据减出数字大小判断两个字符串大小
}
int main()
{
char* p = "abcd";
char* q = "abc";
int ret = my_strcmp(p, q);
if (ret > 0)
{
printf("p > q\n");//象征性的表示p指向的字符串比q指向的字符串大
}
else if (ret < 0)
{
printf("p < q\n");
}
else
{
printf("p == q\n");
}
return 0;
}
函数原型说明: char * strstr ( char * str1, const char * str2 );;
Returns a pointer to the first occurrence of str2 in str1, or a null
pointer if str2 is not part of str1.
【参数1】str1用于查找字符串的母串。
【参数2】str2待查找字符串的子串。
- *函数返回类型:char ,返回查找到的地址
- 函数功能:查找一个字符串是否属于另一个字符串
- 如果在str1中找到了字符串str2,则返回str1中字串str2的起始地址 如果查找不到,返回空指针NULL
- 如果str1中含有多个str2字符串中字符内容,返回的是str2中第一个被查找到的字符地址起,从那个位置开始打印后面的字符串。 库函数的实现: 横线上面的结果为abcabbc,下面的结果为(null)空指针。**
在这里插入代码片#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{
char* p = "abcabbc";//
//char* q = "abcbbc";
char* q = "abc";
strstr( p, q);
//_________________________________________
//char* p = "abcabbc";//
//char* q = "abcbbc";
// strstr(p, q);
printf("%s\n", strstr(p, q));
return 0;
}
my_strstr的模拟实现及算法分析:
一个字符串判断是否属于另一个字符串,首先这个字符串要不为空字符串,如果为空字符串,直接返回母字符串的内容,否则比较字符内容。在母字符串字符比较内容不为0的情况下比较(你想要是一个母字符串的内容比要比较的子串的内容都小这个字符串是绝对不会属于那个母字符串的内容的),之后比较,如果比较的字符串第一个字符相同,则都向后移动比较,相同继续下去,如果比较的子字符串为\0证明比完了,这个字符也属于母字符串,返回母字符串比较元素的起始位置开始打印字符。比较内容不相等,重新下一乱比较,母字符串返回到开始比较的下一个位置,而子字符串每次都是从起始位置比较的。因此该模拟代码定义了五个指针变量,s1,s2用于向后依次移动比较字符,cp用于记录母字符串的比较位置,str2用于子字符串与母字符串比较时,某一位置比较不成功,回退下一轮比较时,重新指向下一个比较位置。这是对模拟代码的大致解释。
#include
#include
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
char* cp = str1;
char* s1 = NULL;
char* s2 = NULL;
if (*str2 == '\0')
return (char*)str1;
while (*cp != '\0')
{
s1 = cp;
s2 = str2;
while ((*s1 == *s2) && (*s1 != '\0') && (*s2!= '\0'))
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cp;//找到字串的情况
}
cp++;
}
return NULL;//找不到字符的情况
}
动图模拟:
后面带n的字符串操作函数,在使用时要考虑比较,追加,复制等的长度,更加完善一点,但实现思路基本一致。
库函数实现代码:
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{//情况一
//char arr1[20] = "abcdef";
//char arr2[] = "qwer";
//strncpy(arr1, arr2, 6);
//printf("%s\n", arr1);//qwer
//________________________________________________________
//情况二
char arr1[20] = "abcdef";
char arr3[] = "qwer";
strncpy(arr1, arr3, 4);
printf("%s\n", arr1); //结果qwerabcdef
return 0;
}
在这里插入代码片#include<stdio.h>
#include
//模拟实现strncpy
char* my_strncpy(char* dest, const char* src, size_t n)
{
char* dest_start = dest;
while ((n > 0) && (*src != '\0'))
{
*dest = *src;
dest++;
src++;
n--;
}
while (n > 0)
{
*dest = '\0';
dest++;
n--;
}
return dest_start;
}
int main()
{
char arr1[10] = "abcdefg";
char arr2[] = "1234";
my_strncpy(arr1, arr2, 4);
printf("%s", arr1);
return 0;
}
函数原型说明:
char * strncat ( char * destination, const char * source, size_t num
); Appends the first num characters of source to destination, plus a
terminating null-character. If the length of the C string in source is
less than num, only the content up to the terminating nullcharacter is
copied.strncmp
这个函数和strcat很相似,其他参数基本一样只是增加了一个追加参数的限制num,当strncat追加时,分两种情况追加:
1.如果追加的字符数大于源字符串,追加完源字符串的\0即可,
2.如果追加的字符串小于源字符串,追加完需要追加的字符后,编译器自己在后面再追加一个\0即可。下面的代码就大致解释了它的功能。 strncat库函数的代码实现:
在这里插入代码片#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{//情况一
/*char arr1[20] = "hello ";
char arr2[] = "world";
strncat(arr1, arr2, 10);
printf("%s\n", arr1);*//结果hello world
//_______________________________________________________________________
//情况二
char arr1[20] = "hello ";
char arr2[] = "world";
strncat(arr1, arr2, 3);
printf("%s\n", arr1);//结果hello wor
return 0;
}
算法简单分析:
如图:
情况一
情况二
my_strnca的模拟实现
#include
#include
char* my_strncat(char* dest, const char* src, size_t num)
{
char* s = dest;
//dest找到\0的位置
while (*dest!='\0')
{
dest++;
}
while (num)
{
//将src中的字符追加给dest
*dest = *src;
dest++;
src++;
//如果src以及指向\0的位置,提前结束循环
if (*src == '\0')
{
break;
}
num--;
}
*dest = '\0';//字符个数追加完毕后,再单独追加'\0'
return s;
}
int main()
{
char arr1[15] = "1234a";
char arr2[] = "abcd";
my_strncat(arr1, arr2,4 );
printf("%s", arr1);//结果1234aabcd
return 0;
}
函数原型说明: int strncmp ( const char * str1, const char * str2, size_t num> ); 比较到出现两个字符不一样或者一个字符串结束(遇到\0)或者num个字符全部比较完。
这个函数加了一个比较个数的限制,是一个字符一个字符的比较,其他和strcmp相似,相对容易。
返回值解释: 方法:比较p1指向的内容与p2指向的内容
(1)如果相等返回0
(2)如果p1指向的内容大于p2指向的内容,返回一个大于0的数,
(3)如果p1指向的内容小于p2指向的内容,返回一个小于0的数
此时图中字符为例‘a’=‘a’, ‘a’=‘a’, ‘b’<'c’所以p1
在这里插入代码片#include<stdio.h>
#include
int main()
{
char arr1[] = "aab";
char arr2[] = "aac";
int ret = 0;
ret = strncmp(arr1, arr2, 3);
if (ret > 0)
{
printf("arr1 > arr2\n");
}
else if (ret == 0)
{
printf("arr1 = arr2\n");
}
else
{
printf("arr1 < arr2\n");
}
return 0;
}
模拟实现可以自己去看下源码实现下,或者自己写一个,(你自己不去尝试只看别人的,那知识永远是别人的)
前面我们介绍的是操作字符串的库函数,字符串拷贝可以使用strcpy函数,追加可以使用strncat函数,还有其它的库函数,这些函数只是针对字符串的,对于整形,结构体,浮点数等的拷贝,追加什么的就不能实现,为什么呢?其实有很多原因你想,首先我们看上面看上面函数原型时,调用函数时,他们的参数都是用char *的指针接收,这和其它数据类型就不符合,其次这些字符串操作函数对\0极其敏感(以\0作为结束标志或者以\0作为开始追加位置)所以很容易出错。最后你操作这些数据的大小如何设置呢????以什么为单位?所以接下来我们一起来了解内存操作函数,他们是操作内存块来实现对各种类型数据,进行拷贝,移动,比较的,我们在调用时指定操作的内存块大小,这个大小是以最小的数据类型大小char为基本单位的,要操作多大的数据,操作几个字节就行
函数原型说明: void * memcpy ( void * destination, const void * source, size_t num );
函数参数:
参数1:chardestination, 表示拷贝的目的地址
参数2:charsource,,表示内存拷贝的起始地址
参数3:num,类型:size_t,表示拷贝内存字节的个数(拷贝个数为非负数)
函数返回类型: void*, 实际上就是返回destination(目的地)的起始位置
注意事项
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到’\0’ 的时候并不会停下来。
拷贝的时候数据的大小也不能超过目的空间所能容纳的大小
**c标注按规定这个函数是不可以拷贝源地址和拷贝的目的地相重合的拷贝情况的。
由于我们这个函数任何数据类型都能拷贝,所以函数调用传参时我们用void xxx的指针接收,传过来的两个参数,在对void 修饰的指针进行操作前要先强制转换我我们想得到的类型。
这里直接模拟实现memcpy 函数了,库函数和它一样的用。(但vs里的这个函数设计的也可也拷贝重叠部分),我们这里是按照c标准进行的模拟。
void* my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[5] = { 1,2,3,4,5};
int arr2[7] = { 0 };
my_memcpy(arr2, arr1, 16);//将1,2,3,4拷贝到arr2数组里面,可以自己调试看看
return 0;
}
函数原型说明:
void * memmove ( void * destination, const void * source, size_t num ); 函数参数:
参数1:destination, 类型:char* ,表示内存移动的目的地址
参数2:source,类型:char* ,表示内存移动的起始地址
参数3:num,类型:size_t,表示移动内存字节的个数 函数返回类型:
void*, 实际上就是返回destination(目的地)的起始位置
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。如果使用之前的memcpy,拷贝就可能出错,假设我们有一个整型数组 1 2 3 4 5 67 8 9 10 ,如果我们想要将前5个数字拷贝到第3 - 8个位置上,如图:
#include
#include
void* my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int i = 0;
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
//
my_memcpy(arr1+2, arr1, 20);
int sz = sizeof(arr1) / sizeof(arr1[0]);
for (i = 0; i < sz; i++)
{
printf("%d ",arr1[i]);
}
return 0;
}
结果如下:
memmove函数在处理源内存块和目标内存块重叠时,为了避免正真需要被拷贝的数据提前被覆盖的情况,导致拷贝出错,我们在拷贝数据时应该遵照如下原则:
假设我创建一个含有10个元素的字符数组,拷贝的数据大小num=5byte,拷贝数据的首地址假设为c的地址,拷贝到的目的内存块里面数据元素的首地址我们随机假设,之后验证我们的拷贝法则。
我举一种从后向前拷贝的情况,这种从后向前的拷贝,不是向从前拷贝那样,不是把数据从源地址起始位置放在目标空间的首地址,之后继续向后拷贝,而是从尾巴后面拷贝,也就是找到要拷贝数据的末尾位置,从末尾位置拷贝数据到目标内存块最后一个位置,慢慢向数据起始位置移动拷贝。
其余情况根据移动法则自己探究下,自己设置不同的dest和src的关系,动手移一下,这样更好。
memmove模拟实现代码:
#include
#include
void* my_memmove(void* dest, const void*src, size_t num)
{
void* ret = dest;
assert(dest && src);
if (dest < src)
{
//前->后
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
//后->前
while (num--)//19
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
int main()
{
char arr1[11] = "abcdefghij";
my_memmove(arr1+2, arr1, 5);
printf("%s\n", arr1);
return 0;
}
函数原型说明: int memcmp ( const void * ptr1,const void * ptr2,size_t num );
比较从ptr1和ptr2指针开始的num个字节,如果相同继续向后比较 ,可以比较任何数据类型,比较单位是字节。
如果ptr1指向的数据大于ptr2指向的数据,返回大于0的整形数据
如果ptr1指向的数据小于ptr2指向的数据,返回小于0的整形数据
如果ptr1指向的数据等于ptr2指向的数据,返回0。
库函数代码如下:
在这里插入代码片#include<string.h>
#include
int main()
{
float arr1[] = { 1.0, 3.0 };
float arr2[] = { 1.0, 3.0 };
int ret = memcmp(arr1, arr2, 2);
//memcmp - strcmp
printf("%d\n", ret);//结果为0
return 0;
}
模拟实现可以自己写下或者参考下源码。
尝试了一些新的东西,形式终于写完了这篇博客了,简单的介绍了几个字符串,内存操作函数及其实现,其实与此相关的函数还有很多,就不一一列举了,感兴趣的老铁可以自己下去再学习下,另外创作不易,如果觉得这篇文章对你有帮助的话,点赞,评论收藏,我先在此谢过大家,文章如有问题,评论区留言。