目录
前言
字符串是C语言中最有用、最重要的数据类型之一,本文是对字符串、字符串函数和字符函数笔记的总结。
一、字符串
1.字符串常量(字符串字面量):用双引号括起来的内容。字符串的结束标志是一个 \0 的转义字符。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串存储在内存中。
如果字符串常量之间没有间隔,或者用空白字符分隔,C将其视为串联起来的字符串常量。如:char greeting[50] = "Hello,and""how are""you" " today!";
等价于:char greeting[50] = "Hello,and how are you today!";
字符串常量属于静态存储类别,这说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即使函数被调用多次。
2.字符串数组的初始化:
定义字符串数组时必须要让编译器知道需要多少空间,我们可以采取两种方法:
(1)用足够的空间的数组存储字符串(必须确保数组元素的个数至少比字符串长度多1,用于容纳\0)(其余所有未被使用的元素都被自动初始化为0,这里的0不是指数字字符0,而是char形式的空字符),如:
(2)省略数组初始化声明中的大小,编译器会自动计算数组的大小,如:
char arr[] = "ant and";
3.字符串数组和字符指针:
char sa[] = "array"; VS char* sp = "point";
(1)数组形式:
在计算机的内存中为数组sa分配一块连续的空间。通常,字符串都作为可执行文件的一部分存储在数据段中。当把程序载入内存时,也载入了程序的字符串。字符串存储在静态存储区中,但是程序在开始运行时才会为该数组分配内存,此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本:一个是在静态内存中的字符串常量,另一个是存储在sa数组中的字符串。此后,编译器便把数组名sa识别为该数组首元素的地址。注意:sa为地址常量,必能更改,如果更改,这就意味着改变了数组的存储位置。可以有sa+1这样的操作,标识数组的下一个元素,但是不允许sa++等操作,递增递减运算符只能用于变量名前,不能用于常量。
(2)指针形式:
编译器也会为字符串在静态存储区预留一块连续的空间,另外,一旦开始执行程序,它会为指针变量sp也留出一块存储位置,并把字符串的地址存储在指针变量中,该变量最终指向字符串的首字符。sp为指针变量,它的值可以改变,因此,可以使用递增递减运算符。
我们来看一段代码:
#include <stdio.h>
#define MSG "I am special"
int main()
{
char sa[] = "I am special";
const char* sp = "I am special";
printf("%p\n", "I am special");
printf("%p\n", sa);
printf("%p\n", sp);
printf("%p\n", MSG);
printf("%p\n", "I am special");
return 0;
}
编译并运行该代码,输出如下:
该程序的输出说明了:
①初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。(sp和MSG的地址相同,而sa的地址不同)
②虽然字符串常量“I am special”在程序中的两个printf()函数中出现了两次,但是编译器只使用了一个存储位置。而且与MSG的位置相同
③静态数据使用的内存与sa使用的动态内存不同。不仅值不同,特定的编译器甚至使用不同的位数表示两种内存
(3)如果要改变字符串,就不要用指针指向字符串常量,可以把非const数组初始化为字符串常量,因为数组获得的是原始字符串的副本。我们来看一段代码:
#include <stdio.h>
int main()
{
char* p = "Klingon";
p[0] = 'F';
printf("Klingon\n");
printf(":Beware the %s!\n", "Klingon");
return 0;
}
p[0] = 'F';这样的行为是未被定义的,这样的语句有可能导致内存访问出错,也有可能会输出Flingon:Beware the Flingons(在内存中的字符串常量只有一份,修改这一份,会影响多出使用的地方),编译器在这方面难以捉摸,所以我们建议在把指针初始化为字符串常量时使用const限定符。即:const char* p1 = "Klingon";
4.指针和字符串
#include <stdio.h>
int main()
{
const char* mesg = "apple";
const char* copy;
copy = mesg;
printf("mesg = %s; &mesg = %p; value = %p\n", mesg, &mesg, mesg);
printf("copy = %s; © = %p; value = %p\n", copy, ©, copy);
return 0;
}
编译并运行该代码,输出如下:
从输出结果我们可以知道:mesg和copy的地址分别是00CFFC50和00CFFC44,mesg和copy都指向同一位置,程序并没有拷贝字符串,两个value值相同。
5.字符串数组和指针数组
char ccolor[][6] = {"red","blue","green"};
char* pcolor [] = {"red","blue","green"};
区别如下:
(1)ccolor是一个内含3个数组的数组,每个数组内含6个char类型的值,共占用18个字节,而pcolor是一个内含3个指针的数组,共占用12个字节。
(2)pcolor中的指针指向初始化时所用的字符串常量的位置,这些字符串常量存储在静态内存中,它所指向的字符串常量不能更改;ccolor中的数组则存储这字符串常量的副本,所以每个字符串都被存储了两次,此外,为字符串数组分配内存的使用率较低,ccolor中的每个元素必须相同,而且必须是能存储最长字符串的大小
6.我们来做一道有关字符串数组的题目:
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
char *str3 = "hello world.";
char *str4 = "hello world.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
编译并运行该程序,输出如下:
str1 and str2 are not same
str3 and str4 are same
//根据前面的总结,我们可以轻松得到这条题的答案
二、字符串的输入
1、gets():
它读取整行输入,直至遇到换行符,然后丢弃换行符,存储其他字符,并在这些字符的末尾添加一个空字符使其成为一个c字符串。
注意:gets()存在安全隐患,gets()无法检查字符串是否装得下输入行,如果输入的字符串过长,会导致缓冲区溢出,如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦除程序中的其他数据,会导致程序异常终止;或者还有其他情况。所以不久C11便摒弃了gets(),然而,在实际应用中,编译器为了可以兼容以前的代码,大部分都支持该函数。
2、fgets():gets()的替代品
(1)char *fgets( char *string, int n, FILE *stream );
string:数据存储的位置
n:读入字符的最大数量
stream:要读入的文件(如果从键盘输入数据,则以stdin(标准输入)作为参数)
(2)如果一切进行顺利,该函数返回的地址和传入的第一个参数相同,如果遇到文件末尾或发生错误,该函数会返回NULL。可以使用feof或ferror确定是否发生错误。
(3)与gets()还有一点不同的是:如果fgets()读到换行符,会把它存储在字符串中。但是有时候我们并不想把\n存储在字符串中,这样的换行符会带来一些麻烦,我们可以自定义一个函数:读取整行输入并用空字符代替换行符,或者读取一部分输入,并丢弃其余部分,看如下代码段:
char* mygets (char* st, int n)
{
char* ret;
int i = 0;
ret = fgets(st, n, stdin);
if(ret)
{
while(st[i] !='\n' && st[i] != '\0')
{
i++;
}
if(st[i] == '\n')
{
st[i] = '\0';
}
else
{
while(getchar() != '\n')
{
continue;
}
}
}
}
我们的这个函数的缺点在于在遇到不合适的输入时毫无反应。
3、scanf():
使用格式控制说明%s,输入参数必须是字符型数组名。该函数以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束。如果指定了字段宽度,如%10s,那么scanf()将读取10个字符或读到第一个空白字符停止。如图表:
输入语句 | 原输入序列 | name中内容 | 剩余输入序列 |
scanf("%s",name); | Fleebert[]Hup | Fleebert | []Hup |
scanf("%5s",name); | Fleebert[]Hup | Fleeb | ert[]Hup |
scanf("%5s",name); | Ann[]Ular | Ann | []Ular |
注:[]代表一个空格
scanf()返回一个整数值,该值等于scanf()成功读取的项数或EOF(读到文件结尾时返回EOF)
其实,scanf()和gets()一样也存在一些潜在的缺点。如果输入行的内容过长,scanf()也会导致数据溢出,不过,%s转换说明中使用字段宽度可防止溢出。
三、字符串的输出
1、puts():
puts()的使用很简单,只需要把字符串的地址作为参数传递给它即可。它在显示字符串时会自动在其末尾添加一个换行符。该函数在遇到空字符时就停止输出,所以必须确保有空字符
2、fputs():
(1)int fputs( const char *string, FILE *stream );
string:要输出的字符串
stream:要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为该参数
(2)与puts()不同的是,fputs()不会在输出的末尾添加换行符
3、printf():
(1)用%s输出字符串,输出参数可以是字符数组名或字符串常量,输出遇'\0'结束。
(2)与puts()不同的是,printf()不会在输出的末尾添加换行符
4、自定义的输出函数
不一定非要使用C库中的标准函数,我们自己也可以设计一个函数。如该函数可以打印一个字符串,并且统计打印的字符数
int myputs(const char* string)
{
int count = 0;
while(*string)//*string != '\0'
{
putchar(*string++);
count++;
}
putchar('\n');
return 0;
}
四、字符串函数
1、字符串长度函数strlen():
size_t strlen( const char *string );
strlen(s);参数s可以是字符数组名或字符串常量
(1)引用头文件<string.h>
#include <stdio.h>
#include <string.h>
int main()
{
const char*str1 = "abcdef";
const char*str2 = "bbb";
if(strlen(str2) - strlen(str1) > 0)
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
编译并运行该代码,输出如下:
str2>str1
//该函数返回的是的无符号整型,无符号数-无符号数=无符号数(没有符号位的概念),3-6=-3,这将被解读成一个很大的正数
char arr = {"a","b","c"};
strlen(arr);//随机值,因为你不知道\0在哪里
(4)自定义的strlen():
#include <stdio.h>
#include <assert.h>
//法一:利用计数器
int mystrlen(const char* str)
{
int count = 0;
assert(str != NULL);
while(*str != '\0')
{
count++;
str++;
}
return count;
}
//法二:函数递归
int mystrlen(const char* str)
{
assert(str != NULL);
if(*str != '\0')
{
return 1 + mystrlen(str + 1);
}
else
{
return 0;
}
}
//法三:指针-指针
int my_strlen(const char *str)
{
char *p = s;
while(*p != '\0' )
p++;
return p-s;
}
int main()
{
char* arr = "abc";
printf("%d\n",mystrlen(arr));
return 0;
}
2、字符串的复制函数strcpy():
char *strcpy( char *strDestination, const char *strSource );
strcpy(s1,s2);参数s1必须是字符型基地址,参数s2可以是字符数组名或字符串常量
(1)引用头文件<string.h>
(2)源字符串必须以 '\0' 结束。
(3)会将源字符串中的 '\0' 拷贝到目标空间。
(4)目标空间必须足够大,以确保能存放源字符串。
(5)目标空间必须可变。
#include <stdio.h>
int main()
{
char arr1[] = "xxxxxxxxxx";
//char arr1[] = { 'a','b','c' };//error 源字符串没有以'\0'结尾
//char arr1[] = "xxx";//error 目标空间不够大
//const arr1[]="xxxxxxxxxx";//error 目标空间不可变
char arr2[] = "hello";
strcpy(arr1, arr2);
}
(6)该函数返回的是目标字符串strDestination的起始地址
(7)自定义的strcpy():
#include <stdio.h>
#include <assert.h>
char* mystrcpy(char* dest, const char* src)
{
assert(src && dest);
char* ret = dest;
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[] = "abcdef";
char arr2[10] = { 0 };
printf("%s\n", MyStrcpy(arr2, arr1));
return 0;
}
3、字符串的连接函数strcat():
char *strcat( char *strDestination, const char *strSource );
strcat(s1,s2);参数s1必须是字符型基地址,参数s2可以是字符数组名或字符串常量
(1)引用头文件<string.h>
(2)源字符串必须以 '\0' 结束(如果没有'\0'你不知道在哪里开始连接)
(3)目标空间必须有足够的大,能容纳下源字符串的内容
(4)目标空间必须可修改
(5)该函数返回的是目标字符串strDestination的起始地址
(6)自定义的strcat():
#include <stdio.h>
#include <assert.h>
char* mystrcat(char* dest, const char* src)
{
assert(dest, src);
char* ret = dest;
//1.找到目标字符串的末尾
while (*dest != '\0')
{
dest++;
}
//2.追加源字符串直到\0
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = "abc";
char arr2[] = { 'd','e','f','\0'};
printf("%s\n", mystrcat(arr1, arr2));
return 0;
}
4、字符串的比较函数strcmp():
int strcmp( const char *string1, const char *string2 );
strcmp(s1,s2);参数s1,s2,可以字符数组名或字符串常量
(1)引用头文件<string.h>
(2)该函数比较的是字符串的内容,不是长度。该函数的返回值表示string1和string2的关系
该函数的比较规则是:从两个字符串的首字符串开始,依次比较相对应的字符(比较字符的ASCII码值),直到出现不同的字符或遇到'\0'为止。如果所有的字符都相同,则返回0;否则,以第一个不相同字符的比较结果为准,返回两个字符的差(即第一个字符-第二个字符的差)。
(3)自定义的strcmp():
#include <stdio.h>
#include <assert.h>
int mystrcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while(*s1 == *s2)
{
if(*s1 == '\0')
return 0;
else
{
s1++;
s2++;
}
}
return *s1 - *s2;
}
int main()
{
char arr1[] = "abcd";
char arr2[] = "abc";
int ret;
ret = mystrcmp(arr1,arr2);
if(ret == 0)
{
printf("arr1 = arr2");
}
else if(ret > 0)
{
printf("arr1 > arr2");
}
else
{
printf("arr1 < arr2");
}
return 0;
}
其实,strcmp,strcpy,strcat:长度不受限制的字符串函数(不够安全)
但是,strncpy,strncat,strncmp:长度受限制的字符串函数(相对安全)
5、字符串的复制函数strncpy():
char *strncpy( char *strDest, const char *strSource, size_t count );
(1)引用头文件<string.h>
(2)strcpy()的问题在于,它不能检查目标空间能否容纳源字符串的副本,strncpy()的第三个参数指明可以拷贝的最大字符数。如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。该函数返回目标字符串strDest的起始地址。
(3)自定义的strncpy():
#include <stdio.h>
#include <assert.h>
#include <string.h>
char* mystrncpy(char* dest, const char* src, int n)
{
assert(dest && src);
char* ret = dest;
int offset = 0;
if (strlen(src) < n)
{
offset = n - strlen(src);
n = strlen(src);
}
while (n--)
{
*dest++ = *src++;
}
while (offset--)
{
*dest = '\0';
}
return ret;
}
int main()
{
char str1[20] = "abcdefg";
char str2[] = "xxxxx";
int n;
scanf("%d", &n);
mystrncpy(str1, str2, n);
printf("%s\n", str1);
return 0;
}
6、字符串的连接函数strncat():
char *strncat( char *strDest, const char *strSource, size_t count );
(1)引用头文件<string.h>
(2)strcat()无法检查参数1能否容得下参数2,如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元就会出现问题。strncat()的第三个参数指定了最大添加字符数。该函数返回目标字符串strDest的起始地址。
(3)自定义的strncat():
#include <stdio.h>
#include <assert.h>
#include <string.h>
char* myStrncat(char* dest, const char* src, int n)
{
char* ret = dest;
assert(dest && src);
while (*dest != '\0')
{
dest++;
}
if (strlen(src) < n)
{
n = strlen(src);
}
while (n)
{
*dest++ = *src++;
n--;
}
*dest = '\0';
return ret;
}
int main()
{
char str1[20] = "hello";
char str2[] = "world";
int n;
scanf("%d", &n);
myStrncat(str1, str2, n);
printf("%s\n",str1);
return 0;
}
7、字符串的比较函数strncmp():
int strncmp( const char *string1, const char *string2, size_t count );
(1)引用头文件<string.h>
(2)与strcmp()不同的是:该函数比较到出现另个字符不一样或者一个字符串结束或者count个字符全部比较完
8、字符串查找函数strstr():
char *strstr( const char *string, const char *strCharSet );
(1)引用头文件<string.h>。
(2)在string中搜索strCharSet。该函数返回一个指针,指向字符串中第一个出现的strCharSet,如果strCharSet未出现在字符串中,则返回NULL。如果strCharSet指向长度为零的字符串,则函数返回字符串。
(3)自定义的strstr():
#include <stdio.h>
#include <assert.h>
char* mystrstr(const char* str1, const char* str2)//只是查找,不会修改内容
{
assert(str1 && str2);
char* s1;
char* s2;
char* cp = str1;
if (*str2 == '\0')
{
return str1;
}
while (*cp)
{
s1 = cp;
s2 = str2;
while (*s1 == *s2 && *s2 != '\0' && *s1 !='\0')
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cp;
}
cp++;
}
return NULL;
}
int main()
{
char arr1[] = "i am a good student,a smart student";
char arr2[] = "student";
char* ret = mystrstr(arr1, arr2);//查找arr1中arr2第一次出现的位置
if (ret == NULL)
{
printf("找不到\n");
}
else
{
printf("%s\n", ret);
}
return 0;
}
9、切分字符串函数strtok():
char *strtok( char *str, const char *sep );
(1)引用头文件<string.h>
(2)sep参数是个字符串,定义了用作分隔符的字符集合
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "zpw@bitedu.tech";
char arr2[100] = { 0 };//临时数据
char sep[] = "@.";
//char* pch;
char* ret = NULL;
strcpy(arr2, arr1);//将数据拷贝一份,处理arr数组的内容
//pch = strtok(arr2, sep);
//while(pch!=NULL)
//{
// printf("%s\n",pch);
// strtok(NULL, sep);
//}
//其实,我们第一次传的是非空指针,后面传的全部是空指针,利用这个规律,我们可以巧妙的利用for循环
for (ret = strtok(arr2, sep); ret != NULL;ret = strtok(NULL,sep))
{
printf("%s\n", ret);
}
return 0;
}
10、strerror():
char *strerror( int errnum );
(1)引用头文件<string.h>和<errno.h>
(2)该函数的作用是返回错误码所对应的错误信息,C语言库函数调用失败的时候,会把错误码存储到errno变量中。
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
printf("%s\n",strerror(errno));
}
else
{
printf("打开文件成功\n");
}
return 0;
}
编译并运行该代码,输出如下:
因为我是以读的形式打开文件的,并且我的电脑中没有这个文件,所以会打开文件失败。
11、perror():
(1)引用头文件<stdio.h>或<stdlib.h>
(2)该函数的功能:打印的功能+strerror()的功能
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt","r");
if(pf == NULL)
{
perror("测试");
}
else
{
printf("打开文件成功\n");
}
return 0;
}
编译并运行该代码,输出如下:
五、字符函数
该表所列函数在头文件<ctype.h>中说明。
函数 |
如果他的参数符合下列条件就返回真1,否则返回假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
|
任何可打印字符,包括图形字符和空白字符
|