C++各类函数对象

C++各类函数对象

普通函数

特殊函数

  • 时间:

    clock()返回当前时间,但是这个返回值不一定是秒,也不一定是同一个类型.但是ctime库文件,通过定义了一些值解决了这些.

    但头文件ctime(较早的实现中为time.h)提供了这些问题的解决方案。首先,它定义了一个符号常量CLOCKS_PER_SEC,该常量等于每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,
    可以得到秒数。或者将秒数乘以CLOCKS_PER_SEC,可以得到以系统时间单位为单位的时间。其次,ctime将clock_t作为clock()返回类型的别名(参见本章后面的注释“类型别名”),这意味着可以将变量声明为clock_t类型后转化.

    该程序以系统时间单位为单位(而不是以秒为单位)计算延迟时间,避免了在每轮循环中将系统时间转换为秒。

  • cctype:c的ctype

内联函数

inline关键字,但是一般编译器都会优化,所以没啥意义.

参数

函数传参

可以在声明中使用const来表示这个数组不可修改.同时也禁止const的地址赋给非const指针,处于安全的需求,可以先用const,如果需要改变再去掉.

注意传参是从右往左,所以参数如果是函数要小心,尤其是c的strtok之类的.

引用变量

C++重载了&,用来声明引用,比如要把rodents作为rats的别名,可以这样做:

int rats;
int &rodents =  rats;

引用必须声明的时候初始化.

如果赋值为常量,那么也应该加上const.

如果是强制转化也需要,比如:

double a = 1.1;
const int &b = (int)a;

大多数的解释是a被转化后,是把转化值放入一个临时常量,把这个常量赋值,所以b必须用const来修饰.

这个用在函数传参时,可以如同指针一样直接修改,而不是复制一遍.一般的编译器使用常量指针实现这个功能的.

void fun(int &b){b+=10;}
int main(){
    int a = 10;
    fun(a);
    cout << a; //a=20
}

引用这个特性主要是为了结构和类服务的,为此还有个特性,基类引用可以接受派生类对象

默认参数

C++可以设置默认参数值.

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值:

int harpo(int n,int m=4,int j=5);
//VALID
int chico(int n,int m=6,int j);
//INVALID
int groucho(int k=1,int m=2,int n=3);
//VALID

实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。因此,下面的调用是不允许的:
beeps harpo(3,,8);/invalid,doesn't set m to 4

重载

主要是通过名称修饰做到的,导致逆向难度被加大捏(凸(艹皿艹 ))

故而使用C库,务必使用extern "C",因为C语言的函数没有改变名字,所以正常调用编译器也不知道,要提醒的.

重载只和参数列表(类型,个数,顺序)有关,和返回值无关。故而返回值不同但是参数列表一致的函数是无效重载。

Lambda

基本语法

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}

mutable

默认情况下,Lambda 表达式不能修改按值捕获的变量(捕获的副本被视为 const)。mutable 关键字的作用是解除这种限制,允许在 Lambda 内部修改按值捕获的变量副本。

#include <iostream>

int main() {
    int x = 10;
    
    // 不使用 mutable:按值捕获的 x 是 const,无法修改
    auto func1 = [x]() {
        // x = 20;  // 错误:不能修改按值捕获的变量
        std::cout << "func1: " << x << std::endl;
    };
    
    // 使用 mutable:允许修改按值捕获的 x 副本
    auto func2 = [x]() mutable {
        x = 20;  // 合法:修改的是副本,不影响外部 x
        std::cout << "func2 内部: " << x << std::endl;
    };
    
    func1();  // 输出:func1: 10
    func2();  // 输出:func2 内部: 20
    std::cout << "外部 x: " << x << std::endl;  // 输出:外部 x: 10(原变量未变)
    return 0;
}

异常属性

异常属性主要通过 noexcept 关键字声明,用于告诉编译器 Lambda 表达式是否可能抛出异常,帮助编译器进行优化或强制异常安全。

  • noexcept:表示 Lambda 绝对不会抛出任何异常。
  • noexcept(表达式):根据表达式的布尔结果决定是否可能抛出异常(表达式为 true 表示不抛异常)。
#include <iostream>

int main() {
    // 声明为不会抛出异常
    auto safe_func = []() noexcept {
        std::cout << "这是一个安全的函数,不会抛异常" << std::endl;
    };
    
    // 可能抛出异常(未声明 noexcept)
    auto risky_func = [](int x) {
        if (x == 0) {
            throw std::runtime_error("除数为 0");  // 可能抛异常
        }
        return 10 / x;
    };
    
    safe_func();
    try {
        risky_func(0);
    } catch (const std::exception& e) {
        std::cout << "捕获异常: " << e.what() << std::endl;
    }
    return 0;
}

捕获列表(指直接使用相关变量)

  • [] 空捕获列表

  • [name1, name2, ...] 捕获一系列变量,被捕获的变量在 Lambda 表达式被创建时拷贝,而非调用时才拷贝.意思就是说,如果第一次已经调用,后续的值都是这个值,而不管被捕获变量的变化.下例:

    #include <iostream>
    
    int main() {
      int a = 1;
      auto foo = [a] { return a; };
      int b = foo();
      a = 100;
      int c = foo();
      std::cout << b << ',' << c << std::endl;
      return 0;
    }
    //输出是1,1
    
  • [&name1,&name2] 引用捕获, 从函数体内的使用确定引用捕获列表,这个就会变化了,因为捕获的是引用.

  • [&] 引用捕获从函数体内的使用确定引用捕获列表

  • [=] 值捕获, 从函数体内的使用确定值捕获列表

  • 表达式捕获 lambda在c++14可以初始化,那么就可以在里面写表达式.

需要注意的是,如果按引用捕获,要注意是否悬空捕获,比如说lambda把一个局部变量引用了,结果lambda又被一个全局容器使用了,那么就会出现这个问题.所以相比默认&,显式地&name更为地好,至少会提醒你.

也许有人会想,那我默认按值引用就好了,但是如果这个值本身是个指针,后续被delete了.那也是有问题的.甚至即使你使用智能指针,也有可能.所以还是显式指出来算了.

总而言之就是不要默认捕获,因为你更本不知道捕获的有没有问题,指代不明

列表初始化

值捕获只能捕获左值,但在C++14后允许在捕获列表初始化了,就可以使用move语义来初始化右值.

泛型lambda

在参数列表中运用auto的被称为泛型lambda,因为编译器会生成一堆模板.

对于泛型lambda有个小问题,就是如果参数是按值引用,形参是左值,传进去是拷贝,效率会有点问题.

那么可以用转发来实现,简单来说是,对auto&&形参使用decltypestd::forward它们.

函数容器

std::functional

std::functional是相比直接调用函数指针更安全(类型安全),更方便(可以容纳各种函数)的方式,换言之即函数的容器.(不知道为什么,我写代码时不用调用这个库)

C++11 std::function 是一种通用、多态的函数封装, 它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作, 它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的), 换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。 例如:

#include <functional>
#include <iostream>

int foo(int para) {
    return para;
}

int main() {
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

std::bindstd::placeholder

std::bind 则是用来绑定函数调用的参数的, 它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数, 我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。

std::placeholder是配合,这个是有顺序的.

完整例子:

void foo(int a, int b, int c) {
    std::cout << a << b << c << std::endl;
}
int main() {
    // 将参数1,2绑定到函数 foo 上,
    // 但使用 std::placeholders::_1 来对第一个参数进行占位
    auto bindFoo = std::bind(foo, std::placeholders::_1, std::placeholders::_2,2);
    auto bindFoo_ = std::bind(foo, std::placeholder::_2, std::placerholders::_1, 2);
    // 这时调用 bindFoo 时,只需要提供第一个参数即可
    bindFoo(1,2);// 输出122
    bindFoo_(1,2); // 输出212
}

对于成员函数也可以,不过有些特殊规则,下例:

class w{int add(int a, int b){return a+b;}};
w w1;
auto add1 = std::bind(&w::add, &w, 10, 1)//第一个参数的引用必须保留,第二个参数必须是类对象,等于说代替this指针.

最困难的是传递左值右值会发生什么?结论是无论你的函数是如何传参,传递左值一定是拷贝,传递右值一定是移动.大概原因(没看源码)是,bind是一个闭包,如果是引用传递,就不符合闭包的定义了,那么只能采取按值传递,也就是移动和拷贝.

Lambda和bind

相比之下,最好都用Lambda,因为它能实现bind的功能而且更简单更好.

bind初始化时会调用参数的函数

auto L = [](int x){f1(f2())}; //这里只是封装,没用调用f2()
auto B = std::bind(f1, f2, _1); //这里会调用f2来传递,正确写法看下面
auto B = std::bind(f1, std::bind(f2), _1);

孰优孰劣可以看出

bind无法直接识别重载

void foo(int a);
void foo(int a, int b);

auto L = [](int b){foo(1, b)};
auto B = std::bind(foo, 1, b);//报错,除非对foo强制类型转化

Lambda可以内联,而bind是函数指针

性能的差距

适合bind的情况

C++11的情况,但是应该不会用这么老的,14出现初始化列表就可以全用Lambda了.

仿函数

类通过重载()来模拟函数的方式.

posted @ 2025-08-30 13:45  T0fV404  阅读(2)  评论(0)    收藏  举报