目录
一,列表初始化
二,initializer_list
三,auto与decltype
1)auto
2)decltype
四,nullptr
五,范围for
六,新加容器
1)array
2)forward_list
3)unordered_map和unordered_set
七,右值引用(重点!!!)
1)左值
2)右值引用
3)右值引用和左值引用的联系
4)右值引用的适用场景
5)右值引用使用的坑及解决办法
八,新的类功能
1)默认成员函数
2)强制生成默认函数的关键字default:与禁止生成默认函数的关键字delete:
3)final与override
九,可变参数模板
十,lambda
十一,包装器
前言:在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于
C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,
C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本篇博客主要讲解实际中比较实用的语法。
一,列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。例如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加,一切皆可{}。
例如:
struct Data{
int _year;
int _month;
int _day;
};
int main(){
int a{666};
int arr{1,2,3,4,5};
Data d{2024,4,20}
int* p=new int{666};
}
二,initializer_list
在查看C++各种容器的时候我们会发现很多类的构造函数里面都有initializer_list的初始化方式,
比如常见的vector,list
initialiszer_list是什么呢?大家可以自己先查看文档,也可以直接看我讲解
initializer_list - C++ Referencehttps://legacy.cplusplus.com/reference/initializer_list/initializer_list/?kw=initializer_list
initializer_list其实很简单,里面就像一个数组一样暂时存储初始化的数据,然后需要Initializer_list的容器就写一个这个构造函数,初始化的时候就不需要多次调用构造函数或者多次insert插入元素,直接一步到位在构造函数里面将数据全部插入。其实我们早就用过initializer_list只是我们不知道而已。
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = {10, 20, 30};
return 0;
}
initilaizer_list的源码模拟思路,你可以写一个数组,或者指针指向这些内容,然后写一个迭代器就简单粗略的模拟好了,在需要使用这个的容器的里面直接使用它的迭代器进行构造或者赋值就行了。过于简单就不模拟了。
三,auto与decltype
1)auto
学习pathon的应该很熟悉auto,auto是自动推导类型,将这项工作交给编译器。
struct Data{
int _year;
int _month;
int _day;
};
int mian(){
auto a=666;
auto b="stl";
auto p=new Data;
Data c;
auto d=c;
}
auto的弊端,一时auto一时爽,代码复杂调试火葬场。当代码项目简单的时候我们很难看出auto的弊端,在代码复杂,一环套一环并且使用了很多模板之后,即使你使用decltype,auto的具体类型还是很折磨人,所以说尽量少用auto。
2)decltype
decltype没啥好说的,就是查看类型,可以配合cout使用,打印类型,因为它的返回值是字符串,参数只有一个是变量或者自定义结构的实例化。
四,nullptr
在C语言里面留下了一个坑NULL可以是0,这样子带来了很多问题,NULL既可以代表0,也可以代表空指针。因此C++里面提出了nullptr,nullptr只代表空指针,并且会进行安全检查。
五,范围for
范围for的语法使用类似for循环,但是更加简单,无需写循环结束条件,和循环条件改变,用于遍历数据元素,它底层是借助迭代器完成的遍历元素,因此使用范围for,底层必须有迭代器,范围for一般配合迭代器使用,但是有一个坑,就是改变里面的元素需要使用引用。
int main(){
int arr[]={7,8,9,0};
vector<int> v={1,2,3,4,5,6};
for(auto e:arr){
cout<<arr; //输出arr数组里面的东西
}
for(auto& e:arr){ //如果想要改变里面的元素需要使用引用
cout<<e;
e++;
}
}
六,新加容器
1)array
在C++11里面新加了容器array,array与数组类似但不相同,首先它是有固定的大小的,并且无法扩容,里面加入了迭代器,允许拷贝构造,并且安全检查更加严格,大家如果还想了解更多,可以查看下面的链接
array - C++ Referencehttps://legacy.cplusplus.com/reference/array/array/?kw=array
#include<array>
#include<iostream>
using namespace std;
int main() {
array<int, 5> arr{ 1,2,3,4,5 };
array<int, 5> arr1 = arr;
for (auto e : arr)
cout << e;
return 0;
}
2)forward_list
其实这就是C++11推出来的单链表,我们之前学习的stl模板list底层其实是带头双向循环链表,如果不懂这两个可以看我之前的文章,这里只贴链接,大家有兴趣自行观看。
http://t.csdnimg.cn/NoSUthttp://t.csdnimg.cn/NoSUt
forward_list - C++ Referencehttps://legacy.cplusplus.com/reference/forward_list/forward_list/?kw=forward_list
3)unordered_map和unordered_set
这个底层就是哈希,我之前的文章进行了详细的讲解,大家点击链接就行了。
http://t.csdnimg.cn/HX6PKhttp://t.csdnimg.cn/HX6PK
七,右值引用(重点!!!)
在C++11我个人觉得一个最重要的改变就是右值引用,在学习右值引用之前我们需要先学习什么是右值引用。
我们以前接触最多的是左值引用,也就是我们以前正常情况下的引用,右值引用也是引用,只不过它们两个能够引用的值的范围不同。左值引用只能引用左值,那么什么是左值呢?
1)左值
int a=66;
int& b=a;
const int& c=a;
int *p=&a;
int*& pp=p;
const int*& ppp=pp;
int& p1 = *p;
上面的就是左指引用,语法上只有一个&,左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
注:左值的特点是可以取等于号右边元素的地址。
2)右值引用
int add(int a, int b) {
return a + b;
}
int main() {
int&& a = 10;
int x = 1, y = 3;
int&& b = x + y;
int&& d = add(x , y);
}
上面的就是常见的右值引用场景,大家是不是感到很奇怪,10,x+y,这些能被引用吗?这就牵扯到右值引用的底层原理了。右值引用的底层并不是和左值引用一样两个变量指向一块空间的,而是将这块空间里面的值移动到一块新的栈上空间,并且指向它(移动并不是很准确,应该用粗暴的拷贝才正确,并不会破坏原空间的内容,为什么是粗暴呢?因为它只会根据空间的大小,不会调用拷贝构造等构造函数,而是你告诉我它的空间有多大,我不管他们之间的联系,直接无脑类似于移动过去一样,这有什么好处呢,不调用拷贝构造等构造函数,可以节省很多开销,特别是深拷贝,无脑就是效率高,但是大家可能会说带来了一个问题,如果是深拷贝呢?大家有没有发现右值有一个特点,就是它们不是将亡值,就是在常量区,也就是只有右值引用才能改变,哪里存在深拷贝出现的重复析构等问题呢?因为右值引用本身并不释放深拷贝的空间,只是一个使用者,并不具有管理权)。
注:右值引用等于号右边的值不可以取地址
3)右值引用和左值引用的联系
左右值引用其实是可以相互指向的
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
注:move函数是把变量等的性质从左值转化成右值,无其它作用,并不会移动复制空间,仅仅是改变性质。
move - C++ Referencehttps://legacy.cplusplus.com/reference/utility/move/?kw=move
4)右值引用的适用场景
前面说了右值引用是粗暴的窃取别人的资源来使用,减少了调用构造函数等开销,如果是浅拷贝等,大家知道浅拷贝调用构造函数的开销并不多,可能是一个int,静态数组,或者一个没用new很大空间的类,但是一旦涉及到深拷贝,类似于list,你永远不知道它的空间到底有多大,深拷贝的开销有多大,但是如果允许修改它指向的空间,我们就可以使用右值引用,这样子只需要粗暴无脑复制头节点就行了,效率嘎嘎就上去了,因此右值引用在处理深拷贝是十分实用。
5)右值引用使用的坑及解决办法
这是为什么呢?因为右值引用在传递一次之后就自动变成了左值,如果我们中间有一次忘记调用了move,那我们前面的所有准备全部报废了,它会转为调用左值引用。
模板里面提出了一种解决办法,就是完美转发,完美转发就是模板里面加两个取地址,
这个并不代表是右值引用,而是根据传过来的参数,不改变参数的性质,自行判断类型,可能是左值引用,也可能是右值引用。
八,新的类功能
1)默认成员函数
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。(也就是右值引用)2)
2)强制生成默认函数的关键字default:与禁止生成默认函数的关键字delete:
class A {
public:
A(A&& a) = default;
A(A& a) =delete;
};
在C++里面,自己写的类,如果你写了左值引用拷贝构造函数那就就不会默认生成移动拷贝构造函数,使用default就可以强制默认生成移动拷贝构造函数。
反之如果加上delete则不会生成默认函数,并且就是程序员想要生成也会报错。
3)final与override
在C++中,final
关键字用于指定类、函数或方法不能被继承或重写。它主要用于实现类的封闭性和多态性的控制。(注意只能修饰虚函数)
class A final
{
public:
A(A&& a) = default;
virtual void add() final{
}
};
override显式地指定一个成员函数在派生类中重写(override)了基类中的虚函数,防止重写错了函数。
使用方法与final类似,但是要注意是写在派生类里面重写的函数,否则就会报错。
九,可变参数模板
大家还记得printf和scanf这两个函数吗?这两个函数可以有无限多的参数,在C++11里面提出的可变参数模板就可以实现类似的功能。
template <class ...Args>
void ShowList(Args... args)
{}
int main() {
ShowList(1, 2, 3, 4, 5, 6, 7,8);
}
其定义的时候是前面有三个点,使用的时候三个点在后面,使用的时候需要大家摸索试探。
大家是不是觉得很神奇,为什么可以加无限参数。可变参数模板有两种方法拿到里面的值。
一个是递归方法
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
还有一种就是逗号表达式
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行
printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...
(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
十,lambda
有时候,我们以前碰到过需要传函数或者仿函数作为参数的情况,例如,sort里面就有一个参数支持传函数或者仿函数,在C++11里面提供了一种类似函数和仿函数的办法——lambda,它也可以完成一部分操作,它是匿名的我们无法直接拿到,需要借助模板(看十一包装器)。在一些简单和使用次数少的场景它很方便,大家先认识一下它的语法。
//模仿一个sort的lambda
[](const int& g1, const int& g2) ->int{return g1 < g2; }
//[]是捕捉列表可以捕捉前后文的变量
//()里面的是类似于函数参数
//->类型 这个是显示表明返回类型,如果返回类型明确可以省略
//{}里面是lambda的操作内容
使用方法1——作为函数参数
vector<int> v{ 7,6,5,4,3,2,1 };
sort(v.begin(), v.end(), [](const int& g1, const int& g2) {return g1 < g2; });
使用方法2——捕捉参数方法
int a = 1;
int b = 2;
int c = 3;
int sum= ([=](int c) {return a + b+c; })(c);
/*[var] :表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针*/
相信大家看完上面代码已经学会使用lambda了,那就跳过了。
十一,包装器
上面说了lambda是匿名的,我们无法直接拿到,但是我们可以借助包装器拿到他,包装器同时也叫适配器,那包装器是什么东西呢?
function包装器:
#include<functional>//头文件
class A {
public:
int operator()(int a, int b) {
return a + b;
}
};
int add3(int a, int b) {
return a + b;
}
int main(){
function<int(int, int)> add = [](int a, int b) {return a + b; };
function<int(int, int)> add1 = A();
function<int(int, int)> add2 = add3;
return 0;
}
//function里面第一个int 是返回值类型,(int,int)是函数参数
包装器不论它赋值对象是函数,仿函数还是lambda它都可以驾驭,这个有什么用呢?当我们函数传参的时候,我们有时候不确定的时候,我们不使用模板使用function就可以不使用模板生成三份代码,减少了内存的消耗,提高了效率。
为什么function包装器可以实现这个效果呢?我们就需要看它的构造函数了,function是一个类,其实很简单,因为function里面用了模板包装了一层,为什么包装一层就可以减少代码的消耗呢?大家想想,如果只包装一层,我们假设一个类A里面用了function,那就最多只生成成了三个function,类A并不需要生成三份,function是官方库里面的,大小和内容并不复杂,开销其实很小,但如果是生成三个类A,我们就无法知道三份类A的开销有多大了。
包装器里面还有blid包装器等,这个用于增加函数参数和可以更换函数参数顺序,这个因为并不是很重要,大家自行搜索文章学习即可。