第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 知识体系:字符统计的核心逻辑

下面这张图总结了字符统计的完整流程。我建议你把它记在脑子里。

字符统计核心流程 输入字符串 遍历每个字符(直到 '\0') 字符分类判断 isalpha() | isdigit() | isspace() | 其他 字母计数++ 数字计数++ 空格计数++ 特殊字符++ 循环继续

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