一、字符串的定义
字符串是以空字符‘\0’结尾的char类型数组。定义字符串的方式有三种。
1.字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量,也称作字符串常量。如果字符串常量之间用空白字符分隔(或者没有分隔),C会将其视为串联起来字符串常量。
字符串常量属于静态存储类别,在整个程序的生命期内存在。用双引号括起来的内容被视为指向该字符串存储位置的指针,指针指向的内容是字符串的首字符。
2.字符串数组和初始化
① const char m1[40] = " Limit yourself to onr line's worth ";
②const char m2[40]={ 'L','I','M','\0'};
③const char m3[ ]=" Limit yourself to onr line's worth ";
注意: 字符串数组初始化可以直接用双引号括起来的字符串常量,如果要一个个初始化,一定要自己在数组最后加上空字符,数组大小可以自己设置(一定要预留空字符的大小),也可以让编译器自己确定(只能用在创建并初始化的情况,否则不能用),声明数组时,数组大小必须是可求值的整数。
3.指针表示法创建数组
const char *pt1 = " Limit yourself to onr line's worth ";
注意: pt1 和 m1都是该字符串的地址,但是这两种形式并不完全相同 。
带双引号的字符串已经在系统中留出了存储空间(静态存储),字符串数组在创建好了之后,程序在运行时才会为数组分配空间,并且把静态区存储的字符串拷贝一份过去(现在有两个字符串,一个是字符串常量里的,一个是字符串数组里的,存储的内容一样),并且数组名是个常量,不能改变其值,这意味着你改变数组里的值,原字符串不会受到影响。
指针形式开始执行程序,系统只会给指针留出一个存储空间(int型),并且把字符串的地址存储在指针变量中(相同字符串只有一份),指针的值可以改变,如果通过指针改变了字符串,那么整个字符串全都变了,指针元素所指向的字符串不必存储在连续的内存中。
因此,如果打算修改字符串或者为字符串输入预留空间,就用字符串数组,否则会出现未定义的情况(内存访问错误等);
如果字符串保持不变,用指向const指针更有效率。
事实上,字符串的大部分操作需要用到指针,比如拷贝字符串,对于字符串数组,除了用到函数外,不然就要一项项进行拷贝,指针则可以直接赋值,让两者指向同一个位置,效率更高。
二、字符串输入/输出函数
1.字符串输入要做的第一件事就是分配空间,最简单的方法就是在声明时显式子指明数组的大小。
为什么不推荐用指针,指针如果没初始化,会指向任何地方,这是非常严重的错误。
2.为字符串分配好内存后,便可以通过读取字符串的函数来读取字符串:
①gets()函数/puts()函数------------------不推荐
gets()函数读取整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符,并在字符末尾添加上空字符,最终读取字符串到指定数组中;
不足:
无法检查数组是否装得下输入行,如果过长会导致缓冲区溢出(会访问未分配内存,导致擦除其他数据等问题),这带来了安全问题,现在几乎不再使用。
puts()函数:
参数:字符串的地址传入,该函数在显示字符串时会自动在其末尾添加一个换行符(gets会舍弃换行符),函数遇到空字符会停止(如果没有空字符,会一直乱输入存储中的数据,直到遇到空字符,不要这样做)
②fgets()函数/fputs()函数
fgets()和fputs()用于处理文件的输入输出——函数有三个参数,第一个参数是字符串要存储的数组,第二个参数是指明了读入字符的最大数量(包括空字符)或者遇到读到的第一个换行符结束,第三个参数指明要读入的文件,如果输入从键盘输入的数据,则以stdin作为参数。
与gets()不同点:gets()会舍弃换行符,fgets()会保留换行符。
fgets()函数返回char的指针,如果一切顺利,函数返回与传入的第一个参数相同(字符串第一个字符的地址),如果到文件结尾,返回空指针,在代码中,可以用0或者NULL代替。
fputs()函数
函数有两个参数,第一个参数是你要输出字符串的数组名,第二个参数指明要写入的文件,如果在计算器显示器上显示,stdout作为参数。该函数不同于puts()函数,不会在末尾加换行符。
这一对函数优缺点:
优点:处理过长输入几乎没问题,可以根据是否有换行符来判断是否读取了一行。
第一次读入9个字符后,会继续循环读入,循环输出。
系统使用缓冲IO,这意味着用户在按下enter键之前,所有的输入被存储在缓冲区中,按下enter键在输入中加入了一个换行符,并把整行输入发送给fgets()函数,因此可以根据是否有换行符来判断是否读取了一行。
坏处:不想要换行符怎么办?
① 在以存储的字符串里查找,将其替换为空字符;
②仍有字符串在缓冲区怎么办? 丢弃掉
③综合起来,碰到换行就变为空字符,并把换行符后的字符全丢掉
③scanf()函数/printf()函数
scanf()函数从第一个非空白字符读取,直到遇到空白字符(空行、空格、换行、制表)停止,更像是读取一个单词的函数,可以指定字段宽度来限制读取的字符个数(防止溢出),scanf()函数会返回独到的项数(1个字符串或者2个字符串,而不是字符的数量),读到文件结尾会返回EOF.
printf()函数以字符串地址作为参数,不会自动在每个字符串末尾加换行符需要手动加,优点在于很方便打印多个字符串。
综上所述:
对于输入:输入过长,使用fgets()函数,并且对于超出的字符,可选择不同的处理方式;
如果目标存储区装得下输入行,gets()和fgets()均可;
如果输入包含工具名,库存量,单价等混合数据,scanf()最方便(通过字宽限制溢出)。
对于输出:puts()自动加上换行符;
fputs()不会加换行符;
printf() 对于多个字符串输出最方便。 printf("%s,%s,%s.",name,name2,name3)
三、自定义输入/输出函数
不一定非要使用C库中的标准函数,可以自己定义输入输出函数。
s_gets()函数——读取整行输入并用空字符代替换行符,或者读取一部分输入,丢弃其他部分
测试条件:
while(string[i] !=0)
putchar(string[i++]);
还可以这样写 while(*string)
putchar(string[i++]);
参数 const char * string 和const char string[]等价,但要处理字符串,实际参数可以为数组名、字符串常量、指针等,声明为第一种情况可以提醒用户,实际参数不一定是数组。
四、字符串函数
均要使用string.h头文件
①strlen() ——输出字符串长度(不包括空字符);
②stract(arr,arr2)——第二个字符接到第一个字符后边,返回第一个参数;
缺点:无法检查第一个字符是否能容纳下添加进来新字符的长度,可能会出现问题。
③strnact(arr,arr2,n)——添加到第n个字符或空字符结束(n不包括空字符,一定要为空字符空出位置)
④strcmp(arr1,arr2)会依次比较每个字符,直到发现第一对不同的字符为止,然后返回相应的值。如果两个字符串相同,返回0,如果arr1的字符ascii码在arr2字符的后边,返回正数,反之,返回负数。(大写字母在小写字母前边)
注意:strcmp比较的是字符串,不要往里传字符。
⑤strncmp(arr1,arr2,n)限定函数只查找前n个字符的比较情况。
⑥strcpy(arr1,arr2)——把arr2数组中的字符串拷贝到arr1中,必须要保证1中有足够的空间。
该函数返回的是第一个参数的值(一个字符的地址),并且第一个参数不必指向数组的开始。
⑦strncmp(arr1,arr2,n)——第三个参数指明拷贝的最大字符数(不包括空字符,提前预留)
⑧sprintf(arr," ")——该函数是把输入写入数组,而不是打印到屏幕上,后边参数同printf(),该函数同样可以把多元素多格式组合成一个字符串。
五、字符串示例:字符串排序
处理一个字母表排序字符串问题
#include<stdio.h>
#include<string.h>
char* s_gets(char* p, int n);
void str_sort(char* p[], int n);
int main(void)
{
char input[20][81];
char* pt[20];
int i = 0;
int k = 0;
puts("Input up to 20;ines,and i will sort them.");
puts("To stop,press the Enter key at a line's start.");
while (i < 20 && s_gets(input[i], 81)!= NULL && input[i][0] != '\0')
{
pt[i] = input[i];
i++;
}
str_sort(pt, i);
puts("Here is the sorted list:");
for (k = 0; k < i; k++)
puts(pt[k]);
return 0;
}
void str_sort(char* p[], int n)
{
char* temp;
int i = 0;
int j = 0;
for (i = 0; i < n-1; i++)
{
for (j = i+1; j < n; j++)
{
if (strcmp(p[i], p[j]) > 0)
{
temp = p[i];
p[i] = p[j];
p[j] = p[i];
}
}
}
}
char* s_gets(char* p, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(p , n, stdin);
if(ret_val )
{
while(p[i] != '\n' && p[i] != '\0')
i++;
if (p[i] == '\n')
p[i] = '\0';
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
分析: 第一步,存储输入,并用指针指向每一行输入;需要定义一个二维数组,一个指针数组,什么情况下赋值?(小于20,并且读入不为空,首字符不为空)
第二步:数组比较——调用函数——冒泡排序(i从0开始,j从i+1开始比较,交换)
第三步:输出排序好的数组——循环(用指针输出,因为是指针位置变化了,数组原内容不变)——s_gets()函数——fgets()进行输入,保证顺利进行,if判断,同时需要一个指针指向fgets(),while循环到换行符或零字符进行不同情况判断。
为什么选择排序指针?——方便,只需要指针进行赋值,不然数组赋值还需要用到strcpy函数。
六、ctype.h字符函数和字符串
第七章介绍的ctype.h系列函数,这些函数不能处理字符串,但是可以处理字符串中的字符。
以下程序分别把字符串转换为大写,统计标点符号个数。
直接判断字符更简单;字符判断除了s_get()
也可以通过strchr寻找'\n',如果地址不是空,将其替换为’\0‘;和s_get()区别在于后边多余的字符s_get处理了,寻找这个没处理(在缓冲区,会影响下一次输入),因为本程序只有一句输入,所以不需要处理。
七、命令行参数
C编译器允许main没有参数或者有两个参数,当有两个参数时——命令行里,第一个参数为命令行中的字符串数量(包括程序名字,如上边的test_11),第二个参数是每个字符串地址存在第二个参数数组里,程序名一般在argv[0]内。
在命令行中,程序参数就直接在名字后边写,用空格隔开。
在许多环境里,允许用双引号括起来形成一个参数。
八、字符串转换为数字
在VS2022环境里,我们可以通过printf()/fprintf()/scanf()实现字符串和数字数值的转换,对于命令行中,如何将字符串改为数字呢?——atoi()函数
atoi()函数介绍:
()里输入字符串(数字字符串/开头是数字的字符串),返回里边的数字,如果参数不是数字,函数立马返回0.
还有其他函数: atol();atof();
除此以外,在除过命令行系统中还有更智能的函数——可以识别和报告字符串中的首字符是否是数字;
strtol()函数——str->long,可以指定数字的进制;
strtoul()函数——str->unsigned long,可以指定数字的进制;
strtod()函数——str->double.
函数原型:long strtol(const char * restrict nptr,char ** restrict endptr,int base);
nptr是指带转换字符串的指针,endptr是一个指针地址,被设置为表示输入数字结束字符的地址,base表示以什么进制写入数字(最多到36进制)。