C语言函数深度解析:从指针传参到工程化设计
发布时间:2026/10/5 15:43:53 锦皓数字建站

有一说一C语言的函数这块是很多初学者从“会写代码”跨到“会设计代码”的一道坎。函数本身语法很简单返回值、参数、调用翻两页书就懂了但真正到了写项目、做课程设计、刷OJ题的时候你会发现函数背后牵扯出来的一堆问题——指针传参、变量生命周期、分文件编译、回调机制、工具链报错——随便一个都能卡你半天。这篇文章我就围绕函数这条主线把常见的坑、常用的套路、以及我怎么排查问题的方法一次说清楚。1. 函数设计的核心思路别把函数当“大礼包”要当“接口”1.1 函数拆分的粒度决定了你后期改代码的心情很多人写C语言程序习惯性把所有逻辑全塞进main函数里。大一交作业这么干勉强能过但一旦程序超过两三百行这种写法会让你寸步难行。我在带新人或者看别人代码的时候判断一个人有没有入门第一眼就看他函数怎么拆。函数拆分的核心原则只有一条一个函数只做一件事。听起来像废话但实际操作中绝大多数人做不到。比如有人会写一个函数既做数据输入、又做排序、又做输出——这看起来是“封装”了实际上是把三件事的耦合焊死在了一起。后续你想把排序算法从冒泡换成快排你会发现输入输出的逻辑也被牵连修改。我一般建议拆分的粒度是一个函数控制在20到50行以内。超过这个长度先停下来想想是不是该拆了。比如冒泡排序C语言实现通常的教学代码就是一个函数搞定但我自己写的时候会拆成swap和bubble_sort两个函数swap只管交换两个元素bubble_sort只负责排序逻辑的循环控制。这样有个直观的好处如果排序结果不对我能先单独测试swap有没有写错再去查循环边界不用在一坨代码里翻来翻去。另外函数命名要直白看到名字就知道这个函数干什么。fun这种命名法在大学作业里非常常见比如你搜“fun函数的作用”很多人会一头雾水——因为fun本身没有语法含义它就是一个普通的函数名。但从工程习惯来说fun这个名字等于什么都没说。我建议用“动词名词”的结构比如read_students、calculate_average、print_report。这不仅是给别人看的更是给三天后的自己看的。1.2 局部变量、全局变量、静态变量变量的“活动范围”决定了函数的可复用性初学者最容易踩的坑就是把变量全定义成全局变量觉得这样写方便任何函数都能直接访问。这在计算机二级C语言的答题里可能不会扣分但在实际工程里几乎是灾难。全局变量带来的最大问题是你无法确定一个函数到底“碰”了哪些数据——它在任何地方都可能被修改排查bug的时候你无从下手。函数内部定义的局部变量生命周期从函数调用开始、到函数返回结束每次调用都会重新创建。这是最安全的变量类型也是函数可复用的根基。如果一个函数只通过参数接收数据、通过返回值输出结果不碰任何全局变量那这个函数就可以被原封不动地搬到另一个项目里用。这就是所谓的“纯函数”思路虽然C语言不完全支持函数式编程那种严格约束但这个设计习惯能让你的代码质量上一个台阶。static修饰的局部变量值得单独拿出来说。它和普通局部变量的区别是初始化只执行一次函数调用结束后变量不会销毁下次调用时保留上次的值。这个特性在计数器场景下很好用比如写一个next_id函数每次调用返回一个递增的编号。但要注意static变量的生命周期是全局的、作用域是局部的如果你想让函数“记住”状态但又不想暴露给其他函数用static就对了。千万别用它来替代参数传递否则函数就不可控了。注意C语言里没有“按引用传递”这回事。你写的int *p传递的依然是指针的值只是这个值恰好是地址。理解这一点是理解整个指针与函数关系的关键。2. 参数传递与指针C语言函数最难的一关2.1 值传递的原理为什么形参改了实参没变C语言的函数参数只有一种传递方式值传递。也就是说调用函数的时候实参的值会被复制一份给形参函数内部操作的是副本不是原变量。这是无数初学者崩溃的根源——写了一个swap(int a, int b)在函数里交换了a和b结果回到main函数一看两个变量纹丝不动。用内存的视角解释就很清楚了。假设你在main函数里定义了int x 3, y 5;调用swap(x, y)的时候编译器在栈上为形参a和b分配了新的内存空间把3和5分别复制进去。swap函数内部交换的是a和b这两个新空间的数值跟x、y所在的原始空间一点关系都没有。等函数返回a和b的空间被回收交换结果直接蒸发。要想真正修改调用方的变量必须传递变量的地址也就是指针。swap(int *a, int *b)接收的是x和y的地址函数内部通过解引用操作*a和*b去修改那份内存上的数据这样才能影响到外部变量。这个逻辑想通了C语言指针就算入门了。我在解释这个概念的时候喜欢用一个类比值传递相当于你把自己家的钥匙复印件给了一个人他在复印件上画画你家门锁不变指针传递相当于你直接把钥匙给了他他能打开你家的门在墙上涂鸦。虽然比喻不完全精确但帮助记忆足够了。2.2 数组作为函数参数为什么传数组不用加取地址符数组作为函数参数是另一个高频困惑点。你在main里定义int arr[10]调用modify_array(arr, 10)函数声明是void modify_array(int a[], int n)。你可能会有疑问数组名是不是整个数组的“值”为什么不需要写arr真相是数组名在大多数表达式中会隐式地退化为指向首元素的指针。所以arr传递给函数的时候实际上传递的是arr[0]即第一个元素的地址。这依然是值传递只不过传递的这个“值”恰好是一个地址。函数内部通过这个地址去访问和修改数组元素所以数组的修改会“穿透”到外部。这里有一个关键的区别。sizeof(arr)在main函数里返回的是整个数组占用的字节数比如int arr[10]返回40假设int占4字节但在函数参数里sizeof(a)返回的是指针的大小通常是8字节64位系统或4字节32位系统。这就是为什么函数接收数组时通常要额外传一个长度参数——因为函数内部已经丢失了数组长度的信息。你搜“C语言字符串逆序pta”这类题经常让你实现一个逆序函数如果你在函数内部用strlen可以拿到长度但如果传入的是普通int数组就必须依靠传递长度参数了。二维数组传参稍微复杂一点。int matrix[3][4]传到函数里函数声明写成void process(int matrix[][4], int rows)是可行的因为编译器需要知道每行有多少列才能计算地址偏移。但如果你写成void process(int **matrix, int rows)编译器会报类型不兼容——因为int[3][4]退化成指针后是int (*)[4]也就是指向包含4个int的数组的指针而不是指向指针的指针。这些问题在考试题里会以各种变形出现理解了内存布局就不需要死记硬背。2.3 函数指针的声明和用途从语法噩梦到回调机制函数指针是C语言里看起来最吓人的语法之一。比如int (*func_ptr)(int, int);——这行声明告诉你func_ptr是一个指针它指向一个“接收两个int参数并返回int值”的函数。很多人第一次看到这个声明直接劝退但实际掌握了拆解方法就简单了先找到变量名func_ptr然后看它被什么修饰。*说明它是指针向外一层(*func_ptr)后面跟着(int, int)说明它指向的函数有两个int参数最左边int说明函数的返回类型是int。那函数指针到底有什么用最常见的场景就是实现回调函数。所谓回调就是你把一个函数的地址传给另一个函数让后者在合适的时候去调用它。标准库里的qsort就是最典型的例子。qsort要排序任意类型的数组它不知道你的元素是int还是double还是结构体更不知道怎么比较两个元素所以它要求你传入一个比较函数的指针由你来定义比较规则。举个例子你要对一个结构体数组按成绩排序需要写一个比较函数typedef struct { char name[32]; int score; } Student; int compare_by_score(const void *a, const void *b) { const Student *sa (const Student *)a; const Student *sb (const Student *)b; return (sa-score sb-score) - (sa-score sb-score); } // 调用 qsort(students, count, sizeof(Student), compare_by_score);回调模式的核心价值在于解耦qsort只负责排序排序的具体“大小判断”规则交给调用方。所以你想按姓名排序只需要再写一个compare_by_name换一个函数指针传进去就行排序算法本身不需要改一行代码。理解了这一点再看嵌入式里常见的定时器回调、中断回调就明白它们本来就是一回事。注意函数指针的类型必须与回调函数签名完全一致包括参数类型、个数和返回值类型。不一致的代码编译时往往不会直接报错但运行时可能会栈出错这类问题非常隐蔽。3. 函数的声明、定义与分文件把代码组织成工程3.1 为什么需要函数声明编译器是按顺序工作的如果你把函数定义放在main函数后面而在main里先调用了它编译器会报错或者给出警告。很多人不能理解明明我函数写对了为什么不让我调用原因是编译器处理源文件是按顺序从上到下的。它在main函数里看到一个陌生的函数名calculate_average它需要知道这个函数接收什么参数、返回什么类型才能生成正确的调用代码。在它没看到函数定义之前它不知道这些信息所以报“隐式声明”错误。解决办法是函数声明。函数声明也叫函数原型它告诉编译器“我这个函数是存在的签名是long滴定义在后面”。声明写在调用之前编译器看到声明就能生成调用代码了至于实现到底在哪里链接的时候再去查找。这就是头文件机制的基础——.h文件里放声明.c文件里放定义其他文件只要#include这个头文件编译器就认识你的函数了。一个完整的声明是int calculate_average(int scores[], int count);注意分号结尾。如果只写calculate_average();那是不完整的声明编译器会假设参数是未知的、返回类型是int这在现代C标准里是不允许的。所以声明必须写全签名。注意如果你把函数定义在调用之前就不需要单独写声明了。这也是为什么很多教材的代码都是先写自定义函数、再写main函数的原因——省事。但工程上更常见的做法是头文件声明源文件定义分离。3.2 多文件编译怎么把函数拆到多个.c文件里当代码量上了规模把函数全写在一个.c文件里会非常痛苦。比如你写一个学生管理系统输入输出、排序、查找、文件读写全在一个文件里动辄上千行找函数要反复滚动屏幕。这时候就需要做函数分文件。我的习惯是按照功能模块划分文件。比如main.c程序入口负责调用student.c学生管理的核心逻辑增删改查student.h声明student.c里需要暴露的函数utils.c通用工具函数比如字符串处理、时间格式化utils.hutils.c的声明头文件的关键在于防止重复包含。如果main.c同时包含了a.h和b.h而b.h又包含了a.h那么a.h的内容会被处理两次导致重复定义的错误。解决办法就是用预处理指令#ifndef STUDENT_H #define STUDENT_H typedef struct { int id; char name[32]; int score; } Student; void add_student(Student *students, int *count, Student new_stu); void print_students(const Student *students, int count); #endif这段代码的意思是如果没有定义过STUDENT_H这个宏就定义它并执行后面的声明如果已经定义过说明头文件已经被包含过了直接跳过。这是头文件守卫的标准写法写头文件的时候一定要记得加。多文件编译的命令也要提一下。如果直接用gcc编译多个文件gcc main.c student.c utils.c -o program。文件少的时候没问题但文件多了、依赖复杂了最好用Makefile或者CMake来管理。我见过不少人在vscode里配置C语言环境后遇到“undefined reference to”这类错误——大部分是链接阶段漏了某个.c文件或者头文件声明了但实现没写。遇到“undefined reference”先别慌检查一下编译命令里是不是把所有需要的.c文件都写进去了。3.3 Windows下编译环境常见问题git、pip、claude识别失败的原因在配置C语言编译环境时很多人会卡在系统提示“无法将‘git’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这个报错的本质是PowerShell在PATH环境变量里找不到名为git的可执行文件。原因通常有三个软件没安装、安装的时候没勾选加入PATH、或者PATH配置了但当前终端窗口没有重新加载。解决方案也比较直接。以工具链为例Windows下做C语言开发常见的组合是MinGW-w64 VS Code。安装MinGW-w64的时候要把bin目录路径比如C:\mingw64\bin手动加入到系统环境变量Path中。装完以后新开一个终端窗口输入gcc --version能正常输出版本信息就说明环境OK了。如果你输入gcc报“无法识别”十有八九是Path没配好或者装的是没有bin目录的简化包。pip命令报同样错误也是同一类问题。解决思路一致去官网装Python的时候在安装器第一页勾选“Add Python to PATH”或者装完后手动把Scripts目录加进Path。这类问题不是C语言独有的但我在教新人配环境的时候发现很多人卡在这一步就直接放弃了其实原理通了就非常快。至于claude无法识别那一般是CLI工具没安装或安装后没有链接到Path。原理和上面完全一样解决思路也一致。学会这一套“Path排查法”以后遇到任何“无法识别命令”的报错都能从容应对。4. 常用函数库与实操场景文件读写、字符串处理、数学计算4.1 字符串处理函数的坑strcpy为什么危险字符串处理是C语言函数实用中最容易出问题的地方。strcpy是高频函数但它的安全性问题在后来的C11标准里引入了strcpy_s来解决。strcpy的原型是char *strcpy(char *dest, const char *src)作用是把src指向的字符串复制到dest指向的内存空间包括结尾的\0。问题在于它不检查dest的缓冲区够不够大。如果src的长度超过了dest容量的上限数据就会写越界覆盖掉相邻内存中其他变量的值。这种问题在表面上可能不会立刻崩溃但会在某个随机时刻产生诡异的结果或者被用于攻击。这就是为什么很多C语言安全规范建议尽量使用strncpy并手动限定最大复制长度。比如从文件读入一行字符串用fgets是更稳妥的选择char buf[256]; if (fgets(buf, sizeof(buf), fp) ! NULL) { // 处理buf中的内容 }fgets会读入最多sizeof(buf) - 1个字符并自动在末尾加\0这就大大降低了缓冲区溢出的风险。需要注意的是fgets会把换行符\n也读进缓冲区所以如果你后续用strcmp之类的函数做比较可能需要先把末尾的\n去掉。很多PTA题目里字符串逆序、字符串比较的题目出现的隐藏bug往往都跟换行符和\0的处理有关。4.2 C语言文件读写操作从文本到二进制文件操作在C语言里通过FILE *指针和一整套库函数实现。我见过很多学生写文件读写代码的时候开了一个文件忘了关导致后续操作莫名失败。基本原则是fopen和fclose必须成对出现最好在fopen成功之后立刻想到后面要fclose。典型流程是定义FILE *fp fopen(data.txt, r);然后检查fp是否为NULL。如果为NULL说明打开失败可能是文件不存在、没有权限或者路径写错了。这里有一个容易被忽略的点程序的工作目录不一定是源代码所在目录特别在VS Code或者某些集成开发环境里运行时相对路径的基准目录可能和你想的不一样。遇到“怎么都打不开文件”的问题第一步排查绝对路径是否正确把fopen(C:\\Users\\xxx\\data.txt, r)写全试试。文件读写分为文本模式和二进制模式。文本模式在Windows下会自动将\n转换为\r\n写入时反过来转换二进制模式则不做转换。如果你处理的是图片、压缩包这类二进制文件必须以二进制模式打开否则数据会被悄悄改动。用fread和fwrite可以按块读写数据适合结构体数组的存取例如把学生信息整体写入文件下次启动程序再读回来。注意C语言程序运行完即使你没有调用fclose操作系统在进程结束时会回收资源但如果在长时间运行的服务程序里频繁开文件不关最终会耗尽文件描述符导致fopen失败。写代码的时候务必养成及时关闭文件的习惯。4.3 数学库函数与特殊函数陷阱C语言的数学函数放在math.h头文件里比如sin、cos、sqrt、pow、fabs等等。使用这些函数时一个经典坑点是在gcc命令的编译阶段链接libm数学库否则会报“undefined reference to sin或cos”。加上-lm参数即可gcc main.c -lm -o program。很多初学者以为pow是简单函数却不知道它对整数运算有精度隐患。因为pow是以浮点数方式计算的当你调用pow(10, 2)时结果可能是99.99999999999999强行赋值给int就变成了99。这种阴森古怪的问题会浪费你大量的排查时间。做整数次幂时自己写一个循环乘法的函数往往更可靠。再看一下erfc这类特殊函数的查表问题。erfc是互补误差函数在概率统计和信号处理中经常出现。如果你的编译器或者平台库没有直接提供erfc可以通过数值近似公式来实现也可以查表加插值。但要注意不同编译器版本、甚至不同标准下的erfc精度可能有细微差别临界值比较时要预留误差裕量。4.4 管道函数和select多进程与I/O多路复用的一点拓展在嵌入式或者系统编程中你会接触到pipe函数和select函数。pipe用于创建一对文件描述符一个写端、一个读端实现进程间通信。用法很简单int fd[2]; pipe(fd); // fd[0]为读端fd[1]为写端但这里面容易踩的坑是父进程如果没有及时关闭不必要的写端或读端可能会导致阻塞或者数据错乱。多进程通信里面“关闭不需要的端口”是铁律。如果你在写一个类似 shell 管道命令的小程序这一点几乎一定会遇到。select函数则用于I/O多路复用可以让一个线程同时监听多个文件描述符的读、写、异常事件。它经常被用在网络服务器、串口通信中。最基本的套路是FD_ZERO清空集合、FD_SET加入描述符、调用select、再用FD_ISSET判断哪个描述符就绪。select在处理大量连接时效率不高但它的逻辑简单非常适合学习和做中小型项目。5. 常见编译错误与坑位排查实录5.1 undefined reference 和 implicit declaration把常见C语言函数编译错误的排查思路整理成一张速查表对初学者来说非常实用。报错信息常见原因解决方法implicit declaration of function调用了未声明的函数检查头文件是否包含、声明是否写在调用前undefined reference toxxx链接阶段找不到函数实现检查.c文件是否加入编译库是否链接如-lmmultiple definition ofxxx同一函数在多个源文件里重复定义使用头文件只放声明定义放在唯一的.c文件中expected identifier before ( token宏或声明书写错误、头文件缺分号检查结构体定义末尾是否忘记分号宏定义不能乱加分号cannot open source file头文件或源文件路径不对确认文件存在检查#include路径写法、项目配置中的Include Pathsegmentation fault空指针解引用、越界访问检查指针是否初始化用调试器定位崩溃行排查这些报错我的方法论是先读第一条错误不要从最后一条看起。编译器输出的错误是滚动累积的第一条往往才是根因后面都是连带的。比如“implicit declaration”报在my_func这一行往上看可能只是某个头文件名拼写错误被#include失败了。5.2 段错误Segmentation Fault的排查流程段错误是C语言新手最怕的运行时错误。核心原因是程序访问了没有权限或不存在的内存地址。最常见的场景有三种空指针解引用、访问已释放的内存、数组越界。排查手段主要有三招。第一招用调试器。如果你在VS Code里开发配置好gdb调试环境直接打断点、单步执行看到哪一行代码触发崩溃问题定位就成功了大半。第二招加打印定位。在可疑区域前后加printf输出标记看程序走到哪里就不再输出了。这种方法土但有效尤其在排查大型循环里的越界时很好使。第三招检查数组下标。C语言的数组边界检查要靠程序员自己保证arr[i]如果i可能为负数或者超过数组长度程序不会像Java那样抛异常而是直接踩到未知内存。有一个被问了很多次的问题“怎么检验非法地址C语言”答案是不要试图去“检验”一个地址是否合法因为标准没有提供安全的方法而是应该保证地址的来源是合法的——比如指针经过malloc分配、指向已有变量、或者从合法数组名得到。靠“试一下会不会崩”来判断地址是否合法本身就是未定义行为程序可能碰巧不崩也可能在某次运行时突然崩掉。5.3 函数内修改数组不生效的诡异问题这个问题的定位过程非常有代表性。有一个读者跑来问写了一个数组排序函数函数内部打印是排好的回到main函数再打印原数组没变。经过沟通发现他在函数内部对参数arr重新赋值了int *arr malloc(...);然后把新数据填进去排序。这当然不会影响外部数组——因为arr这个形参只是指针的副本在函数内部修改指针本身外部指针毫不知情。想要修改外部指针指向的地址也就是让外部指针指向一块新分配的内存必须传递“指针的指针”。比如void allocate_array(int **arr, int n) { *arr (int *)malloc(n * sizeof(int)); if (*arr NULL) { exit(1); } for (int i 0; i n; i) { (*arr)[i] i * i; } } int *data NULL; allocate_array(data, 10);这里**arr存储的是外部变量data的地址通过*arr修改的就是data本身。这个模式在需要“在函数内部分配内存给外部使用”的场景里很常见比如读取文件后把文件内容放入一个动态分配的缓冲区。理解了这个你对C语言指针的理解就基本成型了。6. 嵌入式场景下函数的特殊性与优化6.1 嵌入式C语言中函数设计的几个限制嵌入式领域的C语言函数设计和桌面应用有一些明显区别。首先是栈空间的限制。桌面程序掉用深层递归可能也就占几MB内存但单片机的栈可能只有几KB递归函数稍不注意就把栈爆了程序直接跑飞。所以在嵌入式里我尽量避免递归能用循环解决的绝不用递归。其次是函数调用的开销。每一次函数调用都有压栈、跳转、返回的流程对于对时序要求严格的中断服务函数过度封装可能会导致中断响应时间不可控。但这不是说嵌入式就不能写函数了而是要在性能敏感路径上减少无谓的层次调用。有的嵌入式项目会使用static inline关键字强制编译器将函数体展开到调用处消除调用开销适合非常短的访问函数。还有一个和定时器相关的高频需求定时器的计数累计。有人常问“C语言流量计累计程序怎么写”核心就是用定时中断或外部中断对脉冲次数计数再乘以每脉冲对应的流量系数。这里面关键点在类型选择和溢出问题——累计量要用unsigned long甚至uint64_t并要考虑溢出的处理策略。6.2 中断服务函数ISR里的特殊规则中断服务函数是嵌入式C语言和普通C语言函数差别最大的地方。ISR不能有返回值不能有参数而且应该尽量短小精悍。一个典型的做法是在中断里只做标记和快速数据搬移把复杂的处理放到主循环或者任务里。比如volatile uint8_t flag 0; void TIMER0_IRQHandler(void) { flag 1; // 只是打个标记 clear_interrupt_flag(); // 清中断标志 } int main(void) { while (1) { if (flag) { flag 0; // 在这里做耗时的事情 } } }volatile关键字在这里是必须的因为flag是在中断和主循环之间共享的变量编译器可能会把它的读取优化到寄存器里导致主循环永远看不到变化。如果不加volatile你看代码逻辑感觉没毛病但实际运行就是不稳定。这类问题在嵌入式开发中非常常见也特别考验经验。6.3 Qt槽函数的返回值问题为什么不能返回值走到带界面的开发场景Qt的槽函数和普通C函数有一个明显差异。槽函数可以被信号触发而信号的源通常是框架内部的事件机制不会等待槽的返回值所以槽函数的标准签名是void。如果你试图给槽函数加上返回值在做connect连接的时候返回类型可以是void、bool等有限几种但信号发射方并不会真正“使用”它。也就是说你想通过槽函数带一个处理结果出来要么让槽把结果存在类成员变量里要么通过参数引用的方式把结果返回给调用方。很多第一次接触Qt的C语言程序员会把connect(btn, clicked, this, slotFunction)理解成普通函数调用试图从槽返回值绕来绕去很困惑。这个问题的实质是消息驱动架构之下“回调”的语义是异步的信号发出后立刻返回槽的执行时间点可能在函数栈完全不同的上下文里。因此回调函数拿不到普通函数那样明确的“返回值”必须通过外部状态或引用参数来交互。理解了消息驱动模型的这一点很多Qt和GUI编程的困惑都会迎刃而解。7. 从函数到工程学习路径与练习建议7.1 推荐的学习步骤和练习项目函数这块内容学完之后单纯看书容易忘。我的建议是立刻上手做综合练习。PTA拼题A上有大量C语言函数题比如字符串逆序、冒泡排序实现、结构体排序这些题目短小精悍非常适合练手。翁恺老师的C语言课程讲函数和指针的部分也讲得很细配合他的课后题做下来基础会非常扎实。如果从“会写题目”过渡到“会做项目”我推荐几个循序渐进的小项目学生成绩管理系统涉及结构体、数组、函数拆分、文件读写是C语言所有基本功的集大成者。简易通讯录功能和学生管理系统类似但可以加入动态内存分配和字符串处理。命令行贪吃蛇涉及控制台光标定位、键盘监听、数据更新逻辑对函数划分和状态管理要求更高。简易文件加密工具用按字节读写和异或运算加密文件锻炼二进制文件处理能力。这些项目做完你对函数、指针、文件操作的理解会完全不一样。我见过不少同学说做项目之前觉得自己什么都会做完才发现自己什么都不会——这种落差正是成长的开始。7.2 工程习惯函数注释、命名、代码风格关于函数我觉得最值得养成的三个习惯几乎决定了你写出的代码是“能跑”还是“好维护”。第一个是注释但不是写废话。函数头部的注释应该说明这个函数做什么、参数是什么含义、返回值是什么如果有边界条件和注意事项也写清楚。函数内部的注释应该解释“为什么这么做”而不是“做了什么”——代码本身能表达“做了什么”只有“为什么”才需要注释。第二个是命名。变量名和函数名要见名知意。sum就比s好calculate_total_score就比run好。虽然打字多了一点但代码是写给人看的人需要花30秒读懂的函数用一个好名字只需要3秒。第三个是格式。统一缩进风格大括号的位置要么全KR风格要么全Allman风格不要混用。一个函数只保留一个入口一个出口如果要提前返回尽量确保资源已经妥善处理。这些习惯在面试时非常加印象分在实际协作中更是刚需。8. 最后分享一个我在实际项目中的体会我现在看一眼自己早期写的代码最明显的感受就是函数太“大而全”了。一个函数动辄上百行变量名是a、b、tmp注释几乎没有参数列表塞了七八个。后来维护一个用到双曲函数和数学库的旧项目时这种代码带来的痛苦达到顶峰——每改一行都要提心吊胆生怕影响到另一个逻辑。后来我花了几个晚上按函数拆分重写了一遍程序可读性瞬间提升调试也顺利很多。所以我建议所有刚学完C语言函数的人先别急着往下学链表、学高级数据结构把基础函数的设计练扎实。先把简单的计算器、二维数组操作写明白再逐步加入指针、回调、文件操作。C语言的函数就像乐高积木看起来只是一个方块但如何使用它、如何组合它、如何命名它体现的是一个人对问题拆解的深刻程度。真正学会了C语言的函数你再看其他语言里的方法、子程序、过程会发现天下代码是一家。最后再给一条实操建议多做代码阅读。去找一些开源的小项目比如轻量级的嵌入式驱动库、命令行工具专门盯着别人的函数声明看猜测这个函数的作用、参数为什么这么设计、返回值为什么是这个。然后对照实现验证自己的猜测。这个习惯比做一百道选择题都管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。