1. 函数基础
● 函数:封装了一段代码,可以在一次执行过程中被反复调用。
A、函数头:
● 函数名称 —— 标识符,用于后续的调用
● 形式参数 —— 代表函数的输入参数
● 返回类型 —— 函数执行完成后所返回的结果类型
B、函数体
● 为一个语句块( block ){ },包含了具体的计算逻辑
● 函数声明与定义
– 函数声明只包含函数头,不包含函数体,通常置于头文件中
– 函数声明可出现多次,在同一个作用域其实没必要啊,但函数定义通常只能出现一次(存在例外,内联函数)
C、 函数调用
– 需要提供函数名与实际参数
– 实际参数拷贝初始化形式参数, 类型即可
– 返回值会被拷贝给函数的调用者
– 栈帧结构
● 拷贝过程的(强制)省略
– 返回值优化
– C++17 强制省略拷贝临时对象
● 函数的外部链接
2. 函数详解
A、参数
● 函数可以在函数头的小括号中包含零到多个形参
– 包含零个形参时,可以使用 void 标记
– 对于非模板函数来说,其每个形参都有确定的类型,但形参可以没有名称
– 形参名称的变化并不会引入函数的不同版本甚至有没有名字都不重要,但是类型必须完全一致!
– 实参到形参的拷贝求值顺序不定, C++17 强制省略复制临时对象
● 函数传值、传址、传引用
● 函数传参过程中的类型退化:多为数组最高位会退化
● 变长参数
– initializer_list(推荐):原理是包含两个指针一个指向开头,一个指向结尾的下一个
– 可变长度模板参数
– 使用省略号表示形式参数(不建议)
● 函数可以定义缺省实参
– 如果某个形参具有缺省实参,那么它右侧的形参都必须具有缺省实参
– 在一个翻译单元中(一个cpp中 ),每个形参的缺省实参只能定义一次,声明或者定义中有一次即可,多次会报错
– 具有缺省实参的函数调用时,传入的实参会按照从左到右的顺序匹配形参
– 缺省实参为对象时,实参的缺省值会随对象值的变化而变化
● main 函数的两个版本
– 无形参版本
int main()
{
}
– 带两个形参的版本
int main(int argc, int *argv[])
{
}
argv[0]就是可执行程序名
- argc—非负数,表示从程序运行的环境传递给程序的实参个数。
- argv—指针,指向包含 argc+1个指针的数组的首元素。数组末元素为空指针,若其前面有任何元素,则它们指向空终止多字节字符串,表示从执行环境传递给程序的若干参数。若 argv[θ]不是空指针,或等价地 argc>0,则它指向表示用于调用程序的名称的字符串,或空字符串。
B、函数体
函数体形成域:
– 其中包含了自动对象(内部声明的对象以及形参对象)
– 也可包含局部静态对象
● 函数体执行完成时的返回
– 隐式返回,没有显示return返回,比如void
– 显式返回关键字: return
●return; 语句
●return 表达式 ;
●return 初始化列表 ;
– 小心返回自动对象的引用或指针,不要返回函数内部定义的变量 ,static可以忽略
– 返回值优化( RVO )—— C++17 对返回临时对象的强制优化
C、返回类型
● 返回类型表示了函数计算结果的类型,可以为 void
● 返回类型的几种书写方式
– 经典方法:位于函数头的前部
– C++11 引入的方式:位于函数头的后部
– C++14 引入的方式:返回类型的自动推导
● 使用 constexpr if 构造 具有不同返回类型 的函数 “ ”
● 返回类型与结构化绑定( C++ 17 )
#include <iostream>
struct Str{
int x;
int y;
};
Str fun() {
return Str{};
}
int main()
{
auto & [v1,v2] = fun();
}
●[[nodiscard]] 属性( C++ 17 )
无用的代码:
需要保留返回值:
#include <iostream>
[[nodiscard]] int fun(int a,int b)
{
return a+b;
}
int main()
{
int x = fun(2,3);
}
3. 函数重载与重载解析(c++特有)
A、函数重载:
使用相同的函数名定义多个函数,每个函数具有不同的参数列表,可以是不同的参数个数或参数类型,但是 不能基于不同的返回类型进行重载,会被编译器认为是重新定义函数。
#include <iostream>
int fun(int x)
{
return x+1;
}
double fun(double x)
{
return x+1;
}
int main()
{
std::cout <<fun(3)<< std::endl;
}
B、编译器如何选择正确的版本完成函数调用 ?
– 参考资源: Calling Functions: A Tutorial
C、 名称查找
from chatgpt:
在C++中,查找(lookup)是指在代码中引用标识符(如函数名、变量名、类型名等)时,编译器确定标识符的声明或定义的过程。C++中的查找可以分为限定查找(qualified lookup)和非限定查找(unqualified lookup)两种。
-
限定查找(Qualified Lookup):
**限定查找是指在标识符前加上作用域限定符(如命名空间、类名、对象名等),以明确指定标识符所在的作用域。**编译器只在指定的作用域中查找标识符的声明或定义。例如:namespace A { int x; void foo(); } int main() { A::foo(); // 限定查找 return 0; }
在上述代码中,
A::foo()
使用了限定查找,它明确指定了函数foo()
在命名空间A
中定义。编译器只会在命名空间A
中查找foo()
的定义。 -
非限定查找(Unqualified Lookup):
非限定查找是指在标识符前没有作用域限定符,编译器根据标识符的上下文和查找规则进行查找。 编译器将按照以下顺序进行查找:
- 当前块作用域
- 外层块作用域
- 全局作用域
- 命名空间作用域
- 标准库作用域
- 其他全局命名空间作用域
例如:
#include <iostream>
int x;
void foo() {
std::cout << x << std::endl; // 非限定查找
}
int main() {
int x = 5;
foo(); // 非限定查找
return 0;
}
在上述代码中,foo()
函数中的x
是一个非限定查找,编译器首先在当前块作用域中查找x
,找到了函数内部的局部变量x
,输出结果为5
。如果在函数内部没有找到对应的变量x
,编译器将会继续往外层块作用域、全局作用域等进行查找。
限定查找和非限定查找在C++中是用来控制标识符的可见性和作用域解析的重要机制。通过合理地使用限定查找和非限定查找,可以有效避免标识符冲突和提高代码的可读性和可维护性。
– 限定查找( qualified lookup )与非限定查找( unqualified lookup )
– 非限定查找会进行域的逐级查找 名称隐藏( —— hiding )
– 查找通常只会在已声明的名称集合中进行
– 实参依赖查找( Argument Dependent Lookup: ADL )
● 只对自定义类型生效
D、重载解析:在名称查找的基础上进一步选择合适的调用函数
– 过滤不能被调用的版本 (non-viable candidates)
● 参数个数不对
● 无法将实参转换为形参
● 实参不满足形参的限制条件
– 在剩余版本中查找与调用表达式最匹配的版本,匹配级别越低越好(有特殊规则)
● 级别 1 :完美匹配 或 平凡转换(比如加一个 const )
● 级别 2 : promotion(提升) 或 promotion 加平凡转换
● 级别 3 :标准转换 或 标准转换加平凡转换
● 级别 4* :自定义转换 或 自定义转换 加平凡转换 或 自定义转换加标准转换
● 级别 5* :形参为省略号的版本
● 函数包含多个形参时,所选函数的所有形参的匹配级别都要优于或等于其它函数
4. 函数相关的其它内容
A、递归函数:在函数体中调用其自身的函数
– 通常用于描述复杂的迭代过程:示例
一定要留有出口,小心无限循环!
B、几种特殊函数:
内联函数(inline):把相对简单的函数在调用的地方展开
可以保证在多个翻译单元(.cpp)中引入同一个头文件函数,从程序级别的一处定义原则->翻译单元级别的一处定义原则,一次定义即可
constexpr 函数 (C++11 起 ):11和17标准不一样。需要满足既可以在编译期执行,也可以在运行期执行
consteval 函数 (C++20 起 ):只能在编译期求值,产生常量结果 ,能够提升效果,避免误调用
C、 函数指针
– 函数类型与函数指针类型:
函数类型:返回值类型 + 参数类型
int (*a)[3]; //a是指针
int *a [3]; //a是数组
函数是不能赋值的,数组也不能赋值,会 自动转化为指针类型。
– 函数指针与重载
– 将函数指针作为高阶函数参数
– 将函数指针作为高阶函数返回值
– 小心: Most vexing parse
D、补充:
chatgpt讲解的函数类型与函数指针类型:
在C++中,函数类型和函数指针类型是两个相关但不完全相同的概念。下面我将为您解释这两者之间的区别和使用方法。
-
函数类型(Function Type):
函数类型指的是函数的签名,包括参数类型和返回值类型。函数类型可以用于声明函数、定义函数指针以及作为函数的参数或返回值类型。函数类型的语法格式如下:返回值类型 (*函数名)(参数类型1, 参数类型2, ...);
例如,下面是一个函数类型为
int(int, int)
的示例:int add(int a, int b);
这里的
add
是一个函数,它接受两个int
类型的参数并返回一个int
类型的值。可以通过声明函数指针来使用函数类型。 -
函数指针类型(Function Pointer Type):
函数指针类型是指向函数的指针,它可以存储函数的地址并允许通过指针调用该函数。函数指针的声明方式与函数类型相似,但需要使用*
来表示指针类型。函数指针类型的语法格式如下:返回值类型 (*指针变量名)(参数类型1, 参数类型2, ...);
例如,下面是一个函数指针类型为
int(*)(int, int)
的示例:int (*ptr)(int, int);
这里的
ptr
是一个函数指针,它指向一个接受两个int
类型参数并返回一个int
类型值的函数。可以通过将函数的地址赋给函数指针来使用它。
下面是一个示例,演示了如何声明函数类型、定义函数指针并使用函数指针调用函数:
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
// 声明函数类型
using AddFunction = int(int, int);
// 定义函数指针
AddFunction *ptr = add;
// 使用函数指针调用函数
int result = ptr(3, 4);
std::cout << "Result: " << result << std::endl;
return 0;
}
在上述示例中,我们首先使用 using
关键字声明了一个函数类型 AddFunction
,它与函数 add
的签名相匹配。然后,我们定义了一个函数指针 ptr
,并将 add
函数的地址赋给它。最后,我们通过函数指针 ptr
调用了函数 add
,并将结果打印出来。
总结:
函数类型用于描述函数的签名,可以用于声明函数、定义函数指针以及作为函数的参数或返回值类型。函数指针类型是指向函数的指针,用于存储函数的地址并允许通过指针调用函数。函数