1. C语言函数组成
// 返回类型 函数名 参数列表 int add (int a, int b) { // 函数体 int ret = a + b; // 返回值 return ret; }在C语言中,函数是执行特定任务的独立代码块。一个函数可以接收参数(如果有的话),执行一系列操作,然后可选地返回一个值。C语言中的函数组成主要包括以下几个部分:
返回类型:
函数的返回类型指定了函数执行完成后返回值的类型。例如,int、void、float、char等。void表示该函数不返回任何值。函数名:
这是函数的标识符,用于在程序中调用该函数。函数名必须是合法的标识符,且在程序中是唯一的。参数列表:
参数列表定义了函数执行时可以接收的数据。参数可以有零个或多个,每个参数都有一个类型和一个名称。参数之间用逗号分隔。如果函数不需要参数,则参数列表为空,写成void(例如int add(int a, int b))。函数体:
函数体是包含了一组执行特定任务的语句的大括号{}内的部分。这是函数的实际代码实现。返回值:
在函数体内部,可以使用return语句返回一个值。返回值的类型必须与函数的返回类型匹配。如果函数的返回类型是void,则不能使用return语句返回一个值(但可以使用return;仅表示提前退出)。
2. C语言函数传入传出参数
在读下面内容之前,可以先思考一个问题:为什么使用变量传入参数给函数,在函数里面无法直接改变传入变量的值?
还是以 add 函数为例。c语言代码:
int add(int a, int b) { return a + b; } int main(void) { add(1, 2); return 0; }对应ARM汇编代码:
add: 0x080001AE 4602 MOV r2, r0 ; 把r0的值给 r2寄存器 0x080001B0 1850 ADDS r0, r2, r1 ; r2和r1的值相加,给r0, 用于传出参数 0x080001B2 4770 BX lr ; 跳转回函数调用地址 main: 0x080001B6 2102 MOVS r1, #0x02 ; 立即数赋值. 把 数字2 写入 r1寄存器 0x080001B8 2001 MOVS r0, #0x01 ; 把 数字1 写入 r0寄存器 0x080001BA F7FFFFF8 BL.W add (0x080001AE) ; 调用/进入 add 函数 0x080001BE 2000 MOVS r0, #0x00 ; return 0从汇编代码可以看出:
- 传入参数:对函数传入参数时,需要把参数传给 r 寄存器,进入函数后,函数再从 r 寄存器里获取数据。
- 传出参数:函数传出或者返回值时,同样通过 r 寄存器向外传入参数。
也就是说 r 寄存器可以作为函数传入传出的介质。
以下为ARM寄存器组说明:
| 寄存器 | 别名 | 主要用途 | 特殊说明 |
|---|---|---|---|
R0 | A1 | 函数第 1 个参数 / 函数返回值 | ARM 标准调用约定下,函数返回值必须通过R0返回 |
R1 | A2 | 函数第 2 个参数 | - |
R2 | A3 | 函数第 3 个参数 | - |
R3 | A4 | 函数第 4 个参数 | - |
R4 | V1 | 通用变量寄存器(需被调用者保存) | 若函数修改了R4-R11,需手动保存到栈 |
R5 | V2 | 通用变量寄存器(需被调用者保存) | - |
R6 | V3 | 通用变量寄存器(需被调用者保存) | - |
R7 | V4 | 通用变量寄存器 / 帧指针(FP) | Thumb 指令集中常作为栈帧指针 |
R8 | V5 | 通用变量寄存器(需被调用者保存) | - |
R9 | V6 | 通用变量寄存器 / 平台寄存器 | 部分操作系统中用作全局变量指针(SB) |
R10 | V7 | 通用变量寄存器(需被调用者保存) | - |
R11 | V8 | 通用变量寄存器(需被调用者保存) | - |
R12 | IP | 内部过程调用寄存器 | 用于动态链接、库函数调用时的临时中转 |
R13 | SP | 栈指针(Stack Pointer) | 指向栈顶,硬件自动维护(Cortex-M 为满递减栈) |
R14 | LR | 链接寄存器(Link Register) | 保存函数 / 中断的返回地址,BL指令会自动写入 |
R15 | PC | 程序计数器(Program Counter) | 指向当前执行指令的下一条指令地址,修改PC会直接跳转 |
ARM约定,R0-R3 寄存器传递函数前 4 个参数。
那么如果函数大于四个参数呢?
聪明的你应该已经想到了,内存也可以用于保存参数。ARM正是这样做的:超过4个参数,则进行压栈处理。
| 操作 | 规则 |
|---|---|
| 传参 | R0-R3传递前 4 个参数,超过部分压入栈 |
| 返回值 | 32 位 / 64 位结果通过R0(+R1)返回 |
| 寄存器保存 | R0-R3, R12由调用者保存;R4-R11, LR由被调用者保存 |
当然,不同芯片架构,不同编译器也会有区别,这里不展开讨论。
回到上面的问题:为什么使用变量传入参数给函数,在函数里面无法直接改变传入变量的值?
简单来说,因为数据经过了拷贝,在函数里是改变的并不是原来的变量里的内容。
3. 函数调用和返回
我们还是看上面的例子,但只看调用和返回部分。
add: 0x080001B2 4770 BX lr ; 跳转回函数调用地址 0x080001BE main: 0x080001BA F7FFFFF8 BL.W add (0x080001AE) ; 调用/进入 add 函数 0x080001BE 2000 MOVS r0, #0x00 ; return 0我们在第一篇讲过,汇编代码组织方式可以按功能流程组织代码块,执行不同功能时只需要跳转到对应的代码块。
C语言进入到子函数也是通过跳转方式实现的,跳转前BL会自动将下一条指令的地址放入LR寄存器,子函数通过LR寄存器的地址返回到原来的地址。
我们再看看多层嵌套函数:
function_2: 0x080001B8 4770 BX lr function_1: 0x080001BA B500 PUSH {lr} 0x080001C0 F7FFFFF8 BL.W function_2 (0x080001B4) 0x080001C4 BD00 POP {pc} main: 0x080001CA F7FFFFF6 BL.W function_1 (0x080001BA) 0x080001CE 2000 MOVS r0, #0x00function_1 进入子函数 function_2 前,需要把 LR寄存器 PUSH压栈,返回的时候POP 值直接给到PC,CPU立即跳回调用者的下一行。更多层的函数嵌套也是一样的。
这里有一个小技巧:在调试时,我们可以使用调试器的调用栈功能,如果代码出现fault,就能很直观的看到函数调用关系,方便解bug。
4. C语言代码优化
经过上面的内容,我们能明显发现一个问题,每次调用函数都需要进行参数拷贝,不仅需要消耗拷贝时间,参数多的时候也消耗内存空间。另外,函数调用的越深,栈使用的也会越多。在使用内存资源比较小的芯片,我们需要尽可能的减少资源消耗。
1. 对于参数拷贝问题,我们可以使用以下方法优化:
- 用宏替代简单的函数。
- inline 内联函数。需要注意的是,内联函数代码及变量需要尽可能的少,否则在内联函数展开后可能会适得其反。
但其实一般情况下,编译器已经能做的足够好,只要我们提高编译优化等级就能很好的优化掉此类问题。
2. 对于函数调用深度问题,可以考虑以下方法:
- 避免过度封装函数
- 定时器等回调函数可以设置触发标志,在另一处进行具体函数处理
- 使用事件驱动型架构,降低调用层级