欢迎光临
我们一直在努力

深入理解C++系列(20)——C++11(下)

⭐️博主: 此生决int-@CSDN博客

 速胜派就是最大的投降派!!!

         🔥热门专栏🔥

      深入理解 C++ 系列 | 算法系列

      快速复习系列 | Java 速通系列


文章目录

    • 上期回顾
  • C++11
    • 4. 可变参数模板
      • 4.1 基本语法及原理
      • 4.2 包扩展
      • 4.3 emplace 系列接口
    • 5. 新的类功能
      • 5.1 默认的移动构造和移动赋值
      • 5.2 成员变量声明时给缺省值
      • 5.3 default 和 delete
      • 5.4 final 与 override
    • 6. STL 中一些变化
    • 7. lambda
      • 7.1 lambda 表达式语法
      • 7.2 捕捉列表
      • 7.3 lambda 的应用
      • 7.4 lambda 的原理
    • 8. 包装器
      • 8.1 function
      • 8.2 bind
    • 下期预告
    • 异常
    • 结语

上期回顾

上一篇我们主要学习了 C++11的1至3小节的内容,即C++11的历史,以及C++11引入了左值和右值的概念,以及引申出来的左值引用和右值引用,移动构造和移动赋值等等,那么我们今天接着上一节的内容,继续来学习C++11的一些新特性!

C++11

4. 可变参数模板

可变参数模板就是:

参数数量和参数类型都可以不同。

一个最简单的例子

template<class... Args>
void Func(Args... args)
{
}
//调用
Func();
Func(1);
Func(1, 2);
Func(1, 2, 3);
Func(1, 2, 3, 4);
//全部都可以,

4.1 基本语法及原理

  • C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板。可变数目的参数被称为参数包,存在两种参数包:
    • 模板参数包:表示零或多个模板参数。
    • 函数参数包:表示零或多个函数参数。

template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}

  • 我们用省略号来指出一个模板参数或函数参数表示一个包。在模板参数列表中,class… 或 typename… 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟 … 指出接下来表示零或多个形参对象列表。函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 这里我们可以使用 sizeof… 运算符去计算参数包中参数的个数。
  • 简单来讲就是:

你可以把Args… args看作Args args1 ,Args args2,Args args3,…Args argsn,连这里的n都是不确定的,那么,到底要怎么用呢?那就要用到包扩展!

4.2 包扩展

  • 对于一个参数包,我们除了能计算它的参数个数,我们能做的唯一的事情就是扩展它。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。
  • C++ 还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。

void Print()
{
}

template<class T, class... Args>
void Print(T t, Args... args)
{
cout << t << endl;

Print(args...);//这样实现包扩展
}

调用:

Print(1, 2, 3, 4);

void Print()
{
}

template<class T, class... Args>
void Print(T t, Args... args)//第一次1,2,3,4传过来,1,对应T t,Args… args为Args args2到Args args4,然后传到下面也是Args… args为Args args2到Args args4,
{
cout << t << endl;

Print(args...);//这样实现包扩展
}

那我们这个可变模板参数有什么用呢?看看下面的emplace接口和insert等接口的区别吧!

4.3 emplace 系列接口

即,push_back就有emplace_back,insert就有emplace, 1,功能与insert,push_back等一样 2,不一样的地方,:emplace系列重载了可变参数模版 那么,他就可以

//对于一个date类
v.push_back(Date(2026, 9, 17));
/*
如果有移动构造,那么过程就是:Date临时对象

移动

vector内部
*/

//而emplace
v.emplace_back(2026, 9, 17);
//直接在vector上用传进来的参数直接进行构造!
//把构造 `Date` 所需要的参数直接传给 `vector`,然后在 vector 内部直接构造 `Date`。

template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);

不过要注意:

不能简单地认为 emplace_back 永远比 push_back 快。

如果你已经有一个 Date d:

v.emplace_back(d);

它仍然需要从 d 构造 vector 中的对象,通常并不会凭空消除拷贝。

真正明显体现 emplace_back 优势的是:

v.emplace_back(2026, 9, 17);

这种直接传构造参数的场景。

  • C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是 emplace 还支持新玩法:假设容器为 container<T>,emplace 还支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
  • emplace_back 总体而言是更高效,推荐以后使用 emplace 系列替代 insert 和 push 系列。
  • 传递参数包过程中,如果是 Args&&… args 的参数包,要用完美转发参数包,方式如下:std::forward<Args>(args)…,否则编译时包扩展后右值引用变量表达式就变成了左值。

5. 新的类功能

5.1 默认的移动构造和移动赋值

  • 原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 / const 取地址重载。最重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
  • 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。(与移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。 财哥总结: 目前C++类的默认函数变为了6个,相比之前多了移动构造和移动赋值,这两个函数, 如果自己没有实现拷贝构造、析构函数、拷贝赋值重载,那么编译器会自己生成,否则,要自己写,所以,以后,如果自己要写拷贝,析构,operator=,那么,现在还要加上移动构造和移动赋值

5.2 成员变量声明时给缺省值

非静态成员变量在声明时可以直接给缺省值。 成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化。这个在类和对象部分已经讲过了。

class Date
{
public:
Date()
{
_year = 2026;
_month = 1;
_day = 1;
}

private:
int _year;//C++11一前,就是这样写
int _month;
int _day;
//成员变量声明时给缺省值就是
/* int _year = 2026;
int _month = 1;
int _day = 1;
*/

};

5.3 default 和 delete

简单来说, default:强制生成默认构造函数

class A
{
public:
A(int a=a)//就是我自己实现了构造函数但是我还想让编译器也生成一个默认的构造,(因为我显示写了构造编译器是不会生成默认构造的)
:a(a)...
A() = default;//所以,请编译器生成默认构造函数
};

class A
{
public:
A(const A&) = delete;//禁止拷贝构造。
};
void Func(int);
void Func(double) = delete;//那么,func就只能传int而不能传double
Func(10); // OK
Func(3.14); // ❌

delete:这个函数我明确禁止使用。

  • C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
  • 如果能想要限制某些默认函数的生成,在 C++98 中,是将该函数设置成 private,并且只声明不定义,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。

5.4 final 与 override

这个在继承和多态章节已经进行了详细讲解。 这里简单再讲一下: final ,不允许继续重写/继承。

override: 告诉编译器并检查:这个函数必须重写父类虚函数。

关键字作用你可以怎么理解
default 默认生成函数 “让编译器按默认规则给我生成”
delete 禁止函数使用 “这个函数不允许调用”
override 检查重写 “我明确要重写父类虚函数”
final 禁止继续重写/继承 “到这里结束”

6. STL 中一些变化

新增的容器: 在这里插入图片描述

  • STL 中的新容器,实际最有用的是 unordered_map 和 unordered_set。这两个前面已经进行了非常详细的讲解,其他的大家了解一下即可。
  • STL 中容器的新接口也不少,最重要的就是右值引用和移动语义相关的 push / insert / emplace 系列接口和移动构造和移动赋值,还有 initializer_list 版本的构造等。还有一些无关痛痒的如 cbegin / cend 等,需要时查查文档即可。
  • 容器的范围 for 遍历,这个在容器部分也讲过了。

总结就是:

auto
范围 for
nullptr
unordered_map
unordered_set
array
forward_list
以及 STL 对**移动语义、右值引用、emplace**等特性的利用。

知道这些有什么用呢?现在不是都能用吗?不!比如你参加蓝桥杯的时候,如果没有选C++11的编译器,那么,你就不能用这些哦!

7. lambda

7.1 lambda 表达式语法

Lambda 是一种在代码当前位置定义匿名函数的方式。

最经典的例子就是排序:

sort(v.begin(), v.end(),
[](int x, int y)
{
return x > y;
});

[] 捕获列表
(int a, int b) 参数列表
-> int 返回值类型
{ ... } 函数体

lambda底层还是仿函数 普通的仿函数是这样用的

class Greater
{
public:
bool operator()(int x, int y)
{
return x > y;
}
};

sort(v.begin(), v.end(), Greater());

Lambda 可以赋值给变量

auto Add = [](int a, int b)
{
return a + b;
};
cout << Add(10, 20) << endl;

  • lambda 表达式本质是一个匿名函数对象,底层就是仿函数,跟普通函数不同的是它可以定义在函数内部。lambda 表达式语法使用层面而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象。
  • lambda 表达式的格式:
部分说明
[capture-list] 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数。捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
(parameters) 参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 () 一起省略。
-> return type 返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{ function body } 函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

7.2 捕捉列表

  • lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
  • 第一种捕捉方式:在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y, &z] 表示 x 和 y 值捕捉,z 引用捕捉。
  • 第二种捕捉方式:在捕捉列表中隐式捕捉,我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了哪些变量,编译器就会自动捕捉那些变量。
  • 第三种捕捉方式:在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x] 表示其他变量隐式值捕捉,x 引用捕捉;[&, x, y] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
  • lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
  • 默认情况下,lambda 捕捉列表是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改。mutable 加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。 例子:

int x = 10;
auto f = [x]()
{
cout << x << endl;
};
x = 20;
f();//输出10,外面的 `x` 后面改成 20,不影响 Lambda 内部保存的那份数据

总结

[x] // 只值捕获 x
[&x] // 只引用捕获 x

[=] // 默认值捕获
[&] // 默认引用捕获

[=, &x] // 默认值捕获,但 x 按引用捕获
[&, x] // 默认引用捕获,但 x 按值捕获

注意: [=] 和 [&] 并不是把“所有局部变量”无条件捕获,而是按需捕获 Lambda 表达式中实际使用到的外部变量。

7.3 lambda 的应用

  • 学习 lambda 表达式之前,我们使用的可调用对象只有函数指针和仿函数对象。函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
  • lambda 在很多其他地方用起来也很好用,比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等。lambda 的应用还是很广泛的。

7.4 lambda 的原理

  • lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象。也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
  • 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同。lambda 参数 / 返回类型 / 函数体就是仿函数 operator() 的参数 / 返回类型 / 函数体(注释1),lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参。当然隐式捕捉,编译器要看使用了哪些就传哪些对象。 注释1,那句话的意思大致就是,下面的lambda里面的x相当于仿函数Lambda里面的x,y对应y,返回值对应operator()的返回值

int x = 10;

auto f = [x](int y)
{
return x + y;
};
//可以想象成
class Lambda
{
public:
Lambda(int x)
: _x(x)
{
}
int operator()(int y)
{
return _x + y;
}
private:
int _x;
};

8. 包装器

8.1 function

一个通用的可调用对象包装器。 它可以包装:普通函数、函数指针、Lambda、仿函数、bind对象、成员函数等 例如:

//普通函数
int Add(int a, int b)
{
return a + b;
}
//lambda
function<int(int, int)> f =
[](int a, int b)
{
return a + b;
};
//仿函数
struct Add
{
int operator()(int a, int b)
{
return a + b;
}
};
//因为上面的都是int(int,int),所以可以
function<int(int, int)> f = Add();
//然后直接调用
f(10, 20);

template <class T>
class function; // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;

  • std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可调用对象,包括函数指针、仿函数、lambda、bind 表达式等。存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
  • 以上是 function 的原型,它被定义在 <functional> 头文件中。参考:std::function – cppreference.com
  • 函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function 的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。下面的第二个代码样例展示了 std::function 作为 map 的参数,实现字符串和可调用对象的映射表功能。

8.2 bind

把函数的一部分参数提前固定下来,生成一个新的可调用对象。 例如:

int Sub(int a, int b)
{
return a b;
}
//正常
Sub(10, 3);
//但是如果我们想要固定第一个参数为10,只需传b,就可以
auto f = bind(Sub, 10, placeholders::_1);
//_1就表示传过来的第一个参数

int Add(int a, int b)
{
return a + b;
}

auto f = bind(Add,
placeholders::_2,
placeholders::_1);

怎么理解? 在这里插入图片描述

// simple (1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

  • bind 是一个函数模板,它也是一个可调用对象的包装器,可以把它看做一个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。bind 可以用来调整参数个数和参数顺序。bind 也在 <functional> 这个头文件中。
  • 调用 bind 的一般形式:auto newCallable = bind(callable, arg_list);,其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。
  • arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1 / _2 / _3… 这些占位符放到 placeholders 的一个命名空间中。

下期预告

异常

结语

  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。   也欢迎订阅我的 深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++ 算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线 快速复习系列:知识梳理、查漏补缺,考前冲刺必备 Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


  愿每一次敲下键盘,都比昨天更进一步!

  愿每一行代码落下,都让未来多一种可能!

赞(0)
未经允许不得转载:171主机测评 » 深入理解C++系列(20)——C++11(下)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址