learncpp-20 函数

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>头文件中定义
posted @ 2024-07-22 22:22  dengkang1122  阅读(0)  评论(0编辑  收藏  举报