首页 / 资讯中心 / 文章详情

C语言入门不绕路:环境搭建、指针初体验与新手高频坑

C语言入门不绕路:环境搭建、指针初体验与新手高频坑 ★ FEATURED ARTICLE
决定学C语言那天我在笔记本上只写了一行字从今天开始不绕路了。之前大半年我一直徘徊在各种语言教程里能照着写点小工具但只要别人追问一句那个内存错误到底怎么回事为什么这段代码快那段代码慢我就彻底哑火。后来有个朋友点了一句你不是想搞懂底层吗那就回去老老实实学C。这大概就是这个记录的开端。这篇博文不讲高深理论就是我从零开始学C的第一阶段全记录包括环境怎么搭、第一个程序怎么跑通、变量和指针该怎么理解、以及那些不看文档就容易踩进去的坑。适合两类人一类是零基础准备从C入门的另一类是写了几年脚本想回来补地基的。C也许不是最友好的入门语言但从它开始你会明白很多魔法其实都是可解释的机制。1. 为什么要从C开始它不是最难的入门而是最直接的入门1.1 学语法容易学计算机怎么想很难C这门语言的语法其实很小常用关键字就那么几十个不像很多现代语言动辄几百个内置概念。真正的难点从来不在语法而在于你怎么让思维贴近计算机的运行方式变量存在哪、数组在内存里是什么形状、函数调用时现场是怎么保存的。这些东西在别的语言里基本被藏起来了C一个都不藏所以它反而成了理解计算机的一条捷径。用打比方来说开自动挡的车可以完全不懂发动机但你想彻底搞懂发动机原理就得拆开看活塞和曲轴。C就是这个机械结构。1.2 C教给你的三件事换个语言也通用第一是内存观。所有数据最终都躺在内存里指针就是内存的门牌号。第二是数据表示char、int、float在内存里是不同规格的抽屉占多大、能存多大范围都跟字节数直接相关。第三是控制权C没有自动垃圾回收也没有运行时替你检查错误错了就错给你看逼着你搞清楚每一步发生了什么。这三件事一旦在C里理解透再回头看解释型语言或带垃圾回收的语言很多魔法都会变成可预见的机制。我在学到指针之后再回头理解其他语言里的引用、拷贝、闭包明显顺畅很多。这也是我坚持从C开始的最大动力。1.3 C现在还在干什么很多人问学C是不是过时了实际上C照样活跃在操作系统、嵌入式设备、数据库引擎、网络协议栈这些场景里。那些场景的共同点是离硬件近、对性能敏感、资源有限需要程序员精确控制内存和行为。换句话说凡是不容出错、讲究效率的底层系统C依然是主力。所以学C不只是为了应付课程更是给以后的系统编程、数据结构、嵌入式方向打底子。退一步说就算你最终写应用层理解C也会让你调试其他语言的问题时更有方向感。1.4 哪些人适合现在开始如果你是刚接触编程C作为第一门语言是很稳的路线如果你已经会写一点代码但老觉得自己在照葫芦画瓢也完全可以回来补一遍C。C不是一门学完就丢掉的语言它是地基中的地基。第一周确实容易劝退调试报错也容易让人灰心但只要你的目标是搞懂原理而不是快速出效果C反而是效率最高的起点之一。我的建议是别把它当任务把它当成你探索计算机内部的一次长期旅行心态稳了进度反而快。2. 环境搭建实录从零到第一条可执行程序2.1 选编译器免费、标准、命令行可用C语言本身是标准文本不是某家公司独占的所以编译器选择非常多。我的选择标准就三条免费、贴近通用标准、能在命令行里用。开源编译器基本都满足。安装方式主要看你的操作系统。Windows平台需要下载安装包装完可能还要配置一下环境变量让系统能找到编译命令Linux和苹果系统通常一条安装命令就能完成或者系统自带。装完之后打开终端敲一条查看版本信息的命令能正常输出版本号就说明编译器已经就位。整个过程十分钟以内别在这里卡太久卡住了就去求助搜索引擎把报错原文贴上去比什么都有用。2.2 编辑器与IDE新手怎么选不后悔我的建议是前期用文本编辑器终端的组合。这个方案的好处有三第一它逼着你搞清楚从源文件到可执行程序之间的每一步第二以后学调试器、学版本管理都要和终端打交道提前适应不亏第三这也是很多从业者的日常模式你和他们的习惯贴近搜问题、问别人的时候沟通成本最低。如果你实在不习惯纯命令行用集成开发环境IDE也完全可以按钮一点就能编译运行适合把注意力先放在语言本身。但要留意IDE的工程配置有时会把编译细节藏起来报错时反而不好定位。我现在的折中方案是日常练习用轻量编辑器需要断点调试的时候再用IDE。2.3 验证环境编写第一份源代码文件新建一个目录专门放练习代码文件名用英文小写加.c后缀比如hello.c。写代码之前先明确一点你敲进去的是源代码计算机不认这个必须经过编译器加工才能运行。先写一个最简单的程序#include stdio.h int main(void) { printf(hello, c\n); return 0; }保存后在终端进入该目录执行编译命令形如cc hello.c -o hello再执行运行命令./hello看到屏幕输出hello, c就说明你的整套工具链是通的。这一步的意义不在于我写出了第一个程序而在于你把源代码→编译→可执行文件→运行这条链路完整走了一遍。以后遇到任何想不通的问题都可以回到这条链路上找答案。3. 拆解第一个程序源代码到可执行文件之间发生了什么3.1 一条编译命令背后的四道工序很多人以为敲完编译命令就直接生成了能运行的程序其实中间隔着四道工序。第一道是预处理处理所有以#开头的指令比如#include把你的头文件内容粘贴进来宏定义在这里展开第二道是编译把C代码翻译成汇编语言第三道是汇编把汇编翻译成机器指令生成目标文件第四道是链接把目标文件和你用到的库函数合并最终生成完整的可执行程序。把这四道工序拆开理解最大的好处是排错时能分阶段。比如以后看到undefined reference这类错误就知道问题出在链接阶段通常是某个函数只声明了没实现或者漏了库。3.2 逐行读hello.c没有一行是顺手写的#include stdio.hstdio.h是标准输入输出头文件的声明集合printf的声明就在里面。现在不用关心全部内容但要记住原则用哪个标准函数就得包含对应的头文件。int main(void)这是程序的入口操作系统运行你的程序时从这里开始执行。C标准规定main的返回类型必须是intvoid表示不接受命令行参数。网上有些老教材写void main不属于标准写法一开始就按规范来免得后面养成坏习惯。printf(hello, c\n);把格式化的字符串输出到屏幕\n是换行符。注意源文件里写的是反斜杠加n两个字符输出结果才是换行。如果忘了写\n程序运行完会紧接着输出系统提示符看起来就像没换行。return 0;把0作为退出状态返回给操作系统约定俗成0代表正常结束非0代表异常。以后写脚本判断程序是否成功判断的就是这个返回值。3.3 改完代码必须重新编译新手最容易忽略的一步这里必须单独提醒你在编辑器里改了源代码直接去运行电脑执行的还是旧的可执行文件。因为任何一个改动都要重新经过编译才能变成新的可执行文件。我刚开始学的时候就犯过这个毛病改了几次没反应还以为是代码写得不对后来才发现是根本没重新编译。这个修改→重新编译→运行→看结果的循环就是你以后每天要重复几百遍的基本节奏。节奏对了效率不会低。3.4 面对编译报错的心态第一次编译就报错太正常了。我的第一条错误是少写一个分号编译器给我报了一长串英文当时差点以为代码全错了。后来才知道编译器报告的错误行号有时和真实位置有偏差比如漏了大括号它会把错误定位到文件末尾。正确做法是从第一条报错开始处理解决完再重新编译别一口气想改完所有错误因为后面的报错经常是前面错误的连锁反应。这个一条一条来的习惯能替你省下大量时间。4. 变量、类型与输入输出最基础也最容易看会了但写不对4.1 先把字节这个单位刻进脑子里C语言里每个变量都要声明类型类型决定了它占多少字节、能存什么范围的数据。初学先记住下面这几个类型常见占用典型范围以32位对齐为例格式化占位符char1字节-128 ~ 127%cint4字节约 -21亿 ~ 21亿%dfloat4字节约 7位有效数字%fdouble8字节约 15位有效数字%lflong long8字节范围非常大%lld特别提醒两件事。第一char虽然叫字符本质是整数它存的是字符编码大多数情况就是ASCII码所以char既能当字符用也能当小整数用。第二不同平台下int的字节数不保证一样老平台上int可能是2字节跨平台代码要留意类型长度。理解字节数之后很多问题就通了整数溢出就是抽屉太大塞不下数据从边缘溢出结果完全不可预期。4.2 printf和scanf一对格式化的双胞胎输出用printf输入用scanf它们的第一个参数都是格式字符串后面的参数按占位符依次对应。新手最容易踩的坑有两个。第一个是占位符和变量类型不匹配比如用%d打印long long用%f打印int结果完全不可信第二个是scanf里漏写代码长这样int age; scanf(%d, age); // 正确传的是age的地址 scanf(%d, age); // 错误把age的值当成地址用为什么scanf非要地址因为它要把读到的值写进这个变量里而C函数默认是值传递scanf拿不到变量本身只能拿到地址再找到它。这个地址的概念你现在先有个印象学到指针会彻底明白。还有一个常见现象连续用scanf读数字再读字符缓冲区里可能残留换行符导致读字符时直接读到换行而不是你输入的字符。真遇到这种为什么跳过输入的问题优先怀疑缓冲区。4.3 用sizeof验证我以为的类型大小与其背文档不如自己动手验证。C提供一个运算符叫sizeof可以测出类型或变量占多少字节printf(%zu\n, sizeof(int)); printf(%zu\n, sizeof(char));在多数平台的普通机器上你能看到4和1。用这种方式确认类型大小比自己死记硬背靠谱而且等以后学到结构体时sizeof会经常出现在内存计算的场景里。顺带一提printf里打印sizeof要用%zu别用%d这也是个容易踩的小坑。4.4 运算符里的三个隐形陷阱整数除法5 / 2结果是2不是2.5。两个整数相除商是整数小数部分直接丢弃。想要浮点结果得让至少一边是浮点数写成5.0 / 2。自增自减的位置i和i前者先取当前值再自增后者先自增再取新值。单独写成一行没区别混进表达式里就很容易算错建议新手把它单独放一行别图省事。短路逻辑A B中如果A为假B根本不会执行A || B中如果A为真B也不会执行。利用这个特性可以写简洁的判断但也容易在以为B会执行的时候踩坑。5. 控制流与函数把代码从一条直线变成一张网5.1 三种循环各自适合什么for循环适合步数明确的场景比如从0遍历到9for (int i 0; i 10; i) { printf(%d\n, i); }while循环适合条件为先的场景比如不知道循环几次只知道满足条件就继续do-while保证循环体至少执行一次适合先做一次再根据结果决定要不要继续的场景。初学阶段容易忽略的细节有两个for括号里三个部分用小写分号分隔千万别写成逗号while循环里更新条件的语句别漏否则就是死循环。我自己就写过一次死循环终端刷屏根本停不下来最后只能强行结束进程。那次之后我才真正记住循环里必须有能让条件变成假的操作。5.2 if和switch分支判断的两个派系if-else if-else是最常用的分支结构新手容易把else if误写成else后面直接跟条件。switch适合对单个整数值做多分支判断看起来比一长串if更清爽但C的switch有一个历史遗留坑每个case分支结束一定要写break否则会穿透到下一个case。这种错误编译器不报、运行时不崩只是逻辑上多执行了不该执行的分支排查起来特别头疼因为它表现成结果不对而不是程序坏了。另外判断相等用赋值用这两个符号写混了编译器有时候只给警告不报错。一定要把编译警告当回事警告不是烦你的是救你的。5.3 函数的本质参数是复制给你的改不动外面函数让代码复用但新手最容易误解的是参数传递方式。C默认是值传递意思是调用函数时实参的值被复制了一份给形参函数内部怎么修改形参都不影响外面的变量。所以想写一个交换两个变量的swap函数如果参数直接传int代码编译运行全正常可交换的只是两份拷贝外面的变量纹丝不动void swap_bad(int a, int b) // 外面的变量不会变 { int tmp a; a b; b tmp; }要让函数真正修改外面的变量只能传地址也就是指针。这个知识点我是学到指针之后回头看函数才彻底想通的值传递解决只读指针参数解决要改。函数还有个作用域问题定义在函数里的局部变量只在函数内有效函数返回后它就被回收了所以千万别返回局部变量的地址。这个在指针章节还要重点展开。5.4 break和continue循环里的两个控制杆在循环体里break用于立即跳出整个循环continue用于跳过本次循环的剩余部分、直接进入下一轮。这两个关键字本身不难但新手容易用成万能逃生门遇到复杂循环就乱放。我的建议是能用循环条件本身表达的就别用breakbreak一多代码逻辑就很难跟踪。continue则要小心别造成死循环比如先continue后写循环变量的更新语句那更新就永远不会执行。6. 指针初体验难了三天通了之后就再也回不去了6.1 把变量想象成带门牌号的房间内存就是一排排有地址的房间每个变量占据其中一间或几间。你声明int a 10编译器会在内存里给a分配一个地址a是房间的门牌号*是按门牌号进去拿里面的内容。举个例子int a 10; int *p a; // p保存a的地址 printf(%d\n, *p); // 输出10新手对星号的两个角色容易犯晕声明里的星号是声明一个人是指针表达式里的星号是访问这个地址指向的值。我这里有个记忆方法声明时跟在类型后面表示这是存放地址的变量使用时放在指针变量前面表示取出该地址上的内容。指针必须先指向一个合法的地址再用否则就是野指针解引用野指针常常直接崩溃是C语言里最出名的定时炸弹之一。记住一句话不指向合法地址的指针就当它不存在碰都别碰。6.2 数组名就是首元素的地址数组和指针的关系是我学C时最大的顿悟点。声明int arr[5]arr本身就是数组首元素的地址arr[i]其实是*(arr i)的语法糖int arr[5] {1, 2, 3, 4, 5}; int *p arr; // 数组名已经是地址不需要加 printf(%d\n, *(p 2)); // 输出3也就是arr[2]这里要特别注意一个细节p 1移动的距离不是1个字节而是1个int的大小也就是通常的4个字节。指针加1是按照它指向的类型的大小来移动的。这个规则决定了指针运算的结果也是新手容易懵的地方。想明白数组名就是地址之后字符串就很好理解了字符串本质上就是字符数组以\0结尾。abc在内存里其实是a、b、c、\0四个字节。所以给字符串分配数组时大小永远要记得多留一个位置放结尾符。char s[3]去存abc表面看三个字符刚好放下实际越界了一个字节而越界往往不会立刻报错。6.3 指针最常见的三个翻车现场野指针声明了指针变量但没赋值就解引用指针的值是随机的访问随机地址结果不可预期。悬垂指针指向了一个已经失效的局部变量。函数返回后局部变量被回收指向它的指针就成了悬垂指针再访问就是踩内存。空指针解引用指针是NULL还去解引用大多数平台会直接报段错误。排查这类问题不要盯着代码念咒语要加打印语句看地址和值的变化或者用调试器在崩溃位置看函数调用栈。我第一次遇到段错误整整花了一个晚上才定位到是一个未初始化的指针。等指针通了之后你会觉得之前学的变量、数组、函数全部串起来了因为它们本质上都在讲内存地址怎么用。指针这一关必须自己写代码去撞只看书不练三个月也通不了。7. 新手高发坑清单这些错我都替你踩过了7.1 scanf漏写数据源错了一切都白搭这个前面提过但值得单独说一次。给基本类型变量读值scanf从第二个参数开始必须传地址。漏掉程序不一定立刻崩溃但读进来的值不可信后面的所有逻辑都在错误数据上运行。遇到数据很奇怪的问题第一步就去检查scanf调用把漏掉的补上。7.2 数组越界不报错但会悄悄改别人的内存C不检查数组下标是否越界。对长度为5的数组做arr[10]操作编译器不管、运行时不报错但你的程序可能已经写坏了旁边变量的内存。这类bug的特点就是随机有时正常有时崩溃换个编译器优化选项表现还不一样。应对的办法是自己在写代码时维护好边界循环条件的临界值比如0和n-1要反复验证越是偶尔出错的问题越要先怀疑越界。7.3 浮点数不能直接比大小0.1 0.2在C里不等于0.3这不是C的问题而是浮点数用二进制表示有限小数时天然存在误差。所以比较浮点数应该判断它们的差是否足够小#include math.h if (fabs(a - b) 1e-6) { // 认为相等 }这个经验在后面的数值计算里频繁用到早记住早轻松。7.4 字符串数组大小永远要加1存abc至少要4个字节因为结尾有一个\0。这不是文档里的边角料而是很多字符串功能的基础字符串长度、拷贝、拼接全部依赖\0来定位结尾。数组开小了字符串函数会把\0写到数组外的内存越界问题又来了。所以每次声明字符数组我都会先数一下字符个数再默默加1。7.5 一张表收拢高频小坑现象常见原因检查思路程序崩溃在scanf附近漏了或格式串写错从上到下重读scanf调用循环次数不对边界条件写错比如和混用用纸笔跑一遍0和n两个边界值整数运算结果变负int溢出换long long或改算法条件判断总是不成立写成了开启编译警告并认真看警告do-while编译不过do-while结尾漏了分号检查循环末尾有没有分号修改代码没反应忘记重新编译确认是否重新编译过这些小坑每一个单独看都不难但它们有一个共同点不会连环报错只会让程序静悄悄地错。这也是C语言最有性格的地方——它把内存的控制权全部交给你同时也把出错的成本交给你。养成小步快跑的习惯能挡掉大多数问题每写一个功能就编译一次、运行一次别一口气写几百行再开炮。我刚学的时候吃过这个亏写完一百行以为没问题一编译二十个错误根本不知道从哪改起。现在的规矩是任何新代码先保证能编译运行再加下一个功能。最后讲点心里话。学C的开始阶段最难熬的不是语法而是那种总觉得自己没学会的焦虑。我第一周每天都在怀疑自己是不是太笨。后来总结出一个土办法每天只定一个小目标今天搞懂变量和打印明天搞懂循环后天搞懂函数每学一个知识点就在编辑器里从零写一遍不看教程、不复制粘贴。这个办法笨但特别管用因为C的很多概念光看不会必须亲手敲一遍才知道自己卡在哪。如果你也正准备开始学C我只劝你一句不用急着囤一堆书能把一本标准的入门教材从头到尾认真读下来、把里面的习题全部自己写一遍就已经超过大多数人了。后面遇到不明白的再回过来翻翻我的这些记录你会发现上面说的每个坑你迟早都会撞上一次。撞就撞吧撞完你就真学会了。
阅读完成 · 觉得有帮助?
咨询建站