0. 前置知识:什么是函数?
在 C 语言中,函数是一段完成特定功能的独立代码块,可以被重复调用。它的核心作用是 “封装逻辑,复用代码”。例如,你可能写一个函数来计算两个数的和,或者写一个函数来打印一段文字。
函数的本质是 “解决问题的步骤”:把复杂的任务拆分成小步骤,每个步骤用一个函数实现,代码会更清晰、更易维护。
1. 无参函数的定义与特点
1.1 无参函数的标准格式
无参函数的定义形式为:
返回值类型 函数名() {
// 函数体(具体执行的代码)
}
- 返回值类型:函数执行后返回结果的类型(如
int
、void
等)。如果不需要返回结果,用void
声明。 - 函数名:给函数起的名字(符合 C 语言命名规则,如
print_hello
)。 - 括号 ():表示 “无参数”(括号内为空,或显式写
void
,如void func(void)
,两者等价)。
1.2 无参函数的典型场景
无参函数适用于功能固定、不需要外部输入的场景。常见例子包括:
-
初始化操作:比如初始化硬件(如串口、LED 灯)、初始化全局变量。
void init_led() { // 代码:配置LED引脚为输出模式 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); }
这段代码是单片机开发中常见的 LED 初始化函数,每次调用都会执行固定的引脚配置逻辑,不需要额外参数。
-
固定输出功能:比如打印一段提示语、生成固定格式的日志。
void print_welcome() { printf("********************************\n"); printf("* 欢迎使用学生管理系统! *\n"); printf("********************************\n"); }
每次调用
print_welcome()
,都会输出相同的欢迎界面,不需要传入任何数据。 -
循环执行的固定任务:比如在嵌入式系统中,周期性读取传感器(传感器地址固定)。
void read_temp_sensor() { // 代码:读取固定地址的温度传感器数据 float temp = I2C_Read(0x48); // 0x48是传感器的固定I2C地址 printf("当前温度:%.1f℃\n", temp); }
1.3 无参函数的优缺点
- 优点:逻辑简单、调用方便。不需要考虑参数传递的复杂度,适合功能高度固定的场景。
- 缺点:灵活性差。如果需求稍有变化(比如打印不同的欢迎语),就需要修改函数本身,无法通过外部输入调整行为。
2. 有参函数的定义与核心概念
2.1 有参函数的标准格式
有参函数的定义形式为:
返回值类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) {
// 函数体(使用参数完成逻辑)
}
- 参数列表:括号内是 “形式参数”(简称 “形参”),表示函数需要的输入数据类型和名称。
- 形参的作用:在函数内部,形参是变量,用于接收调用函数时传入的 “实际参数”(简称 “实参”)。
2.2 关键概念:形参 vs 实参
-
形参(形式参数):函数定义时声明的参数,相当于 “占位符”。它只有在函数被调用时才会分配内存,函数执行结束后内存释放。
例如:int add(int a, int b) { // a和b是形参 return a + b; }
-
实参(实际参数):调用函数时传入的具体数据。实参可以是常量、变量或表达式,但必须与形参的类型和数量匹配。
例如:int x = 3, y = 5; int sum = add(x, y); // x和y是实参(也可以直接写add(3,5))
2.3 有参函数的参数传递方式
在 C 语言中,参数传递有两种方式:值传递和地址传递(指针传递)。
2.3.1 值传递(最常用)
- 规则:将实参的值复制一份传给形参。函数内部对形参的修改不会影响实参。
- 示例:
void swap(int a, int b) { // 形参a、b是实参的“副本” int temp = a; a = b; b = temp; // 仅交换形参的值,实参不受影响 } int main() { int x = 10, y = 20; swap(x, y); // 调用swap函数 printf("x=%d, y=%d\n", x, y); // 输出:x=10, y=20(未交换) return 0; }
为什么没交换?因为swap
函数中的a
和b
是x
和y
的 “复制值”,修改它们不会改变原始的x
和y
。
2.3.2 地址传递(用于修改外部变量)
- 规则:将实参的地址(指针)传给形参。函数内部通过指针直接操作实参的内存,修改会影响实参。
- 示例:
void swap(int *a, int *b) { // 形参是指针,接收实参的地址 int temp = *a; *a = *b; // 通过指针修改实参的内存 *b = temp; } int main() { int x = 10, y = 20; swap(&x, &y); // 传入x和y的地址(实参是指针) printf("x=%d, y=%d\n", x, y); // 输出:x=20, y=10(交换成功) return 0; }
这里swap
函数的形参是指针(int *a
),通过&x
和&y
获取实参的地址,直接操作原始内存,因此能真正交换x
和y
的值。
2.4 有参函数的典型场景
有参函数适用于功能依赖外部输入的场景。常见例子包括:
2.4.1 数学计算(需输入数据)
比如计算两个数的乘积、求数组的平均值等:
// 计算两个整数的乘积(值传递)
int multiply(int a, int b) {
return a * b;
}
// 计算数组的平均值(地址传递,避免复制大数组)
float average(int *arr, int length) { // arr是数组的地址,length是数组长度
int sum = 0;
for (int i = 0; i < length; i++) {
sum += arr[i];
}
return (float)sum / length;
}
2.4.2 条件判断(需输入判断依据)
比如判断一个数是否为质数、判断字符串是否相等:
// 判断n是否为质数(n是输入参数)
int is_prime(int n) {
if (n <= 1) return 0; // 小于等于1不是质数
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return 0; // 能被整除,不是质数
}
return 1; // 是质数
}
2.4.3 硬件控制(需输入配置参数)
比如控制 LED 灯的亮度(通过 PWM 占空比)、设置串口的波特率:
// 配置串口波特率(USART是串口硬件地址,baud是波特率参数)
void uart_init(USART_TypeDef *USART, int baud) {
USART->BRR = SystemCoreClock / baud; // 根据baud计算波特率寄存器值
USART->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // 使能串口收发
}
2.5 有参函数的优缺点
- 优点:灵活性强。通过参数调整,同一个函数可以实现多种功能(比如
average
函数可以计算任意数组的平均值)。 - 缺点:复杂度高。需要考虑参数的类型匹配、传递方式(值 / 地址)、边界条件(如数组长度是否越界)等问题。
3. 无参函数与有参函数的对比
对比维度 | 无参函数 | 有参函数 |
---|---|---|
输入依赖 | 无(功能固定) | 有(功能依赖参数) |
灵活性 | 低(修改功能需改函数本身) | 高(通过参数调整功能) |
复用性 | 低(仅适用于固定场景) | 高(适用于多种场景) |
参数传递复杂度 | 无(无需处理参数) | 高(需处理类型、传递方式等) |
4. 实际开发中的选择策略
4.1 何时用无参函数?
- 功能高度固定,且未来不会变化(如打印固定日志)。
- 不需要外部数据输入(如初始化硬件)。
- 简化代码调用(避免传递参数的开销)。
4.2 何时用有参函数?
- 功能需要根据不同输入调整(如计算不同数的和)。
- 需要复用函数处理多组数据(如计算多个数组的平均值)。
- 需要与外部数据交互(如读取不同地址的传感器)。
5. 常见误区与注意事项
5.1 无参函数的 “空参数” 声明
在 C 语言中,无参函数的参数列表可以写void
(显式声明无参),也可以留空。但留空不代表 “任意参数”,而是 “参数类型不确定”(这是 C 语言的历史遗留问题)。为了避免歧义,建议显式写void
:
// 推荐写法(显式声明无参)
void print_hello(void) {
printf("Hello, world!\n");
}
// 不推荐写法(可能被误解为“参数类型不确定”)
void print_hello() {
printf("Hello, world!\n");
}
5.2 有参函数的参数类型匹配
实参和形参的类型必须严格匹配,否则会导致错误或未定义行为。例如:
// 函数声明需要int类型参数
void print_number(int n) {
printf("Number: %d\n", n);
}
int main() {
float x = 3.14;
print_number(x); // 警告:将float传给int会截断小数(3.14→3)
return 0;
}
5.3 地址传递的风险
使用指针传递参数时,若函数内部修改了指针指向的内存,可能会影响外部变量。需确保操作的合法性(如指针不为空、数组不越界):
// 错误示例:修改空指针指向的内存(崩溃!)
void set_value(int *p, int value) {
*p = value; // 如果p是NULL,会导致段错误
}
int main() {
int *ptr = NULL;
set_value(ptr, 10); // 危险!ptr未指向有效内存
return 0;
}
6. 总结:如何快速区分无参函数与有参函数?
- 看函数定义的括号:无参函数括号内是空或
void
;有参函数括号内有参数类型和名称。 - 看功能是否依赖外部输入:无参函数 “自己搞定一切”;有参函数 “需要你给信息才能做事”。
用生活类比,形象理解 “无参函数” 与 “有参函数”
为了让你快速记住这两个概念,我们先抛开代码,用生活中的场景打个比方:
1. 无参函数:像自动售货机的 “取货按钮”
假设你家楼下有一台自动售货机,里面只卖一种矿泉水(比如 “固定款”)。你只需要按一下 “取货按钮”(触发函数),机器就会 “哐当” 掉出一瓶水。这个过程不需要你输入任何额外信息(比如 “要冰的”“要大瓶的”)—— 按钮的功能是固定的、无输入要求的。
这就是无参函数:函数执行时不需要 “外部输入数据”,它的功能是提前设定好的,无论何时调用,行为都一致。
2. 有参函数:像咖啡店的 “点单系统”
再想象你走进一家咖啡店,想点一杯咖啡。店员会问你:“要美式还是拿铁?要冰的还是热的?糖要加多少?”(这些问题就是在 “要参数”)。你回答了这些问题(输入参数),店员才会根据你的要求制作咖啡(函数根据参数执行逻辑)。
这就是有参函数:函数执行时需要 “外部输入数据”(参数),不同的参数会让函数产生不同的行为或结果。
一句话总结
- 无参函数:“我不需要你给我东西,我自己知道该做什么”(功能固定,无输入)。
- 有参函数:“你得告诉我具体要求,我才能帮你做事”(功能灵活,依赖输入)。