在C语言中,字符串是以空字符 '\0' 结尾的字符数组。由于C语言没有提供专门的字符串类型,所有字符串操作都需要借助字符数组和标准库函数完成。
<string.h> 头文件中提供了大量用于字符串处理的函数,涵盖复制、连接、比较、查找、分割等常见操作。
掌握这些函数是编写健壮、高效C程序的基础。本章将详细介绍这些函数的用法、注意事项及安全编程技巧。
C语言中字符串的两种常见表示方式:
char str[10] = "Hello";,存储在栈或静态区,可修改内容。char *str = "Hello";,指向字符串常量,通常存储在只读数据段,不可修改。#include <stdio.h>
int main() {
char str1[] = "Hello"; // 可修改
char *str2 = "World"; // 不可修改
str1[0] = 'h'; // 正确
// str2[0] = 'w'; // 错误!可能导致程序崩溃
printf("%s %s\n", str1, str2);
return 0;
}
strlen 函数返回字符串的长度(不包括结尾的 '\0')。
size_t strlen(const char *str);
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
printf("字符串: \"%s\"\n", str);
printf("长度: %zu\n", strlen(str)); // 输出 13(不包括结尾的'\0')
return 0;
}
strlen 返回 size_t 类型,这是一个无符号整数类型,使用 %zu 格式符输出。
char *strcpy(char *dest, const char *src);
将 src 指向的字符串(包括结尾的 '\0')复制到 dest 指向的数组。如果目标数组不够大,会发生缓冲区溢出。
char dest[10];
strcpy(dest, "Hello"); // 正确,占6字节(包括'\0')
// strcpy(dest, "Hello, World!"); // 错误!目标空间不足,溢出
char *strncpy(char *dest, const char *src, size_t n);
最多复制 n 个字符到 dest。如果 src 的长度小于 n,则剩余部分用 '\0' 填充;如果大于等于 n,则不会自动添加结尾的 '\0',需要手动处理。
#include <stdio.h>
#include <string.h>
int main() {
char dest[10];
strncpy(dest, "Hello, World!", sizeof(dest) - 1); // 最多复制9个字符
dest[sizeof(dest) - 1] = '\0'; // 确保以'\0'结尾
printf("%s\n", dest); // 输出 "Hello, Wo"
return 0;
}
strncpy 不会自动添加结尾的 '\0',如果源字符串长度 ≥ n,目标字符串将不会以空字符结尾,后续使用可能出错。
char *strcat(char *dest, const char *src);
将 src 追加到 dest 的末尾,覆盖 dest 原来的 '\0'。要求 dest 有足够空间容纳拼接后的字符串。
char *strncat(char *dest, const char *src, size_t n);
最多追加 n 个字符,并自动添加结尾的 '\0'。
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello";
char src[] = ", World!";
strncat(dest, src, sizeof(dest) - strlen(dest) - 1); // 安全连接
printf("%s\n", dest); // 输出 "Hello, World!"
return 0;
}
比较两个字符串的字典序,返回:
0:相等<0:第一个不匹配的字符在 str1 中较小(或 str1 是 str2 的前缀)>0:第一个不匹配的字符在 str1 中较大int strcmp(const char *str1, const char *str2);
int strncmp(const char *str1, const char *str2, size_t n); // 只比较前n个字符
#include <stdio.h>
#include <string.h>
int main() {
char s1[] = "apple";
char s2[] = "banana";
char s3[] = "apple";
if (strcmp(s1, s2) < 0)
printf("%s 小于 %s\n", s1, s2);
if (strcmp(s1, s3) == 0)
printf("%s 等于 %s\n", s1, s3);
// 比较前3个字符
if (strncmp(s1, s2, 3) < 0)
printf("前3个字符比较:apple < banana\n");
return 0;
}
strchr(str, c):在字符串中查找字符 c 第一次出现的位置,返回指针,未找到返回 NULL。strrchr(str, c):查找字符 c 最后一次出现的位置。strstr(str, substr):查找子串 substr 第一次出现的位置。#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World! Hello, C!";
char *p;
p = strchr(str, 'W');
if (p) printf("找到 'W' 在位置: %ld\n", p - str); // 输出7
p = strrchr(str, 'l');
if (p) printf("最后一个 'l' 在位置: %ld\n", p - str); // 输出14
p = strstr(str, "World");
if (p) printf("子串 \"World\" 起始位置: %ld\n", p - str); // 输出7
return 0;
}
strtok 用于将字符串按分隔符拆分为多个令牌(token)。注意:它会修改原字符串(将分隔符替换为 '\0'),且不是线程安全的。
char *strtok(char *str, const char *delim);
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "apple,banana,orange,grape";
const char delim[] = ",";
// 第一次调用传入原字符串
char *token = strtok(str, delim);
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, delim); // 后续调用传NULL
}
// 此时 str 已被修改,原字符串中的分隔符被替换为 '\0'
// 所以 str 现在只包含第一个令牌 "apple"
return 0;
}
strtok_r(POSIX)或 strtok_s(C11可选)。
将格式化数据写入字符串,类似于 printf,但输出目标是字符串。
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...); // 安全版本
#include <stdio.h>
int main() {
char buffer[50];
int a = 10;
float b = 3.14159;
// 不安全:可能溢出
// sprintf(buffer, "a=%d, b=%.2f", a, b);
// 安全:指定最多写入49个字符,留一个给'\0'
snprintf(buffer, sizeof(buffer), "a=%d, b=%.2f", a, b);
printf("%s\n", buffer); // 输出 "a=10, b=3.14"
return 0;
}
snprintf 替代 sprintf,避免缓冲区溢出风险。
strncpy、strncat、snprintf、strncmp,并注意手动添加结尾 '\0'。sizeof 和 strlen 计算缓冲区剩余空间:在拼接或复制时确保不溢出。strtok:注意它修改原字符串且不是线程安全的,考虑使用 strtok_r 或手动解析。strchr、strstr 可能返回 NULL,应始终检查。strcpy_s、strcat_s 等边界检查函数,但需要编译器支持(如 MSVC)且非所有平台都支持。#include <stdio.h>
#include <string.h>
#include <ctype.h>
int countWords(const char *str) {
int count = 0;
int inWord = 0;
while (*str) {
if (isspace(*str)) {
inWord = 0;
} else if (!inWord) {
inWord = 1;
count++;
}
str++;
}
return count;
}
int main() {
char sentence[200];
printf("请输入一个英文句子: ");
fgets(sentence, sizeof(sentence), stdin);
// 移除换行符
size_t len = strlen(sentence);
if (len > 0 && sentence[len-1] == '\n')
sentence[len-1] = '\0';
int words = countWords(sentence);
printf("单词数: %d\n", words);
// 使用 strtok 分割单词并输出
printf("单词列表:\n");
char copy[200];
strcpy(copy, sentence); // 复制一份,因为 strtok 会修改原字符串
char *token = strtok(copy, " \t\n");
while (token) {
printf(" %s\n", token);
token = strtok(NULL, " \t\n");
}
return 0;
}
strcpy、strcat 等不安全函数时未保证目标空间足够,是C程序中最常见的安全漏洞之一。strncpy 后忘记手动添加结尾空字符,导致后续字符串操作越界。strchr 返回 NULL 时未检查,继续使用该指针导致崩溃。strtok:strtok 使用静态内部状态,不是线程安全的。使用 strtok_r 替代。sizeof 对指针计算长度:当字符串作为函数参数传递时,sizeof(str) 返回的是指针大小(通常8),而不是数组长度。应额外传递长度参数。字符串处理是C语言编程的核心技能之一。掌握标准库提供的函数,并养成安全编码的习惯,能够有效避免缓冲区溢出等常见问题。 下一章我们将学习指针,这是C语言的精髓,也是理解字符串底层操作的关键。