news 2026/10/30 1:53:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
0基础C语言积跬步之函数

我所使用的编译器是VS2022

目录

  1. 函数的概念
  2. 库函数
  3. 自定义函数
  4. 形参和实参
  5. return 语句
  6. 数组做函数参数
  7. 嵌套调用和链式访问
  8. 函数的声明和定义
  9. 作用域和生命周期

一、函数的概念

函数就是可以实现一段特定功能的代码

我们常见的两种类型的函数:

  • 库函数
  • 自定义函数

二、库函数

库函数就是在不同编译器下,编译器厂商根据ASNI C(美国国家标准协会制定的C语言标准),编写的能实现特定功能代码的函数,且这些函数比较常用,我们只需要写其对应的头文件,就能找到此函数对应的声明和定义,从而直接使用。比如print,scanf等

比如我们想使用一个库函数: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>

此代码中,我们调用了scanf,printf,sqrt三个库函数,调用前两个必须用到头文件#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函数的参数x,y就叫做实参

Change函数定义中创建的int x,int y就是用于接收函数传来的两个实参,叫做形参

形参和实参的三个规则:

  • 形参和实参的名字可以一样
  • 形参必须写明数据类型
  • 改变形参不会改变实参

如上述代码中,我们想通过利用Change函数中创建的形参的改变,来改变主函数中创建的实参的变量的值

运行截图:

我们会发现,改变前和改变后,x,y的值依然是初始化的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; }

这个在指针中会讲到,传实参的地址后我们自定义函数内部改变的形参的地址就和实参一样了,如此以来,因为改变的是同一个地址里的参数,所以改变形参的值就等同于改变实参的值了

运行截图:

如上图可见,成功改变了x,y的数值!

五、return 语句

函数中一般都会写return语句,作用是返回函数的数据类型的值

下面是几个return语句使用的注意事项:

  1. return语句之后可以是一个数值,也可以是一个表达式,如果是表达式优先计算表达式,再将表达式的计算结果返回
  2. 如果函数的返回类型是void类型,那return可以选择不写,也可以选择写return,后面什么都不用加
  3. 如果return的值和函数要求返回的数据类型不一样,编译器会自动将return的值的类型改成函数要求返回的数据类型
  4. 当函数中return语句执行后,会直接结束函数并跳出,后面的语句不再执行
  5. 如果函数存在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类型的值,浮点型强制转换为整型,不存在四舍五入,直接截断、舍去全部小数部分,只保留整数。(int)3.8得以验证此结论,不进行四舍五入。

在此我们扩展一个库函数round()函数:专门用来对小数进行四舍五入

#include <stdio.h> #include <math.h> int main() { printf("%.0f", round(3.8)); return 0; }

使用round库函数要包括头文件#include <stdio.h>

运行截图:

3.8四舍五入为4,结果打印4

(5)注意事项5举例解释

#include <stdio.h> int test(int a) { if (a > 0) { return 10; } // 没有else,没有return,分支走这里就没返回值,非法 } int main() { int a = 0; scanf("%d", &a); int ret=test(a); printf("%d\n", ret); return 0; }

如果a>0,会return 10,但当a<=0时,却没有写返回值,这样的话就不是函数的所有路径都有返回值,存在没有返回值的路径,编译器会报警告

运行截图:

编译器会从栈里随机拿一个数来凑数返回,例如上图输入-10,返回了1

六、数组做函数参数

当数组作为函数的参数时,有几个要点:

  1. 函数传参的实参个数必须和形参个数一样
  2. 实参是数组名,形参也可以写成数组形式,不过要有类型和数组名和数组大小,数组名可以和实参数组名一样也可以不一样,类型和大小必须相同
  3. 形参如果是一维数组,数组大小可以省略不写
  4. 形参如果是二维数组,行可以省略不写,但列不能省略
  5. 形参的数组和实参的数组其实操作的是同一个数组,因为实参的数组名其实就是数组的首元素的地址,传给的形参,形参直接用实参的数组,而不会新创建数组

演示数组传参:

我们写一个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 add=Add(x, y); int sub=Sub(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),会先在屏幕上打印43,43为两个字符,所以printf("%d", 43)的返回值为2,代入函数为printf("%d", printf("%d", 2)),printf("%d", 2)会在屏幕上打印2,为1个字符,返回值就为1,代入函数中为printf("%d",1),最后在屏幕上打印了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 Studio(VS)编译器之中依然能正确运行,这是因为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)

常见自定义类型包括:

  1. 结构体struct
  2. 枚举enum
  3. 共用体 / 联合体union
  4. typedef类型重命名

我们来讲讲union(共同体/联合体)

union和struct

union和struct结构体 是兄弟,但内存用法完全相反。

struct:

struct的内存是叠加使用,有多少个结构体变量,每个结构体变量数据类型所占内存的大小全部加起来就是struct结构体开辟的空间大小,相加的时候要遵循内存对齐规则进行运算

比如:

struct Student { int age; // 偏移20,占用4 char name[20]; // 偏移 0,占用20 char ch; // 偏移24,占用1 double xiaoshu; // 要偏移必须是8的倍数 };

这个结构体类型struct Student开辟的空间是20+4+1+7+8=40字节

内存对齐规则是指,struct所开辟的空间大小:

  • 每个成员偏移量是自身大小的整数倍
  • 最终大小必须是它定义的变量中最大数据类型所占内存大小的整倍数

偏移量和内存大小不同,偏移量是指的变量开辟内存的初始位置与初始开辟的位置的差,内存大小是指开辟的空间大小

所以上述代码的struct Student类型在开辟空间时,按照下方的逻辑进行开辟空间

1. char name[20]

  • 起始偏移:0

  • 0是1的倍数✅

  • 放 20 字节

  • 结束偏移:20

2. int age(4 字节)

  • 起始偏移:20

  • 20 是 4 的倍数 ✅

  • 放 4 字节

  • 结束偏移:24

3. char ch(1 字节)

  • 起始偏移:24

  • 24 是 1 的倍数 ✅

  • 放 1 字节

  • 结束偏移:25

4. double xiaoshu(8 字节)

重点来了!double 必须放在 8 的倍数地址上!

当前结束偏移是 25,25不是 8 的倍数所以要填充 7 个字节跳到32,然后再开辟8个字节的大小

  • 填充:7 字节

  • 起始偏移:32

  • 放 8 字节

  • 结束偏移:40

所以最终struct Student开辟的空间是40个字节的大小

union:

union的内存是覆盖使用,开辟的内存空间为最大的共同体变量数据类型的所占的内存的大小

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.num=10000时,在内存中开辟四个空间的地址中存放了这个值

那我们走到了u.ch='a',我们不会重新开辟空间给ch,而是在刚才num开辟的空间上,改变了所占第一个字节的值,将60改成了61,61为十六进制,对应十进制为97,对应ascll码值为字符'a',可以看下方图片,改变值的地址是同一个地址,说明没有重新开辟新空间,而是直接覆盖,然后调用printf("%c\n", u.ch)时只取用1个字节的值打印,也就是只取十六进制61对应的ascll值来打印

这就是union开辟和使用空间的方式

所以回到最开始的问题:比如我们要写个加法函数,我们可以使用三个文件

我们一般把函数的声明和自定义类型的声明放在(.h)头文件中 如:(add.h)

把函数实现的定义放在(.c)文件中 如:(add.c)

把函数实现的总逻辑放在(.c)文件中 如:(test.c)

如下图所示:

运行截图:

九、作用域和生命周期

作用域:

局部变量的作用域为局部变量所在的括号的范围

就单个文件来说全局变量的作用域为整个文件

生命周期:

局部变量的生命周期是:在局部变量作用域开始时创建,出了作用域后局部变量变销毁

全局变量的生命周期是:在创建工程时创建,整个工程文件结束后销毁

static和extern

static可以用来改变

  • 修饰局部变量
  • 修饰全局变量
  • 修饰函数

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申请内存空间,重新创建初始化一个新的i,i++后变为1,所以每次都会打印1

代码二中变量int i被static修饰,说明当其出了函数之后,仍然存在,不被销毁,于是i就会不断累加,而不是每次都变为0,所以会打印1 2 3 4 5

其实static修饰局部变量的本质是,将原本在内存栈区中的局部变量放入了静态区,使得其拥有了和静态区数值一样的特征,出了程序后才会被销毁,从而改变了生命周期,但它的作用域依然没变,只能在属于他的{}中使用

内存分为栈区,堆区和静态区,分别存放不同的变量:

  • 手动申请的内存(malloc、calloc、realloc)放在堆区
  • 局部变量和函数参数放在栈区
  • 全部变量和静态变量放在静态区(静态变量就是static修饰的局部变量或全部变量)

static修饰全局变量

修饰全局变量,对全局变量的约束如下:

  • 作用域:仅限当前这一个 .c 文件
  • 不能跨文件使用,被static锁住本文件

我们来看两段代码来理解:

(1)代码一

add.c

int g_val = 2026;

test.c

#include <stdio.h> extern int g_val; int main() { printf("%d\n", g_val); return 0; }

运行截图:

(2)代码二

add.c

static 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.c

static int Add(int x, int y) { return x+y; }

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.c

static 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去掉,会发生报错:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 1:50:29

Qwopus-3.6-27B-Coder-MTP-GGUF:终极AI编程助手入门指南

Qwopus-3.6-27B-Coder-MTP-GGUF&#xff1a;终极AI编程助手入门指南 Qwopus-3.6-27B-Coder-MTP-GGUF是一款基于Qwopus3.6-27B-v2构建的推理增强型智能编码模型&#xff0c;专为开发者打造的AI编程助手。它继承了v2基础模型强大的推理能力——在MMLU-Pro&#xff08;300ex&…

作者头像 李华
网站建设 2026/10/30 1:48:12

ENVI处理Landsat 8遥感影像全流程与避坑指南

简介&#xff1a;这份ENVI遥感影像处理实验报告面向地信、遥感及相关专业学生与初学者&#xff0c;围绕Landsat8多光谱影像的系统处理流程展开&#xff0c;可用于课程实习参考、实验报告撰写借鉴与操作复盘。资源包仅含1个docx文档&#xff0c;大小约7.92MB&#xff0c;正文以文…

作者头像 李华
网站建设 2026/10/30 1:40:26

macOS 系统声音回环:BlackHole 驱动安装与配置完整指南

macOS 系统声音回环&#xff1a;BlackHole 驱动安装与配置完整指南 【免费下载链接】BlackHole BlackHole is a modern macOS audio loopback driver that allows applications to pass audio to other applications with zero additional latency. 项目地址: https://gitcod…

作者头像 李华
网站建设 2026/10/30 1:40:08

MagpieTTS Multilingual 357M:NVIDIA革命性多语言语音合成模型完整指南

MagpieTTS Multilingual 357M&#xff1a;NVIDIA革命性多语言语音合成模型完整指南 &#x1f916; 终极多语言语音合成解决方案 - NVIDIA MagpieTTS Multilingual 357M是一款革命性的文本转语音模型&#xff0c;支持9种语言和5种不同音色&#xff0c;为开发者和企业提供高质量…

作者头像 李华