字符串字面量与存储
字符串变量: 字符数组 vs 字符指针
字符串的读写
字符串库函数: strcpy 与 strlen
任务: 自己实现 strlen
更多库函数与综合案例
字符串字面量: 用双引号括起来的字符序列. 程序处理的名字、句子、文件名……都是字符串; 本章讲字符串常量(字面量)与字符串变量.
这一串打印出来是四行诗 (诗只在这里出现一次):
Candy
Is dandy
But liquor
Is quicker.
--Ogden Nash
字符串字面量可以包含转义序列; 转义字符经常出现在 printf/scanf 的格式串里.
最常用的是 \n: 每个 \n 使光标移到下一行.
printf("one\ntwo\n");
输出为两行:
one
two
字符串是字符数组, 用空字符 '\0' 标记结束; C 库提供一组字符串处理函数.
当C编译器在程序中遇到长度为 \(n\) 的字符串字面量时, 它会为该字符串分配 \(n + 1\) 个字节的内存.
这块空间存字符串的字符, 外加一个额外字符: 空字符 标记字符串的结尾.
空字符是所有位都为0的字节, 因此它用转义序列 '\0' 表示.
字符串字面量 "abc" 存储成 4 个字符的数组:
空串 "" 只存一个空字符:
凡是能用 char * 指针的地方, 都能直接用字符串字面量; 字面量也可以像数组一样取下标.
char *p;
p = "abc"; /* p 指向 'a' */
char ch;
ch = "abc"[1]; /* ch = 'b' */
赋值让 p 指向字符串的第一个字符; "abc"[1] 就是字符 'b'.
把 0 ~ 15 的数字转成十六进制字符:
char digit_to_hex_char(int digit)
{
return "0123456789ABCDEF"[digit];
}
这里把字符串字面量当作只读数组: digit 是下标, 返回值是对应字符.
包含单个字符的字符串字面量与字符常量不同.
"a"由指针表示.
'a'由整数表示.
合法调用:
printf("\n");
非法调用:
printf('\n'); /*** 错误的 ***/
#define STR_LEN 80
char str[STR_LEN + 1];
str[0] = 'H';
str[1] = 'i';
str[2] = '\0'; /* 现在 str 是字符串 "Hi" */
任何一维字符数组都可以用来存储字符串, 但必须以空字符 '\0' 结尾.
要存 80 个字符, 数组必须声明为 81: 多出的 1 个字节留给空字符; 先 #define STR_LEN 80 再加 1 是惯用法.
用数组存字符串的难点:
很难判断一个字符数组是否被当作字符串;
字符串处理函数必须小心处理空字符;
确定字符串长度需要搜索空字符.
STR_LEN + 1 的数组可保存长度 0 ~ STR_LEN 的字符串; 实际长度取决于空字符的位置. 不留空字符空间, 程序执行时可能得到不可预知的结果.
声明时可以直接用字符串初始化. 编译器把 "June 14" 当作字符数组初始化式的缩写, 并自动补上空字符.
char date1[8] = "June 14";
char date2[9] = "June 14";
char date3[7] = "June 14";
char date[] = "June 14"; /* 数组 */
char *date = "June 14"; /* 指针 */
两种声明都能当字符串用, 但:
数组: date 是数组名, 内容可以修改, 存放的是自己的一份副本.
指针: date 是变量, 可以改指向; 指向不应修改的字符串字面量.
char *p;
char str[STR_LEN + 1];
p = str; /* 让 p 指向字符数组 */
char *p; 不会为字符串分配空间. 使用 p 作为字符串前, 必须让它指向一个字符数组; 也可以指向以后动态分配的字符串.
char str[] = "Are we having fun yet?";
printf("%.6s\n", str);
puts(str);
要打印字符串的一部分, 可以使用转换说明 %.ps
p 是要打印的字符数量.
printf("%.6s\n", str); 将打印 Are we
puts(str); 打印整个字符串并自动换行 (相当于 printf("%s\n", str)).
转换说明 %ms 将在大小为 m 的字段内打印字符串.
如果字符串少于 m 个字符, 则会在字段内右对齐输出.
如果要强制左对齐, 在 m 前面加一个减号.
m 和 p 可以组合使用:
转换说明 %m.ps 会使字符串的前 p 个字符打印在大小为 m 的字段中.
例如:
printf("%10.6s|\n", str); /* 字段宽 10, 取前 6 个字符, 右对齐 */
printf("%-10.6s|\n", str); /* 左对齐 */
转换说明 %s 允许 scanf 把字符串读入字符数组:
char str[20];
scanf("%s", str); /* str 是数组名, 无需 & */
scanf 先跳过空白字符, 再读入字符, 直到遇到空白;
空格、制表符、换行符都会让它停止;
它在末尾自动存储一个空字符.
因此 scanf 通常读不到整行.
要读取整行, 历史上常用 gets, 它的特点是:
不跳过开头的空白字符, 一直读到换行符才停止;
丢弃换行符, 用空字符代替.
但 gets 有严重缺陷, 下一页细讲; 现在应改用 fgets.
不要使用 gets 读取字符串:
gets 不检查缓冲区大小, 输入过长会溢出 (缓冲区溢出漏洞的经典来源)
C11 标准已把 gets 从语言中移除; OJ 平台上的老题也要改用 fgets
fgets 会连换行符一起读入 (和 gets 不同), 需要时手工去掉末尾的 \n.
去掉换行符的写法 (strlen 声明在 <string.h> 中).
char line[100];
/* Wrong: gets(line); —— 危险且已移除 */
fgets(line, sizeof(line), stdin); /* 正确: 带上限长度 */
size_t l = strlen(line);
if (l > 0 && line[l - 1] == '\n')
line[l - 1] = '\0';
程序员经常自己编写输入函数. 设计时要考虑:
是否在存储前跳过空白字符?
什么字符让函数停止读取 (换行/任意空白/其他)? 这个字符存不存?
输入太长无法存储时, 丢弃多余字符还是留给下一次输入?
假设需要这样一个函数: 不跳过空白, 在第一个换行符处停止 (不存储), 并丢弃多余字符.
函数原型: int read_line(char str[], int n);
若输入行超过 n-1 个字符, 多出的字符被丢弃; 函数返回实际存入的字符数.
int read_line(char str[], int n)
{
int ch, i = 0;
while ((ch = getchar()) != '\n' && ch != EOF) {
if (i < n - 1)
str[i++] = ch;
}
str[i] = '\0';
return i;
}
getchar 每次读入一个字符;
循环在换行符或 EOF 处停止, 换行符不存入数组;
i < n - 1 给空字符留位置, 超出的字符读入后直接丢弃;
循环后补 '\0', 返回实际存储的字符数 (输入过长时返回 n-1).
int count_spaces(const char s[])
{
int count = 0;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == ' ')
count++;
}
return count;
}
int count_spaces(const char *s)
{
int count = 0;
for (; *s != '\0'; s++) {
if (*s == ' ')
count++;
}
return count;
}
由于字符串是以数组的方式存储的, 可以使用下标来访问字符串中的字符.
要处理字符串 s 中的每个字符, 可以设置一个循环来对计数器 i 进行自增并通过表达式 s[i] 选择字符.
计算字符串中空格数的函数:
用指针运算代替下标:
strcpy(str2, "abcd");
/* str2 现在包含 "abcd" */
strcpy(str1, str2);
/* str1 现在包含 "abcd" */
strcpy函数的原型:
char *strcpy(char *s1, const char *s2);
strcpy、strlen、strcat、strcmp 都声明在 <string.h> 中, 使用前要 #include <string.h>.
strcpy将字符串s2复制到字符串s1中.
准确地说, strcpy将 s2指向的字符串 复制到 s1指向的数组中.
strcpy返回s1(指向目标字符串的指针).
调用strcpy, 把字符串"abcd"存储到str2中:
把str2的内容复制到str1:
strlen函数的原型:
size_t strlen(const char *s);
size_t是一个typedef名称, 表示C的一种无符号整型.
strlen返回字符串s的长度, 不包括空字符. 例子:
int len;
len = strlen("abc"); /* len is now 3 */
len = strlen(""); /* len is now 0 */
strcpy(str1, "abc");
len = strlen(str1); /* len is now 3 */
任务: 写 int my_strlen(const char *s), 返回字符串长度 (不含结尾的 '\0'), 分别用下标版和指针版实现
在 main 中验证:
"abc" 的长度 = 3
"" 的长度 = 0
"hello world" 的长度 = 11
提示:
下标版: 从 s[0] 数到 s[i] == '\0' 为止
指针版: *s 非零就计数并 s++, 循环结束时走过的步数就是长度
不要调用 <string.h> 的 strlen, 自己数
#include <stdio.h>
int my_strlen_index(const char *s)
{
int n = 0;
while (s[n] != '\0') /* 下标版 */
n++;
return n;
}
int my_strlen_pointer(const char *s)
{
const char *p = s;
while (*p != '\0') /* 指针版 */
p++;
return (int)(p - s);
}
int main(void)
{
const char *tests[] = {"abc", "", "hello world"};
for (int i = 0; i < 3; i++)
printf("\"%s\" 的长度 = %d / %d\n", tests[i],
my_strlen_index(tests[i]),
my_strlen_pointer(tests[i]));
return 0;
}
字符串以 '\0' 结尾, 长度不包含这个空字符
s[i] 就是 *(s + i): 下标版和指针版本质相同, 只是写法不同
指针版用 p - s 得到走过的元素个数——这就是 11-1 讲的指针相减
参数写 const char * 表明函数不会修改字符串
<string.h> 的 strlen 返回 size_t (无符号); 自己实现返回 int 便于 %d 打印
char str1[20], str2[20];
strcpy(str1, "abc");
strcat(str1, "def");
/* str1 now contains "abcdef" */
strcpy(str1, "abc");
strcpy(str2, "def");
strcat(str1, str2);
/* str1 now contains "abcdef" */
char str1[20], str2[20];
strcpy(str1, "abc");
strcpy(str2, "def");
strcat(str1, strcat(str2, "ghi"));
/* str1 now contains "abcdefghi";
str2 contains "defghi" */
strcat函数的原型:
char *strcat(char *s1, const char *s2);
strcat将字符串s2的内容附加到字符串s1的末尾.
它返回s1(指向结果字符串的指针). strcat示例:
与strcpy一样, strcat返回的值通常会被丢弃.
左侧最后一段代码使用了返回值:
int strcmp(const char *s1, const char *s2);
if (strcmp(str1, str2) < 0) /* is str1 < str2? */
…
if (strcmp(str1, str2) <= 0) /* is str1 <= str2? */
…
strcmp函数的原型:
strcmp比较字符串s1和s2, 根据s1是小于、等于还是大于s2, 返回一个小于、等于或大于0的值.
检查str1是否小于str2:
检查str1是否小于或等于str2 :
通过选择适当的运算符(<、 <=、 >、 >=、 ==、 !=), 可以测试str1和str2之间的任何可能的关系.
输入日期和提醒: 24 妈妈的生日
输入日期和提醒: 5 18:00 与朋友聚餐
输入日期和提醒: 26 看电影
输入日期和提醒: 7 10:30 看牙医
输入日期和提醒: 12 交作业
输入日期和提醒: 5 上课
输入日期和提醒: 12 上课
输入日期和提醒: 0
日期 提醒
5 上课
5 18:00 与朋友聚餐
7 10:30 看牙医
12 上课
12 交作业
24 妈妈的生日
26 看电影
remind.c 维护一个月的每日提醒列表: 用户逐条输入"日期 + 提醒", 输入 0 结束, 程序按日期排序打印全部提醒.
总体策略:
读入一系列日期和提醒;
按日期有序存储;
显示.
日期用 scanf, 整行提醒用 read_line: 排序要比较日期, 读提醒要保留空格.
#define MAX_REMIND 50
#define MSG_LEN 60
char reminders[MAX_REMIND][MSG_LEN + 3];
char day_str[3], msg_str[MSG_LEN + 1];
int day, i, j, num_remind = 0;
for (;;) {
scanf("%2d", &day); /* 读日期 */
if (day == 0) break; /* 0 结束 */
sprintf(day_str, "%2d", day);
read_line(msg_str, MSG_LEN + 1);
for (i = 0; i < num_remind; i++) /* 找插入位置 */
if (strcmp(day_str, reminders[i]) < 0) break;
for (j = num_remind; j > i; j--) /* 从后往前腾位 */
strcpy(reminders[j], reminders[j - 1]);
strcpy(reminders[i], day_str); /* 放入日期 */
strcat(reminders[i], msg_str); /* 拼上提醒 */
num_remind++;
}
下面是 main 的核心代码 (节选), 完整程序见 code/remind.c.
reminders 是二维字符数组, 每一行存"日期 + 提醒".
日期先用 sprintf 格式化成两位字符串, 再与提醒拼接.
插入时从后向前腾出位置, 保持按日期有序.
使用 strcmp、strcpy、strcat, 需要 #include <string.h>.
最后的按顺序打印循环省略了, 见 remind.c:for (i = 0; i < num_remind; i++) printf(" %s\n", reminders[i]);
size_t strlen(const char *s)
{
size_t n = 0;
for (; *s != '\0'; s++) {
n++;
}
return n;
}
size_t strlen(const char *s)
{
size_t n = 0;
for (; *s; s++) {
n++;
}
return n;
}
n的初始化移到它的声明中:
条件*s != '\0'与*s != 0相同, 这又与*s相同.
使用这些观察的strlen版本:
字符串复制的惯用法:
while ((*s1++ = *s2++))
;
先把 *s2 赋给 *s1, 再让两个指针后移; 赋值为空字符时循环结束.
二维字符数组, 每行一个字符串:
char planets[][8] = {"Mercury", "Venus", "Earth",
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"};
行数可以省略, 但必须指定列数.
用指针数组模拟不规则数组:
char *planets[] = {"Mercury", "Venus", "Earth",
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"};
每个元素是一个指针, 指向对应字符串字面量; 各行长度可以不同.
二维数组每行按最长的字符串分配, 大量空字符被浪费; 字符串长短不一时, 用指针数组模拟不规则数组.
1. 概念: "abc" 和 'a' 的区别是什么?"abc" 在内存里占几个字节?
2. 概念: char s[] = "hello"; 与 char *p = "hello"; 有什么区别?
3. 代码阅读: 下面的程序输出什么?
char s1[20] = "abc", s2[] = "def";
strcpy(s1, s2);
strcat(s1, "ghi");
printf("%s %d\n", s1, (int)strlen(s1));
4. 纠错: 下面的代码有两处错误,找出来并改正。
char s[3];
strcpy(s, "abc");
char *p;
strcpy(p, "hi");