第8章 字符串

1. 字符串的深入理解

1.1 字符串的基本概念

在 C 语言中,字符串是由零个或多个字符组成的序列,以空字符 '\0' 结尾。空字符是一个值为 0 的字符,用于标记字符串的结束。

1.1.1 字符串的内存表示

字符串在内存中是连续存储的字符序列,最后跟着一个空字符。

1
2
3
+---+---+---+---+---+---+
| H | e | l | l | o | \0|
+---+---+---+---+---+---+

对于字符串字面量,编译器会在内存的只读数据段中为其分配空间。

1
2
3
char *str = "Hello";
// "Hello" 存储在只读数据段
// str 存储在栈中,指向 "Hello" 的首地址

对于字符数组,编译器会在栈或堆中为其分配空间(取决于数组的作用域)。

1
2
3
char str[] = "Hello";
// 整个数组存储在栈中
// 包含 'H', 'e', 'l', 'l', 'o', '\0'

1.2 字符串的表示

1.2.1 字符数组

字符数组是最常用的字符串表示方式,可以修改其中的字符。

1
2
3
4
5
6
7
8
// 声明字符数组
char str[10]; // 未初始化,内容不确定

// 初始化字符数组
char str1[] = "Hello"; // 自动计算大小为 6(包含空字符)
char str2[10] = "World"; // 显式指定大小
char str3[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式添加空字符
char str4[] = {'H', 'e', 'l', 'l', 'o'}; // 没有空字符,不是字符串

1.2.2 字符串字面量

字符串字面量是用双引号括起来的字符序列,存储在只读内存中,不能修改。

1
2
3
4
5
6
"Hello, World!"  // 普通字符串字面量
"This is a\nmultiline\nstring" // 包含转义字符的字符串字面量
"String with\tescape\tcharacters" // 包含制表符的字符串字面量

// 字符串字面量的连接
"Hello, " "World!" // 等同于 "Hello, World!"

1.2.3 指针和字符串

可以使用指针来指向字符串,这种方式更加灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指向字符串字面量的指针
const char *str1 = "Hello";

// 指向字符数组的指针
char str2[] = "World";
char *str3 = str2;

// 动态分配的字符串
char *str4 = (char *)malloc(10 * sizeof(char));
if (str4 != NULL)
{
strcpy(str4, "Hello");
free(str4);
}

1.3 字符串的特性

  • 以空字符结尾 - 字符串必须以 '\0' 结尾,否则不是有效的 C 字符串
  • 连续存储 - 字符串的字符在内存中连续存储
  • 长度可变 - 字符串的长度由空字符的位置决定
  • 字符编码 - 字符串中的字符使用 ASCII 或其他编码(如 UTF-8)

2. 字符串库函数详解

C 语言标准库 <string.h> 提供了丰富的字符串处理函数。

2.1 字符串长度函数

2.1.1 strlen 函数

1
size_t strlen(const char *s);
  • 功能:计算字符串的长度,不包括空字符
  • 参数s - 指向字符串的指针
  • 返回值:字符串的长度
  • 注意:如果 s 不是以空字符结尾,会导致未定义行为
1
2
char str[] = "Hello";
size_t len = strlen(str); // len = 5

2.1.2 strnlen 函数

1
size_t strnlen(const char *s, size_t maxlen);
  • 功能:计算字符串的长度,但最多检查 maxlen 个字符
  • 参数s - 指向字符串的指针,maxlen - 最大检查长度
  • 返回值:字符串的长度或 maxlen(取较小值)
  • 注意:C99 引入,用于防止对非空终止字符串的无限循环
1
2
char str[] = "Hello";
size_t len = strnlen(str, 10); // len = 5

2.2 字符串复制函数

2.2.1 strcpy 函数

1
char *strcpy(char *dest, const char *src);
  • 功能:将 src 指向的字符串复制到 dest 指向的缓冲区
  • 参数dest - 目标缓冲区,src - 源字符串
  • 返回值:指向 dest 的指针
  • 注意
    • dest 必须有足够的空间容纳 src 字符串
    • 不检查缓冲区大小,可能导致缓冲区溢出
    • src 必须以空字符结尾
1
2
3
char dest[10];
char src[] = "Hello";
strcpy(dest, src); // dest 现在包含 "Hello"

2.2.2 strncpy 函数

1
char *strncpy(char *dest, const char *src, size_t n);
  • 功能:将 src 指向的字符串的前 n 个字符复制到 dest 指向的缓冲区
  • 参数dest - 目标缓冲区,src - 源字符串,n - 要复制的最大字符数
  • 返回值:指向 dest 的指针
  • 注意
    • 如果 src 的长度小于 n,会用空字符填充到 n 个字符
    • 如果 src 的长度大于或等于 n,不会在 dest 末尾添加空字符
1
2
3
char dest[10];
char src[] = "Hello";
strncpy(dest, src, 10); // dest 现在包含 "Hello" 后跟 5 个空字符

2.3 字符串拼接函数

2.3.1 strcat 函数

1
char *strcat(char *dest, const char *src);
  • 功能:将 src 指向的字符串拼接到 dest 指向的字符串末尾
  • 参数dest - 目标字符串,src - 要拼接的字符串
  • 返回值:指向 dest 的指针
  • 注意
    • dest 必须有足够的空间容纳拼接后的字符串
    • destsrc 都必须以空字符结尾
1
2
3
char dest[20] = "Hello, ";
char src[] = "World!";
strcat(dest, src); // dest 现在是 "Hello, World!"

2.3.2 strncat 函数

1
char *strncat(char *dest, const char *src, size_t n);
  • 功能:将 src 指向的字符串的前 n 个字符拼接到 dest 指向的字符串末尾
  • 参数dest - 目标字符串,src - 要拼接的字符串,n - 要拼接的最大字符数
  • 返回值:指向 dest 的指针
  • 注意
    • 总是在拼接后的字符串末尾添加空字符
    • dest 必须以空字符结尾
1
2
3
char dest[20] = "Hello, ";
char src[] = "World!";
strncat(dest, src, 3); // dest 现在是 "Hello, Wor"

2.4 字符串比较函数

2.4.1 strcmp 函数

1
int strcmp(const char *s1, const char *s2);
  • 功能:比较两个字符串
  • 参数s1 - 第一个字符串,s2 - 第二个字符串
  • 返回值
    • 小于 0:s1 小于 s2
    • 等于 0:s1 等于 s2
    • 大于 0:s1 大于 s2
  • 注意:比较是基于字符的 ASCII 值
1
2
3
char str1[] = "apple";
char str2[] = "banana";
int result = strcmp(str1, str2); // result < 0

2.4.2 strncmp 函数

1
int strncmp(const char *s1, const char *s2, size_t n);
  • 功能:比较两个字符串的前 n 个字符
  • 参数s1 - 第一个字符串,s2 - 第二个字符串,n - 要比较的最大字符数
  • 返回值:与 strcmp 相同
1
2
3
char str1[] = "apple";
char str2[] = "applesauce";
int result = strncmp(str1, str2, 5); // result = 0

2.5 字符串查找函数

2.5.1 strchr 函数

1
char *strchr(const char *s, int c);
  • 功能:在字符串中查找字符 c 第一次出现的位置
  • 参数s - 要搜索的字符串,c - 要查找的字符
  • 返回值:指向找到的字符的指针,如果未找到则返回 NULL
1
2
char str[] = "Hello, World!";
char *ptr = strchr(str, 'W'); // 指向 'W' 的指针

2.5.2 strrchr 函数

1
char *strrchr(const char *s, int c);
  • 功能:在字符串中查找字符 c 最后一次出现的位置
  • 参数s - 要搜索的字符串,c - 要查找的字符
  • 返回值:指向找到的字符的指针,如果未找到则返回 NULL
1
2
char str[] = "Hello, World!";
char *ptr = strrchr(str, 'o'); // 指向最后一个 'o' 的指针

2.5.3 strstr 函数

1
char *strstr(const char *haystack, const char *needle);
  • 功能:在字符串 haystack 中查找子字符串 needle 第一次出现的位置
  • 参数haystack - 要搜索的字符串,needle - 要查找的子字符串
  • 返回值:指向找到的子字符串的指针,如果未找到则返回 NULL
1
2
char str[] = "Hello, World!";
char *ptr = strstr(str, "World"); // 指向 "World" 的指针

2.6 字符串分割函数

2.6.1 strtok 函数

1
char *strtok(char *str, const char *delimiters);
  • 功能:分割字符串为标记
  • 参数str - 要分割的字符串(第一次调用)或 NULL(后续调用),delimiters - 分隔符字符串
  • 返回值:指向下一个标记的指针,如果没有更多标记则返回 NULL
  • 注意
    • 第一次调用时,str 指向要分割的字符串
    • 后续调用时,str 应为 NULL
    • 会修改原始字符串,将分隔符替换为 NULL
1
2
3
4
5
6
7
char str[] = "apple,banana,orange";
char *token = strtok(str, ",");
while (token != NULL)
{
printf("%s\n", token);
token = strtok(NULL, ",");
}

2.7 内存操作函数

2.7.1 memset 函数

1
void *memset(void *s, int c, size_t n);
  • 功能:将内存块的前 n 个字节设置为值 c
  • 参数s - 指向内存块的指针,c - 要设置的值,n - 要设置的字节数
  • 返回值:指向 s 的指针
1
2
char str[10];
memset(str, '0', 10); // 将 str 的前 10 个字节设置为 '0'

2.7.2 memcpy 函数

1
void *memcpy(void *dest, const void *src, size_t n);
  • 功能:将 src 指向的内存块的前 n 个字节复制到 dest 指向的内存块
  • 参数dest - 目标内存块,src - 源内存块,n - 要复制的字节数
  • 返回值:指向 dest 的指针
  • 注意destsrc 不能重叠
1
2
3
char src[] = "Hello";
char dest[10];
memcpy(dest, src, 6); // 复制包括空字符在内的 6 个字节

2.7.3 memmove 函数

1
void *memmove(void *dest, const void *src, size_t n);
  • 功能:与 memcpy 类似,但可以处理 destsrc 重叠的情况
  • 参数:与 memcpy 相同
  • 返回值:指向 dest 的指针
1
2
char str[] = "Hello, World!";
memmove(str + 7, str + 0, 5); // 重叠复制,结果为 "Hello, Hello!"

2.7.4 memcmp 函数

1
int memcmp(const void *s1, const void *s2, size_t n);
  • 功能:比较两个内存块的前 n 个字节
  • 参数s1 - 第一个内存块,s2 - 第二个内存块,n - 要比较的字节数
  • 返回值
    • 小于 0:s1 小于 s2
    • 等于 0:s1 等于 s2
    • 大于 0:s1 大于 s2
1
2
3
char str1[] = "Hello";
char str2[] = "Hello";
int result = memcmp(str1, str2, 5); // result = 0

2.8 安全的字符串函数

C11 标准引入了一些安全的字符串函数,它们在 <string.h> 中声明。

2.8.1 strcpy_s 函数

1
errno_t strcpy_s(char *dest, rsize_t destsz, const char *src);
  • 功能:安全地复制字符串
  • 参数dest - 目标缓冲区,destsz - 目标缓冲区的大小,src - 源字符串
  • 返回值:成功返回 0,失败返回错误码
  • 注意:如果 destsz 不够大,会调用约束处理函数
1
2
char dest[10];
strcpy_s(dest, sizeof(dest), "Hello");

2.8.2 strcat_s 函数

1
errno_t strcat_s(char *dest, rsize_t destsz, const char *src);
  • 功能:安全地拼接字符串
  • 参数dest - 目标字符串,destsz - 目标缓冲区的大小,src - 要拼接的字符串
  • 返回值:成功返回 0,失败返回错误码
1
2
char dest[20] = "Hello, ";
strcat_s(dest, sizeof(dest), "World!");

2.8.3 strnlen_s 函数

1
size_t strnlen_s(const char *s, rsize_t maxlen);
  • 功能:安全地计算字符串长度
  • 参数s - 要计算长度的字符串,maxlen - 最大检查长度
  • 返回值:字符串的长度或 maxlen(取较小值)
1
2
char str[] = "Hello";
size_t len = strnlen_s(str, 10);

2.8.4 strtok_s 函数

1
char *strtok_s(char *str, const char *delimiters, char **context);
  • 功能:安全地分割字符串
  • 参数str - 要分割的字符串(第一次调用)或 NULL(后续调用),delimiters - 分隔符字符串,context - 用于存储分割状态的指针
  • 返回值:指向下一个标记的指针,如果没有更多标记则返回 NULL
1
2
3
4
5
6
7
8
char str[] = "apple,banana,orange";
char *context;
char *token = strtok_s(str, ",", &context);
while (token != NULL)
{
printf("%s\n", token);
token = strtok_s(NULL, ",", &context);
}

3. 字符串操作的高级技巧

3.1 字符串反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <string.h>

void reverse_string(char *str)
{
if (str == NULL)
{
return;
}

int length = strlen(str);
char *start = str;
char *end = str + length - 1;

while (start < end)
{
// 交换字符
char temp = *start;
*start = *end;
*end = temp;

// 移动指针
start++;
end--;
}
}

int main(void)
{
char str[] = "Hello, World!";
printf("原字符串:%s\n", str);
reverse_string(str);
printf("反转后:%s\n", str);
return 0;
}

3.2 字符串大小写转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <ctype.h>

void to_upper(char *str)
{
if (str == NULL)
{
return;
}

while (*str != '\0')
{
*str = toupper((unsigned char)*str);
str++;
}
}

void to_lower(char *str)
{
if (str == NULL)
{
return;
}

while (*str != '\0')
{
*str = tolower((unsigned char)*str);
str++;
}
}

int main(void)
{
char str1[] = "Hello, World!";
char str2[] = "HELLO, WORLD!";

to_upper(str1);
printf("转换为大写:%s\n", str1);

to_lower(str2);
printf("转换为小写:%s\n", str2);

return 0;
}

3.3 字符串去空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <ctype.h>

char *trim_whitespace(char *str)
{
if (str == NULL)
{
return NULL;
}

// 去除前导空格
while (isspace((unsigned char)*str))
{
str++;
}

if (*str == '\0')
{
return str;
}

// 去除尾随空格
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end))
{
end--;
}

// 添加结束符
*(end + 1) = '\0';

return str;
}

int main(void)
{
char str[] = " Hello, World! ";
printf("原字符串:'%s'\n", str);
printf("去空格后:'%s'\n", trim_whitespace(str));
return 0;
}

3.4 字符串替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char *replace_string(const char *src, const char *old_str, const char *new_str)
{
if (src == NULL || old_str == NULL || new_str == NULL)
{
return NULL;
}

size_t src_len = strlen(src);
size_t old_len = strlen(old_str);
size_t new_len = strlen(new_str);

if (old_len == 0)
{
return strdup(src);
}

// 计算需要替换的次数
int count = 0;
const char *p = src;
while ((p = strstr(p, old_str)) != NULL)
{
count++;
p += old_len;
}

// 计算新字符串的长度
size_t new_str_len = src_len + count * (new_len - old_len);

// 分配内存
char *result = (char *)malloc(new_str_len + 1);
if (result == NULL)
{
return NULL;
}

// 执行替换
char *q = result;
p = src;
while ((p = strstr(p, old_str)) != NULL)
{
// 复制旧字符串之前的部分
size_t len = p - src;
memcpy(q, src, len);
q += len;

// 复制新字符串
memcpy(q, new_str, new_len);
q += new_len;

// 跳过旧字符串
src = p + old_len;
p = src;
}

// 复制剩余部分
strcpy(q, src);

return result;
}

int main(void)
{
const char *src = "Hello, World! World is beautiful.";
const char *old_str = "World";
const char *new_str = "C Language";

char *result = replace_string(src, old_str, new_str);
if (result != NULL)
{
printf("原字符串:%s\n", src);
printf("替换后:%s\n", result);
free(result);
}

return 0;
}

4. 字符串的内存管理

4.1 字符串的内存分配

4.1.1 栈上分配

1
char str[100];  // 在栈上分配 100 字节的空间
  • 优点:分配和释放速度快
  • 缺点:空间大小固定,可能不够用

4.1.2 堆上分配

1
2
3
4
5
6
char *str = (char *)malloc(100 * sizeof(char));  // 在堆上分配 100 字节的空间
if (str != NULL)
{
// 使用字符串
free(str); // 释放内存
}
  • 优点:空间大小可以动态调整
  • 缺点:需要手动管理内存,容易导致内存泄漏

4.1.3 静态存储区分配

1
static char str[100];  // 在静态存储区分配 100 字节的空间
  • 优点:生命周期长,不需要手动释放
  • 缺点:空间大小固定,可能导致内存浪费

4.2 字符串的内存泄漏

字符串操作中常见的内存泄漏情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 错误:内存泄漏
char *get_string(void)
{
char *str = (char *)malloc(100 * sizeof(char));
if (str != NULL)
{
strcpy(str, "Hello");
}
return str; // 调用者可能忘记释放
}

// 正确的做法
void use_string(void)
{
char *str = get_string();
if (str != NULL)
{
printf("%s\n", str);
free(str); // 手动释放
}
}

4.3 字符串的缓冲区溢出

字符串操作中常见的缓冲区溢出情况:

1
2
3
4
5
6
7
8
// 错误:缓冲区溢出
char str[10];
strcpy(str, "Hello, World!"); // 字符串长度超过缓冲区大小

// 正确的做法
char str[10];
strncpy(str, "Hello, World!", sizeof(str) - 1);
str[sizeof(str) - 1] = '\0'; // 确保字符串以空字符结尾

5. 字符串的编码和国际化

5.1 ASCII 编码

ASCII(American Standard Code for Information Interchange)是最基本的字符编码,使用 7 位表示 128 个字符。

5.2 扩展 ASCII 编码

扩展 ASCII 使用 8 位表示 256 个字符,包含了一些特殊符号和非英语字符。

5.3 Unicode 编码

Unicode 是一种国际标准编码,旨在包含世界上所有的字符。

5.3.1 UTF-8 编码

UTF-8 是一种变长编码,使用 1-4 个字节表示一个字符:

  • ASCII 字符使用 1 个字节
  • 欧洲和中东字符使用 2 个字节
  • 东亚字符使用 3 个字节
  • 其他字符使用 4 个字节

5.3.2 UTF-16 编码

UTF-16 使用 2 或 4 个字节表示一个字符。

5.3.3 UTF-32 编码

UTF-32 使用 4 个字节表示一个字符。

5.4 多字节字符串

C 语言提供了 <wchar.h> 头文件,用于处理宽字符和多字节字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main(void)
{
setlocale(LC_ALL, ""); // 设置本地化环境

wchar_t wstr[] = L"你好,世界!";
wprintf(L"%ls\n", wstr);

return 0;
}

6. 字符串处理的性能优化

6.1 避免不必要的字符串复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 低效:不必要的字符串复制
char *process_string(const char *str)
{
char *copy = strdup(str);
// 处理 copy
free(copy);
return strdup(str); // 再次复制
}

// 高效:直接使用原字符串
const char *process_string(const char *str)
{
// 处理 str(不修改)
return str;
}

6.2 使用适当的字符串函数

1
2
3
4
5
6
7
8
9
// 低效:使用 strcat 拼接多个字符串
char str[100] = "";
strcat(str, "Hello");
strcat(str, ", ");
strcat(str, "World!");

// 高效:使用 sprintf 或 snprintf
char str[100];
snprintf(str, sizeof(str), "%s%s%s", "Hello", ", ", "World!");

6.3 预分配足够的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 低效:频繁重新分配内存
char *build_string(void)
{
char *str = NULL;
size_t len = 0;

for (int i = 0; i < 100; i++)
{
char temp[10];
sprintf(temp, "%d", i);
size_t temp_len = strlen(temp);
char *new_str = (char *)realloc(str, len + temp_len + 1);
if (new_str == NULL)
{
free(str);
return NULL;
}
str = new_str;
strcat(str, temp);
len += temp_len;
}

return str;
}

// 高效:预分配足够的内存
char *build_string(void)
{
char *str = (char *)malloc(1000 * sizeof(char)); // 预分配足够的内存
if (str == NULL)
{
return NULL;
}

str[0] = '\0';
for (int i = 0; i < 100; i++)
{
sprintf(str + strlen(str), "%d", i);
}

return str;
}

6.4 使用指针操作替代数组下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 低效:使用数组下标
int count_vowels(const char *str)
{
int count = 0;
for (size_t i = 0; i < strlen(str); i++)
{
char c = str[i];
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
{
count++;
}
}
return count;
}

// 高效:使用指针操作
int count_vowels(const char *str)
{
int count = 0;
while (*str != '\0')
{
char c = *str;
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
{
count++;
}
str++;
}
return count;
}

7. 常见的字符串相关错误和调试

7.1 常见错误

7.1.1 缓冲区溢出

1
2
3
// 错误:缓冲区溢出
char str[10];
strcpy(str, "Hello, World!"); // 字符串长度超过缓冲区大小

7.1.2 空指针解引用

1
2
3
// 错误:空指针解引用
char *str = NULL;
strlen(str); // 未定义行为

7.1.3 字符串没有以空字符结尾

1
2
3
// 错误:字符串没有以空字符结尾
char str[5] = {'H', 'e', 'l', 'l', 'o'};
strlen(str); // 未定义行为,会一直搜索直到找到空字符

7.1.4 修改字符串字面量

1
2
3
// 错误:修改字符串字面量
char *str = "Hello";
str[0] = 'h'; // 未定义行为,字符串字面量存储在只读内存中

7.1.5 内存泄漏

1
2
3
4
5
6
7
// 错误:内存泄漏
char *get_string(void)
{
char *str = (char *)malloc(100 * sizeof(char));
strcpy(str, "Hello");
return str; // 调用者可能忘记释放
}

7.2 调试技巧

7.2.1 使用断言

1
2
3
4
5
6
7
8
9
10
11
#include <assert.h>

char *safe_strcpy(char *dest, size_t dest_size, const char *src)
{
assert(dest != NULL);
assert(src != NULL);
assert(dest_size > 0);

// 函数实现
// ...
}

7.2.2 使用调试宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define DEBUG 1

#if DEBUG
#define LOG_STRING(str) printf("%s: %s\n", #str, str)
#else
#define LOG_STRING(str) do { } while (0)
#endif

int main(void)
{
char str[] = "Hello";
LOG_STRING(str); // 在调试模式下输出字符串
return 0;
}

7.2.3 使用内存调试工具

  • Valgrind - 用于检测内存泄漏和内存错误
  • AddressSanitizer - 用于检测内存错误
  • LeakSanitizer - 用于检测内存泄漏

8. 字符串的高级应用示例

8.1 示例 1:简单的字符串解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 解析键值对
void parse_key_value(const char *str)
{
char *copy = strdup(str);
if (copy == NULL)
{
return;
}

char *token = strtok(copy, ",");
while (token != NULL)
{
char *key = token;
char *value = strchr(token, '=');
if (value != NULL)
{
*value = '\0';
value++;
printf("键: %s, 值: %s\n", key, value);
}
token = strtok(NULL, ",");
}

free(copy);
}

int main(void)
{
const char *str = "name=John,age=30,city=New York";
parse_key_value(str);
return 0;
}

8.2 示例 2:简单的字符串加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>

// 简单的 Caesar 密码加密
void caesar_encrypt(char *str, int shift)
{
if (str == NULL)
{
return;
}

while (*str != '\0')
{
if (isalpha((unsigned char)*str))
{
char base = islower((unsigned char)*str) ? 'a' : 'A';
*str = (char)(((*str - base + shift) % 26 + 26) % 26 + base);
}
str++;
}
}

// 简单的 Caesar 密码解密
void caesar_decrypt(char *str, int shift)
{
caesar_encrypt(str, 26 - shift);
}

int main(void)
{
char str[] = "Hello, World!";
int shift = 3;

printf("原字符串:%s\n", str);
caesar_encrypt(str, shift);
printf("加密后:%s\n", str);
caesar_decrypt(str, shift);
printf("解密后:%s\n", str);

return 0;
}

8.3 示例 3:简单的 JSON 解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 简单的 JSON 解析器(仅支持基本的键值对)
void parse_json(const char *str)
{
// 跳过空白字符
while (isspace((unsigned char)*str))
{
str++;
}

// 检查是否是对象
if (*str != '{')
{
printf("不是有效的 JSON 对象\n");
return;
}
str++;

// 解析键值对
while (*str != '}')
{
// 跳过空白字符
while (isspace((unsigned char)*str))
{
str++;
}

// 解析键
if (*str != '"')
{
printf("无效的键\n");
return;
}
str++;

const char *key_start = str;
while (*str != '"')
{
str++;
}
size_t key_len = str - key_start;
char *key = (char *)malloc(key_len + 1);
if (key == NULL)
{
return;
}
strncpy(key, key_start, key_len);
key[key_len] = '\0';
str++;

// 跳过冒号和空白字符
while (isspace((unsigned char)*str))
{
str++;
}
if (*str != ':')
{
printf("缺少冒号\n");
free(key);
return;
}
str++;
while (isspace((unsigned char)*str))
{
str++;
}

// 解析值
const char *value_start = str;
if (*str == '"')
{
// 字符串值
str++;
while (*str != '"')
{
str++;
}
}
else if (isdigit((unsigned char)*str) || *str == '-')
{
// 数字值
while (isdigit((unsigned char)*str) || *str == '.' || *str == 'e' || *str == 'E' || *str == '+' || *str == '-')
{
str++;
}
}
else if (*str == 't' || *str == 'f' || *str == 'n')
{
// 布尔值或 null
if (strncmp(str, "true", 4) == 0)
{
str += 4;
}
else if (strncmp(str, "false", 5) == 0)
{
str += 5;
}
else if (strncmp(str, "null", 4) == 0)
{
str += 4;
}
}

size_t value_len = str - value_start;
char *value = (char *)malloc(value_len + 1);
if (value == NULL)
{
free(key);
return;
}
strncpy(value, value_start, value_len);
value[value_len] = '\0';

// 打印键值对
printf("键: %s, 值: %s\n", key, value);

// 释放内存
free(key);
free(value);

// 跳过逗号和空白字符
while (isspace((unsigned char)*str))
{
str++;
}
if (*str == ',')
{
str++;
}
while (isspace((unsigned char)*str))
{
str++;
}
}
}

int main(void)
{
const char *json = "{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}";
parse_json(json);
return 0;
}

9. 小结

本章深入介绍了 C 语言中字符串的概念、表示方法、操作技巧以及高级应用。字符串是 C 语言中非常重要的数据类型,在实际编程中经常使用。

9.1 关键知识点

  • 字符串的本质:以空字符 '\0' 结尾的字符序列
  • 字符串的表示:字符数组、字符串字面量、指针
  • 字符串库函数strlenstrcpystrcatstrcmpstrchrstrstr
  • 安全的字符串函数strcpy_sstrcat_sstrnlen_sstrtok_s
  • 字符串的内存管理:栈分配、堆分配、静态存储区分配
  • 字符串的编码:ASCII、UTF-8、UTF-16、UTF-32
  • 字符串的性能优化:避免不必要的复制、使用适当的函数、预分配内存、使用指针操作
  • 常见错误:缓冲区溢出、空指针解引用、字符串没有以空字符结尾、修改字符串字面量、内存泄漏

9.2 学习建议

  • 多写代码:通过实际编程练习掌握字符串的使用
  • 理解原理:理解字符串的内存表示和库函数的实现原理
  • 注意安全:始终使用安全的字符串函数,避免缓冲区溢出等问题
  • 学习调试:掌握使用调试工具检测和修复字符串相关错误的技巧
  • 关注性能:了解字符串操作的性能特性,编写高效的字符串处理代码

字符串处理是 C 语言编程的基础,掌握好字符串处理技巧对于编写高质量的 C 程序至关重要。通过本章的学习,希望读者能够深入理解字符串的概念,掌握字符串库函数的使用,以及编写安全、高效的字符串处理代码。

在后续章节中,我们将学习结构体和其他复合数据类型,以及文件输入/输出等内容。