当前位置: 首页> 房产> 建筑 > 百度站长中心_免费的制作网站_seo专业推广_杭州网站关键词排名

百度站长中心_免费的制作网站_seo专业推广_杭州网站关键词排名

时间:2025/8/23 9:23:03来源:https://blog.csdn.net/Talon7/article/details/145918694 浏览次数:0次
百度站长中心_免费的制作网站_seo专业推广_杭州网站关键词排名

C++11新特性

  • 1、统一的列表初始化
    • 1.1、{}初始化
    • 1.2、C++11中的std::initializer_list
  • 2、声明和类型
    • 2.1、auto
    • 2.2、decltype
    • 2.3、nullptr
    • 2.4、using
  • 3、范围for
  • 4、STL中的一些变化
  • 5、右值引用和移动语义
    • 5.1、左值和右值
    • 5.2、左值引用和右值引用
    • 5.3、移动构造和移动赋值
    • 5.4、左值引用和右值引用的价值
    • 5.5、引用折叠和完美转发
    • 5.6、实现list支持push_back的移动拷贝
  • 6、可变参数模板
    • 6.1、基本语法和使用
    • 6.2、empalce系列接口
    • 6.3、实现list支持emplace_back函数
  • 7、新的类功能
  • 8、lambda
    • 8.1、C++98的例子
    • 8.2、lambda表达式语法
    • 8.3、lambda基本使用
    • 8.4、lambda的原理
  • 9、包装器
    • 9.1、function
    • 9.2、bind
  • 10、智能指针
  • 11、线程库

1、统一的列表初始化

1.1、{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

#include <iostream>using namespace std;
struct Point
{Point(int x, int y):_x(x),_y(y){}int _x;int _y;
};int main()
{int arr1[] = { 1, 2, 3, 4, 5 };Point p = { 0, 0 };return 0;
}

C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
1、内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
2、{}初始化的过程中,可以省略掉=。
3、C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/insert多参数构造的对象时,{}初始化会很方便。

// 对于整型
int x = 1;
int y = { 3 };
int z{ 2 };// 对于数组
int a1[] = { 1, 2, 3 };
int a2[]{4,5,6};// 对于自定义类型
Point p1(0, 0);
Point p2 = { 1, 1 };
Point p3{ 2, 2 };// 对于动态申请的空间
int* ptr1 = new int[3] {1, 2, 3};
Point* ptr2 = new Point[2]{ {0, 0}, {1, 1} };

同时需要注意的是:对于p2其实是支持了多参数的构造函数隐式类型转换,本来是先构造一个临时对象然后再拷贝构造p2,这里被编译器优化成直接构造。
一切皆可用{}初始化,并且可以不写=,但是我们日常定义还是写上比较好。


1.2、C++11中的std::initializer_list

先来看看下面两行代码有何不同:

vector<int> v1 = { 1,2,3,4,5 };
Point p1 = {1, 1};

p1就是上面所说的支持多参数构造函数隐式类型转换,如果在Point类构造函数前面加上explicit就不能这么写了,explicit就是用来禁止构造函数隐式类型转换的。
而v1这里跟p1不同,这里是因为调用了initializer_list的vector构造函数。


下面来看看什么是initializer_list:
在这里插入图片描述
initializer_list是C++11新增的一个类,它的实现类似图中,_start指针指向数组的第一个位置,_finish指向最后一个数据的下一个位置,并且支持了迭代器。

auto il = { 10, 20, 30 };

这行代码本质上是调用了initializer_list的构造函数,所以il的类型是initializer_list<int>。我们可以使用typeid.name打印出类型来看看:

cout << typeid(il).name() << endl;

在这里插入图片描述

所以为什么vector可以支持这样初始化,这是因为vector中有个参数为initializer_list<T>的构造函数,如下图:
在这里插入图片描述

在这里插入图片描述
因此v1可以这样初始化是因为有个参数为initializer_list<T>的构造函数,里面大致实现就是先reserve开空间,然后使用迭代器遍历il将数据push_back到vector中,当然也可以调用vector的迭代器区间初始化函数。
p1是支持了多参数构造函数的隐式类型转换,所以本质上调用的是构造函数。本来是先构造一个临时对象,这个临时对象再去拷贝构造p1,对于连续的构造+拷贝构造编译器优化为直接一个构造。


之前我们模拟实现的vector是不支持这种初始化方式的,现在我们可以添加一个这种构造函数:

vector(initializer_list<T> il)
{reserve(il.size());for (auto e : il)push_back(e);
}

添加这个构造函数后就可以支持这种初始化方式了。对于前面我们写的list、map、set等都可以添加这个构造函数。


下面来看看map的使用:

map<string, string> dict = { {"sort", "排序"}, {"left", "左边"} };

这是先支持了initializer_list的构造函数,然后又支持了多参数构造函数的隐式类型转换。


2、声明和类型

2.1、auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int i = 10;
auto p = &i;
auto& r = i;
auto pf = malloc;

auto自动推导类型,可以直接推导指针的类型,但是不能直接推导引用类型,如果要推到引用类型需要加上&。对于指针类型可以加*,也可以不加。


2.2、decltype

现在假设我们需要根据一个变量或者表达式的类型来定义类型,该如何实现呢?
可以直接使用auto吗?是不行的,因为如果使用auto必须定义+初始化,不能只有定义。

int i = 10;
auto p = &i;
auto pf = malloc;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;

在这里插入图片描述
我们利用typeid().name()可以打印出类型,那么可以用这个来定义吗?也是不行的,因为这个函数本质上返回的是一个字符串。
那么就需要使用decltype来解决了:关键字decltype将变量的类型声明为表达式指定的类型。

template<class Func>
class B
{
private:Func _f;
};int main()
{int i = 10;auto p = &i;auto pf = malloc;cout << typeid(p).name() << endl;cout << typeid(pf).name() << endl;decltype(pf) pf2;B<decltype(pf)> b1;const int x = 1;double y = 2.2;B<decltype(x* y)> b2;return 0;
}

上面我们只定义p2不进行初始化,使用auto是无法做到的。使用decltype还可以推导出表达式类型作为类模板参数。


2.3、nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

如下可能会匹配出错:

void func(int)
{cout << "void func(int)" << endl;
}void func(int*)
{cout << "void func(int*)" << endl;
}int main()
{func(NULL);return 0;
}

这时候NULL应该匹配func(int*),但是由于NULL本质上是C语言定义的常量宏,因此匹配了func(int)。


2.4、using

在过去,using是用来展开命名空间,或者展开命名空间中的部分对象/函数。
C++11允许用 using 替代 typedef 定义类型别名,语法更直观且支持模板。

1. 基本类型别名

// 旧写法 (typedef)
typedef int MyInt;
typedef void(*FuncPtr1)(int, double);// C++11 使用 using
using ll = long long;
using FuncPtr2 = void(*)(double, double);

2. 模板类型别名
using 可以直接为模板定义别名,而 typedef 无法直接实现:

template<class T>
using Vec = vector<T>;Vec<int> v; // 等价于 std::vector<int>

3. 对比 typedef 的优势

// 旧式模板别名需要包裹在结构体中
template <typename T>
struct OldVec {typedef std::vector<T> type;
};
OldVec<int>::type oldNumbers;  // 繁琐// 使用 using 更简洁
template <typename T>
using NewVec = std::vector<T>;
NewVec<int> newNumbers;        // 直接使用

3、范围for

范围for搭配auto来使用,非常方便。
在过去我们遍历vector:

vector<int> v = { 1, 2, 3, 4, 5 };
for (size_t i = 0; i < v.size(); i++)
{cout << v[i] << " ";
}
cout << endl;

有了范围for,我们可以搭配auto来使用,如下:

vector<int> v = { 1, 2, 3, 4, 5 };
for (auto e : v)
{cout << e << " ";
}
cout << endl;

范围for的底层就是迭代器,支持了迭代器就能使用范围for。


4、STL中的一些变化

在这里插入图片描述
用红色圈起来的就是C++11新增加的容器,其中最有用的就是unordered_map和unrodered_set。array是定长数组,使用很鸡肋,跟普通数组的区别就是array对于越界的检查非常严格,因为它重载的operator[]里面直接对pos进行断言,但是我不如直接使用vector。forward_list是单向链表,支持单向迭代器,但是插入删除它只支持了在某个数据后面插入/删除,因为在某个数据前面插入/删除效率比较低,所以这个也比较鸡肋,不如直接使用list。

在这里插入图片描述
迭代器这里新增加了4个函数,对应const迭代器,但是begin/end和rbegin/rend也重载了对应const版本,所以四个函数也是有点鸡肋。

在这里插入图片描述
所有容器都支持了{}列表初始化的构造函数。

在这里插入图片描述
新增了右值引用和可变参数模板。

在这里插入图片描述
新增了移动构造和移动赋值,性能获得很大提升。


5、右值引用和移动语义

5.1、左值和右值

左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

int fmin(int a, int b)
{return a < b ? a : b;
}int main()
{// 以下的ptr、b、c、*p都是左值int* ptr = new int(0);int b = 1;const int c = 2;"xxxxxxx";const char* p = "xxxxxxx";p[2];// 以下几个都是常见的右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);return 0;
}

思考:字符串"xxxxxxx"是左值还是右值?
是左值,字符串常量是可以获取到地址的,如下图:

在这里插入图片描述


5.2、左值引用和右值引用

Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。右值引用相比左值引用多一个&。

思考:左值引用能否引用右值?右值引用能否引用左值?
1、左值引用不能直接引用右值,但是const左值引用可以引用右值。
2、右值引用不能直接引用左值,但是右值引用可以引用move(左值)。

double x = 1.1, y = 2.2;
// 左值引用:给左值取别名
int a = 10;
int& r1 = a;// 左值引用能否给右值取别名?
// const左值引用可以
const int& r2 = 20;
const double& r3 = x + y;// 右值引用:给右值取别名
int&& r4 = 10;
double&& r5 = x + y;// 右值引用能否给左值取别名?
// move以后的左值可以
int&& r6 = move(a);

语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。


需要注意的是:变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。

int&& r1 = 10;
r1++;
int&& r2 = move(r1);

r1右值引用字面常量10,虽然r1是右值引用,但是r1的属性还是左值,是可以取到r1的地址的,也可以对r1进行++/--,这时候再用r2去引用r1就不行了,因为r1的属性是左值,而右值是不能引用左值的,因此需要move(r1)才能被r2引用。而const左值引用是不能修改的。
const左值引用可以引用右值,延长对象的声明周期,但不能修改对象。右值引用也可以延长对象的声明周期并且是可以修改对象的。


5.3、移动构造和移动赋值

下面我们将通过string类来进行测试,先给出string类的代码:

namespace zzy
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
zzy::string func()
{zzy::string str("xxxxxxxxxxxxxxxxxxx");// ...return str;
}int main()
{zzy::string ret1 = func();zzy::string ret2;ret2 = func();return 0;
}

在这里插入图片描述


我们画图进行分析,对于ret1不管怎么样最后返回还是要去拷贝构造ret1,然后将str的空间释放。对于ret2来说,最后走的赋值运算符重载也是要用临时对象去拷贝给ret2,然后将临时对象空间释放。如下图:
在这里插入图片描述
屏蔽掉ret1的代码,来看ret2:
对于ret2来说,先用str拷贝构造临时对象然后释放str,接着这个临时对象再去拷贝给ret2,释放这个临时对象。这样需要拷贝两次,消耗是很大的,这里还只是一个string对象,如果是vector<string>,甚至是vector<vector<string>>消耗就会更大。
因此有没有这样一种思路,直接把str那段空间直接转移给ret2,这样就不需要拷贝了。

C++11支持了移动构造和移动赋值,可以减少拷贝,提高性能。
我们在string类中加入如下代码:

string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动拷贝" << endl;swap(s);return *this;
}

再次运行程序查看:
在这里插入图片描述
我们发现走了这里的移动赋值,先用str拷贝构造临时对象,然后这个临时对象是右值,所以走的是移动赋值,直接将临时对象的数据转移给了ret2,因此只发生了一次深拷贝。
在这里插入图片描述
如图,func()返回值是右值,所以ret2=func()走的是移动拷贝,如果ret2=左值,那么走的就是const左值引用的深拷贝了。
另外右值又分为纯右值和将亡值,我们把内置类型的右值称为纯右值,自定义类型的右值称为将亡值。


接着再加入移动构造,屏蔽ret2,来看ret1:

string(string&& s):_str(nullptr)
{cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);
}

在这里插入图片描述
发现只有移动拷贝,这是为什么呢?不应该先拷贝构造一个临时对象,然后这个临时对象再去移动拷贝ret1吗,这样就有一次深拷贝,一次移动拷贝。
根据我们前面的分析,实际上并不需要这样,直接把str的数据转移给ret1就行了,不需要再进行拷贝了,所以这里编译器做了优化。

在这里插入图片描述
这里编译器优化了。按我们原来的理解,应该先拷贝构造一个临时对象,然后再用这个临时对象再去拷贝构造ret1,但是连续的拷贝构造+拷贝构造直接优化成一个拷贝构造,所以只有一次拷贝构造,这是编译器第一个优化。
str被编译器识别成右值-将亡值,这是第二个优化,因此走的是移动构造,直接将数据转移给了ret1。
因此这里存在编译器的两层优化:
1、连续的拷贝构造+拷贝构造优化成一个拷贝构造。
2、编译器把str识别成了右值—将亡值。


下面放开ret2,屏蔽ret1,继续分析:

在这里插入图片描述
这里为移动拷贝+移动赋值。
因为这里是拷贝构造+赋值运算符重载,因此不存在编译器的优化。
编译器优化:连续的构造+拷贝构造优化成一个构造。连续的拷贝构造+拷贝构造优化成一个拷贝构造。
但是str被识别成了将亡值,所以走的是移动拷贝,将数据转移给临时对象,然后临时对象也是右值,再走的移动赋值,将数据转移给ret2,因此不存在拷贝。

因此,有了移动构造+移动赋值,对于上面的两种情况:
1、使用返回值拷贝构造新对象。
2、使用返回值拷贝构造已存在对象。
都不再需要拷贝,直接将数据转移,性能得到很大的提升。


5.4、左值引用和右值引用的价值

左值引用核心价值是减少拷贝,提高效率。如:传参和传引用返回。
右值引用核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,如:传值返回。
下面来看右值引用的场景:

场景1:自定义类型中深拷贝的类,必须传值返回的场景。
在这里插入图片描述
下面测试std::string:

std::string ret3("1111111111111111111111111111");
std::string copy1 = ret3;
move(ret3);
std::string copy2 = ret3; 
std::string copy3 = move(ret3); 

思考:copy1-3分别是什么拷贝?
分析:ret3是左值,因此copy1是拷贝构造。copy2还是拷贝构造。而copy3是移动构造。
所以我们可以推测,move底层实现应该是将ret3转换成右值返回,move的返回值是右值。

下面来看链表的插入:

list<zzy::string> lt;
zzy::string s1("1111111111111111111");
lt.push_back(s1);cout << endl << endl;
zzy::string s2("1111111111111111111");
lt.push_back(move(s2));cout << endl << endl;
lt.push_back("2222222222222222222");

分析这三种情况:
在这里插入图片描述
第一种情况:先构造一个对象s1,由于s1是左值,因此是拷贝构造。
第二种情况:先构造对象s2,move(s2)是右值,所以走的是移动构造。
第三种情况:先构造一个临时对象,由于临时对象是右值,所以走的是右值引用的push_back,所以走的还是移动构造。

场景2:容器的插入接口,如果插入的对象是右值,可以利用移动构造转移资源给数据结构中的对象。


5.5、引用折叠和完美转发

看下这段代码会输出什么:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}int main()
{PerfectForward(10); int a;PerfectForward(a); PerfectForward(std::move(a)); const int b = 8;PerfectForward(b);PerfectForward(std::move(b)); return 0;
}

输出结果如图:
在这里插入图片描述
首先,模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。实参左值,他就是左值引用(引用折叠),实参右值,他就是右值引用。
那么也就是说这里的t可以把所有类型都接受了,那输出应该是:右值引用、左值引用、右值引用、const左值引用、const右值引用。
那为什么结果都是左值引用呢?

这是因为对于10和move(a)传过去t的类型为右值引用,但是右值引用的本身属性是左值,也就是t是可以获取到地址的,所以t的类型虽然是右值引用,但是它本身属性是左值,所以再向下传递给下一个函数时匹配的就是左值引用。b是const int对象,传过去实例化的是const int&,move(b)传过去就是const int&&,虽然最后都退化为左值了,但是可以看到const属性并没有丢失。
变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要用完美转发实现。

完美转发forward:
在这里插入图片描述
完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现。

修改函数代码:

Fun(forward<T>(t));

再次运行查看结果:
在这里插入图片描述


最后再来看看右值引用的属性为什么是左值:
在这里插入图片描述
当我们走移动构造时,s的类型为右值引用,需要将s的数据和当前对象进行交换,所以需要调用swap函数,而swap函数是左值引用,因此s本身的属性必须是左值,否则无法调用swap完成资源转移。这也是为什么右值引用变量的属性会被编译器识别为左值。


5.6、实现list支持push_back的移动拷贝

首先push_back需要重载一个能支持右值引用的函数,而push_back中又复用了insert,所以insert也需要支持。

void push_back(const T&& x)
{insert(end(), forward<T>(x));
}iterator insert(iterator pos, const T&& x)
{Node* newnode = new Node(forward<T>(x));Node* cur = pos._node;Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return newnode;
}

并且注意:只要往下一层传,就需要完美转发,保持它原来右值属性,否则在下一层就会匹配左值了。
由于insert还调用了节点的构造函数,因此节点构造我们也要支持:

list_node(T&& val):_prev(nullptr), _next(nullptr), _val(forward<T>(val))
{}

节点这里初始化列表也需要完美转发,然后往下传就是string的移动构造了。


在这里插入图片描述
那么这里的T&&反正是万能引用,既能匹配左值也能匹配右值,那么我们是不是可以不用实现上面的那个函数了?是的,可以不实现,但是需要给构造函数加上模板,如下图:
在这里插入图片描述
同时没有了默认构造,所以我们哨兵位头节点的初始化需要给值:
在这里插入图片描述
运行后查看结果:
在这里插入图片描述


6、可变参数模板

6.1、基本语法和使用

在过去,我们也遇到过可变参数,例如printf函数的使用:
在这里插入图片描述

printf("%d", x);
printf("%d %d", x, y);

printf函数可以接受我们传入的多个参数。这里的可变参数实现原理是类似开了一个数组存起来。


下面我们要看的是可变参数模板:

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

Args是一个模板参数包,args是一个函数形参参数包。
声明一个参数包Args… args,这个参数包中可以包含0到任意个模板参数。

我们可以通过以下方式计算出有多少个参数:
在这里插入图片描述


但是不能通过数组的方式去遍历,如果要遍历,需要使用下面的方式:

template<class T>
void ShowList(T t)
{cout << t << " ";cout << endl;
}template<class T, class ...Args>
void ShowList(T t, Args... args)
{cout << t << " ";ShowList(args...);
}template <class ...Args>
void CppPrint(Args... args)
{ShowList(args...);
}
int main()
{CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 3);return 0;
}

说明:这里CppPrint(1)将1传给args,然后args继续往下传给ShowList,由于只有一个参数,所以走的是更匹配的模板参数只有T的函数。CppPrint(1, 2),将1和2往下传给ShowList,匹配的是T和Args两个模板参数的ShowList,先输出1,然后将2继续往下传递,而这时候只有2一个参数,所以走的是只有T的模板函数。三个参数也是类似上述过程。
第一个ShowList函数相当于是作为结束条件的函数。


但是这样写的话,如果不传参就会报错,因为没有匹配的无参函数,所以可以这么写:

void ShowList()
{cout << endl;
}template<class T, class ...Args>
void ShowList(T t, Args... args)
{cout << t << " ";ShowList(args...);
}template <class ...Args>
void CppPrint(Args... args)
{ShowList(args...);
}
int main()
{CppPrint();CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 3);return 0;
}

这么写不传参也可以调用。


还有更加“变态”的写法:

template <class T>
void PrintArg(T t)
{cout << t << " ";
}//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{int a[] = { (PrintArg(args), 0)... };cout << endl;
}int main()
{CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 2.2);CppPrint(1, 2, 2.2, string("xxxx"));return 0;
}

分析如下:
在这里插入图片描述
下面这么写会好一点:

template <class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{int a[] = { PrintArg(args)... };cout << endl;
}int main()
{CppPrint(1);CppPrint(1, 2);CppPrint(1, 2, 2.2);CppPrint(1, 2, 2.2, string("xxxx"));return 0;
}

下面来看它的一个使用场景:

class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date构造" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date拷贝构造" << endl;}private:int _year;int _month;int _day;
};template <class ...Args>
Date* Create(Args... args)
{Date* ret = new Date(args...);return ret;
}int main()
{Date* p1 = Create();Date* p2 = Create(2023);Date* p3 = Create(2023, 9);Date* p4 = Create(2023, 9, 27);Date d(2023, 1, 1);Date* p5 = Create(d);return 0;
}

这里有了可变模板参数,并且构造函数都给了缺省值,构造就很好用了。
p1-p4你传值就用所给的值去构造,不传就用缺省值。p5这里传的是对象,那么就走拷贝构造。因此这里可变参数的意义在于:你传日期就走日期的构造,你传对象就走拷贝构造。


6.2、empalce系列接口

C++11给容器接口新增了emplace系列接口:
在这里插入图片描述
这里截出来的是list,其他的vector、map、set也是支持的。
push_back这里支持了万能引用的函数,那么对于右值就可以完美转发往下层走,这样可以实现移动构造,提高性能。
emplace系列函数支持了可变模板参数,我们来看看:

// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });

接下来我们把数据类型换成之前给的string和Date,再来看看:

// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, zzy::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.push_back(make_pair(30, "sort"));std::list<Date> lt;
Date d(2023, 9, 27);
// 只能传日期类对象
lt.push_back(d);// 传日期类对象
// 传日期类对象的参数包
// 参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象
lt.emplace_back(d);
lt.emplace_back(2023, 9, 27);

在这里插入图片描述
可以看到,调用emplace_back,由于有了可变模板参数,所以直接将参数包一路往下传,一直到string的构造函数,直接去构造。而调用push_back,需要先构造临时对象,然后右值引用延长临时对象生命周期,然后在push_back中完美转发往下层传,最后走的移动构造。
一个是直接构造,另一个是先构造再移动构造,所以其实差别并不大。

对于内置类型,先构造对象再去push_back需要走构造+拷贝构造,而利用emplace_back只需要走构造即可。


6.3、实现list支持emplace_back函数

template<class... Args>
void emplace_back(Args&&... args)
{Node* newnode = new Node(forward<Args>(args)...);Node* tail = _head->_prev;newnode->_prev = tail;tail->_next = newnode;newnode->_next = _head;_head->_prev = newnode;++_size;
}template<class... Args>
list_node(Args&&... args): _prev(nullptr), _next(nullptr), _val(forward<Args>(args)...)
{}

在这里插入图片描述
测试我们自己的代码,发现跟库里的是一样的。
上面打叉部分是因为我们自己实现的多了哨兵位头节点,所以多出来一些构造。

这里可变参数得加万能引用,因为emplace_back可能是多个参数,也可能是一个对象,如果是一个对象,我们要去调用移动拷贝,需要完美转发。


7、新的类功能

原来C++类中,有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载,最重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会会动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}/*Person(const Person& p):_name(p._name),_age(p._age){}*//*Person& operator=(const Person& p){if(this != &p){_name = p._name;_age = p._age;}return *this;}*///~Person()//{}private:zzy::string _name;int _age;
};int main()
{	Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}

如上:没有实现移动构造和移动赋值,且没有实现构造、赋值、析构,因此编译器会自动生成移动构造和移动赋值。运行结果如图:
在这里插入图片描述
对于左值进行深拷贝,对于右值进行移动拷贝。这里的_name是自定义类型,所以会去走它的移动构造和移动赋值。

当我们把析构函数放开,再次运行:
在这里插入图片描述
因为析构函数实现了,所以不会自动生成移动构造和移动赋值(必须构造、赋值、析构都没有实现才会自动生成)。那么这里走的就是深拷贝了。

但是这里是有办法可以让他强制生成的,如下:

Person(Person&&) = default;
Person& operator=(Person&&) = default;

在这里插入图片描述

但是其实也没必要,像这里虽然_name是自定义类型,但是里面的析构、构造、赋值函数都实现了,不需要我们操心了,所以这里Person类的析构、构造、赋值直接用默认的就行,移动构造和移动赋值也是一样的。而当我们需要写析构的时候,那么构造、赋值我们也就需要写。所以可以看到它们都是一起出现的。


8、lambda

8.1、C++98的例子

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

struct Goods
{string _name;  // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };

现在我们需要对自定义类型商品进行排序,我们可以在商品类中重载operator<和operator>,但是这样只能实现对某一个数据如价格进行排序,如果我们还想对评价进行排序就不行了。
所以我们可以类似下面实现仿函数,然后将仿函数对象传给sort使用:

struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
sort(v.begin(), v.end(), ComparePriceLess());     // 价格升序
sort(v.begin(), v.end(), ComparePriceGreater());  // 价格降序

但是上面的写法还是太复杂了,每次为了实现一个排序算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
有了lambda表达式,我们就可以这么写:

// 评价的降序
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {return gl._evaluate > gr._evaluate; }); 

8.2、lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明:
1、[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
2、(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
3、mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
4、->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
5、{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

捕获列表说明:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var。
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)。
[&var]:表示引用传递捕捉变量var。
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)。
[this]:表示值传递方式捕捉当前的this指针。


8.3、lambda基本使用

int a = 1, b = 2;
auto add1 = [](int x, int y)->int {return x + y; };
auto add2 = [](int x, int y) {return x + y; };
cout << add1(a, b) << endl;  // 3
cout << add2(a, b) << endl;  // 3

使用auto来自动推导lambda表达式的类型,使用方式如上。
并且返回值可以省略,编译器可以根据你的返回值自动推导类型。
add1和add2功能是相同的,但是它们是两个不同的类型,因此如果直接add1=add2进行赋值是不行的。


现在我有个rate变量,需要将x+y的值乘上rate返回,但是我不想把rate作为参数传过去怎么办?

double rate = 2.55;
auto add3 = [rate](int x, int y) {return (x + y) * rate; };
cout << add3(a, b) << endl;  // 7.65

如上,使用捕捉列表,在[]写上你要捕捉的变量,需要注意的是,这样写捕捉的变量是const的,所以不能在函数体内对其修改,并且这样捕捉实际上是拷贝,捕捉的是父作用域中的变量。


下面来实现交换两个变量的值:

int a = 1, b = 2;
auto swap1 = [add1](int& x, int& y) {int tmp = x;x = y;y = tmp;cout << add1(x, y) << endl; // 3func(); // func函数是全局的,不需要捕捉可以直接调用。
};
swap1(a, b);
cout << a << " " << b << endl;  // 2 1

函数体内可以有多条语句,并且可以把add1捕捉,然后在函数体内调用这个函数。
需要注意的是:当前所在函数内的变量、对象等需要捕捉才能使用,如果是全局的函数则可以直接调用。

这是一种实现方式,通过引用传参交换,还可以使用引用捕捉的方式:

int a = 1, b = 2;
auto swap2 = [&a, &b] {int tmp = a;a = b;b = tmp;
};
swap2();
cout << a << " " << b << endl;  // 2 1

引用捕捉的写法如上,如果不加&就是复制捕捉,由于没有参数所以可以省略(),也没有返回值可以省略->returntype。


再来看mutable的作用:

auto swap2 = [a]() mutable {a++;
};

当我们不加&时,这里就是复制捕捉,但是a是const属性的,不可修改,如果想要在函数体内修改a,需要在参数列表后面加上mutable。


如果我们需要捕捉很多变量或对象,就需要一个一个写,但是这样太麻烦了,所以有了下面的方式:

int a = 1, b = 2;
double rate = 2.55;
auto test = [=]() mutable {cout << a << " " << b << " " << rate << endl;
};
test();  // 输出1 2 2.55

=捕捉,捕捉的是所有父作用域的变量,注意是复制捕捉。我们也可以用&捕捉,这样就是引用捕捉父作用域的所有变量:

int a = 1, b = 2;
double rate = 2.55;
auto add1 = [](int x, int y)->int {return x + y; };
auto test = [&]() mutable {a++, b++, rate++;cout << add1(a, b) << endl; // 5
};
test();
cout << a << " " << b << " " << rate << endl;  // 2 3 3.55
auto test = [&, a]() mutable {a++, b++, rate++;cout << add1(a, b) << endl; // 5
};
test();
cout << a << " " << b << " " << rate << endl; // 1 3 3.55

这样是引用捕捉父作用域所有变量除了a,所以外面打印a的值还是不变的。


8.4、lambda的原理

来看看使用仿函数和lambda的区别:

class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}private:double _rate;
};int main()
{double rate = 0.66;Rate r1(rate);r1(10000, 2);// lambdaauto r2 = [rate](int money, int year)->double {return money * rate * year; };r2(10000, 2);return 0;
}

可以看到,使用仿函数需要定义对象,然后再去调用operator(),而直接使用lambda就方便很多了。
下面我们再看看汇编代码:
在这里插入图片描述
在这里插入图片描述
可以看到,lambda也是去调用了operator(),所以lambda的底层就是仿函数。


9、包装器

9.1、function

template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}double f(double i)
{return i / 2;
}struct Functor
{double operator()(double d){return d / 3;}
};int main()
{// 函数指针cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lambda表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

上面的f调用对象可能是什么?
可能是函数指针、仿函数、lambda表达式,运行程序,我们发现count定义了三份,每份count的地址都不一样。
现在我们想让F类型统一,也就是不管是函数指针、仿函数还是lambda都实例化出同一个类型F,该如何解决呢?

包装器可以很好的解决上面的问题:
在这里插入图片描述
包装器function包含于头文件<functional>,Ret表示的是调用函数的返回值,Args…表示的是被调用函数的形参。
使用方式如下:

function<double(double)> f1 = f;
function<double(double)> f2 = Functor();
function<double(double)> f3 = [](double d)->double { return d / 4; };
useF(f1, 11.11);
useF(f2, 11.11);
useF(f3, 11.11);

上面我们用包装器将函数指针、仿函数、lambda表达式包装成f1、f2、f3,它们类型是相同的,然后再去调用useF函数,运行后发现只定义了一个count,且打印输出的地址是一样的。

我们还可以这样使用:

vector<function<double(double)>> v = { f, Functor(), [](double d)->double { return d / 4; } };
for (auto& e : v)
{useF(e, 11.11);
}

将它们都放到vector里面去,原来它们的类型是函数指针、仿函数、lambda,原来是不可能一起存到vector里面的,因为它们类型不同,但是经过包装器包装后它们的类型相同,就可以放到一起了。


9.2、bind

int Sub(int a, int b)
{return a - b;
}

现在有个Sub函数,正常我们传入x、y,那么计算的返回结果就是x-y。现在我想让他们反过来,比如传入10和5,原来计算返回结果为10-5=5,现在我想反过来计算5-10=-5,该如何实现呢?

需要使用bind:bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。 bind可以用来调整参数个数和参数顺序。bind也在<functional>这个头文件中。

function<int(int, int)> sub1 = bind(Sub, placeholders::_1, placeholders::_2);
cout << sub1(10, 5) << endl;  // 5function<int(int, int)> sub2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << sub2(10, 5) << endl;  // -5

通过bind绑定,这里的placeholders::_1表示的是第一个参数,placeholders::_2表示第二个参数,看下图分析:
在这里插入图片描述
placeholders实际上是一个命名空间,这里了解一下用法即可。
首先对于bind里面的参数,传参给Sub函数是位置匹配的,不管你是placeholders::_x,都是按顺序传给Sub函数的。
然后看调用subx的传参,10必定传给placeholders::_1,5必定传给placeholders::_2。通过控制placeholders::_1和placeholders::_2的顺序来调整参数的顺序。如果_1在前_2在后那么传过去就是10和5,然后再传给Sub就是10和5。如果_2在前_1在后,传过去就是5和10,传给Sub也就是5和10。


下面有一个加法函数,需要乘上利率,但是利率我不想传参,并且我想实现灵活的利率,也可以使用bind来实现:

double Plus(int a, int b, double rate)
{return (a + b) * rate;
}function<double(int, int)> f1 = bind(Plus, placeholders::_1, placeholders::_2, 4.0);
function<double(int, int)> f2 = bind(Plus, placeholders::_1, placeholders::_2, 4.2);
function<double(int, int)> f3 = bind(Plus, placeholders::_1, placeholders::_2, 4.4);
cout << f1(5, 3) << endl;
cout << f2(5, 3) << endl;
cout << f3(5, 3) << endl;

下面调整参数固定参数位置,发现它们还是使用的placeholders::_1和placeholders::_2,而不是placeholders::_2和placeholders::_3。

double Plus(double rate, int a, int b)
{return (a + b) * rate;
}function<double(int, int)> f1 = bind(Plus, 4.0, placeholders::_1, placeholders::_2);
function<double(int, int)> f2 = bind(Plus, 4.2, placeholders::_1, placeholders::_2);
function<double(int, int)> f3 = bind(Plus, 4.4, placeholders::_1, placeholders::_2);
cout << f1(5, 3) << endl;
cout << f2(5, 3) << endl;
cout << f3(5, 3) << endl;

类内静态成员函数的绑定:

class SubType
{
public:static int sub1(int a, int b){return a - b;}
};function<int(int, int)> f1 = bind(SubType::sub1, placeholders::_1, placeholders::_2);
function<int(int, int)> f2 = bind(&SubType::sub1, placeholders::_1, placeholders::_2);
cout << f1(10, 5) << endl;
cout << f2(10, 5) << endl;

类内静态成员函数绑定要指明类域,也可以在前面加上&,上面两种写法相同。


类内非静态成员函数的绑定:

class SubType
{
public:static int sub1(int a, int b){return a - b;}int sub2(int a, int b, int rate){return (a - b) * rate;}
};
SubType st;
function<int(int, int)> f3 = bind(&SubType::sub2, st, placeholders::_1, placeholders::_2, 4.0);
function<int(int, int)> f4 = bind(&SubType::sub2, SubType(), placeholders::_1, placeholders::_2, 4.2);
function<int(int, int)> f5 = bind(&SubType::sub2, &st, placeholders::_1, placeholders::_2, 4.4);
cout << f3(5, 3) << endl;
cout << f4(5, 3) << endl;
cout << f5(5, 3) << endl;

类内非静态成员函数的绑定第一个参数必须取地址,第二参数需要传对象,第二个参数可以加&,也可以不加。


10、智能指针

篇幅有限,下一篇文章继续介绍。


11、线程库

篇幅有限,下一篇文章继续介绍。

关键字:百度站长中心_免费的制作网站_seo专业推广_杭州网站关键词排名

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: