20 函数
20.1 函数指针
- 函数和变量一样,也是在内存中被分配了一块地址。因此,函数指针就是一个保存函数的内存地址的变量
- 函数也是有类型的,例如foo()这个函数的类型就是
返回整数且不带参数
int foo(){return 5;}
<<
操作符不知道如何输出函数指针(因为有无数种可能的函数指针),因此foo会被转换成bool
,并且由于foo这个函数的指针是非void,所以应该被转换成true
std::cout<<foo(); // 输出5
std::cout<<foo; // 输出1,也就是true
int (*p)()
表示p是一个指向没有参数、返回类型为int的任何函数的指针;而int* p()
表示p是一个没有参数、返回类型为int*的函数- 要创建一个常量函数指针,const放在*后面:
int (*const p)()
- 如何解析一个复杂的声明:
螺旋法则
(https://c-faq.com/decl/spiral.anderson.html) - 函数指针的类型(参数类型和返回类型)必须和函数的类型一致
int foo();
double goo();
int hoo(int x);
int (*p1)(){&foo}; // ok
int (*p2)(){&goo}; // 编译报错:different return type
double (*p3)(){&goo}; // ok
int (*p4)(){&hoo}; // 编译报错:different number of parameters
int (*p5)(int){&hoo}; // ok
- 与基本类型不同,C++会隐式地将函数转换为函数指针,如果获取函数地址时可以省略
&
;但是函数指针不会转换为void指针,反之亦然
int foo();
int (*p1)(){foo}; // ok
void* p2{foo}; // 编译报错(可能有些编译器会允许)
- 函数指针可以初始化或者赋值为
nullptr
int (*p)(){nullptr}; // ok
- 通过函数指针调用函数的两种方式:
- 通过显式解引用
- 通过隐式解引用
int foo(int x){return x;}
int (*p)(int){&foo};
(*p)(5); // return 5
p(7); // return 7
- 在调用函数指针之前校验是否为
nullptr
,对一个空的函数指针解引用会导致未定义的行为 - 使用函数指针调用函数时,函数的
默认参数将失效
当编译器遇到对含有默认参数的函数的正常调用时,会重写函数调用以包含默认参数,这个过程发生在编译期间
但是通过函数指针调用函数的过程是在运行期间被解析的,因此这种调用没有被编译器重写以包含默认参数
- 函数指针可以
将函数作为参数传递给另一个函数
,这个作为参数的函数有时称为回调函数
- 当函数的参数是函数类型时,会被转换成函数指针类型
// 这两种方式是等价的
void selectionSort(int* array, int size, bool comparisonFcn(int,int))
void selectionSort(int* array, int size, bool (*comparisonFcn)(int,int))
- 使用类型别名简化函数指针的语法
using vp = void(*)(int);
vp ptr; // ptr是一个函数指针,指向一个void (int)类型的函数
- 定义和存储函数指针的另一种方式是
std::function
int foo() {
return 5;
}
int (*p1)(){&foo};
std::cout << (*p1)() << std::endl; // 5
std::function<int()> p2{&foo};
std::cout << p2() << std::endl; // 5
std::cout << (*p2)() << std::endl; // 编译报错:通过std::function生成的函数指针只能通过隐式解引用调用函数,不能使用显式解引用
std::function p3{&foo}; // 可以使用类型推断,无需指定类型参数
std::cout << p3() << std::endl; // 5
- 使用类型别名时必须指定类型参数
using functionPtr = std::function<bool(int,int)>
- 可以使用
auto
关键字来推断函数指针的类型,但是会隐藏函数的参数类型和返回类型 - 推荐使用
std::function
来声明函数指针
如果只需要使用一次函数指针的类型(例如单个参数或返回值),可以直接使用
std::function
如果需要多次函数指针的类型,最好为std::function
设置别名
20.6 lambda表达式(匿名函数)
- lambda表达式语法:
[captureClause] (parameters) -> returnType
{
statements;
}
returnType是可选的,如果returnType被省略了,则会使用
auto
来推断返回类型
如果未指定returnType并且没有参数,则整个参数列表可以省略
- 当需要
一个一次性的函数作为参数
传递给其他函数时,优先使用lamba表达式 - 可以使用lambda定义来初始化一个lambda变量,以便后续使用
- lambda变量是一种特殊的对象,它们的类型是
functor
。functor类有一个成员函数对操作符()
进行了重载,这使得functor类型的对象可以像函数一样被调用。(因此这些对象也称为函数对象
)
auto isEven{
[](int i){
return (i%2)==0;
}
};
std::cout << isEven(3) << std::endl; // 0
std::cout << isEven(4) << std::endl; // 1
- 如果捕获子句为空,则可以使用
普通函数指针类型
存储lambda表达式;如果捕获子句不为空,则可以使用std::functino
类型或者使用auto
进行类型推断
// 普通指针类型(适用于捕获子句为空)
int (*addFunc1)(int,int){
[](int a,int b){
return a+b;
}
};
std::cout << addFunc1(1, 2) << '\n'; // 3
// std::function(捕获子句可以不为空)
std::function addFunc2{
[](int a, int b) {
return a + b;
}
};
std::cout << addFunc2(3, 4) << '\n'; // 7
// auto(捕获子句可以不为空)
auto addFunc3{
[](int a, int b) {
return a + b;
}
};
std::cout << addFunc3(5, 6) << '\n'; // 11
如果想要使用lambda变量的实际类型,只能通过
auto
- 假设我们需要把一个lambda变量传递给一个函数,有以下四种形参的形式:
auto lambda = [](int a) {
std::cout << a << ' ';
};
// 1.std::function
void repeat1(int repetitions, std::function<void(int)> fn) {
for (int i = 0; i < repetitions; ++i) {
fn(i);
}
}
repeat1(3,lambda); // 输出0 1 2
// 2.模板参数
template<typename T>
void repeat2(int repetitions, T fn) {
for (int i = 0; i < repetitions; ++i) {
fn(i);
}
}
repeat2(3,lambda); // 输出0 1 2
// 3.简化的函数模板语法(C++20)
void repeat3(int repetitions, auto fn)
{
for (int i{ 0 }; i < repetitions; ++i)
fn(i);
}
repeat(3,lambda); // 输出0 1 2
// 4.普通函数指针(lambda的捕获子句必须为空)
void repeat4(int repetitions, void (*fn)(int)) {
for (int i = 0; i < repetitions; ++i) {
fn(i);
}
}
repeat4(3,lambda); // 输出0 1 2
- 当lambda表达式有一个或多个
auto
参数(也叫泛型lambda
)时,编译器将从对lambda的调用中推断出所需的参数类型
[](auto& a, auto& b){
return a[0]==b[0];
}
当
auto
用于lambda中时,它只是模板参数的简写
- 从C++17开始,如果lambda的结果满足常量表达式的要求,则lambda是隐式的constexpr。这通常需要满足两个条件:
- lambda必须没有捕获子句或者所有捕获子句都为constexpr
- lambda调用的函数必须是constexpr
auto
解析为的每种不同类型都会对应生成唯一的lambda表达式(因此包含独立的静态局部变量
)
auto print{
[](auto value) {
static int callCount{0};
std::cout << callCount++ << ": " << value << '\n';
}
};
print("hello");// 0: hello
print("world");// 1: world
print(1);// 0: 1
print(2);// 1: 2
print("ding dong");// 2: ding dong
上述代码会生成
void(const char*)
和void(int)
两种类型的lambda表达式,静态局部变量callCount
不会在这两个lambda表达式中共享
- 当使用
auto
存储lambda表达式时,变量的返回类型将根据lambda表达式里的return语句进行类型推断(lambda表达式里的所有return语句必须返回相同的类型)
auto divide{
[](int x, int y, bool intDivision) {
if (intDivision) {
return x / y;
} else {
return static_cast<double >(x) / y;// 编译报错:Return type 'double' must match previous return type 'int' when lambda expression has unspecified explicit return type
}
}
}
上述报错有两种解决方案:
1.进行显式转换使得return语句的返回类型都相同
2.为lambda表达式指定返回类型,让编译器做隐式转换
auto divide{
[](int x, int y, bool intDivision) -> double {
if (intDivision) {
return x / y;// 编译器会对返回结果进行隐式转换
} else {
return static_cast<double >(x) / y;
}
}
}
- 对于常见的操作(加法、取反、比较),不需要自己编写lambda表达式,标准库中提供了许多基本的
可调用对象(函数对象)
,它们在<function>
头文件中定义