第13章 字符串中字符统计:统计字母、数字、空格、特殊字符
字符统计,听起来很简单对吧?
不就是遍历一遍字符串,然后判断每个字符属于哪一类吗?
嗯,原理确实不复杂。但我在实际项目中踩过的坑,让我对这个话题有了更深的理解。
13.1 字符分类的基本原理
字符统计的核心,说白了就是分类计数。
你需要把每个字符归到四个桶里:字母、数字、空格、特殊字符。
判断依据是什么?就是ASCII码的范围。
ASCII码分类区间:
- 大写字母:'A' ~ 'Z'(65 ~ 90)
- 小写字母:'a' ~ 'z'(97 ~ 122)
- 数字:'0' ~ '9'(48 ~ 57)
- 空格:' '(32)
- 其他:特殊字符(包括标点、控制字符等)
你可能会问:为什么不直接用字符比较?
当然可以。而且我个人更推荐用字符比较,代码可读性更好。
13.2 基础实现:手写判断逻辑
先看一个最直接的版本。我当年刚入行时就是这么写的。
#include <stdio.h>
#include <string.h>
void count_chars(const char *str, int *letters, int *digits,
int *spaces, int *others) {
*letters = *digits = *spaces = *others = 0;
while (*str) {
if ((*str >= 'A' && *str <= 'Z') ||
(*str >= 'a' && *str <= 'z')) {
(*letters)++;
}
else if (*str >= '0' && *str <= '9') {
(*digits)++;
}
else if (*str == ' ') {
(*spaces)++;
}
else {
(*others)++;
}
str++;
}
}
int main() {
char text[] = "Hello, 2024! How are you?";
int letters, digits, spaces, others;
count_chars(text, &letters, &digits, &spaces, &others);
printf("字符串: %s\n", text);
printf("字母: %d\n", letters);
printf("数字: %d\n", digits);
printf("空格: %d\n", spaces);
printf("特殊字符: %d\n", others);
return 0;
}
运行结果:
字符串: Hello, 2024! How are you?
字母: 13
数字: 4
空格: 4
特殊字符: 3
这个代码有什么问题?
嗯,功能上没问题。但可扩展性差。如果你要增加分类(比如区分大小写字母),就得改if-else结构。
13.3 使用标准库函数:更优雅的方式
C语言标准库提供了ctype.h,里面有一组字符分类函数。
我个人习惯用这些函数,代码更简洁,也不容易出错。
#include <stdio.h>
#include <string.h>
#include <ctype.h>
void count_chars_ctype(const char *str, int *letters, int *digits,
int *spaces, int *others) {
*letters = *digits = *spaces = *others = 0;
for (int i = 0; str[i] != '\0'; i++) {
if (isalpha(str[i])) {
(*letters)++;
}
else if (isdigit(str[i])) {
(*digits)++;
}
else if (isspace(str[i])) {
(*spaces)++;
}
else {
(*others)++;
}
}
}
提示:isspace() 不仅匹配空格' ',还匹配制表符'\t'、换行符'\n'等空白字符。如果你只想统计空格,记得用 str[i] == ' '。
13.4 避坑指南:我踩过的三个坑
坑一:忽略中文字符
我曾经在一个日志分析工具里用了上面的代码。结果发现特殊字符数量异常高。
排查了半天,才发现日志里有中文注释。中文字符在ASCII判断下全成了「特殊字符」。
如果你处理的是UTF-8编码的中文,每个汉字占3个字节,每个字节都大于127。用isalpha()判断会返回假。
注意:处理多字节编码(如UTF-8、GBK)时,不能直接用单字节的字符分类函数。你需要先做编码识别,或者用宽字符函数(如 iswalpha())。
坑二:缓冲区溢出
有一次我接手一个老项目,代码里直接用了scanf("%s", buf)来读用户输入。
用户输入了一长串字符,直接溢出了缓冲区,程序崩溃了。
从那以后,我所有读字符串的操作都加长度限制:fgets(buf, sizeof(buf), stdin)。
坑三:忘记处理空字符串
空字符串也是合法输入。如果传入空串,你的函数应该返回全0,而不是崩溃或输出垃圾值。
我见过有人写循环时直接用while (*str++),空串时指针直接越界了。
13.5 进阶:统计字符频率
有时候你不仅要知道总数,还想知道每个字符出现了多少次。
比如分析一段英文文本中哪个字母用得最多。
#include <stdio.h>
#include <ctype.h>
void char_frequency(const char *str, int freq[256]) {
// 初始化
for (int i = 0; i < 256; i++) {
freq[i] = 0;
}
// 统计
while (*str) {
unsigned char ch = (unsigned char)*str;
freq[ch]++;
str++;
}
}
void print_frequency(int freq[256]) {
printf("字符频率统计(仅显示出现次数>0的):\n");
for (int i = 0; i < 256; i++) {
if (freq[i] > 0) {
if (isprint(i)) {
printf(" '%c' (%3d): %d次\n", i, i, freq[i]);
} else {
printf(" 0x%02X: %d次\n", i, freq[i]);
}
}
}
}
int main() {
char text[] = "Hello, World!";
int freq[256];
char_frequency(text, freq);
print_frequency(freq);
return 0;
}
关键点:用 unsigned char 做索引,避免负数下标。char 类型在有些编译器上默认是 signed,值为128~255的字符会变成负数。
13.6 知识体系:字符统计的核心逻辑
下面这张图总结了字符统计的完整流程。我建议你把它记在脑子里。
13.7 性能优化:一次遍历就够了
字符统计的时间复杂度是O(n),n是字符串长度。这是最优的,因为你至少得看每个字符一次。
空间复杂度是O(1),只用了几个计数器。
但如果你用频率数组(大小256),空间也是O(1),因为256是常数。
性能建议:
- 用指针遍历比用下标快一点点(但差别不大,编译器会优化)
- 把
strlen()结果存起来,不要在循环条件里每次都调用 - 如果字符串超长(比如几MB),考虑用多线程分块统计
13.8 实战:统计文件中的字符
最后,我分享一个实际项目中用过的代码。统计一个文本文件里各类字符的数量。
#include <stdio.h>
#include <ctype.h>
int count_file_chars(const char *filename,
int *letters, int *digits,
int *spaces, int *others) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
*letters = *digits = *spaces = *others = 0;
int ch;
while ((ch = fgetc(fp)) != EOF) {
if (isalpha(ch)) {
(*letters)++;
} else if (isdigit(ch)) {
(*digits)++;
} else if (isspace(ch)) {
(*spaces)++;
} else {
(*others)++;
}
}
fclose(fp);
return 0;
}
int main() {
char filename[] = "sample.txt";
int letters, digits, spaces, others;
if (count_file_chars(filename, &letters, &digits,
&spaces, &others) == 0) {
printf("文件: %s\n", filename);
printf("字母: %d\n", letters);
printf("数字: %d\n", digits);
printf("空白字符: %d\n", spaces);
printf("其他字符: %d\n", others);
printf("总字符数: %d\n",
letters + digits + spaces + others);
}
return 0;
}
注意这里用fgetc()返回的是int,不是char。为什么?因为EOF是-1,用char存会丢失信息。
嗯,这个细节坑过不少人。
13.9 总结
字符统计看起来简单,但做好并不容易。
- 基础判断用
ctype.h函数,别自己写ASCII范围判断 - 注意多字节编码问题,中文不是单字节字符
- 频率数组用
unsigned char做索引 - 文件读取用
int接收fgetc()的返回值 - 空字符串和边界情况一定要处理
这些经验,都是我一行一行代码调试出来的。希望你少走弯路。
公众号:蓝海资料掘金营,微信deep3321