美文网首页
C++11中的函数知识总结

C++11中的函数知识总结

作者: bookxiao | 来源:发表于2019-05-10 16:00 被阅读0次

    函数包含两个要素:函数签名和函数体。

    其中函数签名确定了函数的类型;函数体确定了它的功能。

    说到函数式编程,核心就是我们可以把函数当做其它类型一样:可以声明函数变量、可以赋值、可以当做参数传递给函数、也可以作为函数返回的类型。

    1 函数和函数指针的定义

    当我们定义一个函数类型时,函数名、形参列表、返回值、函数体缺一不可。

    当我们声明一个函数变量时,则不需要指定函数体,且以;结尾:

    
    // 以下是一个函数定义
    
    int func(int a, int b)
    
    {
    
        return a + b;
    
    }
    
    // 以下是函数声明
    
    int func(int a, int b);
    
    

    C++中变量的类型包括:

    • 基本类型(整型、浮点型、字符型)

    • 自定义结构体或类

    • 复合类型(数组)

    • 指针

    • 函数类型

    对于函数的形参和返回值而言,它们可以是除数组类型或函数类型之外其他的任意类型。

    那么如果确实要返回数组类型或者函数类型怎么办呢?这就需要借助到指针了:指向数组的指针,和指向函数的指针。

    
    // 定义一个指向int[10]类型的数组的指针
    
    int a[10];
    
    int (*pa) [10] = a;
    
    // 定义一个指向 int (int)类型的函数的指针
    
    int (*pf) (int, int) = func;
    
    

    使用数组指针访问数组时,必须写上解指针符号:

    
    (*pa)[0] = 1
    
    

    使用函数指针调用函数时,可以省略解指针符号:

    
    pf(a, b);
    
    

    接下来看看如何定义一个函数,返回一个数组指针:

    
    int (*func1(int val))[10]
    
    {
    
        int (*pa)[10] = (int(*)[10])(new(int)[10]);
    
        for(auto i = 0; i < 10; ++i) {
    
            (*pa)[i] = val + i;
    
        }
    
        return pa;
    
    }
    
    int main()
    
    {
    
        auto pa = func1(3);
    
        // 因为func1是在堆上分配的数组,所以需要delete它
    
        delete (int *)pa;
    
    }
    
    

    再看如何返回一个函数指针:

    
    // func2 形参列表为空,然后返回一个函数指针:需要2个int形参,返回int
    
    int (*func2())(int, int)
    
    {
    
        return func;
    
    }
    
    

    当我们把一个函数名称当做值使用时(即除了调用函数之外的其它用法),它会自动转换成函数指针。

    tips

    1. 上面那种定义返回函数指针的函数,用的还是兼容C的写法。在现代C++中,可以使用尾置返回类型的方式来定义:
    
    auto func2() -> int (*)(int, int);
    
    
    1. 可以使用decltype定义函数指针类型。但是decltype一个函数名称时,得到的是函数类型,而不是函数指针类型:
    
    // 定义一个函数
    
    int retfunc(const int& a, const int& b);
    
    // 定义一个函数,返回指向int(const int&, const int&)函数类型的指针
    
    // 以下两种写法等价
    
    int(*getFunc(const int& x))(const int&, const int&);
    
    decltype(retfunc)* getFunc(const int& x);
    
    

    2 lambda表达式

    lambda表达式,就是传说中的匿名函数:即没有名字的“函数”。

    
    int main()
    
    {
    
        int a = 10;
    
        auto fl = [&a](int x) -> int { a++; return x > a ? a : x };
    
        std::cout << a << " " << fl(3) << " " << a << std::endl;
    
    
    
        return 0;
    
    }
    
    

    例如,上例中,我们定义了一个lambda对象fl:它按引用捕获了调用它的函数的局部变量a,需要传入一个参数,并返回int值。

    在lambda表达式中,仅能也是只需要捕获定义它的函数的自动局部变量。对于静态局部变量或函数外部变量,不用捕获也是可以访问的。

    对于在类的成员函数中定义的lambda表达式,除了可以捕获局部变量之外,还可以捕获这个类的非静态的成员变量(跟捕获局部变量一样)。对成员变量,还有个额外的规则:如果捕获了this指针,那么自动获取所有成员变量的访问权限。

    如果需要在lambda表达式中修改按值捕获的变量,需要在参数列表和尾置返回类型之间加上mutable关键字:

    
    auto fl = [a](int x) mutable -> int {
    
        return x + a;
    
    }
    
    

    使用bind绑定参数

    
    auto newCallable = bind(callable, arg_list);
    
    

    bind可以看做是从一个可调用对象到另外一个可调用对象的映射。跟lambda表达式一样,bind返回的也是一个可调用对象。

    callablenewCallable这两个可调用对象的形参列表,以及实参的顺序都是可以随意调整的。

    在调用bind时,我们在arg_list中,不仅可以传入任意具体的实参变量,也可以传入形如_n的“占位符”。占位符的作用,就是将调用newCallable时的参数,映射到callable时的参数:_1就是映射成newCallable的第一个参数,_2就是第二个参数,依次类推。有多少个“占位符”,就表示在调用newCallable时需要传入多少个参数。

    举个例子:

    
    // 我们有个需要传入2个参数的函数funcA
    
    int funcA(int x, int y);
    
    int a;
    
    // 有一个占位符,所以调用funcB时,需要传入一个参数
    
    auto funcB = bind(funcA, a, _1);
    
    int b;
    
    funcB(b); // 等价于 funcA(a, b)
    
    

    而且在arg_list中,_n的顺序和位置是任意的,比如_2可以在_1前面:

    
    int funcA(int x, int y, int z);
    
    int a;
    
    auto funcB = bind(funcA, _2, a, -1);
    
    int b, c;
    
    funcB(b, c); // 等价于 funcA(c, a, b);
    
    

    注:_n是定义在名字空间std::placeholders中的,所以需要先using namespace std::placeholders

    绑定引用参数

    在使用bind做函数映射时,对于那些不是占位符的参数,是将其拷贝到bind返回的可调用对象中的。如果某些参数不支持拷贝呢?比如ostream

    可以使用标准库里的ref函数返回一个变量的引用类型:

    
    ostream& print(ostream& os, const string& s, char c);
    
    ostream os;
    
    auto f = bind(print, ref(os), _1, ' ');
    
    f("hello, world");// 等价于 print(os, "hello, world", ' ');
    
    

    其实这没有改变bind的拷贝行为,因为ref()返回的就是一个可拷贝的对象,只不过它的内部定义了一个原来参数的引用类型,并且保证拷贝后都引用同一个变量。

    不信,我们可以自己实现一个类myref(为了简单起见,没有实现成模板类,只能转ostream引用):

    
    class myref {
    
    public:
    
        // 包含了引用类型的成员变量,只能在构造函数里面显式初始化
    
        myref(ostream& os) : os_(os) {}
    
        // 保证可以将它转换成一个ostream引用类型
    
        operator ostream& ()
    
        {
    
            return os_;
    
        }
    
    private:
    
        ostream& os_;
    
    };
    
    

    除了ref之外,还可以用cref返回变量的const引用类型。

    绑定类成员函数

    bind针对成员函数,提供了特别的支持,只要你把指向类实例的指针作为第二个参数传递即可。

    
    class Test {
    
    public:
    
        int func(int v);
    
    };
    
    Test t;
    
    auto f = bind(&Test::func, &t, std::placeholders::_1);
    
    

    注意,对普通函数,当我们把函数名字当做值使用时,会自动转换成函数指针;但是对于成员函数,我们必须显式写上取址符。

    3 函数对象

    如果一个类实现了函数调用运算符operator(),那么它的对象就是一个函数对象。如果这个类还定义了其它的成员变量,那么它的对象就是一个有状态的函数对象,比普通的函数拥有更强大的能力。

    知识点:lambda表达式就是一个函数对象:

    • 它定义了函数调用运算符operator()
    • 如果它按值捕获了外部变量,那么它就定义了相应的成员变量,并在构造函数中初始化这些成员变量;
    • 如果它按引用捕获了外部变量,那么编译器会直接使用这些引用,而不会在类中创建相应的成员变量。所以需要程序员保证在lambda对象生存期间,它捕获的引用变量要一直可访问;
    • 默认operator()const的,如果它被定义成mutable,那么它的operator()就不是const的。

    函数/函数指针、bind返回值、lambda表达式、函数对象等,这5种对象都有一个特点就是我们都可以对它执行函数调用。我们将其称为“可调用对象”。

    “可调用对象”的一个重要属性,就是它的调用形式(或函数签名):包括返回类型和一个实参类型列表。

    虽然这5种可调用对象的类型是不一样的,但是他们可能拥有相同的调用形式。

    例如,以下对象都实现了相同的调用形式int (int, int):

    
    // 普通函数和函数指针
    
    int add(int a, int b) { return a + b; }
    
    int (*padd)(int, int) = add;
    
    // lambda表达式
    
    auto mod = [](int a, int b) -> int { return a - b; }
    
    // 函数对象
    
    struct divide {
    
        int operator()(int den, int div) {
    
            return den / div;
    
        }
    
    };
    
    

    如果我们要把这些对象放进同一个容器呢?因为它们类型不同,是没法做到的:

    
    std::map<std::string,int(*)(int,int)> binops;
    
    binops.insert(make_pair("add", add)); // OK
    
    binops.insert(make_pair("mod", mod)); // 错误,类型不匹配
    
    binops.insert(make_pair("divide", divide())); // 错误,类型不匹配
    
    

    我们需要有一种类型,所有这些可调用对象都能自动转换成这种类型。标准库提供的function类就是啦!

    
    function<int(int,int)> f;
    
    f = add; // OK
    
    f = mod; // OK
    
    f = divide(); // OK
    
    f = bind(add, _1, _2); // OK
    
    

    只要我们定义一个调用形式一样的function对象,就可以保存所有调用形式一样的可调用对象。

    Q:如何实现将类A自动转换成类B?
    A: 有两种方法:在类A中重载类型转换运算符;在类B中重载复制构造函数和赋值运算符。但是不要两种方法同时用,会产生二义性,导致编译失败。

    相关文章

      网友评论

          本文标题:C++11中的函数知识总结

          本文链接:https://www.haomeiwen.com/subject/uovaoqtx.html