目录
目录
一、统一的列表初始化
二、声明
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()
{vector<int>v = { 0,1,2,3,4 };list<int>l = { 2,3,5,6,30 };map<string, string>mp = { {"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,里面有完整模拟实现代码
二、声明
1.auto
2.decltype
int main()
{int x = 1;double y = 2.2;decltype(x * y) ret = x * y;//这个用法和auto没啥区别vector<decltype(x * y)>v;//这里只能用decltype//vector<auto>v,错误写法return 0;
}
也就是说,当我们需要类型作为参数时,只能用decltype
3.nullptr
这个也不多说,因为C++官方将NULL定义为了0,所以加了一个nullptr表示空指针
三、范围for
底层就是迭代器遍历容器。
int main()
{vector<int>v{ 1,2,3,4,5,6 };for (auto& e : v)//范围forcout << e << " ";return 0;
}
四、STL中的变化
多了静态数组、单链表和哈希表,还有一些接口,如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<class _Ty>//下面的函数参数和万能引用有关,后面再说,这里只要记住函数返回值被强转成了右值
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{// forward _Arg as movablereturn ((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<class T>
void Perfect(T&& x)//万能引用,既可以传左值,也可以传右值
{Fun(forward<T>(x));//forward<T>(x)在传参的过程中保持了x的原生类型属性,称为完美转发//可能有人觉得多此一举,但是上面我曾说过右值引用过的右值具有左值的属性,//所以如果写Fun(x)在传参时,传右值结果会是传的左值,//而如果写Fun(move(x)),则左值也会变成右值//所以这里用完美转发forward<T>(),解决所有问题
}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 <class ...Args>
void ShowList(Args... args)
{}//sizeof...(args) 可以查参数个数
举个例子
void _ShowList()
{cout << endl;
}template <class T, class ...Args>
void _ShowList(T val, Args...args)
{cout << val << " ";_ShowList(args...);
}template <class ...Args>
void ShowList(Args... args)
{_ShowList(args...);
}int main()
{ShowList(1, 2.2, 'x', "hhhh");//打印return 0;
}
上面的这段代码可以打印不同类型的参数,大家可以看一下,带入递归,理解一下
解析:上面的代码可以看成是模板参数的递归,正常的递归函数都是需要有递归出口的,而上面模板函数的递归出口在于参数列表为空,下面画个图帮大家理解一下
这个还有另一种打印方式
template <class T>
int Print(T val)
{cout << val << " ";return 0;
}
template <class ...Args>
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 <class... Args>
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语法较多,其他重要的语法会在后续章节进行讲解,敬请期待)
未完待续…………