
简介C语言.pdf 是一份系统讲解 C 语言入门知识的讲义适合零基础或刚接触编程的读者定位于帮助初学者建立编程思维并理清学习路径。内容从“什么是编程语言”这一基础问题切入逐步展开到 C 语言的历史特点、与 C 的关系、学习难度与就业方向并专门讲解进制转换、内存存储、字符编码等底层概念为后续理解指针和内存操作铺路。整个资源为一个 PDF 文档压缩包约 11.19MB目录覆盖编程基础到 C 语言初探等章节结构清晰既能系统性学习也方便按需查阅。目前已有 192 人学习/下载。通过这份讲义读者可以少走弯路快速建立 C 语言的知识框架既能理解计算机如何存储和处理信息也为日后从事系统编程、嵌入式开发或进一步学习 C 打下扎实基础。1. 这份 C 语言讲义是什么从进制到函数一份能照着走完的入门路线图C 语言一直是编程圈里争议最大的入门语言有人说它指针难啃、有人嫌它语法啰嗦但真正写过嵌入式、啃过操作系统源码的人都会承认C 语言是菜鸟和大神的分水岭。这份标题就叫“c语言.pdf”的讲义其实不是一本薄薄的小册子翻目录就能看到它从“什么是编程语言”一路铺到“函数、数组、字符串处理”总共 18 章的结构里几乎覆盖了初学者前三个月要碰的所有知识点。我下载之后通读了一遍发现它的内容组织方式很适合自学——每章用大白话开头再逐步落到代码层面不像很多教程一上来就扔语法规则。如果你正打算入门 C 语言或者学了一半卡在缓冲区、数组越界这类问题上这份讲义值得你从头到尾过一遍。2. 先把三块地基打牢进制转换、字符编码与内存存储2.1 进制转换不用背公式理解权值展开就行讲义的 1.9 到 1.11 节集中讲了二进制、八进制、十六进制以及数据在内存中的二进制存储。很多初学者觉得进制转换就是应付考试的实际写代码根本用不到这个想法在 C 语言里站不住脚。你后面学到位运算、看内存地址、调试嵌入式程序时十六进制几乎是唯一的人类可读格式。比如你在调试器里看到一个变量的值是0x7ffe如果脑子里不能快速反应出这和十进制 32766 的关系排错速度会慢很多。常见的做法是记住一个原则任何进制转十进制就是把每一位的数字乘以该进制的位置权重再求和。反过来十进制转二进制就是不断除以 2 取余数。讲义里给的例题和表格很适合跟着手算一遍但我的建议是别满足于会算要会用代码验证#include stdio.h // 将一个十进制数按指定进制输出为字符串支持2/8/16进制 void convert(unsigned int num, int base) { char buf[32]; // 32位int最多31个二进制位加结束符够用 int index 0; if (num 0) { // 边界情况0在任何进制下都是0 buf[index] 0; } while (num 0) { int remainder num % base; buf[index] (remainder 10) ? (0 remainder) : (A remainder - 10); num / base; } for (int i index - 1; i 0; i--) { // 逆序输出 putchar(buf[i]); } putchar(\n); } int main() { convert(255, 2); // 输出 11111111 convert(255, 8); // 输出 377 convert(255, 16); // 输出 FF return 0; }这段代码的原理就是“除基取余法”num % base取出当前最低位的数字num / base把数字右移一位最后逆序输出得到完整结果。base参数可以换成 2、8、16 甚至任意进制逻辑不变。需要注意的是字符数组buf的大小32 位unsigned int最大才 32 个二进制位所以 32 字节足够但如果你处理的是 64 位整数这里一定要扩容否则会越界。手动转换和代码验证配合着来进制这块就能真正过关而不是停留在“会做作业题”的层面。2.2 字符编码ASCII、GBK 和 Unicode别让中文变成乱码讲义用了一整节讲 ASCII、GB2312/GBK 和 Unicode这在很多入门教程里是被一笔带过的。但 C 语言里字符编码的坑几乎每个初学者都会踩一次。C 语言标准本身不规定源代码用什么编码编译器默认按系统区域设置解析这就导致同一个源文件在中文 Windows 上编译和在 Linux 上编译字符串里的中文可能表现完全不同。核心要理清的是三件事ASCII 是 7 位编码只覆盖英文字母、数字和符号GBK 是中文编码用两个字节表示一个汉字兼容 ASCIIUnicode 是字符集UTF-8 是它在存储和传输中的一种编码方案汉字占 3 个字节。C 语言的char类型只有一个字节所以直接用char数组存中文字符串时每个汉字会占两个或三个数组元素这就引出了后面字符串处理函数的很多隐蔽问题。讲义里强调“谁说 C 语言使用 ASCII 码”是误解就是想纠正这一点——C 语言的char只是 8 位整数具体解释成什么字符由运行环境的编码决定。这里给一个验证编码行为的例子#include stdio.h #include string.h int main() { char str[] 你好C; printf(字符串长度: %zu\n, strlen(str)); // sizeof(str)-1含结束符 printf(字节内容: ); for (size_t i 0; i strlen(str); i) { printf(%02X , (unsigned char)str[i]); } printf(\n); return 0; }如果在 UTF-8 环境下运行strlen的结果是 7——“你”占 3 字节、“好”占 3 字节、C占 1 字节再加上结束符\0总共 8 字节存储。这个例子能直观说明sizeof和strlen的差异根源就在编码。我用%02X打印每个字节时特意把str[i]强制转换成unsigned char是因为char在部分平台是有符号的直接以%X打印一个负值字节会出现FFFFFFXX这种多余前缀这属于另一个容易混淆的细节。3. 搭环境、写第一个程序编译器、IDE 和编译链接的本质3.1 编译器怎么选别盲目追求新版标准讲义第 2 章花了很大篇幅讲编译器选型和 IDE 选择核心结论是初学者不要纠结 GCC、Clang、MSVC 哪个更强先选一个能稳定跑通的。C 语言标准从 C89 到 C99 再到 C11看起来更新了很多但实操中很多教学代码仍然按 C89 的风格写——变量声明集中在函数开头。新标准允许在for循环里直接声明变量但如果你用的是老式编译器或者某些教学平台的在线编译器这种写法会直接报编译错误。我见过的翻车案例是某同学把教材里的 C99 代码粘贴到学校机房的老编译器里满屏 declaration error然后误以为自己代码写错了。我的建议是本地装一个 GCC 就好版本不需要追新能用-stdc11参数就够了。Windows 用户装 MinGW-w64macOS 用户用自带的 ClangLinux 用户系统自带的 GCC 基本都能用。CN 的初学者还会遇到另一个坑手机 IDE。讲义专门有一节讲手机编程但我的真实体验是手机写代码当玩具可以正式学习还是得用电脑——屏幕太小、编译速度慢、调试功能几乎为零尤其是后面学到指针和内存相关的内容时没有调试器几乎寸步难行。验证环境是否正常第一行代码跑通是最有效的测试#include stdio.h int main(void) { printf(Hello, C Language\n); return 0; }这段代码里的printf是标准库提供格式化输出的函数。\n是换行符它把光标移到下一行让输出看起来更整齐。main函数必须返回int这是 C 标准的规定虽然有些老编译器允许你写void main()但那是非标准行为移植到其他平台可能编译失败。在命令行里运行gcc hello.c -o hello再执行./hello如果能看到输出说明你的环境是完备的。3.2 源文件到可执行文件编译和链接到底是两道工序讲义把编译和链接分开讲我觉得这是它做得最好的部分之一。很多自学者直接用 IDE 一键运行完全不理解背后的过程导致后面学多文件项目时一头雾水。编译和链接其实是两个独立阶段编译阶段把.c源文件翻译成目标文件.o或.obj链接阶段把多个目标文件和库文件合并成可执行文件。编译阶段要经过预处理、编译、汇编三个步骤。预处理处理#include、#define这些指令编译把预处理后的代码翻译成汇编指令汇编再把汇编指令转成机器码。链接阶段最常遇到的问题就是“undefined reference”通俗地说就是编译器在某个目标文件里找不到它引用的函数定义。比如你写了自己的函数声明但忘记实现它或者实现写在了另一个文件里却没有参与编译链接器就会报这个错。我不建议你直接去背编译原理但对一个概念要有直观认识编译错误和链接错误是两回事。编译错误通常是语法层面的比如少写一个分号错误提示会给出文件和行号照着改就行。链接错误则不会告诉你具体哪一行只告诉你哪个符号找不到这种问题新手往往摸不着头脑。遇到后者先检查是不是漏了文件再检查函数名拼写是否一致——C 语言区分大小写Print和print是两个完全不同的函数。4. 输入输出与缓冲区入门阶段最常见的翻车现场4.1 scanf 的行为逻辑读的是格式化数据不是读“一行”讲义的 4.2 到 4.8 节集中讲输入输出其中缓冲区相关内容是全书信息量最大的部分之一。很多初学者对scanf的理解是“从键盘读一个值”这个理解太粗糙了。scanf真正做的是从标准输入流中按格式匹配字符序列遇到空白符空格、换行、Tab时按格式要求跳过或终止然后把匹配结果写入变量地址。一个典型困惑是为什么我输入了一个整数之后再输入一个字符程序表现就很奇怪看下面的代码#include stdio.h int main() { int num; char ch; printf(请输入一个整数: ); scanf(%d, num); printf(请输入一个字符: ); scanf(%c, ch); printf(num%d, ch%d\n, num, ch); return 0; }运行后你输入5再按回车程序可能没等你输入字符就输出了结果而且ch的值是 10换行符的 ASCII 码。原因在于第一次scanf(%d)读到5就停止了但回车键产生的\n还留在缓冲区里第二次scanf(%c)直接读到了这个残留的换行符根本没给你输入的机会。解决的常见手法是在格式串里加空格让scanf跳过空白符scanf( %c, ch); // 注意 %c 前面有一个空格这个空格告诉scanf跳过一个或多个空白字符再读取下一个非空白字符。给%c加空格本质是把缓冲区的残留换行“吃掉”。这个知识点如果没搞懂后面学循环、学字符处理时会反复踩同一个坑。4.2 缓冲区解决输入输出疑难杂症的总钥匙讲义的 4.5 到 4.7 节花了三节篇幅讲缓冲区这个投入非常有必要。缓冲区从本质上说就是一块内存区域数据先写到缓冲区里再在特定时机刷到目的地。键盘输入会先进入输入缓冲区scanf从缓冲区取数据printf输出则先进入输出缓冲区遇到换行或程序结束时才刷新到屏幕。搞清楚缓冲区之后很多“诡异”行为就有了解释比如while (getchar() ! \n);这段代码很多人第一次看到时不明白为什么要写一个这样的空循环。它的作用是清空输入缓冲区里剩余的字符直到读到换行符为止。在混合读取数字和字符串的时候这是一个非常实用的防御手段#include stdio.h int main() { int age; char name[50]; printf(输入年龄: ); scanf(%d, age); while (getchar() ! \n); // 清空缓冲区吃掉残留的换行 printf(输入姓名: ); fgets(name, sizeof(name), stdin); // 用fgets读一行字符串 printf(年龄: %d, 姓名: %s\n, age, name); return 0; }这里用fgets替代scanf(%s)是因为fgets会限制读取长度防止缓冲区溢出这是安全性的一个基本习惯。while (getchar() ! \n);这行的原理就是不断从缓冲区读一个字符只要不是换行就继续读直到把上次输入残留的换行也读掉。注意这个循环如果放在文件末尾且文件结尾没有换行符可能会造成死循环但在交互式输入场景下基本没问题。讲义的 4.9 和 4.10 节会提到模拟密码输入和非阻塞式键盘监听这些属于进阶玩法会涉及getch()、_kbhit()这类非标准函数Windows 和 Linux 的实现方式不同。我的建议是第一遍读的时候知道有这回事就行等把基础语法掌握之后再回来试不然容易被非标准函数干扰。5. 避坑笔记循环、数组、字符串这几个坎怎么过5.1 内存溢出数组越界不报错才是最危险的事很多初学者写 C 语言时会遇到一个奇怪的现象数组越界了程序竟然还能正常跑甚至输出了奇怪的值。比如定义int a[5]你去访问a[5]编译器不报错、运行时不崩溃看起来一切正常。这其实是最危险的一种情况——C 语言不检查数组边界访问越界位置时它只是在按内存地址去取值那个地址上恰好存了一个无关紧要的值所以程序继续运行。等到程序最终崩溃或者输出异常时错误已经发生在很远的地方了。这种隐蔽性让越界成为 C 语言最难排查的问题之一。讲义里专门有一节讲“数组的越界和溢出”强调的就是这个点。我建议你做一个实验来建立直觉#include stdio.h int main() { int a[5] {10, 20, 30, 40, 50}; int b 100; printf(a[5] %d\n, a[5]); // 越界访问a数组之后的4个字节 printf(b %d\n, b); return 0; }在我测试的多数编译器上a[5]恰好取了b的值或者栈上其他变量的值输出结果变成了 100 或一个随机数。这说明越界访问会读取到相邻变量甚至栈上的垃圾数据。要避免这种问题除了自己小心还可以养成两个习惯第一凡是循环遍历数组循环条件里写数组长度而不是写死数字第二涉及字符串操作时优先使用strncpy、snprintf这类带长度限制的函数而不是strcpy、sprintf。现代编译器也提供了防御手段。GCC 和 Clang 都支持-fsanitizeaddress编译选项它在运行时检测越界访问、释放后使用等内存错误。排查越界类问题第一件事就是加这个参数重新编译gcc -fsanitizeaddress -g program.c -o program ./program加了 AddressSanitizer 之后越界访问会立刻报错并给出访问地址、分配地址和栈回溯信息定位问题会轻松一个量级。我的习惯是写完一段涉及数组或指针的代码先带着-fsanitizeaddress编译跑一遍测试用例通过了再拿掉这个参数做正式编译。5.2 字符数组与字符串不是所有 char 数组都是字符串C 语言的字符串本质上就是以\0结尾的字符数组。这个概念几乎每个教材都会讲但初学者仍然会混淆两个场景一个是char str[] hello;另一个是char str[] {h, e, l, l, o};。前者自动在末尾加了一个\0所以它是一个真正的字符串后者只是一个字符数组没有结束符。用strlen去计算后者的长度结果是未定义的因为strlen会一直向后找\0直到遇到内存中的某个零字节为止。这个未定义行为可能导致输出一个很离谱的数字甚至造成段错误。看这段代码#include stdio.h #include string.h int main() { char s1[] hello; char s2[] {h, e, l, l, o}; printf(s1 length: %zu\n, strlen(s1)); // 正常输出5 printf(s2 length: %zu\n, strlen(s2)); // 未定义行为结果随机 return 0; }为什么s2的长度不稳定因为s2没有\0结尾strlen会继续扫描 s2 在栈上之后的内存直到找到一个值为零的字节。这个位置取决于编译器栈布局、其他局部变量内容所以每次运行结果可能都不一样。解决方法是手动补上结束符char s2[] {h, e, l, l, o, \0};。还有一个我经常看到的误用用strcpy往一个长度不够的字符数组里拷贝长字符串。strcpy不做边界检查但它仍然会把完整字符串连同\0一起写入目标地址后果就是覆盖掉目标数组之后的内存破坏其他变量或返回地址。所以现代 C 编程的实践里能替换strcpy的地方我都换成了strncpy或显式指定长度即使多写几行代码也比内存被破坏后花几天排查要好。5.3 自增自减与求值顺序同一行代码不同编译器结果不同自增和自减--是 C 语言里语法糖最多、也是最好展示“未定义行为”的地方。i i这种表达式在 C 标准里属于未定义行为——同一个变量在同一表达式中被多次修改中间没有序列点。不同编译器可能算出完全不同的结果甚至在同一个编译器不同优化级别下结果都不同。讲义里讲自增自减时给了一个建议不要在复杂表达式里混用自增自减把每一步拆开写。这个建议看起来很笨但能省掉很多玄学排错时间。比如一个常见需求把数组元素往后挪给新元素腾位子。新手会想用a[i] a[i]这种写法但两边的i求值顺序是不确定的结果可能不是你预想的。我的做法是先取旧值再让指针移动#include stdio.h void shift_right(int arr[], int len, int pos) { for (int i len - 1; i pos; i--) { arr[i] arr[i - 1]; // 先把前一个元素复制到当前位置 } } int main() { int a[5] {1, 2, 3, 4, 5}; shift_right(a, 5, 1); // 把2之后的所有元素向右移动一位 a[1] 99; for (int i 0; i 5; i) { printf(%d , a[i]); // 输出 1 99 2 3 4 } return 0; }shift_right函数的逻辑是从后往前遍历防止先移动前面的元素把后面的覆盖掉。如果从前往后移动会出现元素被覆盖丢失的问题。这里的循环条件和赋值语句都要注意边界i pos而不是i pos因为最后一个要移动的位置是pos 1。这个例子同时也演示了为什么数组插入操作成本高——每插入一个元素都要移动后续所有元素时间复杂度是 O(n)这也是后面学链表数据结构时的重要对比点。5.4 整数溢出与类型转换隐式转换带来的精度丢失C 语言的数据类型转换分自动和强制两种。自动转换发生在不同类型的数据参与运算时编译器会把表达式中所有操作数统一为一种类型规则是“向精度更高的类型看齐”。但这个规则有两个常见陷阱。第一个陷阱是整数除法的截断int a 7 / 2;得到的结果是 3 而不是 3.5。两个整数相除结果还是整数小数部分直接丢弃。很多新手写平均值时踩这个坑明明算出来应该是 87.5输出却成了 87。修正方法很简单把其中一个操作数转成浮点#include stdio.h int main() { int sum 175, count 2; float avg (float)sum / count; // 先把sum转成float除法按浮点运算 printf(avg %.1f\n, avg); // 输出 87.5 return 0; }第二个陷阱是隐式转换导致的无符号数溢出。unsigned int和int混用时int会被自动转成unsigned int。如果int是负数转换后的结果会变成一个很大的正数参与比较时结果完全超出直觉。经典的例子是unsigned int a 1; a - 2;的结果不是 -1而是一个接近 4294967295 的大数。写循环条件时尤其要注意用unsigned控制递减循环i 0永远成立循环变死循环。5.5 循环嵌套与 break 陷阱只看代码好像对跑起来却不对break只能跳出最近的一层循环或switch这个规则很多人知道但一到嵌套循环里就犯迷糊。比如你要在一个二维数组里查找某个值找到后想提前结束整个双层循环直接在里层循环写break只能跳出里层外层还会继续结果就是你明明找到了值程序却还在跑甚至多次输出“找到了”。常见的解法是用一个标志变量控制外层循环或者直接使用goto——虽然goto名声不好但在“跳出多层循环”这个特定场景里它是各标准里最干净的做法。不过 C 语言的goto只能在函数内部跳转而且不能跳过变长数组的定义使用时要注意位置。我给一个标志变量方案的示例#include stdio.h int main() { int matrix[3][3] {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int target 5; int found 0; // 0表示未找到 for (int i 0; i 3 !found; i) { for (int j 0; j 3; j) { if (matrix[i][j] target) { printf(找到 %d位置 (%d, %d)\n, target, i, j); found 1; // 置1后外层循环条件判断为假循环结束 break; // 跳出内层循环 } } } return 0; }注意外层循环的条件里写了!found这样一旦内层找到目标并设置标志外层下一次迭代就会因条件不满足而终止。相比在循环外再判断一次found然后强行break这个方法更简洁也不会出现“循环已经结束但代码还在执行多余逻辑”的情况。6. 看完讲义后的下一步用三个技巧把知识变成可运行的能力读完这份讲义不等于学会 C 语言中间隔着的就是动手量。我给三个学完基础章节之后可以立刻执行的动作每个都比“再看一遍书”有效得多。第一个动作是回头重写讲义的每个示例但不看原文。讲义 6.10 节的冒泡排序是经典的练手题目很多人一眼能看懂排序逻辑合上书自己写却写不出来。关键在于排序里有两种循环边界外层控制轮数、内层控制每轮的比较次数。写错边界的典型结果是排序后首尾元素不对或者越界访问。建议你至少独立写三遍第一遍照着思路写第二遍不看参考代码写第三遍换一种写法比如从后往前冒泡把算法逻辑内化成自己的。第二个动作是给讲义里的代码加防御性检查。凡是数组操作先检查索引是否越界凡是涉及字符串拷贝先确认目标空间长度足够凡是写循环先考虑边界条件空数组、单元素数组、满数组。这些检查看似啰嗦却是从“能跑”到“能上生产”的关键分水岭。我见过太多代码功能对但只在特定输入下对输入一变就翻车。把防御性编码变成肌肉记忆后面写项目会省很多事。第三个动作是给自己找一个 300 行以内的小目标项目。讲义内容覆盖了变量、循环、数组、函数、输入输出刚好可以做一个命令行版的简单记账程序。功能不需要多能用循环接收输入、用数组存数据、用函数封装逻辑、用 printf 格式化输出账目。这个项目会强迫你把零散的知识点串成完整的逻辑链条遇到问题再去回翻讲义对应章节——这种“按需回查”的学习方式比从头再读一遍效率高得多。回过头来看这份讲义它的价值在于把概念讲得足够“软”。进制转换不是让你背公式而是让你看到权值展开的本质缓冲区不是玄学而是有章可循的数据流动过程编译链接不是黑匣子而是两条分明的流水线。但你如果只读不做这些解释就只是文字。从我的经验来看C 语言入门最大的瓶颈从来不是智商而是看得懂、写不出的假熟练。从那以后我每次推荐这份讲义给新手都强制要求他们先跑通示例代码、再做改版练习、最后提交一个小程序才算真正读完这一章。希望帮到你。本文还有配套的精品资源点击获取