文章目录
助记提要
- 字面串中转义序列的注意;
- 字面串跨行的两种方式;
- 字面串、字符串、字符常量的区别;
- 字符串变量的初始化;
- 声明字符数组和声明字符指针的区别;
- puts和printf写字符串的特点;
- scanf和gets读字符串的特点;
- 字符串库函数strcpy、strcat、strlen、strcmp的使用;
- 编写strlen函数并改进;
- 如何存储字符串数组;
- 如何访问程序的命令行参数;
13章 字符串
13.1 字面串
字面串是用双引号括起来的字符序列。
字面串中使用转义序列
字面串仲可以包含转义序列。
注意 使用八进制和十六进制的转义序列时,要把转义序列和普通字符区分开。
八进制的转义序列在3个数字后结束,或者在第一个非八进制数的字符处结束。如"\1234"
表示\123
和4
两个字符,而\189
表示\1
、8
、9
三个字符。
十六进制的转义序列不限于3个数字,到第一个非十六进制数的字符处结束。大部分编译器会把十六进制转义序列的范围限制在\x0
~\xff
。
字面串跨行延续
两种方式。
- 使用
\
结尾,下一行延续字面串;
下一行的字面串必须从行的起点继续,会破坏程序的缩进结构。 - 多段字面串之间进使用空白字符分割时,编译器会将它们合并为一条字面串。
字面串存储
字面串是当做字符数组存储的。
编译器在遇到字面串时,会分配一个长度为n+1的内存空间。多一个位置用来存储标志字符串结尾的空字符\0
。
空字符是所有位都为0的字节。字符常量'0'
的ASCII编码值是48。
字面串的操作
字面串是做为字符数组存储的,编译器将其看做char *
类型的指针。
允许使用char *
类型指针的地方都可以使用字面串。
char *p;
// p指向字符a的位置
p = "abc";
字面串也可以取下标。
char ch;
// 字面串取下标
ch = "abc"[1];
// 把数字转为16进制字符形式
char digit_to_hex_char(int digit)
{
return "0123456789ABCDEF"[digit];
}
字面串虽然被当做数组表示,但是字面串不能改变。而字符数组是可以改变的。
char *p = "abc";
printf("%c\n", *p); // *p 为字符a
printf("%s\n", p); // p为整个字面串
// 错误!!!
*p = 'd';
字面串、字符串、字符常量
字面串属于源文件的一部分,是一串使用引号围起来的文本。
字面串经过编译后生成字符串,字符串是指位于系统存储器里面以空字符终止的字符序列。
只包含一个字符的字面串不同于字符常量。
字面串"a"
是用指针表示的,而字符常量'a'
是用整数表示的。
13.2 字符串变量
C语言规定每个字符串都必须以空字符结尾。
只要保证字符串是以空字符结尾的,任何一维字符数组都能用来存储字符串。
声明长度为n+1的字符数组并不是说它存储的字符串长度是n。字符串的长度取决于空字符的位置。
初始化字符串变量
// 数组形式初始化
char carr[5] = {'a', 'b', 'c', 'd', '\0'};
// 使用字面串初始化
char carr[5] = "abcd";
初始化器不能填满字符串变量时,编译器会在额外的空间填充空字符。
初始化器的长度刚好等于数组长度时,可以编译成功,但是编译器不会存储空字符,导致数组无法做为字符串使用。
初始化器超过数组的长度是非法的。有时会截取前面的一部分字符存到数组中,结尾没有空字符,也不能做为字符串使用。
声明字符串时可以省略长度。编译器会根据初始化器自动确定长度(n+1)。
char carr[] = "abcd";
不指定长度,不代表长度可变。编译结束后,数组的长度就是固定的了。
字符数组和字符指针
比较两种声明:
char carr[] = "abcd";
char *carr = "abcd";
第一个是字面量声明的数组,第二个是指向字面量的指针。由于数组和指针之间的关系,这两个声明都可以当做字符串使用。
未初始化的指针变量不能做为字符串。
它们的不同之处有两点:
- 声明为数组时,可以修改其中的元素。声明为指针时,指向字面量,不可以修改字面量。
- 声明为数组时,
carr
是数组名,不能作为左值。声明为指针时,carr
是指针变量,可作为左值,所以可以指向其它的字符串。
char carr[5] = "abcd";
// 数组可修改元素
carr[2] = 'x';
char *carr = "abcd";
// 不可修改指向的字面量包含的元素
// carr[2] = 'x';
// 指针变量可以指向别的字面量
carr = "efgh";
13.3 读写字符串
puts函数写字符串
puts函数可以直接写指定字符串,写完后自动添加一个换行符:
char str[] = "Have fun!";
puts(str);
printf写字符串
printf
需要使用转换说明符%s
写字符串:
printf("%s\n", str);
printf
函数会逐个写字符串中的字符,直到遇到空字符为止。
如果没有空字符,printf
会越过字符串结尾继续写,直到遇到空字符为止。
格式%m.ps
可以指定显示的字符数量为p个,显示长度为m个字符长度。
// 显示前5个字符
printf("%.5s\n", str);
// 前5个字符在大小为10的栏内右对齐显示
printf("%10.5s\n", str);
// 左对齐
printf("%-10.5s\n", str);
如果字符串长度超过m,不会截断,而是显示整个字符串。
scanf读字符串
scanf
函数可以使用转换说明符%s
把字符串读入字符数组:
scanf("%s", str);
str
前不需要加&
号,因为数组名会被当做指针。
scanf
函数会跳过空白字符,然后读入字符并存到数组中,直到遇到空白字符为止。因此scanf
函数读入的字符串不包括空字符。
scanf
会自动在字符串末尾存储一个空字符。
输入字符串可能会比用来存储它的字符串变量长。
scanf
函数无法检测数组何时被填满,存储时可能会越过数组边界。
可以使用%ns
来指定可存储的最多字符数为n个。
gets函数
gets函数在历史上一直用于读入整行输入,但是由于安全问题(无法检测数组边界),在C11被移除。
gets函数不会跳过空白字符。
gets函数会持续读入直到遇到换行符为止。
gets会用空字符代替换行符。
逐个字符读字符串
通过逐个字符读入,能更大程度地控制读入操作。
自己编写的输入函数:
不跳过空白符,在第一个换行符处停止读取,忽略额外输入的字符。
// 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;
}
getchar
读取的字符以int
类型返回,因此ch
的类型定义为int
。
13.4 访问字符串中的字符
既可以通过数组下标自增访问,也可以通过指针访问。
统计字符串中空格数的函数:
// const表示该函数不会改变数组
int count_spaces(const char s[])
{
int count = 0, i;
for (i = 0; s[i] != '\0'; i++)
if (s[i] == ' ')
count++;
return count;
}
字符串可以通过空字符来确定其长度。如果s不是字符串,函数就需要第二个参数指明数组长度。
使用指针实现上述函数:
int count_spaces(const char *s)
{
int count = 0;
for (; *s != '\0'; s++)
if (*s == ' ')
count++;
return count;
}
const表示不能修改s指向的数组。而传给函数的形参是指针变量,可以修改,不影响实际参数的指针。
13.5 C语言的字符串库
<string.h>函数库
C语言的运算符无法操作字符串。
字符串不可以通过赋值运算符复制到字符数组。
char str1[10], str2[10];
// str1 = "abc";
// str1 = str2;
可以使用关系运算符或判等运算符,但是比较的不是字符串,而是指针值,因此不会产生预期效果。
if (str1 == str2) ...
<string.h>
头文件中定义了操作字符串的函数。
其中的函数的形式参数声明为char *
类型,对应的实际参数可以是字符数组、char *
类型的变量或字面串。
在函数声明中没有带const
的字符串形参,调用函数时可能会发生改变。
strcpy复制字符串
// strcpy的原型
char *strcpy(char *s1, const char *s2);
函数strcpy
把s2复制给s1。
// 字面串复制给str2
strcpy(str2, "abcd");
// str2复制给str1
strcpy(str1, str2);
// 字面串同时复制给str1和str2
strcpy(str1, strcpy(str2, "abcd"));
注意 strcpy
函数无法检查str2
指向的字符串大小是否适合str1
指向的数组。strcpy
会一直复制到第一个空字符为止,可能会越过str1
的边界。
strncpy
速度慢些,但是更安全。它有第三个参数用于限制复制的字符数。
// str2中最多复制str1长度的字符
// 如果str2过长,str1会没有终止的空字符
strncpy(str1, str2, sizeof(str1));
// 更安全的做法
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1)-1] = '\0';
strlen求字符串长度
// strlen的原型
size_t strlen(const char *s);
用于测量字符串的长度,而不是数组的总长度。
strcat字符串拼接
// strcmp的原型
char *strcmp(char *s1, const char *s2);
把字符串s2的内容追加到s1末尾,返回s1的指针。
// 把内容追加到字符串1
strcat(str1, "abc");
strcat(str1, str2);
// 使用返回值连续拼接
strcpy(str1, "abc");
strcpy(str2, "def")
strcat(str1, strcat(str2, "ghi"));
注意 如果str1的数组不足够容纳str2指向的字符串中的字符,会一直复制到第一个空字符为止,可能会越过str1
的边界。
strncat
函数速度慢些,但更安全。第三个参数为待复制的字符数。
// 注意第三个参数的计算
strncat(str1, str2, sizeof(str1) - strlen(str1) - 1)
strcmp字符串比较
// strcmp原型
int strcmp(const char *s1, const char *s2);
s1小于s2则返回小于0的值,等于则返回0,大于则返回大于0的值。
适用于关系运算符和判等运算符。
比较时使用字典顺序进行比较。如果前i位字符相同,s1的第i+1位字符在字符集中的数值小于s2的第i+1位字符,则s1小于s2。
前面字符都一致的两个字符串,短的字符串较小。
13.6 字符串惯用法
有时需要重写标准库提供的函数功能。
自己重写这些功能时,需要使用别的名字,即使不包含该函数所属的头。
搜索字符串的结尾
strlen
函数初步实现
size_t strlen(const char *s)
{
size_t n;
// 遍历到结束,统计字符数
for (n = 0; *s != '\0'; s++)
n++;
return n;
}
*s != '\0'
与*s != 0
一样,因为空字符的整数值就是0。而条件测试*s != 0
与直接测试*s
是一样的,在*s
不为0时才为真。
自增操作s++
可以写到前面的条件测试表达式里,效果一样。
改进版1:
size_t strlen(const char *s)
{
size_t n = 0;
for (; *s++;)
n++;
return n;
}
这个for语句可以使用while语句代替。
空字符的地址减去第一个字符的地址即为字符串的长度。这样用一次计算代替每次循环中n的自增操作,可以加快运行速度。
改进版2:
size_t strlen(const char *s)
{
const char *p = s;
while (*s)
s++;
return s - p;
}
注意 指针变量p需要使用const
声明,因为把s赋值给p会给s指向的字符串带来风险,编译器会警告。
注意 不可用while (*s++)
,这样会多加一次。
拼接字符串
strcat
初步实现
char *strcat(char *s1, const char *s2)
{
char *p = s1;
// 确定s1末尾空字符位置
while (*p != '\0')
p++;
// 把s2复制到p指向的位置
while (*s2 != '\0'){
*p = *s2;
p++;
s2++;
}
*p = '\0';
return s1;
}
改进版:
char *strcat(char *s1, const char *s2)
{
char *p = s1;
while (*p)
p++;
while (*p++ = *s2++)
;
return s1;
}
第二个循环测试的是赋值表达式的值。复制空字符后,赋值表达式值为假,循环结束。所以不需要另外加空字符。
13.7 字符串数组
字符串数组的存储
使用二维的字符数组,可以存储字符串数组:
char weekday[][4] = {"Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"};
这种存储方式会浪费很多空间。因为二维数组的列数由存储的最长字符串而定,其它字符串行占不满的空间使用空字符填充。
为避免浪费空间,可以创建一个指向字符串的指针数组:
char *weekday[] = {"Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"};
可以使用weekday[0]
取到第一个字符串。
命令行参数
为了可以访问命令行参数,必须把main函数定义为含有两个参数的函数:
int main(int argc, char *argv[])
{ ... }
argc
是命令行参数的数量,包括程序本身。
argv
是指向命令行参数的指针数组,命令行参数以字符串的形式存储。其中argv[0]
表示程序名,argv[1]
到argv[argc-1]
指向剩下的命令行参数。
argv[argc]
是一个空指针。
访问命令行参数的方式
for (int i = 1; i < argc; i++)
printf("%s\n", argv[i]);
// 定义指向指针的指针
char **p;
// NULL表示空指针
for (p = &argv[1]; *p != NULL; p++)
printf("%s\n", *p);
更多相关内容参考: 《C语言程序设计:现代方法》笔记