目录
一、统一的列表初始化
二、声明
1.auto
2.decltype
3.nullptr
三、范围for
四、STL中的变化
五、右值引用和移动语义(重点)
在c++11之前,我们能用{}初始化数组和结构体
struct Point {
int x;
int y;
};
int main()
{
int a[] = { 1,2,3,4 };
Point p = { 1,1 };
return 0;
}
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 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4] { 0 };
return 0;
}
//创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date x(2023, 11, 30);
Date y = { 2023,11,30 };
Date z{ 2023,11,30 };
return 0;
}
这些功能的实现和initializer_list这个容器有关
使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器增加了std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值
int main()
{
vectorv = { 0,1,2,3,4 };
listl = { 2,3,5,6,30 };
mapmp = { {"string","字符串"} ,{"sort","排序"} };
for (auto x : v)
{
cout << x << " ";
}
cout << endl;
for (auto x : l)
{
cout << x << " ";
}
cout << endl;
for (auto x : mp)
{
cout << x.first << ":" << x.second << endl;
}
v = { 1,2,34,5,6 };
for (auto x : v)
{
cout << x << " ";
}
cout << endl;
return 0;
}
这个{}初始化和赋值不难实现,我就拿之前写过的模拟实现的vector来举个例子,这里放关键的函数,如果对vector的模拟实现感兴趣可以去看C++入门篇8,里面有完整模拟实现代码
int main()
{
int x = 1;
double y = 2.2;
decltype(x * y) ret = x * y;//这个用法和auto没啥区别
vectorv;//这里只能用decltype
//vectorv,错误写法
return 0;
}
也就是说,当我们需要类型作为参数时,只能用decltype
这个也不多说,因为C++官方将NULL定义为了0,所以加了一个nullptr表示空指针
底层就是迭代器遍历容器。
int main()
{
vectorv{ 1,2,3,4,5,6 };
for (auto& e : v)//范围for
cout << e << " ";
return 0;
}
多了静态数组、单链表和哈希表,还有一些接口,如cbegin、cend、emplace等
大致说说这些容器的情况:静态数组array比较鸡肋,因为vector完全够用,forward_list单链表也作用不大,也就是比较省空间,哈希表还是很有用的
我们之前学的引用又被叫做左值引用,其实不管是什么引用,都是给对象取别名
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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& pval = *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 main()
{
double&& x = 1.1;
const double&& y = 1.2;
x = 1.3;
y = 1.5;//错误
return 0;
}
那么左值引用能引用右值吗?右值引用能引用左值吗?
int main()
{
//左值引用可以引用右值,但要加const修饰,本质是权限的放大问题
//int& t1 = 1;//不行
const int& t2 = 1;
//右值引用无法引用左值
int x = 0;
//int&& rx = x;
//const int&& rx = x;
int&& rx = move(x);//但可以引用被move以后的左值
return 0;
}
总结:
1.左值引用只能引用左值,但加上const的左值引用既能引用左值,也能引用右值
2.右值引用只能引用右值,但是右值引用可以引用被move过的左值
了解了上面的内容之后,我们来谈谈右值引用的作用和使用场景
C++中引入引用的概念本意是为了节省空间,左值引用已经满足了大部分的场景,如传参,做返回值(该对象在出了函数作用也还存在),但是如果该对象出了函数作用域后就销毁呢?如果不需要深度拷贝还好,一旦需要深度拷贝,就会浪费开辟空间需要的时间,如下面的场景---基于我在C++初级篇7string中附上的代码
我们可以很明显的感觉到在上面的过程中,空间的创建其实是不必要的,我们可以直接将str的资源直接交给s,没有必要另外创建两个对象
那么如何实现呢?
string(string&& tmp)//移动构造
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(tmp);
}
string& operator=(string&& tmp)//移动赋值
{
swap(tmp);
return *this;
}
这里的右值又称为将亡值,即生命周期快要结束,那么我们就可以将这个变量的资源交给需要它的对象,如下图
注意:string&&和string&虽然都是引用,但是类型是不同的,所以虽然const string&也能引用右值,但是C++的函数调用要求使用参数最匹配的函数,所以左值和右值的调用会分别调用最匹配的
当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义, 这里的强转指的是函数返回值
template//下面的函数参数和万能引用有关,后面再说,这里只要记住函数返回值被强转成了右值
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
//要谨慎使用move,不然可能出现下面的情况
int main()
{
zxws::string s1("hello world");
// 这里s1是左值,调用的是拷贝构造
zxws::string s2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
// 资源被转移给了s3,s1被置空了。
zxws::string s3(std::move(s1));
return 0;
}
STL容器插入接口函数也增加了右值引用版本,提高了插入效率
函数模板中的万能引用和完美转发
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
void Perfect(T&& x)//万能引用,既可以传左值,也可以传右值
{
Fun(forward(x));//forward(x)在传参的过程中保持了x的原生类型属性,称为完美转发
//可能有人觉得多此一举,但是上面我曾说过右值引用过的右值具有左值的属性,
//所以如果写Fun(x)在传参时,传右值结果会是传的左值,
//而如果写Fun(move(x)),则左值也会变成右值
//所以这里用完美转发forward(),解决所有问题
}
int main()
{
int x = 1;
Perfect(x);
const int y = 0;
Perfect(y);
Perfect(move(x));
Perfect(move(y));
return 0;
}
注意:只有当T&&中的T是被推导出来的时候,T&&才是万能引用
C++11新增了两个默认成员函数---移动构造和移动赋值
- 如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
还多了几个关键字:
- 强制生成默认函数的关键字default
- 禁止生成默认函数的关键字delete
- 继承和多态中的final与override
class A { public: A(){} A(const A& a) = delete; ~A() = default; //final 和 override 在多态中讲过 };
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template
void ShowList(Args... args)
{}
//sizeof...(args) 可以查参数个数
举个例子
void _ShowList()
{
cout << endl;
}
template
void _ShowList(T val, Args...args)
{
cout << val << " ";
_ShowList(args...);
}
template
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1, 2.2, 'x', "hhhh");//打印
return 0;
}
上面的这段代码可以打印不同类型的参数,大家可以看一下,带入递归,理解一下
解析:上面的代码可以看成是模板参数的递归,正常的递归函数都是需要有递归出口的,而上面模板函数的递归出口在于参数列表为空,下面画个图帮大家理解一下
这个还有另一种打印方式
template
int Print(T val)
{
cout << val << " ";
return 0;
}
template
void ShowList(Args... args)
{
int a[] = { Print(args)... };//{(Print(args), 0)...}将会展开成((Print(arg1),0),(Print(arg2),0), (Print(arg3),0), etc... )
//利用创建数组需要知道开辟空间大小,强行让编译器执行打印函数
cout << endl;
}
int main()
{
ShowList(1, 2.2, 'x', "hhhh");
return 0;
}
上面的打印代码确实很难理解,也很奇怪,无法理解的话,就暂且认为它是一种语法规定就行
实际上,可变参数列表的用处不在上面所说的打印,而是在于emplace系列接口的实现,给一个emplace_back的函数声明
template
void emplace_back (Args&&... args);
它既支持可变参数,也支持万能引用,那么相对正常的插入,它的优势体现在哪里?
就单纯拿push_back和emplace_back来比较,我写一个list中emplace_back的模拟实现给大家看看
如果看不太明白,可以用emplace_back("hello",1)和push_back(make_pair("hello",2))去代入
其实push_back()也就比emplace多了一次移动拷贝,效率上差不了多少(在需要深度拷贝的时候),当不需要深度拷贝且类比较大时,emplace的效率就会比较高
(C++11语法较多,其他重要的语法会在后续章节进行讲解,敬请期待)
未完待续…………