一、strlen函数
strlensize_t strlen ( const char * str );
易错点,返回的是无符号整型:如图所示,打印>
还有就是形参类型const char* (加上const更安全)
int main()
{
if (strlen("ab") - strlen("abcd") > 0)//2-4=-2
{ // -2在内存中存放的补码形式:11111111 11111111 11111111 11111110
printf(">"); //两个无符号数相减,得到无符号数,该补码当无符号处理。所以结果为其正数
}
else
{
printf("<");
}
return 0;
}
strlen函数的三种实现版本:
1、计数器版本:创建一个变量去实现计数,直到字符串结束符’\0‘
int my_strlen(char* shu)//为安全起见,形参类型应加上const
{
int count = 0;//计数
while (*shu++)//当*shu为'\0'时,没有再进入循坏,计算的是'\0'之前的字符个数
{
count++;
}
return count;
}
int main()
{
char shu[] = "I am Ok!";
printf("%d ", my_strlen(shu));
return 0;
}
2、递归版本:在不建立临时变量去计数,用递归方式计数
int my_strlen1(char* shu)
{
if (*shu)
{
return 1 + my_strlen1(shu+1);
}
return 0;
//或者是:
//if (*shu != '\0')
// return 0;
//else
// return 1 + my_strlen1(shu + 1);
}
int main()
{
char shu[] = "I am Ok!";
printf("%d ", my_strlen1(shu));
return 0;
}
递归中要注意自加自减和+的区别,否则将会出现问题:如下代码
void convert(char* a, int n)
{
int i;
printf("\n%p", a);
//if ((i = n / 10) != 0) a+1版本
//convert(a + 1, i);
if ((i = n / 10) != 0)
convert(a++, i); a++版本
*a = n % 10 + '0';
//a++的版本等同于:
//int i;
//printf("\n%p", a);
//if ((i = n / 10) != 0)
//{
// convert(a, i);
// a++;
//}
//*a = n % 10 + '0';
}
int main()
{
int number;
char str[10] = " ";
scanf("%d", &number);
convert(str, number);
printf("\n");
puts(str);
return 0;
}
a+1版本:
补充:关于递归函数,要记得新函数是旧函数的临时拷贝, 旧函数里的指针a,不是新函数里的a,我们在调试的监视窗口可以发现其实每次递归a的地址(&a)都在变化,因为创建了一个新的a去接收地址,所以其所存的地址(*a)是一样的,最后的打印的地址结果相同,在递归完回去时,开始在该代码中进行相应的自加操作
而在a+1中,相当于b=a+1,在递归过程中已经不是原来的a。
3、指针-指针版本:两指针相减得到两地址间的字符个数
int my_strlen2(char* shu)
{
char* shu2 = shu;
while (*shu2++)//这个版本在判断'\0'跳出循环后仍自加1,
;
//或者是:
//while (*shu2)//和上面版本的区别是判断'\0'后不再自加。
// shu2++;
//return shu2 - shu;
return shu2-shu-1;//所以指向的是结束符后的地址要减1才能得到中间除结束符外的字符个数
}
int main()
{
char shu[] = "I am Ok!";
printf("%d ", my_strlen2(shu));
return 0;
}
二、strcpy函数
char* strcpy(char * destination, const char * source );
注意:
1、形参类型和所代表对象,前一个是目的地的地址,后面是拷贝的对象地址
2、注意拷贝对象要有字符串结束符'\0',其中可能出现问题的如下面这种初始化方式
char shu[] = {'w','a' };//'\0'符号位置未知
结果如图所示,'\0'位置未知,wa字符拷贝完成后继续拷贝内容(内容乱码),直至结束符终止
3、要保证目标空间足够大,以确保能存放拷贝的字符串
4、目标空间必须可变
目标内容是常量字符串不可修改
5、strcpy函数的模拟实现
char* my_strcpy(char*shu, char*shu1) //拷贝的对象应该加上const会比较安全,const char*shu { assert(shu && shu1); char* p = shu1; while (*shu1++ = *shu++) ; return p; } int main() { char shu[] = "what?"; char shu1[] = "I don't know!"; my_strcpy(shu,shu1); printf("%s ", shu1); return 0; }
2.1、strncpy函数
char * strncpy ( char * destination, const char * source, size_t num );
1、拷贝我们限定数量的字符串到目标空间 ,即形参num接收我们所设定的大小
2、如果拷贝字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
模拟实现strncpy函数
char* my_strncpy(char* dest, char* sour, int count)//要加const { char* p = dest; while (count&&(*dest++ = *sour++))//如果有一方为0则跳出循环 { //这个赋值方式刚好将dest指针指向'\0'后面的地址,实现了如果拷贝字符串小于num,则可以直接在后面 count--; } if (count) { while (count--)//跳出循环时count=0,进入条件判断时,判断的就是减后的值,所以当减为0后,没有进入而是立马跳出循环 { //如果为count--,跳出循环时count=-1,进入条件判断时先判断未减1的值,进入循环后减1,所以值为0时还在循环体里,再判断时跳出循环,但也自减为-1 *dest++ = '\0'; } } return p; } int main() { char shu[] = "what?"; char shu1[] = "I!"; my_strncpy(shu,shu1,4); printf("%s", shu); return 0; }
关于(*dest++ = *sour++)和while (count--)我们可以发现,
dest指向的是'\0'后面的地址,所以当wile里的判断条件是count--时,我们可以发现其本质是将原本拷贝不足的位数又增加了一位赋为'\0',如下图:
当while条件里为--count时,其实把之前在dest指向在'\0'后面的那一位给减掉了(因为字符拷贝后指向'\0‘后面的地址,相当于是自动为后面不够拷贝的内容赋上了'\0’)
如下图才是拷贝位数为4的结果:
但其实可以发现,在最后的打印结果中,二者是一样的(打印字符串时,遇到'\0'就结束了),只是记得要区分自加和自减后分别的值是什么。
三、strcat函数
char * strncat ( char * destination, const char * source, size_t num )
a、追加时时先找到目标字符串的'\0',然后进行追加(拷贝内容从覆盖'\0‘的位置开始),所以自己不能给自己追加:因为追加时‘\0'被覆盖,循环追加没有结束,陷入死循环
b、还有就是函数的追加内容不一定是字符数组:
char* my_strcat(char* shu1,const char* shu) { while (*shu1) { shu1++; } while (*shu1++ = *shu++) ; return p; } int main() { //char shu1[37] = "haha ! "; //printf("%s ", my_strcat(shu1,"but i think you're right." )); const char* shu = "but i think you're right."; char shu1[37] = "haha ! "; printf("%s ", my_strcat(shu1, shu)); return 0; }
注意:
1、同上必须以字符串结束符结束
2、目标空间足够大,能容纳追加的字符串长度
3、目标空间必须可变,
4、strcat函数的模拟实现
我们可以知道strcat函数的返回类型时char*,但void也可以实现同样的结果
char* my_strcat(char* shu1, char* shu) { char* p = shu1; while (*shu1++) ; shu1 = shu1 - 1;//和注释的分别是shu1指向的位置要-1得到指向'\0'的位置, //记得在条件里判断是否为'\0'时又进行了自加操作 //while (*shu1) //{ // shu1++; //} while (*shu1++ = *shu++) ; return p; } int main() { char shu[] = "but i think you're right."; char shu1[37] = "haha ! "; printf("%s ", my_strcat(shu1, shu)); return 0; }
3.1、strncat函数
char * strncat ( char * destination, const char * source, size_t num );
模拟实现:
char* my_strncpy(char* shu2, char* shu1, int count) { char* p = shu2; while (*shu2) { shu2++; }//完成循环后的shu2指向'\0' while (count--) { if ((*shu2++ = *shu1++)==0)//当一直到拷贝字符串为'\0'时结束,直接返回p,此时shu2其实时指向'\0'后面地址 return p; } *shu2 = '\0';//如果设定的count小于拷贝字符个数,则拷贝完成后要加'\0'结束。 return p; } int main() { char shu1[23]; char shu2[33]; strcpy(shu1, "That's tree!"); strcpy(shu2, "look!"); my_strncpy(shu2, shu1, strlen(shu1)); printf("%s ", shu2); }
四、strcmp函数
int strcmp ( const char * str1, const char * str2 );
标准规定:
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
int main() { const char* p = "why"; const char* q = "when"; if (strcmp(p, q) > 0)//大于则返回大于0的数 { printf(">");//第一个字符小于第二个 } else if (strcmp(p, q))//小于则返回大于0的数 { printf("<"); } else printf("=");//相等返回0 return 0; }
strcmp函数的模拟实现:
int my_strcmp (const char * src, const char * dst) { int ret = 0 ; assert(src != NULL); assert(dest != NULL); while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst) ++src, ++dst; //在这个判断条件中,如果两值相等取反后为非0则继续比较,而和*det逻辑与是因为如果出现'\0'则终止继续往下比较已经可以分出大小 if ( ret < 0 ) ret = -1 ; else if ( ret > 0 ) ret = 1 ; return( ret ); } //易读版本: int my_strcmp(const char* p, const char* q) { while (*p == *q) { if (*p == '\0') return 0; p++; q++; } return *p - *q; // 等同于:*p > * q ? (p - q) : (q - p); //不一定是1或-1,返回值只要满足大于0或小于0的数即可 //*p > * q ? 1 : -1; //等同于:if (*p > *q) //{ // return 1; //} //else //{ // return -1; //} }
4.1、strncmp函数
int strncmp ( const char * str1, const char * str2, size_t num );
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。模拟实现:
int my_strncmp1(const char* first, const char* last, int count) { int x = 0; if (!count) { return 0; } for (; x < count; x++) { if (*first == 0 || *first != *last) { return(*(unsigned char*)first - *(unsigned char*)last); } first += 1; last += 1; } } //自己写的版本,当作笔记记下,以后再更改(最好不要看) int my_strncmp(char* shu, char* shu1, int count) { while (count) { if (*shu == *shu1&&*shu==0) { return 0; } shu++; shu1++; count--; if (*shu != *shu1) { return *shu - *shu1; } } return 0; } int main() { char shu[] = "haha"; char shu1[] = "haxixi"; printf("%d ", my_strncmp(shu, shu1, 4)); return 0; }
五、 strstr函数
char * strstr ( const char *str1, const char * str2);
char* my_strstr(const char* str1, const char* str2) { const char* s1 = NULL; const char* s2 = NULL; char* cp = (char*)str1;//cp为返回值,和赋值的形参类型不同,需要强制类型转换 while (*cp) { if (*str2 == '\0')//if ( !*str2 ) { return (char*)str1; } s2 = str2; s1 = cp; while (*s1 && *s2 && (*s2== *s1)) {//当判断条件只有*str2== *str1时,我们如过遇到字符匹配过程中同时以'\0'结束的两字符串时,还是会继续进入循环,造成越界访问的错误 s1++; s2++; } if (*s2 == '\0')//当查找至*s2为'\0'时,我们就算是找到,且返回指针并结束查找了 { return cp; } cp++; } return NULL;//当*s1未查出且查找至为'\0‘时跳出循环返回空指针 } int main() { char shu[] = "wogaizenmebanaaa"; char shu1[] = "zenmeban"; printf("%s ", my_strstr(shu, shu1)); return 0; }
六、strtok函数
char * strtok ( char * str, const char * sep );
sep参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标 记。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容 并且可修改。)
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串 中的位置。
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标 记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
int main() { const char *shu = "19191919.11.22/19"; char temp[111]; strcpy(temp, shu); const char* p = "./"; char*ps=strtok(temp, p); //while (ps != NULL)//一开始找到标记,并将其改为'\0',第一个参数则记录其地址,然后返回开头开始查找的地址, //{ // printf("%s\n", ps); // ps=strtok(NULL, p);//从函数中保存的标记位置开始查找下一个标记 //} for (ps; ps != NULL; ps=strtok(NULL, p))//从标记位置开始查找,若后面无内容则返回'\0', {//实际的清晰表达(ps=strtok(temp, p),ps != NULL; ps=strtok(NULL, p)) printf("%s\n", ps); } return 0; }
七、strerror函数
char * strerror ( int errnum );
//必须包含的头文件
#include <stdio.h>
#include <string.h>
#include <errno.h>
该函数功能是返回错误码,所对应的错误信息。
我们知道在调用库函数时,都会设置错误码,其保存在erron这个变量中,所以当我们将该变量传给该函数时,可以得到相应的错误信息的解释
和strerror类似的的perror函数:
perror函数,直接打印错误信息,
而strerror是把错误码转换为错误信息,若要打印该信息则要自己操作
函数原型:void perror(const char* str)
头文件#include<stdio.h>
如图上下版本区别,perror操作简单且自己可以不用printf打印信息。
字符转换
int tolower ( int c );
int toupper ( int c );
int main() { int i = 0; char ch[21] = { 0 }; scanf("%s",ch); while (ch[i])//判断是否为'\0',是则跳出循环 { if (isupper(ch[i]))//判断逐个字符元素是否为大写,是的话进入循环 { ch[i] = tolower(ch[i]);//将i对应的大写字符元素转换为小写再存入相应i的数组位置中 } printf("%c", ch[i]); i++;//注意这个i的位置,是要在打印后,若是在前则会跳过一个字符元素打印 } return 0; }
字符分类函数:
函数 如果他的参数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v'
isdigit 十进制数字 0~9
isxdigit 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母a~z或A~Z
isalnum 字母或者数字,a~z,A~Z,0~9
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
八、memcpy函数
void * memcpy ( void * destination, const void * source, size_t num );
a、函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 '\0' 的时候并不会停下来。
b、该内存函数可以拷贝所有数据类型,如形参接收的数据类型void*
如果source和destination有任何的重叠,复制的结果都是未定义的
c、模拟实现:
void my_memcpy(void* shu1, const void* shu, int count) { void* start = shu1;//char* start = (char*) shu1; while (count--)//拷贝过程中一个字节一个字节拷贝,所以count--可以达到计数的效果 { *(char*)shu1 = *(char*)shu;//考虑到所有数组类型都通用,所以将指针类型强制转换成char* shu1 = (char*)shu1 + 1; shu = (char*)shu + 1;//因为强制转换是临时的效果,所以在以一个字节一个字节继续向后拷贝时,还要进行强制类型转换成char*才可以继续一个字节一个字节的拷贝 } return start; } int main() { int shu[] = { 1,2,33,77,99 }; int shu1[12] = { 0 }; my_memcpy(shu1, shu, 12); return 0; }
d、该函数拷贝不重叠的内存,可以这样理解,在同一个内存空间里,若是改变了一个数,然后又要用该数的原值去赋予另一个数,则是失败的,因为原值已经改变,所以后来的重叠部分所赋予的值不是我们期望的那样,就相当于交换三个数没有设置临时变量去储存,当我们把a用b初始化时,a已经不存在,b再用a去初始化其值还是自己赋予a的那个值,b没有发生变化,达不到预期交换的效果,在这个函数里则是拷贝的效果。
如果要想可以拷贝重叠的内存则可以用下面的函数:
8.1、memmove函数
void * memmove ( void * destination, const void * source, size_t num );
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理
void* my_memmove(void* dest, const void* shu, int count) { void* start = dest;//char* start = (char*) shu1; if (dest< shu)//记得区分一下目标地址和拷贝地址的位置来确定是否可以成功拷贝 { while (count--) { *(char*)dest = *(char*)shu; dest = (char*)dest + 1; shu = (char*)shu + 1; } } else//当在同一块内存中拷贝,目标地址大于拷贝内容地址时,从后往前拷,可以避免重叠部分再次被利用(此时利用的不是初值) { while(count--)//很巧妙的是。数组下标是0开始,所以指向的地址应为实际字节数-1 { *((char*)dest + count) = *((char*)shu + count); //count--实现了指针移动 } } return start; } int main() { int shu[] = { 1,2,33,77,99 }; my_memmove(shu+1, shu, 12); return 0; }
九、memcmp函数
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
a、该函数也是按字节为单位比较,比较内存前前num个字节
b、memcmp在两个字符串的'\0'之后会继续比较,所以要保证num不能超过最短字符串长度。
int my_memcmp(const void* p, const void* q, int n) { char* p1 = (char*)p; char* q1 = (char*)q; while (n--) { if (*p1!=*q1) return *p1 - *q1; p1++; q1++; } return 0; } int main() { char shu[] = "haobuxiangqiaole"; char shu1[] = "haohaoqiaoba"; int ret=my_memcmp(shu, shu1, 3); if (ret > 0) { printf(">"); } else if (ret) { printf("<"); } else { printf("="); } return 0; }
十、memset函数
void*memset(void*ptr,int value,size_t num)
注意:该函数以字节为单位设置内存
如上图所示,引用该函数把数组前4位元素的每个字节都初始化成1,注意是每个字节,而不是每个元素,所以我们可以说在对整型数组进行操作时,其实是不能对每个元素进行相应的赋值操作。
后面看到了这篇博客感觉挺好的【C进阶】 字符串函数和字符分类函数_CS semi的博客-CSDN博客