C++11新增内容详解(一)

一、列表初始化(区别于初始化列表)

介绍:包括多参数函数隐式类型转换在内的,一切皆可列表初始化,其可以不加赋值符号

如:

//这是之前就常见的
int a1[]={1,2,3,4,5};
int a2[5]={0};

//列表初始化结构体与普通内置类型,且不加赋值符号
struct Text
{
    int t1;
    int t2;
};

Text p {1,2};
int x2 {2};

//对类初始化和临时变量接收
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{

	}

protected:
	int _year;
	int _month;
	int _day;

};

Date d1 {2025,3,10};//需要类本身存在三参数构造函数
const Date& d2 {2025,3,10};//接收initializer_list中间转化的临时变量

 此处d2处如果不加const,会出现如下报错:

这是因为initializer list转化的中间变量具有“常性 ”

需要const修饰方可接收 

1.补:遇到类型截断时的措施

列表初始化如果遇到类型截断,会报警告或者错误

short {100};

二、初始化列表initializer_list

介绍:{}中可以有任意多个值,可以实现挨个插入容器当中

对比于列表初始化:列表初始化的{}中只能有指定数目的值,一次只能为容器提供一个对象

实际上,它本身是一个类模板

template<class T> class initializer_list;

起作用与容器很像,其存储原理是:在栈上开一块数组的空间,initializer_list类型的对象中只存储该数组空间的首尾指针其在32位下大小位8,存储的位置也在栈上

2.补:初始化列表和列表初始化可以混用

在本身有初始化列表构造的情况下,可以用列表初始化来配合初始化列表进行构造

 例如vector中

vector<int> v6{ {1},{2},{3},{4} };

三、decltype读取类型并用于创建

decltype是declaration type的缩写,declaration意思是声明,综合起来看是声明类型的意思

整体用法与typeid有些类似,但是decltype的推导结果可以直接作为类型来使用

如:

const int i=1;
decltype(&i) p;//可以生成一个const int*类型的变量p

cout<<typeid(p).name()<<endl;//可以打印查看p的类型

vector<decltype(&i)> vp;//可以生成一个存储const int*类型的顺序表

四、nullptr空指针

C++中NULL被置为0,可能会带来这样的问题:

0既可以为整型常量,又可以为指针常量

有种替代方案是:(void*)0,但也存在使用的时候需要强转的问题

nullptr表示空指针,很好的解决了NULL本身的问题

五、左值引用与右值引用

5.1什么是左值和左值引用

左值是一个表示数据的表达式,我们可以获取它的地址一般情况下可以对他进行赋值(const修饰除外),左值可以出现在赋值符号的左,右两侧

左值引用是左值的引用,给左值取别名

5.2什么是右值和右值引用

右值也是一个表示数据的表达式,我们不可以获取它的地址,右只能出现在赋值符号的右侧

 右值引用是右值的引用,给右值取别名

例如:常量,表达式返回值,函数返回值(返回值类型为左值引用的除外) 

5.3结合例子体会左值引用和右值引用的使用

5.3.1左值

左值最大的特点是:可以取地址

int* p=new int(10);
int b=1;
const int c=b;//左值可以在赋值符号右侧

*p=10;//*p可以取地址,也是一个左值
string str("111111");
str[0];

5.3.2右值

右值最大的特点是:不可以取地址

 常见的像常量,临时对象,匿名对象等等

double x=1.1 , y=2.2;//这时左值,服务于下边的x+y

10;
x+y;
fmin(x,y);//是一个库函数,返回值为double类型
string("111111");

5.3.3左值引用给左值取别名

直接用即可

//结合上述左值举例
int*& ll1=p;
int& ll2=b;
int& ll3=*p;

5.3.4右值引用给右值取别名

直接用即可

//结合上述右值举例
int&& rr1=10;
double&& rr2=x+y;
double&& rr3=fmin(x,y);

5.3.5左值引用给右值取别名

不可以直接用

其中涉及的问题是:临时变量等右值具有常性,需要加const来修饰左值引用

const int& lr1=10;
const double& lr2=x+y;
const double& lr3=fmin(x,y);
const string& lr4=string("111111");
5.3.5补:vector中push_back的实现就是例子

在实现vector时,传给push_back的参数类型是const T&

这个参数类型十分巧妙。不论是传过来左值还是右值都可以正常接收

5.3.6右值引用给左值取别名

不可以直接用

其中涉及的问题是:二者本身就冲突,必须要借助函数move,使用如move(左值)的方式来将左值转化为右值 

move的作用就是传入左值,然后返回相对应的右值

int*&& rl1=move(p);
int&& rl2=move(b);
int&& rl3=move(*p);
string&& rl4=move(s);

5.4他们的底层实现

我们要注意:底层汇编的实现与上层语法表达的意义有时是背离的

正如左值引用和右值引用,其实他们的底层都是指针

左值引用的汇编:

观察可以发现,汇编层面其实就是很简单的将p地址对应的值通过lea+mov给到ll1地址对应的值 

右值引用的汇编:

 观察不难发现,实际上就是多了一步

将x+y计算出来的过程 :

开了一块栈空间

其中跨度大小位50h 

之后还是通过lea+mov给到rr2地址对应的值 

5.4补:既然底层相同,那么move的作用相当于什么呢?

由上我们已经知道了在汇编层其实左值引用和右值引用没有冲突,那么拦截他们混用的一定是语法层

所以move几乎就是一个类型转换 

 例如 

string&& rl4=move(s);

就可以是

string&& rl4=(string&&)s;

 完全可以依照需求进行强转

六.右值引用的意义与移动语义

⭐6.1右值引用最核心的意义

实际上稍早一些的编译器(C++11标准运用之前),都需要避开大对象传值返回

例如杨辉三角题目中涉及到的vector<vector<int>>

因为这一缺陷被诟病已久,在C++11时提出了移动语义的概念来解决这一问题:

引用是为了减少拷贝

而移动构造配合右值引用更是可以略去传值返回的拷贝深拷贝的类,移动构造才有意义

略去拷贝的原理是

右值对应的类类类型都属于临时创建的对象,不能取地址,用完就要消亡(称为“将亡值”)

而移动构造实际上就是把该对象swap一下,让他的资源不去释放而是直接利用,过程中传参为右值引用类型

如:

string(string&& str)
{
    swap(str);
}

6.2 有无移动构造的对比

假设这样一个场景:

在一个函数中,运行过程中创建了一个类的实例化str,之后必须传值返回这个str 

①无移动构造

编译优化后会有一次拷贝构造的消耗 

②有移动构造

编译优化后会有0次拷贝构造的消耗  

6.3 VS2022:编译器优化实现省略拷贝

假设6.2中的场景对应函数to_string,传入整形,返回字符串,在函数实现过程中会新建str来作为之后的返回值

在main函数中调用语句:

string s1=to_string(1234);

 具体实现过程如图示:

to_string函数运行过程中不会再新建str,而会直接找到main,去其中构造s1,这样以后有无移动构造都不会让传值传参有任何消耗 

 6.3补:在Linux中,g++有一条单独的指令可以关闭编译器优化

g++ -O0 +[.cpp文件名] -o +[可执行程序名]

6.4移动赋值

我们如果把6.3中的

string s1=to_string(1234);

更改为

string s1;
s1=to_string(1234);

那么还会触发编译器的自动优化吗?

答案是不会的, 为此我们需要对赋值重载函数进行添加,添加一个传右值引用的参数对应的重载,即“移动赋值”

string& operator=(string&& s)
{
    swap(s);
    return *this;//交换后还要帮助完成释放的工作
}

6.5移动拷贝在一些容器的插入过程中也会起到提效的作用

如list有对应的push_back接口:传入右值引用

void push_back (value_type&& val);

具体使用如:

list<string> lt;

//情况一:先进行普通拷贝,再走拷贝构造
string s1("111");
lt.push_back(s1);

//情况二:先进行普通拷贝,再走移动拷贝
lt.push_back(string("222"));

//情况三:先进行普通拷贝,再走移动拷贝
lt.push_back("333");

//走移动拷贝,但会导致s1的资源被转移
lt.push_back(move(s1));

⭐6.6右值引用正常情况下为右值的引用,那么右值引用本身的属性是什么?

 是左值,因为如移动构造中,是需要通过swap来减少消耗的,而我们知道右值具有常性,不可能顺利完成swap,因此要swap必须传入左值

因为这个缘故,如果我们要模拟实现push_back传入右值引用的接口(就是移动拷贝)

必须要传入insert一个move(str),而在list的构造中又需要new一个节点......

 综合来看需要若干次move,还需要理清何时move

6.7万能引用(又名引用折叠)

例如要写一个函数模板

template<class T>
void func(T&& x)
{//...}

如果每次都要写一个左值版本和一个右值版本就太麻烦了

 因此规定:

T&&类型参数在进行接收时

①传入T&&,即识别为T&&,右值引用

②传入T&,即识别为T&,左值引用

6.8完美转发

因为右值引用本身的属性时左值,而一次次需要时才move又太过麻烦,为此提出了“完美转发

的概念,来实现“传入左值引用则不变,传入右值引用则更改属性为右值

实际上,完美转发是一个类模板

forward<T>([传入参数])

完美转发的例子:

void Func(int& x){//...}

void Func(const int& x){//...}

void Func(int&& x){//...}

void Func(const int&& x){//...}

template<class T>
void testFunc(T&& t)
{
    //???
}

我们希望???的位置可以帮助我们实现的是

void testFunc(int& t){Func(t);}

void testFunc(const int& t){Func(t);}

void testFunc(int&& t){Func(move(t));}

void testFunc(const int&& t){Func(move(t));}

无论填入Func(t)还是Func(move(t))都无法满足要求 

因此我们要填的是:

Func(forward<T>(t))

此时,模板实例化为左值引用,保持属性传给Func

模板实例化为右值引用,转变属性回到右值再传给Func

七、增加了几个新的容器

新增的容器右4个,其中三个之前已经介绍过了,

<array>

<unordered_map>

<unordered_set>

①对于array

主要解决的是C中这个问题:

越界读检查不出,越界较大写也查不出

int a[10]={0};
cout<<a[10]<<endl;//不报错,但越界了
cout<<a[11]<<endl;//不报错,但越界了

a[15]=1;//越界较严重,写也查不出

虽然对于越界的检查更加严格了,但当静态数组太大的时候容易导致栈溢出 

 ②unordered_map和unordered_set

可以有效提高效率,详情可参考

C++中的unordered_set,unordered_map,哈希表/散列表以及哈希表的模拟实现_遍历 unorder set-优快云博客

③新的<forward_list> 

他是一个单链表,只允许头插/头删(很少用,他可以的list都可以)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值