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"
包含两个字符(\123
和4
),"\189"
包含三个字符(\1
,8
和9
)。
- 在3个数字或非八进制数字符处结束。例如:
- 十六进制数转义序列:
- 结束于第一个非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";
—— 定义字符数组,数组内容为Hellochar 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函数
scanf
与gets
函数的区别:
- 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 == s2 | 0 | “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 按日期排序显示一个月的日程
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;
}