一、列表统一初始化
1.1 { } 初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Ponit
{
int x;
int y;
};
//一切都可以用列表初始化
int main()
{
int i = 0;
int j = {0};
int z{0};
int array1[]{1,2,3,4,5}
Ponit p{1 , 2};
//我们之前写的Date类也能使用列表初始化
//按理来说只能这样初始化
//构造
Date d1(2024,5,1);
//隐式类型转化
//构造+拷贝构造-》优化
Date d2 = {2024,5,2};
Date d3{2024 , 5 ,3};
}
//要一次性构造多个Date对象时
Date* p1 = new Date[3]{d1 ,d2 ,d3};
Date* p1 = new Date[3]{{2024,5,1} ,{2024,5,2} ,{2024,5,3}};
1.2 std::initializer_list类型
在C++库中vector和list容器还有这样的用法
int main()
{
vector<int> v1 = {1,2,3,4};
list<int> l1 = {10,20,30,40};
}
这个格式看起来很眼熟跟我们上面写到的Date用初始化列表初始化格式很像
但这两个不是一个概念
int main()
{
vector<int> v1 = {1,2,3,4};
list<int> l1 = {10,20,30,40};
//多参数构造类型转化 构造+拷贝构造-》优化直接构造
//跟类对应参数个数匹配,个数不一样就报错
Date d2 = {2024,5,1}
}
vector和list用到C++11中新增的类型:initializer_list
上面vector和list格式用到了新增的initializer_list构造
int main()
{
//默认情况下C++会把列表默认识别为initializer_list对象
auto il = { 1, 2, 3, 4, 5 };
cout << typeid(il).name() << endl;
initializer_list<int> il1 = { 1, 2, 3, 4, 5 };
return 0;
}
只能是同类型
我们这里可以大概描绘以下initializer_list是怎么工作的,
initializer_list会找一个常量区把这个数组存下来
我们构造出来的initializer_list对象中存放着两个指针:假设叫begin和end,他俩指向数组起始和结束位置的下一个位置
initializer_list还支持以下函数接口
有了begin和end想必大家应该要知道这意味着,initializer_list支持迭代器遍历
int main()
{
initializer_list<int> il1 = { 1, 2, 3, 4, 5 };
initializer_list<int>::iterator it = il1.begin();
while(it != il1.end())
{
cout<< *it << endl;
++it;
}
cout<<endl;
return 0;
}
库里面的vector和list都是支持initializer_list这种格式构造的但是之前我们写的myvector不支持
要写应该initializer_list的构造函数即可
//额外添加的initializer_list构造函数
vector(initializer_list<T> lt)
{
reserve(lt.size());
for (auto& e : lt)
{
push_back(e);
}
}
map的initializer_list使用方法
pair<string, string> kv1("sort", "排序");
//调map的initializer_list构造函数
//隐式类型转换成pair类型
map<string, string> dict = { kv1,{"insert", "插入"} , {"get","获取"} }
for (auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
二、声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
2.1 auto
2.2 decltype
大致意思就是推导类型
关键字decltype将变量的类型声明为表达式指定的类型
他和区别就是typeid(变量名).name()生成的只是把类型以字符串的方式打印出来
而decltype是能推到对象类型,而且这个类型是可以直接用的
用来实例化模板,或者再定义对象
三、新容器
STL的变化:
- 新容器
- 新的构造, initializer_list的构造
- 移动构造移动赋值
- 右值引用版本的插入
- 其他一些不太重要的(不实用)的接口增加。
1.新容器
下图中圈住的就是C++11新增容器
比较有用的就是无序map和set
array:
c语言没有办法对数组越界能做应该很好的检查只会抽查。
array一旦出现越界的读写都会检查出来,因为array底层用的是operator[ ]。
不过我们一般会使用vector代替。
forward_list:单链表
节省空间。
四、右值引用和移动语义
1、右值引用和左值引用
右值引用和右值引用都是给对象取别名
左值可以出现在赋值符号左边,右值不能出现在赋值符号左边
可以取地址的都是左值,const修饰过的左值,还能取地址,所以还是左值。
左值:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;//常变量
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值和右值引用:
右值不能取地址
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等
右值引用就是对右值的引用,给右值取别名。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左值引用和右值引用的比较
左值引用不能给右值取别名,但是const左值引用可以。(权限的放大和缩小)
int mian()
{
int i = 0;
int j = 1;
//const左值引用右值
const int& r1 = 10;
const int& r2 = i+j;
//右值引用
int&& rr1 = 10;
int&& rr2 = i + j;
}
move调用
右值引用同样不能给左值取别名
但是右值引用能给move后的左值取别名
int main()
{
int i = 9;
int&& rr1 = move(i);//变量i是左值
}
2.右值和左值引用的使用场景
2.1左值引用使用场景
做参数和返回值可以提高效率,不用而外构造
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
bit::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
2.2左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:**bit::string to_string(int value)**函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
to_string函数内的ret出了作用域就销毁,
用右值引用也不行,
ret对象是无法拯救的,不过ret对象指向的资源是可以拯救的
这里右值引用可以间接的解决这个问题。这里就要用到右值引用中的移动构造和移动赋值的概念
3.移动构造和移动赋值
在我们自己写的string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
string的移动构造:
//移动构造
string(string && s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
C++为了描述更形象一些把右值分为了纯右值和将亡值
1.纯右值(内置类型右值)
2.将亡值(自定义类型右值)
有移动构造和没有移动构造时的区别:
没有移动构造:
会构造两块空间,析构两块空间代价很大。
有移动构造:
把ret识别为一个将亡值
直接把让临时对象指向ret原本指向的空间
不过这里ret2= goat::to_string(1234);
时还是会产生拷贝
解决上面赋值拷贝时多开一个空间的方法就是用移动赋值
移动赋值:
string$ operator=(string&& s)
{
swap(s);
return *this;
}
编译器优化:
补充结论:右值被右值引用引用后的属性是左值还是右值?
是左值
右值不能直接修改,但是右值被右值引用以后需要被修改否则无法实现移动构造和移动赋值
4.C++ —— swap深拷贝
5.move
move能把左值修改为右值,但是不是修改左值本身只是返回值是右值。
五、万能引用
函数模板+右值引用
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); //const 左值
PerfectForward(std::move(b)); //const 右值
}
我们预期输出的应该是每个函数调用右边注释的输出类型
但是实际输出确不一样
这也是因为我们传右值之后t
的类型是右值引用的引用,我们上面说过右值被右值引用引用后的类型是左值。
解决方法:【完美转发】
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
};
六、类的新功能
1.默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
-
移动构造的限制条件:
如果你没有自己实现移动构造函数,且没有实现析构函数 +拷贝构造+拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
1.1 default
如果想要实现自己实现了例如析构函数后,还要编译器生成默认移动构造我们可以使用default强制生成。//强制编译器生成 list(T&& t) = default; list(const T& t1) = default;
1.2 delete强制不能生成默认构造(用法同default)
-
移动赋值重载的限定条件:
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 +拷贝构造+拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似) -
注意:如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2.继承和多态中的final和override
详情见多态和继承文章
七、函数模板可变参数
7.1模板参数概念
模板参数包Arg
是一个模板参数包,可以包含任意多个模板参数
args
是函数形参参数包,和Arg一样包含任意多个函数参数。
template<class ...Arg>
void ShowList(Arg ...args)
{}
int main()
{
ShowList(1);
ShowList(1, 'A');//可以是不同类型
}
7.2可变参数的个数计算:
template<class ...Arg>
void ShowList(Arg... args)
{
cout<<sizeof...(args)<<endl;
}
7.3 参数包解析
void _ShowList()
{
cout<<endl;
}
//编译时递归推演
template<class T,class ...Arg>
void _ShowList(const T& val,Arg... args)
{
cout << val<<" ";
_ShowList(args...);
}
template<class ...Arg>
void ShowList(Arg... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');//可以是不同类型
ShowList(1, "hellow", 2.3, '1');
}
利用第一个模板参数val,依次解析获取参数值
八、emplace
8.1emplace_back和push_back
我们发现emplace_back传的是可变参数还是万能引用(见上文)
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
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' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
emplace_back和push_back除了用法上,没什么区别。