前言:这篇博客主要用于字符串函数的实现,而淡化了相应的语法细节,具体的使用细节可以在C语言相关概念和易错语法(5)中查看,这篇博客中只会重复比较重要的。
size_t引用的头文件(选其一):<stddef> <stdio> <stdlib> <string> <time> <wchar>
NULL引用的头文件(选其一):<stddef> <stdlib> <string> <wchar> <time> <locale> <stdio>
字符串函数和内存函数共有注意事项:传的所有参数均不能为NULL
1.strlen
标准格式:size_t strlen(const char* str);
(1)size_t是无符号数导致的陷阱
对strlen返回值的任何操作均为无符号数(正数),因此在遇到1-2这种算式时,size_t类型对应计算结果是个很大的数字,因为其补码首位并不代表符号位
当然,你可以使用强制类型转换来达到你原来想要达到的目的,但前提是你必须知道要这么做。
(2)strlen传递空指针导致程序的错误
strlen不能传入空指针,因此在自己实现strlen函数的同时要注意使用assert来进行断言,使函数更加安全
以下是利用递归模拟实现strlen函数的方法:
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* arr)
{
assert(arr);
if (!*arr)
{
return (size_t)0;
}
else
{
return (size_t)1 + (size_t)my_strlen(arr + 1);
}
}
int main()
{
char arr[] = "Hello,world!";
printf("%d\n", (int)my_strlen(arr));
return 0;
}
(3)由strlen延伸的函数strnlen
标准格式:size_t strnlen(const char* str, size_t num)
这个函数不太常用,但根据其它该类型的函数,你也可以猜出它的功能:
i有限定作用,决定strnlen计算大小的最大值
2.strcpy
标准格式:char * strcpy ( char * destination, const char * source )
返回的是char * destination中的char*
(1)这个函数需要注意的是在copy时源字符串包括\0都会被拷贝
(2)不能传NULL
在这里只有p1是空指针,p2是常量字符串
(3)目标数组一定要给大小且大小一定要合适,否则拷贝可能会出现越界访问的情况
可以看到,arr1被越界访问,原有字符全部被替换,\0也被#替换,导致printf在打印时也出现越界访问
下面是模拟实现strcpy的代码:
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcpy(char* arr2, const char* arr1)
{
assert(arr2 && arr1);
char* str = arr2;
while (*arr2++ = *arr1++);
return str;
}
int main()
{
char arr1[] = "Hello,World!";
char arr2[20] = "####################";
my_strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
实现效果如下
需要注意的是,while括号内部的表达式都会正常执行,执行之后的返回值作为while评判的值,如果为0(假)就停止,这里当读到最后一个\0时,表达式先执行,所以arr1中的\0成功被赋给arr2,然后表达式返回0被判为假,跳出循环。
(4)strcpy延伸函数strncpy
标准格式:char * strncpy ( char * destination, const char * source, size_t num )
注意:strncpy拷贝时有num参数确定要拷贝多少个字符,源字符串大小<num时,默认补\0,
中途遇到\0会停止对source的访问,默认将剩下要求的个数的元素全部改为\0
源字符串大小>num时,不会拷贝\0,这点要和strcpy区分
3.strcat
标准格式:char * strcat ( char * destination, const char * source );
根据这3个函数,可以总结出参数部分一般源在后,目标在前
(1)strcat覆盖目标字符串\0,并将源字符串包括\0一并追加过来,之后停止 strncat回将指定大小的字符追加过来,但最后还会补一个\0,追加的大小相当于num+1,如果追加的个数刚好是sizeof arr,最后会有两个\0,因为无论什么情况,strcat都会再加上一个\0
(2)追加找的是destination的\0,如果在你的字符串中有\0,会直接从\0开始追加并把后面的值全部覆盖
(3)destination一定要指定大小,不然很大可能会越界访问
下面是模拟实现strcat的代码
#include <assert.h>
#include <stdio.h>
#include <string.h>
char* my_strcat(char* arr1, const char* arr2)
{
assert(arr1 && arr2);
int i = 0;
int j = 0;
int sz1 = (int)strlen(arr1);
int sz2 = (int)strlen(arr2);
for (i = 0; i < sz1; i++)
{
if (!arr1[i])
break;
}//i就是要追加的位置,也表示前面有多少个字符
while (arr1[i++] = arr2[j++]);
return arr1;
}
int main()
{
char arr1[20] = "Hello,";
char arr2[] = "World!";
printf("%s\n", my_strcat(arr1, arr2));
return 0;
}
(4)strcat延伸函数strncat
标准格式
char * strncat ( char * destination, const char * source, size_t num );
strncat中追加的num源字符串大小时,只会将源字符串到\0追加,不会再多加\0
其他功能均与前面的函数相似,不再阐述
4.strcmp
标准格式:
int strcmp ( const char * str1, const char * str2 );
(1)arr1 - arr2,大于就返回大于0的数,小于就返回小于0的数
(2)strcmp不是比较字符串的长度,而是比较对应下标字符的ASCII码值谁大。比如"abcdef"和"acb",后面的字符串比前面的字符串大,因为同为下标1时,c的ASCII值比b大。
下面是strcmp的模拟实现
#include <assert.h>
#include <stdio.h>
#include <string.h>
int my_strcmp(const char* arr1, const char* arr2)
{
assert(arr1 && arr2);
int i = 0;
int j = 0;//不能只用一个i,arr1[i++] == arr2[i++]这种行为未定义,属于无效代码
while (arr1[i++] == arr2[j++])//当两者不等时,i和j依然会继续进行一次++操作,因此i和j在任何情况下都会大1
{
if (!arr1[i - 1]|| !arr2[i - 1])//防止越界访问,这里i已经多加了一个1,所以是i-1,不能用i--,这样会导致i连续减2次
break;
}
i--;
j--;
return arr1[i] - arr2[j];
}
int main()
{
char arr1[] = "Hello!";
char arr2[] = "Hola!";
printf("%d\n", my_strcmp(arr1, arr2));
return 0;
}
(3)strcmp的延伸函数strncmp
标准格式:
int strncmp ( const char * str1, const char * str2, size_t num );
strncmp只比较num个字符,这个函数比较简单,不再阐述
5.strtok
标准格式:
char * strtok ( char * str, const char * delimiters );
(1)这个函数要学会使用,主要用于分割字符串,达到分离信息的目的,可借助for的巧妙之处一次性达到目的
(2)分割过程忽略连续的分隔字符,且分隔字符串中字符的顺序没有任何影响
(3)分割字符串中最好不要有\0,\0之后的字符不会起效
同样,被切割字符串也要注意\0的位置,\0之后的字符都会被忽略
下面是strtok的模拟实现:
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strtok(char* arr1, const char* arr2)
{
assert(arr2);//arr1可以是NULL
//当第一次传进来arr1时,初始化一个静态变量
static char* str = NULL;
if (!arr1)
{
arr1 = str;
}
int sz1 = (int)strlen(arr1);
int sz2 = (int)strlen(arr2);//字符串中最好都不要\0,strlen有限制
if (!sz1)
return NULL;
int i = 0;
for (i = 0; i < sz1; i++)
{
int j = 0;
for (j = 0; j < sz2; j++)
{
if (arr1[i] == arr2[j])//在i这个位置应该分割
{
arr1[i] = '\0';
str = arr1 + i + 1;//跳过\0,记录新的字符串的起始点
return arr1;
}
}
}
str++;
return arr1;
}
int main()
{
char* p = NULL;
char arr1[] = "192.168.200.2";
char arr2[] = ".";
for (p = my_strtok(arr1, arr2); p != NULL; p = my_strtok(NULL, arr2))
{
printf("%s\n", p);
}
return 0;
}
6.strerror
标准格式:
char * strerror ( int errnum );
这个函数是用来储存错误信息(在调用完一次库函数的时候),我们可以利用它来获取在调用库函数的过程中可能遇到的一些问题,一般来说要和errno配合使用,因为库函数的错误码是写在errno这个全局变量上的,注意要引用头文件<errno.h>
下面用一段模拟库函数给errno赋值的代码来说明strerror的作用:
#include <errno.h>
#include <stdio.h>
#include <string.h>
int Fun(void* p)
{
if (!p)
{
errno = 1;
return -1;
}
else
{
errno = 0;
return 0;
}
}
int main()
{
int i = 0;
Fun(NULL);
printf("%s\n", strerror(errno));
Fun((void*) & i);
printf("%s\n", strerror(errno));
return 0;
}
运行的结果是
这个错误信息是我自己设置的,因此打印的时候就会显示我设置的错误码对应的信息,在我们使用的库函数的内部,都会有这样一个操作,在当库函数执行异常时修改errno的值,以供程序员参考。值得注意的是,errno存在覆盖现象,所以要注意及时使用。
7.strstr
标准格式:
const char * strstr ( const char * str1, const char * str2 );
char * strstr ( char * str1, const char * str2 );
strstr是找到参数中后一个字符串在前一个字符串中出现的位置,找不到就返回NULL,找到就返回第一次出现的首元素的首地址
下面是模拟实现strstr的一个方法:
#include <stdio.h>
#include <string.h>
#include <assert.h>
const char* my_strstr(const char* arr1, const char* arr2)
{
assert(arr1 && arr2);
int sz1 = (int)strlen(arr1);
int sz2 = (int)strlen(arr2);
if (sz2 > sz1)
return NULL;
if (sz1 == sz2)
{
if (arr1[0] != arr2[0])
return NULL;
}
if (!sz2)
return arr1;
int i = 0;
for (i = 0; i < sz1; i++)
{
int j = 0;
int k = 0;
for (j = 0, k = 0; j < sz2; j++, k++)
{
if (arr1[i + k] != arr2[j])
break;
if (k == sz2 - 1 && arr1[i + k] == arr2[j])
return (const char*)arr1 + i;
}
}
return NULL;
}
int main()
{
char arr1[] = "Hello,World!";
char arr2[] = "W";
printf("%s\n",my_strstr(arr1, arr2));
return 0;
}
在这里,我使用的是双重for的方式,当然,你也可以使用双指针来实现这个函数,它们的本质都是BF算法,还有一种KMP算法更加高效,后面的博客我会专门讲解。
代码上的注意事项:
在前面strcpy,strcmp我都采用了一种基于while(表达式)来实现一定的技巧。这其中有一点容易混淆:
先看下面这段代码,思考为什么num是-1而不是0
这就涉及到后置减的执行逻辑了,这里比较细节,但又确实很重要
(1)num值2传给while作为表达式的返回值,为真,但在进行下一步操作前(执行while内部表达式之前)num--为1
(2)执行了内部表达式(空表达式)num的值依然是1,这时num再传给while值1作为表达式的返回值,为真,但在进行下一步表达式之前num--为0
(3)在执行完空表达式后num的值是0,这时传给while值0作为表达式的返回值,为假,应该跳出循环,但就像前面两步一样,在进行“跳出循环”操作前,num--为-1,于是,num在经历这样的操作后最终值是-1
其根本原因是num--是后置减,返回值是在减操作前就传过去了,然后紧接着就会进行减操作,该操作和前面的传回表达值的操作相绑定,也就是说无论外界什么情况,无论发生什么,num最终都会减1