网站页面 原则,如何防止网站挂马,域名空间做网站,个人网站可以做企业网站吗文章目录 一、函数基础1.基本函数定义2.函数的声明与定义3.函数调用 二、函数详解1.参数2.函数体3.返回类型 三、函数重载与重载解析1.函数重载2.重载解析 四、函数相关的其他内容1.递归函数2.内联函数3.constexpr函数#xff08;C11起#xff09;4.consteval 函数 (C20 起 )… 文章目录 一、函数基础1.基本函数定义2.函数的声明与定义3.函数调用 二、函数详解1.参数2.函数体3.返回类型 三、函数重载与重载解析1.函数重载2.重载解析 四、函数相关的其他内容1.递归函数2.内联函数3.constexpr函数C11起4.consteval 函数 (C20 起 )5.函数指针  一、函数基础 函数封装了一段代码可以在一次执行过程中被反复调用。函数包括函数头函数定义与函数体。引入函数实现了逻辑封装与逻辑复用。 
1.基本函数定义 基本的函数定义包括函数名称、形式参数、返回类型 函数头  函数名称-标识符用于后续的调用  C标识符的一些主要要求 字母数字组成标识符只能由字母大小写区分、数字和下划线组成。不以数字开头标识符不能以数字开头必须以字母或下划线开始。大小写敏感C是大小写敏感的语言因此identifier和Identifier是两个不同的标识符。保留字不能使用C的保留字作为标识符。例如不能使用if、for、int等关键字作为函数名或变量名。长度限制标识符的长度可能受到编译器和系统的限制但至少前31个字符在C标准中是有定义的。标准符合性标识符应当遵循C标准避免使用与系统或库函数同名的标识符以防止潜在的冲突。可读性尽管C允许使用下划线以外的特殊字符如$或作为标识符但通常建议避免这样做以保持代码的可读性和跨平台兼容性。命名约定C社区有一些非强制性的命名约定例如 驼峰命名法第一个单词的首字母小写后续单词的首字母大写如myFunction。下划线命名法所有单词都小写并用下划线分隔如my_function。匈牙利命名法    形式参数-代表函数可以接受的输入值它定义了参数的类型和名称。参数列表可以为空即函数不接受任何输入。  返回类型-函数执行完成后所返回的结果类型   函数体 为一个语句块 block 包含了具体的计算逻辑  
returnType functionName(parameterType1 param1, parameterType2 param2) {// 函数体return returnValue;
}2.函数的声明与定义 函数的声明与定义 函数声明只包含函数头不包含函数体通常置于头文件中用于声明在其他文件中定义的函数。这允许在编译时建立函数的接口而不必暴露其实现细节。 头文件通常使用预处理指令#ifndef、#define和#endif来避免重复包含。 // 在头文件 myMathFunctions.h 中的函数声明
#ifndef MYMATHFUNCTIONS_H
#define MYMATHFUNCTIONS_Hint add(int a, int b);
double multiply(double a, double b);#endif // MYMATHFUNCTIONS_H函数声明可出现多次但函数定义通常只能出现一次一次定义原则存在例外内联函数可以在多个翻译单元中出现多次每个翻译单元出现一次。它通常放在源文件.cpp文件中。 // 在源文件 myMathFunctions.cpp 中的函数定义
#include myMathFunctions.hint add(int a, int b) {return a  b;
}double multiply(double a, double b) {return a * b;
}在主程序main.cpp中可以通过包含头文件来使用这些函数 #include iostream
#include myMathFunctions.hint main() {std::cout  5  3    add(5, 3)  std::endl;std::cout  5.5 * 2    multiply(5.5, 2)  std::endl;return 0;
}3.函数调用 函数调用的基本语法: 
returnType functionName(arguments);returnType是函数的返回类型functionName是函数的名称arguments是传递给函数的实际参数列表 函数调用需要提供函数名与实际参数  实际参数拷贝初始化形式参数  实际参数和形式参数 实际参数Actual parameters在函数调用中提供的参数它们是传递给函数的值或变量。形式参数Formal parameters在函数定义中声明的参数用于接收传递给函数的实际参数。   返回值会被拷贝给函数的调用者  栈帧结构 在C中函数调用是通过使用调用栈call stack来实现的。每当一个函数被调用时一个新的栈帧stack frame会被创建并推入调用栈。栈帧是存储在调用栈上的一块内存它包含了关于函数调用的所有信息包括   参数传递给函数的参数值。局部变量函数内部定义的变量。返回地址函数执行完毕后返回到调用它的代码位置的地址。寄存器状态函数调用可能会改变一些寄存器的值这些值需要在函数返回之前保存并恢复。函数的返回值如果函数有返回值返回值也会存储在栈帧中。  C函数调用时栈帧示例 #include iostreamvoid functionA(int a, int b) {int localVar  a  b;std::cout  In functionA:   localVar  std::endl;// 当前栈帧包含参数a, b, localVar, 以及返回地址
}int main() {int arg1  5;int arg2  10;functionA(arg1, arg2);return 0;
}当main函数调用functionA时以下步骤发生 arg1和arg2的值被推送到调用栈上作为functionA的参数。为functionA创建一个新的栈帧并将其推入调用栈。在functionA的栈帧中局部变量localVar被创建并初始化。functionA执行并打印localVar的值。一旦functionA执行完毕它的栈帧被弹出调用栈控制权返回到main函数并恢复main函数的状态。main函数继续执行直到程序结束。  栈帧的生命周期 栈帧的生命周期与它所属的函数调用相同。当函数调用开始时栈帧被创建当函数返回时栈帧被销毁。栈帧的创建和销毁是自动进行的由编译器和底层硬件共同管理。   函数的外部链接   C支持两种链接类型内部链接internal linkage和外部链接external linkage。默认情况下函数具有外部链接。   具有外部链接的函数可以在程序的不同编译单元中被定义和使用。这意味着如果一个函数在一个源文件中定义它可以在其他源文件中被调用即使这些源文件是分开编译的。 // file1.cpp
int add(int a, int b) {return a  b;
}// file2.cpp
extern int add(int a, int b); // 声明函数告诉编译器是一个外部链接的函数它的定义在程序的其他地方。
int main() {int result  add(1, 2); // 调用函数return 0;
}在这个例子中add函数在file1.cpp中定义并在file2.cpp中通过extern关键字声明。extern关键字告诉编译器该函数的定义在程序的其他部分。  
二、函数详解 
1.参数 函数可以在函数头的小括号中包含零到多个形参  包含零个形参时可以使用 void 标记 void func(void)
{}对于非模板函数来说其每个形参都有确定的类型但形参可以没有名称类型用于编译器检查  形参名称的变化并不会引入函数的不同版本函数的名称相同且形参的类型相同即重复定义  实参到形参的拷贝求值顺序不定 函数调用时使用实参拷贝初始化形参但多个形参时拷贝初始化执行顺序不确定在不同编译器上不确定因此下面代码的写法就非常危险 #include iostreamvoid fun(int x, int y)
{std::cout  y;
}int main()
{int x  0;fun(x, x);
}若实参为临时对象C17 强制省略复制临时对象   
函数传值、传址、传引用 传值 传值是将参数的值复制到函数内部的局部变量中。这种方式下函数内部对参数值的修改不会影响原始变量。 void incrementByValue(int n) {n  1; // 修改的是局部变量的副本不影响传入的原始变量
}int main() {int a  10;incrementByValue(a);// a 仍然是 10return 0;
}传址 传址是将参数的内存地址直接传递给函数。这种方式允许函数直接修改原始变量的值。 void incrementByAddress(int* p) {*p  1; // 通过指针修改原始变量的值
}int main() {int a  10;incrementByAddress(a);// a 现在是 11return 0;
}传引用 传引用是通过引用传递参数即传递一个对象的别名。这种方式也允许函数修改原始变量的值并且比传址更安全因为它避免了指针的使用。 void incrementByReference(int r) {r  1; // 直接修改原始变量的值
}int main() {int a  10;incrementByReference(a);// a 现在是 11return 0;
}选择哪种方式 当你需要函数内部修改原始变量时使用传址或传引用。当你希望函数接收一个值并且不关心原始变量时使用传值。当你传递大型对象或数组时为了避免复制开销通常使用传址或传引用。使用传引用可以提高代码的可读性并减少对指针的错误使用。 函数传参拷贝初始化过程中的类型退化 数组类型退化 当你将一个数组作为参数传递给函数时数组类型退化为指针类型。这意味着你失去了数组的原始大小信息。 一维数组  多维数组  为了避免数组类型退化传递数组的引用确保数组的尺寸信息传递给函数   函数类型退化 当函数类型用作参数或返回类型时它们退化为指针类型 void callFunction(void (*func)()) {// func 是一个指向无参数无返回值函数的指针
}void a()
{}int main()
{callFunction(a);
}对于函数类型退化通常不需要特别处理。  
变长参数允许函数接收任意数量的参数 initializer_list要求变长参数类型一致包含两个指针begin与end迭代器可查看https://zh.cppreference.com/w/cpp/utility/initializer_list #include iostream
#include initializer_listvoid fun(std::initializer_listint par)
{}int main()
{fun({1, 2, 3, 4, 5});
}可变长度模板参数 变长参数类型可以不一致在模板章节具体讲解  使用省略号表示形式参数 不建议在C中使用省略号表示形式参数  
函数可以定义缺省实参 在C中函数的缺省默认实参允许你在函数声明或定义时为某些参数提供默认值。这意味着在调用函数时如果没有为这些参数提供值编译器会自动使用这些默认值。推荐的做法是在函数定义中指定默认参数这样只会有一个函数版本避免了链接错误。 如果某个形参具有缺省实参那么它右侧的形参都必须具有缺省实参 #include iostream
#include initializer_list//编译会出错y也必须有缺省实参
void fun(int x  0, int y)
{}int main()
{fun(1,2);
}在一个翻译单元中每个形参的缺省实参只能定义一次  具有缺省实参的函数调用时传入的实参会按照从左到右的顺序匹配形参  缺省实参为对象时实参的缺省值会随对象值的变化而变化 不建议这么干 #include iostreamint x  3;
void fun(int y  x)
{std::cout  y; //4
}int main()
{x  4;fun();  //fun(x)
}main 函数的两个版本 
在C中main函数是程序的入口点它可以有两种形式 无形参版本 int main() {// 程序代码return 0;
}main函数只返回一个整数通常返回0表示程序成功执行。  C标准规定main函数必须返回一个整数值如果返回0表示程序成功结束否则表示程序异常结束。   带两个形参的版本 这种形式的main函数接受两个参数argc和argv。这两个参数提供了对命令行参数的访问。 int main(int argc, char* argv[]) {// 程序代码return 0;
}argcargument count是一个整数表示命令行中传递给程序的参数个数包括程序名称。argvargument vector是一个指向字符串数组的指针包含了命令行参数。第一个元素argv[0]是程序的名称随后的元素argv[1]、argv[2]等是传递给程序的其他参数。argv数组中的最后一个元素之后有一个以nullptr在C11及以后版本中或NULL在旧版C中结尾的指针表示参数列表的结束。 示例 #include iostreamint main(int argc, char* argv[]) {if (argc ! 3){std::cerr  arg invalid!return -1;}std::cout  Number of command line arguments:   argc  std::endl;for (int i  0; i  argc; i) {std::cout  Argument   i  :   argv[i]  std::endl;}return 0;
}运行结果 Number of command line arguments: 1
Argument 0: ./output.s2.函数体 函数体定义了函数的行为即当函数被调用时所执行的一系列操作。函数体内部可以包含变量声明、控制流语句如if、for、while等、函数调用、表达式等。 
函数体形成域 
其中包含了自动对象内部声明的对象以及形参对象也可包含局部静态对象 
函数体执行完成时的返回 隐式返回不推荐的做法–执行到函数的右大括号自动跳出函数回到函数的调用者  在C中如果一个函数的返回类型不是 void那么在函数体执行完成时如果没有显式地使用 return 语句返回一个值编译器会根据函数的返回类型进行隐式返回。对于非 void 返回类型的函数如果没有 return 语句或者 return 语句没有返回值函数的执行流程结束时编译器会执行以下操作 如果函数的返回类型是内置类型如 int, float, double, char 等编译器会生成一个未定义的值undefined value作为返回值。这是因为C标准规定未初始化的局部变量的值是未定义的。如果函数的返回类型是类类型编译器会构造一个临时对象作为返回值。这个临时对象的值同样是未定义的因为对象的构造函数没有被显式调用。对于数组类型编译器无法隐式返回一个数组因为数组不能被拷贝。如果函数声明为返回数组那么这本身就是一个语法错误。对于指针类型编译器会生成一个未定义的指针值作为返回值。对于引用类型编译器会生成一个未定义的引用目标。 需要注意的是使用隐式返回是不推荐的做法因为它会导致不可预测的行为可能会引发难以发现的错误。最佳实践是在所有非 void 类型的函数中都使用显式的 return 语句来返回一个明确的值。这样可以提高代码的可读性和可维护性并避免潜在的错误。 隐式返回示例 #include iostreamint implicitReturn() {// 函数体为空没有显式的 return 语句
}int main() {int result  implicitReturn();// result 的值是未定义的std::cout  result; //4198762//return 0;		//main()函数是一个特殊的函数可以使用隐式返回相当于返回0表明执行成功
}main()函数是一个特殊的函数可以使用隐式返回相当于返回0表明执行成功。其他所有函数要想使用隐式返回返回类型要求void。   显式返回关键字 return  return; 语句–返回类型必须是void void fun()
{//...语句return;
}return 表达式 ; — 用于从函数返回一个值给调用者的语句 示例 int add(int a, int b) {return a  b; // 返回两数之和
}int main() {int sum  add(5, 10);// sum 的值现在是 15return 0;
}return 表达式的重要点 返回值类型return 表达式的类型必须与函数的返回类型兼容。如果函数返回一个非 void 类型return 表达式必须产生一个与返回类型匹配的值。构造函数在构造函数中return 表达式是可选的并且通常不使用。如果需要返回一个对象通常是通过构造函数的初始化列表来完成的。析构函数析构函数不能返回任何值因为它们的返回类型是 void。表达式求值return 表达式会被求值其结果会被返回。如果表达式是复杂的它可能包含变量、函数调用、算术运算等。控制流return 语句可以作为控制流的一部分用于在满足特定条件时提前退出函数。返回局部对象返回一个局部对象的引用或指针是不安全的因为局部对象的生命周期在函数返回后就结束了。返回动态分配的内存返回动态分配的内存如指针需要确保内存在适当的时候被释放否则会导致内存泄漏。   return 初始化列表 ; 在C中返回初始化列表允许你直接在函数返回值时初始化对象初始化列表是自动对象在fun()函数结束后自动对象就会被销毁。这种语法特别适用于那些需要返回复杂类型如类对象或结构体的函数。 返回初始化列表的基本语法如下 ReturnType FunctionName(Parameters) {return {arg1, arg2, ...};
}示例 struct Point {int x, y;Point(int x, int y) : x(x), y(y) {}
};Point createPoint(int x, int y) {return {x, y};  // 使用返回初始化列表
}注意 返回初始化列表不能用于虚函数因为虚函数的返回类型是多态的。如果函数的返回类型是引用类型不能使用返回初始化列表。返回初始化列表在某些情况下可以提高效率因为它可以直接在栈上构造对象避免了不必要的拷贝。    小心返回自动对象的引用或指针 在C中返回一个局部对象的引用或指针通常称为自动对象因为它们的生命周期仅限于函数的执行过程是一个常见的错误因为它会导致悬挂引用dangling reference或无效指针invalid pointer问题。尝试通过悬挂引用或无效指针访问这些内存区域将导致未定义行为。 悬挂引用指的是一个引用指向一个已经销毁的对象无效指针指的是一个指针指向一个已经释放的内存区域 返回局部对象的引用或指针是一个常见的陷阱应该通过返回对象的拷贝、使用智能指针、引用参数、静态局部变量等方法来避免。  返回值优化 RVO —— C17 对返回临时对象的强制优化    返回值优化Return Value Optimization简称RVO是一种编译器优化技术用于改善含有返回对象的函数的性能。在C中当一个函数返回一个对象时编译器通常会在内存中创建一个临时对象然后将这个临时对象复制或移动到调用者的内存空间。这个过程可能会因为不必要的复制操作而降低程序的效率。 返回值优化的工作原理 RVO的目的是避免这种不必要的复制它允许编译器在函数返回时直接在调用者的内存空间中构造返回的对象。这样就不需要创建一个临时对象从而提高了程序的性能。   
3.返回类型 返回类型表示了函数计算结果的类型可以为 void 在C中函数的返回类型确实表示了函数计算结果的类型。返回类型可以是基本数据类型、类类型、结构体类型、枚举类型、指针类型、引用类型等或者是void。当函数的返回类型是void时它表示该函数不返回任何值。  返回类型的几种书写方式  经典方法函数返回类型位于函数头的头部 ReturnType FunctionName(ParameterList) {// 函数体
}C11 引入的方式位于函数头的后部 C11标准引入了尾随返回类型允许将返回类型放在函数头的后部用-符号指明 auto FunctionName(ParameterList) - ReturnType {// 函数体
}这种写法在模板编程和Lambda表达式中特别有用因为它允许编译器根据函数体中的返回语句来推导返回类型。  C14 引入的方式返回类型的自动推导 C14进一步简化了返回类型的书写引入了返回类型自动推导。使用auto关键字编译器会根据函数体中的代码自动推导返回类型 auto FunctionName(ParameterList) {// 函数体
}这在定义计算并返回特定类型的值的函数时非常有用因为程序员不需要显式声明返回类型。  C17使用 constexpr if构造具有不同返回类型的函数   C17引入了constexpr if这允许在编译时根据条件选择不同的执行路径。虽然constexpr if本身并不直接影响函数的返回类型但它可以用于创建在不同条件下返回不同类型值的函数。这通常与返回类型自动推导结合使用 //条件condition为常量表达式
auto FunctionName(bool condition, int a, double b) {if constexpr (condition) {// 当condition为true时返回整数类型return a;} else {// 当condition为false时返回浮点数类型return b;}
}函数返回类型与结构化绑定 C 17  在C17中引入了结构化绑定Structured Bindings特性它允许从具有多个成员的复合数据类型中提取多个变量简化了对结构体、类、对pair、元组等类型的访问。然而结构化绑定本身并不直接影响函数的返回类型只是提供了一种使用返回值的方式它主要用于简化对这些类型的使用。 结构化绑定的基本用法   结构化绑定允许你从一对括号中指定多个变量名这些变量名对应于结构体或元组中的成员 struct Point {int x, y;
};auto [x, y]  Point{1, 2};函数返回类型与结构化绑定   当你使用结构化绑定时通常是为了简化对复合类型中数据的访问而不是改变函数的返回类型。不过你可以设计一个函数使其返回一个结构体或元组然后使用结构化绑定来提取返回的值。 #include utilitystd::pairint, int getPair() {return {10, 20};
}int main() {auto [first, second]  getPair();// 使用first和second变量return 0;
}函数返回类型自动推导与结构化绑定   结合使用C14的返回类型自动推导和结构化绑定可以创建简洁且易于使用的函数 auto getPoint() {return Point{3, 4};
}int main() {auto [x, y]  getPoint();// 使用x和y变量return 0;
}[[nodiscard]] 属性 C 17  在C17中标准库引入了一个新的属性 [[nodiscard]]它用于标记函数以指示函数的返回值不应该被忽略。这个属性是编译器的一个提示它告诉程序员调用这些函数时应该使用返回值。 [[nodiscard]] 通常用于以下几种情况 资源获取即初始化当一个函数返回一个资源比如打开文件返回的文件句柄或者创建动态内存返回的指针程序员应该使用这个返回值否则可能会造成资源泄露。错误报告某些函数用于错误报告比如返回错误码或异常对象这些返回值不应该被忽略因为它们包含了重要的状态信息。重要的计算结果当一个函数进行了重要的计算并且这个结果对于程序的逻辑至关重要时可以使用 [[nodiscard]] 来确保程序员不会忘记使用这个结果。 示例 #include iostream
#include memory[[nodiscard]] std::unique_ptrint createResource() {std::cout  Resource created.\n;return std::make_uniqueint(42);
}int main() {auto resource  createResource(); // OK: 使用了返回值// auto ignoredResource  createResource(); // Warning: 忽略了返回值return 0;
}注意 [[nodiscard]] 是一个属性不是函数的一部分。它不改变函数的签名只是对编译器的一个额外说明。忽略带有 [[nodiscard]] 属性的函数的返回值将导致编译器警告但不是错误。这允许库的作者提示用户注意返回值而不强制他们在所有情况下都必须使用返回值。[[nodiscard]] 可以用于任何返回非void类型的函数包括构造函数、析构函数、以及普通成员函数。[[nodiscard]] 可以与模板函数一起使用以确保在模板实例化时也考虑返回值。某些编译器可能提供了类似的编译器特定属性如 [[gnu::warn_unused_result]]但在C17中[[nodiscard]] 是标准属性被所有遵循标准的编译器支持。通过使用 [[nodiscard]]开发者可以提高代码的安全性和健壮性确保重要的返回值不会被无意中忽略。  
三、函数重载与重载解析 
1.函数重载 函数重载Function Overloading是C语言的一个特性它允许在同一个作用域内定义多个具有相同名称但参数列表不同的函数。函数重载使得同一个函数名可以用不同的参数类型或数量来调用增加了语言的灵活性。 
函数重载的规则与要点 
参数列表不同重载的函数必须在参数的类型、数量或两者方面有所不同。返回类型不参与重载函数的返回类型不能作为重载的依据。仅根据返回类型不同而参数列表相同的函数编译器会报错。模板函数模板函数的实例化可以产生看似重载的效果但实际上它们是通过模板参数来实现不同的行为。const修饰符对于成员函数是否在参数后添加const关键字也会影响重载解析因为const成员函数和非const成员函数被视为不同的函数。函数签名函数签名包括函数名和参数列表但不包括函数返回类型。 
2.重载解析 在C中当对一个函数进行调用时编译器需要确定应该执行哪个函数版本这一过程称为重载解析Overload Resolution。 名称查找Name Lookup–重载解析第一步  限定查找 qualified lookup 与非限定查找 unqualified lookup   限定查找是指在查找名称时明确指定了名称所在的命名空间或类作用域。这种查找方式通常使用作用域解析运算符::来实现。限定查找可以避免名称冲突并确保引用的是特定命名空间或类中的实体。 namespace MyNamespace {int myVar  10;
}int main() {int myVar  20;// 使用限定查找访问MyNamespace中的myVarint value  MyNamespace::myVar;return 0;
}非限定查找是指在查找名称时不指定名称所在的命名空间或类作用域。这种查找方式在没有明确指定作用域的情况下进行编译器会根据当前的作用域和using声明来确定标识符的实体。 namespace MyNamespace {int myVar  10;
}int main() {int myVar  20;// 使用非限定查找访问myVarint value  myVar; // 这里的myVar指的是main函数中的局部变量而不是MyNamespace中的myVarreturn 0;
}在这个例子中由于没有使用作用域解析运算符编译器默认查找的是当前作用域中的myVar。   非限定查找会进行域的逐级查找–名称隐藏 hiding  当前作用域查找首先在当前作用域例如一个函数或一个类的成员函数内部开始。如果在当前作用域中找到了标识符查找结束。局部作用域如果当前作用域中没有找到标识符查找会扩展到包含当前作用域的局部作用域。命名空间如果查找还没有结束编译器会检查当前作用域所在的命名空间。如果在命名空间中找到了标识符查找结束。全局作用域如果以上步骤都没有找到标识符查找会扩展到全局作用域。 名称隐藏是指在更内层的作用域中定义的标识符会隐藏同名的外层作用域中的标识符。这意味着如果内层作用域和外层作用域中存在同名的标识符内层作用域中的标识符会优先被访问。 int globalVar  10; // 全局变量void function() {int globalVar  20; // 局部变量隐藏了全局变量// 在这个作用域内globalVar 指的是局部变量 20
}int main() {int globalVar  30; // main 函数的局部变量隐藏了全局变量function(); // 在 function 内部globalVar 是 20// 在 main 函数内部globalVar 是 30return 0;
}globalVar 在不同的局部作用域中被定义每个局部定义都隐藏了外层作用域中的同名标识符。  查找通常只会在已声明的名称集合中进行  实参依赖查找 Argument Dependent Lookup: ADL  当编译器在解析函数调用时如果函数名没有被限定即没有使用作用域解析运算符::编译器会执行以下查找步骤  非限定查找首先在当前作用域包括局部作用域、命名空间、类作用域等中查找函数名。  实参依赖查找如果非限定查找没有找到匹配的函数编译器会根据函数参数的类型在参数所属的命名空间和类作用域中进行查找。 应用场景重载运算符当定义了类的重载运算符如operator时即使没有使用类的作用域编译器也能通过ADL找到正确的运算符函数。 class MyClass {
public:MyClass operator(const MyClass other) const {// 实现两个MyClass对象的加法}
};void someFunction(const MyClass obj) {MyClass result  obj  MyClass(); // ADL 允许编译器找到 MyClass::operator()
}重载解析在名称查找的基础上进一步选择合适的调用函数 函数重载解析的一般步骤与规则  过滤不能被调用的版本 (non-viable candidates) 在重载解析的开始阶段编译器会排除那些明显不能匹配调用的函数版本。这些版本可能由于以下原因被认为是不可行的  参数个数不对 调用时提供的参数数量与任何重载版本的参数数量都不匹配  无法将实参转换为形参 提供的实参类型无法通过任何方式转换为对应的形参类型  实参不满足形参的限制条件 例如形参有特定的类型限定如const限定实参不满足这些限定。   在剩余版本中查找与调用表达式最匹配的版本匹配级别越低越好 一旦过滤掉不可行的候选版本编译器会尝试在剩余的候选版本中找到与调用表达式最匹配的函数版本。匹配的级别分为几个等级从最好到最差  级别1 完美匹配Perfect Match或平凡转换Trivial Conversion例如添加或移除const或volatile限定或者数组或函数指针到它们的指针类型。  级别2 提升Promotion如从int到long或提升加平凡转换。  级别3 标准转换Standard Conversion如从派生类指针到基类指针或者从float到double或标准转换加平凡转换。  级别4 自定义转换Conversion Function如使用构造函数或转换操作符或自定义转换加平凡转换或自定义转换加标准转换。  级别5 形参为省略号Ellipsis即变参函数如int foo(...)。  匹配级别规则 当函数包含多个形参时编译器将选择所有形参的匹配级别都优于或等于其他函数的函数版本。如果所有候选函数的匹配级别都相同那么 如果存在完美匹配则优先选择完美匹配的版本如果没有完美匹配但有多个平凡匹配选择第一个声明的版本如果没有完美匹配或平凡匹配编译器将尝试找到匹配级别最低的版本  编译器会根据以下步骤和规则来选择正确的函数版本 函数匹配过程 候选函数集合编译器首先生成一个候选函数集合包含所有名称匹配的函数。参数匹配编译器尝试将传递给函数的实参与候选函数的形参进行匹配。标准转换编译器允许一些标准的类型转换如整数到浮点数的转换、数组到指针的转换、类对象到其基类指针的转换等。构造函数调用如果实参不直接匹配任何形参编译器会考虑使用构造函数创建临时对象以匹配相应的形参。模板匹配如果候选函数中包含模板函数编译器会尝试实例化模板以找到匹配的函数。引用绑定对于通过引用传递的参数编译器会尝试绑定到实参的引用。const和volatile修饰符最佳匹配的函数应该符合const和volatile的约束。  函数选择过程 最佳匹配编译器尝试找到与实参最匹配的函数即需要最少的用户定义转换的函数。引用相加性如果一个函数的参数可以通过添加或移除const或volatile来匹配而另一个函数不需要这样的转换那么后者会被优先选择。转换成本如果存在多个函数都可以匹配编译器会根据转换的成本来选择最佳匹配成本较低的转换会被优先选择。函数的const正确性如果函数的参数是const或volatile修饰的调用时传递的对象也应该是const或volatile的或者通过const_cast进行显式转换。SFINAESubstitution Failure Is Not An Error在模板函数的情况下如果实例化失败编译器会忽略该模板函数。  编译器警告与错误 歧义调用如果编译器无法确定唯一的最佳匹配它会产生歧义调用错误。未使用的返回值如果函数返回一个非void类型并且调用它的返回值被忽略编译器会发出警告除非函数被标记为[[nodiscard]]。函数签名不匹配如果实参与所有候选函数的形参都不匹配编译器会报错。  
示例 
#include iostream
#include stringvoid print(int a) {std::cout  int:   a  std::endl;
}void print(double a) {std::cout  double:   a  std::endl;
}void print(const std::string a) {std::cout  string:   a  std::endl;
}int main() {print(10);        // 匹配第一个版本print(3.14);      // 匹配第二个版本print(hello);   // 匹配第三个版本使用string类的构造函数进行匹配return 0;
}四、函数相关的其他内容 
1.递归函数 递归函数在函数体中调用其自身的函数。基本思想是它允许通过将问题分解为更小的子问题来解决复杂问题。在C中递归函数的使用非常普遍尤其是在处理如树结构遍历、排序算法、图搜索等场景时。 
递归函数的基本构成 一个递归函数通常包含两个主要部分 
基本情况Base Case这是递归终止的条件防止无限递归。在每个递归调用的最底层函数将达到一个不再进行递归调用的状态。递归情况Recursive Case这是函数调用自身的情况它逐渐将问题分解成更小的问题直到达到基本情况。 
使用递归函数的步骤 
定义基本情况确定函数何时不再递归调用自身而是返回一个直接的答案。确保递归有进展确保每次递归调用都向基本情况靠近一步以避免无限递归。考虑性能递归可能会带来额外的内存开销因为每次递归调用都需要存储在调用栈上并且有时可以通过迭代方法更高效地实现相同的结果。 
示例计算阶乘 
#include iostream// 递归函数计算阶乘
unsigned long factorial(unsigned int n) {// 基本情况如果n为0或1阶乘为1if (n  0 || n  1) {return 1;}// 递归情况n!  n * (n-1)!else {return n * factorial(n - 1);}
}int main() {unsigned int number  5;std::cout  Factorial of   number   is   factorial(number)  std::endl;return 0;
}递归函数的注意事项 
避免重复计算在某些情况下递归可能导致重复计算例如在没有记忆化的情况下多次计算相同的子问题。这可以通过记忆化技术也称为缓存或动态规划来解决。栈溢出风险如果递归太深可能会耗尽程序的调用栈导致栈溢出错误。效率问题递归可能比迭代解决方案更慢因为它涉及更多的函数调用和返回操作。 
2.内联函数 在C中内联函数inline function是一种特殊的函数它可以在编译时被插入到每个调用该函数的地方而不是通过常规的函数调用机制执行。使用内联函数的目的是为了减少函数调用的开销尤其是在函数体较小且调用频繁的情况下。 
内联函数的定义 内联函数通常使用inline关键字定义。当你在一个类定义中或者在函数声明的同时提供函数体时可以使用inline关键字。 
类定义中 
class MyClass {
public:inline void myFunction() {// 函数体}
};在函数声明的同时提供定义 
inline int add(int a, int b) {return a  b;
}内联函数的工作原理 当编译器处理内联函数时它会尝试将函数的代码直接插入到每个调用点从而避免了生成函数调用的机器代码。这可以减少函数调用的开销包括参数传递、栈帧的创建和销毁等。 
内联函数的使用场景 内联函数最适合于小型、频繁调用的函数 
访问器和修改器函数这些函数通常只包含一行或几行代码用于获取或设置对象的成员变量。小型工具函数一些小型的工具函数如简单的数学运算或类型转换函数可能从内联中受益。 
内联函数的注意事项 
编译器优化inline关键字只是一个请求编译器可以选择忽略它。编译器会根据自己的优化策略和函数的复杂性来决定是否将函数内联展开。多文件定义如果一个内联函数在多个编译单元通常是多个不同的.cpp文件中定义可能会导致链接错误。为了解决这个问题通常将内联函数的定义放在头文件中并在头文件中使用inline关键字使函数从程序级别的一次定义原则变成翻译单元级别的一次定义原则。过度使用过度使用内联可能会导致代码膨胀从而增加缓存失效的可能性反而降低程序的性能。 
3.constexpr函数C11起 C11标准引入了constexpr函数的概念它允许在编译时计算并确定函数的返回值。constexpr函数通常用于定义那些不会修改程序状态且所有操作都是已知的编译时表达式的函数。一般的函数是在运行期进行求值constexpr函数是在编译期进行求值因此函数体中所有表达式不能有只能在运行期进行的语句。 
constexpr函数的基本用法 constexpr关键字用于声明一个函数使其可以在编译时求值。 
constexpr int add(int a, int b) {return a  b;
}add函数被声明为constexpr这意味着它只能包含一个返回语句并且所有的操作都应该是编译时可知的。 
constexpr函数的限制 
简单函数体constexpr函数只能有一个语句通常是返回语句。编译时求值函数内的所有表达式都应该是可以在编译时求值的。不修改程序状态constexpr函数不能有副作用如修改全局变量或进行I/O操作。 
constexpr函数的用途 
定义常量表达式constexpr函数常用于定义数学常量或物理常量等。模板元编程在模板编程中constexpr函数可以用于在编译时计算模板参数。优化性能由于constexpr函数可以在编译时求值它可以减少运行时计算从而提高程序性能。 
constexpr与字面类型 从C14开始constexpr还可以用于声明变量编译期常量并且如果一个变量被声明为constexpr那么它的类型必须是一个字面类型literal type。字面类型的数据成员也必须是字面型这意味着它们的值可以在编译时确定。 
constexpr int value  add(3, 4); // 在C14及以后value的值在编译时确定constexpr与lambda表达式 C14标准允许使用constexpr lambda表达式这允许创建更复杂的编译时计算。 
auto constexpr_constant  [](int a, int b) - int {return a  b;
};
constexpr int result  constexpr_constant(1, 2); // 在C14及以后result的值在编译时确定4.consteval 函数 (C20 起 ) consteval是C20引入的一个特性它允许声明可以在编译时执行的复杂函数。consteval函数是constexpr函数的一个扩展它允许更复杂的编译时计算包括但不限于 
非内联的函数体。使用循环和分支语句。调用其他consteval函数。 
consteval 函数的基本用法 
consteval int expensive_computation() {// 执行一些复杂的计算int result  0;for (int i  0; i  1000000; i) {result  i;}return result;
}constexpr int const_value  expensive_computation();expensive_computation函数被声明为consteval这意味着它可以包含循环和分支语句并且可以在编译时执行。然后我们可以在constexpr上下文中使用这个函数来初始化一个常量。 
consteval 函数的限制 尽管consteval函数比constexpr函数更加灵活但它们仍然有一些限制 
编译时求值consteval函数的所有操作都必须在编译时完成不能有任何运行时的行为。不能抛出异常consteval函数不能包含任何可能抛出异常的代码。不能修改程序状态consteval函数不能有副作用如修改全局变量或进行I/O操作。 
consteval函数的用途 
执行复杂的编译时计算consteval函数可以执行复杂的计算包括循环和分支这些在constexpr函数中是不允许的。生成编译时常量consteval函数可以用于生成更复杂的编译时常量这些常量可以在程序的其他地方使用。模板元编程在模板元编程中consteval函数可以用于在编译时执行更复杂的逻辑。 
consteval与constexpr的区别 虽然consteval函数是constexpr函数的扩展但它们之间有一些关键的区别 在C20中constexpr函数既能在编译期执行又能在运行期执行。而consteval函数只能在编译期执行。  函数体的复杂性constexpr函数只能有一个语句通常是返回语句而consteval函数可以包含更复杂的函数体包括循环和分支语句。  调用限制constexpr函数只能在constexpr上下文中调用而consteval函数既可以在constexpr上下文也可以在非constexpr上下文中调用。  内联函数、consteval与constexpr函数都是翻译单元一次定义原则。根据不同的需求是要展开还是要编译期运行选择不同的关键字。 5.函数指针 在C中函数指针是一种指向函数的指针它允许你将函数作为参数传递给其他函数或者将函数赋值给指针变量。 在C中函数指针的应用范围比较小。因为在C中有很多方式可以作为函数指针的代替品这些代替品功能更加强大使用起来更加安全。如Lambda表达式 函数指针的声明 声明函数指针时你需要指定函数的返回类型、参数类型以及指针的名称。 
int (*functionPtr)(int, int);这个声明表示functionPtr是一个指向接受两个int参数并返回int的函数的指针。 
函数指针的初始化 可以使用函数名来初始化函数指针。 
int add(int a, int b) {return a  b;
}int main() {int (*functionPtr)(int, int)  add; // 将函数的地址赋给指针//另一种写法using FuncPtr  int (*)(int, int);FuncPtr functionPtr  add;return 0;
}使用函数指针 一旦函数指针被初始化你可以像调用普通函数一样调用它所指向的函数又或者解引用函数指针再调用 
int result  functionPtr(3, 5); // 调用add函数int result  (*functionPtr)(3, 5); // 调用add函数函数指针与重载 当你尝试使用函数指针指向一个重载函数时问题出现了由于编译器需要根据参数类型来解析重载函数所以你必须指定函数指针指向确切的函数签名。 
这意味着如果你有两个重载的函数你不能直接声明一个指向这些重载函数的通用函数指针。你必须为每个不同的函数签名声明一个不同的函数指针类型。 
void print(int i) {std::cout  Integer:   i  std::endl;
}void print(double d) {std::cout  Double:   d  std::endl;
}void (*ptr1)(int)  print;  // OK: 指向print(int)的函数指针
// void (*ptr2)(double)  print;  // Error: print是模糊的因为print是重载的void (*ptr2)(double)  (void (*)(double))print;  // OK: 明确指出是指向print(double)的函数指针ptr1正确地指向了print(int)但是直接将print赋值给ptr2会引发错误因为编译器不知道指向哪个print函数。通过强制类型转换(void (*)(double))你明确指出了函数指针应该指向的函数签名。 
解决方案函数指针到函数对象 在C中解决函数重载和指针问题的一个常用方法是使用函数对象或仿函数functors。这在标准模板库STL中非常常见例如std::function。 
#include functional
#include iostreamvoid print(int i) {std::cout  Integer:   i  std::endl;
}void print(double d) {std::cout  Double:   d  std::endl;
}int main() {std::functionvoid(int) func1  print;  // 绑定到print(int)std::functionvoid(double) func2  printdouble;  // 模板参数指定重载版本func1(10);    // 调用print(int)func2(20.5);  // 调用print(double)return 0;
}通过使用std::function你可以存储指向不同重载函数的指针并通过调用func1和func2来解决重载问题。 
函数指针作为函数参数 函数指针可以作为参数传递给其他函数这在实现回调函数时非常有用 
void doWork(int (*operation)(int, int), int a, int b) {int result  operation(a, b);// 使用result做一些操作
}int main() {doWork(add, 3, 5); // 传递add函数的指针作为参数return 0;
}将函数指针作为返回值 
#include iostreamint inc(int x)
{return x  1;
}int dec(int x)
{return x - 1;
}auto fun(bool input)
{if(input)return inc;elsereturn dec;
}int main()
{std::cout  (*fun(true))(100); //101
}函数的指针和数组 函数指针可以用于创建函数数组这在实现多态或者分派机制时很有用 
void (*actions[])(int)  {doWork1, doWork2, doWork3};int main() {actions[0](10); // 调用doWork1函数return 0;
}函数指针和const限定符 函数指针可以与const限定符一起使用以表明指针指向的函数不会修改它所操作的对象的状态 
void (*const constFunctionPtr)(int)  doWork1;函数指针与C标准库 函数指针在C标准库中也有广泛应用例如在sort算法中使用比较函数 
void sort(int arr[], int n,bool (*compare)(int, int)) {for (int i  0; i  n - 1; i) {for (int j  0; j  n - i - 1; j) {if (compare(arr[j], arr[j  1])) {// 交换 arr[j] 和 arr[j  1]}}}
}bool compare(int a, int b) {return a  b; // 降序排序
}int main() {int arr[]  {1, 2, 3, 4, 5};int n  sizeof(arr) / sizeof(arr[0]);sort(arr, n, compare);return 0;
}注意小心Most vexing parse https://en.wikipedia.org/wiki/Most_vexing_parse