Week 04: 函数
掌握函数定义、参数传递、头文件组织、静态函数与内联函数.
1. 函数基础
1.1 函数定义
// 返回类型 函数名(参数列表) { 函数体 }
int add(int a, int b) {
return a + b;
}
// 无返回值
void print_hello(void) {
printf("Hello\n");
}
// 无参数 (显式 void)
int get_random(void) {
return 42;
}1.2 函数声明 (原型)
// 声明 (告诉编译器函数的存在)
int add(int a, int b);
// 定义 (实现函数)
int add(int a, int b) {
return a + b;
}
int main(void) {
int result = add(1, 2); // 调用
return 0;
}1.3 声明与定义的区别
// 声明: 可以有多次
int add(int, int); // 参数名可省略
int add(int a, int b); // 参数名可保留
// 定义: 只能有一次
int add(int a, int b) {
return a + b;
}1.4 旧式函数定义 (K&R 风格)
// 不推荐, 仅为了理解旧代码
int add(a, b)
int a;
int b;
{
return a + b;
}2. 参数传递
2.1 值传递 (Pass by Value)
C 语言只有值传递:
void modify(int x) {
x = 100; // 修改的是副本
}
int main(void) {
int a = 10;
modify(a);
printf("a = %d\n", a); // 10 (未改变)
return 0;
}2.2 传递指针
通过指针模拟引用传递:
void modify(int *p) {
*p = 100; // 修改指针指向的值
}
int main(void) {
int a = 10;
modify(&a);
printf("a = %d\n", a); // 100 (已改变)
return 0;
}2.3 传递数组
数组作为参数时衰变为指针:
void print_array(int arr[], int len) {
// arr 是指针, 不是数组
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 8 (指针大小)
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
printf("sizeof(nums) = %zu\n", sizeof(nums)); // 20 (数组大小)
print_array(nums, 5);
return 0;
}等价写法:
void print_array(int arr[], int len);
void print_array(int *arr, int len); // 等价
void print_array(int arr[5], int len); // 5 被忽略2.4 传递多维数组
// 必须指定除第一维外的所有维度
void print_matrix(int mat[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
}
// 或使用指针
void print_matrix(int (*mat)[3], int rows); // 等价
int main(void) {
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
print_matrix(matrix, 2);
return 0;
}2.5 const 修饰参数
// 承诺不修改指针指向的内容
void print_string(const char *s) {
// s[0] = 'X'; // 编译错误
printf("%s\n", s);
}
// 指针本身不可变
void func(char *const p) {
// p = NULL; // 编译错误
p[0] = 'X'; // OK
}
// 两者都不可变
void func(const char *const p) {
// p = NULL; // 编译错误
// p[0] = 'X'; // 编译错误
}3. 返回值
3.1 返回基本类型
int add(int a, int b) {
return a + b;
}
double divide(double a, double b) {
if (b == 0) {
return 0.0; // 错误处理
}
return a / b;
}3.2 返回指针
// 危险: 返回局部变量的指针
int *bad_function(void) {
int x = 10;
return &x; // 警告: 返回局部变量地址
}
// 正确: 返回静态变量
int *ok_function(void) {
static int x = 10;
return &x;
}
// 正确: 返回动态分配的内存
int *good_function(void) {
int *p = malloc(sizeof(int));
*p = 10;
return p; // 调用者负责 free
}
// 正确: 返回传入的指针
int *pass_through(int *p) {
*p = 10;
return p;
}3.3 返回结构体
struct Point {
int x, y;
};
// 返回结构体 (值拷贝)
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
// 使用
struct Point p = make_point(3, 4);对于大型结构体, 值拷贝可能有性能开销. 可以考虑:
- 通过指针参数返回
- 返回指向静态或堆内存的指针
4. 多文件编程
4.1 项目结构
project/
├── main.c # 主程序
├── math_ops.h # 头文件 (声明)
├── math_ops.c # 实现文件 (定义)
└── Makefile4.2 头文件
// math_ops.h
#ifndef MATH_OPS_H // 头文件保护
#define MATH_OPS_H
// 函数声明
int add(int a, int b);
int subtract(int a, int b);
#endif4.3 实现文件
// math_ops.c
#include "math_ops.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}4.4 主程序
// main.c
#include <stdio.h>
#include "math_ops.h"
int main(void) {
printf("%d\n", add(1, 2));
printf("%d\n", subtract(5, 3));
return 0;
}4.5 编译与链接
# 分步编译
gcc -c main.c -o main.o
gcc -c math_ops.c -o math_ops.o
gcc main.o math_ops.o -o program
# 一步编译
gcc main.c math_ops.c -o program4.6 头文件保护
// 方法 1: #ifndef/#define/#endif
#ifndef HEADER_H
#define HEADER_H
// 内容
#endif
// 方法 2: #pragma once (非标准, 但广泛支持)
#pragma once
// 内容5. 静态函数
5.1 static 关键字
static 限制函数的可见性为当前文件:
// file1.c
static int helper(int x) { // 只在 file1.c 可见
return x * 2;
}
int public_func(int x) {
return helper(x) + 1;
}// file2.c
// helper() 在这里不可见
// int x = helper(10); // 链接错误
int y = public_func(10); // OK5.2 static 的作用
- 封装: 隐藏实现细节
- 避免命名冲突: 不同文件可以有同名 static 函数
- 优化: 编译器知道函数不会被外部调用, 可以内联
6. 内联函数
6.1 inline 关键字
inline int square(int x) {
return x * x;
}6.2 inline 的含义
inline 是建议, 不是命令:
- 编译器可能内联, 也可能不内联
- 即使不写 inline, 编译器也可能自动内联
- 内联意味着在调用处展开函数体, 避免函数调用开销
6.3 inline 与头文件
// math.h
#ifndef MATH_H
#define MATH_H
// 在头文件中定义 inline 函数
static inline int square(int x) {
return x * x;
}
#endif为什么需要 static:
- 每个包含头文件的 .c 文件都会有这个函数的副本
static避免链接时的多重定义错误
6.4 C99 inline 语义
// 在头文件中
inline int func(int x) {
return x * 2;
}
// 在一个 .c 文件中提供外部定义
extern inline int func(int x);7. 递归函数
7.1 基本递归
unsigned long factorial(unsigned int n) {
if (n == 0) {
return 1; // 基准情况
}
return n * factorial(n - 1); // 递归情况
}7.2 递归的栈消耗
每次递归调用都会在栈上分配栈帧:
factorial(5)
└── factorial(4)
└── factorial(3)
└── factorial(2)
└── factorial(1)
└── factorial(0)深度递归可能导致栈溢出.
7.3 尾递归
// 非尾递归: 递归调用后还有操作
unsigned long factorial(unsigned int n) {
if (n == 0) return 1;
return n * factorial(n - 1); // 需要保存 n
}
// 尾递归: 递归调用是最后一个操作
unsigned long factorial_tail(unsigned int n, unsigned long acc) {
if (n == 0) return acc;
return factorial_tail(n - 1, n * acc); // 可以优化为循环
}
unsigned long factorial(unsigned int n) {
return factorial_tail(n, 1);
}使用 -O2 编译, GCC 可能将尾递归优化为循环.
8. 函数指针
8.1 基本语法
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main(void) {
// 声明函数指针
int (*op)(int, int);
op = add; // 指向 add 函数
printf("%d\n", op(3, 2)); // 5
op = sub; // 指向 sub 函数
printf("%d\n", op(3, 2)); // 1
return 0;
}8.2 typedef 简化
typedef int (*operation)(int, int);
operation op = add;
printf("%d\n", op(3, 2));8.3 回调函数
void foreach(int *arr, int len, void (*callback)(int)) {
for (int i = 0; i < len; i++) {
callback(arr[i]);
}
}
void print_int(int x) {
printf("%d\n", x);
}
void print_square(int x) {
printf("%d\n", x * x);
}
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
printf("原始值:\n");
foreach(nums, 5, print_int);
printf("平方:\n");
foreach(nums, 5, print_square);
return 0;
}8.4 函数指针数组
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }
int main(void) {
// 函数指针数组
int (*ops[])(int, int) = {add, sub, mul, divide};
int a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("ops[%d](%d, %d) = %d\n", i, a, b, ops[i](a, b));
}
return 0;
}9. 可变参数函数
9.1 stdarg.h
#include <stdarg.h>
int sum(int count, ...) {
va_list args;
va_start(args, count); // 初始化
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // 获取下一个参数
}
va_end(args); // 清理
return total;
}
int main(void) {
printf("%d\n", sum(3, 10, 20, 30)); // 60
printf("%d\n", sum(5, 1, 2, 3, 4, 5)); // 15
return 0;
}9.2 printf 的实现原理
// 简化版 printf
void my_printf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
while (*fmt) {
if (*fmt == '%') {
fmt++;
switch (*fmt) {
case 'd':
printf("%d", va_arg(args, int));
break;
case 's':
printf("%s", va_arg(args, char *));
break;
case '%':
putchar('%');
break;
}
} else {
putchar(*fmt);
}
fmt++;
}
va_end(args);
}10. 函数属性与静态断言
10.1 C11 _Noreturn 关键字
标准的不返回函数声明:
#include <stdnoreturn.h>
noreturn void die(const char *msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
// 或直接使用 _Noreturn
_Noreturn void abort_program(void) {
abort();
}C23 变化: [[noreturn]] 属性语法取代 _Noreturn.
10.2 C11 静态断言 _Static_assert
编译期断言, 条件为假时编译失败:
#include <assert.h>
// C11: _Static_assert 需要两个参数
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
// C23: 第二个参数可选
static_assert(sizeof(long) >= 4);
// 常见用途
struct Packet {
uint32_t header;
uint8_t data[60];
};
_Static_assert(sizeof(struct Packet) == 64, "Packet must be 64 bytes");
// 类型大小检查
_Static_assert(sizeof(size_t) == sizeof(void *), "size_t != pointer size");10.3 GCC 扩展属性
// 不返回
void die(const char *msg) __attribute__((noreturn));
// 纯函数 (无副作用, 仅依赖参数)
int square(int x) __attribute__((const));
// 已弃用
int old_func(void) __attribute__((deprecated));
// 格式检查
void my_printf(const char *fmt, ...)
__attribute__((format(printf, 1, 2)));
// 弱符号
void weak_func(void) __attribute__((weak));11. 练习
11.1 交换函数
编写函数交换两个整数的值.
11.2 字符串长度
实现 my_strlen 函数.
11.3 通用排序
使用函数指针实现通用的比较排序函数.
12. 思考题
- 为什么 C 只有值传递?
- 数组为什么会衰变为指针?
- static 函数有什么好处?
- inline 一定会内联吗?
- 如何避免深度递归的栈溢出?
13. 本周小结
- 函数定义: 声明与定义, 参数与返回值.
- 参数传递: 值传递, 指针传递.
- 多文件编程: 头文件, 头文件保护.
- 静态函数: static 限制可见性.
- 内联函数: inline 建议.
- 函数指针: 回调, 函数指针数组.
- 可变参数: stdarg.h.
函数是程序的基本组成单元. 理解参数传递和函数指针, 是编写灵活代码的基础.