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

查看所有标签

猜你喜欢:

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

Python编程初学者指南

Python编程初学者指南

[美]Michael Dawson / 王金兰 / 人民邮电出版社 / 2014-10-1

Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python可以用于很多的领域,从科学计算到游戏开发。 《Python编程初学者指南》尝试以轻松有趣的方式来帮助初学者掌握Python语言和编程技能。《Python编程初学者指南》共12章,每一章都会用一个完整的游戏来演示其中的关键知识点,并通过编写好玩的小软件这种方式来学习编程,引发读者的兴趣,降低学习的难度。每章最后都会......一起来看看 《Python编程初学者指南》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具