C++函数基础知识

为什么函数参数需要指针

  • 提高传输效率:直接传地址而不需要进行原类型下的值拷贝,如果原类型很大,这将是很耗时的;而地址的拷贝总是整数
  • 利用函数的副作用:可能需要改变原内存中的数据,利用指针才能修改
  • 如果确定不想修改地址中的值,可以传指针常量

函数指针

指向函数的指针

#include<bits/stdc++.h>
using namespace std;

double f(int);
int g();

int main() {
// 声明函数指针
double (*fp)(int) = &f; // 解释:声明一个函数指针,返回值是double,带一个参数int
int (*gp)() = &g; // 声明一个函数指针,返回值是int,不需要参数;*gp外面的()用来改变优先级,后面的()表示在描述参数,两者配合告诉编译器在声明函数指针
cout << (*fp)(1) << endl; // 2.2
cout << (*gp)() << endl; // 0
}

double f(int a) {
return a + 1.2;
}

int g() {
return 0;
}

运行时环境

函数声明原则(函数原型)

  • 先声明后使用
  • 声明时可以不给出定义,定义的位置随意(但要在声明后),如果不在同一文件需要引入或扩展声明告诉编译器声明的位置
  • 属于statement
  • 声明参数时可以只声明类型而不写形参
  • 编译器负责检查定义时的形参类型和声明是否一致
double f(int, int, double);

函数定义原则

  • 定义不允许嵌套(不像python可以def里面套def)
  • 先声明后使用
  • 声明之后要给出定义,可以立刻给出,也可以之后给出
double f(int a, int b, double c) {
return a * b * c;
}

函数的执行机制

  1. 建立被调用函数的栈空间
  2. 参数传递
    • 值传递
    • or 引用传递
  3. 保存调用函数的运行状态
  4. 将控制权转交被调函数

函数调用

main函数调用其他函数,函数调用在内存中以栈帧的形式存在

函数栈帧的基地址在寄存器ebp中,栈顶指针(即栈中当前活动的内存位置)在寄存器esp

栈帧的结构:

  • 函数调用前EBP 保存的是调用者的栈帧基地址。
  • 函数调用时:新函数的 EBP 被保存到栈中,然后 EBP 被更新为当前函数的栈帧基地址。
  • 函数返回时:栈恢复到调用函数的状态,EBP 恢复为调用者的栈帧基地址。

栈操作:

  • 函数调用时ESP 会减小,因为函数的返回地址和参数会被推入栈中。
  • 函数返回时ESP 会增大,栈空间会被清理,恢复到调用函数的状态。

参数传递

  • 传值
  • 传引用

传值

int add(int a, int b) {
return a + b;
}

int main() {
int r = add(1, 2);
}

传引用

int add(int& a, int& b) {
return a + b;
}

int main() {
int a = 1, b = 2;
int r = add(a, 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++并不支持按名称传递,想要使用就需要利用宏

#define CALL_BY_NAME(x) (x * x) // 传递一个表达式

void func(int x) {
std::cout << x << std::endl;
}

int main() {
int a = 3;
func(CALL_BY_NAME(a)); // 相当于 func(a * a)
}

按值结果传递

Call by Value-Result(也称为 Call by Copy-Return)是一个更少见的传递方式,通常不直接出现在 C++ 中。它结合了 Call by ValueCall by Reference 的特点:

  • 传递方式:在函数调用开始时,实参的值会被复制到函数的形参中;然后,函数执行时,形参会根据需要修改,但是在函数结束时,形参的值会复制回实参。
  • 特点:这种方式可以理解为,首先按值传递实参,然后按结果传递回实参,即返回值会被赋给调用者的变量。

C++模拟call by value-result:

void func(int x) {
x = x + 1; // 这里修改了参数
}

int main() {
int y = 5;
y = func(y); // 在函数返回时,y 的值会根据 x 的值更新
// 在真正的按值结果传递中,直接func(y)后y的结果就被形参x更新了(有点类似传引用)
}

函数重载

也是一种多态的体现

原则

  • 函数名相同但参数列表不同,参数列表不同表现为(个数、类型、顺序至少其一有区别)
  • 切记不看返回值(因为返回值不是函数调用处必须使用的,函数调用时可能忽略返回值,那编译器就不知道该调哪个函数了)

匹配规则

  • 严格匹配
  • 若需要类型转换:
    • 优先内置转换
    • 其次用户定义

详细的匹配规则见 C++面向对象编程进阶II

实现原理

编译器维护符号表

默认参数

C++支持在函数声明时给出默认参数值,且若有,必须在声明时给出

python 一样,默认参数优先放在参数列表中靠右位置,且不能间断(即不能中间插个普通参数)

可以匿名给出参数默认值,即声明时还是不必须写形参

默认参数可能会带来歧义!!!

示例:

void f(int); // 没有默认参数的函数

void f(int, int=2); // 带默认参数的函数,匿名给出默认参数,但在具体调用时因为可以不写第二个参数,导致与上面那个函数产生歧义
// 即f(1)该调用哪个?
// 上述代码编译错误

正确例子:

#include <bits/stdc++.h>
using namespace std;

int f(int, int=2);

int main() {
cout << f(1) << endl; // 3
cout << f(1, 3) << endl; // 4
}

int f(int a, int b) {
// 若调用时不传参数b,则默认b = 2
return a + b;
}

内联函数

inline关键字用于声明匿名函数

编译器如何实现内联函数

与编译器优化中的函数内联(在函数调用的地方将函数展开,用函数定义的代码替换函数调用)相同,编译器将为inline函数创建一段代码,在调用点以相应的代码替换

因此被inline修饰的函数在编译过程中就已经被替换成具体的代码了,而不再会发生函数调用,那为什么还要用内联函数?

目的

  • 提高可读性:编译器眼中没有函数但程序员不是,可以提高代码的可读性
  • 提高效率:编译器不再需要进行函数调用,而是直接顺序地执行等价代码,避免了函数调用的时间开销

限制

  • 内联函数不能是递归的
  • 不能是函数指针
inline int add(int a, int b) {
return a + b;
}

int c = add(1, 2);
// 编译后变为int c = 1 + 2;

使用场景

适合于频率高、简单的小段代码

事实上即使不使用inline关键字,在开启O1/O2/O3指定函数内联的编译器优化的情况下,编译器自己也会将源代码中一些简短的函数内联优化掉,从而避免频繁的函数调用。

避免滥用inline

经过计算机组成原理课程的学习,我们知道计算机硬件具有时空局部性的特点,即连续时间内更可能访问连续的空间(比如遍历数组),而大量inline的使用可能会导致代码体积的增大,使得代码本身占的空间变大,可能会导致病态的换页,降低指令快取装置的命中率。

缺点总结:

  • 增大目标代码
  • 病态的换页
  • 降低指令快取装置的命中率

(•‿•)