目录
引言
一、函数的基本概念
二、函数的定义与结构
一、函数的定义
二、函数的分类
1. 库函数(标准函数)
2. 自定义函数
三、函数的调用、参数和返回值
1. 调用格式
调用的前提:函数声明或定义在前
调用的执行流程
2.函数的参数
3、函数的返回值
四、函数的声明(函数原型)
一、函数声明的格式
二、为什么需要函数声明?
三、函数声明的位置
1. 放在调用函数之前(如main函数之前)
2. 放在头文件(.h)中
四、函数声明的注意事项
五、函数的嵌套调用
函数嵌套调用的基本形式
执行流程分析
程序执行流程
六、函数的递归调用
递归的核心要素
递归调用的执行机制
七、数组作为函数参数
一、一维数组作为函数参数的形式
1. 数组形式声明(推荐,更直观)
2. 指针形式声明(与数组形式等价)
二、二维数组作为函数参数
1.正确的声明方式:
2、注意事项
引言
在 C 语言中,函数是模块化编程的核心单位,用于封装一段具有特定功能的代码块,实现代码复用、逻辑分离和程序结构优化。理解函数的定义、调用及特性是掌握 C 语言的关键。
一、函数的基本概念
函数是完成特定任务的独立代码单元,具有以下特点:
- 独立性:函数内部的代码逻辑与其他部分隔离,通过参数和返回值与外界交互。
- 复用性:定义一次后可被多次调用,避免重复编码。
- 模块化:将复杂程序拆分为多个函数,便于调试和维护。
二、函数的定义与结构
一、函数的定义
C 语言函数的定义格式如下:
返回值类型 函数名(参数列表) {
// 函数体(实现功能的代码)
[return 返回值;] // 若返回值类型为void,可省略return
}
各部分说明
- 返回值类型:函数执行完毕后返回的数据类型(如int、float、char等)。若无需返回值,用void表示。
- 函数名:遵循标识符命名规则(字母、数字、下划线,首字符不为数字),需见名知意(如sum、getMax)。
- 参数列表:函数接收的输入数据,格式为 “类型 参数名,类型 参数名…”。若无需参数,可写void或空括号。
- 函数体:用{}包裹的代码块,包含实现功能的语句。
示例:定义一个求两数之和的函数
// 返回值类型为int,参数为两个int类型的a和b
int add(int a, int b) {
int result = a + b;
return result; // 返回计算结果
}
二、函数的分类
根据函数的来源可以分为两类:库函数和自定义函数
1. 库函数(标准函数)
由 C 语言标准库提供的预定义函数,用于实现常用功能(如输入输出、字符串处理、数学运算等),无需用户定义即可直接调用,但需包含对应的头文件。
特点:
- 由编译器厂商实现,符合 C 语言标准;
- 功能通用,可直接复用,避免重复开发。
常见类别及示例:
- 输入输出函数:printf()、scanf()(需包含<stdio.h>);
- 字符串函数:strlen()、strcpy()(需包含<string.h>);
- 数学函数:sqrt()(开平方)、pow()(幂运算,需包含<math.h>);
- 内存操作函数:malloc()、free()(需包含<stdlib.h>)。
2. 自定义函数
由用户根据具体需求自行定义的函数,用于实现特定的逻辑。
特点:
- 灵活性高,可根据需求定制功能;
- 是模块化编程的核心,用于拆分复杂程序。
示例:
// 自定义函数:求两数中的最大值
int getMax(int a, int b) {
return a > b ? a : b;
}
三、函数的调用、参数和返回值
使用已经定义的函数称为 “函数调用”,通过函数名传递参数,获取返回值(若有)。
1. 调用格式
函数调用的格式根据是否有返回值略有不同:
-
有返回值的函数:需用对应类型的变量接收返回结果(或直接作为表达式使用)。
返回值类型 变量名 = 函数名(实参列表);
// 示例:调用add函数,接收返回值
int sum = add(3, 5); -
无返回值的函数(void 类型):直接调用,无需接收返回值。
函数名(实参列表);
// 示例:调用printHello函数
printHello();
示例:调用add函数
#include <stdio.h>
// 函数定义
int add(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 5;
// 调用add函数,将实参x、y传递给形参a、b,接收返回值
int sum = add(x, y); //此时a=x=3,b=y=5
/*这时候的sum = add(x,y),代入上面函数定义进行计算*/
printf("和为:%d\\n", sum); // 输出:和为:8
return 0;
}
调用的前提:函数声明或定义在前
编译器在编译时需要知道函数的 “原型”(返回值类型、参数类型),否则会报错。因此,函数调用需满足以下条件之一:
- 函数定义在调用之前(定义本身包含了原型信息);
- 调用之前已进行函数声明(提前告知编译器函数原型)。
示例:函数声明的必要性
#include <stdio.h>
// 函数声明(告知编译器:存在一个add函数,接收两个int,返回int)
int add(int a, int b);
int main() {
int result = add(2, 3); // 调用前已声明,编译通过
printf("%d\\n", result); // 输出5
return 0;
}
// 函数定义(在调用之后,需提前声明)
int add(int a, int b) {
return a + b;
}
调用的执行流程
2.函数的参数
实参和形参的关系
- 形参:函数定义时的参数(如add中的a、b),仅在函数内部有效,相当于局部变量。
- 实参:函数调用时传递的参数(如add(x, y)中的x、y),可以是常量、变量或表达式。
- 传递规则:C 语言采用值传递,即实参的值会被复制给形参,形参的修改不会影响实参。
用表格的形式进行区分:
形参 | 函数定义时括号中的参数(如int add(int a, int b)中的a、b) | 仅在函数内部有效(函数调用时创建,执行结束后销毁) | 相当于函数内部的局部变量,用于接收实参的值 |
实参 | 函数调用时括号中的参数(如add(3, 5)中的3、5) | 与调用处的变量 / 表达式作用域一致 | 可以是常量、变量、表达式(如add(x+2, y*3)) |
示例:值传递的特性(形参修改不影响实参)
#include <stdio.h>
// 尝试交换两个变量的值(值传递无法实现)
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
// 函数内部a、b已交换,但不影响实参
}
int main() {
int x = 1, y = 2;
swap(x, y);
printf("x=%d, y=%d\\n", x, y); // 输出:x=1, y=2(未交换)
return 0;
}
【解释】
3、函数的返回值
返回值是函数执行完毕后向调用者返回的结果,由return语句指定,其类型需与函数定义的返回值类型一致。
返回值的基本规则
-
返回值类型匹配:return语句后的表达式类型必须与函数定义的返回值类型一致(或可隐式转换,如int→float)。
// 正确:返回值类型为int,return后是int类型
int getOne() {
return 1; // 正确
}// 错误:返回值类型为int,return后是float类型(无法隐式转换)
int getPi() {
return 3.14; // 编译警告:返回值被截断为3
} -
void类型的函数:无需返回值,return语句可省略,或仅用return;提前结束函数。
void printMsg() {
printf("Hello");
return; // 可省略,函数执行到末尾自动结束
} -
一次函数调用仅返回一个值:C 语言函数只能返回一个值,若需返回多个结果,可通过指针参数间接修改(如swap函数),或返回结构体(包含多个成员)。
总结
- 函数调用:通过函数名触发执行,需提前声明或定义函数,遵循 “先声明 / 定义,后调用” 原则。
- 参数传递:C 语言采用值传递,形参是实参的副本;通过指针参数可修改实参,数组参数需额外传递长度。
- 返回值:由return语句指定,类型需与函数定义一致;void函数无返回值,且不可返回局部变量的地址。
四、函数的声明(函数原型)
在 C 语言中,函数声明(又称函数原型)是一种告诉编译器函数名称、返回值类型和参数类型的声明语句。它的主要作用是让编译器在编译阶段提前知道函数的 “接口信息”,确保函数调用的合法性(如参数类型、数量匹配),即使函数定义在调用之后也能正常编译。
一、函数声明的格式
函数声明的基本格式与函数定义的首部类似,只需指定返回值类型、函数名和参数类型(参数名可省略),结尾加;即可:
返回值类型 函数名(参数类型1, 参数类型2, …);
// 或(保留参数名,增强可读性)
返回值类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, …);
//第一种
int Max(int,int)
//第二种
int Max(int x,int y);
说明:
- 返回值类型:与函数定义中的返回值类型完全一致(如int、float、void等)。
- 函数名:必须与函数定义中的名称完全相同(区分大小写)。
- 参数类型:需按顺序列出函数定义中每个参数的类型;参数名可写可不写(写参数名仅为了提高可读性,对编译器无实际意义)。
- 结尾的;:函数声明是语句,必须以分号结束,这是与函数定义(用{}包裹函数体)的核心区别。
二、为什么需要函数声明?
C 语言编译器是 “单遍编译” 的,即从代码开头到结尾逐行解析。这也是为了便于对于函数调用的合法性进行检查。如果函数的定义在调用之后,编译器在遇到函数调用时会因不知道函数的参数和返回值信息而报错。
示例:缺少函数声明导致的错误
#include <stdio.h>
int main() {
int result = add(2, 3); // 错误:编译器此时不知道add函数的存在
printf("%d\\n", result);
return 0;
}
// 函数定义在调用之后
int add(int a, int b) {
return a + b;
}
编译时会提示类似 “‘add’未声明” 的错误,此时就需要通过函数声明解决。
三、函数声明的位置
函数声明可以放在两个位置,根据场景选择:
1. 放在调用函数之前(如main函数之前)
这是最常见的方式,直接在源文件中函数调用处的前面声明。
示例:在main前声明函数
#include <stdio.h>
// 函数声明(参数名可省略)
int add(int, int); // 或 int add(int a, int b);
int main() {
int result = add(2, 3); // 调用时编译器已知add的接口,编译通过
printf("%d\\n", result); // 输出5
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
2. 放在头文件(.h)中
当多个源文件(.c)需要调用同一个函数时,可将函数声明放在头文件中,通过#include引入,避免重复声明。
示例:多文件编程中的函数声明
-
add.h(头文件,存放函数声明):
// 防止头文件重复包含
#ifndef ADD_H
#define ADD_H// 函数声明
int add(int a, int b);#endif
-
add.c(源文件,存放函数定义):
#include "add.h" // 引入头文件(非必需,但建议保持一致)
// 函数定义
int add(int a, int b) {
return a + b;
} -
main.c(源文件,调用函数):
#include <stdio.h>
#include "add.h" // 引入头文件,获取add函数的声明int main() {
printf("%d\\n", add(2, 3)); // 正确调用
return 0;
}
四、函数声明的注意事项
声明与定义必须一致 函数声明的返回值类型、函数名、参数类型和数量必须与函数定义完全一致,否则会导致编译错误。
// 函数定义
float multiply(int x, float y) {
return x * y;
}
// 错误的声明(返回值类型不匹配)
int multiply(int x, float y); // 编译报错
// 错误的声明(参数类型顺序不匹配)
float multiply(float x, int y); // 编译报错
参数名可省略,但建议保留 声明时参数名对编译器无意义,但保留参数名可提高代码可读性,方便他人理解参数的用途。
// 合法但可读性差
int calculate(int, float, char);
// 更推荐(参数名说明用途)
int calculate(int num, float rate, char type);
void参数的特殊性 对于无参数的函数,声明时需明确写void(或空括号,但建议写void以显式说明无参数)。
// 函数定义:无参数
void printHello() {
printf("Hello\\n");
}
// 正确的声明(显式说明无参数)
void printHello(void);
// 也合法(但不推荐,在C++中含义不同)
void printHello();
可重复声明 同一个函数可以在多个地方重复声明(只要声明内容一致),但函数定义只能有一次。
// 第一次声明
int add(int a, int b);
// 第二次声明(与第一次一致,合法)
int add(int x, int y);
// 函数定义(仅一次)
int add(int a, int b) {
return a + b;
}
通过正确使用函数声明,既能解决 “函数定义在后,调用在前” 的编译问题,也能在多文件编程中实现函数的跨文件调用。记住:声明与定义必须一致,参数名可省但建议保留,无参函数用void显式声明。
五、函数的嵌套调用
在 C 语言中,函数的嵌套调用是指在一个函数的执行过程中调用另一个函数,而被调用的函数在执行过程中还可以继续调用其他函数。这种调用方式形成了函数调用的层次结构,是 C 语言中非常常见且重要的编程方式。
函数嵌套调用的基本形式
#include <stdio.h>
// 函数3:最内层函数
void func3() {
printf("这是func3\\n");
}
// 函数2:调用func3
void func2() {
printf("进入func2,准备调用func3\\n");
func3(); // 嵌套调用func3
printf("离开func2\\n");
}
// 函数1:调用func2
void func1() {
printf("进入func1,准备调用func2\\n");
func2(); // 嵌套调用func2
printf("离开func1\\n");
}
// 主函数:程序入口,调用func1
int main() {
printf("进入main,准备调用func1\\n");
func1(); // 调用func1
printf("离开main,程序结束\\n");
return 0;
}
执行流程分析
上述代码的执行顺序是:
输出结果:
进入main,准备调用func1
进入func1,准备调用func2
进入func2,准备调用func3
这是func3
离开func2
离开func1
离开main,程序结束
实际示例:计算圆的面积
#include <stdio.h>
// 计算平方
float square(float x) {
return x * x; //返回结果为X乘以X的结果
}
// 计算圆的面积
float circle_area(float radius) {
const float PI = 3.14159; //定义一个常量PI
// 嵌套调用square函数计算半径的平方
return PI * square(radius);
//圆的面积公式是:π × 半径 ²,所以用 PI 乘以半径的平方(通过 square 函数获得)
}
// 显示圆的面积
void display_area(float area) {
printf("圆的面积是: %.2f\\n", area);
} //void表示该函数没有返回值
int main() {
float r = 5.0;
// 嵌套调用:main调用display_area,而display_area的参数来自circle_area的返回值
display_area(circle_area(r));
return 0;
/*
首先执行circle_area(r),计算半径为 5.0 的圆的面积
然后将circle_area(r)的返回值作为参数传递给display_area函数
display_area函数负责将结果显示在屏幕上
return 0;:表示程序正常结束*/
}
程序执行流程:
程序执行流程
- 在circle_area函数中,又调用了square(radius)函数
- square函数计算 5.0 的平方,返回 25.0
- circle_area函数用 PI(3.14159)乘以 25.0,得到面积约为 78.53975
六、函数的递归调用
递归调用是指一个函数直接或间接调用自身的过程。递归是一种强大的编程技巧,特别适合解决可以分解为相似子问题的任务,如阶乘计算、斐波那契数列、树结构遍历等。
递归的核心要素
递归要能正常工作,必须满足两个关键条件:
递归调用的执行机制
递归调用的过程可以理解为 "入栈" 和 "出栈" 的过程(数据结构中的“栈”):
- 每次调用函数时,系统会在栈内存中为其分配空间(保存局部变量、返回地址等)
- 当达到终止条件时,函数开始逐层返回,释放栈空间,并将结果向上传递
示例 1:计算阶乘
阶乘的数学定义为:n! = n × (n-1) × (n-2) × … × 1,且0! = 1。这是递归的经典案例:
#include<stdio.h>
long Factorial(int n)
{
long result = 1;
if (n == 0)
result = 1;
//终止条件:当n == 0时,将result设为 1(因为 0! = 1)
else
{
result = Factorial(n – 1) * n;
return result;
//递归调用:当 n > 0 时,通过Factorial(n – 1) * n计算阶乘,即 n! = (n-1)! × n
}
}
int main()
{
int n;
long result;
printf("请输入n的值:");
scanf("%d", &n);
result = Factorial(n);
printf("%d! = %ld\\n", n, result);
return 0;
}
输出结果:
请输入n的值:3
3!=6
请输入n的值:0
0!=1
示例2:汉诺塔问题
汉诺塔是经典的递归问题,规则是:将 n 个盘子从 A 柱移到 C 柱,每次只能移动一个盘子,且大盘子不能放在小盘子上。
#include <stdio.h>
// 函数功能:将n个盘子从from柱移到to柱,借助aux柱
// 参数说明:
// n:盘子数量
// from:源柱子
// aux:辅助柱子
// to:目标柱子
void hanoi(int n, char from, char aux, char to) {
// 终止条件:只有1个盘子时,直接从from移到to
if (n == 1) {
printf("将盘子1从%c移到%c\\n", from, to);
return;
}
// 步骤1:将n-1个盘子从from移到aux,借助to
hanoi(n-1, from, to, aux);
// 步骤2:将第n个盘子从from移到to
printf("将盘子%d从%c移到%c\\n", n, from, to);
// 步骤3:将n-1个盘子从aux移到to,借助from
hanoi(n-1, aux, from, to);
}
int main() {
int num;
printf("请输入汉诺塔的盘子数量:");
scanf("%d", &num);
printf("移动%d个盘子的步骤如下:\\n", num);
// 从A柱移到C柱,借助B柱
hanoi(num, 'A', 'B', 'C');
return 0;
}
七、数组作为函数参数
在C语言中,数组作为函数参数传递时,实际上传递的是数组首元素的地址(即指针),而非整个数组的副本。这意味着函数内部对数组元素的修改会影响原始数组。
一、一维数组作为函数参数的形式
数组作为函数参数时,有两种常见的声明方式(本质相同):
1. 数组形式声明(推荐,更直观)
// 形参声明为"数组类型",但实际会被视为指针
void func(int arr[], int length) {
// 函数体:访问数组元素
}
2. 指针形式声明(与数组形式等价)
// 形参声明为指针,明确表示接收数组首地址
void func(int *arr, int length) {
// 函数体:访问数组元素
}
核心特性:数组名退化为指针
C 语言中,数组名作为参数传递时,会自动转换为指向数组首元素的指针(即 “退化”)。这意味着:
- 函数内部无法通过sizeof(arr)获取原数组的总大小(sizeof(arr)得到的是指针的大小,通常为 4 或 8 字节)
- 函数对数组元素的修改会影响原数组(因为操作的是同一块内存)
示例:求学生的平均成绩
一维数组score,存放5个学生成绩,用一个函数求学生的平均成绩。
#include <stdio.h>
float average(float array[5])
{
int i;
float sum = 0, aver;
for(i=0;i<5;i++)
sum += array[i];
aver = sum / 5;
return aver;
}
int main()
{
float score[5], aver;
int i;
printf("请输入5名学生的成绩:\\n");
for (i = 0; i < 5; i++)
scanf("%f", &score[i]);
aver = average(score);
printf("平均成绩为:%5.2f\\n", aver);
return 0;
}
输出结果:
请输入5名学生的成绩:
65 78 97 88 90
平均成绩为:83.60
二、二维数组作为函数参数
二维数组作为参数时,必须指定第二维(及更高维)的大小,第一维可以省略。原因是:二维数组在内存中按行连续存储,编译器需要知道每行的元素个数才能正确计算地址。
1.正确的声明方式:
// 第二维大小必须指定(这里是3)
void func(int arr[][3], int rows) {
// 访问元素:arr[i][j]
}
示例:操作二维数组
#include <stdio.h>
// 打印二维数组(3列)
void print_2d_array(int arr[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\\n");
}
}
int main() {
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
print_2d_array(matrix, 2); // 传递数组和行数
return 0;
}
2、注意事项
必须传递数组长度: 由于数组退化为指针,函数无法直接获取原数组的长度,因此需要额外传递长度参数(一维数组传递元素总数,二维数组传递行数)。
函数内修改会影响原数组: 数组参数本质是指针,函数内部对元素的修改会直接反映到原数组中(与普通变量的值传递不同)。
避免用 sizeof 计算函数内数组大小: 函数内sizeof(arr)得到的是指针的大小(如 4 字节),而非原数组的总大小,这是常见错误。
二维数组的维度限制: 除第一维外,其他维度必须显式指定,否则编译器无法正确解析数组结构。
示例:有一个3X4的矩阵,求所有元素中的最大值。
#include <stdio.h>
// 函数功能:找出3×4矩阵中的最大值
// 参数说明:
// matrix[][4]:3行4列的二维数组(必须指定第二维大小4)
// rows:矩阵的行数(这里固定为3)
int find_max(int matrix[][4], int rows) { //rows:传递矩阵的行数(这里是 3)
int max = matrix[0][0]; // 假设第一个元素为初始最大值
// 遍历整个矩阵
for (int i = 0; i < rows; i++) { // 遍历行
for (int j = 0; j < 4; j++) { // 遍历列(第二维固定为4)
if (matrix[i][j] > max) {
max = matrix[i][j]; // 更新最大值
}
}
}
return max;
}
int main() {
// 定义一个3×4的矩阵并初始化
int matrix[3][4] = {
{12, 45, 33, 20},
{8, 56, 19, 72},
{37, 15, 49, 63}
};
// 调用函数求最大值
int max_value = find_max(matrix, 3);
// 输出结果
printf("矩阵中所有元素的最大值是:%d\\n", max_value);
return 0;
}
输出结果为:
矩阵中所有元素的最大值是:72
评论前必须登录!
注册