函数
函数的概述
函数: 实现一定功能的,独立的代码模块,对于函数的使用,一定是先定义,后使用。
使用函数的优势: ① 我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码量。 ② 借助函数可以减少重复性的代码。 ③ 实现结构化(模块化:C 语言中的模块化其实就是多文件 + 函数)程序设计思想。
关于结构化设计思想: 将大型的任务功能划分为相互独立的小型的任务模块来设计(多文件 + 函数)
函数的作用: (1) 代码复用:避免重复编写相同功能的代码。 (2) 模块化设计:将复杂程序拆分成多个小功能模块,每个函数负责一个独立任务,使代码逻辑结构更加清晰。 (3) 便于维护和调试:单个函数功能单一,出现问题容易定位和修改,不需要改动整个程序。 (4) 提高开发效率:便于多人协同开发时,分工明确,编写不同函数,最终组合成完整程序
函数是 C 语言程序的基本组成单元: C 语言程序必须包含一个
main
函数,可以包含零个或多个其他函数。
函数的分类
按来源分:
库函数: C 语言标准库实现的并提供使用的函数,如:
scanf()
、printf()
、fgets()
、fputs()
、strlen()
…自定义函数: 需要程序员自行实现,开发中大部分函数都是自定义函数。
按参数分:
无参函数: 函数调用时,无需传递参数,可有可无返回值,如:
show_all();
有参函数: 函数调用时,需要参数传递数据,经常需要配套返回值来使用,如:
printf("%d\n", 12);
按返回值分:
有返回值函数: 函数执行后返回一个值,如:
if (scanf("%d", &num) != 1)
无返回值函数(
void
): 函数仅执行操作,不返回值
从函数调用的角度分:
主调函数: 主动去调用其他函数的函数。(
main
函数只能作为主调函数)被调函数: 被其他函数调用的函数。
举例
//主调函数
int main()
{
//被调函数
printf("hello world!\n");
}
注意:很多时候,尤其是自定义函数,一个函数可能既是主调函数,又是被调函数
int fun_b()
{
printf("函数B\n");
}
// fun_a是主调函数
int fun_a()
{
printf("函数A\n");
// fun_b是背调函数
fun_b();
}
int main()
{
// fun_a是被调函数
fun_a();
}
以上案例中,fun_a()相对fun_b()来说是主调函数,同时对于main()函数来说,它又是被调函数。
函数的定义
定义
语法
[返回类型] 函数名([形参列表]) //--函数头|函数首部
{
函数体语句; //--函数体:整个{}包裹的内容属于函数体,函数的{}不能省略
}
函数头
返回类型:函数返回的类型
函数名:函数的名称,遵循标识符命名(不能以数字开头,只能包含大小写字母、下划线、数字。建议:小写+下划线)
形参列表:用于接收主调函数传递的数据,若有多个参数,使用
,
分隔,切换每一个形参需要知名类型。
① 形参列表(被调函数):主调函数给被调函数传递数据:主调函数 → 被调函数
② 返回类型(被调函数):被调函数给主调函数返回数据:被调函数 → 主调函数
通过生活中的案例理解函数调用:
假设:饮料店的工作人员通过榨汁机榨取新鲜果汁
理解:
工作人员:主调函数
榨汁机:被调函数
水果:传递的参数
果汁:函数的返回值
①工作人员向榨汁机放入一个水果:主调函数调用被调函数,并传递数据 ②工作人员用杯子接收榨汁机榨出的果汁:主调函数接收被调函数返回的数据
说明
函数的返回值:就是返回值的类型,两个类型可以不同,但是必须能够进行转换
举例
double fun_a() // 函数的返回类型是:double
{
return 12; // 函数的返回值是:int
}
// 分析:此时需要转换,函数在执行的时候,会自动提升int的类型为double,此时属于隐式转换,正常转换,以上正确
--------------------------------------------------
int[] fun_b() //函数的返回类型是:int[]
{
return 12; //函数的返回值是:int
}
//分析:此时需要提升int为int[],int不能转换为int[],所以错误!
---------------------------------------------------
int fub_c() //函数返回类型是:int
{
return 12.3; //函数的返回值是double
}
//此时需要将double类型转换为int类型,浮点型转整数,会丢失小数部分,保留整数位,以上正确
可将函数的返回类型理解为变量的类,将函数的返回值理解为变量的值
#include <stdio.h>
double fun_a()
{
return 12;// 就是将int类型的12赋值给double类型的匿名变量 int -->double
}
int fun_b()
{
return 12.5;// 就是将double类型的12.5赋值给int类型的匿名变量double --> int 此时会舍弃掉小数部分
}
double fun_c()
{
return 12.5; // 就是将double类型的12.5赋值给double类型的匿名变量double --> double
}
int main(int argc,char *argv[])
{
// 接收函数返回值,函数返回什么类型,就用什么类型接收
double result1 = fun_a();// 主调函数使用double来接收被调函数返回的double,double --> double
printf("%lf\n",result1);
int result2 = fun_b(); // 主调函数使用int来接收被调函数返回的int,int --> int
printf("%d\n",result2);
int result3 = (int)fun_c(); // 主调函数使用int来接收被调函数返回的double,int --> (int)double
printf("%d\n",result3);
return 0;
}
在C语言中,无返回值时,应明确使用void类型(空类型/无类型),举例:
void test() //此时这个函数没有返回值,若需要提前结束函数,写法:return;
{
printf("hello world!\n");
}
//下面写法等价于上面
void test()
{
printf("hello world!\n");
return; //一般这个return;省略不写
}
在C语言中,C89标准允许函数的返回类型标识符可以省略,如果省略,默认返回int。C99/C11标准要求必须明确指定返回类型,不再支持默认int类型,举例
//写法1(C89标准),main的返回类型是int类型,默认返回值是0,等价于写法2 不推荐
main()
{
...
}
//写法2(C99后推荐)等价于上面写法
int main()
{
return 0;
}
- 函数中返回语句的形式为
return(表达式)
或者return 表达式
//写法1
int main()
{
return(0);
}
//写法2
int main()
{
return 0;
}
如果
参数列表
中有多个参数,则他们要用,
分隔:即使他们类型相同,在形式参数中只能逐个进行说明,举例:
//正确举例
int avg(int x, int y, int z)
{
...
}
//错误举例
int avg(int x, y, z)
{
...
}
如果
`形参列表
中没有参数,我们也可以不写,用void标识
int main()
{
...
}
int main(void)
{
...
}
C89开始提供了变长参数,也就是一个函数的参数个数可以是不确定的。需要引入
<stdarg.h>
语法:
[返回类型]函数名(参数1,...)
{
...
}
举例:
#include <stdio.h>
#include <stdarg.h>
// 计算n个整数的平均值
double average(int n, ...) { // ...只能放在 具体的参数列表的后面
va_list args; // 声明参数列表对象
int sum = 0;
va_start(args, n); // 初始化参数列表,n是最后一个固定参数
// 遍历所有可变参数
for (int i = 0; i < n; i++) {
// 获取一个int类型的参数
sum += va_arg(args, int);
}
va_end(args); // 清理参数列表
return (double)sum / n;
}
int main() {
printf("平均值:%.2f\n", average(3, 10, 20, 30)); // 20.00
printf("平均值:%.2f\n", average(5, 1, 2, 3, 4, 5)); // 3.00
return 0;
}
案例
案例1
需求:计算1~n之间自然数的阶乘‘
代码
#include <stdio.h>
/**
* 定义一个函数,实现1~n之间的阶乘计算
* @param n:阶乘上限
* @return n的阶乘值
*/
size_t fun_1(int n)
{
int i; // 循环变量
size_t s = 1; // 阶乘值,初始值是1
for (i = 1; i <= n; i++) s *= i;
return s;
}
int main(int argc,char *argv[])
{
printf("1~12的阶乘结果是:%lu\n", fun_1(12));
printf("1~20的阶乘结果是:%lu\n", fun_1(20));
printf("1~30的阶乘结果是:%lu\n", fun_1(30));
printf("1~40的阶乘结果是:%lu\n", fun_1(40));
return 0;
}
运行结果
案例2
需求:计算一个圆台两个面的面积之和
实现:
定义一个函数,传递一个圆的半径,返回一个圆的面积
代码
#include <stdio.h>
#include <math.h>
#define PI 3.1415926
/**
* 定义一个函数:实现圆的面积计算
* @param r:圆的半径
* @return 圆的面积
*/
double cicle_area(double r)
{
// return PI * r * r;
return PI * pow(r,2.0); // pow(底数,指数);编译需要加 -lm
}
int main(int argc,char *argv[])
{
// 定义两个半径,两个面积
double r1,r2,area1,area2;
printf("请输入两个圆的半径:\n");
scanf("%lf,%lf", &r1, &r2);
// 计算面积
area1 = cicle_area(r1);
area2 = cicle_area(r2);
printf("一个圆台两个面的面积之和是%lf\n", area1 + area2);
return 0;
}
编译命令
gcc demo.c -lm
形参和实参
形参(形式参数)
定义
函数定义时指定的参数,形参是用来接收数据的。函数在定义时,系统不会为形参申请类型,只有当函数调用时,系统才会为形参申请类型。主要用于存储实际参数,并且当函数返回时(执行return),系统 会自动回收为形参申请的内存。
C语言中所有的参数传递都是值传递。
若要修改实参,需要传递指针,指针本质上也是值传递
案例:
需求:判断一个数是奇数还是偶数
代码
#include <stdio.h>
/**
* 方式1
*/
void fun1(int n) // 这里的n就是形参
{
if (n % 2 == 0)
{
printf("%d是偶数!\n", n);
return; // 提前结束函数,后续代码不再执行
}
printf("%d是奇数!\n", n);
}
/**
* 方式2
*/
int fun2(int n)
{
if (n % 2 == 0)
{
printf("%d是偶数!\n", n);
return -1;
}
printf("%d是奇数!\n", n);
return 0;
}
int main(int argc,char *argv[])
{
fun1(5);
fun2(5);
return 0;
}
实参(实际参数)
定义
实参是函数调用时由主调函数传递给被调函数的具体的数据。实参可以是常量,变量,表达式,带有返回值的函数等
关键特性:
类型多样性
实参可以是常量,变量或表达式
例:
fun(12); //常量作为实参 fun(a); //变量作为实参 fun(a+12); //表达式作为实参 fun(func()); //带有返回值的函数作为实参
2.类型转换
当实参和形参类型不同是,会按照赋值规则进行类型转换。
类型转换可能导致精度丢失
例
#include <stdio.h> //求一个数的绝对值 double fabs(double a) { return a < 0 ? -a : a; } int main() { int x =12,y = -12; int x1 = (int)fab(x); //此时,x会被隐式转换为double类型,fabs返回的是double类型的数据 int y1 = (int)fabs(y); }
注意:函数调用时,通过实参给形参赋值。形参类似于变量,实参类似于变量的值。
函数调用时:
主调函数通过实参给被调函数的形参赋值,此时可以理解为;将主调函数的值赋给被调函数的变量。
函数返回时:
被调函数通过返回值给主调函数赋值,可理解为:将被调函数的值赋给主调函数的变量
3.单向值传递
C语言采用单向值传递机制(赋值的方向:实参→形参)
实参仅将其值赋给形参,不传递实参本身。
形参值不会影响实参
案例
void modify() //n的变量地址:0x11 { n = 20; //修改0x11这个空间的数据位20 } int main() { int n = 10; //n的变量地址0x21 modify(n); //将0x21中的数据赋给0x11这个空间 printf("%d\n", n); //结果为10 }
4.内存独立性
实参和形参在内存中占据不同的空间
形参拥有独立的内存地址
#include <stdio.h>
int fun(int n) // n是形参
{
printf("形参n的值:%d\n", n);
n += 5; // 修改形参的数据
return n;
}
int main()
{
int a = 10;
printf("调用前实参a的值:%d\n", a); // 10
// 变量作为实参
int res = fun(a); // a 是实参
printf("调用后实参a的值:%d\n", a); // 10
printf("函数返回值:%d\n", res); // 15
// 常量作为实参
fun(12); //字面量12作为实参
//表达式作为实参
fun(a + 12); //表达式作为实参
return 0;
}
案例:
需求:输入4个数,要求用一个函数求出最大数。
分析
设计一个函数
- 多次复用这个函数实现最终的求值
代码
#include <stdio.h> /** * 定义一个函数,求2个数的最大值 * @param x,y:参与比较的整数 * @return 返回最大值 */ int get_max(int x, int y) { return x > y ? x : y; } int main(int argc,char *argv[]) { // 定义4个变量,用来接收控制台输入 int a,b,c,d; // 定义一个变量,存储最大值 int max; printf("请输入4个整数:\n"); scanf("%d%d%d%d", &a, &b, &c, &d); // 求a,b最大值 max = get_max(a,b); // 求a,b,c最大值 max = get_max(max,c); // 求a,b,c,d最大值 max = get_max(max,d); printf("%d,%d,%d,%d中的最大值 是%d\n",a,b,c,d,max); return 0; }
运行结果
函数的返回值
定义:
若不需要返回值,函数可以没有return语句
// 如果返回类型是void,return关键字可以省略 void fun1() { ... // return; } // 这种写法,return关键字也可以省略,但是此时默认返回是 return 0 int fun2() { ... // return 0; } // 这种写法,return关键字也可以省略,但是此时默认返回是 return 0 fun3() // 如果不写返回类型,默认返回int,C99/C11之后不再支持省略返回类型 { ... // return 0; }
一个函数中可以有多个return语句,但是同一时刻只有一个return语句被执行
#include <stdio.h> int eq(int num) { if (num % 2 == 0) return 0; return 1; } int main() { int num = 5; printf("%d是一个%s\n", num, eq(num) == 0 ? "偶数" : "奇数"); //5是一个奇数 }
返回类型一般情况下要和函数中return语句返回的数据类型一致,如果不一致,要符合C语言中的隐式转换规则
double add(int a, int b) // 返回类型是double
{
return a + b; // 返回值的类型是int
}
// 简化理解: double add = a + b;
案例:
输入两个整数,要求用一个函数求出最大值
实现1:不涉及类型转换
#include <stdio.h>
int get_max(int x, int y)
{
if (x > y) return x;
return y;
}
int main()
{
int a,b,max;
printf("请输入两个整数:\n");
scanf("%d%d",&a,&b);
max = get_max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,max);
}
实现2:涉及类型转换--隐式转换
#include <stdio.h>
double get_max(int x, int y) // int隐身转换为double
{
if (x > y) return x;
return y;
}
int main()
{
int a,b,max;
printf("请输入两个整数:\n");
scanf("%d%d",&a,&b);
max = (int)get_max(a,b); // 显示转换
printf("%d,%d中的最大值是%d\n",a,b,max);
}
实现3:涉及类型转换--显示转换
#include <stdio.h>
int get_max(int x, int y) // 将double类型转换为int类型,可以隐式转换,也可以显示转换
{
double z;
z = x > y ? x : y;
return (int)z; // 显示转换
}
int main()
{
int a,b,max;
printf("请输入两个整数:\n");
scanf("%d%d",&a,&b);
max = get_max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,max);
}
函数的调用
调用方式
①函数语句
test(); // 对于无返回值的函数,直接调用
int res = max(2,4); // 对于有返回值的函数,一般需要在主调函数中接收被调函数的返回值
②函数表达式
4 + max(2,4)
scanf("%d", &num) != 1
(c = getchar()) != '\0'
③函数参数
printf("%d", (int)fabs(number)); // 函数作为实参
注意:函数可以作为函数的实参,如果要作为形参,必须使用函数指针。
在一个函数中调用另一个函数具备以下条件:
被调用的函数必须是已经定义的函数。
若使用库函数,应在本文件开头用
#include
包含其对应的头文件。若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
函数的声明
函数调用时,往往要遵循先定义后使用的原则,但我们对函数的调用操作出现在函数定义之前,则需要对函数进行声明。
定义;
完整的函数使用应该分为三部分
[函数声明]
int max(int x, int y, double z); //函数声明只保留函数头,便于编译系统进行检查
int max(int ,int ,double ); //函数声明时,可以省略形参名称
函数声明如果是在同一个文件,一定要定义在文件中所有函数定义的最前面。如果有对应的 .h
文件,可以将函数的声明抽取到 .h
中。
函数定义
int max(int x, int y, double z); //函数定义时,一定不能省略形名称
{
return x > y ? x : y > z ? : y : (int)z;
}
函数定义的时候,不能省略形参的数据类型、参数个数、参数名称,位置要和函数声明完全一致。
函数定义时参数列表要和函数声明时的参数列表完全对应,同时函数定义要保留形参名称
函数调用
int main()
{
printf("%d\n",max(4,5,6));
}
作用
C语言的函数声明是为了提前甘肃编译系统函数的名称、返回类型和参数,这样在函数实际定义之前就能安全调用它,避免编译错误,同时检查参数和返回值是否正确。相当于给编译器一个“预告”,确保代码正确编译和运行
使用
错误示例:被调函数写在主调函数之后
// 主调函数 int main() { printf("%d\n", add(12, 13)); // 编译报错,因为函数未经过声明,编译系统无法检查函数的合法性 } // 被调函数 int add(int x, int y) { return x + y; }
正确示例:主调函数在被调函数之后
// 被调函数 int add(int x, int y) { return x + y; } // 主调函数 int main() { printf("%d\n", add(12, 13)); // 编译正确 }
注意:如果函数的调用比较简单,如 a 函数调用 b 函数,b 函数定义在 a 函数之前,此时是可以省略函数声明的。
正确演示:被调函数和主调函数无法区分前后,必须要增加函数声明
// 函数声明 void funa(int, int); void funb(int, int); // 函数定义 void funb(int a, int b) { ... // 函数调用 funa(); } void funa(int a, int b) { ... // 函数调用 funb(); } int main() { // 函数调用 funa(12,13); }
声明的方式:
函数头加上分号:
int add(int a, int b);
函数头加上分号,可省略形参名称,但不能省略参数类型:
int add(int, int);
变量和函数底层工作原理【扩展】
一、变量的底层执行机制
变量本质是内存中的一块存储空间,其底层处理涉及编译期的符号解析和运行时的内存分配与访问。
编译阶段:符号表与地址映射 编译器为每个变量创建符号表条目,记录变量名、类型、作用域和内存偏移量(而非实际地址):
全局变量和静态变量:分配到数据段(已初始化)或 BSS 段(未初始化),并计算其在段内的偏移量。
局部变量:记录其在栈帧中的相对位置(基于栈指针的偏移量)。
运行阶段:内存分配与访问
全局 / 静态变量:程序加载时,操作系统将数据段和 BSS 段加载到内存的固定位置,变量的实际地址 = 段起始地址 + 编译期计算的偏移量。
局部变量:函数调用时,CPU 为函数创建栈帧,局部变量的地址 = 栈指针(SP) + 编译期确定的偏移量(通常为负数,因栈向下生长)。
动态变量(malloc):通过系统调用在堆中分配内存,返回的指针是堆中实际地址,由内存管理模块(如 glibc 的 ptmalloc)维护。
访问变量的底层指令 访问变量时,CPU 通过地址计算得到内存地址,再执行加载(load)或存储(store)指令。例如,
int a = 5;
会被编译为:计算a
的地址,然后执行store 5
到该地址。
二、函数的底层执行机制
函数的执行本质是指令流的跳转与栈帧管理,涉及函数调用、栈帧创建、参数传递和返回值处理。
编译阶段:函数地址与指令生成 编译器将函数体编译为一系列机器指令,存储在代码段(只读),并在符号表中记录函数名与起始地址。函数参数和返回值的传递方式(如栈传递、寄存器传递)由调用约定(如 cdecl、stdcall)决定,编译器按约定生成对应指令。
函数调用的底层步骤
步骤 1:参数入栈:调用者将参数按约定顺序(通常从右到左)压入栈中,或放入指定寄存器(如 x86-64 的部分参数用寄存器传递)。
步骤 2:保存返回地址:CPU 将下一条指令的地址(函数调用后的执行点)压入栈中,供函数返回时使用。
步骤 3:跳转至函数入口:执行
call
指令,将程序计数器(PC)设置为函数的起始地址,开始执行函数指令。步骤 4:创建栈帧:函数执行的第一条指令通常为:
push ebp ; 保存调用者的栈帧基址 mov ebp, esp ; 用当前栈指针作为新栈帧的基址 sub esp, N ; 为局部变量分配N字节的栈空间
此时栈帧包含:参数、返回地址、上一个栈帧基址(ebp)、局部变量。
步骤 5:执行函数体:按编译生成的指令执行逻辑,访问局部变量(通过 ebp 偏移)、操作参数(通过 ebp 正偏移)。
步骤 6:返回结果:返回值通常存入指定寄存器(如 x86 的 eax,x86-64 的 rax),或通过栈传递(大型结构体)。
步骤 7:恢复栈帧并返回,执行:
mov esp, ebp ; 释放局部变量的栈空间 pop ebp ; 恢复调用者的栈帧基址 ret ; 弹出返回地址到PC,跳转回调用者
三、关键底层概念
内存分段:代码段(指令)、数据段(全局变量)、BSS 段(未初始化全局变量)、栈(局部变量 / 函数调用)、堆(动态内存)。
栈帧:每个函数调用对应一个栈帧,包含参数、返回地址、局部变量,由 ebp(基址指针)和 esp(栈指针)界定。
地址绑定:变量和函数的地址在编译期(静态绑定)或加载 / 运行期(动态绑定,如共享库)确定。
总结
变量:通过编译期符号表记录偏移量,运行时映射到实际内存地址,通过 CPU 的加载 / 存储指令访问。
函数:通过
call
指令跳转至代码段执行,借助栈帧管理参数、局部变量和返回地址,最终通过ret
指令返回。