C++11 是 C++ 程序设计语言标准的一个新的版本,在 2011 年由 ISO 批准并发布。C++11 新标准从而代替了原来的 C++98 和 C++03.。C++11 标准是对 C++ 的一次巨大的改进和扩充。在核心语法,STL 标准模板等方面增加众多新功能。例如新增 auto,deltype,nullptr 等关键字,增加范围 for 循环,新增 lambda 表达式等。
文章目录
一、 健壮性改进
1.1 原始字面量R"()"
在C++11中添加了定义原始字符串的字面量,定义方式为:R “xxx(原始字符串)xxx”其中()两边的字符串可以省略。原始字面量R可以直接表示字符串的实际含义,而不需要额外对字符串做转义或连接等操作。
string str = "D:\hello\world\test.text";
cout << str << endl; // D:helloworld est.text
string str1 = "D:\\hello\\world\\test.text";
cout << str1 << endl; // D:\hello\world\test.text
string str2 = R"(D:\hello\world\test.text)";
cout << str2 << endl; // D:\hello\world\test.text
在R"(D:\hello\world\test.text)"使用了原始字面量R()中的内容就是描述路径的原始字符串,无需做任何处理
注意:在R “xxx(raw string)xxx” 中,原始字符串必须用括号()括起来,括号的前后可以加其他字符串,所加的字符串会被忽略,并且()前后加的字符串必须相同。
1.2 long long 类型
C++11 标准要求 long long 整型可以在不同平台上有不同的长度,但至少有64位。long long 整型有两种∶
- long long - 对应类型的数值可以使用 LL (大写) 或者 ll (小写) 后缀
long long num1 = 123456789LL;
long long num2 = 123456789ll;
- unsigned long long - 对应类型的数值可以使用 ULL (大写) 或者 ull (小写) 或者 Ull、uLL (等大小写混合)后缀
unsigned long long num1 = 123456789ULL;
unsigned long long num2 = 123456789ull;
unsigned long long num3 = 123456789uLL;
unsigned long long num4 = 123456789Ull;
1.3 空指针nullptr
在底层源码中NULL这个宏是这样定义的:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
也就是说如果源码是C++程序NULL就是0,如果是C程序NULL表示(void*)0。
原因:是由于 C++ 中,void * 类型无法隐式转换为其他类型的指针,此时使用 0 代替 ((void *)0),用于解决空指针的问题。
C++ 中将 NULL 定义为字面常量 0,并不能保证在所有场景下都能很好的工作,比如,函数重载时,NULL 和 0 无法区分:
#include <iostream>
using namespace std;
void func(char *p){
cout << "void func(char *p)" << endl;
}
void func(int p){
cout << "void func(int p)" << endl;
}
int main(){
func(NULL); // 想要调用重载函数 void func(char *p)
func(250); // 想要调用重载函数 void func(int p)
//最终均调用了void func(int p)
return 0;
}
引入了一个新的关键字nullptr。nullptr 专用于初始化空类型指针,不同类型的指针变量都可以使用 nullptr 来初始化:
int* ptr1 = nullptr;
char* ptr2 = nullptr;
double* ptr3 = nullptr;
对应上面的代码编译器会分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。
1.4 constexpr(修饰常量表达式)
const关键字,从功能上来说这个关键字有双重语义:变量只读,修饰常量
变量只读并不等价于常量
void func(const int num){
const int count = 24;
int array[num]; // error,num是一个只读变量,不是常量
int array1[count]; // ok,count是一个常量
int a1 = 520;
int a2 = 250;
const int& b = a1;
b = a2; // error
a1 = 1314;
cout << "b: " << b << endl; // 输出结果为1314
}
在C++11中添加了一个新的关键字constexpr
,这个关键字是用来修饰常量表达式的。所谓常量表达式,指的就是由多个(≥1)常量(值不会改变)组成并且在编译过程中就得到计算结果的表达式。
常量表达式和非常量表达式的计算时机不同
非常量表达式只能在程序运行阶段计算出结果
常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
在C++11中添加了constexpr
关键字之后就可以在程序中使用它来修饰常量表达式,用来提高程序的执行效率。在使用中建议将 const
和 constexpr
的功能区分开,即凡是表达“只读”语义的场景都使用const
,表达“常量”语义的场景都使用 constexpr
。
const int m = f(); // 不是常量表达式,m的值只有在运行时才会获取。
constexpr int i=520; // 是一个常量表达式
constexpr int j=i+1; // 是一个常量表达式
对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现的),直接用 constexpr 修饰是不行的。
只有实例化对象时可以修饰。
constexpr struct Test{ // error 不能用constexpr修饰
int id;
int num;
};
int main(){
constexpr Test t{ 1, 2 }; // 实例化时,可以constexpr修饰
constexpr int id = t.id;
constexpr int num = t.num;
t.num += 100; // error,不能修改常量
cout << "id: " << id << ", num: " << num << endl;
return 0;
}
1.4.1 constexpr修饰模板函数
C++11 语法中,constexpr
可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。
如果 constexpr
修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr
会被自动忽略,即该函数就等同于一个普通函数。
#include <iostream>
using namespace std;
struct Person {
const char* name;
int age;
};
// 定义函数模板,使用constexpr 修饰
template<typename T>
constexpr T dispaly(T t) {
return t;
}
int main(){
struct Person p { "luffy", 19 };
//传入变量时,忽略constexpr,为普通函数
struct Person ret = dispaly(p);
cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;
//传入常量时,为常量表达式函数
constexpr int ret1 = dispaly(250);
cout << ret1 << endl;
constexpr struct Person p1 { "luffy", 19 };
constexpr struct Person p2 = dispaly(p1);
cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl;
return 0;
}
1.4.2 constexpr修饰构造函数
如果想用直接得到一个常量对象,也可以使用constexpr
修饰一个构造函数,这样就可以得到一个常量构造函数了。
常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。
#include <iostream>
using namespace std;
struct Person {
// 常量构造函数,实例化对象时就是一个常量
constexpr Person(const char* p, int age) :name(p), age(age){}
const char* name;
int age;
};
int main(){
constexpr struct Person p1("luffy", 19);
cout << "luffy's name: " << p1.name << ", age: " << p1.age << endl;
return 0;
}
1.5 final和override
1.5.1 final
C++中增加了final
关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和Java的final
关键字的功能是类似的。如果使用final
修饰函数,只能修饰虚函数,并且要把final
关键字放到类或者函数的后面。
1. 修饰函数
如果使用final
修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了
class Base{
public:
virtual void test(){
cout << "Base class...";
}
};
class Child : public Base{
public:
void test() final{ // final修饰,阻止子类重写父类的这个函数
cout << "Child class...";
}
};
class GrandChild : public Child{
public:
// 语法错误, 不允许重写
void test(){
cout << "GrandChild class...";
}
};
2. 修饰类
使用final
关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类。
class Base{
public:
virtual void test() {
cout << "Base class...";
}
};
class Child final: public Base{ //final修饰,Child类不会再有子类
public:
void test(){
cout << "Child class...";
}
};
// error, 语法错误
class GrandChild : public Child{
public:
}
1.5.2 override
override
关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高了代码的可读性,和final
一样这个关键字要写到方法的后面。使用方法如下:
class Base{
public:
virtual void test(){
cout << "Base class...";
}
};
class Child : public Base{
public:
void test() override { // override修饰
cout << "Child class...";
}
};
使用了override关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。
1.6 模板的优化
二、 实用性
2.1 自动类型推导auto
C++11中auto
并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto
并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto
声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。使用语法如下:
auto x = 3.14; // x 是浮点型 double
auto y = 520; // y 是整形 int
auto z = 'a'; // z 是字符型 char
auto nb; // error,变量必须要初始化
auto double nbl; // 语法错误, 不能修改数据类型
2.1.1 auto的限制
auto关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
- 不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
- 不能用于类的非静态成员变量的初始化。
class Test{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化,因为static在编译阶段存在,那时类还不存在
static const auto v3 = 10; // ok
}
- 不能使用auto关键字定义数组。
- 无法使用auto推导出模板参数。
2.1.2 auto应用
1. 用于STL遍历
在C++11之前,定义了一个stl容器之后,遍历的时候常常会写出这样的代码:
#include <map>
int main()
{
map<int, string> person;
map<int, string>::iterator it = person.begin();
for (; it != person.end(); ++it) {
// do something
}
return 0;
}
可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,使用了auto之后,就变得清爽了不少:
#include <map>
int main(){
map<int, string> person;
// 代码简化
for (auto it = person.begin(); it != person.end(); ++it){
// do something
}
return 0;
}
2. 用于泛型编程
在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:
#include <iostream>
#include <string>
using namespace std;
class T1{
public:
static int get(){
return 10;
}
};
class T2{
public:
static string get(){
return "hello, world";
}
};
// val如果不为auto,则需要再定义一个模板参数,因为A::get()的返回值可能是string or int
template <class A>
void func(void){
auto val = A::get();
cout << "val: " << val << endl;
}
int main(){
func<T1>();
func<T2>();
return 0;
}
2.2 自动推导decltype
在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的decltype关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:
decltype (表达式)
decltype 是“declare type”的缩写,意思是“声明类型”。decltype的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a+3.14) c = 52.13; // c -> double
decltype(a+b*c) d = 520.1314; // d -> double
可以看到decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。
Tip:表达式是函数调用,使用decltype推导出的类型和函数返回值一致。
decltype(func_int()) a = 0;//推导结果为func_int()的返回值类型
2.3 using的使用
在 C++中可以通过 typedef
重定义一个类型,语法格式如下:
typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;
C++11中规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名,即使用using
。
using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;
通过using
和typedef
的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using
的优势就能凸显出来了,看一下下面的例子:
// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);
// 使用using定义函数指针
using func_ptr1 = int(*)(int, double);
func_ptr其实是一个别名,其本质是一个函数指针,指向的函数返回类型是int,函数参数有两个分别是int,double类型。
使用using
定义函数指针别名的写法看起来就非常直观了,把别名的名字强制分离到了左边,而把别名对应的实际类型放在了右边,比较清晰,可读性比较好。
2.3.1 模板的别名
使用typedef
重定义类似很方便,但是它有一点限制,比如无法重定义一个模板。比如我们需要一个固定以int类型为key的map,它可以和很多类型的value值进行映射,如果使用typedef
这样直接定义就非常麻烦:
typedef map<int, string> m1;
typedef map<int, int> m2;
typedef map<int, double> m3;
在这种情况下我们就不自觉的想到了模板:
template <typename T>
typedef map<int, T> type; // error, 语法错误
在C++11中,新增了一个特性就是可以通过使用using
来为一个模板定义别名,对于上面的需求可以写成这样:
template <typename T>
using mymap = map<int, T>;
2.4 委托构造函数和继承构造函数
2.4.1 委托构造函数
委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化,避免冗余代码。下面举例说明:
#include <iostream>
using namespace std;
class Test{
public:
// 实例化时,可以初始化1个或者两个或者三个变量
Test() {};
Test(int max) {
this->m_max = max > 0 ? max : 100;
}
Test(int max, int min):Test(max) { // 委托定义m_max
this->m_min = min > 0 && min < max ? min : 1;
}
Test(int max, int min, int mid):Test(max, min) { //委托定义m_max,m_min
this->m_middle = mid < max && mid > min ? mid : 50;
}
int m_min;
int m_max;
int m_middle;
};
int main(){
Test t(90, 30, 60);
cout << "min: " << t.m_min << ", middle: "
<< t.m_middle << ", max: " << t.m_max << endl;
return 0;
}
这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
2.4.2 继承构造函数
C++11中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。
继承构造函数的使用方法:通过使用using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。
#include <iostream>
#include <string>
using namespace std;
class Base{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
int m_i;
double m_j;
string m_k;
};
class Child : public Base{
public:
using Base::Base; // 使用基类构造函数来初始化继承的变量
};
int main(){
Child c1(520, 13.14);
cout << "int: " << c1.m_i << ", double: " << c1.m_j << endl;
Child c2(520, 13.14, "i love you");
cout << "int: " << c2.m_i << ", double: "
<< c2.m_j << ", string: " << c2.m_k << endl;
return 0;
}
另外如果在子类中隐藏了父类中的同名函数(也就是重载了父类函数),也可以通过using的方式在子类中使用基类中的这些父类函数:
#include <iostream>
#include <string>
using namespace std;
class Base{
public:
Base(int i) :m_i(i) {}
Base(int i, double j) :m_i(i), m_j(j) {}
Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}
void func(int i) {
cout << "base class: i = " << i << endl;
}
void func(int i, string str) {
cout << "base class: i = " << i << ", str = " << str << endl;
}
int m_i;
double m_j;
string m_k;
};
class Child : public Base{
public:
using Base::Base;
using Base::func; // 使用父类的函数
void func(){
cout << "child class: i'am luffy!!!" << endl;
}
};
int main(){
Child c(250);
c.func(); // child class: i'am luffy!!!
c.func(19); // base class: i = 19
c.func(19, "luffy"); // base class: i = 19, str = luffy
return 0;
子类中的func()函数隐藏了基类中的两个func()因此默认情况下通过子类对象只能调用无参的func(),在上面的子类代码中添加了using Base::func;之后,就可以通过子类对象直接调用父类中被隐藏的带参func()函数了。
2.5 基于范围的for循环
C++11基于范围的for循环,语法格式:
for (declaration : expression){
// 循环体
}
在上面的语法格式中declaration
表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression
是要遍历的对象,它可以是表达式、容器、数组、初始化列表等。
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> t{ 1,2,3,4,5,6 };
for (auto &value : t){ //delaration用引用类型,就不会产生t元素的副本,提升效率
// 并且如果需要在遍历过程中修改元素的值,需要使用引用。
cout << value << " ";
}
cout << endl;
return 0;
}
对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用const
定义保存元素数据的变量,在定义的时候建议使用const auto &
,这样相对于const auto
效率要更高一些。
2.5.1 使用细节
1. 关系型容器
使用基于范围的for循环有一些需要注意的细节,先来看一下对关系型容器map的遍历:
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main(void){
map<int, string> m{
{1, "lucy"},{2, "lily"},{3, "tom"}
};
// 基于范围的for循环方式
for (auto& it : m) {
cout << "id: " << it.first << ", name: " << it.second << endl;
}
// 普通的for循环方式
for (auto it = m.begin(); it != m.end(); ++it) {
cout << "id: " << it->first << ", name: " << it->second << endl;
}
return 0;
}
在上面的例子中使用两种方式对map进行了遍历,通过对比有两点需要注意的事项:
- 使用普通的for循环方式(基于迭代器)遍历关联性容器,
auto
自动推导出的是一个迭代器类型,需要使用迭代器的方式取出元素中的键值对(和指针的操作方法相同):
it->first
it->second - 使用基于范围的for循环遍历关联性容器,
auto
自动推导出的类型是容器中的value_type
,相当于一个对组(std::pair
)对象,提取键值对的方式如下:
it.first
it.second
2. 元素只读
通过对基于范围的for循环语法的介绍可以得知,在for循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值,但是这并不适用于所有的情况,对应set容器
来说,内部元素都是只读的,这是由容器的特性决定的,因此在for循环中auto&
会被视为const auto &
。
#include <iostream>
#include <set>
using namespace std;
int main(void){
set<int> st{ 1,2,3,4,5,6 };
for (auto &item : st) { // auto&会被视为const auto &
cout << item++ << endl; // error, 不能给常量赋值
}
return 0;
}
除此之外,在遍历关联型容器时也会出现同样的问题,基于范围的for循环中,虽然可以得到一个std::pair
引用,但是我们是不能修改里边的first值的,也就是key值。
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main(void){
map<int, string> m{
{1, "lucy"},{2, "lily"},{3, "tom"}
};
for (auto& item : m) {
// item.first 是一个常量
cout << "id: " << item.first++ << ", name: " << item.second << endl; // error
}
return 0;
}
3. 访问次数
基于范围的for循环遍历的对象可以是一个表达式或者容器/数组等。假设我们对一个容器进行遍历,在遍历过程中for循环对这个容器的访问频率是一次还是多次呢?我们通过下面的例子验证一下:
#include <iostream>
#include <vector>
using namespace std;
vector<int> v{ 1,2,3,4,5,6 };
vector<int>& getRange(){
cout << "get vector range..." << endl;
return v;
}
int main(void){
for (auto val : getRange()){
cout << val << " ";
}
cout << endl;
return 0;
}
输出:
get vector range...
1 2 3 4 5 6
对应基于范围的for循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。如果是普通的for循环,在每次迭代的时候都需要判断是否已经到了结束边界。
2.6 lamdda表达式
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:
[capture](params) opt -> ret {body;};
其中capture
是捕获列表,params
是参数列表,opt
是函数选项,ret
是返回值类型,body
是函数体。
1. capture
捕获列表:
- [] - 不捕捉任何变量
- [&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
- [=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
拷贝的副本在匿名函数体内部是只读的 - [=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo,foo是外部变量的名称
- [bar] - 按值捕获 bar 变量, 同时不捕获其他变量
- [&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
- [this] - 捕获当前类中的this指针
- 让lambda表达式拥有和当前类成员函数同样的访问权限
- 如果已经使用了 & 或者 =, 默认添加此选项
#include <iostream>
#include <functional>
using namespace std;
class Test {
public:
void output(int x, int y) {
auto x1 = [] {return m_number; }; // error,没有捕获外部变量
auto x2 = [=] {return m_number + x + y; }; // ok
auto x3 = [&] {return m_number + x + y; }; // ok
auto x4 = [this] {return m_number; }; // ok
auto x5 = [this] {return m_number + x + y; }; // error,捕获this指针,可访问类内部成员,没有捕获到变量x,y,因此不能访问。
auto x6 = [this, x, y] {return m_number + x + y; }; // ok
auto x7 = [this] {return m_number++; }; // ok
}
int m_number = 100;
};
在匿名函数内部,需要通过lambda表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下lambda表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。
2. 参数列表
和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。
auto f = [](){return 1;} // 没有参数, 参数列表为空
auto f = []{return 1;} // 没有参数, 参数列表省略不写
3. opt 选项,
不需要可以省略
- mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
- exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
4. 返回值类型
在C++11中,lambda表达式的返回值是通过返回值后置语法来定义的。
// 完整的lambda表达式定义
auto f = [](int a) -> int{
return a+10;
};
// 忽略返回值的lambda表达式定义,自动推导
auto f = [](int a){
return a+10;
};
一般情况下,不指定lambda表达式的返回值,编译器会根据return
语句自动推导返回值的类型,但需要注意的是labmda表达式不能通过列表初始化自动推导出返回值类型。
// ok,可以自动推导出返回值类型
auto f = [](int i){
return i;
};
// error,不能推导出返回值类型
auto f1 = [](){
return {1, 2}; // 基于列表初始化推导返回值,错误
};
2.6.1 函数本质
被mutable修改是lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性。
int a = 0;
auto f1 = [=] {return a++; }; // error, 按值捕获外部变量, a是只读的
auto f2 = [=]()mutable {return a++; }; // ok
最后再剖析一下为什么通过值拷贝的方式捕获的外部变量是只读的:
lambda表达式的类型在C++11中会被看做是一个带operator()
的类,即仿函数。(仿函数知识可见博客:【C++】STL 第十章)
按照C++标准,lambda表达式的operator()
默认是const
的,一个const
成员函数是无法修改成员变量值的。
mutable选项的作用就在于取消operator()的const属性。
对于没有捕获任何变量的lambda表达式,还可以转换成一个普通的函数指针:
using func_ptr = int(*)(int);
// 没有捕获任何外部变量的匿名函数
func_ptr f = [](int a){
return a;
};
// 函数调用
f(1314);
标签:11,函数,int,auto,特性,C++,using,表达式,构造函数
From: https://blog.csdn.net/qq_50921201/article/details/139665604