C++函数
C++函数基础知识
为什么函数参数需要指针
- 提高传输效率:直接传地址而不需要进行原类型下的值拷贝,如果原类型很大,这将是很耗时的;而地址的拷贝总是整数
- 利用函数的副作用:可能需要改变原内存中的数据,利用指针才能修改
- 如果确定不想修改地址中的值,可以传指针常量
函数指针
指向函数的指针
|
运行时环境

函数声明原则(函数原型)
- 先声明后使用
- 声明时可以不给出定义,定义的位置随意(但要在声明后),如果不在同一文件需要引入或扩展声明告诉编译器声明的位置
- 属于statement
- 声明参数时可以只声明类型而不写形参
- 编译器负责检查定义时的形参类型和声明是否一致
double f(int, int, double); |
函数定义原则
- 定义不允许嵌套(不像python可以def里面套def)
- 先声明后使用
- 声明之后要给出定义,可以立刻给出,也可以之后给出
double f(int a, int b, double c) { |
函数的执行机制
- 建立被调用函数的栈空间
- 参数传递
- 值传递
- or 引用传递
- 保存调用函数的运行状态
- 将控制权转交被调函数
函数调用
main
函数调用其他函数,函数调用在内存中以栈帧的形式存在
函数栈帧的基地址在寄存器ebp
中,栈顶指针(即栈中当前活动的内存位置)在寄存器esp
中
栈帧的结构:
- 函数调用前:
EBP
保存的是调用者的栈帧基地址。 - 函数调用时:新函数的
EBP
被保存到栈中,然后EBP
被更新为当前函数的栈帧基地址。 - 函数返回时:栈恢复到调用函数的状态,
EBP
恢复为调用者的栈帧基地址。
栈操作:
- 函数调用时:
ESP
会减小,因为函数的返回地址和参数会被推入栈中。 - 函数返回时:
ESP
会增大,栈空间会被清理,恢复到调用函数的状态。
参数传递
- 传值
- 传引用
传值
int add(int a, int b) { |
传引用
int add(int& a, int& b) { |
调用约定
- __cdecl
- __stdcall
- __fastcall
- __thiscal
__cdecl
、__stdcall
、__fastcall
和 __thiscall
都是 调用约定(calling conventions)的一部分,定义了函数调用时,如何传递参数、返回值,以及函数如何清理栈等细节。不同的调用约定通常影响程序的性能和兼容性,并且在不同的平台和编译器之间可能会有所不同。
特性 | __cdecl |
__stdcall |
__fastcall |
__thiscall |
---|---|---|---|---|
传递参数的顺序 | 从右到左 | 从右到左 | 前两个通过寄存器,剩余通过栈 | this 通过 ECX,其他参数通过栈 |
堆栈清理 | 调用者负责清理 | 被调用者负责清理 | 调用者负责清理 | 调用者负责清理 |
返回值传递 | 通过 EAX | 通过 EAX | 通过 EAX | 通过 EAX |
常见应用 | C/C++ 中常用函数 | Windows API 函数 | 高性能函数调用,少量参数的优化 | C++ 成员函数(默认) |
其它少见的传参方式(应该不重要)
- 按名称传递 call by name
- 按值结果传递 call by value-result
按名称传递
Call by Name
是一个较为少见的参数传递方式,最初由 Algol 60
等编程语言引入。它的特点是:
- 参数传递:函数不会直接传递参数的值,而是将表达式(即函数参数)作为一个代码片段传递给函数。每次在函数体内使用该参数时,都会重新计算该参数的值。
- 计算延迟:当你使用这个参数时,它将被重新求值,即使函数在多次调用中使用了相同的参数。
C++
并不支持按名称传递,想要使用就需要利用宏
|
按值结果传递
Call by Value-Result
(也称为 Call by Copy-Return)是一个更少见的传递方式,通常不直接出现在 C++ 中。它结合了 Call by Value
和 Call by Reference
的特点:
- 传递方式:在函数调用开始时,实参的值会被复制到函数的形参中;然后,函数执行时,形参会根据需要修改,但是在函数结束时,形参的值会复制回实参。
- 特点:这种方式可以理解为,首先按值传递实参,然后按结果传递回实参,即返回值会被赋给调用者的变量。
C++
模拟call by value-result:
void func(int x) { |
函数重载
也是一种多态的体现
原则
- 函数名相同但参数列表不同,参数列表不同表现为(个数、类型、顺序至少其一有区别)
- 切记不看返回值(因为返回值不是函数调用处必须使用的,函数调用时可能忽略返回值,那编译器就不知道该调哪个函数了)
匹配规则
- 严格匹配
- 若需要类型转换:
- 优先内置转换
- 其次用户定义
详细的匹配规则见 C++面向对象编程进阶II
实现原理
编译器维护符号表
默认参数
C++支持在函数声明时给出默认参数值,且若有,必须在声明时给出
与python
一样,默认参数优先放在参数列表中靠右位置,且不能间断(即不能中间插个普通参数)
可以匿名给出参数默认值,即声明时还是不必须写形参
默认参数可能会带来歧义!!!
示例:
void f(int); // 没有默认参数的函数 |
正确例子:
|
内联函数
inline
关键字用于声明匿名函数
编译器如何实现内联函数
与编译器优化中的函数内联(在函数调用的地方将函数展开,用函数定义的代码替换函数调用)相同,编译器将为inline
函数创建一段代码,在调用点以相应的代码替换
因此被inline
修饰的函数在编译过程中就已经被替换成具体的代码了,而不再会发生函数调用,那为什么还要用内联函数?
目的
- 提高可读性:编译器眼中没有函数但程序员不是,可以提高代码的可读性
- 提高效率:编译器不再需要进行函数调用,而是直接顺序地执行等价代码,避免了函数调用的时间开销
限制
- 内联函数不能是递归的
- 不能是函数指针
inline int add(int a, int b) { |
使用场景
适合于频率高、简单的小段代码
事实上即使不使用inline
关键字,在开启O1/O2/O3
或指定函数内联的编译器优化的情况下,编译器自己也会将源代码中一些简短的函数内联优化掉,从而避免频繁的函数调用。
避免滥用inline
经过计算机组成原理课程的学习,我们知道计算机硬件具有时空局部性的特点,即连续时间内更可能访问连续的空间(比如遍历数组),而大量inline的使用可能会导致代码体积的增大,使得代码本身占的空间变大,可能会导致病态的换页,降低指令快取装置的命中率。
缺点总结:
- 增大目标代码
- 病态的换页
- 降低指令快取装置的命中率
(•‿•)