C++可调用对象详解

发表于:2016-9-05 10:04

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:PhiliAI    来源:51Testing软件测试网采编

  C++中有几种可调用对象:函数,函数指针,lambda表达式,bind创建的对象,以及重载了函数调用符的类。
  一. 函数
  函数基础的东西(形参,实参,返回,参数传递,声明,递归...)这些,不再叙述,都是基础得不能再基础的东西了。局部对象,有了CSAPP的基础这些都不是问题,重点讲下C++11的某些重要特性和函数指针。
  可变形参函数:
  C++11有两种办法传递可变形参(其实学了模板以后可以用模板的自动递归来展开所传递的参数,这个后面再说)。
  1. 第一种是initializer_list,这是一个标准库类型(其实是个模板)。
  std::initializer_list<T>{ };
  可以使用列表初始化来进行初始化,T表示的是参数类型,initializer_list可以被拷贝,但是一定要注意的是,它是一种引用拷贝,也就是说拷贝后新的list和被拷贝的list是的元素都是共享的。
  2. 第二种是省略符形参,熟悉C的人对这个也应该很熟悉了,就是printf和scanf这些函数所用的方法。
  void print(int, ...);
  省略符形参其实是为了方便访问特殊的C代码而设计的,这些代码其实是使用了标准库varargs的功能(C标准库)。
  如果我们使用这种方法传递省略形参,一定要注意,这种代码只能用来处理C++和C通用版本的东西,对于C++内的对象,这种方法大多数情况下都是不行的。
  例子:
1 #include <stdarg.h>
2 void method(int i, ...)
3 {
4     int v;
5     va_list params;//声明可变參数列表
6     va_start(params, i); //依据可变參数前的參数获得可变參数的起始地址
7     do {
8         v = va_arg(params, int); //从可变參数列表中获得參数的值,类型为int
9         printf("%d", v);
10     } while (v != 0);
11     va_end(params);
12 }
13
14 void format(const char* format, ...)
15 {
16     char buf[1024];va_list params;
17     va_start(params, format);
18     vsnprintf_s(buf, 1024, 1024, format, params);
19     va_end(params);
20     printf("%s", buf);
21 }
  (注意省略符前面的逗号可以省略。)
  注意C++内,函数可以返回引用(但是不能返回局部对象的引用,类的成员函数还可以指定返回左值还是返回右值),另外C++11还支持列表初始化返回。
  C++可以返回数组,我们可以使用别名来返回数组(一定要知道数组的维度)。
  typedef int arr[10];//typedef方法
  using Arr = int[10];//别名用法
  Arr *func0(int i);
  int(*func1(int i))[10];
  auto func2(int i) -> int(*)[10];
  另外C++还可以有尾置返回类型:
1 //尾置返回类型(明确知道函数返回的指针指向什么地方)
2 int odd[] = { 1,3,5,7,9 };
3 int even[] = { 0,2,4,6,8 };
4
5 decltype(odd) *arrPtr(int i)
6 {
7     return (i % 2) ? &odd : &even;
8 }
  C++重要的特性之一:函数重载(main函数不能被重载),函数重载需要满足形参列表不同的条件(但要注意编译器无法区别顶层const,如果有两个相同位置且相同类型的参数,编译器并不会区分它们的顶层const的区别,但是底层const会区别)。如果我们传递一个非常量对象或者一个指向非常量对象的指针的时候,编译器会优先选择非常量对象。
1 const std::string &shortString(const std::string &s1, const std::string &s2)
2 {
3     return s1.size() <= s2.size() ? s1 : s2;
4 }
5 std::string &shortString(std::string &s1, std::string &s2)
6 {
7     auto &r = shortString(const_cast<const std::string &>(s1),
8         const_cast<const std::string &>(s2));//注意这里的shortSring必须要const_cast
9                                              //因为在这里s1和s2都是非常连版本,而编译器会优先选择非常量版本进行调用,导致递归出错
10     return const_cast<std::string &>(r);
11 }
  const_cast和重载(注意自己一定要清楚const_cast之前的变量是不是const,如果是const,那么就会产生未定义的行为)。
  函数的默认实参,从左往右,从某个形参开始起,如果它被赋予了默认形参,那么其后面的所有形参都要赋予默认形参,默认形参在给定的作用域内只能被赋予一次(即使函数有可能被多次声明,但是后续声明不能为已经添加过默认形参的形参添加默认形参)。而且要注意,局部变量是不能去定义默认形参的。primer 5e上面的例子:
1 size_t wd = 80;char def = ' ';
2 size_t ht();
3 string screen(size_t = ht(), size_t = wd, char = def);
4 string window = screen();//调用screen(ht(), 80 ' ')
5
6 void f2()
7 {
8     def = '*';
9     size_t wd = 100;
10     window = screen();//调用screen(ht(), 80 '*')
11 }
  内联函数和constexpr函数:简单来说,内联函数可以在汇编的时候把函数体展开,条件就是函数体必须要短小,不过很多时候就算你指定了函数为内联函数,编译器并不会承认,在很多编译器中,内联inline关键字是一个可选选项,编译器并不会强制执行iniline操作。
  对于constexpr函数,其是一个C++11用于之指定常量表达式的方法constexpr函数需要满足:返回类型和所有形参类型都是字面值类型,而且函数体必须有且只有一条return语句,因为constexpr函数只有返回值,所以constexpr被隐式指定为内联函数,constexpr函数也可以包含其他语句,只要这些语句不执行任何操作就可以了(比如可以有空语句,以及类型别名)。
  1 constexpr size_t scale(size_t cnt)
  2 {
  3     using test = int;
  4     return cnt * 2;
  5 }
  要注意,C++允许constexpr函数返回一个非常量值,而且当constexpr函数的实参不是字面值常量的时候,返回值有可能不是常量表达式。
  int t;
  int arr[scale(100)];
  auto ret = scale(t);
  scale(100)返回的是一个常量表达式,但是scale(t)则返回的就是size_t类型。
  内联函数和constexpr函数都应该放在头文件里面,内联函数和constexpr函数与其他函数最大的不同就是,内联函数和constexpr函数是可以多次定义的,但是这多次定义必须完全一致,所以内联函数和constexpr函数一般都定义在头文件内。
  函数匹配和二义性问题:由于C++由函数重载,所以函数匹配是一件很让编译器头疼的事情,编译器将实参类型到形参类型的转换分为以下几个等级:(从上到下优先级依次降低)。
  精确匹配(实参类型和形参类型是一样的,或者实参从数组类型或者函数类型转换成对应的指针类型,向实参添加顶层const或者从实参中消除顶层const)(对于底层const,如果同时定义了非常量版本和常量版本,如果传入非常量,那么就会调用非常量版本函数,如果传入的实参为常量版本,则会调用常量版本的函数)。
  通过const实现的匹配
  通过类型提升来实现的匹配(比如整型提升)。
  通过算术转换或者指针转换实现的匹配。
  通过类类型转来来匹配的转换。
  二. 函数指针
  1. 函数指针顾名思义就是指向函数的指针,类似于
  int(*pf)(std::string s1);
  指针名的括号必不可少,不然会认为是返回int的指针,使用函数指针可以直接让指针等于函数名,C++规定给函数名取地址和直接使用函数名都是可以得到函数的地址:
  1 int test(std::string s1) { return 0; }
  2 int(*pf)(std::string s1);
  3 pf = test;
  4 pf = &test;
  5 test(std::string());
  6 (*test)(std::string());
  上面的两种使用方法都是等价的。同时在C++11中,我们也可以使用尾置返回类型来声明一个函数的返回值为一个函数指针(和数组指针类似)
  auto foo() -> int(*)(std::string s1);
  也可以用decltype来指定返回的函数指针的类型(注意函数名的前面一定显式添加星号表明返回的是一个函数指针而不是函数本身)
  decltype(test) *getfcn();
  三. lambda表达式(匿名函数对象,一个C++的语法糖)
  一个lambda表达式表示一个可调用的代码单元,我们可以将其理解为有一个未命名的内联函数,与任何函数类似,一个lambda具有一个返回类型,一个参数列表和一个函数体,并且这个函数体是可以定义在函数内部的。
  lambda必须使用尾置返回类型来指定返回类型,不能有默认形参,并且具有以下形式:
  [capture list](parameter list) -> return type{function body }
  lambda表达式可以忽略返回类型和形参,形如:
  auto fcn = [] {return 42;};//定义了一个可调用对象fcn为一个lambda表达式
  //忽略参数列表,忽略返回类型(都为空)
  有一些标准库的算法的谓词,其形参参数可能不能满足我们的需求,这个时候我们可以使用lambda表达式的参数捕获来间接获得更多的参数,比如标准库的<algorithm>里面有find_if函数,它接受一个一元谓词,可以在规定范围内找到满足条件的第一个迭代器,假设现在我们给定一个vector<string>,我们要找到在整个vector内不大于长度l的第一个迭代器,如果我们一定要使用find_if这个标准库函数来实现这个功能,这个时候我们就要用到lambda的参数捕获了。
  先来个例子:
1 #include <algorithm>
2 #include <iostream>
3 #include <vector>
4
5 using std::vector;
6 using std::find_if;
7 using std::for_each;
8 using std::cout;
9 using std::endl;
10
11 void searchSegment(vector<int> nums, const int floorSize, const int ceilSize)
12 {
13     std::stable_sort(nums.begin(), nums.end(),
14         [](const int &a, const int &b) { return a < b;});//注意sort和stable_sort当comp(x,x)的时候一定要返回false
15     auto segFloorIndex = find_if(nums.begin(), nums.end(),
16         [&floorSize](const int &a) ->bool { return a >= floorSize; });
17     auto segCeilIndex = find_if(nums.begin(), nums.end(),
18         [&](const int &a) { return a >= ceilSize; });
19     cout << "The size of the segment is " << segCeilIndex - segFloorIndex << endl;
20 }
  捕获分值捕获和引用捕获,引用捕获就在变量前面加&就可以了,引用捕获可以解决某些类不能被拷贝的问题(比如输入输出流),另外捕获还可以是隐式的,比如我上面的算segCeilIndex的时候,就可以直接一个&就代表捕获所有的引用参数了。
  另外捕获可以混用(引用和值捕获混用),这个时候捕获列表的第一个元素必须是&(引用捕获)或者=(值捕获)(不能同时隐式值捕获和隐式引用捕获,只能是其中一种情况),如果才用了一种隐式捕获情况,另一种捕获必须显示,比如采用了隐式引用捕获,那么值捕获的所有值都必须显式捕获。
  lambda表达式值捕获的值也是可以变的,只是这个时候必须使用mutable关键字,(非常量引用捕获理所当然可以修改,这个不用多说)比如:
  int v;
  auto f = [v]()mutable {return ++v; };
  有些时候lambda表达式的尾置返回类型不能被省略,最常见的就是if_else的情况发生(?:表达式是可以省略返回参数的),就例如上面的segFloorIndex加了个->bool。
  lambda表达式质上是一个重载了函数调用符的类(看5),很适合在一些函数短小而且只用写几次的地方,可以把代码写得很好看。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号