C++11中的函数

栏目: C · 发布时间: 5年前

内容简介:函数包含两个要素:函数签名和函数体。其中函数签名确定了函数的类型;函数体确定了它的功能。说到函数式编程,核心就是我们可以把函数当做“一等公民”:可以声明函数变量、可以赋值、可以当做参数传递给函数、也可以作为函数返回的类型。

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

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

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

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中的函数》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Apache Modules Book

The Apache Modules Book

Nick Kew / Prentice Hall PTR / 2007-02-05 / USD 54.99

"Do you learn best by example and experimentation? This book is ideal. Have your favorite editor and compiler ready-you'll encounter example code you'll want to try right away. You've picked the right......一起来看看 《The Apache Modules Book》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具