0基础C语言积跬步之函数

发布时间:2026/10/30 1:53:32
0基础C语言积跬步之函数 我所使用的编译器是VS2022目录函数的概念库函数自定义函数形参和实参return 语句数组做函数参数嵌套调用和链式访问函数的声明和定义作用域和生命周期一、函数的概念函数就是可以实现一段特定功能的代码我们常见的两种类型的函数库函数自定义函数二、库函数库函数就是在不同编译器下编译器厂商根据ASNI C美国国家标准协会制定的C语言标准编写的能实现特定功能代码的函数且这些函数比较常用我们只需要写其对应的头文件就能找到此函数对应的声明和定义从而直接使用。比如printscanf等比如我们想使用一个库函数sqrt 功能计算平方根的函数#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include math.h int main() { double a 0; scanf(%lf, a); printf(%lf,sqrt(a)); return 0; }sqrt函数的标准格式是doublesqrt(doublex);它的返回值是double类型的且传给它的值也要使用double类型的值使用它必须包含头文件#include math.h此代码中我们调用了scanfprintfsqrt三个库函数调用前两个必须用到头文件#include stdio.h,使用库函数必须包含其对应的头文件不然会报错运行截图%f%lf%llf默认打印六位小数三、自定义函数自定义函数就是我们自己编写一个函数包括返回类型函数名参数自定义函数的标准格式是返回类型 函数名(参数类型 参数名){函数体}例如 int Add(int x, int y) { return x y; }函数名不能和C语言规定的关键字重合函数体就是函数的定义写要实现的功能的逻辑代码我们想编写一个能实现加法功能的函数Add来计算a和b的合,代码如下#include stdio.h int Add(int x, int y)//定义Add函数的功能 { return x y; } int main() { int x 0, y 0; scanf(%d%d, x, y); int ret Add(x, y);//调用Add函数 printf(%d\n, ret); return 0; }四、形参和实参实参在我们调用函数时给函数传递的参数就叫做实际参数简称实参形参在我们写函数定义时函数用于接收实参而创建的参数叫做形式参数简称形参下面我们看一段代码来理解#include stdio.h void Change(int x, int y)//形参 { x 10; y 20; } int main() { int x 0; int y 0; printf(改变前:%d %d\n, x, y); Change(x, y);//实参 printf(改变后:%d %d\n, x, y); return 0; }主函数里传给Change函数的参数xy就叫做实参Change函数定义中创建的int xint y就是用于接收函数传来的两个实参叫做形参形参和实参的三个规则形参和实参的名字可以一样形参必须写明数据类型改变形参不会改变实参如上述代码中我们想通过利用Change函数中创建的形参的改变来改变主函数中创建的实参的变量的值运行截图我们会发现改变前和改变后xy的值依然是初始化的0完全没变这正符合了改变形参不会改变实参这个规则。原因是因为形参和实参的地址不一样形参的两个参数的是在运行过程中调用函数后才向内存申请的空间调用完出函数之后这个内存空间还会销毁其空间和实参所在空间地址并不一样所以我们改变形参改变的是形参的地址中的值但实参地址中的值并不会因此影响形参和实参互相拥有独立的空间所以改变形参实参的值不会改变但有一个方法可以解决传递实参的地址给形参形参用指针变量来接收这个地址并通过这个地址来找到实参从而改变实参#include stdio.h void Change(int* x, int* y) { *x 10; *y 20; } int main() { int x 0; int y 0; printf(改变前:%d %d\n, x, y); Change(x,y); printf(改变后:%d %d\n, x, y); return 0; }这个在指针中会讲到传实参的地址后我们自定义函数内部改变的形参的地址就和实参一样了如此以来因为改变的是同一个地址里的参数所以改变形参的值就等同于改变实参的值了运行截图如上图可见成功改变了xy的数值五、return 语句函数中一般都会写return语句作用是返回函数的数据类型的值下面是几个return语句使用的注意事项return语句之后可以是一个数值也可以是一个表达式如果是表达式优先计算表达式再将表达式的计算结果返回如果函数的返回类型是void类型那return可以选择不写也可以选择写return后面什么都不用加如果return的值和函数要求返回的数据类型不一样编译器会自动将return的值的类型改成函数要求返回的数据类型当函数中return语句执行后会直接结束函数并跳出后面的语句不再执行如果函数存在if等分支语句那么需要将每一种分支结果都有return值保证程序函数每一种分支的结果都有返回值不存在无返回值的结果3注意事项3举例解释#include stdio.h int fun() { return 3.8f; } int main() { int ret fun(); printf(%d\n, ret); printf(%d\n, (int)3.8); return 0; }运行截图可以看到return的值是个3.8f是个float类型的值但是函数要求的返回值的类型是int类型此时编译器就会把3.8强制转化为int类型的值浮点型强制转换为整型不存在四舍五入直接截断、舍去全部小数部分只保留整数。int3.8得以验证此结论不进行四舍五入。在此我们扩展一个库函数round函数专门用来对小数进行四舍五入#include stdio.h #include math.h int main() { printf(%.0f, round(3.8)); return 0; }使用round库函数要包括头文件#include stdio.h运行截图3.8四舍五入为4结果打印45注意事项5举例解释#include stdio.h int test(int a) { if (a 0) { return 10; } // 没有else没有return分支走这里就没返回值非法 } int main() { int a 0; scanf(%d, a); int rettest(a); printf(%d\n, ret); return 0; }如果a0,会return 10但当a0时却没有写返回值这样的话就不是函数的所有路径都有返回值存在没有返回值的路径编译器会报警告运行截图编译器会从栈里随机拿一个数来凑数返回例如上图输入-10返回了1六、数组做函数参数当数组作为函数的参数时有几个要点函数传参的实参个数必须和形参个数一样实参是数组名形参也可以写成数组形式不过要有类型和数组名和数组大小数组名可以和实参数组名一样也可以不一样类型和大小必须相同形参如果是一维数组数组大小可以省略不写形参如果是二维数组行可以省略不写但列不能省略形参的数组和实参的数组其实操作的是同一个数组因为实参的数组名其实就是数组的首元素的地址传给的形参形参直接用实参的数组而不会新创建数组演示数组传参我们写一个Print函数用于打印数组#include stdio.h void Print(int arr[],int sz) { int i 0; for (i 0; i sz; i) { printf(%d , arr[i]); } return; } int main() { int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; int sz sizeof(arr) / sizeof(arr[0]); //我们写一个打印此数组的自定义函数Print Print(arr, sz); return 0; }我们传了数组名arr和数组元素个数sz给Print函数Print函数用int arr[]来接收实参的数组名arr用int sz来接收实参的sz运行截图七、嵌套调用和链式访问嵌套调用定义函数是可以嵌套调用的就是在一个函数里调用另一个函数比如我们想写一个判断某年某月的天数的函数我们就需要在判断天数的函数中调用判断是否为闰年的函数因为和平年不同闰年的时候2月的天数不一样有29天而平年的时候只有28天于是就有了我们以下的代码#include stdio.h int is_leap_year(int year) { if ((year % 4 0 year % 100 ! 0) || year % 400 0) { return 1; } return 0; } int get_days_of_month(int year, int month) { int days[] { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month 0 month 12) { int day days[month]; if (is_leap_year(year) month 2) { return day 1; } return day; } return 0; } int main() { int year 0; int month 0; while(scanf(%d %d, year, month)!EOF) { int days get_days_of_month(year, month); if (days ! 0) { printf(%d年%d月天数是%d\n,year,month,days); } else { printf(输入错误\n); } printf(\n); } return 0; }我们在get_days_of_month函数中调用了is_leap_year函数main函数中调用了scanf printf get_days_of_month函数以上都称之为函数的嵌套调用运行截图嵌套定义(不可行函数可以嵌套调用但是不能嵌套定义而且一个函数的功能最好就一个不能在一个函数里定义太多个功能比如说求闰年函数和打印闰年函数两个函数要分开定义不能嵌套定义也不能写在同一个函数里让这个函数即判断闰年又打印闰年这样不符合函数要高内聚的规范比如int Add(int x, int y) { return x y; int Sub(int x, int y) //在Add函数中嵌套定义了Sub函数 { return x - y; } }这样写程序会直接报错因为函数不能嵌套定义这是C语言的语法规则所有函数必须是「并列关系」不允许一个函数定义在另一个函数内部。正确写法两个函数分别分开定义所有函数必须是「并列关系」不允许一个函数定义在另一个函数内部。 #include stdio.h int Add(int x, int y) { return x y; } int Sub(int x, int y) { return x - y; } int main() { int x 0, y 0; scanf(%d%d, x, y); int addAdd(x, y); int subSub(x, y); printf(%d\n, add); printf(%d\n, sub); return 0; }运行截图链式访问定义链式访问就是在一个函数中调用另一个函数另一个函数的返回值作为此函数的参数下面给出两个例子(1)#include stdio.h #include string.h //使用库函数strlen()必须加添加该头文件 int main() { // 链式访问把 strlen 的返回值直接传给 printf 当参数 printf(%zd\n, strlen(abcdef)); return 0; }我们在printf中调用了strlen函数并将strlen函数的返回值作为printf的参数进行打印这就叫做链式访问代码中使用的库函数strlen()必须添加#include string.h头文件, strlen库函数的返回类型是size_t类型本质是unsigned int类型size_t类型的值打印用的是%zd 占位符2#include stdio.h int main() { printf(%d, printf(%d, printf(%d, 43))); return 0; }这串代码是printf函数中调用了printf函数的返回值作为它的参数首先我们要知道printf函数的返回值是printf打印的字符的个数所以我们从最里层开始拆解打印 printf(%d, 43)会先在屏幕上打印4343为两个字符所以printf(%d, 43)的返回值为2代入函数为printf(%d, printf(%d, 2))printf(%d, 2)会在屏幕上打印2为1个字符返回值就为1代入函数中为printf(%d1)最后在屏幕上打印了1所以总结printf(%d, printf(%d, printf(%d, 43)))就会打印4321运行截图这就是链式访问在一个函数里调用其他函数的返回值作为此函数的参数八、函数的声明和定义单个文件单个test.c文件比如判断闰年的函数#include stdio.h int is_leap_year(int year) //函数的定义 { if (((year % 4 0) (year % 100 ! 0)) || (year % 400 0)) return 1; else return 0; } int main() { int year 0; scanf(%d, year); int r is_leap_year(year); if (r 1) printf(闰年\n); else printf(⾮闰年\n); return 0; }如果我们将函数的的定义放在调用这个函数之前的地方那么编译器就能识别到这个函数但如果把定义放在调用这个函数的后方呢我们来看一串代码#include stdio.h int main() { int year 0; scanf(%d, year); int r is_leap_year(year); if (r 1) printf(闰年\n); else printf(非闰年\n); return 0; } int is_leap_year(int year)//函数的定义 { if (((year % 4 0) (year % 100 ! 0)) || (year % 400 0)) return 1; else return 0; }咱将函数的定义放在了调用函数之后这样子会报警告但是在Visual StudioVS编译器之中依然能正确运行这是因为VS 对“函数未声明”做了兼容处理。它会自动识别后面的函数自动补声明所以能跑、能用但还是会报上述警告所以正确写法是我们在调用函数之前要对函数进行声明#include stdio.h int is_leap_year(int year); //函数的声明 int main() { int year 0; scanf(%d, year); int r is_leap_year(year); if (r 1) printf(闰年\n); else printf(非闰年\n); return 0; } int is_leap_year(int year) //函数的定义 { if (((year % 4 0) (year % 100 ! 0)) || (year % 400 0)) return 1; else return 0; }int is_leap_year(int year);就是函数的声明函数的声明要包括函数的返回值类型、函数名、函数参数最后要加分号函数参数可以省略参数名只保留参数的数据类型例如int is_leap_year(int);这样声明也是可以滴多个文件一般企业在写代码时会使用多个文件分开写代码功能越多越复杂使用的文件越多1一般把函数的声明和自定义类型的声明放在头文件.h常见自定义类型包括结构体struct枚举enum共用体 / 联合体uniontypedef类型重命名我们来讲讲union(共同体/联合体union和structunion和struct结构体 是兄弟但内存用法完全相反。structstruct的内存是叠加使用有多少个结构体变量每个结构体变量数据类型所占内存的大小全部加起来就是struct结构体开辟的空间大小相加的时候要遵循内存对齐规则进行运算比如struct Student { int age; // 偏移20占用4 char name[20]; // 偏移 0占用20 char ch; // 偏移24占用1 double xiaoshu; // 要偏移必须是8的倍数 };这个结构体类型struct Student开辟的空间是20417840字节内存对齐规则是指struct所开辟的空间大小每个成员偏移量是自身大小的整数倍最终大小必须是它定义的变量中最大数据类型所占内存大小的整倍数偏移量和内存大小不同偏移量是指的变量开辟内存的初始位置与初始开辟的位置的差内存大小是指开辟的空间大小所以上述代码的struct Student类型在开辟空间时按照下方的逻辑进行开辟空间1. char name[20]起始偏移00是1的倍数✅放 20 字节结束偏移202. int age4 字节起始偏移2020 是 4 的倍数 ✅放 4 字节结束偏移243. char ch1 字节起始偏移2424 是 1 的倍数 ✅放 1 字节结束偏移254. double xiaoshu8 字节重点来了double 必须放在 8 的倍数地址上当前结束偏移是 2525不是 8 的倍数所以要填充 7 个字节跳到32然后再开辟8个字节的大小填充7 字节起始偏移32放 8 字节结束偏移40所以最终struct Student开辟的空间是40个字节的大小unionunion的内存是覆盖使用开辟的内存空间为最大的共同体变量数据类型的所占的内存的大小union Un { char ch; //占1字节 int num; //占4字节 }; int main() { union Un u; u.num 10000; // 此时 num 有效ch 乱码 printf(%d\n, u.num); printf(%c\n, u.ch); u.ch a; // 覆盖内存num 变成乱码 printf(%c\n, u.ch); printf(%d\n, u.num); return 0; }因为char ch占1字节int num占4字节最大的为4个字节所以union Un开辟的空间大小就为4个字节我们可以调试看一下如图所示当我们给u.num赋值时我们开辟了四个空间大小我们走完u.num10000时在内存中开辟四个空间的地址中存放了这个值那我们走到了u.cha我们不会重新开辟空间给ch而是在刚才num开辟的空间上改变了所占第一个字节的值将60改成了6161为十六进制对应十进制为97对应ascll码值为字符a,可以看下方图片改变值的地址是同一个地址说明没有重新开辟新空间而是直接覆盖然后调用printf(%c\n, u.ch)时只取用1个字节的值打印也就是只取十六进制61对应的ascll值来打印这就是union开辟和使用空间的方式所以回到最开始的问题比如我们要写个加法函数我们可以使用三个文件我们一般把函数的声明和自定义类型的声明放在.h)头文件中 如add.h)把函数实现的定义放在.c文件中 如add.c)把函数实现的总逻辑放在.c文件中 如test.c)如下图所示运行截图九、作用域和生命周期作用域局部变量的作用域为局部变量所在的括号的范围就单个文件来说全局变量的作用域为整个文件生命周期局部变量的生命周期是在局部变量作用域开始时创建出了作用域后局部变量变销毁全局变量的生命周期是在创建工程时创建整个工程文件结束后销毁static和externstatic可以用来改变修饰局部变量修饰全局变量修饰函数static修饰局部变量修饰局部变量我们来看使用static和不使用static的两段代码结果的不同1代码一#include stdio.h void test() { int i 0; i; printf(%d , i); } int main() { int i 0; for (i 0; i 5; i) { test(); } return 0; }运行截图2代码2#include stdio.h void test() { //static修饰局部变量 static int i 0; i; printf(%d , i); } int main() { int i 0; for (i 0; i 5; i) { test(); } return 0; }运行截图为什么加了static后结果就变了呢这是因为static修饰局部变量时会改变局部变量的生命周期使其出了作用域之后仍不销毁代码一中i出了函数之后都会被销毁下次运行此函数时都会重新为i申请内存空间重新创建初始化一个新的ii后变为1所以每次都会打印1代码二中变量int i被static修饰说明当其出了函数之后仍然存在不被销毁于是i就会不断累加而不是每次都变为0所以会打印1 2 3 4 5其实static修饰局部变量的本质是将原本在内存栈区中的局部变量放入了静态区使得其拥有了和静态区数值一样的特征出了程序后才会被销毁从而改变了生命周期但它的作用域依然没变只能在属于他的{}中使用内存分为栈区堆区和静态区分别存放不同的变量手动申请的内存malloc、calloc、realloc放在堆区局部变量和函数参数放在栈区全部变量和静态变量放在静态区静态变量就是static修饰的局部变量或全部变量static修饰全局变量修饰全局变量对全局变量的约束如下作用域仅限当前这一个 .c 文件不能跨文件使用被static锁住本文件我们来看两段代码来理解1代码一add.cint g_val 2026;test.c#include stdio.h extern int g_val; int main() { printf(%d\n, g_val); return 0; }运行截图2代码二add.cstatic int g_val 2018;test.c#include stdio.h extern int g_val; int main() { printf(%d\n, g_val); return 0; }运行截图这是因为static修饰了全局变量g_val使得其作用域变成add.c这一个文件无法跨文件使用g_val所以会报无法解析外部符号g_val的警告全局变量本身使用外部链接属性的但在被static修饰过后就变成了内部链接属性无法给全部变量所在的文件以外的文件使用声明了也不行extern声明外部符号代码中的extern使用来声明外部符号的例如上述代码中变量g_val在文件add.c中但如果你想在test.c中使用就需要在test.c中声明一下这个符号的存在extern int g_val;extern使用语法extern 数据类型 变量名;test.c中extern int g_val;的作用是直接把add.c中的g_val拿来使用使用的都是同一块内存而不是重新创建变量static修饰函数static修饰函数的作用和修饰全部变量大差不差也是将函数约束为作用域仅限当前这一个 .c 文件可以使用不能跨文件使用被static锁住本文件函数也是有外部链接属性的当用static修饰之后就会变成内部链接属性无法给函数所在的文件以外的文件使用声明了也不行add.cstatic int Add(int x, int y) { return xy; }test.c#include stdio.h extern int Add(int x, int y); int main() { printf(%d\n, Add(2, 3)); return 0; }运行结果报警告无法解析因为函数Add在add.c的文件里被static修饰无法被test.c文件使用所以使用建议只在当前文件内部调用的工具函数一律加static修饰隔离权限、避免命名冲突、更安全。add.cstatic int Add(int x, int y) { return x y; }test.c#include stdio.h extern int Add(int x, int y); int Add(int x, int y) { return x y; } int main() { printf(%d\n, Add(2, 3)); return 0; }运行截图我在add.c和test.c两个文件中都定义了Add函数为啥不会报重复定义函数名Add的警告这是因为add.c中的Add函数被static修饰使用add.c以外的外部文件根本看不到完全不清楚有Add这个函数的存在所以可以在其它文件中重新定义Add函数不会报错如果我们把add.c文件中修饰函数的static去掉会发生报错

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询