函数
8. 函数
函数是 C 语言中用于组织和封装代码的基本单位。通过函数,可以将复杂的问题分解为更小、更易管理的部分,提高代码的可重用性、可读性和可维护性。理解函数的定义、调用、参数传递、返回值以及作用域等概念,是编写高效和结构化 C 程序的关键。
8.1 函数的定义与调用
函数是完成特定任务的一段代码块。C 语言允许程序员自定义函数,以便在多个地方重复使用相同的代码逻辑。
函数的定义
语法:
返回类型 函数名(参数列表) {
// 函数体
// 可选的返回语句
}
- 返回类型:函数执行完毕后返回的数据类型,如
int、float、void等。 - 函数名:函数的名称,用于在程序中调用该函数。
- 参数列表:函数接受的输入参数,可以是零个或多个,每个参数由类型和名称组成,用逗号分隔。
- 函数体:包含函数执行的具体代码。
示例:
#include <stdio.h>
// 函数定义:打印欢迎信息
void greet() {
printf("欢迎使用C语言程序!\n");
}
int main() {
// 函数调用
greet(); // 输出: 欢迎使用C语言程序!
return 0;
}
输出:
欢迎使用C语言程序!
详细解释:
void greet(): 定义了一个名为greet的函数,返回类型为void,表示该函数不返回任何值。- 在
main函数中,通过greet();调用了greet函数,执行其内部的printf语句。
函数的调用
函数调用是指在程序的某个位置执行已定义的函数。通过函数名和参数(如果有)来调用函数。
示例:
#include <stdio.h>
// 函数定义:计算两个整数的和
int add(int a, int b) {
return a + b;
}
int main() {
int num1 = 5;
int num2 = 10;
int sum;
// 调用add函数,传递num1和num2作为参数
sum = add(num1, num2);
printf("num1 + num2 = %d\n", sum); // 输出: num1 + num2 = 15
return 0;
}
输出:
num1 + num2 = 15
详细解释:
int add(int a, int b): 定义了一个名为add的函数,接受两个int类型的参数,返回它们的和。- 在
main函数中,通过add(num1, num2)调用add函数,将num1和num2的值传递给add函数,并将返回值赋给sum变量。
更多示例
-
无参数且有返回值的函数:
#include <stdio.h> // 函数定义:返回固定值 int getNumber() { return 42; } int main() { int number = getNumber(); printf("获得的数字是: %d\n", number); // 输出: 获得的数字是: 42 return 0; }输出:
获得的数字是: 42 -
带参数且无返回值的函数:
#include <stdio.h> // 函数定义:打印两数之和 void printSum(int a, int b) { int sum = a + b; printf("a + b = %d\n", sum); } int main() { int x = 7; int y = 3; printSum(x, y); // 输出: a + b = 10 return 0; }输出:
a + b = 10 -
函数嵌套调用:
#include <stdio.h> // 函数定义:计算平方 int square(int num) { return num * num; } // 函数定义:计算立方 int cube(int num) { return num * square(num); // 调用square函数 } int main() { int number = 4; int result = cube(number); printf("number的立方是: %d\n", result); // 输出: number的立方是: 64 return 0; }输出:
number的立方是: 64
注意事项
- 函数名唯一性:在同一作用域内,函数名必须唯一,不能与其他变量或函数重名。
- 返回类型一致性:函数的返回类型必须与函数体内的
return语句返回的类型一致。 - 参数类型匹配:函数调用时传递的参数类型应与函数定义中的参数类型匹配,否则可能导致隐式类型转换或错误。
8.2 函数参数与返回值
函数参数和返回值是函数与外界交互的主要方式。通过参数传递数据给函数,通过返回值将结果传递回调用者。
函数参数
函数参数是函数接受的输入数据,用于在函数内部执行特定操作。参数可以是基本数据类型、数组、指针等。
传值与传址:
- 传值(Call by Value):将参数的值复制一份传递给函数,函数内部对参数的修改不会影响外部变量。
- 传址(Call by Reference):传递参数的地址(指针),函数内部通过指针可以修改外部变量的值。
示例:
-
传值示例:
#include <stdio.h> // 函数定义:交换两个数的值(传值) void swapByValue(int a, int b) { int temp = a; a = b; b = temp; printf("函数内部: a = %d, b = %d\n", a, b); // 输出交换后的值 } int main() { int x = 5; int y = 10; printf("调用前: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10 swapByValue(x, y); printf("调用后: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10 return 0; }输出:
调用前: x = 5, y = 10 函数内部: a = 10, b = 5 调用后: x = 5, y = 10解释:
- 在
swapByValue函数中,a和b是x和y的副本,函数内部的交换不影响main函数中的x和y。
- 在
-
传址示例:
#include <stdio.h> // 函数定义:交换两个数的值(传址) void swapByReference(int *a, int *b) { int temp = *a; *a = *b; *b = temp; printf("函数内部: a = %d, b = %d\n", *a, *b); // 输出交换后的值 } int main() { int x = 5; int y = 10; printf("调用前: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10 swapByReference(&x, &y); printf("调用后: x = %d, y = %d\n", x, y); // 输出: x = 10, y = 5 return 0; }输出:
调用前: x = 5, y = 10 函数内部: a = 10, b = 5 调用后: x = 10, y = 5解释:
- 在
swapByReference函数中,a和b是指向x和y的指针,通过指针修改了x和y的实际值。
- 在
函数返回值
函数可以通过返回值将结果传递回调用者。返回值的类型由函数定义中的返回类型决定。
示例:
-
返回单一值:
#include <stdio.h> // 函数定义:计算两个数的和 int add(int a, int b) { return a + b; } int main() { int num1 = 7; int num2 = 3; int sum; // 调用add函数并接收返回值 sum = add(num1, num2); printf("sum = %d\n", sum); // 输出: sum = 10 return 0; }输出:
sum = 10 -
返回多个值(通过指针):
#include <stdio.h> // 函数定义:计算两个数的和与差 void calculate(int a, int b, int *sum, int *diff) { *sum = a + b; *diff = a - b; } int main() { int x = 15; int y = 5; int sum, difference; // 调用calculate函数 calculate(x, y, &sum, &difference); printf("sum = %d, difference = %d\n", sum, difference); // 输出: sum = 20, difference = 10 return 0; }输出:
sum = 20, difference = 10解释:
- 通过传递指针,
calculate函数可以同时返回多个值(sum和diff)。
- 通过传递指针,
更多示例
-
函数不返回值:
#include <stdio.h> // 函数定义:打印学生信息 void printStudentInfo(char name[], int age) { printf("学生姓名: %s\n", name); printf("学生年龄: %d\n", age); } int main() { char studentName[] = "张三"; int studentAge = 20; // 调用printStudentInfo函数 printStudentInfo(studentName, studentAge); return 0; }输出:
学生姓名: 张三 学生年龄: 20 -
函数返回指针:
#include <stdio.h> #include <string.h> // 函数定义:返回两个字符串中较长的那个 char* getLongerString(char *str1, char *str2) { if (strlen(str1) > strlen(str2)) { return str1; } else { return str2; } } int main() { char string1[] = "Hello"; char string2[] = "Hello, World!"; char *longer; // 调用getLongerString函数 longer = getLongerString(string1, string2); printf("较长的字符串是: %s\n", longer); // 输出: 较长的字符串是: Hello, World! return 0; }输出:
较长的字符串是: Hello, World!
注意事项
- 函数声明:在函数被调用之前,必须有函数的声明(函数原型),或者将函数定义放在调用之前。否则,编译器无法识别函数,可能导致错误。
- 返回类型一致性:函数返回的值必须与函数定义中的返回类型一致,否则会产生类型不匹配的错误。
- 指针安全:在使用指针作为函数参数或返回值时,确保指针指向有效的内存,避免出现悬挂指针或野指针。
8.3 局部变量与全局变量
变量的作用域决定了变量在程序中的可见范围。C 语言中的变量分为局部变量和全局变量两种类型。
局部变量
定义:局部变量是在函数或代码块内部定义的变量,其作用域仅限于定义它的函数或代码块内。
特点:
- 作用域有限:只能在定义它的函数或代码块内部访问。
- 生命周期短:当函数或代码块执行结束时,局部变量被销毁。
- 命名冲突少:不同函数可以使用相同的局部变量名,不会互相影响。
示例:
#include <stdio.h>
void display() {
int count = 10; // 局部变量
printf("count = %d\n", count);
}
int main() {
display(); // 输出: count = 10
// printf("count = %d\n", count); // 错误:count在main中不可见
return 0;
}
输出:
count = 10
全局变量
定义:全局变量是在所有函数外部定义的变量,其作用域覆盖整个文件,从定义的位置到文件结束。
特点:
- 作用域广:在定义它的文件中的所有函数都可以访问。
- 生命周期长:程序运行期间全局变量一直存在。
- 易引发命名冲突:不同文件中的全局变量如果同名,会导致命名冲突,需谨慎管理。
示例:
#include <stdio.h>
int globalVar = 100; // 全局变量
void display() {
printf("globalVar = %d\n", globalVar); // 访问全局变量
}
int main() {
printf("globalVar 在main中 = %d\n", globalVar); // 输出: 100
display(); // 输出: 100
globalVar = 200; // 修改全局变量
printf("修改后的 globalVar 在main中 = %d\n", globalVar); // 输出: 200
display(); // 输出: 200
return 0;
}
输出:
globalVar 在main中 = 100
globalVar = 100
修改后的 globalVar 在main中 = 200
globalVar = 200
局部变量与全局变量的区别
| 特性 | 局部变量 | 全局变量 |
|---|---|---|
| 定义位置 | 函数内部或代码块内 | 所有函数外部 |
| 作用域 | 仅限于定义它的函数或代码块内 | 整个文件内的所有函数 |
| 生命周期 | 从定义到函数或代码块结束 | 从程序开始到程序结束 |
| 初始化 | 如果未初始化,默认值不确定 | 如果未初始化,默认值为 0(静态存储) |
| 命名冲突 | 低(不同函数可用相同名称) | 高(同名全局变量会冲突) |
示例比较:
#include <stdio.h>
int global = 50; // 全局变量
void func1() {
int local = 10; // 局部变量
printf("func1 - local = %d, global = %d\n", local, global);
}
void func2() {
// printf("func2 - local = %d\n", local); // 错误:local在func2中不可见
printf("func2 - global = %d\n", global);
}
int main() {
int local = 20; // main函数的局部变量
printf("main - local = %d, global = %d\n", local, global);
func1();
func2();
return 0;
}
输出:
main - local = 20, global = 50
func1 - local = 10, global = 50
func2 - global = 50
注意事项
- 命名规范:为了避免命名冲突和提高代码可读性,建议遵循命名规范。例如,全局变量可以使用前缀
g_,如g_counter。 - 变量的生命周期:理解变量的生命周期有助于合理地管理内存和资源,避免出现内存泄漏或悬挂指针。
- 避免滥用全局变量:过多使用全局变量可能导致程序难以维护和调试,建议仅在必要时使用全局变量。
8.4 递归函数
递归函数是指在函数内部调用自身的函数。递归是一种强大的编程技术,适用于解决可以分解为相似子问题的问题,如阶乘计算、斐波那契数列、树的遍历等。
递归的基本概念
- 基准情形(Base Case):递归的终止条件,当满足基准情形时,不再进行递归调用。
- 递归情形(Recursive Case):函数通过调用自身来解决问题的一部分,逐步接近基准情形。
递归函数的定义与示例
示例 1:计算阶乘
阶乘是递归函数的经典示例。阶乘定义为:n! = n * (n-1)!,其中0! = 1。
#include <stdio.h>
// 函数定义:递归计算阶乘
long factorial(int n) {
if (n == 0) { // 基准情形
return 1;
} else { // 递归情形
return n * factorial(n - 1);
}
}
int main() {
int number = 5;
long fact = factorial(number);
printf("%d 的阶乘是: %ld\n", number, fact); // 输出: 5 的阶乘是: 120
return 0;
}
输出:
5 的阶乘是: 120
详细解释:
- 当
n等于0时,函数返回1,这是递归的终止条件。 - 否则,函数返回
n * factorial(n - 1),逐步递减n,直到达到基准情形。
示例 2:斐波那契数列
斐波那契数列的定义为:F(n) = F(n-1) + F(n-2),其中F(0) = 0,F(1) = 1。
#include <stdio.h>
// 函数定义:递归计算斐波那契数
int fibonacci(int n) {
if (n == 0) { // 基准情形1
return 0;
} else if (n == 1) { // 基准情形2
return 1;
} else { // 递归情形
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
int main() {
int term = 10;
printf("斐波那契数列第 %d 项是: %d\n", term, fibonacci(term)); // 输出: 斐波那契数列第 10 项是: 55
return 0;
}
输出:
斐波那契数列第 10 项是: 55
详细解释:
- 当
n等于0或1时,函数返回0或1,作为基准情形。 - 否则,函数递归调用自身,计算前两项的和。
递归的优缺点
优点:
- 简洁:递归可以用简短的代码解决复杂的问题,代码易于理解。
- 适用性强:适用于分治法、树的遍历、图的搜索等问题。
缺点:
- 效率低:递归调用会增加函数调用栈的开销,可能导致性能下降。
- 栈溢出:过深的递归调用可能导致栈溢出,程序崩溃。
- 重复计算:某些递归算法会进行大量重复计算,影响效率(如斐波那契数列)。
优化递归
- 使用尾递归:将递归调用放在函数的最后一步,某些编译器可以优化尾递归,减少栈空间使用。
- 记忆化:存储已经计算过的结果,避免重复计算。
- 转换为迭代:将递归逻辑转换为迭代逻辑,通常更高效。
示例:使用记忆化优化斐波那契数列
#include <stdio.h>
// 定义一个数组用于存储已计算的斐波那契数
long memo[100] = {0};
// 函数定义:递归计算斐波那契数(带记忆化)
long fibonacciMemo(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
// 如果已经计算过,直接返回结果
if (memo[n] != 0) {
return memo[n];
}
// 计算并存储结果
memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
return memo[n];
}
int main() {
int term = 50;
printf("斐波那契数列第 %d 项是: %ld\n", term, fibonacciMemo(term)); // 输出: 斐波那契数列第 50 项是: 12586269025
return 0;
}
输出:
斐波那契数列第 50 项是: 12586269025
解释:
- 通过
memo数组存储已计算的斐波那契数,避免重复计算,大幅提升效率。
注意事项
- 确保基准情形:递归函数必须有明确的基准情形,避免无限递归。
- 控制递归深度:避免过深的递归调用,防止栈溢出。
- 理解递归流程:递归函数的执行流程较为复杂,需仔细分析每一步的调用和返回。
8.5 函数的声明与分离编译
在大型项目中,函数的声明与定义通常需要分离,以提高代码的组织性和可维护性。C 语言通过函数声明(函数原型)和分离编译实现这一目标。
函数的声明(函数原型)
函数声明告诉编译器函数的名称、返回类型和参数类型,但不包含函数的具体实现。函数声明通常放在头文件中,供多个源文件引用。
语法:
返回类型 函数名(参数类型1, 参数类型2, ...);
示例:
// function.h
#ifndef FUNCTION_H
#define FUNCTION_H
// 函数声明
int add(int a, int b);
void greet();
#endif
函数的定义
函数定义包含函数的具体实现,通常放在源文件(.c文件)中。
示例:
// function.c
#include <stdio.h>
#include "function.h"
// 函数定义:计算两个数的和
int add(int a, int b) {
return a + b;
}
// 函数定义:打印欢迎信息
void greet() {
printf("欢迎使用C语言函数示例!\n");
}
分离编译
分离编译是将代码分为多个源文件和头文件,分别编译,然后链接生成最终的可执行文件。这样可以提高代码的组织性,方便多人协作和代码重用。
示例项目结构:
project/
│
├── main.c
├── function.c
├── function.h
└── Makefile
main.c:
#include <stdio.h>
#include "function.h"
int main() {
int x = 10;
int y = 20;
int sum;
// 调用greet函数
greet(); // 输出: 欢迎使用C语言函数示例!
// 调用add函数
sum = add(x, y);
printf("sum = %d\n", sum); // 输出: sum = 30
return 0;
}
function.h:
#ifndef FUNCTION_H
#define FUNCTION_H
// 函数声明
int add(int a, int b);
void greet();
#endif
function.c:
#include <stdio.h>
#include "function.h"
// 函数定义:计算两个数的和
int add(int a, int b) {
return a + b;
}
// 函数定义:打印欢迎信息
void greet() {
printf("欢迎使用C语言函数示例!\n");
}
Makefile(用于自动化编译):
# Makefile
CC = gcc
CFLAGS = -Wall -g
# 目标可执行文件
TARGET = main
# 源文件
SRCS = main.c function.c
# 头文件
HEADERS = function.h
# 生成可执行文件
$(TARGET): $(SRCS) $(HEADERS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
# 清理编译生成的文件
clean:
rm -f $(TARGET)
编译与运行:
在项目目录下,使用以下命令编译项目:
make
然后运行生成的可执行文件:
./main
输出:
欢迎使用C语言函数示例!
sum = 30
优点
- 代码组织性强:将函数声明和定义分离,代码更易于管理和维护。
- 重用性高:头文件可以被多个源文件引用,方便代码重用。
- 编译效率高:修改一个源文件无需重新编译所有文件,只需重新编译受影响的文件。
注意事项
- 头文件保护:使用预处理指令(如
#ifndef、#define、#endif)防止头文件被多次包含,导致重复定义错误。 - 一致性:确保函数声明和定义中的参数类型、返回类型一致,否则可能导致编译错误或未定义行为。
- 依赖管理:在使用多个源文件时,确保正确管理文件之间的依赖关系,避免链接错误。
8.6 总结
函数是 C 语言中组织和封装代码的基本单位,通过函数的定义和调用,可以实现代码的模块化和重用。掌握函数的参数传递、返回值、作用域、递归以及函数声明与分离编译等概念,对于编写高效、可维护的 C 程序至关重要。
- 函数的定义与调用:理解如何定义函数,如何在程序中调用函数,确保函数的参数和返回值类型匹配。
- 函数参数与返回值:掌握传值和传址的区别,了解如何通过函数返回单一值或多个值。
- 局部变量与全局变量:了解变量的作用域和生命周期,合理使用局部变量和全局变量以提高代码的可读性和安全性。
- 递归函数:理解递归的基本概念,掌握递归函数的定义和优化方法,避免递归调用导致的性能问题。
- 函数的声明与分离编译:学会将函数声明放在头文件中,实现分离编译,提高代码的组织性和可维护性。