ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言函数从入门到实战:声明、传参、指针与递归

C语言函数从入门到实战:声明、传参、指针与递归 学C语言学到“函数”这一章很多人第一次有了“编程不是写作文而是搭积木”的感觉。前面变量、循环、数组还能顺着写到了函数突然冒出声明、定义、形参实参、传值传址、局部变量静态变量甚至还有函数指针和回调一套组合拳下来直接卡住的不在少数。我在帮同学看代码时听到最多的几个问题就是为什么函数里交换了两个数回到main又变回去了为什么我明明写了函数编译器还报“隐式声明”那个叫fun的函数到底是什么来头是不是内置函数这篇文章就围绕C语言函数这个主题把声明定义、参数传递、作用域、函数指针、递归以及经典练习题全部过一遍。适合正在系统学C语言基础的人也在刷翁恺练习题或浙大PTA题目的同学以及工作中想回头补一补函数设计思路的开发者。读完你能解决三件事看得懂函数的来龙去脉写得出带参数带返回值的正经函数碰到“隐式声明”“段错误”这类报错知道从哪里下手。1. 函数到底是什么先搞懂“封装”这件事1.1 从复制粘贴到“菜谱式”复用先看一个很朴素的场景你需要在程序里打印三次九九乘法表。如果不使用函数做法是把九九乘法表的循环原样复制三遍。一旦想要改格式比如把printf(%d*%d%2d, i, j, i*j)改成其它布局就得在三个地方同时改漏掉一处输出就变成“同一个程序两种风格”非常尴尬。函数解决的就是这件事把一段需要重复使用的逻辑抽出来起个名字放到一边要用的时候直接调用。生活里最像的其实是饭店后厨。客人点的菜不会因为没有“菜谱”就让每个厨师从头研发一遍而是先有标准菜谱原料是什么下锅顺序是什么火候多大装盘什么样。菜谱本身是固定的每次来新订单厨师照着菜谱做一遍就行。这里的“原料”就是函数的参数“端出去的菜”就是函数的返回值“菜谱内容”就是函数体。这种把细节藏起来、只保留“输入输出接口”的做法专业点叫封装。封装之后主函数会变得非常短可读性也高很多。我常跟初学者说如果某个函数里充满了“复制粘贴过来的几十行代码”多半是前期没有做好封装这时候停下来重构比硬着头皮继续写更重要。1.2 函数的三要素返回值、函数名、参数列表一个函数定义长得通常是这个样子int add(int a, int b) { return a b; }拆开看三部分返回类型这里是int代表函数计算完之后要还给调用者一个整数。如果函数只干活但不还东西返回类型写void。函数名这里是add以后调用时就用这个名字。函数名最好是“动词名词”结构比如printTable、findMax一眼能看出来它干什么。参数列表括号里的int a, int b是形参相当于函数内部使用的“占位变量”。调用时传入的具体数值叫实参。很多人一直忽略一个事实main本身也是一个函数只不过它是程序启动时系统自动调用的“入口函数”。返回值类型是intreturn 0表示正常结束。理解了这一点再回头看“自定义函数”就不会觉得神秘了。另一个让初学者疑惑的点是“fun函数的作用”。这个fun不是C语言关键字也不是内置函数而是很多教材喜欢用的示例函数名它是function的缩写。谭浩强老师那本《C程序设计》里经常能看到fun、f这种占位名字意思就是“这里放一个函数你主要看它的逻辑和调用方式”。实际项目里千万别这么命名换成calculateAverage、getUserInput这种有意义的名字三个月后再看代码你会感谢自己。至于“内置函数”准确的叫法是标准库函数。C语言本身只规定了语法不规定谁帮你实现printf、scanf、sqrt这些功能。编译器厂商会自带一份标准库比如stdio.h里声明了输入输出函数math.h里声明了sqrt、pow等数学函数string.h里声明了strlen、strcpy等字符串工具。你写#include stdio.h本质就是把别人写好的函数声明拿过来用函数体在库文件里已经编译好了链接时自动拉进来。2. 函数声明与定义编译器要“先见一面”2.1 被“隐式声明”坑过的举手下面这段代码很多初学者第一次跑的时候直接被报错劝退#include stdio.h int main() { int sum add(3, 5); printf(%d\n, sum); return 0; } int add(int a, int b) { return a b; }报错信息大致是implicit declaration of function add即“对函数add进行了隐式声明”。为什么因为C语言源文件是从上到下顺序编译的。编译器扫描到add(3, 5)这一行时它之前只见过main压根不知道add是什么、参数列表长什么样、返回类型是什么。为了能继续编译早期C标准允许编译器“猜”一下没见过的函数就当它是int add()可以接收任意参数。这种“猜”很危险参数类型对不上、返回类型对不上都可能产生难以排查的运行时错误所以从C99标准开始隐式函数声明基本就被禁止了现代编译器会直接报错。解决办法就是在使用add之前先让它见一面#include stdio.h int add(int a, int b); // 函数声明也叫函数原型 int main() { int sum add(3, 5); printf(%d\n, sum); return 0; } int add(int a, int b) { // 函数定义函数体在这里 return a b; }前面那个只有分号、没有函数体的叫函数声明作用是告诉编译器这个函数存在返回类型是int需要两个int参数。后面的完整实现叫函数定义。声明就像“合同”定义就像“施工图”。2.2 函数原型的最佳实践我见过不少新手把声明写在某个函数内部比如在main函数里先声明再调用。这虽然合法但很不推荐如果项目里有十多个函数难道每个调用点都来一句声明吗更好的做法是把所有自定义函数的声明集中放在源文件顶部紧跟#include之后如果项目由多个源文件组成就把声明放进头文件比如add.h里写int add(int a, int b);add.c里写函数体main.c里#include add.h。编译多文件时用类似这样的命令gcc main.c add.c -o app这里有个小细节函数声明里形参名可以省略int add(int, int);也能过但建议保留名字因为名字本身是文档。另外如果只在当前源文件里用不打算被其他文件调用可以在函数前加static比如static void helper(void)这样外部文件看不到它也不怕和别人写的同名函数撞车。2.3 为什么printf不用你声明很多人会想既然要用函数必须先声明那为什么我从来没声明过printf因为stdio.h里已经帮你写好了int printf(const char *format, ...)这个声明。#include stdio.h这行预处理指令本质上就是“把stdio.h这个头文件的内容原样粘贴到当前文件开头”。粘进来的内容里包含了一堆函数声明和宏定义编译器看了自然就认识printf。写自定义函数时如果函数比较多也同样应该有自己的“stdio.h”也就是头文件。这是C语言组织大型项目的重要手段不是可有可无的仪式。3. 参数传递传值传址为什么swap老是失败3.1 传值调用形参是实参的复印件先看初学者必写、也必翻车的交换函数#include stdio.h void swap(int x, int y) { int t x; x y; y t; } int main() { int a 3, b 5; swap(a, b); printf(a%d b%d\n, a, b); // 输出的还是 3 和 5 return 0; }输出了a3 b5函数里明明交换了为什么没用因为调用swap(a, b)时C语言做的是传值调用把变量a和b的数值复制一份存进函数的形参x和y。x和y在内存里是全新的变量有自己的地址函数里交换的是x和y跟外面的a和b没有半毛钱关系。生活化的类比是你给了朋友一份合同的复印件让朋友在上面改朋友改得很开心但你手里的原件一个字都没变。想要让朋友改到“原件”你得把原件本尊交给他或者告诉他原件放在哪里让他自己去取。3.2 传址调用把地址交出去才能改外面的东西C语言里把一个变量的地址传给函数函数就能通过地址找到外面的内存直接修改它void swap(int *x, int *y) { int t *x; *x *y; *y t; } int main() { int a 3, b 5; swap(a, b); printf(a%d b%d\n, a, b); // 正常输出 5 和 3 return 0; }这里int *x表示“x是一个指向int的指针里面存的是地址”。调用时写a也就是把变量a的地址传进去。函数里*x表示“沿着x里存的地址找到那块内存读写它”。这相当于你告诉朋友合同原件在我抽屉里钥匙给你按地址去改。指针这玩意儿为什么难因为它多了一层“地址”的概念。建议初学时脑子里记住两句话指针变量里存的是地址*指针变量是在“解引用”去访问地址对应的内存。后面第5章还会看到函数本身也有地址函数名就是入口地址。另外还有一个容易被忽略的细节如果函数只需要读取外部数据、不需要修改参数类型可以加const保护。比如strlen的声明是size_t strlen(const char *str)const char *str意思是“这个指针指向的字符串不会被我修改”。写函数时养成习惯只读参数尽量加const不仅能防手滑改坏数据还能让代码意图更清楚。3.3 数组传参传的其实是地址不是整份拷贝很多人在函数里算数组长度时翻车void printArray(int arr[]) { printf(size%zu\n, sizeof(arr) / sizeof(arr[0])); }结果size输出的是1或者2完全不等于数组长度。原因在于函数参数写成int arr[]和int *arr是等价的编译器把数组名当成指针来处理。在64位系统上sizeof(arr)解得的是指针大小通常是8字节而sizeof(arr[0])是4字节相除等于2。如果数组更长结果就更乱了。正确做法是把长度一起传进来void printArray(int arr[], int n) { for (int i 0; i n; i) { printf(%d , arr[i]); } putchar(\n); }这也能解释为什么很多C语言标准库接口都要带一个长度参数比如qsort需要size_t count参数。C语言不像其它语言那样能在运行时拿到“数组总长度”数组名一旦退化成指针长度信息就丢了。经典的冒泡排序函数正好用上这些知识#include stdio.h void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - i - 1; j) { if (arr[j] arr[j 1]) { int t arr[j]; arr[j] arr[j 1]; arr[j 1] t; } } } } int main() { int a[] {5, 2, 8, 1, 9}; int n sizeof(a) / sizeof(a[0]); bubbleSort(a, n); for (int i 0; i n; i) printf(%d , a[i]); return 0; }这里bubbleSort(a, n)把数组名a传入因为数组名会退化为指向首元素的指针所以函数内部能够直接修改原数组的元素不需要用。这也是“数组参数比较特殊”的原因它不是传整份数组的拷贝而是传了首地址。4. 作用域与生命周期函数里的变量为什么“失忆”4.1 局部变量、全局变量、静态局部变量变量不是“写在哪都能用”的得看它在哪个作用域里。局部变量定义在函数内部只在函数体内有效。每次函数被调用时创建函数返回时销毁。它的生命周期是“这一次调用”作用域是“这一个函数”。全局变量定义在函数外所有函数都能访问。生命周期是整个程序运行期间。全局变量确实方便但滥用会带来很难查的耦合问题你不知道是哪个函数偷偷改了它。我一般建议能不用就不用实在要用就集中在文件开头并且用清晰的命名。静态局部变量定义在函数内加上static关键字作用域仍然是函数内但生命周期延长到整个程序。它只在第一次执行到声明时初始化一次之后函数退出也不会销毁。静态局部变量最典型的使用场景是“计数器”int getNextId(void) { static int id 1000; return id; }第一次调用返回1000第二次返回1001第三次返回1002。如果把static去掉id就是普通局部变量每次调用都被重新初始化为1000计数功能就没了。如果定义成全局变量虽然也能计数但其它函数也可能碰它反而容易埋雷。4.2 不要返回局部变量的地址这个地方踩坑的人非常多而且它不会直接报错而是“有时候行、有时候崩”玄学一般。典型代码#include stdio.h char *getString(void) { char buf[] hello; return buf; // 危险返回了局部变量的地址 } int main() { char *p getString(); printf(%s\n, p); return 0; }buf是getString内部的局部数组函数返回后栈上的这块内存已经被“标记为可回收”随时可能被其它函数调用覆盖。运气好时输出hello紧接着调一个printf可能又变成乱码。这是典型的未定义行为。解决这个问题的常用方案有三种让调用者提供缓冲区void getString(char *out, int size) { snprintf(out, size, hello); }调用者自己做好内存管理函数只负责往里写。在函数内使用static数组char *getString(void) { static char buf[] hello; return buf; }但这要求你接受“函数每次返回的是同一块内存”这个事实调用者如果多次调用后还想保留第一次的结果就会互相覆盖。用malloc动态分配内存但调用者用完必须free否则会造成内存泄漏。这三种方案没有绝对的优劣对应不同场景。关键是初学时要明确一个原则函数不要返回指向“自己栈上局部数据”的地址。4.3 extern让函数看到别的文件里的全局变量多文件项目中如果fileA.c里定义了全局变量int count 0;fileB.c里想用它可以在fileB.c顶部写extern int count;。extern的意思是“这个变量在别处定义我在这里声明一下它的存在”。同理函数本身在多个文件之间也是默认外部可见的除非你用static修饰。所以头文件里放的都是“外部可见的声明”具体定义放在对应的.c文件中。理解了这个再去看那些工程项目的目录结构就不会觉得乱。5. 函数指针与回调把函数当作数据来用5.1 函数也有地址函数名就是入口地址前面讲指针时一直在说“变量有地址可以拿指针存”。函数也一样有地址程序编译后每个函数的代码在内存里有自己的位置函数名在表达式里会被当成函数的入口地址。函数指针的声明形如int add(int a, int b) { return a b; } int (*p)(int, int); // p是一个指向“返回int、两个int参数”的函数指针 p add; // p指向add函数 int result p(3, 5); // 通过函数指针调用注意括号很关键。如果写成int *p(int, int)含义完全不同那是一个返回int *的函数而不是函数指针。很多初学者在这里被符号优先级折磨记住“(*p)表示p是指针后面的括号是它的参数列表”。函数指针有什么用最直接的例子是把函数当作参数传给另一个函数这就是回调函数的核心。5.2 函数指针数组用一张“函数表”实现计算器先写四个简单的运算函数int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; }然后定义一个函数指针数组#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } int main() { int (*ops[4])(int, int) {add, sub, mul, divide}; int a 20, b 4; for (int i 0; i 4; i) { printf(result[%d] %d\n, i, ops[i](a, b)); } return 0; }读者可以自己试着改成“根据菜单选项调用对应函数”。这种把操作存进数组、用下标选择的做法叫表驱动。后面做菜单程序、指令解析器时会发现它比一堆switch清爽很多。5.3 回调函数把“比较规则”交给库函数C语言标准库里的qsort是理解回调的绝佳入口#include stdio.h #include stdlib.h int cmpInt(const void *a, const void *b) { int x *(const int *)a; int y *(const int *)b; return x - y; } int main() { int arr[] {3, 1, 4, 1, 5, 9, 2, 6}; int n sizeof(arr) / sizeof(arr[0]); qsort(arr, n, sizeof(int), cmpInt); for (int i 0; i n; i) printf(%d , arr[i]); return 0; }qsort这个库函数负责“排序流程”但“两个元素谁大谁小”这个规则它不知道所以它要求你传一个比较函数进来在需要比较时调用你写的cmpInt。你写的函数不是直接被你调用的而是被qsort在内部调用这就是回调。为什么非得回调因为排序算法的逻辑和“元素类型大小”是两件事。如果你给double数组排序x - y的逻辑还差不多如果给结构体按姓名排序比较规则完全不同。让调用者把规则传进来qsort就能通用于任何类型数据这就是函数指针带来的通用性。6. 递归函数自己调用自己其实很自然6.1 基线条件加递归表达式两步走递归是指一个函数在函数体内调用它自己。递归不是绕圈打转而是“把一个大规模问题拆成小规模问题每次缩小范围直到小到可以直接解决”。经典阶乘long long factorial(int n) { if (n 1) return 1; // 基线条件 return n * factorial(n - 1); // 递归表达式 }调用factorial(4)时实际展开是4 * factorial(3) 4 * (3 * factorial(2)) 4 * (3 * (2 * factorial(1))) 4 * (3 * (2 * 1)) 24每次调用都会在栈上开辟新的函数栈帧保存参数、局部变量、返回地址。所以递归必须有明确的基线条件否则就是无限递归最终导致栈溢出。网上常有人问“为什么我的递归程序一运行就Segmentation fault”大概率是基线条件没写或永远不满足。6.2 字符串逆序用递归交换首尾PTA和很多入门练习里都有一道“字符串逆序”的题。迭代写法很直接但递归写法能帮助理解“拆小问题”的思路#include stdio.h #include string.h void reverse(char *s, int left, int right) { if (left right) return; char t s[left]; s[left] s[right]; s[right] t; reverse(s, left 1, right - 1); } int main() { char s[] hello; reverse(s, 0, strlen(s) - 1); printf(%s\n, s); return 0; }判断条件left right就是递归出口当左右指针相遇或越过时说明中间区域已经处理完。每次递归都让区间缩窄两个字符所以总递归深度是字符串长度的一半安全得很。但要注意递归不是什么场景都适合。斐波那契数列用朴素递归写起来很漂亮long long fib(int n) { if (n 2) return 1; return fib(n - 1) fib(n - 2); }fib(50)会算到天荒地老因为存在大量重复计算。它不是“算法有问题”而是“解法选错了”。看到递归题目先估算一下展开的调用次数再决定是否用迭代或动态规划改写。这也是函数设计里很重要的一环不只要“跑得对”还要“跑得快”。7. 函数实战九九乘法表、字符串逆序与5x5鞍点7.1 一个函数只做一件事重写九九乘法表九九乘法表是练习函数拆分的绝佳题目。核心逻辑是两层循环但我们可以把它拆成两个函数一个负责打印第n行一个负责循环调用每一行。#include stdio.h void printRow(int n) { for (int i 1; i n; i) { printf(%d*%d%2d , i, n, i * n); } putchar(\n); } void printTable(void) { for (int n 1; n 9; n) { printRow(n); } } int main() { printTable(); return 0; }这个设计的好处是如果把“打印一行”拆成独立函数以后想改成倒序打印、打印某一行只需要改动或调用printRow主函数的逻辑完全不变。“一个函数只做一件事”是我学函数后最受益的习惯。每次看到超过三四十行的函数我都会考虑它是不是该拆了。7.2 5×5矩阵鞍点问题stdio.h和limits.h配合实战搜索热词里有一个很典型的题“使用stdio.h和limits.h用C语言解决计算5×5鞍点问题”。这道题对理解“把步骤拆成函数”很有帮助。题干是在一个5×5矩阵中如果一个元素既是它所在行的最大值又是它所在列的最小值就称为鞍点。要求输出鞍点位置和值没有则提示无鞍点。解题时最容易踩的坑是“只找第一个满足条件的值”但矩阵中可能出现多个鞍点也可能出现一行里有多个最大值都需要检查。limits.h在这里的作用是提供INT_MAX和INT_MIN用来做比较基准。#include stdio.h #include limits.h void findSaddlePoint(int a[5][5]) { int found 0; for (int i 0; i 5; i) { int rowMax INT_MIN; for (int j 0; j 5; j) { if (a[i][j] rowMax) rowMax a[i][j]; } for (int j 0; j 5; j) { if (a[i][j] rowMax) { int colMin INT_MAX; for (int k 0; k 5; k) { if (a[k][j] colMin) colMin a[k][j]; } if (a[i][j] colMin) { printf(鞍点: a[%d][%d] %d\n, i, j, a[i][j]); found 1; } } } } if (!found) { printf(无鞍点\n); } } int main() { int matrix[5][5]; printf(请输入5x5矩阵\n); for (int i 0; i 5; i) { for (int j 0; j 5; j) { scanf(%d, matrix[i][j]); } } findSaddlePoint(matrix); return 0; }这段逻辑分两步走先拿INT_MIN当比较基准扫出第i行最大值再对这个最大值用INT_MAX当基准扫描它所在列看是不是该列最小值。因为每次都是针对“候选值”做检查所以即使行最大值有重复也能全部覆盖。如果用不好limits.h新手可能会把初始值设成0一旦矩阵里有负数逻辑就直接错了。这就是为什么要提“使用stdio.h和limits.h”输入输出靠stdio.h最值初始化靠limits.h两个头文件各有各的用处。7.3 搭配教材和题库练谭浩强、翁恺、浙大PTA函数这章光看书是不够的。我推荐三套资源搭配使用谭浩强《C程序设计》第六版语法讲得细例子简单适合从头跟到尾写一遍。书里的fun函数示例不要嫌俗可以在旁边改成有实际含义的名字顺便练改名能力。翁恺的C语言课程讲法更贴近“到底怎么写程序解决问题”尤其是函数设计那块听完再看书会通透很多。网上能搜到配套练习。浙大PTA编程题大量基础题能检验你是否真的会写函数。通常一道题会要求你实现某个函数接口输入输出接好就行非常适合训练“按接口写代码”的感觉。我见过太多人看视频感觉全会一上PTA就卡。原因就是函数不是看会的是写会的。每学一个新知识点哪怕只是十行的小demo也要在本地跑通再往后走。8. 编译环境与命令报错先把工具链打通再说写代码8.1 “无法将‘gcc’项识别为cmdlet、函数、脚本文件或可运行程序的名称”很多Windows用户在PowerShell里敲gcc会看到这样一行gcc : 无法将“gcc”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这不是C语言语法错误而是系统的环境变量PATH里没有指向gcc.exe所在的目录。命令行解释器在执行命令时会按照PATH里的目录列表逐个找这个可执行文件找不到就把“无法识别”四个字甩给你。搜索热词里出现了一大堆类似报错比如pnpm、claude、make、nmp无法识别机制一模一样。解决办法分两步安装MinGW-w64并记住它安装后bin目录的完整路径在Windows设置里搜索“环境变量”把bin目录追加到Path中然后新开一个终端窗口再测试。有些朋友改完环境变量发现还是不行原因多半是旧终端没重启或者路径写错了。可以用where gcc试一下能不能找到。这里有一个很好的类比命令行靠PATH目录列表查找可执行程序编译器靠“函数声明列表”查找函数。找不到命令shell报“无法识别”找不到函数声明编译器报“隐式声明”。底层逻辑都是“先登记注册再使用”。8.2 VSCode配置C语言环境以及Ubuntu虚拟机里的gccVSCode写C语言不是装个扩展就完了。标准做法是安装MinGW-w64记住bin目录路径在VSCode里安装C/C扩展创建一个.vscode/tasks.json配置编译任务把command指向gcc创建一个.vscode/launch.json配置调试器。如果嫌配置麻烦初学阶段完全可以用命令行先跑起来在项目目录打开终端执行gcc hello.c -o hello ./hello.exe在Ubuntu虚拟机里也一样先安装编译工具链sudo apt update sudo apt install build-essential然后写个hello.c执行gcc hello.c -o hello ./hello很多大学生用虚拟机配C语言环境卡点往往不是命令本身而是build-essential没装全或者目录权限不对。用gcc --version能确认编译器是否存在这一步可以先做。8.3 VSCode里函数、变量跳转失效怎么办搜索热词里还有一个典型问题VSCode里C/C所有的函数、变量都没办法跳转。这通常不是代码问题而是插件“不知道去哪找定义”。C语言的函数定义经常在.c文件里声明在.h文件里编辑器要建立整个项目的符号索引才能从调用处跳到定义处。常规排查顺序是打开命令面板输入C/C选择“C/C: Edit Configurations (UI)”在IntelliSense配置里把compilerPath指向你的gcc.exe把includePath指到MinGW的include目录确保系统头文件能被解析确认文件都被加入同一个工作区没有“孤儿文件”。这一堆操作听起来和C语言语法无关但它们解释了同一个原则当一个符号“声明”和“定义”没有被工具正确关联时人和编辑器都会迷失方向。9. 函数学习的常见报错与排查速查表我按自己带人写代码时的经验把函数相关的高频报错整理成一张速查表现象可能原因解决办法编译报implicit declaration of function xxx函数调用处之前没有声明或没包含对应头文件在文件顶部加函数原型或#include对应的头文件链接报undefined reference to xxx编译器看到声明了但链接器找不到函数定义确认写有函数定义的.c文件参与了编译比如gcc main.c func.c或检查函数名拼写编译报conflicting types for xxx函数被调用时编译器按默认int处理过后面真实定义类型不一致把函数声明放在调用之前并保证声明和定义类型完全一致运行时Segmentation fault段错误解引用野指针、返回局部变量地址后使用、数组越界用调试器或printf定位崩溃位置检查指针是否有效数组传参同时传长度函数交换外部变量失败使用了传值调用没有传地址参数改成指针调用时传a,bsizeof(arr)在函数内结果不对数组参数退化成指针sizeof得到指针大小不要试图在函数里用sizeof求长度由调用者传入长度命令执行报“无法将gcc识别为cmdlet、函数、脚本文件或可运行程序的名称”PATH环境变量没有包含编译器目录配置PATH后重开终端用where gcc验证VSCode无法跳转到函数定义includePath或编译器路径没配置在C/C配置里设置compilerPath、includePath排查这类问题最重要的是“分步定位”。先看是编译错误、链接错误还是运行时错误。编译错误基本是语法和声明问题链接错误大概率是函数没定义或没参与编译运行时报错则要怀疑指针和内存。新手经常三个混在一起越改越乱。我的建议是把报错信息完整复制下来去搜索而不是只看第一行很多问题搜索引擎里已经有成千上万人问过答案早就在那里等你。还有一个独家小技巧在函数入口和出口各加一句printf可以快速判断崩溃发生在哪个调用链上。比如int foo(int x) { printf(enter foo, x%d\n, x); // ... 函数体 ... printf(exit foo\n); return result; }先别急着觉得这很土。定位完问题后把这些调试打印删掉就好。如果函数特别多这种“打点”比调试器更直观尤其适合在初学阶段还没完全掌握单步调试的时候。10. 学函数这件事我的一些个人经验我带新手时最爱说的一句话是学函数不是学语法而是学“划分责任”。谁负责获取输入谁负责计算谁负责输出函数边界划清楚了程序自然就清晰了。写函数之前先在纸上写清楚“输入是什么、输出是什么、内部大致几步”比直接埋头敲代码有效十倍。我见过很多同学卡在“这个变量在函数里能不能用”“这个指针要不要free”这些小问题上其实只要回到“这个数据归谁管”这个根本问题大部分纠结都能解开。还有一个让代码质量提升明显的小习惯给函数和变量起名字时把“动词”和“名词”分清楚。getInput、printResult、calAverage越往下写越能感受到命名带来的幸福感。到最后你会在实际项目里发现那些能让人一眼看懂的函数往往比语法花哨的函数更受欢迎。函数这一章是整个C语言的分水岭跨过去之后指针、结构体、文件操作都会顺很多。如果你正在翁恺练习题或者PTA上被函数题目折磨别急把这里面的示例代码亲手敲一遍再回头去刷题你会有一种“原来如此”的顿悟感。
返回列表