C语言函数

一、函数的本质与作用
 
1. 本质理解
 
函数是 一段可复用的代码块,相当于“工具”:
 
- 输入:通过参数接收外部数据

- 处理:在函数体内完成逻辑

- 输出:通过返回值给外部反馈
 
比如计算两数之和的函数,就像一个“加法工具”,给它两个数(输入),它返回和(输出)。
 
2. 核心作用
 
- 模块化编程:把大问题拆成小函数(如游戏开发中,移动、攻击、渲染拆成独立函数)

- 代码复用:避免重复写相同逻辑(比如多个地方要排序,写一个排序函数反复用)

- 降低复杂度:读代码时,看函数名就知道功能(如 calculateSalary() ,不用看内部细节)


二 、函数的基本语法(定义+声明+调用)

1.函数定义:完整实现逻辑

返回值类型 函数名(参数列表) {
    // 函数体:实现具体功能
    return 返回值; // 若返回值类型为void,可省略return
}

- 返回值类型:

- 如 int (返回整数)、 float (返回小数)、 void (无返回值)

- 决定了 return 后数据的类型,比如 int 函数必须 return 整数

- 函数名:

- 遵循标识符规则(字母、数字、下划线,首字母不能是数字)

- 建议见名知意(如 getMax() 表示“获取最大值”)

- 参数列表:

- 格式: 类型 变量名 ,多个参数用逗号分隔(如 int a, float b )

- 作用:接收外部传入的数据,相当于函数的“原材料”

- 函数体:

- 用 {} 包裹的代码,实现具体逻辑

- 可以定义局部变量(仅函数内有效)

示例:计算两数之和

int add(int a, int b) { // 定义函数add,接收两个int参数
    int sum = a + b;    // 函数体:计算和
    return sum;         // 返回int类型结果
}

2. 函数声明:告诉编译器“函数存在”
 
- 场景:如果函数定义在调用之后(或在其他文件),需要提前声明

- 语法: 返回值类型 函数名(参数列表); (注意末尾有分号)
 
示例:声明 + 调用 + 定义

// 声明函数(告诉编译器:有一个add函数,接收int a和int b,返回int)
int add(int a, int b); 

int main() {
    int result = add(3, 5); // 调用函数(必须先声明/定义)
    return 0;
}

// 定义函数(实现具体逻辑)
int add(int a, int b) { 
    return a + b;
}

 3. 函数调用:使用函数完成功能
 
- 语法: 函数名(实参列表); 

- 实参:调用时传入的具体值,必须和形参类型、顺序、数量匹配
 
示例:多种调用方式

#include <stdio.h>

// 无返回值函数
void printHello() {
    printf("Hello!\n");
}

// 有返回值函数
int multiply(int a, int b) {
    return a * b;
}

int main() {
    // 调用无返回值函数(直接执行)
    printHello(); 

    // 调用有返回值函数(结果存到变量)
    int res = multiply(4, 5); 
    printf("4*5=%d\n", res); 

    // 直接用返回值参与运算
    printf("(3*2)+(4*5)=%d\n", multiply(3,2) + multiply(4,5)); 
    return 0;
}

三、函数的分类(按定义主体)
 
1. 库函数:C标准库提供的“现成工具”
 
- 特点:

- 由C标准库实现,直接可用

- 需包含对应头文件(如 <stdio.h> 、 <math.h> )

- 常用分类:

- 输入输出: printf() (输出)、 scanf() (输入)

- 字符串操作: strlen() (求长度)、 strcpy() (复制字符串)

- 数学运算: sqrt() (平方根)、 pow() (幂运算)

- 内存操作: malloc() (动态分配内存)、 free() (释放内存)
 
示例:用 sqrt() 计算平方根

#include <stdio.h>
#include <math.h> // 必须包含数学库头文件

int main() {
    double num = 16.0;
    // 调用库函数sqrt,计算平方根
    double result = sqrt(num); 
    printf("sqrt(%.1f) = %.1f\n", num, result); // 输出:4.0
    return 0;
}

2. 自定义函数:自己实现的“专属工具”
 
- 特点:根据需求自由定义逻辑,完全可控

- 场景:实现业务逻辑(如游戏中的“玩家攻击函数”、“数据排序函数”)
 
示例:判断是否为质数

#include <stdio.h>
#include <stdbool.h> // 启用bool类型(C99及以上支持)

// 自定义函数:判断n是否为质数
bool isPrime(int n) {
    if (n <= 1) return false; // 1及以下不是质数
    for (int i=2; i*i <=n; i++) {
        if (n%i == 0) return false; // 能被整除,不是质数
    }
    return true; // 是质数
}

int main() {
    int num = 17;
    if (isPrime(num)) {
        printf("%d是质数\n", num); // 输出:17是质数
    } else {
        printf("%d不是质数\n", num);
    }
    return num;
}

四、参数传递的2种方式(值传递 vs 地址传递)
 
1. 值传递:“复制一份”给函数
 
- 原理:调用时,实参的值复制给形参,函数内修改形参不影响实参

- 语法:直接传变量/常量(如 add(3,5) 、 printNum(num) )
 
示例:值传递的特点

#include <stdio.h>

void changeValue(int x) {
    x = 100; // 修改的是形参x(实参num不受影响)
    printf("函数内x=%d\n", x); // 输出:100
}

int main() {
    int num = 10;
    changeValue(num); // 传值调用
    printf("函数外num=%d\n", num); // 输出:10(实参没被修改)
    return 0;
}

2. 地址传递(指针传递):“传变量的地址”
 
- 原理:

- 实参是变量的地址(如 &num ),形参是指针(如 int *p )

- 函数内通过 *p 操作,直接修改实参的内存值

- 语法:形参用指针( int *p ),调用时传地址( changeValue(&num) )
 
示例:用地址传递交换两个数

#include <stdio.h>

// 地址传递:交换a和b的值
void swap(int *a, int *b) {
    int temp = *a; // *a访问指针指向的变量(实参num1)
    *a = *b;       // 修改实参num1的值
    *b = temp;     // 修改实参num2的值
}

int main() {
    int num1 = 5, num2 = 10;
    printf("交换前:num1=%d, num2=%d\n", num1, num2); // 5,10

    swap(&num1, &num2); // 传地址

    printf("交换后:num1=%d, num2=%d\n", num1, num2); // 10,5
    return 0;
}

3.两种传递方式的对比

传递方式特点适用场景
值传递

形参是实参的“副本”

修改不影响实参

只需要用实参的值,

不修改实参

址传递

形参是实参的“地址”

可直接修改实参

需要修改实参、

传大数据(优化)


五、函数的存储与生命周期(内存视角)
 
1. 函数代码存在哪?
 
- 代码段(Code Segment):

- 函数的二进制指令存在这里,只读(防止被意外修改)

- 多个调用共享同一段代码(高效复用)
 
2. 函数的参数和局部变量存在哪?
 
- 栈(Stack):

- 调用函数时,参数、局部变量会被压入栈

- 函数返回时,栈帧(Stack Frame)销毁,局部变量失效

- 特点:自动分配和释放,速度快,但空间有限
 
栈帧示意图(简化)

|  高地址  | 
|  ...    |
|  调用者的栈帧  |
|  函数A的栈帧   | ← 调用函数A时,参数和局部变量存在这里
|  低地址  |

3. 全局变量和静态变量存在哪?
 
- 全局/静态区(Global/Static Segment):

- 全局变量(定义在函数外)、 static 修饰的局部变量存在这里

- 程序运行期间一直存在,生命周期长
 
示例:静态变量的生命周期

#include <stdio.h>

void test() {
    static int count = 0; // 静态变量,存在全局区
    count++;
    printf("count=%d\n", count);
}

int main() {
    test(); // count=1(第一次调用,初始化)
    test(); // count=2(第二次调用,复用之前的count)
    test(); // count=3
    return 0;
}

六、函数的高级特性
 
1. 递归函数:自己调用自己
 
- 核心条件:

- 终止条件(否则无限递归,栈溢出!)

- 每次递归缩小问题规模,逼近终止条件

- 经典示例:计算阶乘

#include <stdio.h>

// 递归计算n!:n! = n * (n-1)! ,终止条件n=0时返回1
int factorial(int n) {
    if (n == 0) return 1; // 终止条件
    return n * factorial(n-1); // 递归调用
}

int main() {
    int n = 5;
    printf("%d! = %d\n", n, factorial(n)); // 输出:120
    return 0;
}

- 递归的风险:

- 栈溢出:递归太深(如计算 factorial(10000) ),栈帧过多导致栈溢出

- 调试难度:多层递归时,调用栈复杂
 
2. 函数指针:指向函数的指针
 
- 语法: 返回值类型 (*指针名)(参数列表); 

- 作用:

- 用指针“存储函数地址”,实现动态选择函数(如排序时选不同比较函数)
 
示例:用函数指针实现“动态运算”

#include <stdio.h>

// 加法函数
int add(int a, int b) { return a + b; }
// 减法函数
int sub(int a, int b) { return a - b; }

int main() {
    // 定义函数指针:指向“接收两个int,返回int”的函数
    int (*op)(int, int); 

    // 让指针指向add函数
    op = add; 
    printf("3+5=%d\n", op(3,5)); // 输出:8

    // 让指针指向sub函数
    op = sub; 
    printf("10-4=%d\n", op(10,4)); // 输出:6

    return 0;
}

3. 回调函数:把函数当参数传递
 
- 原理:

- 把函数指针作为参数,传给其他函数

- 被调用的函数(回调函数)在特定时机执行

- 经典场景:排序( qsort 库函数需要传入比较函数)
 
示例:用 qsort 排序数组(自定义比较函数)

#include <stdio.h>
#include <stdlib.h> // 包含qsort

// 比较函数:用于qsort,升序排序int数组
int compareInt(const void *a, const void *b) {
    // 转换为int指针,取值比较
    return *(int*)a - *(int*)b; 
}

int main() {
    int arr[] = {5, 2, 8, 1, 9};
    int len = sizeof(arr)/sizeof(arr[0]);

    // qsort参数:数组、元素个数、元素大小、比较函数(回调)
    qsort(arr, len, sizeof(int), compareInt); 

    for (int i=0; i<len; i++) {
        printf("%d ", arr[i]); // 输出:1 2 5 8 9
    }
    return 0;
}

4. 内联函数(Inline Function):减少函数调用开销
 
- 语法:用 inline 修饰函数(建议配合 static 或在头文件定义)

- 原理:

- 编译时,把函数体“内联”到调用处,替代函数调用

- 优点:减少调用开销(跳转、栈操作等)

- 缺点:代码膨胀(每个调用处复制函数体)

- 适用场景:短小、频繁调用的函数(如获取配置参数)
 
示例:内联函数的使用

#include <stdio.h>

// 内联函数:返回两数中的较小值
inline static int min(int a, int b) {
    return a < b ? a : b;
}

int main() {
    // 编译时,这里会替换成:int res = (3 < 5 ? 3 : 5);
    int res = min(3, 5); 
    printf("min is %d\n", res); // 输出:3
    return 0;
}

七、函数的设计原则(写出高质量函数)
 
1. 单一职责原则
 
- 要求:一个函数只做一件事(如 calculateSum() 只计算和, printResult() 只负责输出)

- 反例:一个函数又计算又打印又修改全局变量,逻辑混乱
 
2. 高内聚、低耦合
 
- 高内聚:函数内部逻辑紧密相关,不依赖外部无关变量

- 低耦合:函数之间通过参数、返回值交互,减少全局变量依赖
 
3. 避免副作用
 
- 副作用:函数除了返回值,还修改全局变量、外部状态(难预测、难调试)

- 建议:优先写“纯函数”(输入决定输出,不影响外部)
 
示例:纯函数 vs 有副作用的函数

// 纯函数(推荐):输入a和b,输出和,不影响外部
int add(int a, int b) { 
    return a + b;
}

// 有副作用(不推荐):修改全局变量,逻辑隐蔽
int globalNum = 10;
int badAdd(int a) { 
    globalNum += a; // 修改全局变量
    return globalNum;
}

4. 合理控制参数和返回值
 
- 参数:

- 数量不宜过多(最多5-7个,否则难维护)

- 区分输入参数(只读)和输出参数(通过指针修改)

- 返回值:

- 清晰表达函数的结果(如 bool isSuccess() 返回执行状态)

- 复杂结果可用指针作为输出参数(如 void getStats(int *min, int *max) )


八、常见问题与调试技巧
 
1. 常见错误
 
- 未声明函数:调用函数前没声明/定义,编译器报错“implicit declaration”

- 参数不匹配:实参与形参类型/数量不一致(如 add(3, 5.0) ,形参是 int )

- 返回值遗漏:有返回值的函数忘记 return (或 return 类型不匹配)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值