概念
函数是一段独立代码块, 用以执行特定任务或计算并可能返回结果
void Hello() {
printf("Hello World\n");
}
组成
函数头
包含返回类型、函数名和参数列表
返回类型指定函数返回值类型(可选)
函数名是调用函数时所使用标识符
参数列表(在括号内)包含函数所接收输入参数类型和名称
函数体
包含执行特定任务所使用语句序列, 并以返回语句(如果有返回值)或大括号结束
- 示例, 求和函数
int Add(int x, int y) {
return x + y
}
声明与定义
声明
函数声明告诉编译器函数原型, 包括函数名、返回类型和参数列表, 不包含函数实现代码
函数声明主要作用是让编译器在编译过程中知道函数存在, 以便在后续代码中可以正确调用
当编译器遇到函数调用时, 会根据声明信息来检查调用合法性, 如函数名是否正确、实参与形参类型和个数是否一致等
函数声明通常出现在头文件(.h文件)中, 或者在使用该函数之前进行声明
声明语法格式与函数定义类似, 但不需要函数体, 只需在函数名后括号内列出参数类型和参数名(也可以只列出参数类型而不写参数名), 并在末尾加上分号
- 示例, 声明函数
// add_module.h
#ifndef __ADD_MODULE_H__
#define __ADD_MODULE_H__
int Add(int x, int y);
#endif // __ADD_MODULE_H__
定义
函数定义是函数核心部分, 包含函数完整实现, 其给出实现函数具体代码, 包括函数名、返回类型、参数列表以及函数体, 通常出现在源文件(.cpp文件)中
当程序执行到函数调用时, 会跳转到该函数定义处执行相应代码, 并根据需要返回结果
语法格式包括函数头(与声明类似)和函数体(用大括号所括起来语句集)
- 示例, 函数定义
#include "add_module.h"
int Add(int x, int y) {
return x + y;
}
联系
函数声明与定义共同构成函数完整描述
声明提供函数接口信息, 而定义则实现函数功能
在程序中, 函数声明与定义必须保持一致, 包括函数名、返回类型、参数列表以及参数类型等
区别
函数声明不分配存储空间, 只告诉编译器函数存在和接口信息
函数定义则分配存储空间, 并包含函数完整实现代码
形参与实参
形参(parameter)
函数定义中参数, 指定函数所期望接收数据类型和名称
形参在函数被调用时并不实际存储数据, 而是作为一个占位符, 用于在函数体内引用所传递给函数实际数据
作用域仅限于函数体内, 在函数外部无法直接访问形参
实参(argument)
函数调用时传递参数, 与形参一一对应, 按照函数定义中指定顺序和类型进行匹配
- 示例, 函数定义
int Add(int x, int y) {
return x + y;
}
参数传递
拷贝传递(pass by value)
函数接收实参副本, 对形参修改不影响实参
函数调用时, 会把实参值会被拷贝到形参中, 实参和形参仅值相同, 却是两个完全独立变量, 在函数内部对形参做任何修改都不会影响实参
graph LR;
subgraph parameter
A1(int x)
B1(double y)
C1(char z)
end
subgraph argment
A(int x)
B(double y)
C(char z)
end
A--只拷贝值-->A1
B--只拷贝值-->B1
C--只拷贝值-->C1
值拷贝适用于基本数据类型(如 int, float, char 等)和struct
- 示例, 函数调用值传递
graph LR;
subgraph parameter
subgraph double y
V3(value: 2)
A4(address: 0x7ffe3a9d4650)
end
subgraph int x
V2(value: 1)
A2(address: 0x7ffe3a9d465c)
end
end
subgraph argment
subgraph double y
V1(value: 2)
A1(address: 0x7ffe3a9d4680)
end
subgraph int x
V(value: 1)
A(address: 0x7ffe3a9d4688)
end
end
V-.①仅拷贝值.->V2
V1-.①仅拷贝值.->V3
#include <stdio.h>
void Add(int x, double y) {
printf("parameter x address = %p\nparameter y address = %p\n", &x, &y);
x += 1;
y += 2;
}
int main() {
int x = 1;
double y = 2;
printf("argment x address = %p\nargment y address = %p\n", &x, &y);
Add(x, y);
printf("x = %d\ny = %f\n", x, y);
return 0;
}
优点
函数内部对参数修改不会影响到调用者
提供参数封装性, 使函数可以安全地使用参数而不必担心外部副作用
缺点
对于大型结构体或类实例, 因为需要复制整个对象, 拷贝可能会导致性能问题
无法直接修改调用者变量
引用传递(pass by reference)
函数接收对实参引用(C++)或指针(C), 对形参修改直接影响实参
指针传递
实参和形参类型为指针, 函数调用时会传递变量地址值拷贝, 形参可通过指针特性间接操作实参所指向变量, 因此在函数内部实参可以通过指针修改指向对象内容
- 示例, 函数调用指针传递
graph LR;
subgraph double b
V5(value: 2)
A5(address: 0x7fffb01a1860)
end
subgraph int a
V4(value: 1)
A4(address: 0x7fffb01a1868)
end
subgraph parameter double *y
V3(value: 0x7fffb01a1860)
end
subgraph parameter int *x
V2(value: 0x7fffb01a1868)
end
subgraph argment double *y
V1(value: 0x7fffb01a1860)
end
subgraph argment int *x
V(value: 0x7fffb01a1868)
end
V4-->A4--①-->V
V5-->A5--①-->V1
V-.②仅拷贝值.->V2
V1-.②仅拷贝值.->V3
V2--③通过地址值修改-->A4
V3--③通过地址值修改-->A5
#include <stdio.h>
void Add(int *x, double *y) {
printf("parameter x value = %p\nparameter y value = %p\n", x, y);
*x += 1;
*y += 2;
}
int main() {
int a = 1;
double b = 2;
int *x = &a;
double *y = &b;
printf("argment x value = %p\nargment y value = %p\n", x, y);
Add(x, y);
printf("x = %d\ny = %f\n", a, b);
return 0;
}
C++引用传递
形参直接引用实参, 形参和实参是同一个变量, 对形参修改会影响实参
- 示例, 函数调用引用传递
graph LR;
subgraph parameter
subgraph double y
V3(value: 2)
A3(address: 0x7ffea6489f00)
end
subgraph int x
V2(value: 1)
A2(address: 0x7ffea6489f08)
end
end
subgraph argment
subgraph double y
V1(value: 2)
A1(address: 0x7ffea6489f00)
end
subgraph int x
V(value: 1)
A(address: 0x7ffea6489f08)
end
end
A<--同一个变量-->A2
A1<--同一个变量-->A3
#include <iostream>
void Add(int &x, double &y) {
printf("parameter x address = %p\nparameter y address = %p\n", &x, &y);
x += 1;
y += 2;
}
int main() {
int x = 1;
double y = 2;
printf("argment x address = %p\nargment y address = %p\n", &x, &y);
Add(x, y);
printf("x = %d\ny = %f\n", x, y);
return 0;
}
特殊函数
内联函数
内联函数是在编译时展开到调用点的函数, 以减少函数调用开销, 通常用于短小且频繁调用函数
inline int Add(int x, int y) {
return x + y;
}
lambda函数
C++11及更高版本中所引入一种匿名函数对象
模板函数
C变长参数
#include <stdarg.h>
void func(char *fmt, ...)
声明va_list
va_list ap;
va_list类型用于声明一个变量, 将一次引用各个参数
初始化 va_start
va_start(ap, fmt)
将ap初始化为指向第一个参数指针
处理当前参数 va_arg
va_arg(ap, 类型)
调用va_arg将当前指向参数转换为对应类型并返回, 同时指针移动对应步长, 指向下个参数
清理 va_end
va_end()
执行清理工作
- 示例
#include <stdio.h>
#include <stdarg.h>
void MiniPrint(char *fmt, ...) {
// 声明ap, 依次引用各参数
va_list ap;
// 初始化
va_start(ap, fmt);
// 处理每个参数
for (char* p = fmt; *p; p++) {
if (*p == '%') {
char v = *(p + 1);
if (v == 'd') {
int n = va_arg(ap, int);
printf("%d", n);
}
else if (v == 'f') {
double d = va_arg(ap, double);
printf("%f", d);
}
else if (v == 's') {
for (char* s = va_arg(ap, char *); *s; s++) {
putchar(*s);
}
}
else {
putchar(*p);
}
} else (*p != '%') {
putchar(*p);
}
}
va_end(ap);
}
int main() {
char *name = "dmjcb";
int age = 21;
double weight = 98.2;
MiniPrint("name = %s, age = %d, weight = %f", name, age, weight);
return 0;
}
function stack frame
函数调用栈帧(function stack frame)是在程序执行过程中当函数调用发生时, 由操作系统或运行时环境为该函数所分配一块内存区域
用于存储函中局部变量、参数、返回地址等信息, 为函数执行提供必要环境
功能
存储函数局部变量和参数, 确保函数在执行过程中能够访问和操作这些数据
存储函数返回地址, 确保函数执行完毕后能够正确地返回到调用该函数地方继续执行
支持函数嵌套调用和递归调用, 通过为每个函数调用分配独立栈帧空间, 避免不同函数调用之间数据冲突
结构
局部变量区
存储函数内部所声明局部变量
局部变量生命周期与函数执行周期相关, 函数执行完毕后, 局部变量所占用空间会被释放
参数区
存储函数调用时所传递参数
参数传递方式可能因编程语言和编译器而异, 通常通过栈传递
返回地址区
存储函数执行完毕后需要返回调用处地址, 即下一条指令地址
frame pointer(帧指针)
指向该函数栈帧底部, 在函数执行时可以使用帧指针来访问局部变量和函数参数
其他信息
栈帧还可能包含其他信息, 如上一个栈帧指针(用于支持递归调用和异常处理)、栈指针(指向栈顶地址)等
寄存器
基址指针寄存器(如x86架构中%ebp), 用于指向当前栈帧底部, 通过它可以访问到栈帧中局部变量和参数
栈指针寄存器(如x86架构中%esp), 用于指向当前栈顶地址, 即最后被压入栈数据所在内存地址
在函数调用过程中, 栈指针会随着数据压入和弹出而移动
函数调用过程
一般函数
编译调试
clang *.c -g -o 可执行程序
lldb 可执行程序
设置断点运行
break 函数名
run
查看调用栈
使用backtrace(或简写为bt)命令来查看当前函数调用栈
显示一个函数调用栈列表, 包括每个函数名称和地址
切换特定栈帧
使用frame命令来切换到特定栈帧
例如, frame N会切换到编号为N栈帧
查看栈帧信息
info frame
, 查看当前栈帧详细信息, 包括栈帧地址、大小和上一个栈帧指针等
info args
查看当前函数参数
info locals
查看当前函数局部变量
info registers
查看当前寄存器值
- 示例, gdb显示栈帧
// main.c
#include <stdio.h>
int Add(int a, int b) {
int sum = a + b;
return sum;
}
int main() {
int result = Add(3, 5);
printf("result = %d\n", result);
return 0;
}
(1) main函数被调用时, 系统会创建一个栈帧, 包含main函数局部变量和参数
(2) main函数中调用Add函数时, 会将add函数参数3和5压入栈中, 并将add函数返回地址也压入栈中
(3) 系统会为Add函数创建一个新栈帧, 包含Add函数局部变量(如sum)和参数(从栈中弹出3和5)
(4) Add函数执行完毕后, 会将结果8放入其栈帧返回位置, 并弹出其栈帧, 返回到main函数
(5) main函数从栈中弹出add函数返回值8, 并将其存储在局部变量result中
(6) 最后main函数打印出结果并返回