C语言字符串完全入门教程
欢迎来到C语言的世界!字符串是编程中最常见也是最重要的数据类型之一。然而,与许多现代高级语言不同,C语言没有内置的、可以直接操作的“字符串”类型。在C语言中,字符串实际上是字符数组,并且遵循一套特定的约定。正是这些特性使得C语言的字符串既强大又灵活,但也伴随着一些需要格外注意的细节和陷阱。
本教程旨在帮助你彻底理解C语言字符串的本质、操作方法以及常见的注意事项,带你从入门到熟练掌握。
我们将涵盖以下内容:
- C语言字符串的本质:字符数组与空终止符
- 字符串的声明与初始化
- 字符串的输入与输出
- C标准库
<string.h>
:字符串常用操作函数详解- 计算长度:
strlen()
- 复制:
strcpy()
和strncpy()
- 连接:
strcat()
和strncat()
- 比较:
strcmp()
和strncmp()
- 查找:
strchr()
,strrchr()
,strstr()
- 计算长度:
- 字符串与指针:深入理解其关系
- 字符串的常见问题与陷阱
- 总结与实践建议
准备好了吗?让我们开始吧!
1. C语言字符串的本质:字符数组与空终止符
在C语言中,字符串被定义为以空字符(Null Character, \0
)结尾的字符序列。
这意味着:
- 字符串不是一个独立的数据类型,它存储在字符数组中。
- 字符串的结束位置由一个特殊的字符
\0
标记。这个\0
字符的ASCII值为0。 - 在处理字符串时,C语言的许多函数都会依赖于
\0
来判断字符串的结束。
举个例子:
字符串 “Hello” 在内存中实际上是这样存储的:
‘H’ | ‘e’ | ‘l’ | ‘l’ | ‘o’ | ‘\0’ |
---|---|---|---|---|---|
这是一个包含6个字符的序列:’H’, ‘e’, ‘l’, ‘l’, ‘o’, 和 ‘\0’。虽然我们通常认为 “Hello” 有5个字符,但在C语言中存储它需要至少6个字节(每个字符占1个字节,包括 \0
)。
核心要点:空终止符 \0
是C语言字符串的灵魂! 没有它,字符数组就只是字符数组,而不是字符串。任何C字符串处理函数在遇到 \0
时都会停止操作。
2. 字符串的声明与初始化
声明一个字符串,本质上就是声明一个字符数组。
方法一:声明一个固定大小的字符数组
c
char str[20]; // 声明一个可容纳最多19个字符(外加一个\0)的数组
这里声明了一个名为 str
的字符数组,它可以存储20个字符。这意味着它可以存储一个长度不超过19的字符串(因为最后一个位置要留给 \0
)。刚声明时,数组中的内容是未定义的(垃圾值),它还不是一个有效的字符串。
方法二:声明并初始化字符数组
你可以使用字符串字面量(string literal)来初始化字符数组。字符串字面量是用双引号 "
括起来的字符序列,例如 "Hello, world!"
。
c
char message[50] = "Hello, C language!"; // 初始化一个字符数组
当你使用字符串字面量初始化字符数组时,C编译器会自动在字面量的末尾添加 \0
。上面的例子中,"Hello, C language!"
包含18个字符,编译器会自动在末尾添加一个 \0
,所以总共是19个字符。数组 message
的大小是50,足够容纳这个字符串。
方法三:让编译器自动确定数组大小
如果你不指定数组大小,编译器会根据初始化字符串字面量的长度(包括 \0
)来确定数组大小。
c
char greeting[] = "Hi"; // 编译器会创建一个大小为 3 的数组 ('H', 'i', '\0')
这种方法非常方便,但需要注意,数组的大小一旦确定就不能改变。
方法四:逐个字符初始化
你也可以像初始化普通数组一样,逐个字符地初始化。但务必手动添加 \0
。
c
char city[] = {'B', 'e', 'i', 'j', 'i', 'n', 'g', '\0'}; // 一个长度为 8 的字符串
如果我们忘了添加 \0
:
c
char not_a_string[] = {'A', 'B', 'C'}; // 这不是一个有效的C字符串!
not_a_string
只是一个包含 ‘A’, ‘B’, ‘C’ 三个字符的数组。如果尝试用 %s
格式符打印它,或者使用 strlen
等函数处理它,程序可能会一直读到内存中某个随机的 \0
才会停止,导致输出乱码甚至程序崩溃。
重要区别:字符数组 vs. 字符串字面量作为指针
除了用字符数组声明字符串外,你还可以使用字符指针指向字符串字面量:
c
char *name = "Alice"; // char pointer pointing to a string literal
这里 name
是一个 char
类型的指针,它指向存储在内存中某个位置的字符串字面量 "Alice"
的第一个字符 ‘A’。
区别:
char array[] = "..."
:array
是一个数组名,它代表了数组的首地址。数组的内容存储在栈上(通常),并且是可修改的。char *pointer = "..."
:pointer
是一个指针变量,它存储了字符串字面量(通常存储在程序的只读数据段)的首地址。这个指针可以指向别处,但它所指向的那个字符串字面量本身是不可修改的。尝试修改*pointer
指向的内容会导致未定义行为(Undefined Behavior),通常表现为程序崩溃(段错误)。
所以,如果你需要一个可以修改内容的字符串,请使用 char array[]
的方式声明。如果你只是想指向一个固定的字符串常量,可以使用 char *pointer = "..."
。
3. 字符串的输入与输出
C语言提供了多种方式来处理字符串的输入和输出。
输出字符串:
printf("%s", str);
:使用%s
格式符打印字符串。printf
会从str
指向的地址开始,一直打印字符直到遇到\0
。puts(str);
:打印字符串,并在末尾自动添加一个换行符\n
。puts
也依赖于\0
来确定字符串的结束。
“`c
include
int main() {
char str[] = “Hello, world!”;
printf(“%s\n”, str); // Output: Hello, world!
puts(str); // Output: Hello, world! (followed by a newline)
return 0;
}
“`
puts
比 printf("%s\n", str)
效率略高,因为它不需要解析格式串。但 printf
更灵活,可以结合其他文本和变量一起输出。
输入字符串:
从标准输入(键盘)读取字符串时需要特别小心,主要涉及缓冲区溢出问题。
-
scanf("%s", str);
:使用%s
格式符读取字符串。scanf
会读取非空白字符序列,并在遇到空白字符(空格、制表符、换行符)时停止。它会自动在读取的字符末尾添加\0
。危险!
scanf("%s", str)
的问题在于,它不知道str
数组有多大,如果输入的字符串长度超过了str
数组的容量(包括\0
的空间),就会发生缓冲区溢出,写入到数组边界以外的内存区域,导致程序错误或安全漏洞。“`c
include
int main() {
char buffer[10]; // 只能安全存储最多 9 个字符的字符串
printf(“Enter a word: “);
scanf(“%s”, buffer); // 如果输入超过 9 个字符,这里会出问题!
printf(“You entered: %s\n”, buffer);
return 0;
}
“` -
gets(str);
:读取一行输入直到遇到换行符或文件结束符,并将换行符替换为\0
。极度危险!
gets
同样不知道目标缓冲区的大小,没有任何办法防止缓冲区溢出。在任何现代C代码中都不应该使用gets
函数!它已经被C11标准移除。 -
fgets(buffer, size, stdin);
:从指定的输入流(这里是stdin
,标准输入)读取最多size-1
个字符到buffer
中,或者直到遇到换行符或文件结束符。它会保留读取到的换行符(如果读取到了),并在末尾添加\0
。这是读取一行字符串的更安全的方式,因为它限制了读取的字符数量,防止了缓冲区溢出。
“`c
include
int main() {
char buffer[20]; // 可以安全存储最多 19 个字符的字符串(包括 \0)
printf(“Enter a line: “);
// 从标准输入读取最多 19 个字符到 buffer 中
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
printf(“You entered: %s”, buffer);
} else {
printf(“Error reading input.\n”);
}
return 0;
}
“`使用
fgets
时需要注意:
1. 它可能会包含末尾的换行符\n
。如果需要去除换行符,可以手动处理:
c
// 去除 fgets 读取到的换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
2. 如果输入超出了size-1
个字符,fgets
只会读取前size-1
个,剩下的会留在输入缓冲区中,影响后续的输入操作。可能需要额外的代码来清空输入缓冲区。
总结输入方法: 优先使用 fgets
来读取一行字符串,并根据需要去除换行符。对于读取单个“单词”(非空白字符序列),如果能确定最大长度,可以使用 scanf
的宽度限定符 %Ns
(例如 scanf("%9s", buffer);
),但这仍然可能留下输入缓冲区中的剩余字符,并且不如 fgets
灵活。永远不要使用 gets
。
4. C标准库 <string.h>
:字符串常用操作函数详解
C标准库提供了 <string.h>
头文件,其中包含了一系列用于操作字符串的函数。使用这些函数可以方便地进行字符串的长度计算、复制、连接、比较、查找等操作。
使用这些函数前,务必包含头文件:#include <string.h>
4.1 计算长度:strlen()
- 功能: 计算字符串的长度(不包括末尾的
\0
)。 - 函数签名:
size_t strlen(const char *s);
- 参数:
s
– 指向待计算长度的字符串的指针。 - 返回值: 字符串的长度(类型为
size_t
,通常是无符号整型)。 - 注意:
strlen
从给定的地址开始查找\0
,因此,如果传入的不是一个有效的C字符串(即没有\0
终止),它会一直读下去,导致错误甚至崩溃。
“`c
include
include
int main() {
char str[] = “C Programming”;
size_t len = strlen(str);
printf(“The length of \”%s\” is %zu\n”, str, len); // Output: The length of “C Programming” is 13
return 0;
}
“`
4.2 复制字符串:strcpy()
和 strncpy()
-
strcpy()
- 功能: 将源字符串复制到目标地址。
- 函数签名:
char *strcpy(char *dest, const char *src);
- 参数:
dest
– 目标地址(字符数组);src
– 源字符串。 - 返回值:
dest
的值(即目标地址)。 - 危险!
strcpy
不检查目标缓冲区的大小。如果dest
的空间不足以容纳src
字符串(包括\0
),就会发生缓冲区溢出。
“`c
include
include
int main() {
char src[] = “Hello”;
char dest[10]; // 目标缓冲区大小为 10
strcpy(dest, src); // src (“Hello\0”) 需要 6 个字节,dest 有 10 个,安全
printf(“Copied string: %s\n”, dest); // Output: Copied string: Hellochar long_src[] = "This is a very long string."; char small_dest[10]; // 目标缓冲区大小为 10 // strcpy(small_dest, long_src); // <-- 危险!long_src 太长,会导致缓冲区溢出! // printf("Copied string: %s\n", small_dest); return 0;
}
“` -
strncpy()
- 功能: 将源字符串的前
n
个字符复制到目标地址。 - 函数签名:
char *strncpy(char *dest, const char *src, size_t n);
- 参数:
dest
– 目标地址;src
– 源字符串;n
– 要复制的最大字符数。 - 返回值:
dest
的值。 - 注意:
strncpy
是为了处理定长缓冲区而设计的,但它的行为有点特殊:- 它最多复制
n
个字符。 - 如果
src
的长度小于n
,剩余的空间会用\0
填充。 - 如果
src
的长度大于或等于n
,strncpy
只会复制前n
个字符, 不会 在dest
的末尾添加\0
! 这意味着如果src
刚好或超过n
长度,dest
可能不是一个有效的C字符串。
- 它最多复制
“`c
include
include
int main() {
char src[] = “World”;
char dest1[10];
strncpy(dest1, src, sizeof(dest1) – 1); // 复制最多 9 个字符
dest1[sizeof(dest1) – 1] = ‘\0’; // 手动确保 null 终止(安全做法)
printf(“Copied (strncpy, safe): %s\n”, dest1); // Output: Copied (strncpy, safe): Worldchar src2[] = "abcdefghijklmn"; // 14 chars char dest2[10]; // Size 10 strncpy(dest2, src2, sizeof(dest2)); // 复制 10 个字符 // dest2 的内容是 "abcdefghij"。它没有以
结尾! // printf("Copied (strncpy, potentially unsafe): %s\n", dest2); // <-- 危险!可能会打印乱码 return 0;char src2[] = "abcdefghijklmn"; // 14 chars char dest2[10]; // Size 10 strncpy(dest2, src2, sizeof(dest2)); // 复制 10 个字符 // dest2 的内容是 "abcdefghij"。它没有以 \0 结尾! // printf("Copied (strncpy, potentially unsafe): %s\n", dest2); // <-- 危险!可能会打印乱码 return 0;
}
``
strncpy
**安全建议:** 使用复制字符串时,总是将
n设置为目标缓冲区大小减1,并在复制后**手动**在目标缓冲区的末尾添加
\0,以确保其是有效的C字符串。或者使用更现代、更安全的函数(如 POSIX 标准中的
strcpy_s或 C11 的
memcpy_s结合长度检查),但这些函数并非所有编译器都支持。最通用的做法是**先检查源字符串长度,再使用
strcpy或
strncpy` 结合手动终止。** - 功能: 将源字符串的前
4.3 连接字符串:strcat()
和 strncat()
-
strcat()
- 功能: 将源字符串连接到目标字符串的末尾。
- 函数签名:
char *strcat(char *dest, const char *src);
- 参数:
dest
– 目标字符串(连接后结果存储在这里,需要有足够的空间);src
– 源字符串。 - 返回值:
dest
的值。 - 危险!
strcat
不检查目标缓冲区是否有足够的空间容纳连接后的字符串。它从dest
中找到\0
的位置,然后从那里开始复制src
的内容(包括src
的\0
)。如果空间不足,就会发生缓冲区溢出。
“`c
include
include
int main() {
char dest[50] = “Hello”;
char src[] = ” World!”;
strcat(dest, src); // dest 原长 5,src 长 7(包括 ! 和 \0),总长 12 + \0 = 13。dest 50 空间足够。
printf(“Concatenated string: %s\n”, dest); // Output: Concatenated string: Hello World!char small_dest[10] = "Short"; // 原长 5 +
= 6 字节 char long_src[] = " and Long"; // 长 9 +char small_dest[10] = "Short"; // 原长 5 + \0 = 6 字节 char long_src[] = " and Long"; // 长 9 + \0 = 10 字节 // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节! // printf("Concatenated string: %s\n", small_dest); return 0;
= 10 字节 // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节! // printf("Concatenated string: %s\n", small_dest); return 0;char small_dest[10] = "Short"; // 原长 5 + \0 = 6 字节 char long_src[] = " and Long"; // 长 9 + \0 = 10 字节 // strcat(small_dest, long_src); // <-- 危险!连接后需要 5 + 9 + 1 = 15 字节,small_dest 只有 10 字节! // printf("Concatenated string: %s\n", small_dest); return 0;
}
“` -
strncat()
- 功能: 将源字符串的前
n
个字符连接到目标字符串的末尾。 - 函数签名:
char *strncat(char *dest, const char *src, size_t n);
- 参数:
dest
– 目标字符串;src
– 源字符串;n
– 要从src
中复制的最大字符数。 - 返回值:
dest
的值。 - 注意:
strncat
比strcat
安全,因为它限制了从src
复制的字符数量。更重要的是,它总是会在连接后的字符串末尾添加\0
。
它复制src
中n
个字符或到src
的\0
为止(取较小者),然后在这个结果后添加\0
。
“`c
include
include
int main() {
char dest[20] = “Hello”; // Original length 5. Max space 19 + \0 = 20
char src[] = ” World Wide Web”; // Length 15
size_t dest_len = strlen(dest); // Current length of dest: 5
size_t space_left = sizeof(dest) – dest_len – 1; // Remaining space for chars: 20 – 5 – 1 = 14// 连接 src 的前 space_left 个字符 strncat(dest, src, space_left); printf("Concatenated (strncat): %s\n", dest); // Output: Concatenated (strncat): Hello World Wide // 如果 space_left 小于 src 的实际长度,只会复制一部分 // 如果 space_left 大于等于 src 的实际长度,会复制 src 的全部内容并添加
char dest2[20] = "Hello"; strncat(dest2, " C!", 20 - strlen(dest2) - 1); // 连接 " C!" 的前 (20-5-1)=14 个字符。实际只有 3个字符。 printf("Concatenated (strncat 2): %s\n", dest2); // Output: Concatenated (strncat 2): Hello C! return 0;// 连接 src 的前 space_left 个字符 strncat(dest, src, space_left); printf("Concatenated (strncat): %s\n", dest); // Output: Concatenated (strncat): Hello World Wide // 如果 space_left 小于 src 的实际长度,只会复制一部分 // 如果 space_left 大于等于 src 的实际长度,会复制 src 的全部内容并添加 \0 char dest2[20] = "Hello"; strncat(dest2, " C!", 20 - strlen(dest2) - 1); // 连接 " C!" 的前 (20-5-1)=14 个字符。实际只有 3个字符。 printf("Concatenated (strncat 2): %s\n", dest2); // Output: Concatenated (strncat 2): Hello C! return 0;
}
``
strncat仍然需要确保目标缓冲区
dest有足够的空间来容纳**原字符串长度 + 要复制的字符数 + 1 (for \0)**。所以,通常在使用
strncat(dest, src, n)之前,会计算目标字符串当前的长度,加上要连接的
n个字符长度,确保不超过目标缓冲区的总大小。上面的例子中计算
space_left` 就是这种安全用法的体现。 - 功能: 将源字符串的前
4.4 比较字符串:strcmp()
和 strncmp()
-
strcmp()
- 功能: 逐个字符比较两个字符串,直到发现差异或到达任一字符串的
\0
。 - 函数签名:
int strcmp(const char *s1, const char *s2);
- 参数:
s1
– 第一个字符串;s2
– 第二个字符串。 - 返回值:
- 如果
s1
和s2
相等,返回 0。 - 如果
s1
在字典序上小于s2
,返回一个小于 0 的整数。 - 如果
s1
在字典序上大于s2
,返回一个大于 0 的整数。
- 如果
- 比较是基于字符的ASCII值进行的。
“`c
include
include
int main() {
char s1[] = “apple”;
char s2[] = “apply”;
char s3[] = “apple”;printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s2, strcmp(s1, s2)); // Output: ... < 0 (e.g., -1) printf("strcmp(\"%s\", \"%s\") = %d\n", s2, s1, strcmp(s2, s1)); // Output: ... > 0 (e.g., 1) printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s3, strcmp(s1, s3)); // Output: ... == 0 char s4[] = "Apple"; printf("strcmp(\"%s\", \"%s\") = %d\n", s1, s4, strcmp(s1, s4)); // Output: ... > 0 (lowercase 'a' > uppercase 'A') return 0;
}
“` - 功能: 逐个字符比较两个字符串,直到发现差异或到达任一字符串的
-
strncmp()
- 功能: 比较两个字符串的前
n
个字符。 - 函数签名:
int strncmp(const char *s1, const char *s2, size_t n);
- 参数:
s1
,s2
– 要比较的字符串;n
– 要比较的最大字符数。 - 返回值: 与
strcmp
类似(小于 0, 0, 大于 0)。 - 比较会在找到差异、达到
n
个字符或遇到任一字符串的\0
时停止(取满足条件的最早时刻)。
“`c
include
include
int main() {
char s1[] = “applepie”;
char s2[] = “applejuice”;printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple") printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju") char s3[] = "app"; printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '
' vs s3 till 'printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple") printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju") char s3[] = "app"; printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app") // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0) return 0;
', "applepie" vs "app") // It stops at the 'printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple") printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju") char s3[] = "app"; printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app") // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0) return 0;
' in s3, then compares 'l' in s1 with 'printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple") printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju") char s3[] = "app"; printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app") // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0) return 0;
' (ASCII 0) return 0;printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s2, strncmp(s1, s2, 5)); // Output: ... == 0 (comparing "apple") printf("strncmp(\"%s\", \"%s\", 7) = %d\n", s1, s2, strncmp(s1, s2, 7)); // Output: ... < 0 (comparing "applepi" vs "appleju") char s3[] = "app"; printf("strncmp(\"%s\", \"%s\", 5) = %d\n", s1, s3, strncmp(s1, s3, 5)); // Output: ... > 0 (comparing s1 till '\0' vs s3 till '\0', "applepie" vs "app") // It stops at the '\0' in s3, then compares 'l' in s1 with '\0' (ASCII 0) return 0;
}
“` - 功能: 比较两个字符串的前
4.5 查找字符:strchr()
和 strrchr()
-
strchr()
- 功能: 在字符串中从头开始查找指定字符的第一次出现。
- 函数签名:
char *strchr(const char *s, int c);
- 参数:
s
– 要搜索的字符串;c
– 要查找的字符(通常以 int 形式传入)。 - 返回值: 指向第一次出现字符
c
的位置的指针;如果未找到,返回NULL
。 - 注意: 查找包含字符串末尾的
\0
字符是有效的。
“`c
include
include
int main() {
char str[] = “Hello, world!”;
char *ptr = strchr(str, ‘o’);
if (ptr != NULL) {
printf(“First ‘o’ found at position: %ld\n”, ptr – str); // ptr – str gives the index
printf(“Substring from first ‘o’: %s\n”, ptr); // Output: o, world!
} else {
printf(“‘o’ not found.\n”);
}ptr = strchr(str, 'z'); if (ptr == NULL) { printf("'z' not found.\n"); // Output: 'z' not found. } ptr = strchr(str, '
'); // Finding the null terminator is valid if (ptr != NULL) { printf("Null terminator found at position: %ld\n", ptr - str); } return 0;ptr = strchr(str, 'z'); if (ptr == NULL) { printf("'z' not found.\n"); // Output: 'z' not found. } ptr = strchr(str, '\0'); // Finding the null terminator is valid if (ptr != NULL) { printf("Null terminator found at position: %ld\n", ptr - str); } return 0;
}
“` -
strrchr()
- 功能: 在字符串中从头开始查找指定字符的最后一次出现。
- 函数签名:
char *strrchr(const char *s, int c);
- 参数:
s
– 要搜索的字符串;c
– 要查找的字符。 - 返回值: 指向最后一次出现字符
c
的位置的指针;如果未找到,返回NULL
。 - 注意: 也包含对
\0
的查找。
“`c
include
include
int main() {
char str[] = “programming”;
char *ptr = strrchr(str, ‘g’);
if (ptr != NULL) {
printf(“Last ‘g’ found at position: %ld\n”, ptr – str); // Output: Last ‘g’ found at position: 9
printf(“Substring from last ‘g’: %s\n”, ptr); // Output: g
} else {
printf(“‘g’ not found.\n”);
}
return 0;
}
“`
4.6 查找子字符串:strstr()
- 功能: 在一个字符串中查找另一个子字符串的第一次出现。
- 函数签名:
char *strstr(const char *haystack, const char *needle);
- 参数:
haystack
– 在其中搜索的字符串(大海);needle
– 要查找的子字符串(针)。 - 返回值: 指向
haystack
中第一次出现needle
子字符串的起始位置的指针;如果未找到,返回NULL
。
“`c
include
include
int main() {
char text[] = “The quick brown fox jumps over the lazy dog.”;
char sub[] = “fox”;
char *ptr = strstr(text, sub);
if (ptr != NULL) {
printf("Substring \"%s\" found in \"%s\".\n", sub, text);
printf("Starting from: %s\n", ptr); // Output: Starting from: fox jumps over the lazy dog.
} else {
printf("Substring \"%s\" not found.\n", sub);
}
char sub2[] = "cat";
ptr = strstr(text, sub2);
if (ptr == NULL) {
printf("Substring \"%s\" not found.\n", sub2); // Output: Substring "cat" not found.
}
return 0;
}
“`
还有其他一些 <string.h>
中的函数,如 memset
, memcpy
, memmove
用于字节操作(也可以用于字符串,但要小心 \0
),以及 strtok
用于字符串分割等。对于入门,掌握上面列出的核心函数已经足够了。
5. 字符串与指针:深入理解其关系
在C语言中,字符串和指针有着密不可分的关系。字符数组名在很多情况下可以被当作指向数组第一个元素的指针。字符串处理函数通常接受 char *
类型的参数,意味着它们操作的是一个地址,从这个地址开始,直到遇到 \0
。
回顾一下前面提到的两种声明字符串的方式:
-
char array[] = "hello";
array
是一个字符数组,它在内存中分配了一块存储空间(通常在栈上)。array
这个名字本身在表达式中会衰退(decay)为指向数组第一个元素的指针(char *
)。- 数组的内容
"hello\0"
存储在这块内存中,可以修改。 sizeof(array)
会得到数组的实际大小(例如 6)。strlen(array)
会得到字符串的长度(例如 5)。
-
char *pointer = "world";
pointer
是一个char
类型的指针变量,它本身占用几个字节的存储空间(通常在栈上)。- 字符串字面量
"world\0"
通常存储在程序的只读数据段。 pointer
存储的是这个只读字符串字面量的首地址。*pointer
指向的内容(即字符串字面量)是不可修改的。sizeof(pointer)
会得到指针变量本身的大小(通常是 4 或 8 字节),而不是字符串的长度。strlen(pointer)
会得到字符串的长度(例如 5)。
示例:通过指针遍历字符串
“`c
include
int main() {
char str[] = “Example”;
char *ptr = str; // ptr 指向 str 的第一个字符 ‘E’
printf("Iterating through string using pointer:\n");
while (*ptr != 'printf("Iterating through string using pointer:\n");
while (*ptr != '\0') {
printf("%c ", *ptr); // Print the character the pointer is pointing to
ptr++; // Move the pointer to the next character
}
printf("\n"); // Output: E x a m p l e
return 0;
') {
printf("%c ", *ptr); // Print the character the pointer is pointing to
ptr++; // Move the pointer to the next character
}
printf("\n"); // Output: E x a m p l e
return 0;
}
“`
理解字符串和指针的关系对于高效和正确地编写C字符串代码至关重要。当你传递一个字符串给函数时,你传递的实际上是它的起始地址(一个指针)。函数通过这个指针访问字符串,并依赖 \0
来知道何时停止。
6. 字符串的常见问题与陷阱
学习C语言字符串,了解常见的陷阱是避免错误的必要步骤。
- 缓冲区溢出 (Buffer Overflow): 这是最常见也是最危险的问题。使用
strcpy
,strcat
,scanf("%s", ...)
而没有检查目标缓冲区大小,是导致安全漏洞的罪魁祸首。永远记住要为\0
预留一个字节空间。 - 忘记空终止符 (
\0
): 如果手动构建字符数组而忘记添加\0
,或者使用strncpy
时源字符串长度等于n
而没有手动添加\0
,那么结果将不是一个有效的C字符串。使用strlen
,printf("%s", ...)
等函数时会读取到无效内存。 - 修改字符串字面量: 通过
char *ptr = "..."
声明的字符串是只读的。尝试通过ptr[i] = ...
或*ptr = ...
修改它们会导致程序崩溃或未预测的行为。 sizeof
vs.strlen
: 对于char array[] = "..."
声明的字符串:sizeof(array)
返回数组的总字节数(包括\0
及未使用的空间),strlen(array)
返回字符串的实际字符数(不包括\0
)。对于char *ptr = "..."
或char *ptr;
:sizeof(ptr)
返回指针变量的大小,与字符串内容无关;strlen(ptr)
返回指针指向的字符串的长度(如果指针指向一个有效的C字符串)。- 局部字符串数组的生命周期: 在函数内部声明的
char array[]
是局部变量,存储在栈上。函数返回后,这块内存会被回收,返回指向这个局部数组的指针是危险的,因为指针指向的内存可能不再有效。如果要返回字符串,通常需要动态分配内存(使用malloc
等),或者将字符串复制到调用者提供的缓冲区中。
7. 总结与实践建议
- 理解本质: C字符串是字符数组,以
\0
结尾。\0
是字符串结束的标志。 - 安全第一: 永远小心缓冲区溢出。优先使用
fgets
进行输入。使用strncpy
和strncat
时,务必计算好目标缓冲区剩余空间,并确保结果字符串以\0
结尾。在可能的情况下,自己手动检查长度后再进行复制/连接。 - 掌握
<string.h>
: 熟悉并正确使用strlen
,strcpy
,strcat
,strcmp
等函数。理解它们的参数、返回值和潜在风险。 - 区分数组与指针: 明确
char array[]
和char *pointer = "..."
的区别,特别是它们的可修改性和内存位置。不要试图修改字符串字面量。 - 动手实践: 理论结合实践,多写代码,尝试不同的字符串操作,故意制造一些错误(如缓冲区溢出)来观察程序行为,加深理解。
- 查阅文档: 遇到不确定的函数用法时,查阅C语言标准库文档是最好的方法。
C语言字符串虽然初看起来有些复杂,但只要掌握了字符数组、空终止符以及标准库函数的使用,并时刻保持对内存和安全性的警惕,你就能有效地处理字符串相关的编程任务。
这篇教程涵盖了C语言字符串的核心概念和常用操作。字符串在C语言中是构建更复杂数据结构(如链表、树中存储字符串数据)和进行文件I/O、网络通信等操作的基础。打好字符串的基础,对于你进一步深入学习C语言至关重要。
祝你在C语言的学习旅程中一切顺利!