文章目录
一、函数基础
函数:封装了一段代码,可以在一次执行过程中被反复调用。函数包括函数头(函数定义)与函数体。引入函数实现了逻辑封装与逻辑复用。
1.基本函数定义
-
函数头
-
函数名称-标识符,用于后续的调用
-
形式参数-代表函数可以接受的输入值,它定义了参数的类型和名称。参数列表可以为空,即函数不接受任何输入。
-
返回类型-函数执行完成后所返回的结果类型
-
-
函数体
- 为一个语句块( block ),包含了具体的计算逻辑
returnType functionName(parameterType1 param1, parameterType2 param2) {
// 函数体
return returnValue;
}
2.函数的声明与定义
函数的声明与定义:
-
函数声明只包含函数头,不包含函数体,通常中,用于声明在其他文件中定义的函数。这允许在编译时建立函数的接口,而不必暴露其实现细节。
头文件通常使用预处理指令
#ifndef
、#define
和#endif
来避免重复包含。// 在头文件 myMathFunctions.h 中的函数声明 #ifndef MYMATHFUNCTIONS_H #define MYMATHFUNCTIONS_H int add(int a, int b); double multiply(double a, double b); #endif // MYMATHFUNCTIONS_H
-
(一次定义原则,存在例外(内联函数可以在多个翻译单元中出现多次,每个翻译单元出现一次))。它通常放在源文件(
.cpp
文件)中。// 在源文件 myMathFunctions.cpp 中的函数定义 #include "myMathFunctions.h" int add(int a, int b) { return a + b; } double multiply(double a, double b) { return a * b; }
在主程序
main.cpp
中,可以通过包含头文件来使用这些函数:#include <iostream> #include "myMathFunctions.h" int 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
是传递给函数的实际参数列表
-
函数调用需要提供函数名与实际参数
-
返回值会被给函数的调用者
-
在C++中,函数调用是通过使用调用栈(call stack)来实现的。每当一个函数被调用时,一个新的栈帧(stack frame)会被创建并推入调用栈。栈帧是存储在调用栈上的一块内存,它包含了关于函数调用的所有信息,包括:
C++函数调用时栈帧示例:
#include <iostream> void 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 <iostream> void fun(int x, int y) { std::cout << y; } int main() { int x = 0; fun(x++, x++); }
-
若实参为临时对象,C++17 强制省略复制
-
函数传值、传址、传引用:
-
传值
传值是将参数的值复制到函数内部的局部变量中。这种方式下,函数内部对参数值的修改不会影响原始变量。
void incrementByValue(int n) { n += 1; // 修改的是局部变量的副本,不影响传入的原始变量 } int main() { int a = 10; incrementByValue(a); // a 仍然是 10 return 0; }
-
传址
传址是将参数的内存地址直接传递给函数。这种方式允许函数直接修改原始变量的值。
void incrementByAddress(int* p) { *p += 1; // 通过指针修改原始变量的值 } int main() { int a = 10; incrementByAddress(&a); // a 现在是 11 return 0; }
-
传引用
传引用是通过引用传递参数,即传递一个对象的别名。这种方式也允许函数修改原始变量的值,并且比传址更安全,因为它避免了指针的使用。
void incrementByReference(int& r) { r += 1; // 直接修改原始变量的值 } int main() { int a = 10; incrementByReference(a); // a 现在是 11 return 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_list> void fun(std::initializer_list<int> 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 <iostream> int 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
表示程序成功执行。 -
带两个形参的版本
这种形式的
main
函数接受两个参数:argc
和argv
。这两个参数提供了对命令行参数的访问。int main(int argc, char* argv[]) { // 程序代码 return 0; }
argc
(argument count)是一个整数,表示命令行中传递给程序的参数个数(包括程序名称)。argv
(argument vector)是一个指向字符串数组的指针,包含了命令行参数。第一个元素argv[0]
是程序的名称,随后的元素argv[1]
、argv[2]
等是传递给程序的其他参数。argv
数组中的最后一个元素之后有一个以nullptr
(在C++11及以后版本中)或NULL
(在旧版C++中)结尾的指针,表示参数列表的结束。
示例:
#include <iostream> int 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.s
2.函数体
函数体定义了函数的行为,即当函数被调用时所执行的一系列操作。函数体内部可以包含变量声明、控制流语句(如if
、for
、while
等)、函数调用、表达式等。
函数体形成域:
- 其中包含了自动对象(内部声明的对象以及形参对象)
- 也可包含局部静态对象
函数体执行完成时的返回:
-
隐式返回(不推荐的做法)–执行到函数的右大括号自动跳出函数回到函数的调用者
-
显式返回关键字: return
-
return;
语句–返回类型必须是void
void fun() { //...语句 return; }
-
return 表达式 ; — 用于从函数返回一个值给调用者的语句
示例:
int add(int a, int b) { return a + b; // 返回两数之和 } int main() { int sum = add(5, 10); // sum 的值现在是 15 return 0; }
-
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
)—— C++17 对返回临时对象的强制优化
3.返回类型
-
返回类型表示了函数计算结果的类型,可以为 void
在C++中,函数的返回类型确实表示了函数计算结果的类型。返回类型可以是基本数据类型、类类型、结构体类型、枚举类型、指针类型、引用类型等,或者是
void
。当函数的返回类型是void
时,它表示该函数不返回任何值。 -
返回类型的几种书写方式
-
经典方法:函数返回类型位于函数头的头部
ReturnType FunctionName(ParameterList) { // 函数体 }
-
C++11 引入的方式:位于函数头的后部
C++11标准引入了尾随返回类型,允许将返回类型放在函数头的后部,用
->
符号指明:auto FunctionName(ParameterList) -> ReturnType { // 函数体 }
这种写法在模板编程和Lambda表达式中特别有用,因为它允许编译器根据函数体中的返回语句来推导返回类型。
-
C++14 引入的方式:返回类型的自动推导
C++14进一步简化了返回类型的书写,引入了返回类型自动推导。使用
auto
关键字,编译器会根据函数体中的代码自动推导返回类型:auto FunctionName(ParameterList) { // 函数体 }
这在定义计算并返回特定类型的值的函数时非常有用,因为程序员不需要显式声明返回类型。
-
C++17使用
constexpr if
构造具有不同返回类型的函数C++17引入了
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 )
在C++17中,引入了(Structured Bindings)特性,它允许从具有多个成员的复合数据类型中提取多个变量,简化了对结构体、类、对(pair)、元组等类型的访问。然而,,它主要用于简化对这些类型的使用。
结构化绑定的基本用法:
结构化绑定允许你从一对括号中指定多个变量名,这些变量名对应于结构体或元组中的成员:
struct Point { int x, y; }; auto [x, y] = Point{1, 2};
函数返回类型与结构化绑定:
当你使用结构化绑定时,通常是为了简化对复合类型中数据的访问,而不是改变函数的返回类型。不过,你可以设计一个函数,使其返回一个结构体或元组,然后使用结构化绑定来提取返回的值。
#include <utility> std::pair<int, int> getPair() { return {10, 20}; } int main() { auto [first, second] = getPair(); // 使用first和second变量 return 0; }
函数返回类型自动推导与结构化绑定:
结合使用C++14的返回类型自动推导和结构化绑定,可以创建简洁且易于使用的函数:
auto getPoint() { return Point{3, 4}; } int main() { auto [x, y] = getPoint(); // 使用x和y变量 return 0; }
-
[[nodiscard]]
属性( C++ 17 )在C++17中,标准库引入了一个新的属性
[[nodiscard]]
,它用于标记函数,以指示函数的返回值不应该被忽略。这个属性是编译器的一个提示,它告诉程序员调用这些函数时应该使用返回值。[[nodiscard]]
通常用于以下几种情况:- 资源获取即初始化:当一个函数返回一个资源,比如打开文件返回的文件句柄,或者创建动态内存返回的指针,程序员应该使用这个返回值,否则可能会造成资源泄露。
- 错误报告:某些函数用于错误报告,比如返回错误码或异常对象,这些返回值不应该被忽略,因为它们包含了重要的状态信息。
- 重要的计算结果:当一个函数进行了重要的计算,并且这个结果对于程序的逻辑至关重要时,可以使用
[[nodiscard]]
来确保程序员不会忘记使用这个结果。
示例:
#include <iostream> #include <memory> [[nodiscard]] std::unique_ptr<int> createResource() { std::cout << "Resource created.\n"; return std::make_unique<int>(42); } int main() { auto resource = createResource(); // OK: 使用了返回值 // auto ignoredResource = createResource(); // Warning: 忽略了返回值 return 0; }
注意:
[[nodiscard]]
是一个属性,不是函数的一部分。它不改变函数的签名,只是对编译器的一个额外说明。- 忽略带有
[[nodiscard]]
属性的函数的返回值将导致编译器警告,但不是错误。这允许库的作者提示用户注意返回值,而不强制他们在所有情况下都必须使用返回值。 [[nodiscard]]
可以用于任何返回非void类型的函数,包括构造函数、析构函数、以及普通成员函数。[[nodiscard]]
可以与模板函数一起使用,以确保在模板实例化时也考虑返回值。- 某些编译器可能提供了类似的编译器特定属性,如
[[gnu::warn_unused_result]]
,但在C++17中,[[nodiscard]]
是标准属性,被所有遵循标准的编译器支持。 - 通过使用
[[nodiscard]]
,开发者可以提高代码的安全性和健壮性,确保重要的返回值不会被无意中忽略。
三、函数重载与重载解析
1.函数重载
函数重载(Function Overloading)是C++语言的一个特性,它。函数重载使得同一个函数名可以用不同的参数类型或数量来调用,增加了语言的灵活性。
函数重载的规则与要点:
- 参数列表不同:重载的函数必须在参数的类型、数量或两者方面有所不同。
- 返回类型不参与重载:函数的返回类型不能作为重载的依据。仅根据返回类型不同而参数列表相同的函数,编译器会报错。
- 模板函数:模板函数的实例化可以产生看似重载的效果,但实际上它们是通过模板参数来实现不同的行为。
- const修饰符:对于成员函数,是否在参数后添加
const
关键字也会影响重载解析,因为const
成员函数和非const
成员函数被视为不同的函数。 - 函数签名:函数签名包括函数名和参数列表,但不包括函数返回类型。
2.重载解析
在C++中,当对一个函数进行调用时,(Overload Resolution)。
编译器会根据以下步骤和规则来选择正确的函数版本:
-
函数匹配过程
- 候选函数集合:编译器首先生成一个候选函数集合,包含所有名称匹配的函数。
- 参数匹配:编译器尝试将传递给函数的实参与候选函数的形参进行匹配。
- 标准转换:编译器允许一些标准的类型转换,如整数到浮点数的转换、数组到指针的转换、类对象到其基类指针的转换等。
- 构造函数调用:如果实参不直接匹配任何形参,编译器会考虑使用构造函数创建临时对象,以匹配相应的形参。
- 模板匹配:如果候选函数中包含模板函数,编译器会尝试实例化模板以找到匹配的函数。
- 引用绑定:对于通过引用传递的参数,编译器会尝试绑定到实参的引用。
- const和volatile修饰符:最佳匹配的函数应该符合
const
和volatile
的约束。
-
函数选择过程
- 最佳匹配:编译器尝试找到与实参最匹配的函数,即需要最少的用户定义转换的函数。
- 引用相加性:如果一个函数的参数可以通过添加或移除const或volatile来匹配,而另一个函数不需要这样的转换,那么后者会被优先选择。
- 转换成本:如果存在多个函数都可以匹配,编译器会根据转换的成本来选择最佳匹配,成本较低的转换会被优先选择。
- 函数的const正确性:如果函数的参数是const或volatile修饰的,调用时传递的对象也应该是const或volatile的,或者通过const_cast进行显式转换。
- SFINAE(Substitution Failure Is Not An Error):在模板函数的情况下,如果实例化失败,编译器会忽略该模板函数。
-
编译器警告与错误
- 歧义调用:如果编译器无法确定唯一的最佳匹配,它会产生歧义调用错误。
- 未使用的返回值:如果函数返回一个非void类型,并且调用它的返回值被忽略,编译器会发出警告,除非函数被标记为
[[nodiscard]]
。 - 函数签名不匹配:如果实参与所有候选函数的形参都不匹配,编译器会报错。
示例:
#include <iostream>
#include <string>
void 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,阶乘为1
if (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函数(C++11起)
C++11标准引入了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与字面类型:
从C++14开始,constexpr
还可以用于声明变量(编译期常量),并且如果一个变量被声明为constexpr
,那么它的类型必须是一个字面类型(literal type)。字面类型的数据成员也必须是字面型,这意味着它们的值可以在编译时确定。
constexpr int value = add(3, 4); // 在C++14及以后,value的值在编译时确定
constexpr与lambda表达式:
C++14标准允许使用constexpr
lambda表达式,这允许创建更复杂的编译时计算。
auto constexpr_constant = [](int a, int b) -> int {
return a + b;
};
constexpr int result = constexpr_constant(1, 2); // 在C++14及以后,result的值在编译时确定
4.consteval 函数 (C++20 起 )
consteval
是C++20引入的一个特性,它允许声明可以在编译时执行的复杂函数。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
函数的扩展,但它们之间有一些关键的区别:
-
在C++20中,
constexpr
函数既能在编译期执行,又能在运行期执行。而consteval
函数只能在编译期执行。 -
函数体的复杂性:
constexpr
函数只能有一个语句,通常是返回语句,而consteval
函数可以包含更复杂的函数体,包括循环和分支语句。 -
调用限制:
constexpr
函数只能在constexpr
上下文中调用,而consteval
函数既可以在constexpr
上下文,也可以在非constexpr
上下文中调用。
5.函数指针
在C++中,函数指针是一种指向函数的指针,它允许你将函数作为参数传递给其他函数,或者将函数赋值给指针变量。
函数指针的声明:
声明函数指针时,你需要指定函数的返回类型、参数类型以及指针的名称。
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 <iostream>
void print(int i) {
std::cout << "Integer: " << i << std::endl;
}
void print(double d) {
std::cout << "Double: " << d << std::endl;
}
int main() {
std::function<void(int)> func1 = print; // 绑定到print(int)
std::function<void(double)> func2 = print<double>; // 模板参数指定重载版本
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 <iostream>
int inc(int x)
{
return x + 1;
}
int dec(int x)
{
return x - 1;
}
auto fun(bool input)
{
if(input)
return inc;
else
return 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;
}