文章目录
本文内容主要涉及的是对于c库函数中的一部分函数进行的讲解,包含函数的使用方法,注意事项,以及模拟实现。
部分库函数应用及模拟实现
字符函数
注:这些字符函数都是c语言的库函数,被定义在头文件
<ctype.h>
中,使用的时候需要包含此头文件
字符分类函数
在系统中存在很多不同种类的字符,它们在内存中存储的都是ASCII码值。当我们自己手动输入的时候,我们当然能很容易的分辨出我们输入的字符是属于哪些字符类型。
现在我们假设一个场景:我们将字符串
"ABCD123efg"
存储在数组arr中,我们现在想从头开始遍历数组,当遍历到的字符是大写字母时就将其打印。如果我们根据ASCII值65~90来判断,确实可以做到。但是当我们需要辨别的字符类型变多了呢?总不能从0开始一直判断ASCII码值吧?麻烦不说,还得去查表ASCII码值。所以我们更倾向于使用一个字符判断函数,输入字符就能判断成功。所以字符分类函数的作用就是如此
现在让我们来看看有哪些字符分类函数
函数名 | 满足什么条件函数值返回真 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空白字符:空格’ ‘,换行符’\n’,回车’\r’,制表符’\t’等 |
isdigit | 十进制’0’到’9’字符 |
isxdigit | 十六进制数字,包括所有十进制数字字符,小写字符a到f,大写A到F |
islower | 小写母a~z |
isalpha | 大写字母A~Z |
iscntrl | 字母a~ z,A~Z |
ispunct | 标点符号,任何不属于数字或字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印的字符,包括图形字符和空白字符 |
现在让我们来试着解决一下前面提出的问题,并且使用且熟悉一下上面提及的字符分类函数
int main() {
char arr[] = "ABCD123efg";
char* p = arr;
while (*p) {
if(isupper(*p)) {
printf("%c ", *p);
}
p++;
}
return 0;
}
这串代码可以解决我们上面提到的问题,让我们来看看是否能正确输出呢?
这些函数的使用都是十分简单的,只要输入的字符满足一定条件,就会返回真值(非0)。
现在让我们再来看一个例子:
写⼀个代码,将字符串中的小写字母转大写,其他字符不变。
第一种方法当然就是我们前面提及的字符转化函数。在<ctype.h>头文件中确实包含了字符的转化函数,可以轻松的将小写字母和大写字母进行转化。这点我们等下再进行细讲。我们先只用字符分类函数进行此类转化操作。
int main() {
char arr[] = "ABCDas123efgeGH";
char* p = arr;
while (*p) {
if (islower(*p)){
*p-=32;
}
printf("%c",*p);
p++;
}
return 0;
}
让我们看看输出是什么:
我们知道,再ASCII码表中,大写字母的值对应其小写字母的值刚好差了32,所以当我们遍历的时候,使用字符函数islower就可以判断是否为小写字符,并且将其值减32后重新赋值给本身,就可以做到小写字符转大写了。如果想要大写转小写,方法类似,只不过过字符函数的使用要改为isupper,并且ASCII码值要+32才可以。
由于字符分类函数使用方法较为简单,在此我就不进行过多的赘述。
字符转换函数
上文提到,<ctype.h>中是有专门让大小写字母转化的函数的。
有两个字符转化的函数:
1
int tolower(int c);
解释:如果 c 是大写字母并且具有小写等效字母,则将 c 转换为其小写等效项。如果无法进行此类转换,则返回的值为 c 不变。
2
int toupper(int c);
解释:如果 c 是小写字母并且具有等效的大写字母,则将 c 转换为其等效的大写字母。如果无法进行此类转换,则返回的值为 c 不变。
但是我们要注意的是:虽然是可以进行字符的转化,但是我们得清楚的是,转化的结果是以返回值带出的,而我们本身输入的字符并不会真的被改变。
如图所示,arr字符存储的就是字符a,但是输入toupper
函数后它本身的值并未改变。这就涉及到函数的传值调用和传址调用了。这个点在我的另一篇文章中早已说明:对指针的理解,里面包含了传值调用和传址调用
感兴趣的读者可以自行前往观看。
所以,对于刚刚上面提及的问题:写⼀个代码,将字符串中的小写字母转大写,其他字符不变。 我们可以采用字符转化函数进行操作。
具体实现如下:
int main() {
char arr[] = "ABCDabc12345efgdhAHD";
char* p = arr;
while (*p) {
if (islower(*p)) {
*p = toupper(*p);
}
printf("%c",*p);
p++;
}
return 0;
}
输出结果:
如此一来,我们就轻松的使用字符转换函数实现了字符之间的转换。
字符串函数
在c语言中,有一类变量叫做字符串变量,对于字符串的处理,如:求长度、赋值,追加等操作,这些操作也都是有专门的库函数对其进行处理的。
这类函数的定义在头文件<string.h>中,使用这些函数的时候需对其进行引用
strlen
strlen函数的定义及使用
size_t strlen ( const char * str );
str指向输入字符串的首元素的地址,strlen函数会返回从str向后直到遇见第一个’\0’的位置之间元素个数。
注意:
1 输入的字符串必须以’\0’结尾,否则会出现随机值。
2 输入的参数是指针变量,如果输入变量类型不匹配可能会出现问题。
3 返回值为size_t
,是无符号整形即unsigned int
4 使用的时候需包含头文件<string.h>
让我们来试着使用一下strlen函数
int main() {
char arr1[] = "abcdef";
char* p = "13as";
if (strlen(arr1) > strlen(p)) printf("Length:arr1>p\n");
else if (strlen(arr1) > strlen(p)) printf("Length:arr1=p\n");
else printf("Length:arr1<p\n");
return 0;
}
输出结果:Length:arr1>p
我们发现,哪怕是使用字符指针也是可以成功求出长度的。因为字符串常量末尾也会自带’\0’字符。
如果我们输入的字符串不是以’'\0’结尾,那么答案会出问题。让我们看看下面这段代码:
int main() {
char arr2[] = { 'a','b','c','d','e' };
printf("%d", strlen(arr2));
return 0;
}
我们发现答案并不是5,而是令人匪夷所思的42。这就是因为我们输入的字符串并不是以’\0’结尾。所以strlen函数会一直向后走,直到找到第一个’\0’字符后,才会返回一个值。但这个值并不一定是42,是个随机值,因为每一次运行程序的时候arr2的地址都是不一样的,所以向后走多少能找’\0’也是随机的。
strlen函数的模拟实现
学会函数的模拟实现能有助于我们对代码的理解,也能提高我们的编程水平。特别是strlen这种比较简单的函数。
现在我将着重介绍三种对于strlen模拟实现的方法:
我们将模拟实现的函数定义如下:
size_t my_strlen(const char* str);
1.计数器
代码如下:
size_t my_strlen(const char* str) {
assert(str);
size_t count = 0;
char* p = str;
while (*p) {
count++;
}
return count;
}
使用assert函数判断str是否为NULL指针,如果为NULL会发出警告中断程序。使用该函数时需包含头文件
<assert.h>
。
2.指针-指针
代码如下:
size_t my_strlen(const char* str) {
assert(str);
char* p = str;
while (*p) {
p++;
}
return p - str;
}
注:指向同一块连续内存的两个指针的值是两个指针相差的元素个数
3.递归
代码如下:
size_t my_strlen(const char* str) {
assert(str);
if (*str == '\0') {
return 0;
}
else {
return 1 + my_strlen(str+1);
}
}
一个字符串可以拆分成第一个字符和剩下的。求字符串的长度的值就是1+第二个字符开始的字符串的长度。刚好发现,这个过程可以一直拆分下去,刚好满足递归的思想。且当传入的字符串首字符为’\0’时,长度就为0.这是终止递归的条件。
strcpy和strncpy
定义和使用
strcpy
char* strcpy(char * destination, const char * source );
destination为目标空间,source
源字符串必须以 '\0’结束。
会将源字符串中的 ‘\0’ 拷⻉到⽬标空间。
⽬标空间必须⾜够⼤,以确保能存放源字符串。
⽬标空间必须可修改。
strcat的使用较为简单,如图:
如果目标空间不够大的话,编译器会报出警告:
strncpy
char * strncpy ( char * destination, const char * source, size_t num
); 拷⻉num个字符从源字符串到⽬标空间。 如果源字符串的⻓度⼩于num,则拷⻉完源字符串之后,在⽬标的后边追加0,直到num个。
strncpy和strcpy的差别就是,strncpy会对赋值字符的个数进行加以限制,相较于strcpy要更加的严谨和安全。对于strncpy的使用我就不过多进行赘述,与strcpy的效果类似,仅在复制的个数上有所不同。
模拟实现
模拟实现strcpy
代码如下:
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while ((*dest++ = *src++))
{
;
}
return ret;
}
对于循环部分写的较为抽象,但是还是比较好理解的。
大致为:当src指向的值不为’\0’时,就赋值给对应位置的dest。赋值后dest和src再自增,即向后移一位。直到碰到’\0’时跳出循环。只不过这里写的较为简略。
模拟实现strncpy
代码如下:
char* my_strncpy(char* dest, const char* src, size_t num) {
char* p = dest;
int count = (int)num;
while (*src&&count) { //当复制的元素足够num个的时候 跳出循环
//当src指向的元素为'\0'也要跳出循环。
*dest++ = *src++;
count--;
}
//如果src中字符的个数足够,不用追加'\0'
if (count != 0) {
while (count) {
*dest++ = '\0';
count--;
}
}
return p;
}
strcat和strncat
定义和使用
strcat
char * strcat(char *dest, const char*src);
源字符串必须以 ‘\0’ 结束。
⽬标字符串中也得有\0 ,否则没办法知道追加从哪⾥开始。
⽬标空间必须有⾜够的⼤,能容纳下源字符串的内容。
strcat的使用:
同样的,对于该库函数的使用,是需要注意上述注意事项的。否则编译器会报出警告,答案可能会与我们预期的不一样。
strncat
char * strncat ( char * destination, const char * source, size_t num
); 将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加⼀个’\0’字符 如果source
指向的字符串的⻓度⼩于num的时候,只会将字符串中到’\0’的内容追加到destination指向的字符串末尾
跟strcat的效果类似,只不过是对追加的元素个数也进行了一个限制。同理,strncat会比strcat更加的安全。对于strnact的使用也不再进行过多赘述。
模拟实现
模拟实现strcat
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;
}
先让dest指针一直向后走找到’\0’的位置,这就是源字符串开始追加的位置。然后只要src指向的不为’\0’就一直赋值给对应的dest指向的元素。然后dest,src自增。
模拟实现strnact
代码如下:
char* my_strncat(char* dest, const char* src, size_t num) {
char* p = dest;
while (*dest) {
dest++;
}
//此时dest指针指向的就是'\0'这个元素,然后就可以让src开始追加了
int count = (int)num;
while (count&&*src) {
*dest++ = *src++;
count--;
}//如果src中的字符是够追加的 这个循环就能跳出来 且count==0
//如果是不够的情况下,那么当src指向'\0'的时候就应该跳出循环
if (*src == '\0') {
*dest = *src;
}
return p;
}
strcmp和strncmp
定义和使用
strcmp
int strcmp (const char * str1, const char * str2)
标准规定:
返回值 | 条件 |
---|---|
>0 | 第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字 |
0 | 第⼀个字符串等于第⼆个字符串,则返回0 |
<0 | 第⼀个字符串小于第⼆个字符串,则返回小于0的数字 |
判断大小依据:根据两个字符串第一个不相同的字符的ASCII码值比较。ASCII码值大的字符更大。反之更小。
如下图所示:
arr1与arr2第一次不匹配的字符是’g’和‘G’,前者的ASCII码值大,arr1>arr2
strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
⽐较str1和str2的前num个字符,如果相等就继续往后⽐较,最多⽐较num个字⺟,如果提前发现不⼀样,就提前结束,⼤的字符所在的字符串⼤于另外⼀个。如果num个字符都相等,就是相等返回0。
对于strncmp的使用也是和strcmp类似,也是对比较的字符数加以限制。只需要多输入一个比较数量的参数就可以了。也不再赘述。
模拟实现
模拟实现strcmp
代码如下:
int my_strcmp(const char* str1, const char* str2)
{
int ret = 0;
assert(str1 != NULL);
assert(str2 != NULL);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
return *str1 - *str2;
}
如果str1和str2指向的字符是一样的,就让str1,str2自增,向后走一步。
如果两个字符串是完全相等的,那么当两个指针指向’\0’的时候仍相等,但这个时候已经比较完成了,所以需要返回0这个值。
如果在遍历的过程中出现不相等的,返回*str1-*str2的值。刚好能满足返回值规定的数字。
模拟实现strncmp
int my_strncmp(const char* str1, const char* str2, size_t num) {
int count = (int)num;
while (count && (*str1 == *str2)) {//当比较的次数够了需要跳出循环
//当str1和str2指向的元素不相等时,也要跳出循环
str1++;
str2++;
count--;
}
if (count == 0) {//如果比较次数为num个时,跳出循环count=0
//也就是说前num个字符相等
return 0;
}
else {
return *str1 - *str2;
}
}
strstr
定义和使用
char * strstr ( const char * str1, const char * str2);
函数返回字符串str2在字符串str1中第⼀次出现的位置
字符串的⽐较匹配不包含 \0 字符,以 \0 作为结束标志
int main() {
char* p1 = "abcdefg";
char* p2 = "abcd";
char* p3 = "ahj";
char* p12 = strstr(p1, p2);
char* p13 = strstr(p1, p3);
printf("%s\n%s\n", p12,p13);
return 0;
}
输出:
若存在字串,则返回字串开始的位置;若不存在,返回null。
模拟实现
代码如下:
char* My_strstr(char* s, char* sub) {
assert(s && sub);
char* s1 = s;
char* s2 = sub;
char* p = NULL;
while (*s1) {//从s1指向的位置开始往后找,直到*s1=='\0'时退出
p = s1;//指针p从s1开始向后找
while (*p==*s2) {//要是p和s2指向的字符一样就往后跳
p++;
s2++;
}
//如果存在这个字串,那么此时s2指向'\0'
if (*s2 == '\0') {
return s1;
}
//不存在的情况下
s1++;
s2 = sub;
}
return NULL;//如果程序能进行到这一步,说明字串sub确实不存在,返回NULL
}
strtok
定义和使用
char * strtok ( char * str, const char * sep);
sep参数指向⼀个字符串,定义了⽤作分隔符的字符集合
第⼀个参数指定⼀个字符串,它包含了0个或者多个由sep字符串中⼀个或者多个分隔符分割的标记
strtok函数找到str中的下⼀个标记,并将其⽤ \0 结尾,返回⼀个指向这个标记的指针。
strtok函数会改变被操作的字符串,所以被strtok函数切分的字符串⼀般都是临时拷⻉的内容并且可修改。
strtok函数的第⼀个参数不为 NULL ,函数将找到str中第⼀个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第⼀个参数为 NULL ,函数将在同⼀个字符串中被保存的位置开始,查找下⼀个标记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
举例解释:
第一次使用strtok函数的时候,第一个参数不能为NULL。此时p将保存arr的地址。strtok函数会往后找到第一个分隔符’.‘,然后把这个字符变为’\0’。然后保存这个地址的位置在一个静态变量中。
从第二次开始,第一个参数如果是NULL的话,就会把上次静态变量存的值返回,并且继续向后找到下一个分隔符。保存此处地址。
以此类推,直到往后找不到分隔符而是找到’\0’时,就会返回一个null。
于是我们可以看见,经过strtok函数的使用,我们可以把一个字符串分割成我们想要的几个字段。
但是,如我们上面写的代码可以看见,这么写代码的方式仅仅适用于理解这个函数。如果分割次数较大,那么这么写代码肯定是不行的。所以我们必须运用循环的方式去写。
我们发现:第一次使用strtok函数的时候,第一个参数传参并不是NULL。而后面的全是NULL。说明第一次的使用有点像我们初始化的操作。所以我们可以尝试着使用以下for循环进行分割。
代码如下:
int main() {
char arr[] = "www.1569857456@qq.com";
char* sep = "@.";
char* p = NULL;
for (p = strtok(arr, sep);p!=NULL;p=strtok(NULL,sep)) {
printf("%s\n", p);
}
return 0;
}
让我们看看输出结果:
我们发现,是可以成功分割的。
将分隔符的数量增加
仍然能正确的分割出我们想要的结果。所以我们就成功的利用了for循环进行了strtok函数的多次使用。这样代码更简洁,且更加合理。
strerror
char* strerror ( int errnum );
函数可以把参数部分错误码对应的错误信息的字符串地址返回来。在不同的系统和C语⾔标准库的实现中都规定了⼀些错误码,⼀般是放在<errno.h>
这个头⽂件中说明的,C语⾔程序启动的时候就会使⽤⼀个全局的变量 errno来记录程序的当前错误码,只不过程序启动的时候errno是 0,表⽰没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会将对应的错误码,存放在 errno中,⽽⼀个错误码的数字是整数很难理解是什么意思,所以每⼀个错误码都是有对应的错误信息的。strerror
函数就可以将错误对应的错误信息字符串的地址返回。
注:strerror函数被定义在<error.h>文件中,使用时需包含此头文件
#include <errno.h>
#include <string.h>
#include <stdio.h>
//我们打印⼀下0~10这些错误码对应的信息
int main()
{
int i = 0;
for (i = 0; i <= 10; i++) {
printf("%s\n", strerror(i));
}
return 0;
}
从0到10对应的错误码信息都在这上面了。strerror返回的值是指向对应错误码字符串的首地址。所以我们可以根据这个函数,将error作为参数传参,来判断我们是哪里出错了。
当然为了更加方便,我们可以使用perror这个函数。这个函数等价于printf+strerror
。使用这个函数就不用我们主动输入error来获得错误码。更加方便地是,我们可以在里面进行输入我们想要标记的内容,这个函数会帮忙一起输出。如:
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
perror("Error opening file unexist.ent");
return 0;
}
输出:Error opening file unexist.ent: No such file or directory
前面是我们自行输入的内容,冒号后会输入错误码。
内存函数
以下讲到的几个内存函数也都是被定义在<string.h>文件当中。他们的操作和字符串函数有些类似。但是,字符串函数只能处理字符串变量,而内存函数是针对内存中的字节进行操作。无论什么类型的数据都能操作。需要我们对内存有一定的理解。
memcpy的模拟和实现
void * memcpy ( void * destination, const void * source, size_t num );
函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置。 这个函数在遇到 ‘\0’
的时候并不会停下来。 如果source和destination有任何的重叠,复制的结果都是未定义的。
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 0 };
memcpy(arr2, arr1, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
通过这个代码,我们可以轻松的将arr1中前20个字节的数据赋值为arr2中前20个字节的数据。再次注意的是,是针对于字节赋值。而不是元素个数。
memcpy的模拟实现:
void* my_memcpy(void* dest, const void* src, size_t num) {
void* p = dest;
int count = (int)num;
while (count) {
*((char*)dest) = *((char*)src);
dest = (char*)dest + 1;
src = (char*)src + 1;
count--;
}
return p;
}
注:void* 指针可以接收任何类型的指针。但是却无法直接进行运算和解引用。所以必须强制转换其指针类型再赋值。因为这个函数是按字节进行赋值。我们经过分析,我们只能一个字节一个字节的进行复制。才有可能按照要求将一定字节的数据赋值。
但前面提到,这种方法不能处理两块重叠的内存,否则不能达到想要的效果。
我们发现不是我们想要的结果(1 2 1 2 3 4 5 8 9 10)
这是因为两块内存重叠了,导致原本应该向后赋值的元素被提前覆盖了。所以memcpy这个函数只能处理两块不重叠的内存空间。
所以,为了能够处理重叠的空间,需要使用函数memmove。
memmove的模拟和实现
void * memmove ( void * destination, const void * source, size_t num
);
和memcpy的差别就是memmove函数处理的源内存块和⽬标内存块是可以重叠的。
如果源空间和⽬标空间出现重叠,就得使⽤memmove函数处理
使用memmove函数确实可以处理两块重叠的内存空间。那么我们如何对memmove进行模拟实现呢?
那我们得先进行一些分析:
我们把src和src+num之间的区域叫做源区,dest和dest+num之间叫做目标区
当dest的地址<src的地址的时候,此时我们发现,虽然区域重叠,但是按照原本memcpy的方法,从前向后依次赋值,是不会覆盖掉还没赋值的字节数据的。所以这个时候运用原来的方法就好。
当dest处于src和src+num之间时候,担心的问题出现了,从前向后赋值会覆盖。但是我们转换思路,如果让src和dest分别都指向自己所指区域的v最后一个字节时,从后往前赋值,就可以达到我们想要的效果了
而当dest位置大于src+num的位置的时候,无论是从前往后还是从后往前赋值都是可以的。
所以模拟实现memmove的时候,得先进行分区域。为了方便起见,我们及那个第二,三种情况合并。
代码如下:
void* my_memmove(void* dest, const void* src, size_t num) {
void* p = dest;
int count = (int)num;
if (dest < src) {
while (count) {
*((char*)dest) = *((char*)src);
dest = (char*)dest + 1;
src = (char*)src + 1;
count--;
}
}
else {
while (count--) {
*((char*)dest + count) = *((char*)src + count);
}
}
return p;
}
这样,我们就成功的模拟了一个memmove函数。感兴趣的读者可以自己前去验证一下。
memset函数的使用
void * memset ( void * ptr, int value, size_t num );
memset是⽤来设置内存,将内存中的值以字节为单位设置成想要的内容。
我们猜想,能不能把整型数组里面的数据全部赋值为1呢?
由于一个整型4个字节,所以按道理整形数组全部赋值,传入的字节数应该=4*元素个数
。
我们发现,答案非常奇怪。我们来调试一下看看内存里面是如何存放的。
我们发现,他是把每个字节赋值成1了,一个整形四个字节01 01 01 01对应的十进制就是16843009。
所以不能全部赋值为某个整数。但是0除外。因为赋值全0,每个字节都是00,一个整形就是00 00 00 00,那么对应的十进制就时0。是可以成功的。
memcmp函数的使用
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
⽐较从ptr1和ptr2指针指向的位置开始,向后的num个字节
返回值与strncmp类似,只不过此时比较额从字符变成了字节。
由于使用较为简单,且与strncmp用法类似,在此我仅举一个用例来演示一下用法。
int main()
{
char buffer1[] = "DWgaOtP12df0";
char buffer2[] = "DWGAOTP12DF0";
int n;
n = memcmp(buffer1, buffer2, sizeof(buffer1));
if (n > 0)
printf("'%s' is greater than '%s'.\n", buffer1, buffer2);
else if (n < 0)
printf("'%s' is less than '%s'.\n", buffer1, buffer2);
else
printf("'%s' is the same as '%s'.\n", buffer1, buffer2);
return 0;
}
输出结果:
以上就是本篇文章的全部内容了,希望广大网友多多指正!