【C语言基础】12 字符串

1、字符串与字面串

字面串:

   用一对双引号括起来的字符序列(程序文本),是源文件的组成部分,仅表示字面意思。如“Hello”(占6个字符,默认含0),与字符串常量不同,字面串是可更改的。

字符串:

   字面串经编译后生成字符串,位于系统存储器内、以空字符结尾的字符序列。
 如char word[] = {'H', 'e', 'l', 'l', 'o', '!', '\0'}; (是字符数组,也是字符串)

字符数组:

   如char word[] = {'H', 'e', 'l', 'l', 'o', '!'}; (是字符数组,但不是字符串)


说明:

  • 字面串中的空格(如"Good morning!")不是空字符,空格的ASCII码是32,空字符的ASCII码是0。

1.1 字面串中的转义序列

字面串中可以使用转义序列,如\n等。
“Hello, \nWorld!”

  • **/八进制数转义序列:
    • 在3个数字或非八进制数字符处结束。例如:"\1234"包含两个字符(\1234),"\189"包含三个字符(\189)。
  • 十六进制数转义序列:
  • 结束于第一个非16进制数字符处。范围通常在\x0~\xff之间。

见本章的转义序列部分


1.2 延续字符串

字符串太长,在下一行延续。

1.2.1 使用字符'\'

次行需顶格写,否则Tab符会被输出。

	printf("Amateurs sit and wait for inspiration, \
the rest of us just get up and go to work. -Stephen King");

1.2.2 分割成多条字符串

编译器会将相邻字符串自动相连,无需顶格写。
*820

	printf("Amateurs sit and wait for inspiration, "
	47"the rest of us just get up and go to work. -Stephen King");

1.3 字符串存储

字符串以字符数组形式存储。编译器会分配 n + 1 的内存空间来存储长度为 n 的字符串。以空字符结尾,标志字符串的结束。空字符所有位均为0,以转义序列\0表示。

说明:

  • 以整数0结尾的一串字符。
  • 不可混淆空字符和零字符。\0'0'等同(整数),但与'0'不同(字符)。
  • 0标志字符串结束,但不属于字符串的一部分。计算字符串长度时不包含它。
  • 字符串以数组形式存在,以数组或指针的形式访问(遍历)。
  • 两个相邻的字符串会被自动连接,成为一个大字符串,不再间隔0。
  • 不能用运算符对字符串作运算
  • string.h提供多种字符串处理函数。
  • char*不一定指向字符串,可能指向单个字符或字符数组,当所指的字符数组结尾有整数0时,才是指向字符串。

1.4 字符串的操作

允许任何可使用 char * 指针的地方使用字符串。

1.4.1 指针指向字符串

char *p;
p = "abc";   //定义一个指针,指向abc。并非创建了一个字符串“abc”

1.4.2 对字符串取下标

C语言允许对指针取下标,也可对字符串取下标。长度为n的字符串,下标范围0~n+1。

char ch1,ch2;
ch1 = "abc"[1];   // ch1 = b
ch2 = "abc"[3];   // ch2 = 0(空字符)

应用举例:

//将0~15的十进制数转换成相应的16进制字符
char digit_to_hex_digit(int digit)
{
	return "0123456789ABCDEF"[digit];
}

1.4.3 常见错误

字符串常量不可被更改,否则将导致未定义行为,引起程序崩溃。
例如:

char *p = "abc";
*p = 'd';          //错误

字面串可以被更改:

char a[] = "hello";
a[1] = 'a';                  //合法
printf("a[] = %s\n", a);     //输出hallo

1.5 字符串与字符常量

  包含单个字符的字符串与字符常量不同。

  • 字符串“a”用指针表示,指向紧跟空字符的字符"a"的内存单元;
  • 字符'a'由整数(ASCII码)表示

2、字符串变量

以空字符结尾的任何一维数组都可以存储字符串。

确定字符串长度的最好方法是搜索空字符。

定义一个存储80个字符的字符串:

#define STR_LEN 80       //不定义81是为了强调字符串最大长度
...
char str[STR_LEN + 1];   //预留空字符的位置
  • 字符串长度: 取决于空字符位置(空字符紧随字符串结尾,不一定在字符数组末尾)长度:0~STR_LEN
  • 字符数组长度: 定义的数组长度,长度为STR_LEN + 1。

2.1 初始化字符串变量

char word[] = "Hello";      //自动计算并分配长度,长度可变
char date[8] = "June 16";   //7+1, 空格不是空字符
char date1[8] = {'J','u','n','e',' ','1','6','\0'};   //与上一个等同
char date2[10] = "June 16"; //初始化器未填满

存储演示:
请添加图片描述

  • 字符串数组未填满时,编译器会自动用空字符补满,与数组初始化器一致(补0)。
  • C语言允许初始化器(不含\0)与数组长度一样,但不被识别为字符串。
char date3[7] = "June 16";  //未预留空字符位

以上只是定义了一个字符数组,不是字符串。


2.2 字符数组与字符指针

char date[] = "June 14";
char *date = "June 14";

以上两声明中的date均可作字符串,但不能互换。

差别:

  • 声明为数组时,字符串内容可修改;声明为指针时,只读,不可修改,原因是声明了一个指向字面串"June 14"的指针,而字面串不可修改,此处与变量的指针不同。
  • 声明为数组时,date是数组名;声明为指针时,date是变量(指针变量),可指向其他字符串。

举例:
例1:char* s = "Hello, world!";

  • s指向字符串常量。只读,内容不可修改,等同于const char* s。此字符串存储于程序的代码段。

例2:char word[] = "Hello world!";

  • 字符串数组,内容可修改。存储于数据段

想要构造一个字符串——使用数组(可修改)
想要处理一个字符串——使用指针(只读,不可修改)

  • char *str = "Hello"; —— 指针str指向内容为Hello的字符数组
  • char word[] = "Hello"; —— 定义字符数组,数组内容为Hello
  • char line[10] = "Hello"; —— 定义长度为10的字符数组line,内部存储了6个字符(包含标识的0)。

3、字符串的读和写

3.1 写字符串

3.1.1 使用printf函数

使用转换说明%s%.ps显示前面的 p 个字符,%ms在长度为m的栏内显示字符串,右对齐,字符串长度超过m时,不会截断,而是完整显示。printf函数写字符串时逐个输出,直到遇到空字符为止。

举例:

char str[] = "Well done!";
printf("%s\n", str);                //完整显示
printf("%12s\n", str);              //在12格内显示,右对齐
printf("%5s\n", str);               //在5格内显示,右对齐
printf("%.7s\n", str);              //显示前7位
printf("%12.4s\n", str);            //在12格内显示前4位,右对齐
printf("%-12.4s\n", str);           //在12格内显示前4位,左对齐

输出结果:
请添加图片描述
注意:

  • 字符串输出格式提供的是存储该字符串数组的首地址,因此 printf 函数的格式串后面的表达式处是 str 而非 *str 。这与单个元素的输出不同,是字符串输出的特殊之处。

3.1.2 使用puts函数

char str[] = "Well done!";
puts(str);             //puts输出结束会自动换行

输出结果:
请添加图片描述

3.1.3 单字符输出——putchar函数

函数原型: int putchar(int c);

  • 函数参数为int类型,而非 char 类型;
  • 输出一个字符(C语言中char占1个字节);
  • 返回值也是int型,表示已写的字符数,EOF(-1)表示写失败。

3.2 读取字符串

3.2.1 用scanf函数

  使用转换说明%s来读取字符串,不需要取地址符&,字符串数组名本身被视为指针。
  scanf 函数读取字符时,寻找字符串起始位置会跳过空白字符,开始读取之后会在遇到非读取类型的数据时停止,存储字符串时会在末尾自动添加一个空字符。

  • 为保证定义的字符串数组不被读入的数据填满,可使用%ns来限制读取字符的个数。

char string[8];
scanf("%s", string);—— 数组名作为指针,没有取地址符&,scanf读入一个单词(到空格、tab或回车为止) 最多允许读入7个字符(结尾需要空出0的位置)
scanf("%7s", string); —— 最多读取7个字符,


3.2.2 用gets函数

scanfgets函数的区别:

  • gets 函数不会在开始读字符串之前跳过空白字符(scanf 会跳过);
  • gets 会持续读取,直到遇到换行符时停止(scanf 函数遇到任意空白字符时停止)
  • gets 不会将换行符存储下来(scanf 会存储,且用空字符代替)

举例:

char sentence[LEN + 1];
printf("Enter a sentence: ");

scanf("%s", sentence);     //输入"To C, or not to C: that is the question."
//gets(sentence);

printf("%s\n", sentence);

输出结果:

  • 使用 scanf 函数:
      遇到空白字符时停止,剩余内容留待下次调用时读取。
    请添加图片描述

  • 使用 gets 函数:
      遇到换行符才停止。
    请添加图片描述

3.2.3 逐个字符读取字符串——getchar函数

函数原型: int getchar(void);

  • 参数为空;
  • 返回值为int类型,返回EOF(-1)表示输入结束:
    • Windows系统是输入 Ctrl + Z 时返回EOF
    • Unix系统是 Ctrl + D 返回EOF
  • 从标准输入读入一个字符;

逐个读取 —— read_line函数

考虑的问题:

  • 开始存储字符串之前,是否跳过空白字符;
  • 何时停止读取:换行符、任意空白字符等,是否需要存储最后的这类字符?
  • 字符串超出数组长度怎么办:忽略还是留待下次调用时读取?

举例:

//  逐个读取字符到str[]中,限制读取个数为n

int read_line(char str[], int n)
{
	int ch, i = 0;
	
	while( (ch = getchar() != '\n') )    //停止读取的条件
		if( i<n ) str[i++] = ch;         //限制存储的字符个数
	str[i] = '\0';                       //结尾存储空字符
	
	return i;                            //返回str中的字符数量
}

3.2.4 常见错误

错误1:指针变量未初始化就修改其指向的数据

char* string;
scanf("%s", string);        //打算把输入的数据存储到string指向的内存区域
string[0] = 'a';            //打算在 *string 的 0 号位写入字符a

以上代码没有对指针进行初始化,char*不一定指向字符串,可能引起未知区域的数据被修改,是非常严重的错误。

错误2:

  • char buffer[100] = “”;
    空字符串,buffer[0] == ‘\0’
  • char buffer[] = “”;
    此字符串长度为1。

4、访问字符串中的字符

  字符串以数组形式存储,可以使用数组下标访问,更方便的是使用指针访问。

  • 字符串作为函数形式参数,可以不必要求字符串长度,通过寻找空字符('\0')即可作为遍历结束条件。

4.1 使用数组下标访问

int count_spaces(const char s[])    //将s声明为数组,const保护所存储的字符串不被修改
{
	int i, count = 0;
	for(i = 0; s[] != '\0'; i++)
		if(s[] == ' ') count++;
	return count;
}

4.2 使用指针访问

int count_spaces(const char *s)    //将s声明为指针,const保护指向的字符串不被修改
{
	int i, count = 0;
	for( ; *s != '\0'; s++)        //s作为指针副本被修改了,但*s还是const型
		if(*s == ' ') count++;
	return count;
  • s可定义为数组,也可定义为指针,二者功能上没有差别,编译器会把数组型形式参数视为指针。

5、字符串库 <string.h>

  运算符无法对字符串进行操作,使用字符串操作函数。将字符串视为数组进行处理。

char str1[11], str2[11];
str = "abc";      //字符串赋值,非法操作
str2 = str1;      //字符串直接复制,非法操作

char str3[11] = "abc";   //合法操作,初始化不是赋值

if(str1 == str2) ...     //合法操作,但比较的是地址,不是字符串

5.0 参考资源

各库函数用法说明
库函数源码下载

5.1 strcpy函数(复制)

5.1.1 strcpy函数

(1) 函数原型
  •    char *strcpy(char *s1, const char *s2);
  • C99: char *strcpy(char *restrict s1, const char *restrict s2);
    • 关键字 restrict 保证两段字符串存储位置不能有重叠,提高系统效率和安全性。
(2) 功能

把 s2 指向的字符串复制给 str1 指向的字符串,并返回复制后的指针 s1 。

(3) 说明
  • 函数返回类型为指针类型,返回值为 s1 (复制的目标对象的指针);
  • s2 是 const 类型,起保护作用;
  • 函数遇到被复制字符串的空字符(\0)时终止。空字符也会被复制
(4) 函数复现
char *mycpy(char *destination, const char *source){
	char ret = destination;                //备份目标地址
	
	while( *source != '\0' )
		*destination++ = *source++;        //逐个复制
	
	*destination = '\0';
	
	return destination;
}

(4) 调用举例
str = "abcd";            //错误用法

strcpy(str1, "abcd");     //合法,给str2赋值
strcpy(str2, str1);       //合法,将str1复制到str2

strcpy( str2, strcpy(str1, "abcd") );   //合法,将str1和str2均赋值为"abcd"

(5) 注意事项

需要保证 str1 (复制目标对象) 的长度不小于 str2 (被复制对象) 的长度。否则函数将越过 str1 定义的数组边界继续复制,直到遇到空字符为止。

char str1[3];
strcpy(str1, "abc");     //错误,str1最多只能容纳长度为2的字符串

5.1.2 strncpy函数

(1) 函数原型

char *strncpy(char *s2, const char *s1, size_t n );
C99: char *strncpy(char *restrict s2, const char *restrict s1, size_t n );

n = sizeof(s2)

(2) 函数功能

限制复制的字符个数,保证str2能容纳复制的字符,不超出 str2 的边界。

(3) 注意

此函数不会复制 str1 最后的空字符


(4) 优化后代码
strncpy( str2, str1, sizeof(str2)-1 );     //预留最后一位
str2[ sizeof(str2)-1 ] = '\0';             //保证最后一位(第n-1位)存储空字符

5.2 strlen函数(求长度)

(1) 函数原型

size_t strlen( const char *s );

(2) 功能

返回 s 指向的字符串的长度。

(3) 说明

  • 返回类型为 size_t 类型( typedef,C语言中的无符号整型),返回字符串的长度;
  • 字符串长度计数不包含空字符;
  • 不同于其他几个函数,此函数返回的不是指针,而是整型值。

(4) 调用举例

int len;

len  = strlen("abc");                 //len = 3
len = strlen(" ");                    //len = 1

strcpy(str1, "abc");
len = strlen(str1);                    //len = 3, 测量的不是数组长度,而是字符串长度
len = strlen( strcpy(str2, "abcd") );  //len = 4

5.3 strcat函数(拼接)

5.3.1 strcat函数

strcat —— string catenation(字符串连接)

(1) 函数原型

   char *strcat(char *str1, const char *str2);
C99: char *strcat(char* restrict s1, const char* restrict s2);

(2) 功能

把字符串 str2 指向的内容,拼接到 str1 的末尾,并返回拼接后的指针 str1 。

(3) 说明
  • 待补充。。。
(4) 函数复现
int* mycat(char* s1, const char* s2)
{
	int i = 0;
	while( s2[i] != '\0' ){
		s1[strlen[s1] + i] = s2[i];
	}
	s1[strlem[s1]+i] = '\0';
	return s1;
}
(5) 调用举例
strcpy(str1, "abc");    //str1赋值
strcpy(str2, "xyz");    //str2赋值
strcat(str1, "def");    //拼接后 str1 == abcdef
strcat(str1, str2);     //拼接后 str1 == abcdefxyz
(5) 注意

与strcpy相同,需要保证 str1 的长度能容纳拼接后的字符串。

char str1[6] = "abc";
strcat(str1, "def");     //程序出错,str1最多只能容纳5个字符

5.3.2 strncat函数

(1) 函数原型

   char *strncat(char *s1, const char *s2,size_t n );
C99: char *strncat(char *restrict s1, const char *restrict s2,size_t n );

n = sizeof(s1) - strlen(s1) - 1

(2) 功能

把字符串 str2 指向的内容,拼接到 str1 的末尾,并返回拼接后的指针 str1 。

(3) 说明
  • 第3个参数是计算 str1 的剩余可用长度,并预留空字符位置。( sizeof计算str1总长度,strlen计算str1已存储的字符串长度,-1预留空字符位置 );
  • 执行速度会比 strcat 更慢一些,但更安全。

5.4 strcmp函数(比较)

5.4.1 strcmp函数

(1) 函数原型

int strcmp( const char *s1, const char *s2 );
C99: int strcmp( const char *restrict s1, const char *restrict s2 );

(2) 功能

函数比较 s1 和 s2 指向的字符串的内容,并依据比较结果返回 —— 小于等于大于 0的值。

(3) 说明
  • 判断标准:依据字符的数值码(ASCII码)

  • 判断流程: 从第一个字符开始逐个比较相应位置的字符的ASCII码。

  • ASCII码的常用性质:

    • A ~ Z、a ~ z、0 ~ 9 的数值码是连续的;
    • 数字 < 大写字母 < 小写字母 。数字(48 ~ 57)、大写字母(65 ~ 90)、小写字母(97 ~ 122);
    • 空格符 ' ' ( ASCII = 32 ) 小于所有打印字符;
    • 空字符'\0'( ASCII = 0 ) 。
比较结果返回值举例
s1 < s2<0“abc” < “bcd”
“abc” < “abcd”
s1 == s20“abc” == “abc”
s1 > s2>0“abc” > “ab”
“abc” > “aba”
(4) 函数复现
int mycmp(const char* s1, const char* s2){
	while( *s1==*s2 && *s1 != '\0' ){
		s1++;
		s2++;
	}
	return *s1 - *s2;     //返回差值
}
(5) 注意
  • 依编译器的不同,返回值不一定是-1 、0 、1。例如strcmp("Abc", "abc")的返回值也可能是 -32 ,A与a的ASCII码恰好相差32。
  • 数组不能直接比较,数组比较的结果永远是False。例如:
char s1[] = "abc";
char s2[] = "abc";
printf("表达式 s1==s2 的值为 %d\n", s1==s2);

程序输出结果为:表达式 s1==s2 的值为 0
因为 s1 和 s2 是指针,比较的是地址,而不是其指向的数组内容。

5.4.2 strncmp函数

(1)函数原型

C99: int strcmp(const char *s1, const char *s2, size_t n);

(2)功能

比较字符串中的前 n 个字符

5.5 字符串搜索

5.5.1 strchr函数(单字符正向检索)

(1) 函数原型

char *strchr(const char *s, int c);

(2) 功能

从字符串左侧开始,逐一搜索找到第一次出现字符 ‘c’ 的位置(指针)。未找到则返回空指针NULL。

(3) 调用举例
  • 找字符的第1个位置
char s[] = "hello";
char *p = strchr(s, 'l');     // p = &s[2]

printf("%s\n", p);            //输出llo
  • 找字符的第2个位置
char s[] = "hello";
char *p = strchr(s, 'l');
p = strchr( p+1, 'l' );       //跳过第一个l, p = &s[3]
  • 将从目标字符起的字符串后半部分复制到其他字符串
char s[] = "hello";
char *p = strchr(s, 'l');
char *t = (char*)malloc( strlen(p)+1 );
strcpy(t, p);
printf("%s\n", t);            //输出llo
free(t);
  • 将从目标字符的字符串前半部分复制到其他字符串
char s[] = "hello";
char *p = strchr(s, 'l');
char c = *p;                  //p位置处的数据备份
*p = '\0';                    //将s在p的位置处填入空字符,将s截断
char *t = (char*)malloc( strlen(s)+1 );
strcpy(t, s);
*p = c;                       //还原字符串s
printf("%s\n", t);            //输出he
free(t);
(4) strrchr函数(单字符反向检索)

从字符串右侧开始,逐一搜索找到第一次出现字符 ‘c’ 的位置(指针)。未找到则返回空指针NULL。

5.5.2 strstr函数(字符串检索)

(1) 函数原型

char *strstr(const char *s1, const char *s2);

(2) 功能

在字符串 s1 中检索字符串 s2 的位置,返回 s1 中指向 s2 第一次出现的指针,区分大小写。未找到则返回空指针NULL。

(3) strcasestr函数

忽略大小写的字符串检索函数。


5.6 综合举例

5.5.1 按日期排序显示一个月的日程

【C补充】【字符串】按日期排序显示一个月的日程


6、字符串数组

6.1 常规字符串数组

与一般二维数组相同,指明列数,忽略行数。

char planet[][8] = { "Mercury", "Venus", "Earth", "Mars", 
				 	 "Jupiter", "Saturn", "Uranus", "Naptune" };

缺点:由于各行字符串长度不一,并非许多行不会被填满,造成空间浪费,对于大型数组尤其如此。

6.2 非整齐的字符串数组

方法:建立特殊的一维数组,数组元素不直接存储字符串,而是存储指向字符串的指针。

char *planet[] = { "Mercury", "Venus", "Earth", "Mars", 
				   "Jupiter", "Saturn", "Uranus", "Naptune" };

planets 内的每一个元素均指向以空字符结尾的字符串的指针。

搜索特定的字符串:

for(i=0; i<9; i++)
	if( planet[i][0] == 'M' )            //寻找以M开头的字符串
		printf("%s\n", planet[i]);       //看下方说明

说明:
  最后的 printf 函数中,使用 planet[i],而非 *planet[i] 是因为输出的是字符串,字符串以数组形式存储,需提供的是字符串数组的的首地址,详见本篇3.1.1节。*planet[0] 本身存储的是单个字符 ‘f’ ,无法按字符串的格式输出。


6.3 命令行参数

(1) 概念

为了能让操作系统使用命令行访问程序信息,必须将 main 函数定义为包含两个参数的函数,分别为 argc 和 argv。

int main( int argc, char *argv[] ){
	...
}

argc —— 参数计数,存储命令行参数的数量。
argv —— 参数向量,指向命令行参数的指针数组。命令行参数以字符串形式存储,因此是 char* 类型。

argv[0]指向程序名,argv[1] ~ argv[argc-1] 指向余下的命令行参数,argv[argc]是空指针,空指针不指向任何地方。

在UNIX系统中,用户输入命令行ls -l remind.c

请添加图片描述

逐行显示命令行参数:

//方法1:
int i;
for(i=1; i<argc; i++){
	printf("%s\n", argv[i]);
}

//方法2:
char **p;
for(p=&argv[1]; *p != NULL; p++ )
	printf("%s\n", *p);

(2) 综合应用:核对输入的命令行中是否有行星的名字

用户输入: planet Jupiter venus Earth fred
程序输出: 命令行中是否有行星名字,若有说明行星序号。

由命令行规则,命令行中的 planet 为程序名。

int main(int argc, char* argv[])
{
	char *planet[] = { "Mercury", "Venus", "Earth", "Mars", 
					   "Jupiter", "Saturn", "Uranus", "Naptune" };
	int i, j;
	
	for(i=1; i<argc; i++){              //检查命令行
		for(j=0; j<8; j++){             //检查字符串
			if(argv[i] == planet[j]){
				printf("%s is planet %d\n", argv[i], j+1);
				break;                  //核对成功,跳出内层循环,检查下一个命令行参数
			}
			if(j == 8)
				printf("%s id not a planet.\n", argv[i]);
		}
	}
	
	return 0;
}