简介面向C语言初学者的系统入门讲义以PDF文档集中整理。C语言是操作系统、嵌入式系统、底层驱动等领域广泛使用的工业语言入门时需先打通基本语法与编程思维。内容从计算机与编程语言的关系讲起对比编译与解释两种执行方式并梳理C语言从KR到ANSI C、C95、C99的标准化历程接着围绕变量初始化、const常量、scanf函数的返回值判断、表达式与运算符优先级、赋值及递增递减运算符等细节展开通过计算最大公约数、时间差和英制单位转换等经典示例帮助读者掌握顺序程序设计、输入输出及常见计算写法。资源共1个PDF文件压缩包大小1.57MB轻量易保存适合配合大学计算机课程、MOOC或自学笔记使用。目前已有13511人学习下载零基础或刚接触C语言的同学可借此快速建立知识框架为后续学习判断、循环、数组、函数与指针等内容打好基础。1. C语言基础这份PDF资料先把语法拆成能落地的代码例子如果你已经看完几节C语言网课却发现自己还是写不出一个完整的小程序那这份C语言基础PDF资料值得用一周时间逐章过一遍。它不是一本塞满语法定义的厚手册而是从最大公约数、猜数字、正序分解整数这些经典小算法切入把变量、表达式、分支、循环、数据类型按章节串起来每个知识点后面都配了能直接编译运行的代码片段是一份典型的“边看边敲”型基础文档。最值钱的部分不是Hello World而是scanf返回值的三种情况、C89与C99在变量定义上的差异、浮点数为什么不能用比较这些容易被IDE掩盖的细节。适合刚学到指针之前、想靠练习把语法钉牢的初学者也适合带新人时当练习题库用。2. 变量、表达式与运算符优先级先弄懂内存怎么走学C语言遇到的第一个认知坎不是语法而是“变量到底是什么”。这份PDF先不讲类型表而是用英制身高换算和时间差计算这两个例子让你先把变量用起来再把类型细节放到后面章节展开。我建议你顺着它的顺序走先会写、再深挖比一上来背char/int/float取值范围高效得多。2.1 变量定义与初始化C89和C99的关键差异PDF里反复强调一句话所有变量在第一次被使用之前应该有过初始化。这不是洁癖而是C语言不会帮你把变量清零。如果你只写int count;然后直接countcount的初值取决于栈上残留的数据程序可能每次跑结果都不一样。最常见的初始化写法是定义时就给初值比如最大公约数这段#include stdio.h int main() { int u 32; int v 26; while (v ! 0) { int temp u % v; u v; v temp; } printf(gcd%d\n, u); return 0; }这里u和v在进入while之前已经初始化第一次判断v ! 0是安全的。temp在循环体内定义每轮循环都重新创建和赋值不需要在外部预先初始化。逻辑上while循环每轮都在把u、v向前滚动直到v变成0此时u保存的就是最大公约数。参数说明%d是int的输出占位符u和v分别是被除数和除数。关于变量定义位置这里藏着C语言版本差异。早期的ANSI C标准要求变量在代码块开头定义C99则允许使用前定义。现在主流编译器默认支持C99及以上所以int i 0;后面再int j 0;这种写法完全合法。但如果你遇到老项目里一堆变量堆在函数开头不用奇怪那是C89时代留下的风格。const常量也值得单独说。C99才支持const命名习惯是全大写#include stdio.h int main() { const int PRICE 3; int total PRICE * 12; printf(total%d\n, total); // PRICE 4; // 编译报错const变量不能赋值 return 0; }const的作用是让编译器帮你检查“不该被修改的量”。金币兑换、税率、屏幕宽度这类固定参数用const表达意图比裸写数字好得多。2.2 表达式与复合赋值读代码前先看优先级C语言的一条语句本质上是一个表达式由运算符和算子组成。PDF里给出了完整的运算符优先级表其中最容易让初学者翻车的是复合赋值与四则运算混在一起的情况。看这个例子#include stdio.h int main() { int total 100; total / 12 6; printf(total%d\n, total); // 输出5不是14 return 0; }这个输出可能出乎意料不是100 / 12 6 14而是100 / (12 6) 5。原因是复合赋值运算符的右侧整体是一个表达式先算126再赋值。如果你按数学直觉拆成total total / 12 6结果就完全错了。这个细节在改别人代码时特别容易踩因为视觉上total / 12 6比total total / (12 6)隐蔽得多。赋值也是运算它有返回值。a b 6之所以能连续赋值是因为赋值表达式的结果就是被赋予的值结合方向从右到左等价于a (b 6)。这也正是if (a b)能编译通过的原因它把b赋给a再判断a是否为真。新手经常把写成编译器往往只给警告不报错最后查半天发现是“赋值代替了判断”。递增递减运算符是C语言的历史遗产。当年在特定的老机器上有专门的INC、DEC指令和--能直接映射成那两条指令如今编译器已经能自动优化a a 1和--继续存在更多是习惯问题。但它们的前缀后缀差异仍然重要表达式效果表达式的值countcount加1count原来的值countcount加1count加1以后的值count--count减1count原来的值--countcount减1count减1以后的值2.3 scanf的返回值输入出错时程序不会告诉你PDF里有一块内容含金量很高scanf函数是有返回值的而且返回值分三种情况。大多数入门资料只教你scanf(%d, a)怎么写不告诉你它返回什么。#include stdio.h int main() { int a 0; int b 0; int ret scanf(%d %d, a, b); printf(ret%d, a%d, b%d\n, ret, a, b); return 0; }你可以亲手验证三组输入输入“3 4”ret2a3b4两个变量都正确读入输入“3,4”ret1a3b保持0第二个参数匹配失败输入“,3 4”ret0a和b都保持0第一个参数就匹配失败关键点是scanf匹配失败时不会修改变量的值。所以变量必须预先初始化否则“读入失败”和“读入成功但值是垃圾”会混在一起极难排查。EOF是第三种情况在stdio.h中定义通常是-1表示输入流结束。Windows下按CtrlZ再回车Linux下按CtrlD。scanf返回值含义典型场景正整数成功读取的参数个数scanf(%d %d)输入“3 4”返回20用户的输入不匹配无法正确读取任何值输入“,3 4”返回0EOF输入流已经结束Windows下CtrlZLinux下CtrlD实际写代码时我一般会把scanf的返回值作为判断条件而不是假设用户一定会按格式输入。后面猜数字程序里这个知识点会直接决定程序是正常提示还是死循环。3. 分支与循环从看懂if到选对循环结构第三章是这份PDF篇幅最重的部分也是初学者从“看得懂代码”到“写得出代码”的分水岭。分支和循环本身不难难的是在不同场景里选对写法什么时候用if-else if什么时候用switch什么时候用for什么时候用do-while。3.1 分支写法从多出口改成单一出口if语句的基本形态是if后面跟一个括号里的逻辑表达式如果结果非零就执行后面大括号里的语句。注意if这一行结束时没有分号这是和普通语句最大的视觉差异。PDF里有一个容易被忽略的编程习惯建议更支持单一出口方式。对比下面两种写法// 多出口写法每个分支里都直接printf if (x 0) { printf(%d, -1); } else if (x 0) { printf(%d, 0); } else { printf(%d, 2 * x); } // 单一出口写法先算结果最后统一输出 int f; if (x 0) { f -1; } else if (x 0) { f 0; } else { f 2 * x; } printf(%d, f);单一出口的好处是逻辑集中f的计算过程全在if-else里输出只有最后一行。调试时你只需要在printf那行打断点就能看到所有分支的最终结果。多出口写法一旦分支变多每个分支里都夹着输出语句排查时要在好几个printf之间跳来跳去。switch-case和if-else是另一对需要选型的组合。switch的控制表达式只能是整数型结果case后面跟的是常量可以是常数也可以是const常量表达式。PDF里那个穿透例子很典型switch (type) { case 1: case 2: printf(你好\n); break; case 3: printf(晚上好\n); case 4: printf(再见\n); break; default: printf(啊什么啊\n); break; }case 1和case 2之间没有break所以type1时会一路穿透到case 2的printf。执行完case 3的printf后没有break会继续执行case 4的printf输出“晚上好再见”两行。这就是switch的穿透特性case只是路标不是边界。如果遇到分段函数我建议尽量用if-else而不是switch。switch(x0)虽然能编译case 1和case 0也能跑但可读性很差。switch更适合“值等于什么就做什么”的离散场景区间判断天生属于if。3.2 三种循环的选型固定次数、至少一次和未知次数C语言有三种循环while、do-while、for。PDF给出的选择规则很实用如果有固定次数用for如果必须执行一次用do-while其他情况用while。for循环像计数循环包含初始化、条件和每轮动作三个部分而且每个表达式都可以省略。for (; 条件;)等价于while (条件)。一个容易忽略的细节是for循环里定义的变量只在循环内有效#include stdio.h int main() { int n; scanf(%d, n); int fact 1; for (int i 1; i n; i) { fact * i; } // printf(%d\n, i); // 编译报错i在循环外不可见 printf(%d!%d\n, n, fact); return 0; }这个特性来自C99标准老标准里for的循环变量定义必须在外部完成。如果循环结束后你还需要访问计数变量就老老实实把变量定义在for外面改用while。这是内存作用域带来的约束不是编译器故意为难你。do-while的适用场景是“至少执行一次”。猜数字游戏就是典型用户至少要猜一次不管猜得对不对。而求平均值读到-1结束循环次数取决于输入用while最自然。还有一个经典陷阱必须记for (i 0; i 5; i); 分号让循环体变成空语句程序会空转5次什么都不做。如果你在分号后面跟了printf它会在循环结束后只执行一次初学者常被这个坑困扰。3.3 综合练习猜数字与求平均数里的输入校验把前面scanf返回值的知识用起来的时机到了。猜数字程序如果只写scanf(%d, a)而不检查返回值用户输入一个字母时a保持旧值程序会陷入“永远猜不对”的死循环。#include stdio.h #include stdlib.h #include time.h int main() { srand(time(0)); int number rand() % 100 1; int count 0; int a 0; printf(我已经想好了一个1到100之间的数。\n); do { printf(请猜测); int ret scanf(%d, a); if (ret ! 1) { while (getchar() ! \n); // 清空输入缓冲 printf(输入无效请输入整数。\n); continue; } count; if (a number) { printf(你猜的数大了。\n); } else if (a number) { printf(你猜的数小了。\n); } } while (a ! number); printf(太好了你用了%d次猜到了答案。\n, count); return 0; }这里srand(time(0))用当前时间做随机种子rand() % 100 1生成1到100之间的数。如果把1去掉范围会变成0到99这是常见偏差。ret ! 1判断输入失败while (getchar() ! \n)清空这一行的残留字符continue跳过统计次数直接进入下一轮猜测。求平均值程序则展示了另一种输入终止方式#include stdio.h int main() { int number; int sum 0; int count 0; while (scanf(%d, number) ! EOF) { if (number -1) { break; } sum number; count; } printf(%f\n, 1.0 * sum / count); return 0; }1.0 * sum把整数提升为浮点数避免整数除法直接砍掉小数。如果你写成sum / count两个int相除结果还是int9除以2会得到4而不是4.5。4. 循环控制与整数分解算法边界条件比算法本身更磨人循环写多了你会发现真正让你调一晚上的往往不是算法思路而是边界条件循环里t 9还是t 9for里i min还是i min。这一章把这些边界和两个经典整数算法放在一起讲透。4.1 整数正序分解mask的构造与循环边界逆序分解整数很简单模10取个位、除10去掉个位。但正序分解麻烦你得先知道这个数最高位是多少。PDF给的做法是构造一个mask也就是10的“位数-1”次方。#include stdio.h int main() { int x; scanf(%d, x); int mask 1; int t x; while (t 9) { t / 10; mask * 10; } // 此时mask是和x位数匹配的最高位权重 do { int d x / mask; printf(%d, d); if (mask 9) { printf( ); } x % mask; mask / 10; } while (mask 0); printf(\n); return 0; }边界条件是这一段的关键。while (t 9)的意思是只要t还有两位及以上就除以10并让mask乘10。以x700为例t从700变70再变7循环时mask从1变10再变100最终mask100。do循环第一轮d 700 / 100 7打印7x变成0mask变成10第二轮d 0 / 10 0打印0x还是0mask变成1第三轮d 0 / 1 0打印0。所以700的输出是“7 0 0”。测试x0会怎样while条件不成立mask保持1do循环执行一次打印0效果正确。负数的情况则需要注意比如x-123d -123 / 100 -1输出带负号且位数分解会乱通常正序分解器只处理非负整数。调试时可以加一行printf(x%d, mask%d\n, x, mask);每轮循环都能看到mask的收缩过程。4.2 最大公约数两种算法枚举与辗转相除的取舍PDF在第一章和第五章两次提到最大公约数第一次用来讲算法思想第二次给出完整代码。枚举法贴合定义但有个边界坑#include stdio.h int main() { int a, b; int min; scanf(%d %d, a, b); if (a b) { min a; } else { min b; } int ret 0; for (int i 1; i min; i) { if (a % i 0 b % i 0) { ret i; } } printf(%d\n, ret); return 0; }注意for循环条件是i min这意味着i根本不会取到min本身。当a4、b4时min4循环跑到i3就停了ret只能记到2输出2而不是4。这个bug在讲义代码里真实存在照抄会出错。把条件改成i min才严谨。枚举法的缺点是循环次数接近min而且ret从0开始累加逻辑上不如辗转相除干净。辗转相除法才是工程上该用的#include stdio.h int main() { int a, b; int t; scanf(%d %d, a, b); while (b ! 0) { t a % b; a b; b t; printf(a%d, b%d, t%d\n, a, b, t); } printf(gcd%d\n, a); return 0; }循环结束时b0a就是最大公约数。同样的a32、b26枚举可能要循环二十几次辗转相除只要几次。调试时那行printf能清楚看到每次迭代中a、b、t三个值如何滚动a变成原来的bb变成余数直到余数为0。4.3 接力break与goto三重循环怎么跳出换零钱问题是经典的“在多重循环里提前退出”场景。假设要凑出x元的零钱分别用1角、2角、5角的硬币三重循环找出第一组解就停#include stdio.h int main() { int x; scanf(%d, x); int exit 0; for (int one 1; one x * 10; one) { for (int two 1; two x * 10 / 2; two) { for (int five 1; five x * 10 / 5; five) { if (one two * 2 five * 5 x * 10) { printf(可以用%d个1角加%d个2角加%d个5角得到%d元\n, one, two, five, x); exit 1; break; } } if (exit 1) break; } if (exit 1) break; } return 0; }one、two、five分别表示1角、2角、5角硬币的个数x * 10把元换算成角。三层循环每层都检查exit标记一旦内层找到解就一层一层break出来。这种方式可读性好但代码里要写三个一模一样的if (exit 1) break很啰嗦。讲义里还给了goto版本for (int one 1; one x * 10; one) { for (int two 1; two x * 10 / 2; two) { for (int five 1; five x * 10 / 5; five) { if (one two * 2 five * 5 x * 10) { printf(可以用%d个1角加%d个2角加%d个5角得到%d元\n, one, two, five, x); goto out; } } } } out: return 0;goto能直接跳出所有循环代码最短。代价是标号out让控制流不再线性别人读代码时要跳来跳去找标号团队规范里经常禁用它。我的习惯是先写接力break只在一次性脚本或者三层以上的嵌套里才考虑goto。另外这段讲义代码有一个真实排版错误if条件末尾多了一个右括号写成x*10))照抄会编译失败需要自己修掉。5. 数据类型陷阱排查整数溢出、浮点比较和字符输入的四类翻车现场语法和循环掌握之后真正的考验是数据类型边界。这一章是排查经验合集每一条都是能直接在代码里复现的翻车现场建议对照着在自己的机器上跑一遍。5.1 整数范围与溢出char做不了大循环变量现象把循环变量定义成char循环条件i 200程序陷入死循环。#include stdio.h int main() { char i 0; while (i 200) { printf(%d\n, i); i i 1; } return 0; }原因有符号char只有1字节取值范围-128到127。i从127再加1时发生溢出结果变成-128于是i永远小于200循环无法退出。现代CPU字长普遍是32位或64位用更短的整数类型不但不会更快反而可能因为内存对齐而更慢。解决循环变量用int。除非你在做底层协议解析需要严格按字节处理否则int是第一选择。补码概念在这里要真正理解。-1的补码是11111111与1相加会溢出得到0这就是补码能把负数的减法变成加法运算的原因。unsigned类型不是为了扩大正数范围而是为了做纯二进制运算尤其是移位。5.2 浮点比较0.1加0.2不等于0.3现象float a 1.345f; float b 1.123f; a b的结果不等于2.468甚至两个浮点数比较f1 f1都可能返回假。#include stdio.h #include math.h int main() { double a 0.1; double b 0.2; double c a b; printf(c%.17f\n, c); if (fabs(c - 0.3) 1e-12) { printf(相等\n); } else { printf(不相等\n); } return 0; }原因二进制浮点数无法精确表示0.1这类十进制小数0.1和0.2在机器里都是近似值相加后误差累积与0.3的比较自然失败。printf用%.17f打印double能看到小数点后第17位附近的误差。解决不要用判断浮点相等用fabs(c - 0.3) 1e-12这种绝对值误差判断。fabs是绝对值函数需要#include math.h并且在Linux下用gcc编译时要加-lm参数否则链接报错。如果你在算钱直接用整数以分为单位别让浮点误差掺和进金额计算。PDF里那句“计算器不用浮点计算”说的就是这事。5.3 字符输入与EOFC的字符是整数输入流判断要看返回值现象一程序里用scanf(%c, c)读字符前面刚执行过scanf(%d, n)结果字符根本没读进去。原因前一个scanf(%d)把数字读走后输入缓冲区里还残留着换行符scanf(%c)把这个换行符当成目标字符读走了。解决在格式串里加一个空格写成scanf( %c, c)。前导空格会让scanf跳过空白字符。现象二Windows下按CtrlZ程序没结束要按两次回车Linux下不知道该怎么结束输入。原因scanf遇到EOF时返回EOF常量在stdio.h里定义通常为-1。Windows输入CtrlZ后需要再回车一次让输入流刷新Linux下用CtrlD。如果你只判断变量值不判断返回值程序可能把EOF当成普通数处理。解决循环用返回值收尾#include stdio.h int main() { int num; while (scanf(%d, num) ! EOF) { // 处理num } return 0; }另一个相关认知char是整数字符1和数字1不是一回事。1的ASCII编码是49所以char c 1时printf(%d, c)输出49printf(%c, c)输出字符1。大小写字母转换利用ASCII表顺序c a - A可以把大写变成小写。5.4 强制类型转换优先级转换符放错位置就是灾难现象想算a除以b的浮点结果写成int i (int)(a / b)得到的是0写成double d (double)(a / b)得到的还是0.0。#include stdio.h int main() { int a 5; int b 6; double d1 (double)(a / b); // 先整数除得0再转double输出0.000000 double d2 (double)a / b; // 先把a转double再除法输出0.833333 printf(d1%f d2%f\n, d1, d2); return 0; }原因强制类型转换的优先级高于四则运算。如果转换成int负数的截断方向是向零取整如果转换整个除法表达式运算顺序是先整数相除再转double精度早已丢失。解决正确做法是只转换其中一个操作数(double)a / b这样整数除法被提升为浮点除法。还要注意转换本身不改变原变量double x 10.6; int y (int)x;执行后x仍然是10.6y是10只是从x那里算出了一个新值。如果你想修改原变量必须显式赋值回去。6. 把这份讲义变成你的调试习惯printf断点、编译三步和回归清单6.1 用printf当临时断点调试循环边界最有效的方式不是单步跟踪而是在关键位置加printf。正序分解整数程序里加一行printf(x%d, mask%d\n, x, mask);每轮循环的mask收缩过程清清楚楚。猜数字程序里加printf(debug: number%d\n, number);你就能看到程序“想”的数字和你猜的关系。发现问题后这行代码删掉即可printf是最便宜的调试工具。6.2 编译三步与回归清单拿到一份PDF资料后别只读代码按三步走gcc -Wall -o guess guess.c ./guess第一步用gcc加-Wall编译让编译器把所有可疑警告亮出来。第二步设计三组输入一组正常值、一组边界值、一组非法值。猜数字试1、100和非数字字符scanf试“3 4”和“3,4”正序分解试700和0。第三步改完一个条件后把旧代码注释掉而不是直接删掉重新编译跑一遍对比行为变化确认新改动没有破坏边界情况。我最初学C语言时习惯“人脑编译”盯着代码觉得逻辑没问题就跳过运行。后来帮一位同学调猜数字程序他把scanf(%d, a)写成了scanf(%f, a)printf里却依然用%d编译器只给警告不报错运行结果全乱他查了一晚上才发现输入格式和输出格式不匹配。从那以后我每拿到一份这样的基础资料都会强制自己把核心代码从头到尾敲一遍再改掉至少一个边界条件用命令行重新编译跑一次才敢说真的看懂了。这份PDF也一样适合下载到本地当手边速查手册从第二章开始逐块对照敲代码把每个坑亲手踩一遍再跨过去。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?