%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # 缓冲区笔记 函数签名、函数原型、函数类型、函数指针 %% # `main` 函数 `main` 函数是 C/C++程序的 **==标准入口点==**。 C++标准中规定的**两种 "合法且可移植"** 的 `main` 函数原型如下 [^1]: - `int main();` - `int main(int argc, char** argv);` 或 `int main(int argc, char* argv[]);` > [!NOTE] > 当需要**结束一个 C++程序**时,除了**从 `main()` 返回**以外,**其它时候应当调用 `exit()`、`quick_exit()` (since C++11) 或者 `terminate()` 来结束程序**。 > [!NOTE] Linux 中支持另一种扩展形式,可接收 `exec` 系列函数传入的 "**环境变量列表**" > > Linux 支持的扩展(非 C++标准):`int main(int argc, char** argv, char** envp);` > ###### 形式一 ```cpp int main() { // 程序代码 return 0; // 该语句可省略, 若省略则编译器会自动添加. (C中不能省略) } ``` 不接收任何参数,返回一个 `int` 值,表示**程序的退出状态**(给到操作系统): - 正常退出时应**返回 `0`,表示成功**; - **非零值** 通常表示**有错误发生**,不同的非零值表示不同类型的错误。 ###### 形式二 ```cpp int main(int argc, char** argv) { //或 int main(int argc, char* argv[]); // argc 表示命令行的"参数数量" // argv 是一个指向"字符指针数组"的指针, 每个元素指向一个"参数字符串" // argv[0] 通常是程序的名称或路径, 或者为一个空字符 // argv[1] 到 argv[argc-1] 是程序实际接收的命令行参数. } ``` 可接受**命令行参数**的 `main` 函数形式:具有**两个形参**。 <br><br> # 函数参数 ## 函数参数传递 **"实参" 是 "形参" 的初始值**。 函数调用时,根据**形参是否为"引用"类型**,分为两种参数传递方式[^2](P187): - **值传递**(passed by value):形参为**值类型**,**形参为==对实参的值进行拷贝/移动构造==后得到的==副本**==; - 也称 "**传值调用**"(called by value) - **引用传递**(passed by reference):形参为**引用类型**,形参**作为实参的引用/别名**。 - 也称 "**传引用调用**"(called by reference) > [!caution] 实参初始化形参时,**实参的顶层 const 会被忽略**。 [^2] (P191) > > - 值传递下,**形参为实参的拷贝副本,形参独立于实参,因此实参的顶层 const 属性不会影响形参**。 > - 引用传递下,没有顶层 "const" 的说法,**引用只有底层 const,且不可被忽略**。 > > ```cpp > // 下列两个函数会被认为 "重复定义" > void fcn(const int i) {} > // void fcn(int i) {} // error: 重复定义 > > // `fcn(const int i)`与`fnc(int i)`二者都是既能接受常量, 也能接受非常量. > // 假如编译器允许同时定义这两个函数版本, 则调用时会产生二义性冲突. > // 因此, 编译器不允许同时定义上面两个版本 > ``` > > <br> ## 默认实参 在函数参数列表中,可以为形参指定默认值,但要求**该形参右侧所有形参也都具有默认值**——即**带有默认值的形参必须全部位于参数列表的后面**。 > [!NOTE] 通常,应当**在==函数声明==中指定默认实参**,并将该声明放在合适的头文件中。 > > **给定作用域中的一个函数形参==只能被赋予一次默认实参==**,即**函数的后续声明**只能为**之前那些没有默认值的形参**添加默认实参,并且**该形参右侧的所有形参必须都已经具有默认值**。 ```cpp string MyFunc(int, int, char=' '); // 首次函数声明. // string MyFunc(int, int, char='*'); // error:不能覆盖/修改一个已存在的默认值 string MyFunc(int, int = 80, char); // 允许添加新的默认实参, 只要该默认实参右侧的所有形参都已具有默认值. ``` <br> ## 可变形参 Variadic Parameters "可变形参"(Varadic Parameters)支持函数接受 **==不定数量的参数==**。 C++提供了以下几种支持**可变形参**的机制: - **C 风格的可变参数列表** - 以**初始化列表** `std::initializer_list` 作为形参,支持同一类型的实参; - **可变参数模版**:支持不同类型的实参; 此外,如果要处理 #### C 风格的可变参数列表 C 风格的可变参数列表**使用 `<cstdarg>` 头文件中定义的宏和函数**,如 `va_start`、`va_arg` 和 `va_end`,来处理可变数量的参数。这种方式继承自 C 语言。 在这一方式下,**函数形参**使用 "**省略符形参**`...`",该形参只能出现在**形参列表的最后一个位置**。 该方式**只应该用于 C 和 C++通用的类型**,因为大多数类型的对象在传递给省略符形参时可能都无法正确拷贝。 ```cpp title:in_c_style.cpp #include <cstdarg> #include <iostream> using namespace std; // 使用`<cstdarg>`库提供的宏和函数处理可变参数. 这一方式继承自C语言. void printNumbers(int count, ...) { va_list args; va_start(args, count); // 初始化args列表;count是最后一个固定参数, 表示参数数量. for (int i = 0; i < count; ++i) { int num = va_arg(args, int); // cout << num << " "; } va_end(args); // 清理工作 } int main() { printNumbers(5, 91, 92, 93, 94, 95); // 需要通过第一个参数显式指定参数数量 } ``` #### 使用 `std::initializer_list` ```cpp title:use_std_initializer_lsit.cpp #include <iostream> #include <initializer_list> using namespace std; // 使用`std::initializer_list`处理同一类型的可变数量参数 void printNumbers(initializer_list<int> args) { // 形参为`std::initializer_list`类模版的实例 for (auto& num : args) { cout << num << " "; } } int main() { // 实参通过`{}`花括号初始化列表的形式传递给`std::initializer_list`类型的形参 printNumbers({91, 92, 93, 94, 95}); // 不需要显式指定参数数量 } ``` #### 可变参数模版 ```cpp /** 通过"可变参数模版'来处理任意数量和类型的参数. * * 处理思路: * 定义两个函数模版, 一个处理单个参数, 另一个处理多个参数. * 当接收到多个参数时, 递归调用处理单个参数的函数模版实例. */ template <typename T> void print(const T& value) { cout << value << endl; } template <typename T, typename... Args> // 可变模版参数 void print(const T& firstArg, const Args&... args) { cout << firstArg << endl; print(args...); // 递归调用 } ``` <br><br><br> # 函数返回类型 函数的返回类型**不能是"数组类型"或"函数类型"**,可以**是==指向数组或函数的指针==**,也可以返回 `lambda` 函数。 对于返回类型为 `void` 的函数,可以省略 `return;` 语句,编译器会自动隐式执行。 <br> ## 函数返回值 **函数返回值**将用于初始化一个 "**==临时对象==**",其即为**函数调用表达式**的结果。(如同**初始化一个变量或函数形参**) 如果返回的是**引用类型**(非局部变量的引用),则 **"函数调用表达式"的值类型是==左值==**,**否则为==右值**==。 > [!caution] 不要返回==**局部变量的引用或指针**==。 > > 如果尝试返回 **局部变量的"引用"** 或 **指向局部变量的指针**,则函数返回后,**引用或指针将指向无效的内存区域**。 > > 这**与返回一个"==值=="不同**,**返回一个值时会发生拷贝**。 > [!NOTE] 函数可返回一个 "花括号列表",语义是对返回值执行 "**列表初始化**"(since C++11) > > 当花括号为空时,意味着执行 "**值初始化**"。 <br><br> ## 尾置返回类型 尾置返回类型语法:**将==函数的返回类型==放在参数列表之后**,而**使用 `auto` 关键字作为函数返回类型的占位符** [^2] (P206)。 尾置返回类型语法:`auto func(params) -> ret-type;` ,这一语法形式 **允许返回类型依赖于==函数参数类型==**。 > [!example] 使用示例 > > ```cpp > // 尾置返回类型(C++11) > auto func(int i) -> int(*)[10]; // 函数func的返回类型为一个指向含有10个整数的数组的指针. > ``` > > [!faq] 为什么需要尾置返回类型 > > C++11 中引入尾置返回类型的主要目的在于: > > (1)解决**泛型编程中的==返回类型依赖==**: > > **编译器在==解析到函数的参数列表之==前,其中的==参数对编译器而言都是不可见==的**。 > 如果 **==返回类型本身依赖于函数参数的类型==** (例如 **需要由函数参数类型进行 `decltype ()` 推断**) ,则**必须使用尾置返回类型** 。 > > (2)为 **==lambda 表达式==指定返回类型** > > lambda 表达式的返回类型只能通过 "尾置返回类型" 语法给出。 > #### 解决返回类型依赖 使用场景:**函数返回类型依赖于==函数参数类型==** 的场景 示例一: ```cpp template<typename T1, typename T2> auto add(T1 x, T2 y) -> decltype(x + y) { return x + y; } ``` 示例二: 用以获取 STL 容器中**关联于元素类型的"迭代器类型"**,或是获取 "**迭代器中的元素类型**" ```cpp template <typename T> auto MyBegin(vector<T>& vec) -> decltype(vec.begin()) { return vec.begin(); } template <typename It> auto fcn(It beg, It end) -> decltype(*beg) { ... return *beg; // 返回序列中一个元素的引用 } ``` #### 用于 lambda 表达式 lambda 表达式的返回类型只能通过尾置返回类型的语法给出。 ```cpp auto lambda = [](int x, int y) -> int { return x + y; }; ``` <br><br><br> # 函数原型/函数声明 > "**函数声明**" 也即 "**函数原型**" (function prototype) 函数声明指定了**函数的接口**——**函数名称**、**返回类型**以及**参数类型列表**。 函数声明为编译器提供了**关于如何调用该函数的必要信息**,但不包括函数的具体实现(即函数体)。 通过函数声明,**编译器能够在函数实际定义之前就进行函数调用的类型检查,确保调用者提供正确类型和数量的参数**。 <br> # 函数类型 **函数类型**由函数的 **==返回类型==** 和 **==参数类型列表==(忽略顶层 const)** 确定。 **函数名称**、**函数形参的==顶层 const==**、**类成员函数的限定符** 不包括在函数类型中。 例如 `int(int, int)` 是一个函数类型,接受两个 int 参数,返回 int 类型。 ```cpp void func(int a); // 函数1 void func(const int a); // 函数2, 与1具有相同的函数类型,顶层const不影响函数类型 void func(int* a); // 函数3 void func(int* const a); // 函数4, 与3具有相同的函数类型,顶层const不影响函数类型 void func(const int* a); // 函数5, 与3和4具有不同的函数类型,底层const影响函数类型 ``` **==可调用对象==都有其自己的类型**: - 每个 lambda 有其**唯一的、未命名的类型**。 - 函数及函数指针的类型则由其**返回类型**和**参数类型列表**决定 <br><br> # 函数签名 > 参见说明:[Is the return type part of the function signature?](https://stackoverflow.com/questions/290038/is-the-return-type-part-of-the-function-signature) **函数签名**(Function Signature)是用于 **==唯一标识函数==的一组特征**。 **函数签名** 包括下列三项,**不包括==返回类型==**。 - **==函数名称==** - **==参数类型列表==**:包括**参数类型**、**顺序**,不包括参数名称。 - 形参的 =="**顶层 `const`" 将被忽略**==,**不影响函数签名**。 - 形参的 ==**"底层 const" 属于函数签名的一部分**==。 - **==模版参数==**(仅对于**模版函数**) 在**函数重载解析**中,编译器主要通过 "**函数签名**" 来区分确定重载版本。 ```c++ // 示例: void foo(int) {} // signature: foo(int) void foo(double) {} // signature: foo(double) void foo(int, double) {} // signature: foo(int, double) class MyClass { void bar() const {} // signature for member function: bar() const void bar() {} // signature for member function: bar() }; ``` > [!caution] **函数形参**中的**顶层 `const` 不影响函数签名** > > 仅仅 **在形参中添加或移除==顶层 const== 不足以区分两个函数**,将被视为**具有相同的函数签名**而报错 "==**重复定义**=="。 > > (1)仅 "**==顶层 const==**" 不同的两个函数将被视为**具有相同的函数签名** > > ```cpp > // 下列两个函数被视为具有相同的函数签名. > void MyFunc(int); > void MyFunc(const int); // 重复定义, 而非重载!形参中的顶层`const` 将被忽略。 > > // 下列两个函数被视为具有相同的函数签名. > void MyFunc(int*); > void MyFunc(int* const); // 重复定义, 而非重载!形参中的顶层`const` 将被忽略。 > ``` > > (2)"**==底层 const==**" 的有无会**影响函数签名**,会被视为**不同的重载版本**。 > > ```cpp > // 下列两个函数将被视为具有"不同"的函数签名, 因为受"底层 const"影响. > void MyFunc(int&); > void MyFunc(const int&); > > // 下列两个函数将被视为具有"不同"的函数签名, 因为受"底层 const"影响. > void MyFunc(int*) > void MyFunc(const int*); > ``` > <br> %% ### 函数模版签名 函数模版签名(The signature of a function template)包含: - 函数签名 - 返回类型 - 模版参数列表(template parameter list) > 关于函数模版签名的说明: > > ![image-20231014222543482|550](_attachment/02-开发笔记/01-cpp/函数相关/cpp-函数相关.assets/IMG-cpp-函数相关-FFA77B62BF29CD9A30B33B9749C63E64.png) ### 模版函数签名 模版函数签名(The signature of a function template specialization)包含: - 函数模版签名 - 实际的模版参数(actual template arguments) > 参考:For reference, here is what the most recent C++0x draft n3000 says about "signature" in `1.3.11`, which is much more complete in its coverage of the different type of entities: > > the **name** and the **parameter type list** (8.3.5) of a function, as well as the **class or namespace** of which it is a member. If a function or function template is a class member its signature additionally includes the **cv-qualifiers** (if any) and the **ref-qualifier** (if any) on the function or function template itself. > > The signature of a **function template** additionally includes its **return type** and its **template parameter list**. > > The signature of a **function template specialization** includes the **signature of the template** of which it is a specialization and its **template arguments** (whether explicitly specified or deduced). [ Note: Signatures are used as a basis for name mangling and linking. — end note ] > A function template by itself is not a type, or a function, or any other entity. No code is generated from a source file that contains only template definitions. In order for any code to appear, a template must be instantiated. %% <br><br> # 函数指针 参见 [[02-开发笔记/01-cpp/类型相关/cpp-指针相关#函数指针|cpp-指针相关-函数指针]] <br><br> # 函数重载 > 函数重载是 C++ 提供的一种多态形式。 **==同一作用域==内**,**函数名相同**,但 **==形参列表不同==** 的多个函数,称之为 "**重载函数**"。 编译器将根据函数调用时提供的参数类型、数量以及顺序来选择**最匹配的函数**执行,这过程称之为 "**函数重载**"。 **函数重载版本**受两个因素影响: 1. **==函数签名==** 2. **类成员函数**的 **==函数限定符==**(`cv` 限定符、引用限定符) - **`cv` 限定符**(**cv-qualifiers**, `const`,`volatile`,`const valatile`) - **引用限定符**(**ref-qualifier**,`&` 与 `&&`) 具有 **==相同函数签名==** 但 **==限定符不同==的类成员函数**,将被**视为不同的==重载版本==**。 > [!caution] 函数重载只针对 "**==同一作用域==内**" > > **不同作用域下(命名空间 Or 类作用域)的函数不构成重载**(即使函数签名完全相同)。 > > 例如,**==内层作用域==里定义或声明的==名称== (变量名或函数名)将"==隐藏=="外层作用域中的同名函数**。 > > ```cpp > void print (const string&); > void print (double); > > int main () { > void print (int); // 在该局部作用域内, `print` 隐藏了外部名称, 因此不会再构成重载. > } > ``` > > > [!caution] 仅返回类型不同**不构成函数重载** > **返回类型**不是函数签名的一部分。 > C++中**不允许两个函数仅仅只有返回类型不同**,而其他部分完全相同,会导致编译错误。 <br><br> ## 函数重载解析(Overload Resolution) ### 函数重载的决策 当存在多个重载版本时,编译器通过以下规则决定调用哪个函数 [^2] (P128): - **精确匹配**:编译器首先寻找**参数类型完全匹配的重载版本**。 - **提升和转换**:如果没有精确匹配,编译器会尝试**通过提升和标准类型转换**来找到匹配的重载。 - **最佳匹配**:如果有多个函数都可以通过提升和转换来匹配,编译器将尝试确定哪个重载提供了“**最佳匹配**”。如果无法确定最佳匹配,会导致编译错误。 - "精确匹配" 优于 "类型转换" 的匹配, ##### 确定最佳匹配 ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-函数相关.assets/IMG-cpp-函数相关-CFB4912D1CE637495B1AAB4554C6B71A.png|672]] ### 函数重载的结果 重载解析时可能有三种结果: - 编译器找到一个**与实参==最佳匹配==(best match)的函数**,并**生成调用该函数的代码**; - 找不到**任何一个函数与调用的实参匹配**,编译器报出 **==无匹配==(no match)错误信息**; - **有多于一个函数可以匹配**,此时报 **==二义性调用错误==**(ambiguous call) <br><br> # 内联函数与 constexpr 函数 参见 [[02-开发笔记/01-cpp/cpp 基本概念/cpp-inline 说明符|cpp-inline 说明符]] 、 [[02-开发笔记/01-cpp/cpp 基本概念/cpp-constexpr 说明符|cpp-constexpr 说明符]] 、 <br><br> # `noexcept` 说明符 `noexcept` 用以**标识该函数 "一定不会抛出异常"**[^3] [^4] ,**必须同时在 "函数声明" 和 "函数定义" 处出现**。 > [!NOTE] `noexcept` 的位置:**位于尾置返回类型之前**,**成员函数的 `const`、引用限定符之后**。 > [!caution] 编译器并不在编译时检查 `noexcept` > > 标识了该说明符的函数,**其内部仍可==含有 `throw` 语句==或==调用可能抛出异常的函数==,将可编译通过**。 > > > [!caution] 若**一个标识为 `noexcept` 的函数在运行时==实际了抛出异常==**,则程序将**直接调用 `std::terminate()` ==终止程序==**。 > [!NOTE] 若函数指针声明为 `noexcept`,则其只能指向 `noexcept` 函数 > > ```cpp > void (*pf1)(int) noexcept; // noexcept函数指针, 只能指向noexcept函数. > ``` > > [!NOTE] 若一个基类的==**虚函数**==声明为 `noexcept`,则派生类中重写时也必须声明为 `noexcept` ### 使用说明 `noexcept` 说明符可用于两种场景: - **明确函数不会抛出异常** - **明确表示无法处理该函数异常**(因此抛出后,将直接导致**程序调用 `std::terminate()` 终止进程**) `noexcept` 可接收一个 "**bool 类型**" 的 **==常量表达式==** 实参,其**值为 `true` 时表示函数不会抛出异常**。 使用示例: ```cpp // 表示f()与g()的异常声明一致. 当`g()`不抛出异常时, `f()`的`except`声明为true, 否则为`false` void f() noexcept(noexcept(g())); // 第一个`noexcept`是函数说明符, 其中的`noexcept`是返回"bool右值常量表达式"的运算符 ``` ### `noexcept` 运算符 存在一个**同名的 `noexcept` 运算符**,其返回值是一个 **`bool` 类型的==右值常量表达式==**(编译期求值),表示**给定的表达式是否会抛出异常**。 `noexcept(expr)` 返回 `true` 的情况:`expr` 中**调用的所有函数都做了 `noexcept` 声明**,且 **`expr` 本身不含有 `throw` 语句**时。 ```cpp int main() { cout << noexcept(safeFunction()) << endl; // true cout << noexcept(unsafeFunction()) << endl; // false cout << noexcept(1 + 2) << endl; // true, 纯算术运算, 不会抛出异常. cout << noexcept(sizeof(int)) << endl; // true, 编译期操作, 不会抛出异常 cout << noexcept(new int) << endl; // false, new可能抛出std::bad_alloc异常. std::vector<int> v = {1, 2, 3}; std::cout << noexcept(v.at(5)) << "\n"; // false,std::vector::at 可能抛出 std::out_of_range } ``` <br><br><br> # 谓词 (Predicate) 谓词 (Predicate) 指代**返回布尔值**的、检查 **单参数或双参数** 特定属性的**函数对象**。 谓词用于表示某种属性或条件的成立,常用 STL 中的算法,例如排序、搜索、替换、删除等。 - ==**一元谓词**==(Unary Predicate):只接受单一参数,检查**单一实参**的某项特性。例如判断某个数是否是素数 `isPrime()` 的函数。 - ==**二元谓词**==(Binary Predicate):接受两个参数,检查、比较**两个实参**的特定属性或条件。 > [!NOTE] 谓词只是对符合某种形式的函数的称谓。 说明示例: ```cpp // 一元谓词 // 下列普通函数或lambda函数对象都可用作一元谓词. 例如在find_if()中使用 bool isPositive(int num) { return num > 0; } auto isPositive = [](int num) { return num > 0;} // 二元谓词 // 下列普通函数或lambda函数对象都可用作二元谓词. 例如在sort()中使用. bool ascending(int a, int b){ return a < b; } auto ascending = [](int a, int b) { return a < b; }; ``` 使用示例: ```c++ // 查找在x和y之间的首个元素 vector<int> coll = {1, 3, 19, 5, 13, 7, 11, 2, 17}; int x = 5, y = 12; auto pos = find_if(coll.begin(), coll.end(), [=](int i) { return i > x && i < y; }); // 等价的基本实现 vector<int>::iterator pos; for (pos = coll.begin(); pos != coll.end(); ++pos) { if (*pos > x && *pos < y) break; } ``` <br><br> # 预定义标识符 `__func__` `__func__` 是**编译器在==每个==函数内==预定义==的一个==局部变量==**,其值为**该函数的"函数名**"。 与宏的工作方式不同,宏在**预处理阶段**展开,`__func__` 的值 **==在编译时==** 确定。 ```cpp void exampleFunction() { std::cout << "This function is: " << __func__ << std::endl; } ``` %% # 其它 ## 函数返回类型为容器时,返回空容器的方式 - 直接返回默认构造的对象: `return std::vector<int>();` - 利用花括号初始化与隐式转换: `retrun {};` ![image-20231014001715216](_attachment/02-开发笔记/01-cpp/函数相关/cpp-函数相关.assets/IMG-cpp-函数相关-8430B37F96261190B1F78389BBF537CF.png) %% <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《C++ Primer》P197 [^2]: 《C++ Primer》 [^3]: 《Effective Modern C++》Item 14 [^4]: 《C++ Primer》P692