news 2026/9/8 17:01:46

C语言统计大写字母个数:从基础题到工程思维的完整拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言统计大写字母个数:从基础题到工程思维的完整拆解

1. 这类题为什么值得认真对待

“1007 - 统计大写英文字母的个数”这个题目,乍看是C语言入门练习里最不起眼的一道:输入一串字符,数一数里面有多少个大写字母。但我在实际带新人、看学生作业的过程中发现,越是这种基础题目,越能暴露问题。很多同学能写出能跑的代码,但一问“为什么这么写”,就说不出所以然了;再让换个输入场景,代码就崩了。

这个题目背后面临的是字符串处理、字符判定、循环结构、数组边界这几大基本功。它们不是孤立的知识点,而是在后续几乎所有C语言项目里都会反复用到的基础能力。比如你以后写命令行工具要解析参数、写通信协议要校验帧格式、写文本处理要过滤非法字符,本质都在做类似的事:逐个检查字符,按规则归类计数。把这道题吃透了,往后很多字符串相关的问题都顺了。

什么人适合认真琢磨它?

  • 刚学完顺序、分支、循环三大结构的C语言初学者,正好拿它做综合练习。
  • 准备计算机等级考试的考生,这类题是上机题库里最高频的一类。
  • 觉得自己基础不牢、想回头查漏补缺的开发者,花30分钟把这个题目的边界想清楚,比盲目刷10道重复题目有价值得多。

我写这篇文章,不打算只丢一个标准答案了事。我想把这个题从题目分析、代码设计到调试排错,整条链路都拆开讲清楚。重点不是说“怎么写对”,而是说“怎么想才能一次写对”。

2. 先把题目真正读懂

2.1 题目到底让你干什么

原题信息很简单,就用一句话描述:统计一段给定字符串中大写英文字母(A-Z)的个数。

但“读懂题目”不是把这句话念一遍就完了。你要先问自己几个问题:

  1. 这段字符串从哪里来?
  2. 存储在哪里?
  3. 以什么方式结束?
  4. 如果里面混了数字、小写字母、空格、标点,怎么处理?
  5. 输出什么格式?

这五个问题,前三个属于输入建模,第四个属于判定规则,第五个属于输出要求。

大部分教材里的经典做法是从标准输入读一行字符,用数组存下来,然后遍历数组,逐个判断。题目本身没有限定输入规模,所以用固定大小的字符数组时要保证能装下最长输入。这牵扯到数组越界的隐患,后面我会专门讲。

2.2 核心考点拆解

这道题的考点分布比较清晰:

  • getchar函数或scanf结合循环读取字符,检查是否读到换行符或EOF。
  • 字符数组的存储与遍历。
  • 用ASCII码值或者ctype.h头文件里的isupper函数进行大写判定。
  • 计数器变量的使用与输出。

很多时候,判题系统不关心你具体用哪种方式读入,只看最终统计结果是否正确。但作为学习,你最好把每一种常见实现都写一遍,这样才能理解它们之间的差异,而不是只会背一种模板。

2.3 边界条件想清楚了吗

这是我最想强调的部分。处理输入类题目时,边界条件没想清楚,代码逻辑再巧妙也可能出错。

这个题目常见的边界场景有:

  • 空输入:直接按回车,没有输入任何可见字符。
  • 输入只有小写字母和数字,没有大写字母,此时计数器应为0。
  • 输入全是A到Z,要能正确统计总数。
  • 输入包含空格、制表符、中文标点等多字节字符时,程序不能崩溃。
  • 输入长度超过数组容量时怎么办。

一个成熟的程序员拿到题目后,不是急着敲,而是先把这些边界在脑子里列一遍,再动手写。这个习惯越早养成越好。我在实际项目里见过不少线上故障,归根结底是边界条件没处理干净,而不是主体逻辑不会写。

3. 字符与ASCII的底层关系

3.1 为什么大写字母能“比较”大小

很多初学者写判断条件时会写

if (ch >= 'A' && ch <= 'Z')

同时又疑惑:字符变量里存的是字母,怎么还能和字母比大小?

原因在于C语言里的char类型本质上是整数类型,它只占1字节,存储的是字符对应的ASCII码值。字符 'A' 的ASCII码是65,'B' 是66,一直到 'Z' 是90。小写字母是97到122。数字字符是48到57。所以字符比较大小,本质上是在比较它们的ASCII码值。

这意味着 'A' 到 'Z' 是一段连续递增的整数序列,于是判断一个字符是否为大写字母,就可以转化为判断它的ASCII码是否落在65到90这个闭区间。

3.2 可读性优先还是性能优先

判断逻辑有两种主流写法:

if (ch >= 'A' && ch <= 'Z')

if (ch >= 65 && ch <= 90)

我强烈建议你写第一种。原因很简单:代码是写给人看的,字符字面量 'A' 一眼就能看懂,65则需要查ASCII表。两种写法编译出来的机器码几乎没差别,所以没有理由牺牲可读性。

还有一个方案是使用C标准库提供的判断函数:

#include <ctype.h> if (isupper((unsigned char)ch))

isupper会基于当前语言环境的字符分类表来判断,代码意图很明确。但有个细节:使用ctype.h中的函数时,参数应该转换成unsigned char类型,否则当char类型在目标平台上有符号且字符值大于127时,可能产生未定义行为。入门阶段也许遇不到这个坑,但最好从一开始就养成规范习惯。

3.3 中文字符和多字节编码的潜在坑

如果输入字符串里出现了中文,问题就复杂了。在GBK编码下,一个汉字占两个字节,每个字节都是大于127的扩展ASCII值;在UTF-8编码下,一个汉字占三到四个字节,每个字节同样大于127。因为'A'到'Z'的ASCII码最大才90,所以这些扩展字符不会误判为大写字母。

单字节ASCII码的常规判定路径本身不会因中文字符崩溃。真正危险的是涉及数组下标计算的场景,比如把每个字节当作一个独立字符来处理,统计长度时一个汉字被算成两个或三个长度。好在本题只关心大写字母个数,所以这个坑影响不大。但如果你以后处理字符串截断、敏感词过滤,就必须把字符编码纳入考量。这些题外话,等遇到具体项目时再深入也不迟。

4. 从需求到代码的完整推导过程

4.1 先画逻辑草图,再写代码

我不建议一上来就写代码。先在纸上或脑子里把流程过一遍:

  1. 初始化计数器为0。
  2. 循环读取字符。
  3. 判断当前字符是否为大写字母。
  4. 如果是,计数器加1。
  5. 字符读取完毕后,输出计数器的值。

这个流程再细化,就到了读入方式的分岔口:是一次性把整行读入数组再统计,还是边读边统计?

两种都合理。区别在于,前者需要为数组预留空间,适合后续还要对字符串做二次处理的场景;后者不需要数组,内存占用低,更轻量。

4.2 实现一:数组存储后统一统计

#include <stdio.h> int main(void) { char str[1024]; int i; int count = 0; printf("请输入一行字符:\n"); fgets(str, sizeof(str), stdin); for (i = 0; str[i] != '\0'; i++) { if (str[i] >= 'A' && str[i] <= 'Z') { count++; } } printf("大写英文字母的个数:%d\n", count); return 0; }

这里使用了fgets读取一行字符串,它能自动在末尾补上字符串结束符'\0',函数第二个参数限定了最多读取的字符数,可以有效防止缓冲区溢出。我推荐初学者直接养成用fgets代替gets的习惯。gets函数无法限制输入长度,输入稍长就会越界写坏内存,这个函数在C11标准里已经被正式移除了。

数组容量设为1024,对本题这种练习场景完全够用。如果输入可能会超过1024个字符,就要考虑使用动态内存分配或者边读边统计的方案。

4.3 实现二:边读边统计的内存优化版本

#include <stdio.h> int main(void) { int ch; int count = 0; printf("请输入一行字符,按回车结束:\n"); while ((ch = getchar()) != '\n' && ch != EOF) { if (ch >= 'A' && ch <= 'Z') { count++; } } printf("大写英文字母的个数:%d\n", count); return 0; }

需要注意,getchar函数的返回值类型是int。为什么不是char?因为getchar在读到文件末尾时会返回EOF,EOF通常定义为-1。而char类型变量不一定能存放负数(取决于平台是否有符号),如果用char接收,会把EOF误判成某个合法字符,导致死循环或错误计数。初学者最容易在这一点上踩坑。

第二个版本的优点是不需要数组,输入再长都不会溢出,因为它一个一个读,读一个处理一个丢一个。缺点是没法在统计完后重新回溯已输入的字符,如果后续还要做别的处理,就得另想办法。

4.4 两种实现怎么选

从判题通过的角度,两者都没问题。从工程习惯的角度,看场景:

  • 如果只是单纯统计个数,用第二个版本更干净,逻辑链条短,不会数组越界。
  • 如果统计完之后还要使用原字符串(比如还要找最长连续大写字母子串),就选第一个版本。
  • 很多教材喜欢用for循环配合数组遍历来教这道题,因为它能顺带强化数组和循环的配合。这也没问题,但要记得给数组预留足够空间。

我个人的建议是:初学阶段把两个版本都写一遍。第一遍帮助建立“字符串在内存里就是连续字符数组”的认知;第二遍帮助理解流式处理的思路。两者都会在后面的学习中反复出现。

5. 扩展:统计功能怎么封装更靠谱

5.1 把逻辑提取成独立函数

如果只是应付一道练习题,main函数里全部写完也能过。但认真一点的话,我建议把统计逻辑封装成一个独立函数,这样它的可复用性会强很多。

int count_uppercase(const char *s) { int count = 0; if (s == NULL) { return 0; } while (*s != '\0') { if (*s >= 'A' && *s <= 'Z') { count++; } s++; } return count; }

在main函数里调用:

#include <stdio.h> int count_uppercase(const char *s); int main(void) { char line[1024]; printf("请输入一行字符:\n"); fgets(line, sizeof(line), stdin); printf("大写英文字母的个数:%d\n", count_uppercase(line)); return 0; }

把统计逻辑独立成函数后,你能很方便地对它做单元测试。比如写几个固定的测试用例,传入空字符串、全小写字符串、混合字符串,检查返回值是否符合预期。这是工程化思维,跟单纯写练习代码的层次不一样。

5.2 函数设计的有趣细节

上面的函数用了指针遍历字符串,while循环里判断当前字符不是'\0',同时判断它是否为大写字母。指针不断向后移动,直到遇见字符串结束符。

这个写法有个隐含好处:因为传入参数用const char *修饰,它向调用者承诺“我只读不改”。如果有人想用这个函数直接修改原字符串,编译器会警告。写库函数时,这个习惯能避免很多误改数据的问题。

如果传入的指针是NULL,函数直接返回0,不崩溃。虽然在本练习场景中不会出现NULL,但把它写成防御性代码,以后在复杂项目里复用时就能少踩一个坑。

5.3 单元测试的简易做法

没有第三方测试框架也能做。自己写一个断言风格的测试入口:

#include <assert.h> void test_count_uppercase(void) { assert(count_uppercase("") == 0); assert(count_uppercase("hello") == 0); assert(count_uppercase("HELLO") == 5); assert(count_uppercase("Hello, World 2024!") == 2); assert(count_uppercase(NULL) == 0); }

然后在main函数里调用test_count_uppercase,跑一遍不报错就说明基本逻辑正确。这个习惯学到就是赚到,以后无论做什么C项目,都能用上。

6. 我在调试这类题目时见过的经典错误

6.1 用scanf误读字符串

新手很爱这样写:

char str[100]; scanf("%s", str);

scanf的%s格式符在遇到空格、换行时会停止读入。如果用户输入"Hello World 2024",scanf("%s")只会把"Hello"读进数组,统计结果变成1而不是2。题目没有明说输入是否包含空格,但按常规理解,应统计整行输入。

我建议用fgets替代,它能完整读入一行,包括中间的空格。如果老师明确要求用scanf,那就用scanf(" %[^\n]", str)这种方式,表示读入除换行符以外的所有字符。但这玩意的写法比fgets费解,且使用不当时同样有越界风险,日常我更推荐fgets。

6.2 混淆字符0与数字0,以及结束符

'\0'是字符串结束符,ASCII码为0,它跟字符'0'(ASCII码48)完全是两回事。如果写成

for (i = 0; str[i] != '0'; i++)

那么遇到第一个字符'0'时循环就提前结束了,大写字母统计会不全。初学者常犯这个错误,检查时一脸懵。要记住,字符串遍历的结束条件是'\0',不是'0'。

6.3 数组越界读,却没有任何报错提示

如果代码写成

char str[100]; gets(str); for (int i = 0; i < 200; i++) { count++; }

输入短还好,输入一旦超过100个字符,gets就会越界写坏栈,程序可能崩溃,也可能不崩溃但行为怪异。C语言对数组越界没有运行时检查,一旦越界,改坏的数据是随机的,这种bug最折磨人。所以,要么用fgets限制长度,要么用动态分配按需扩容。

6.4 忘加大写字母区间上限

if (ch >= 'A')

这个判断会把所有ASCII码大于等于65的字符全算进去,比如小写字母a(ASCII码97)、左方括号'['(ASCII码91)等。必须同时满足小于等于'Z'才算合格的大写字母区间。

6.5 统计完忘了换行输出

在线判题系统通常要求输出结果后换行,不换行的话,结果会和下一个输出项粘在一起,造成格式错误。这道题虽然只输出一行,但养成输出换行的习惯,能避免很多格式类错误。

7. 【表格】常见问题速查与排查方法

为了方便你对照自查,我把常见问题整理成一张表。

序号现象可能原因排查思路
1输入含空格的字符串,统计数量偏少使用了scanf("%s"),读到空格就停了改用fgets,或使用正则表达式格式的scanf写法
2输入整体无大写字符,程序输出了异常大数计数器未初始化,或for循环结束条件写成'0'字符检查int count = 0这一步;打印str确认是否加载完成再统计
3程序统计结果包含了小写字母判断语句漏写了ch <= 'Z'检查if条件,确认是大写闭区间
4读入长字符串后程序崩溃或输出乱码数组越界或gets函数没有长度限制用fgets且传入sizeof(str);确认编译环境是否允许gets
5输入直接回车时,程序卡住或结果不对没有处理换行符就结束的条件while循环条件包含ch != '\n'
6读取到EOF时程序死循环用char型变量接收getchar的返回值改为int ch,比较ch != EOF

排查时记住三步走:先确认输入数据读到了什么,再确认判断逻辑覆盖了什么区间,最后确认计数器的增减路径是否唯一。

8. 这类统计题还能怎么延伸变化

学习任何题目,最好的状态不止是会在原题框架里运转,而是能看到它和别的问题之间的关联。这道统计大写字母的题目,稍加变化就能覆盖更多考点。

8.1 同时统计四种字符类别

把要求扩展一下:统计一行字符里的大写字母、小写字母、数字、其他字符各有多少个。

int upper = 0, lower = 0, digit = 0, other = 0; while ((ch = getchar()) != '\n' && ch != EOF) { if (ch >= 'A' && ch <= 'Z') { upper++; } else if (ch >= 'a' && ch <= 'z') { lower++; } else if (ch >= '0' && ch <= '9') { digit++; } else { other++; } }

这个变化考察的是多分支结构的组织能力。

8.2 按ASCII码区间统计并输出频次

统计每个字母出现的次数,而不是笼统的总数。

int freq[26] = {0}; for (i = 0; str[i] != '\0'; i++) { if (str[i] >= 'A' && str[i] <= 'Z') { freq[str[i] - 'A']++; } }

这里有一个重要技巧:用str[i] - 'A'把字母'A'到'Z'映射成数组下标0到25。不需要用switch或一长串if-else,直接用下标访问计数数组。这就是“字母转下标”的思路,以后学哈希表的前身时还会碰见。

8.3 扩展到文件中字符统计

更贴近真实工程的做法是从文件读取文本,统计大写字母个数:

FILE *fp = fopen("input.txt", "r"); if (fp == NULL) { perror("文件打开失败"); return 1; } while ((ch = fgetc(fp)) != EOF) { if (ch >= 'A' && ch <= 'Z') { count++; } } fclose(fp);

文件和标准输入在C语言里都是流的概念,处理方式几乎一致,只是数据源不同。把输入的来源抽象掉以后,函数就能复用。

8.4 反转字符串并输出,看数据处理

学习数组版本时,通常走到统计就停了,其实还可以继续练习字符串反转,把原字符串从尾部到头部输出。

for (i = len - 1; i >= 0; i--) { putchar(str[i]); }

把统计和反转一起做,就相当于把“读入存储+线性遍历+下标运算”这三个基础能力一次性练到。练习时注意保持代码可读。

8.5 找出最长的连续大写字母段

这个变化需要同时记录“当前大写连续长度”和“历史最长长度”,它们各需要一个计数器。每次遇到非大写字母时,把当前长度和历史长度做比较,再清零当前长度。如果把这个逻辑搞懂了,很多滑动窗口类题目也就有了基础感觉。

9. 关于代码风格的一个小提醒

讲到这里,我想说一个偏经验的话题:判断条件里到底该不该忽略“非大写字母”的情况?很多人的代码写成了

if (ch >= 'A' && ch <= 'Z') { count++; }

没有else分支,这本身是正确的,因为遇到非大写字母时什么都不用做,continue的含义在隐式表达。如果非要写else空语句,反而干扰阅读。

还有一类写法是:

if (ch < 'A' || ch > 'Z') { continue; } else { count++; }

逻辑等价,但多了一个continue跳转,让线性流程多了一个分支跳转。数量级很小的时候性能没差别,从可读性来看,直接用正向判断最自然:是就累加,不是就跳过。不要在没必要的地方增加分支嵌套,这在团队协作时能减少无意义的认知负担。

10. 实际做题时推荐的一种完整代码风格

很多同学问正确的代码结构应该长什么样。这里我给一个相对健壮、输出清晰、适合交作业也适合自测的版本:

#include <stdio.h> #define MAX_LINE 1024 int count_uppercase(const char *str) { int count = 0; if (str == NULL) { return 0; } for (int i = 0; str[i] != '\0'; i++) { if (str[i] >= 'A' && str[i] <= 'Z') { count++; } } return count; } int main(void) { char line[MAX_LINE]; printf("请输入一行字符串:\n"); if (fgets(line, sizeof(line), stdin) == NULL) { printf("读取输入失败\n"); return 1; } printf("大写英文字母的个数:%d\n", count_uppercase(line)); return 0; }

注意fgets成功时会返回传入的缓冲区指针,失败或读到EOF时返回NULL。判断一下这个返回值,既可以避免后续对未初始化数组的访问,也让程序更加健壮。这样的代码放在作业里,无论是人工批改还是自动评测,都可读性和正确性都站得住。

代码里的count_uppercase函数只做一件事:统计输入字符串中有多少个大写字母。main函数只负责获取输入和输出结果。把职责分离出来,主流程一目了然。以后扩展需求时(比如要输出大写字母本身),只需要改count_uppercase。

11. 针对初学者的自测清单

写完了代码,不代表任务结束,你还需要自我检查。建议至少覆盖下面这些测试用例:

  1. 输入"ABCDefgh123":预期输出4。
  2. 输入"abc":预期输出0。
  3. 输入"ABC DEF GHI":预期输出9。
  4. 输入"1234567890":预期输出0。
  5. 输入"!@#$%^":预期输出0。
  6. 输入""(直接回车):预期输出0。
  7. 输入"Z":预期输出1。
  8. 输入"AaBbCc":预期输出3。
  9. 输入长度超过1023的字符串:预期不崩溃。
  10. 输入中文字符和大小写字母混排的字符串:预期只统计大写字母。

把每种情况实际跑一遍,记录输出结果和预期是否一致。这个过程本身就是初学者最容易忽略的关键一步:验证。判题系统替你验证那是考试,平时练习时自己主动验证,才是真正把知识变成能力。

12. 我在实际教学与调试中的几点体会

最后分享一些更主观的经验。

我发现不少同学在这个题上交过一版“看似正确但经不起追问”的代码。比如他们知道大写字母范围是A到Z,知道循环遍历字符串,但如果你问“为什么str[i]能作为条件判断?”,就答不上来。这不是个别现象,而是基础知识没有串成线。所以我在前面花了大篇幅解释ASCII码和字符数组的内存布局,就是希望大家脑子里建立这条链路:字符串在内存里是一块连续的空间,存储的是每个字符的ASCII码值,C语言没有真正的字符串类型,它是靠结尾的'\0'来界定边界的。

在编写实际代码时,有一个细节我也常跟人强调:计数类变量在使用前一定初始化。这个原则听起来很基础,但真到了复杂项目里,某些计数器初始化放在条件分支里,某些分支没执行到就用了,就会产生随机结果。性能调优的时候这种bug极难定位。从这道题就开始养成“声明即初始化”的习惯,以后能省很多事。

在你熟练掌握了数组版本的统计逻辑后,建议再去试试流式版本。流式处理的核心变化在于数据集不落地的处理思维。很多同学第一次写流式处理时不太习惯,总觉得不把数据存起来就没法进行计算。其实一旦体会到“边读边算”的轻巧,遇到超大文件时就不会心慌了。

反复对照两个版本,你会发现它们处理输入的能力边界不同,内存消耗不同,代码复杂度也不同。理解不同实现方案间的权衡,才是编程能力真正进阶的标志。一道看起来只有正解或错解的小题,仔细挖掘,也能挖出这些道道来。希望这篇分析能帮你把这道题背后的知识网络铺开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 17:01:30

2026软件测试面试题最强攻略:从基础八股到测开实战

1. 先搞清楚2026年面试官到底在找什么样的人每年我都会被问到同一个问题&#xff1a;软件测试面试题到底该怎么准备&#xff1f;尤其是临近跳槽季&#xff0c;后台私信里清一色是"求2026最新面试题""有没有最强背诵版"。先说结论&#xff1a;单纯背题已经不…

作者头像 李华
网站建设 2026/9/8 17:01:20

训练1000轮损失不降?反向传播手算一遍就懂了

训练1000轮损失不降&#xff1f;反向传播手算一遍就懂了 【免费下载链接】nndl 邱锡鹏《神经网络与深度学习》第二版与通识版&#xff1a;电子书、章节目录、学习资源与勘误。 项目地址: https://gitcode.com/GitHub_Trending/nn/nndl 训练跑了 1000 轮&#xff0c;损失…

作者头像 李华
网站建设 2026/9/8 17:00:02

three js 13 光照和阴影

文章目录1 灯光的类型2 材质3 如何场景有影子3 平行光4 聚光灯5 点光源1 灯光的类型 平行光 点光 面光 无阴影 射灯 2 材质 以下材质会接受光照 MeshStandardMaterial 标准PBR材质&#xff0c;主要用这个 MeshPhysicalMaterial 高级物理材质 没用 MeshLambertMaterial 兰伯…

作者头像 李华