文章目录
- C++11
-
- 什么是C++11
- 新特性
-
- {}初始化
-
- 简单实例
- {}的类型
- auto
-
- 简单实例
- decltype
- nullptr
- 范围for
- 左右值引用
-
- 左值和右值是什么
- 左值和右值的区分
- 左右值引用
- 左右值引用的作用
- 移动语义
-
- 场景
- move
- 完美转发
- default、delete、final、override
- 可变参数模板
-
- 递归展开
- 逗号表达式展开
- emplace_back和push_back
- STL增加的容器
- lambda表达式
-
- lambda参数解释
-
- 捕捉列表
- 参数列表
- mutable
- ->返回值类型
- {函数体}
- 关于省略
- 包装器
-
- 用法
- bind
-
- 绑定普通函数
- 绑定成员函数
- 总结
C++11 什么是C++11 C++11标准是 ISO/IEC 14882:2011 - Information technology – Programming languages – C++ 的简称 [1] 。
C++11标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)于2011年8月12日公布 [2] ,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于C++11标准的草案(仅编辑上的修正)。此次标准为C++98发布后13年来第一次重大修正。–百度百科
翻译一下,2011年发布的C++标准,对C++98的一次重大修正。新特性 下面的新特性有{}初始化,auto,declytype,nullptr,范围for,STL的变化,左右值引用,lambda表达式,包装器
智能指针和线程库之后补充。{}初始化
C++11提供了{}初始化的方法。
简单实例
int main()
{
//{}的简单使用
using namespace std;
int arr1[] = { 1,2,3,4,5 };
int arr2[]{ 1,2,3,4,5 };
pair(1, 1);
pair{1, 1};
pair* pr = new pair[2]{ {"sort",1} ,{"good",2} };
return 0;
}
文章图片
{}的类型 {}在C++11里面是一个类型,initializer_list,initializer_list - C++ Reference (cplusplus.com),也叫初始化列表。
template
class initializer_list;
文章图片
initializer_list提供了迭代器,所以可以遍历列表内的元素。以vector为例,里面提供了这种类型的构造函数,所以vector < int > v{1,2,3}这种用法就是在调用构造函数
文章图片
我们可以想想可以怎么实现这个功能,遍历initializer_list的元素,挨个赋值给vector容器即可。(没看库里怎么实现的,不过是阐述一种可行的思路
文章图片
STL库中的大多容器都增加了initializer_list相关的构造函数
用初始化列表可以加强类型安全。比如int a=1.2,最后a存储的是1,这就是类型收窄。如果初始化列表的使用导致了类型收窄那编译器就会报错,由此也加强了类型安全,所以建议使用初始化列表初始化。auto
C++98是有auto的,不过在C++11被废弃了,我们现在的int a,就是C++98的auto int aauto是一个存储类型的说明符,作用是推导类型。
简单实例
int main()
{
auto a = 0;
auto b = 1L;
auto c = 1.1;
std::vector::string>::iterator it;
auto d = it;
return 0;
}
//如果有一个类型很长比如std::vectorstring_v;
还要去写它的迭代器就更麻烦了,这种情况下就可以用auto去代替
文章图片
auto一定程度的降低了程序的可读性,并且我们不应该滥用auto.
滥用auto看起来更方便,实际上会造成很多问题,比如难以维护,再比如decltype
string str="ccl"; auto s=str; s.size(); //上面的代码是合理的,但是当我们有一天将string改为const char*时,虽然auto方面没有问题,但是依赖于string类型的代码全部出问题了。
将变量的类型指定为一个表达式的类型
文章图片
typeid(z).name返回的是一个字符串,所以肯定不能拿其返回值当类型了nullptr
nullptr是一个对象,nullptr的类型是nullptr_t,并不等同于((void*)0),nullptr可以隐式的转为任意类型的指针,同时nullptr不能取地址。此外由于nullptr有类型所以是可以捕捉异常的。
文章图片
范围for//网上找的一种可能的实现 struct nullptr_t { void operator&() const = delete; template
inline operator T*() const { return 0; } template inline operator T C::*() const { return 0; } }; nullptr_t nullptr;
遍历容器内的所有元素,如果要修改需要加引用,本质上就是迭代器的遍历。
文章图片
VS监视小技巧左右值引用
文章图片
左值和右值是什么 左值就是可以取地址的,右值就是不可以取地址的。
我们经常可以知道一些关于左右值的判断,而很少听到其真正的定义的一个原因是很难归纳,而且就算归纳了,也需要大量的解释。–《深入理解C++11》右值由两个概念构成,一个是将亡值,一个是纯右值,也有人说右值的别名就是将亡值,因为生动形象,这里我们采用后一种说法。
也有人把左右值定义为,左值可以读写,右值只能读不能写。左值和右值的区分
- 赋值表达式中,左边的全是左值,右边的不一定都是右值,但是右值一定在右边。听起来有点绕
int main()
{
int i = 1;
int j = i;
return 0;
}
文章图片
比如a=b+c,&(b+c)编译时报错,即(b+c)的结果是一个临时变量不能取地址,所以b+c是右值,a是左值。
- 将亡值,听名字就知道是一个即将死亡的值,换句话说,生命周期即将结束的值。比如一些临时变量的值,一些字面值,lambda表达式,都可以看做右值(将亡值)。
一些临时变量,比如一些函数的返回值(返回值是左值引用的另说),1+3,“123”+“456”,lambda表达式,函数sum(1,2)的返回值,这些都是右值,将亡值的概念用在这就十分合适,用完就销毁了。
单个变量的引用算左值左右值引用
- 左值引用,接收左值的引用+const引用
const引用可以理解为接收一个临时的左值,const左值引用只能取地址不能赋值,const左值引用既能接收左值也能接收右值,算是一个例外。move(左值)的作用是把这个左值转为一个右值,但是也可能带来一些问题,比如数据丢失。
- 右值引用,T&&,两个&是为了区分是左值引用还是右值引用
int main()
{
int a = 1;
//左值引用
int& b = a;
const int& x = 1;
//const引用是例外
int& c = 5;
//err //右值引用
int&& m = 1;
int&& n = a;
//err
int&& p = move(a);
return 0;
}
文章图片
左右值引用也符合精准匹配原则,哪个最符合参数类型就会选择哪个函数。左右值引用的作用 看到这,大概对左右值有了一个大概的认识。左值引用我们之前就知道,作用是可以减少拷贝,一定程度上替代了指针(指针有些地方是无法替代的),右值引用的作用是实现移动语义和完美转发。
右值引用后被存储到一个特定的位置,如下所示
文章图片
移动语义:减少拷贝,提高效率
完美转发:保持原来的值属性不变。比如传参,左值传过去还是左值,右值传过去还是右值,利用这一点也可以提高程序的性能,比如减少拷贝。
简单来说,右值引用提高了程序的性能。(在某些场景下提升很大)
移动语义 假如现在需要一个一次性对象A来拷贝构造另一个生命周期更长的对象B,一次性对象A用完就销毁了,但是拷贝构造B前还是得去构造这个一次性对象A,是不是觉得有点浪费。
反正这个一次性对象A的作用也是拷贝出另一个对象B,反正一次性对象A用完就要销毁了,那我们有没有一种办法把A的资源“偷走”给拷贝出来的对象B,这样不就可以减少一次拷贝构造。
移动语义可以解决这个问题。
场景 看下面的场景
class String
{
public:
String() = default;
String(const char* string)
{
printf("构造\n");
_size = strlen(string);
_string = new char[_size];
memcpy(_string, string, _size);
}
String(const String& other)
{
printf("深拷贝\n");
_size = other._size;
_string = new char[_size];
memcpy(_string, other._string, _size);
}
~String()
{
delete[] _string;
}
void Print()
{
for (size_t i = 0;
i < _size;
i++)
{
printf("%c", _string[i]);
}
printf("\n");
}
private:
char* _string;
size_t _size;
};
class Entity
{
public:
Entity(const String& name)//可以看出构造出来的就只在这里用了
:_name(name)//拷贝构造
{
printf("构造Entity\n");
}
void PrintName()
{
_name.Print();
}
private:
String _name;
};
int main()
{
Entity entity("ccl");
entity.PrintName();
return 0;
}
场景中打印entity对象的名字就得需要先构造一个临时对象name,再拿临时对象name去拷贝构造出Entity类的成员_name,这就是两次深拷贝。
文章图片
运行结果也印证了深拷贝两次。
文章图片
那移动语义怎么解决这个问题呢?
或者说移动语义怎么做到偷走它的资源的呢。思路上就是把要用的指针指向本该销毁的内存,再把一次性对象的指针置空,之后一次性对象生命周期结束调用析构函数也不过是delete nullptr,这是合法的。
具体代码可以这样做:
String增加一个移动构造,Entity的构造也增加一个右值的版本由于匹配右值“ccl”
看看具体的更改:
文章图片
文章图片
代码汇总:
class String
{
public:
String() = default;
String(const char* string)
{
printf("构造\n");
_size = strlen(string);
_string = new char[_size];
memcpy(_string, string, _size);
}
String(const String& other)
{
printf("深拷贝\n");
_size = other._size;
_string = new char[_size];
memcpy(_string, other._string, _size);
}
String(String&& other) noexcept//提供一个移动构造的函数 参数是右值
{
_string = other._string;
_size = other._size;
other._string = nullptr;
//必须处理这个临时的字符串,不然两个指针指向同一块内存
other._size = 0;
}
~String()
{
delete[] _string;
}
void Print()
{
for (size_t i = 0;
i < _size;
i++)
{
printf("%c", _string[i]);
}
printf("\n");
}
private:
char* _string;
size_t _size;
};
class Entity
{
public:
Entity(const String& name)//可以看出构造出来的就只在这里用了
:_name(name)//拷贝构造
{
printf("构造Entity\n");
}
Entity(String&& name)
:_name(std::move(name))//move可以把左值转换为右值,用于匹配String的移动构造
{
printf("移动构造Entity\n");
}
void PrintName()
{
_name.Print();
}
private:
String _name;
};
int main()
{
Entity entity("ccl");
entity.PrintName();
return 0;
}
再来看看结果
文章图片
比对一下增加移动拷贝前后的结果
文章图片
随笔记录:编译器优化,如果存在连续的构造等就会进行优化。最后,移动语义本质上允许我们移动对象,换句话说你可以吧一个已经存在的变量转成临时变量,你可以从这个特定的变量中窃取资源,也就达到了提高性能的目的。
- 为什么不直接进行优化?当没有对象接收时如果不产生临时变量会出问题,拿到的就是随机值
- 利用将亡值的特点提高效率,接管即将销毁的空间,减少了一次深拷贝构造
- 不优化的情况下,不管是左值还是右值都是两次构造,只是移动构造效率比左值效率要高不少
- 一个深拷贝的类可以进一步实现移动拷贝,面对一些函数值返回的场景(返回的值是会借助临时变量,临时变量是右值),可以进一步的减少深拷贝,提升效率。
- 很多旧容器都增加了一些右值引用相关的接口提升效率。比如vector
文章图片
move move的作用就是将左值转为右值。
上面通过拷贝构造窃取了资源,那假如我们想通过赋值来直接窃取资源呢?重载=即可。
注意点:处理旧数据,不能窃取自己的资源,防止一块内存被析构两次
String& operator=(String&& other)
{
if (this != &other)//窃取的不是自己的资源
{
//处理旧数据,防止内存泄漏
delete[] _string;
//窃取资源
_string = other._string;
_size = other._size;
//防止一块内存被析构两次
other._string = nullptr;
other._size = 0;
return *this;
}
}
调用时得用move转成右值取匹配赋值的右值版本。
文章图片
用move时得注意可能把数据转走了,毕竟是把左值转成了右值去使用,要是资源被窃取走了还去用就会造成一下问题。完美转发 模板里的&&叫做万能引用,而不是右值引用。
void Test(int&)
{
cout << "左值引用" << endl;
}
void Test(const int&)
{
cout << "const 左值引用" << endl;
}
void Test(int&&)
{
cout << "右值引用" << endl;
}
void Test(const int&&)
{
cout << "const 右值引用" << endl;
}
template
void PerfectForward(T&& t)//万能引用
{
Test(t);
//万能引用右值接受后变成左值
}
int main()
{
int a = 1;
const int b = 1;
PerfectForward(a);
PerfectForward(b);
PerfectForward(1);
return 0;
}
文章图片
万能引用改变了值的左右值属性,该怎么保留之前的属性呢,此时就需要std::forward完美转发保留传参过程中对象的原生类型属性。
使用万能引用
template
void PerfectForward(T&& t)//万能引用
{
Test(std::forward(t));
}
运行结果,发现右值保留了其属性。
文章图片
C++11的类里面新增了两个默认成员函数,即移动构造函数和移动赋值运算符重载。default、delete、final、override
默认的移动构造对于内置类型逐字节拷贝,自定义类型调用其移动构造,若没有实现移动构造就调用其拷贝构造。
- 移动构造函数:自己没有实现移动构造且没有实现析构、拷贝构造、拷贝赋值重载才会生成默认的移动构造。
默认的移动赋值对于内置类型逐字节拷贝,自定义类型调用其移动赋值,若没有实现移动构造就调用其拷贝赋值。
- 移动赋值重载函数:自己没有实现且没有实现析构、拷贝构造、拷贝赋值重载会生成默认的移动赋值。
成员函数加上=default表示强制生成默认的函数。
成员函数加上=delete表示删除这个函数,表现为只声明不实现。
final,被final修饰的类不能继承,修饰的变量成为常量且经初始化后不能改变,修饰的虚函数子类无法重写。
override:写在函数参数列表后面表示这个函数重写了父类的虚函数,编译器就会去检查是否重写了,如果没有重写就会报错。
可变参数模板
可变参数就是参数的个数和类型都是任意的。
最典型的可变参数的使用就是printf了,不在乎有几个参数,反正都能接收。下面就是一个可变参数的函数模板。
文章图片
除了这个外,还可以想想Linux里的命令,后面可以加好几个修饰符,参数的个数类型等都是不定的。
template//...表示模板的个数是不固定的
void ShowArguList(Args...agrs)
{}
我们把…成为参数包,我们无法直接获取参数包里的具体参数,需要一点特别的办法来展开参数包获取具体的参数,下面列举两种方法:递归式和逗号表达式展开
可以拿到每个参数的值,自然也可以拿到值的类型。递归展开
template
void ShowArguList(T value)//递归终止
{
cout << value << endl;
}
template//...表示模板的个数是不固定的
void ShowArguList(T value,Args...args)
{
cout << value << " ";
ShowArguList(args...);
}int main()//调用
{
ShowArguList(1);
ShowArguList(2, 'b');
ShowArguList(3, 'c', 3.33);
ShowArguList(4, 'd', "hello");
return 0;
}
文章图片
逗号表达式展开
template
void ShowArguList(T value)
{
cout << value <<" ";
}
template
void ShowArguList(Args ... args)
{
int arr[] = { (ShowArguList(args),0)... };
cout << endl;
}
int main()
{
/*ShowArguList(1);
*/
ShowArguList(2, 'b');
ShowArguList(3, 'c', 3.33);
ShowArguList(4, 'd', "hello");
return 0;
}
文章图片
逗号表达式那一行可以理解为一种语法,理解不了就记emplace_back和push_back C++11之后就有了一系列的emplace函数,以STL中list的emplace_back(尾插)为例。
(ShowArguList(args),0)…是C++17里的折叠表达式(fold expression),有兴趣可以百度。
文章图片
可以看到参数列表是可变参数和万能引用。直接给结论:emplace_back的效率确实比push_back高一点。
一般对于传过来的参数都是就地构造(不需要移动或者拷贝),而push_back需要先构造出来再拷贝,如果传的是右值且存在移动构造,那emplace_back比push_back高效一点(可以少进行一次移动构造,详情可以参考官方文档的实例std::list::emplace_back - cppreference.com)
总的来说,插入右值且存在移动拷贝的情况下其实两者效率中emplace_back小优,但是差不了太多,因为只节约了一次移动构造。我的VS2022对push_back还进行了优化,下面是vs2022对push_back的优化。。。STL增加的容器
文章图片
下面是我碰到没解决的一个问题,编译器是VS2022,背地里做了太多优化,在此也希望大家不必陷入这种语法细节的泥沼!
文章图片
文章图片
可以看出加了四个容器。
- array,这个容器可以放越界,因为重载了operator[],越界直接报错,这也算是一个优点了…
原生数组的越界的检查得看编译器,有时检查的到有时检查不到,有点玄学
- forward_list,单链表,list也是链表,不过底层是双向循环链表。所以forward_list的好处就是每个元素少存储了一个指针,少四个字节,一百万个元素也就是四百万个字节,也就是4M左右。也算是优点吧。。。此外forward_list没有提供尾插尾删,单链表尾插尾删要找尾,效率不高,所以没有提供相应的接口。
- unordered_map和unordered_set就比较有用了,传送门unordered_set与set的比较
lambda表达式可以看做是一次性的匿名的仿函数(对我们匿名,编译器内部会给他一个名字)
关于lambda表达式的场景,大多能用函数指针的地方都能用lambda替代。此外lambda也是有类型的,可以用auto接收。
这里简单提一下可调用类型没学lambda之前给一个数组排序:
类型定义的对象可以像函数一样去调用
- 什么是可调用类型?
函数指针,仿函数,lambda,包装器。
- 可调用类型包含哪几种?
下面是lambda表达式的实例,说明lambda是有类型的,但是类型的字符串是编译器内部给的,这也是为什么说对于我们来说是"匿名"的,对于编译器来说不是匿名的原因。
文章图片
文章图片
学lambda之后给一个数组排序:
文章图片
个人认为最大的好处是不用取名了,程序的可读性也有很大提升,别人一看就知道是升序还是降序官方文档:Lambda expressions (since C++11) - cppreference.com
微软:C++ 中的 Lambda 表达式 | Microsoft Docs
官方文档已经讲得很详细了,这里给一下常用的用法。
微软给的图:[capature-list](parameters)mutable->return-type {statement}
文章图片
[捕捉列表](参数列表)->返回值类型{函数体}lambda参数解释 捕捉列表 捕捉列表,捕捉列表能够捕捉上文中的变量,编译器根据[]来判断下面的代码是否是lambda表达式,且只能捕捉父作用域 。
捕捉列表使得lambda十分灵活,捕捉列表使得lambda可以拿到外部的变量。
- []:什么都不捕捉,代表不能访问局部变量,只能访问全局变量。
文章图片
- [=]:当前函数栈帧内的所有变量以按值传递的方式捕捉,不能捕捉全局变量,但是可以用。
文章图片
不能捕捉全局变量:
文章图片
- [&]:当前函数栈帧内的所有变量以按值传递的方式捕捉,不能捕捉全局变量,但是可以用。
文章图片
- [var]:以按值传递的方式捕捉变量var
- [&var]:以引用的方式捕捉变量var
文章图片
文章图片
简单来说,捕捉列表可以拿到上文数据,全局变量可以随便用(读写都行),捕捉主要针对的是局部变量,值传递的变量可读不能写(有const属性),引用捕捉的变量可读可写。
具体的多用用就会了。
参数列表 和函数的一样
[](int a, int b) {return a < b;
};
//(int a, int b)就是参数列表
mutable 取消lambda函数的const属性,一般来说lambda都是一个const函数,mutable可以取消其常量属性,但是用了mutable参数列表就不能省略
auto f = [=, &c]()mutable {c = 4, cout << b << " " << c << endl;
};
->返回值类型
auto f = [=, &c]()->int {c = 4, cout << b << " " << c << endl;
return 0;
};
//->int表示返回值为int
可以省略,省略后编译器自动推导,但是写了的话必须就必须和参数列表一起。
{函数体} 这个就不用多说了吧。
关于省略
int main()
{
[] {};
[]() {};
[]() ->int {return 0;
};
[]() mutable->int {return 0;
};
return 0;
}
文章图片
要是写lambda经常报错就给他写全了,看个人习惯,我的习惯是写全,这样可读性好一点。
lambda的实现还是仿函数,就像范围for的本质也不过是编译器,只不过编译器帮我们把很多事做了。
随笔记录:static在函数结束后还在包装器
std::string析构时把size和capacity都置为0了
C++11之后STL库里的swap和algorithm里面的效率差不多了,因为有了右值引用。
const静态成员可以给缺省值(静态成员直接给缺省值是不行的)
文章图片
function - C++ Reference (cplusplus.com)
文章图片
function包装器,也叫适配器,本质上是类模板。
头文件是functional。
用法 function<返回值(参数列表)>包装器的名字=
文章图片
你可能在想,这不就是给函数起个名字吗,有什么大不了的。function因为是一个类型,所以可以玩出很多花样,比如下面的
但是!function是一个类啊,类即类型,自定义类型就可以和STL容器等等交互,那场景一下就大了很多。
int main()
{
std::map>m
{ {"+",[](int a,int b)->int {return a + b;
}},
{"-",[](int a,int b)->int {return a - b;
}},
{"*",[](int a,int b)->int {return a * b;
}},
{"/",[](int a,int b)->int {return a / b;
}}
};
cout << m["+"](1, 2) << endl;
cout << m["-"](1, 2) << endl;
cout << m["*"](1, 2) << endl;
cout << m["/"](1, 2) << endl;
return 0;
}
文章图片
bind
文章图片
本质上是一个函数模板,不过未作探究
直接看用法吧
绑定普通函数
文章图片
固定(绑定)参数的值
int Sum(double a, double b)
{
return a + b;
}
struct Sum2
{
int operator()(double a, double b)
{
return a + b;
}
};
int main()
{
using namespace std::placeholders;
auto func_sum2 = std::bind(Sum, 5, _2);
cout << func_sum2(1, 2) << endl;
return 0;
}
文章图片
bind也可以调换参数的顺序,我暂时没发现使用场景…bind也可以绑定函数模板、成员函数等等…
绑定成员函数 绑定非静态的成员函树需要传this指针,用法如下
class Plus
{
public:
int Sum(int a, int b)
{
return a + b;
}
};
int main()
{
using namespace std::placeholders;
Plus plus;
auto Plus_Sum = std::bind(&Plus::Sum, &plus, _1, _2);
cout << Plus_Sum(1, 2) << endl;
return 0;
}
文章图片
C++11线程库的之后会单写一篇记录。总结
- C++11增加了{}初始化的方式,某种程度上加强了类型安全
- auto推导类型,decltype拿到类型。
- nullptr不是指针,而是一个有类型的对象,范围for本质也不过是迭代器。
- 左右值引用里的右值引用用于移动构造,提高效率。移动构造本质上是浅拷贝,一句话概括就是接管即将销毁的资源,也可以说”偷资源“
- 给类生成默认的函数版本,删除某个成员函数(合理的有声明无定义)。
- 可变参数模板拿到任意个参数,有两种方式进行解包。
- STL增加了一些容器,比如unordered_map,unordered_set,某些场景下比map/set性能更优秀。
- lambda可以看做是一个一次性的函数,用法很灵活。
- 包装器可以包装函数,仿函数,lambda,给他们一个名字,由于包装器是一个类型,所以可以和一些容器用在一起实现一些花哨的操作,包装器可以与bind一起用,bind可以用来绑定数据。
推荐阅读
- C++|C++中四种类型转换方式__笔记
- 前端学习笔记|3.8 JavaScript-DOM节点
- 小白学java|室友打了俩把LOL,我知道了类是怎样加载的
- python|真香啊,手把手教你使用 Python 获取基金信息
- python|考生都难哭了,用 Python 分析了一下,这里才是高考地狱级难度
- Python|Python编程题每日一练day1(附答案)
- 2021SC@SDUSC|seccomp实现安全判题沙箱
- Java小案例|Java小案例(台灯类Lamp,有开关on这个方法...)
- java|lamda表达式是啥(是如何来的呢?如何快速理解lamda表达式)