网站开发与维护是什么,找公司做网站注意事项,家庭组网方案,wordpress采集网址文章目录 前言一、列表初始化#xff08;不同于初始化列表#xff09;二、initializer_list三、decltype关键字四、nullptr五、右值引用移动拷贝和移动赋值被编译器识别成将亡值的原因 左值引用和右值引用的场景和价值右值引用的场景move函数 六、关于右值引用的功能和属性问… 文章目录 前言一、列表初始化不同于初始化列表二、initializer_list三、decltype关键字四、nullptr五、右值引用移动拷贝和移动赋值被编译器识别成将亡值的原因 左值引用和右值引用的场景和价值右值引用的场景move函数 六、关于右值引用的功能和属性问题完美转发总结 前言 一、列表初始化不同于初始化列表
列表初始化是C11的一个新特性不同于初始化列表。
列表初始化在对自定义类型时会调用它的构造函数。 struct Point
{int _x;int _y;
};int main()
{int x1 1;int x2{ 2 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p{ 1, 2 };// C11中列表初始化也可以适用于new表达式中int* pa new int[4]{ 0 };return 0;
}以前学习到的一般只有单个参数的构造函数支持隐式类型转换现在的列表初始化实际上支持了多参数的构造函数支持隐式类型转换。 在上面的例子中就是{1,2}这个初始化列表会走多参数的构造构造出一个Point临时对象再进行拷贝构造只不过这两步被编译器优化成直接构造。 在构造函数前加上explicit关键字就能禁止编译器优化了。 自定义类型本质上还是调用构造函数。 所以还能这样玩
const Point p1 {1,2};
这也是多参数构造支持隐式类型转换构造的临时对象具有常性用const引用才支持。二、initializer_list initializer_list的基本情况如上用法如下
auto il {1,2,3,4,5};il就是一个intializer_list类型等号右边是一个常量数组空间存在常量区intializer_list的大致结构如下
templateclass T
class intializer_list
{const T* _start;const T* _start;
};_start指向等号右边的常量数组空间的起始地址 _finush指向等号右边的常量空间的末尾元素地址的下一个 所以
auto il {1,2,3,4,5};的本质上还是调用il的构造函数。
有了initializer_list之后各种容器就支持了用initializer_list来构造了。 比如说
vectorint v1 {1,2,3,4,5};本质上是先用等号右边的常量数组构造initializer_list然后vector重载了一个用initializer_list来构造vector的函数再调该函数完成从initializer_list到vector的构造。
还有一个容器map
mapstring,string m1 {{sort,排序},{left,左边}};本质上右边的常量数组会被识别成pair所以先调用pair的构造函数生成一个pairstring,string对象然后再调用initializer_listpairstring,string完成从pair到initializer_list的构造最后map支持了用initializer_list构造所以最后构造出了map。
即 常量数组空间—pair–initializer_list pair --map 三、decltype关键字
decltype是一个关键字能推出变量的类型再定义变量 它还可以作模板参数
int a;
//decltype推导对象类型再定义变量
//还可以作模板参数
decltype(a) a1;
cout typeid(a1).name() endl;
vectordecltype(a) v;四、nullptr
由于C中NULL被定义成字面量0这样就可能回带来一些问题因为0既能指针常量又能表示整形常量。
所以出于清晰和安全的角度考虑C11中新增了nullptr用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif结论nullptr的本质就是 (void*)0 五、右值引用
要知道什么是右值先知道什么是左值。 之前学的引用中一般都是左值引用无论左值引用还是右值引用都是给对象取别名。
简单区分一下左值和右值的区别
左值可以获取地址右值不能获取地址
下面详细讲解
1、下面的是左值对左值的引用
int main()
{
// 以下的p、b、c、*p都是左值
int* p new int(0);
int b 1;
const int c 2;
const int * ps hello c;//字符串能取地址也是左值只不过是不能修改而已
// 以下几个是对上面左值的左值引用
int* rp p;
int rb b;
const int rc c;
int pvalue *p;
//左值可以出现在赋值符号的右边
return 0;
}特点
1左值可以修改但是const修饰的左值就不能修改了左值可以被引用2左值可以出现在赋值符号的左边也可以出现在赋值符号的右边。
2.下面几个都是右值
10
xy
fmin(a,b)特点
1右值不能被修改2右值不能取地址3右值不能出现在赋值符号左边
结论 左值引用是给左值取别名右值引用是给右值取别名。
int main()
{//1.右值引用给右值取别名int a 10;double x 1.1, y 2.2;double rx x y; //虽然xy单独都是左值但是xy表达式的返回值是一个右值。//2.左值引用给左值取别名int b 2;int rb b;//3.左值引用既能给左值取别名也能给右值取别名const int c 10; //const引用能给右值取别名//4.右值引用不能给左值取别名除非move()一下int r1 b; //falseint r1 move(b); //truereturn 0;
}还有两个点
1.左值引用既能给左值取别名也能给右值取别名2.右值引用不能给左值取别名除非move()一下
至于什么是move()下面会讲
移动拷贝和移动赋值
在语法上对于内置类型来说把内置类型的右值称为纯右值 把自定义类型的右值称为将亡值。
看下面的例子
dzt::string fun()
{dzt::string str(xxxxxxxxxxxxxxxxxxxxxx);return str;
}int main()
{dzt::string ret fun();//连续的两次拷贝构造会被编译器优化 成一次深拷贝//...return 0;
}
解析
首先调用fun函数对str进行一次构造在返回str时由于返回值类型是传值返回所以str先构造一个临时对象然后fun函数结束后str销毁了临时对象再进行拷贝构造给ret总的来说就是两次深拷贝。
不过现在编译器会对连续的构造/拷贝构造进行优化优化成一次构造/拷贝构造 因为这样的情况下如果返回string会造成更大的问题在fun函数调用结束时就已经将str的资源释放了然而ret还是str的别名一旦访问就会出现非法访问内存问题。
有了移动构造和移动赋值的出现就能完美解决上面两次深拷贝造成效率下降的问题。
在编译器看来因为str虽然是一个左值但是str构造的临时对象后再返回给ret是一个传值返回且str构造临时对象后自己就会销毁不应该多此一举而是直接将str自己的资源直接给ret。因为str会被编译器识别成将亡值。
所以有了移动构造资源转移情况是这样的
具体的实现
以string为例
string(string s):_str(nullptr)
{cout string(const string s) -- 移动构造 endl;swap(s);
}string operator(string s)
{cout string operator(const string s)-- 移动赋值 endl;swap(s); //现代写法顺便在你要释放的时候把我不要的资源带走return *this;//返回的是s的引用此时s已经拿到了想要的资源且不要的资源在出了该作用域后会被将亡值释放
}被编译器识别成将亡值的原因
这里str会被识别成将亡值的两个前提
1.连续的构造/拷贝构造编译器会直接优化成一次构造/拷贝构造2.函数传值返回
所以本质上正是因为有编译器的优化才会减少拷贝次数大大提高效率。 左值引用和右值引用的场景和价值
使用场景 1.做参数和做返回值都可以提高效率。
左值引用的价值
左值引用的出现是为了减少拷贝提高效率。
右值引用的价值
- 为了进一步减少拷贝弥补左值引用没有解决的场景。
右值引用的场景
比如 场景1
自定义类型中深拷贝的类必须传值返回的场景。上面将的移动构造和移动赋值就是这个场景1
场景2
容器的插入接口如果插入的对象是右值可以利用移动构造将资源转移给数据结构中的对象也可以减少拷贝。
比如
int main()
{listdzt::string lt;dzt::string s1(111111111111111111111111111);lt.push_back(s1);cout endl;dzt::string s2(222222222222222222222222222);lt.push_back(move(s2));cout endl;lt.push_back(33333333333333333333333333333);cout endl;return 0;
}如果没有实现移动构造和移动赋值运行后结果如下 分析
1第一条语句调用构造函数是因为string支持单参数的构造函数能隐式类型转换把字符串常量转换成string。2第二条语句调用拷贝构造函数是因为 list的push_back大致如下 在push_back时会new一个节点而该节点存储的val值就是string -此时就会调string的拷贝构造是一次深拷贝。第三条语句调用构造函数的原因是在string的拷贝构造中需要new一块新的空间此时这里我使用的是 现代写法 现代写法 中会调用一次构造函数。
所以每次插入时都会产生一次深拷贝。
如果有了移动构造和移动赋值情况截然不同 编译器会将构造出的临时对象识别成将亡值直接转移资源从而每次都减少了一次深拷贝次数。
第一种情况没有走移动构造是因为先是进行了隐式类型转换调用构造构造了string然后这个string再插入再在push_back函数中调用拷贝构造这并不是连续的行为所以编译器不会优化也就不会将s1识别成将亡值。
这里要区分左值和右值的原因是
如果一个自定义类型的值不是右值但编译器把它当成右值看待的话那在拷贝构造/赋
值时被拷贝对象的资源就会被抢走。
move函数
move函数的返回值是一个右值属性。 比如
string ret move(str);move(str)的返回值是一个右值但是str本身不会被改变成右值。
六、关于右值引用的功能和属性问题完美转发
模板中的不代表右值引用而是代表万能引用。 万能引用既能接收左值又能接收右值 实参是左值它就是左值引用引用折叠 实参是右值它就是右值引用。
//完美转发
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; }
// 模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
// 但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发templatetypename 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); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}运行结果会超乎预期 原因是右值引用的属性还是左值引用只是功能上是右值引用。
右值引用必须修改否则就无法完成移动构造和移动赋值。
在移动构造和移动赋值中都有这么一条语句s是右值右值绝对是无法传给左值的。如果它不能修改的话这条语句是一定会报错的。
所以为了支持移动构造和移动赋值右值引用变量的属性会被编译器识别成左值。
int main()
{int r 10;r;cout r endl;cout r endl;
}尝试运行这段代码r只是右值引用的别名是能修改的也是有地址的。
上面说了为了支持移动构造和移动赋值编译器会将右值强行识别成左值属性。
可如果存在一些场景就需要右值保持右值属性呢 比如: 在容器的插入函数中val是一个右值如果将它识别成右值会匹配下面的构造可是在swap修改的时候就无法修改。
所以就有一个完美转发的东西能解决。 大致使用方法如下
结合上面的代码在这里就用到了完美转发。 保持了原生对象的属性。
// std::forwardT(t)在传参的过程中保持了t的原生类型属性。
templatetypename T
void PerfectForward(T t)
{
Fun(std::forwardT(t));
}总结
C11前半部分就到这里后半部分会持续更新。