C语言并没有专用的字符串数据类型来存储一个字符串,字符串是C语言中重要的一块基本理论,知道如何使用字符串和掌握好字符串可减少实际开发经常出现的错误,本篇博客详细全面总结字符数组与字符串的定义及基本的使用方法,以及常用的库函数和针对内存操作的内存函数。
目录
1、下标引用操作符访问(同整型数组)/或者%s直接输出整个字符串(字符串特有方式)
一、字符数组与字符串
C语言存储字符,其实是存储字符所对应的ASCII码,因此字符可以以%c或者%d输出均可以,需要作为常识记住的是:字符'0'对应的ASCII码为48,字符'A'对应的ASCII码为65,字符'Z'对应的ASCII码为90,字符'a'对应的ASCII码为97,字符'z'对应的ASCII码为122; 字符类型char占用1个字节。
用来存放字符的数组称为字符数组,字符数组实际上是一系列字符的集合,也就是字符串,C语言通常有两种表示字符串的方法:
第一种:定义一个字符数组;char str[]="helloworld";
第二种:字符串指针; const char * str="helloworld";字符串指针不可通过解引用进行修改,因为字符串常量不可修改!
这二者从指针角度理解,本质一样,不论是字符数组名,还是字符串指针,字符串名str都代表字符串首元素的起始地址。区别在于:字符数组名为常量,常量不可以作左值!字符串指针不可以进行解引用再修改值。
1、字符数组的定义与初始化
字符数组的定义:
C语言字符数组定义如下:char 数组名 [数组长度];与普通整型数组相同。只是在初始化方面有所区别。
字符数组的初始化:
方式1:与普通整型数组初始化方式相同,花括号括起来,单个字符用单引号;
第1种:全部元素初始化,可以省略字符数组的长度(编译器自动确定)。
char str[5]={'a','b','c','d','e'}; 或者 char str[]={'a','b','c','d','e'};
第2种:未进行初始化,局部数组声明存储在栈区,此时字符数组的元素为随机值。
char str[5];
第3种:部分元素进行初始化,那么未初始化的元素默认为默认值'\0'
char str[5]={'a','b','c'} str[3]='\0' str[4]='\0'
方式2:字符数组特有的初始化形式,即C语言规定,可以将字符串(双引号括起来)直接赋值给字符数组;
第1种:char str [6]={"abcde"}; 或者为了简便省略花括号:char str [6]="abcde"; 需要注意的是:这里的字符数组的长度为6,这是为了存储'\0',因为用双引号" "包围的字符串会在字符串末尾自动添加'\0',因此,当用字符数组存储字符串时,要注意'\0',要为'\0'留个位置,这意味着,字符数组的长度至少要比字符串的长度加1。因此为了开发方便,这种方式常常不指定数组的长度。
第2种:char str []="abcde";(实际开发常用的方式)
这里有个坑需要注意:字符数组只有在定义的时候,才能将整个字符串一次性地赋值给它。
char str[6];
str ="abcde" ; //错误 字符数组名也是数组名,它是字符数组存储的字符串的首字符的起始地址,是一个常量,无法作为左值!左值必须是变量。
字符串结束标志(重点!!!)
字符串是一系列连续的字符的组合,要想在内存中定位一个字符串,除了要知道它的开头,还要知道它的结尾,找到字符串的开头很容易,知道它的名字(字符数组名或者字符串名)就可以,因为字符数组名或者字符串名都代表首字符的起始地址。那么应该如何找到字符串的结尾?
在C语言中,字符串总是以'\0',作为结尾,所以'\0'也被称为字符串结束标志,或者字符串结束符。'\0'是ASCII码表中的第0个字符,英文称为NUL,中文称为空字符,该字符即不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中的唯一作用就是作为字符串结束标志。0、'\0'、NULL、false 都是 0 的表现形式。
C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到'\0'就认为到达了字符串的结尾,就会结束处理,'\0'至关重要,没有'\0'就意味着永远也到达不了字符串的结尾。
用双引号" "包围的字符串,编译器会隐式的在字符串末尾自动添加'\0',然而通过逐个单引号为字符数组赋值并不会自动添加'\0'。
如:char str[]={'a','b','c'};数组长度为3。 char str[]="abc";数组长度为4。
因此,实际开发会有以下出错点:
程序的逻辑要求必须逐个字符的为字符数组赋值(通常for循环),经常会出现以下问题:
//通过for循环将26个大写英文字母存入字符数组,并以字符串的形式输出 #include <stdio.h> int main() { char str [30]; char c; int i; for(c=65,i=0; c<=90; c++,i++) { str[i]=c; } printf("%s",str); return 0; }
在函数内部定义的变量、数组、结构体、共用体等都成为局部数据,在很多编译器,局部数据的初始值都是随机值,并不是我们通常认为的”零“值,因此本例字符数组定义后,里面存放的是随机值,通过循环赋值后,前26个元素已经被赋值为26个英文字母,但是后4个为随机值,不一定是'\0',因此,printf打印完前26个字符后,接下来后面的不一定是'\0',只有遇到'\0',打印才会结束,有可能到了第50个元素才是'\0',这已经超出数组的范围了!!但是也会打印出来!因此,如果不注意'\0'的后果很严重,不但不能正确处理字符,甚至还会毁坏其他数据!
解决办法:
方式1:在字符串的最后手动添加'\0'即可;
方式2:字符数组定义时便进行初始化,将字符数组的所有元素都初始化为”零“值;从根本解决问题。(实际开发常用方法:因为一般定义的局部变量都需要进行初始化再使用)
//通过for循环将26个大写英文字母存入字符数组,并以字符串的形式输出 #include <stdio.h> int main() { char str [30]; char c; int i; for(c=65,i=0; c<=90; c++,i++) { str[i]=c; } str[i]=0; //方式1 在字符串末尾手动添加0,即'\0' printf("%s",str); return 0; }
//通过for循环将26个大写英文字母存入字符数组,并以字符串的形式输出 #include <stdio.h> int main() { char str [30]={0}; //方式2:字符数组定义时便将字符数组的所有元素都初始化为0,或者说'\0' char c; int i; for(c=65,i=0; c<=90; c++,i++) { str[i]=c; } printf("%s",str); return 0; }
2、字符串的定义
用" "包括的字符序列,其末尾包含一个隐含的结尾标识字符'\0'。 字符串只需要满足以下两个条件其中一点即可。
1) 用" "表示。
2) 字符数组中包含'\0'。 温馨提示: 0、'\0'、NULL、false 都是 0 的表现形式。
3、二者的区别(面试)
以char str[]={'h','e','l','l','o'}; 和char str[]="hello";为例
区别1:sizeof(arr)/sizeof(arr[0])求解字符数组的长度时,---数组空间大小
(1)由双引号" "包围的字符串会自动的在末尾添加'\0',因此,在用sizeof求字符数组长度时,结果会比实际字符数组的长度多1个。 6个(求解的是字符数组所占的内存空间大小)
(2)逐个为字符数组赋值初始化并不会自动添加'\0',因此在用sizeof求字符数组长度时,结果就是实际的字符数组的长度。 5个
区别2:利用字符串处理函数strlen求字符串长度时,-----字符串的实际长度
strlen()字符串处理函数在头文件string.h中,返回的值是字符串的实际长度,不包括'\0',只要遇到'\0',那么之前的字符个数就被认作是该字符串的大小,是一个整数。
(1)由双引号" "包围的字符串会自动的在末尾添加'\0',因此,在用strlen求字符数组长度时,strlen返回的就是实际字符串长度。 5个
(2)逐个为字符数组赋值初始化并不会自动添加'\0',若字符数组定义处也未进行初始化,则利用strlen()求字符数组的长度时,结果为一个随机值,因为不知道什么时候遇到'\0'。
面试题:
char str[]="abc\123\a\0124hello";
(1) 利用sizeof()求数组长度: 12+1=13个
(2) 利用strlen()求数组长度: 12个
二、字符串的访问方式/使用
由于字符串在内存中也是连续存放的,是由一个个的字符组成,对字符数组元素进行大量移动或者拷贝时,同样可以考虑使用内存函数memcpy()、memmove(),更加简单高效,提高开发效率,后续总结内存函数。
上一节已经详细说明,C语言共有两种表示字符串的方法,第一种:字符数组存储字符串;第二种:字符串指针或者字符串常量(区别在于是否可以修改元素)。
第一种:字符数组存储字符串
1、下标引用操作符访问(同整型数组)/或者%s直接输出整个字符串(字符串特有方式)
字符数组(字符串)的访问同整型数组,因为他们在内存都是连续存放的,是通过下标引用操作符操作的,使用方式同变量的使用,str[i]代表字符串中第i个元素,并且字符数组元素的下标也是从0开始的,字符串长度减1结束。对于字符数组(字符串)的遍历通常是通过循环结构,如for循环,i既可以作为循环变量,又可以作为字符数组元素访问的下标定位。
字符数组(字符串)的元素个数(长度)可以通过头文件string.h中的strlen()函数求得,作为循环变量的终止条件,方便程序的实现。
#include <stdio.h>
#include <string.h>
int main()
{
char str[]="helloworld";
int len =strlen(str);
for (int i=0;i<len;i++)
{
printf("%c",str[i]); //通过for循环下标定位,输出每个字符(同数组使用)
}
//或者printf("%s",str); 字符串特有的方式,%s直接输出整个字符串
return 0;
}
2、通过指针自增或者指针+偏移量访问
对于数组而言,不论是字符数组名或者字符串名仍然代表字符串首字符的地址,它就是一个指针(地址)。由于数组在内存是连续存放的,因此可以通过指针对其进行访问,这是由于指针的自增运算和指针加偏移量是具有意义的,这与指针加1的能力有关,更与指针所指向的数据元素的数据类型有关,通过指针可以找到数据存放的内存地址,然后解引用便可以访问到这块内存空间,修改所存储的数据,或者遍历。使用指针自增运算和指针加偏移量的方式访问数组还是有区别的,根据实际开发需要选择。
一、 通过指针加偏移量的方式,此时循环变量可以作为指针的偏移量的次数,从而实现对整个字符串或者字符数组元素的访问。需要注意的是:
*(pstr+i)等价于*(str+i)也等价于pstr[i] ,还等价于str[i] , 第一种是先对指针进行偏移,在进行解引用访问这块内存空间,第三种底层实现本质上和第一种是一样的,字符串名或者字符数组名是字符串首元素的起始地址,因此,这四种使用方式本质一样,常用的是pstr[i]和str[i]。
因此,从下述代码可以得出:字符串名str、指针变量pstr、&str[0]这三种写法等价,都指向字符串的第一个字符元素。
#include <stdio.h>
#include <string.h>
int main()
{
char str[]="helloworld";
int len =strlen(str);
//相当于char *pstr=&str[0]; 字符数组名字符串名是字符串首元素的起始地址,
//现在pstr指字符串首字符元素的起始地址
char *pstr =str;
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%c",*(pstr+i)); 先指针偏移再解引用,也可以使用pstr[i]相当于str[i]
}
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
char str[]="helloworld";
int len =strlen(str);
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%c",*(str+i)); //字符数组名或者字符串名是字符串首元素的起始地址,因此,字符数组名
本质上也是一个指针,可以充当指针变量使用
}
return 0;
}
二、通过指针的自增运算符,访问字符数组(字符串)元素 ,指针自增1,代表指针偏移所指向的数据类型大小的字节数。
#include <stdio.h>
#include <string.h>
int main()
{
char str[]="helloworld";
int len =strlen(str);
//相当于char *pstr=&str[0]; 字符数组名字符串名是字符串首元素的起始地址,
//现在pstr指字符串首字符元素的起始地址
char *pstr =str;
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%c",*pstr++); //先解引用再自增
}
return 0;
}
指针自增或者指针+偏移量访问数组元素的区别:字符数组名代表字符串首元素的地址(十六进制的数),是一个常量,而字符指针pstr是变量(除非特别指明它是常量)它的值可以任意改变,即字符数组名只能指向字符串的开头,而字符指针可以指向字符串的开头,再指向其他元素,这对于自增运算符是不同的,因为自增运算符针对的是变量,而字符数组名str是一个常量地址,不可以进行改变,因此不能使用自增运算符,而字符指针变量pstr是一个变量,保存的是字符串首元素的地址,是可以改变的,故它可以使用自增运算符。
即*pstr++可以使用,但是*str++不可以使用。
第二种:字符串指针或者字符串常量 const char *pstr="helloworld";
如何理解:const char *pstr="heoworld";
字符串中的所有字符在内存中是连续排列的,pstr指向的是字符串的第1个字符(下标为0),通常将第一个字符的地址称为字符串的首地址,字符串中的每个字符的类型都是char ,所以,pstr的类型也必须是char,用const修饰,表示字符串指针pstr所指向的数据(字符串)不可以进行解引用,再修改。因此,这句代码的意思是:定义一个字符串常量"helloworld",用指针pstr指向字符串首字符'h'的地址,并且只能通过指针pstr解引用进行访问,而不能进行解引用修改!!!(常量不可以修改)
用指针定义的字符串常量的使用方式和指针访问字符串方式相同(指针自增或者指针加偏移量),只是它只可以访问,不可对字符串常量进行修改。
一、 通过指针加偏移量的方式,此时循环变量可以作为指针的偏移量的次数,从而实现对整个字符串常量的访问。
#include <stdio.h>
#include <string.h>
int main()
{
const char *pstr="helloworld"; //现在pstr指字符串首字符元素的起始地址
int len =strlen(str);
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%c",*(pstr+i)); 先指针偏移再解引用,也可以使用pstr[i]
}
return 0;
}
二、通过指针的自增运算符,访问字符串常量元素 ,指针自增1,代表指针偏移所指向的数据类型大小的字节数。
#include <stdio.h>
#include <string.h>
int main()
{
const char * pstr="helloworld";//现在pstr指字符串首字符元素的起始地址
int len =strlen(str);
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%c",*pstr++); //先解引用再自增
}
return 0;
}
C语言中表示字符串的两种方式:字符数组和字符串常量。二者的区别是什么??
二者最根本的区别在于:内存中的存储区域不一样。
(1)对于字符数组局部变量存储在栈区,可以进行访问和修改,随着函数调用结束而被销毁;
(2)对于字符串常量存储在常量区,只可以访问,不可以通过指针解引用修改字符串常量,不会随着函数调用结束而被销毁,但是字符串指针变量会随着函数调用结束而被销毁,同时对于并且字符串指针变量的大小只与平台有关,x86操作系统,指针占4个字节。
如:const char * pstr="hello"; *(pstr+2)='b'; 错误!字符串常量不可以进行修改。
因此,实际开发过程中,如果只涉及到字符串的读取(访问),那么设计成字符数组或者字符串常量都可以,如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量!!
三、字符串的输入
在C语言中,有两个函数可以让用户从键盘输入字符串,它们分别是:
(1)scanf()通过格式控制符"%s",输入字符串,当然除了字符串,还能输入其他类型的数据。
(2)gets()直接输入字符串,并且只能输入字符串;
二者的区别在于:
(1)scanf()读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串;
(2)gets()认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对gets()来说就是一个完整的字符串,换句话说,gets()用来读取一整行字符串,可以读取包含空格的字符串。
#include <stdio.h>
int main()
{
//字符数组全部初始化为'\0'
char str1[10]={0};
char str2[10]={0};
char str3[10]={0};
//gets的用法
gets(str1);
//scanf()的用法
scanf("%s",str2);
scanf("%s",str3);
//输出打印看一下
printf("\nstr1:%s\n",str1);
printf("str2:%s\n",str2);
printf("str3:%s\n",str3);
return 0;
}
上述代码,如果第一次输入:hello world回车 第二次输入:hello world回车
那么程序最后打印 str1:hello world str2:hello str3:world
注意,scanf()在读取数据时需要的是数据的地址,这一点是恒定不变的,所以对于 int、char、flat 等类型的变量都要在前边添加&以获取它们的地址。但是在本段代码中,只给出了字符串的名字,却没有在前边添加&,这是为什么呢?因为字符串名字或者数组名字在使用的过程中一般都会转换为地址,所以再添加&就是多此一举,甚至会导致错误!
就目前学到的知识而言,int、char、float 等类型的变量用于 scanf() 时都要在前面添加&,而数组或者字符串用于scanf() 时不用添加&,它们本身就会转换为地址。一定要谨记这一点。
四、字符串转换函数(atoi和itoa函数)编码实现
一、字符串转整型函数atoi()函数
函数原型:int atoi(const char *str);
函数作用:atoi()函数是将字符串转换成整数。头文件<stdlib.h>
返回值://atoi 字符串转整型,处理空格、数字、正负开头;其余开头返回0
注意事项:
1、数字字符前有空格存在则跳过。
2、数字字符前有+、-号作为整数的正负号处理。
3、数字字符前有其他字符则返回0。#define _CRT_SECURE_NO_WARNINGS #include <cassert> #include <stdio.h> #include <string.h> #include <math.h> #include <stdlib.h> #include <ctype.h> //求连续的数字字符的个数,方便进行转换计算 int getcharcount(char* str) { int count = 0; for (int i = 0; str[i] != '\0'; i++) { if (str[i] == ' ' || str[i] == '+' && str[i + 1] != '+' || str[i] == '-' && str[i + 1] != '-') { continue; } else if (isdigit(str[i])) { count++; } else { break; //abc-12abc3 -直接不进行转换结果返回0 } } return count; } //atoi字符串转整型 int my_atoi(char* str) { assert(str != NULL); int len = strlen(str); int count = getcharcount(str); //求连续的数字字符个数 int res = 0; int flag = 1; for (int i = 0; i < len; i++) { if (str[i] == ' ' || str[i] == '+') { continue; } else if (str[i] == '-') { flag = -1; } else { res += (str[i] - '0') * (int)pow(10, count - 1); //字符‘0’和整型数字0的转换只差一个0字符 count--; } } return res * flag; } int main() { char str[] = " -12abc3"; int res = my_atoi(str); //"123" "+123" "-123" "--123" ->转换失败 " -123" "abc12abc3"->转换失败 " -12abc3" -> -12 printf("%d\n", res); return 0; }
二、整型转字符串函数itoa()函数
函数原型:char *itoa( int value, char *str, int base);
函数作用:itoa()函数是将整数类型转为字符串。头文件<stdlib.h>,itoa()函数有三个参数:
第一个参数是要转换的数字;第二个参数是要写入转换结果的目标字符串;第三个参数是转移数字时所用的基数。value :欲转换的数据。 str:目标字符串的地址。 base:转换后的进制数,可以是10进制、16进制等。返回值:返回对应的字符串
#define _CRT_SECURE_NO_WARNINGS #include <math.h> #include <cassert> #include <stdio.h> #include <stdlib.h>//atoi itoa #include<string.h> char* my_itoa(int value, char* buff, int radix) { assert(buff != NULL && radix >= 2); //辗转相除法 //查表法 char alpha[] = "0123456789abcdefghijklmnopqrstuvwxyz"; int flag = 0; if (value < 0 && radix == 10) { buff[0] = '-'; value *= -1; flag = 1; } unsigned int val = (unsigned int)value; int i = flag; while (val != 0) { buff[i++] = alpha[val % radix]; val /= radix; } buff[i] = '\0'; for (int j = 0; j < (i - 1)/2; j++) { char temp = buff[j+flag]; buff[j+flag] = buff[i - 1 - j]; buff[i - 1 - j] = temp; } return buff; } int main() { char buff[128] = { 0 }; //my_itoa(100, buff, 8);//144 //my_itoa(10, buff, 16);//a //my_itoa(10, buff, 2);//1010 //my_itoa(-1, buff, 2);//11111111111111111111111111111111 //my_itoa(-1, buff, 16);//ffffffff //my_itoa(-1, buff, 8);//37777777777 32个1 三位划分 开头补0 011--3 111-7 …… //my_itoa(-1, buff, 10);//-1 my_itoa(-1234, buff, 10);//-1234 printf("%s\n", buff); return 0; }
五、常用的字符处理函数(库函数)
C语言提供了丰富的字符处理函数,如字符检查、转换等函数。使用这些函数之前,要将包含该函数的头文件包含到源程序的开头。字符处理函数的头文件为“ctype.h”。
一、字符分类函数
二、字符转换函数
1. int tolower(int c);
tolower
函数用于将字符转换为小写形式。它接受一个整数参数c
,该参数代表一个字符的ASCII码值或EOF。如果c
是一个大写字母(A-Z),则返回对应的小写字母的ASCII码值;否则,c
保持不变。
2. int toupper(int c);
toupper
函数用于将字符转换为大写形式。它接受一个整数参数c
,该参数代表一个字符的ASCII码值或EOF。如果c
是一个小写字母(a-z),则返回对应的大写字母的ASCII码值;否则,c
保持不变。
六、常用字符串处理函数(库函数)理解编码实现
C语言提供了丰富的字符串处理函数,如字符串的输入、输出、比较、合并、复制、转换、搜索等函数,使用起来非常方便,大大简化了程序的设计。使用这些函数之前,要将包含该函数的头文件包含到源程序的开头。字符串输入输出函数的头文件是“stdio.h”,字符串处理函数的头文件是“string.h”。
长度不受限制的字符串函数
一、求字符串长度函数strlen()
函数原型:size_t strlen ( const char * str );
函数作用:字符串以 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包 含 '\0' ),即字符串的实际长度。
返回值:函数的返回值为无符号整型unsigned int 重命名为:size_t !
注意事项:参数指向的字符串必须要以 '\0' 结束,否则无法正确扫描字符串,返回正确的 实际长度!
//方法一:计数器思想,以数组使用方式(下标引用操作符)访问字符串 int mystrlen( const char* str) { assert(str != NULL); //传入的指针参数检验 int count = 0; for (int i = 0; str[i] != '\0'; i++) { count++; } return count; } //方法一:计数器思想,以指针方式(解引用)访问字符串 int mystrlen(const char* str) { assert(str != NULL); //传入的指针参数检验 int count = 0; while (*str != '\0') //也可以写成*str++ != '\0'; { count++; str++; } return count; } //方法二://不能创建临时变量计数器--采用递归思想 int mystrlen(const char * str) { if(*str == '\0') return 0; else return 1+mystrlen(str+1); //指针向后偏移一个单位,也就是偏移一个字节 } //方法三:指针-指针的方式--两个指针相减结果为之间相差的元素个数 int mystrlen(char *s) { char *p = s; //必须要保证指针的起始地址没有发生变化,否则相减会出错! while(*p != '\0' ) p++; return p-s; }
二、字符串拷贝函数strcpy()
函数原型:char* strcpy(char * destination, const char * source );
函数作用:将源字符串复制到目标字符串,在函数原型内部传入的是源目标字符串的起始地址和目标字符串的起始地址。
返回值:返回的是目标字符串的起始地址。
注意事项:源字符串必须以'\0'结束,因为会将源字符串中的 '\0' 拷贝到目标空间,如果没有就无法确定结束的位置,此外,目标字符串空间必须足够大,以确保能存放源字符串。目标字符串必须是可修改的字符数组,不能是字符串常量!
经常出现的错误1:char str[]="abcd"; str="fgh"; 字符串名为字符串首地址为常量,不可以作左值!
经常出现的错误2:char str1[]="abcd";char str2[]="efg" ;str1=str2;
//方法一:循环遍历赋值,以数组使用方式(下标引用操作符)访问字符串 void mystrcpy(char* arr, const char* brr) { assert(arr!=NULL && brr!=NULL); //传入的指针检验 int i = 0; while (brr[i] != '\0') { arr[i] = brr[i]; i++; } arr[i] = '\0'; //字符赋'\0',表示字符串结束标志 } //方法一:循环遍历赋值,以数组使用方式(下标引用操作符)访问字符串,只是循环条件有所变化 void mystrcpy(char* arr, char* brr) { assert(arr!=NULL && brr!=NULL); //传入的指针检验 int i = 0; while (arr[i] = brr[i]) { i++; } /*在这个while循环中,条件arr[i] = brr[i]被用作循环的条件。这个条件不仅检查循环结束,还执行了一个赋值操作。 第一步:计算brr[i]: 获取源字符串(brr)当前索引i处的值。 第二步:赋值操作:将获取的值赋给目标字符串(arr)相应的索引i。 第三步:检查字符串结束: 赋值后,使用赋值的值作为while循环的条件。如果赋值的值不为0(在C中代表空字符'\0'),则循环继续。 第四步:递增索引(i++): 在赋值和检查后,递增索引i,移动到源和目标字符串中的下一个位置。 这个过程重复进行,直到在源字符串(brr)中遇到空字符'\0'为止,此时条件变为假,循环退出。*/ } //方法一:循环遍历赋值,以指针方式(解引用)访问字符串 void mystrcpy(char* str, const char* src) { assert(str != NULL && src != NULL); while (*src) { *str = *src; //或者就一句:*str++ = *src++; str++; src++; } *str = '\0'; } //方法一:循环遍历赋值,以指针方式(解引用)访问字符串,只是循环条件有所变化 char *mystrcpy(char *dest, const char*src) { char *ret = dest; assert(dest != NULL && src != NULL); while((*dest++ = *src++)) { ; } /*在这个while循环中,条件(*dest++ = *src++)被用作循环的条件。 第一步:赋值和比较操作: *src的值(源字符串的当前字符)被赋值给*dest(目标字符串的当前字符),然后dest和src指针都被递增。这里使用了后缀递增运算符,所以赋值操作完成后才递增指针。这样,赋值和比较操作是一体的,因此如果*src不是空字符(\0),则条件为真。 第二步:循环体:循环体内部是一个空语句(;),即没有具体的操作。这是因为在条件表达式中已经完成了赋值和比较操作,而在循环体内部并没有额外的操作需要执行。 第三步:循环继续:只要*src不是空字符,循环就会继续执行。一旦遇到源字符串的空字符('\0'),条件为假,循环就会退出。 函数返回ret,这是目标字符串的起始地址。这样,调用者可以通过返回值得知复制后的字符串的起始位置。*/ return ret; }
三、字符串追加函数(连接函数)strcat()
函数原型:char * strcat ( char * destination, const char * source );
函数作用:把源字符串追加到目标字符串的后面,连同'\0'一起追加,在函数原型内部传入的是源目标字符串的起始地址和目标字符串的起始地址。
返回值:返回的是目标字符串的起始地址。
注意事项:源字符串必须以 '\0' 结束,因为它把'\0'作为结束标志,目标字符串空间必须有足够的大,能容纳下源字符串的内容,目标字符串必须是可修改的字符数组,不能是字符串常量!并且无法实现自己给自己追加(因为'\0'会被覆盖,导致没有字符串结束标志)。
//方法一:循环遍历赋值,以数组使用方式(下标引用操作符)访问字符串 void mystrcat(char* arr, const char* brr) { assert(arr!=NULL && brr!=NULL); int i = 0; int j; while (arr[i] != '\0') //获取数组arr中字符串的结束符'\0'的位置 { i++; } j = i; //此时arr[j]为结束符 i = 0; while (brr[i]) { arr[i + j] = brr[i]; //从数组arr的结束符处开始连接数组brr的字符串 i++; } arr[i + j] = 0; //字符串连接后,要将数组arr的最后一个字符后面加上结束符'\0' } //方法一:循环遍历赋值,以指针方式(解引用)访问字符串 char * mystrcat(char * dest, const char* src) { char *ret = dest; //保存目标字符串的起始地址,防止后面指针++,指向发生变化 assert(dest != NULL&&src != NULL); while(*dest) //第一步:定位目标字符串的结束标志位置'\0' { dest++; } while((*dest++ = *src++)) //第二步:追加源字符串到目标字符串 { ; } return ret; //返回目标字符串的起始地址 }
四、字符串比较函数strcmp()
函数原型:int strcmp ( const char * str1, const char * str2 );
函数作用:逐一比较两个字符串的每个字符的大小(比较的是字符对应的ASCII码),标准规定:第一个字符串大于第二个字符串,则返回大于0的数字 ;第一个字符串等于第二个字符串,则返回0;第一个字符串小于第二个字符串,则返回小于0的数字。
返回值:返回一个整型,代表两个字符串的大小
注意事项:比较两个字符串不能这样:if("ac">"hd") 这样比较的是首字符的地址大小,毫无意义!
//方法一:循环遍历比较,以数组使用方式(下标引用操作符)访问字符串 int mystrcmp(const char* arr, const char* brr) { assert(arr != NULL && brr != NULL); int i = 0; //这段代码通过循环逐个比较两个字符串的对应字符,直到发现不相等的字符或者遇到字符串结束符为止。如果在比较过程中发现字符串相等(遇到了结束符 \0),则返回 0;否则,返回一个表示不相等的值,具体是 1还是-1取决于哪个字符串的当前字符的ASCII值更大。 while (arr[i] == brr[i]) { if (arr[i] == '\0') return 0; i++; } return arr[i] > brr[i] ? 1 : -1; } //方法一:循环遍历比较,以指针方式(解引用)访问字符串 int mystrcmp(const char* str, const char* src) { assert(str != NULL && src != NULL); while (*str++ == *src++) { if (*str == '\0') return 0; } return *str > *src ? 1 : -1; }
长度受限制字符串函数
五、受限制字符串拷贝函数 strncpy()
函数原型:char * strncpy ( char * destination, const char * source, size_t num );
函数作用:将源字符串的前num个字符复制到目标字符串。如果在复制了num个字符之 前发现源C字符串的结尾(由空字符'\0'标志),则将在目标字符串中用零进行填充,直到总共写入了num个字符。
返回值:目标字符串起始地址
注意事项:如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
六、 受限制字符串追加函数(连接函数)strncat()
函数原型:char * strncat ( char * destination, const char * source, size_t num );
函数作用:将源字符串的前num个字符追加到目标字符串,同时加上一个终止的空字符'\0'。如果源字符串中的C字符串长度小于num,则只复制到终止的空字符为止的内容。
返回值:目标字符串起始地址
注意事项:如果源字符串中的C字符串长度小于num,则只复制到终止的空字符为止的内容。
七、 受限制字符串比较函数(连接函数)strncmp()
函数原型:int strncmp ( const char * str1, const char * str2, size_t num );
函数作用:比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完.标准规定:第一个字符串大于第二个字符串,则返回大于0的数字 ;第一个字符串等于第二个字符串,则返回0;第一个字符串小于第二个字符串,则返回小于0的数字。
返回值:返回一个整型,代表两个字符串的大小。
八、 字符串查找函数strstr()
函数原型:char * strstr ( const char *str1, const char * str2);
函数作用:通常用于在一个字符串中查找指定子字符串的第一次出现,并返回指向该位置的指针。
返回值:函数返回一个指向第一次出现子字符串位置的指针。如果未找到子字符串,返回
NULL
。注意事项:可能需要多次匹配才能查找成功,涉及到指针的回位问题,编码实现是双指针思想!!
#include <stdio.h> #include <cassert> char* my_strstr(const char* strl, const char* str2) assert(str1 !=NULL && str2 !=NULL); //传入指针参数检验 //定义两个遍历字符串的指针,初始化为NULL(双指针思想) const char*s1 = NULL; const char*s2 = NULL; //定义一个指针,用来保存上一次匹配的起始位置 const char* cp = str1; //如果查找的子字符串为空,直接返回str1的地址 if (*str2 =="\0!) return (char*)str1; //开始循环遍历字符串,进行比较, while (*cp) { s1 = cp; s2 = str2; while (*sl !='\0' && *s2 !='\0' && (*s1 == *s2)) { s1++; s2++; } //子字符串查找完毕 if(*s2='\0') { return (char*)cp; } cp++; //重新开始从上一次匹配的起始位置的下一个位置开始 } return NULL; int main() { char arr1[]="abbbcdef"; char arr2[] ="bbc"; //在arr1中查找是否包含arr2数组 char* ret = my strstr(arrl, arr2); if (ret == NULL) { printf("没找到\n"); } else { printf("找到了:%s\n",ret); } return 0; }
九、字符串分割函数strtok()
函数原型:char * strtok ( char * str, const char * sep );
函数作用:这个函数的目的是从字符串str中提取出一系列子字符串,每个子字符串都以 sep中的字符作为分隔符。在第一次调用时,str应该是待分割的字符串,而在后续的调用中,str应该设置为 NULL。
返回值:strtok函数找到str中的下一个标记,并将其用'\0'结尾,返回一个指向这个标记的指针。(注: strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
注意事项:
- strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
- strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。 如果字符串中不存在更多的标记,则返回 NULL 指针。
#include <stdio.h> #include <string.h> int main () char arr[] = "zpw@bitedu.tech"; char* p ="@."; //临时拷贝一份内容,便于操作 char tmp[20] ={ 0 }; strcpy(tmp, arr); char* ret = NULL; for (ret = strtok(tmp, p); ret != NULL; ret=strtok(NULL, p)) { printf("ss\n",ret); } return 0; }
十、将错误码转换为相应的错误消息字符串函数strerror ( )
函数原型:char * strerror ( int errnum );
函数作用:strerror 是一个C标准库函数,用于将错误码转换为相应的错误消息字符串。
返回值:该函数接受一个整数参数
errnum
,代表一个错误码,然后返回一个指向错误消 息字符串的指针。错误消息字符串描述了与给定错误码相关联的错误。注意事项:errnum是定义的代表错误消息的全局变量,使用时必须包含"errno.h"头文件。
#include <stdio.h> #include <string.h> //strerror()函数所在头文件 #include <errno.h> //errno全局变量所在头文件 int main() { //打开文件 FILE *file = fopen("1.txt", "r"); if (file == NULL) { //perror的作用如下:第一步:将错误码转换成对应的错误信息;第二步:打印错误信息 perror("文件打开失败!"); //strerror的作用:将错误码转换为相应的错误消息字符串 printf("Error: %s\n", strerror(errno)); } else { //文件成功打开 fclose(file); } return 0; }
十一、字符首次出现查找函数strchr()
函数原型:char *strchr(const char *str, char ch); 或者第二个参数为: int ch
函数作用:用于在一个字符串中查找指定字符的第一次出现位置。
返回值:函数返回一个指向字符
ch
在字符串str
中第一次出现的位置的指针。如果找不到指定字符,则返回NULL
注意事项:如果想要返回的是字符第一次出现的索引位置,可以利用指针-指针的方式(减去字符串起始位置)得到偏移量,便是索引位置!
//方式一:循环遍历比较,以数组使用方式(下标引用操作符)访问字符串 char * mystrchr(const char *str, char ch) { assert(str!=NULL); int i = 0; while(str[i]!='\0') { if(str[i] == ch) { return &str[i]-str; } i++; } return NULL; } //方式一如果要得到字符串索引位置,利用地址-地址(指针减指针) int mystrchr(const char* str, char ch) { assert(str != NULL); int i = 0; while (str[i] != '\0') { if (str[i] == ch) { return &str[i] - str; } i++; } return NULL; } //方式二:循环遍历赋值,以指针方式(解引用)访问字符串 char * mystrchr(const char * str, char ch) { assert(str!=NULL); const char *p= str; while (*p !='0') { if (*p == ch) { return p-str; } p++; } return NULL; } //方式二:如果要得到字符串索引位置,利用指针减指针 int mystrchr(const char* str, char ch) { assert(str != NULL); const char* p = str; while (*p != '\0') { if (*p == ch) { return p - str; } p++; } return NULL; }
十二、字符最后一次出现查找函数strrchr()
函数原型:char *strrchr( const char *str, char ch ); 或者第二个参数为: int ch
函数作用:用于在字符串中查找指定字符(一个无符号字符)最后一次出现的位置。
返回值:函数返回一个指向最后一次出现的字符位置的指针,如果未找到该字符,则返回
NULL
。//方式一:循环遍历比较,以数组使用方式(下标引用操作符)访问字符串 char* mystrchr(const char* str, char ch) { assert(str != NULL); const char* p = NULL; // 遍历字符串,查找字符最后一次出现的位置 int i = 0; while (str[i] != '\0') { if (str[i] == ch) { p = &str[i]; } i++; } // 返回字符最后一次出现的位置,或者返回NULL表示未找到 return (char*)p; } //方式二:循环遍历赋值,以指针方式(解引用)访问字符串 char * mystrchr(const char *str, char ch) { assert(str!=NULL); const char *p=NULL; // 遍历字符串,查找字符最后一次出现的位置 while (*str != '\0') { if (*str == ch) { p = str; } str++; } // 返回字符最后一次出现的位置,或者返回NULL表示未找到 return (char*)p; }
七、内存操作函数,编码实现
对于在内存连续存放的数据,如果要对大量数据进行移动或者拷贝,可以考虑使用内存函数,直接通过对底层的内存进行操作,方便快捷,易于提高开发效率。使用这些函数之前,要将包含该函数的头文件包含到源程序的开头。内存操作函数的头文件是“string.h”。常用的内存函数有:memcpy()、memmove()、memcmp()、memset()这四个。
一、memcpy()
存在的原因:已经存在的strcpy()与数据类型有关,如果想要拷贝与数据类型无关的数据,就需要重新设计,memcpy()函数是按照字节拷贝,与数据类型无关!
函数原型:void * memcpy ( void * destination, const void * source, size_t num );
函数作用:memcpy()函数用于从源内存地址复制指定数量的字节到目标内存地址。其中destination是指向目标内存的地址,即要将数据复制到的位置。source是指向源内存的地址,即要从哪里复制数据。size_t 代表无符号整型类型的重命名,num是需要复制到字节数
返回值:返回void*类型的指针,指向目标内存的指针(地址),表明与数据类型无关。
注意事项:memcpy()函数不关心数据类型,他只是通过逐字节复制数据,将源内存中的内容复制到目标内存。适用于拷贝内存不重叠的区域!
//方式一:循环遍历赋值,以下标引用操作符访问数组 void * mymemcpy(void* dest, const void * src, size_t num) { assert(dest!=NULL && src!=NULL); //指针强转是因为逐字节复制数据,这取决于指针加一的能力,一次复制一个字节数据,因此强转为char* char *p=(char*)dest; char *q=(char*)src; //逐字节复制数据,循环变量作为指针的偏移量 for(int i=0;i<num;i++) { p[i]=q[i]; } return dest; } //方式二:循环遍历赋值,以指针自增方式访问数组 void * mymemcpy(void* dest, const void * src, size_t num) { assert(dest!=NULL && src!=NULL); void *ret = dest; //保存目标内存的起始地址,方便返回 while(num--) { //将void*强转为char*,方便按照字节操作复制,这取决于指针加1的能力 *(char*)dest=*(char*)src; //指针自增1 dest=(char*)dest+1; src=(char*)src+1; } return ret; }
二、memmove()
函数原型:void * memmove ( void * destination, const void * source, size_t num );
函数作用:memmove()函数与memcpy()函数类似,它也可以用于内存中移动数据(拷贝数据)然而与memcpy()函数不同的是,memmove()函数能够处理源内存与目标内存区域重叠的情况,而不会导致数据因覆盖而被损坏。其中destination是指向目标内存的地址,即要将数据复制到的位置。source是指向源内存的地址,即要从哪里复制数据。size_t 代表无符号整型类型的重命名,num是需要复制到字节数。
返回值:返回void*类型的指针,指向目标内存的指针(地址),表明与数据类型无关。
注意事项:memmove()函数不关心数据类型,他只是通过逐字节复制数据,将源内存中的内容复制到目标内存。可用于拷贝内存重叠的区域!
编码实现这个函数时,数据拷贝是有方向的,从前往后或者从后往前,方向选择不正确可能会导致内存区域移动导致数据覆盖,从而导致数据被损坏!!解决办法:正确的选择数据拷贝的方向!
//方式一:循环遍历赋值,以下标引用操作符访问数组 void * mymemove(void* dest, const void * src, size_t num) { assert(dest!=NULL && src!=NULL); //指针强转是因为逐字节复制数据,这取决于指针加一的能力,一次复制一个字节数据,因此强转为char* char *p=(char*)dest; char *q=(char*)src; //目标地址在源地址之后,从最后一个字节开始移动赋值 if(p>q || p<q+num) { p+=num-1; q+=num-1; for(int i=0;i<num;i++) { p[i]=q[i]; } } else { for(int i=0;i<num;i++) { p[i]=q[i]; } } return dest; } //方式二:循环遍历赋值,以指针自增方式访问数组 void * mymemmove(void* dest,const void* src,size_t num) { assert(dest!=NULL && src!=NULL); char * if(dest<src) { //从前向后拷贝 while(num--) { //将void*强转为char*,方便按照字节操作复制,这取决于指针加1的能力 *(char*)dest=*(char*)src; //指针自增1 dest=(char*)dest+1; src=(char*)src+1; } } else { //从后向前拷贝 while(num--) { //跳到最后一个字节的起始地址向前拷贝 *((char*)dest+num)= *((char*)src+num); } } return ret; }
三、memcmp()
函数原型:int memcmp( const void* ptr1, const void* ptr2, size_t num);
函数作用:memcmp()函数用于比较两块内存区域的内容,它返回一个整数,表示两个内存区域的大小关系。ptr1指向要比较的第一个内存块的指针(地址),ptr2指向要比较的第二个内存块的指针(地址),size_t 代表无符号整型类型的重命名,num是需要比较的字节数。
返回值:如果ptr1>ptr2,则返回一个正整数,如果ptr1=ptr2,则返回零,如果ptr1<ptr2,则返回一个负整数。
注意事项:memcmp()函数会按照字节比较,ptr1和ptr2指向的内存区域的前num个字节,比较的结果通过返回值表示。
四、memset()
函数原型:void *memset( void *ptr, int value, size_t num );
函数作用:memset()函数用于将一块内存区域的内容设置为指定的值。ptr指向要设置的内存区域的地址(指针),value要设置的值,通常是一个无符号字符(unsigned char),num代表要设置的字节数。
返回值:函数返回指向ptr的指针。
注意事项:memset()函数将ptr指向的内存区域的前num个字节设置为指定的值value,但是需要注意memset()函数中的value参数是整型的,但在实际应用中,通常会选择使用无符号字符来作为value参数,例如'\0'、'0',进行清零或者对内存进行初始化。这是因为memset()函数会按字节来设置内存,而value参数实际上是以int类型来传递的,字符'\0'、'0'与整数0本质上是一样的,存储的是ASCII码值,当value是一个正整数时,可能会导致字节设置为非零值,而不是预期的零值,在某些情况下引起程序错误,特别是对内存进行初始化或清零操作。
因此,在实际开发中为避免这个问题,通常建议将value参数设置为无符号字符,类型的零值如'\0'、'0'、这确保了设置内存时,每个字节都被正确的设置为零值,而不会受到int类型的符号位影响!
以上便是字符数组和字符串需要全面掌握内容,掌握基本的理论,遇到实际问题需要具体分析,才不会出现语法使用错误,提高开发效率, 更多精彩内容见下期!欢迎交流分享!