概念
函数是一段独立代码块, 用以执行特定任务或计算, 并可能返回结果
void hello() {
printf("Hello World\n");
}
组成
(1) 函数头, 包含
-
返回类型: 指定函数返回值类型. 可为
void -
函数名: 标识符, 用于调用函数
-
参数列表: 括号内列出输入参数类型和名称
(2) 函数体, 包含执行任务的语句序列, 并以 return(如果有返回值)或大括号结束
- 示例, 求和函数
int add(int x, int y) {
return x + y
}
声明与定义
声明
函数声明告诉编译器函数接口信息, 不包含实现代码通常出现在头文件 .h 中
- 示例, 声明add函数
// add_module.h
#ifndef __ADD_MODULE_H__
#define __ADD_MODULE_H__
int add(int x, int y);
#endif // __ADD_MODULE_H__
定义
函数定义包含函数完整实现, 通常出现在源文件 .c/.cpp 中
- 示例, 定义add函数
#include "add_module.h"
int add(int x, int y) {
return x + y;
}
联系
声明提供函数接口信息, 定义实现功能
二者必须保持一致: 函数名、返回类型、参数类型和顺序一致
区别
函数声明不分配存储空间, 只告诉编译器函数存在和接口信息
函数定义则分配存储空间, 并包含函数完整实现代码
形参与实参
形参(parameter)
函数定义中的参数, 占位符, 用于在函数体内引用传入数据作用域仅在函数内部
实参(argument)
函数调用时传入的实际值, 与形参一一对应
- 示例, add函数
int add(int x, int y) {
return x + y;
}
int r = add(a, b);
参数传递
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\n", &x);
printf("parameter y address = %p\n", &y);
x += 1;
y += 2;
}
int main() {
int x = 1;
double y = 2;
printf("parameter x address = %p\n", &x);
printf("parameter y address = %p\n", &y);
add(x, y);
printf("x = %d\ny = %f\n", x, y);
return 0;
}
运行结果
parameter x address = 0x7FFE3A9D4688
parameter y address = 0x7FFE3A9D4680
parameter x address = 0x7FFE3A9D465C
parameter y address = 0x7FFE3A9D4650
x = 1
y = 2.000000
优点
函数内部对参数修改不会影响到调用者
提供参数封装性, 使函数可以安全地使用参数而不必担心外部副作用
缺点
对于大型结构体或类实例, 因为需要复制整个对象, 拷贝可能会导致性能问题
无法直接修改调用者变量
pass by pointer(指针传递)
函数接收变量地址, 修改形参可影响实参
#include <stdio.h>
void add(int *x, double *y) {
*x += 1;
*y += 2;
}
int main() {
int a = 1;
double b = 2;
add(&a, &b);
printf("a = %d, b = %f\n", a, b);
}
指针传递
实参和形参类型为指针, 函数调用时会传递变量地址值拷贝, 形参可通过指针特性间接操作实参所指向变量, 因此在函数内部实参可以通过指针修改指向对象内容
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;
}
运行结果
argment x address = 0x7FFFB01A1868
argment y address = 0x7FFEFB01A1860
parameter x address = 0x7FFFB01A1868
parameter y address = 0x7FFEFB01A1860
x = 2
y = 4.000000
pass by reference(引用传递)
形参直接引用实参, 形参和实参是同一个变量, 对形参修改会影响实参
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;
}
运行结果
argment x address = 0x7FFEA6489F08
argment y address = 0x7FFEA6489F00
parameter x address = 0x7FFEA6489F08
parameter y address = 0x7FFEA6489F00
x = 2
y = 4.000000
特殊函数
内联函数
内联函数是在编译时展开到调用点的函数, 以减少函数调用开销, 通常用于短小且频繁调用函数
inline int add(int x, int y) {
return x + y;
}
lambda函数
可作为参数传递或立即调用
auto sum = [](int a, int b) { return a + b; };
cout << sum(2, 3);
模板函数
template<typename T>
T add(T a, T b) { return a + b; }
c变长参数
#include <stdarg.h>
void func(char *fmt, ...)
#include <stdio.h>
#include <stdarg.h>
void mini_print(char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
for (char* p = fmt; *p; p++) {
if (*p == '%' && *(p+1)) {
p++;
if (*p == 'd') printf("%d", va_arg(ap,int));
else if (*p == 'f') printf("%f", va_arg(ap,double));
else if (*p == 's') printf("%s", va_arg(ap,char*));
} else putchar(*p);
}
va_end(ap);
}
int main() {
mini_print("name=%s, age=%d, weight=%f\n", "dmjcb", 21, 98.2);
return 0;
}
function stack frame 函数栈帧
函数调用栈帧(function stack frame)是在程序执行过程中当函数调用发生时, 由操作系统或运行时环境为该函数所分配一块内存区域
用于存储函中局部变量、参数、返回地址等信息, 为函数执行提供必要环境
功能
存储局部变量、函数参数
存储返回地址, 支持函数返回
支持递归与嵌套调用, 保证独立空间
结构
局部变量区: 函数内部变量
参数区: 存储调用参数
返回地址区: 存储函数返回位置
帧指针 (frame pointer): 指向栈帧底部
寄存器: 基址指针 %ebp/%rbp、栈指针 %esp/%rsp
函数调用过程
调用函数时, 系统在栈中创建栈帧
参数压栈、返回地址压栈
局部变量分配在栈帧内
函数执行完毕后返回, 弹出栈帧
调试
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;
}
编译为可执行文件
gcc main.c -c -o main
gdb调试
gdb main
终端输出
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...
(No debugging symbols found in main)
在main()设置断点
break main
终端输出
Breakpoint 1 at 0x116f
运行
run
终端输出
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/dmjcb/Documents/code/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555516f in main ()
显示frame
info frame
Stack level 0, frame at 0x7fffffffdb30:
rip = 0x55555555516f in main; saved rip = 0x7ffff7c2a1ca
Arglist at 0x7fffffffdb20, args:
Locals at 0x7fffffffdb20, Previous frame's sp is 0x7fffffffdb30
Saved registers:
rbp at 0x7fffffffdb20, rip at 0x7fffffffdb28
(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函数打印出结果并返回