C++11(1)——右值引用、统一初始化、C++发展史

一、C++的发展史

1.C++的产生

C++的起源可以追溯到1979年,当时本贾尼(C++创始人)在贝尔实验室从事计算机科学与软件工程的研究工作。面对项目中复杂的软件开发任务,特别是模拟和操作系统的开发工作,他感受到了现有语言(如C)在表达能力、可维护性和可拓展性方面的不足。

1983年,他在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形,此时的C++已经有了类、封装、继承等核心概念,为后来的面向对象编程奠定了基础,这一年该语言被正式命名为C++。

C++的标准化工作与1989年开始,并成立一个ANSI和ISO国际标准化组织的联合标准化委员会。1994年标准化委员会提出了第一个标准化草案。该草案增加了部分新特征。

在完成C++标准化的第一个草案不久,STL是惠普实验室开发的一系列软件的统称。通过了标准化的第一个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的提议。STL对C++的扩展超出C++的最初定义范围。虽然增加STL是个很重要的决定,但也因此延缓了C++标准化的进程。

1997年通过了最终草案,1998年C++的ANSI/ISO标准被投入使用。

2.C++11简介

相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更

强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本篇文章主要讲解实际中比较实用的语法。

二、统一的列表初始化

注意:列表初始化和初始化列表不是一个东西,初始化列表是我们构造函数体和函数声明中间的那个东西(之前有提到过)。

1.{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

cpp 复制代码
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语言中创建数组会经常这么写。

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自

定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

cpp 复制代码
struct Point
 {
 int _x;
 int _y;
 };
 int main()
 {
 int x1 = 1;
 int x2{ 2 };//等价于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 };
 

创建对象时也可以使用列表初始化方式调用构造函数初始化。

cpp 复制代码
class Date
 {
 public:
 Date(int year, int month, int day)
 :_year(year)
 ,_month(month)
 ,_day(day)
     {
 cout << "Date(int year, int month, int day)" << endl;
     }
 //....
 }
 int main()
 {
 Date d1(2022, 1, 1); // 老方法
 // C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };//d2和d3的结果相同
 Date d3 = { 2022, 1, 3 };
 return 0;
 }

2、std::initializer_list

initializer_list就是初始化链表,它一般是作为构造函数的参数,C++11对STL中的不少容器就增加

std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。比如我们常见的vector,map,list等。

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

list< int > lt = { 1,2 };

// 使用大括号对容器赋值

v = {10, 20, 30};

需要区分的是,initializer_list和前面的{}初始化并不是同一原理,上面的{}可以理解为隐式类型转换,只能传规定数量的参数(Date类只能传3个,多传就会报错)而initializer_list可以传入多个参数。

三、声明

1、auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

2、decltype

关键字decltype将变量的类型声明为表达式指定的类型。相当于它接收之前的变量类型来声明新的变量。

cpp 复制代码
 int main()
 {
 const int x = 1;
 double y = 2.2;
 decltype(x * y) ret; // ret的类型是double
 decltype(&x) p;      
// p的类型是int*
 cout << typeid(ret).name() << endl;
 cout << typeid(p).name() << endl;
 }
 return 0;

3、nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

在之前的内容中我们多次使用过这个空指针就不作过多解释了。

4、范围for

它的底层是迭代器,由于之前也使用过多次在此不过多解释。
auto与范围for的讲解请点此访问

5、STL中的一些变化

增加了一些新容器,比如unordered_set和unordered_map这两个容器我们在上篇内容已经详细的讲解了,除此之外还有array(静态数组)和forward_list(实际中并不常用)
哈希表与unordered_set和unordered_map

除此之外还有一些新接口:比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。但其中有一个很重要的内容------右值引用。接下来我们就讲解一下有关右值引用的相关内容。

四、右值引用与移动语义

1、左值引用与右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用(&)就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
那什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,**左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。(但左值可以出现在赋值符号右边)**定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。​​​​​​​
因此,我们区分是否是左值的方法就是看其是否可以取地址。

cpp 复制代码
 // 以下的p、b、c、*p、d都是左值
int* p = new int(0);
 int b = 1;
 const int c = 2;
 const int d = b;
 // 以下几个是对上面左值的左值引用
int*& rp = p;
 int& rb = b;
 const int& rc = c;
 int& pvalue = *p;

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

cpp 复制代码
double x = 1.1, y = 2.2;
 // 以下几个都是常见的右值,常量临时对象,匿名对象
10;
 x + y;
 string("11111");

左值引用就是我们之前熟悉的引用(给左值取别名),那右值引用同理,就是给右值取别名,但语法规则有些不同。

cpp 复制代码
 // 以下几个都是对右值的右值引用
int&& rr1 = 10;
 double&& rr2 = x + y;

发现与左值引用相比,右值引用就是多了一个&进行区分。

那左值引用能否给右值取别名呢?即

cpp 复制代码
int &x=10;
const int &y=0;

运行结果我们发现,第一行不成立,第二行成立,也就是说左值引用不能直接给右值取别名,但const左值引用可以。

那反过来,右值引用能不能给左值取别名呢?答案是不能直接引用,需要move(左值)后才可引用。这个move是std中的一个函数,它会返回一个右值引用的值实现引用。(其本质是强制类型转换)

cpp 复制代码
int &&rx1=b;
int &&rx2=move(b);

第一行就会报错,第二行就能正常引用。

2、右值引用的使用场景和意义

我们知道,引用的意义就是减少拷贝提高效率,那么右值引用出现的原因就是有些情况是左值引用没有解决的。左值引用的作用有传参和传返回值,但传返回值的情况并没有完全解决。比如:有些场景只能用传值返回。

返回的是一个局部变量,出了这个作用域就会销毁,如果用左值引用就是野引用了。且传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造,拷贝构造的优化,之前提到过)。为了解决这个问题,引入一个新概念------移动构造。它们的函数参数有些不一样。

cpp 复制代码
string(const string&s) //拷贝构造
string(string &&s)//移动构造

如果不写移动构造的话,传左值和右值都会走拷贝构造,但如果有移动构造的话,传左值会走拷贝构造,传右值会走移动构造。

刚才在上面列举了右值的几种常见类型,但大致分类两类:纯右值:内置类型右值。将亡值:类类型的右值。(比如匿名对象等临时创建的对象,用完就要销毁) 。移动构造的思路就是:反正你出了作用域也没有了,不如把这个资源给我,我就不用再拷贝了。所以移动构造是一种抢夺资源的行为。

我们看一下有移动构造的情况下的优化

优化前:

str先拷贝成一个右值的临时对象然后再通过移动构造赋给s1

优化后:

中间不产生临时对象,直接把str隐式move成右值。
注意:只有深拷贝的类,移动拷贝才有意义。

除此之外,还有一个概念------移动赋值

道理和移动构造类似。

不仅是传值返回,C++11在push_back中也使用了移动构造

其原理与上面的相似。insert函数也如此。

接下来我们来分析一种情况:

cpp 复制代码
//假设我们已经写好了左值和右值的insert
void push_back(T&&x)
{
insert(end(),x);
)

当我们想验证结果时,发现其使用了右值的插入函数,这是我们的预期,但它又走了左值的insert。

这是因为,右值的右值引用其本质是左值。为什么会有这种退化式的设计呢?因为,如果其还是右值,那么我在进行资源交换的时候就无法实现,为了解决这一问题,我们可以用move

cpp 复制代码
void push_back(T&&x)
{
insert(end(),move(x));
)

注意,**x的本质未变,只是为了让编译器识别此处传的是右值。**这种操作可以方便我们一步步传参时保持右值的属性,减少拷贝。

但并不是所有情况move都可以解决问题,下面我们介绍一个新的概念。

3.完美转发

我们通过刚才函数的左值和右值版本发现,貌似构造,插入等函数想提高效率都需要单独写一个右值版本的函数。但C++11时就有一个提问:以后所有函数都要写两个版本吗?会不会太麻烦了?所以,C++在模板部分在此有了一些调整:

cpp 复制代码
template<class T>
void func(T&&x)
{
//....
}

其中"&&"并不是右值引用版本,而被称为万能引用,从结构上看,只是多了一个模板,别的好像并没有太大区别。但虽然这个地方像右值引用,但我可以通过你传的参数进行推导,这样就不用写两个版本了,你传左值他就会生成左值版本的,右值也同理。我们也称引用折叠。

如果模板实例化是左值引用,保留属性直接进行下一步传参;模板实例化是右值引用,右值引用属性会退化成左值,需要转换成右值再进行下一步传参。

但是,编译器是不支持用语法判断所传的类型,比如上面的实例,我们不可能用T=="&"。因此,完美转发的作用就来了。语法如下:

cpp 复制代码
template<class T>
void func(T&&x)
{
  Fun(forward<T>(t));
}

传的是左值则不变,若传的是右值但退化成左值,完美转发就会把t重新以右值的身份传给Fun函数。完美转发适用于我不知道传入的是左值还是右值,不像我们前面明确知道是左右值的情况下可以用move,上面这个情况如果我写成move那么如果传入左值就达不到想要的结果了。他会一律处理成右值。

五、类的新功能

我们在类与对象部分提到,类有6个默认成员函数,但C++又新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

但一般打破这种规则的时候都是深拷贝(有资源要释放),那么需要我们自己写拷贝、析构、赋值重载,当然移动构造和移动赋值也要自己写了。

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

cpp 复制代码
class Person
 {
 public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 :_name(p._name)
 ,_age(p._age)
 {}
比特就业课
Person(Person&& p) = default;
 private:
 bit::string _name;
 int _age;
 };
 int main()
 {
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
 }

禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁

已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即

可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

相关推荐
Source.Liu6 分钟前
【用Rust写CAD】前言
开发语言·rust
jzlhll1238 分钟前
kotlin android Handler removeCallbacks runnable不生效的一种可能
android·开发语言·kotlin
&岁月不待人&10 分钟前
Kotlin 协程使用及其详解
开发语言·kotlin
苏柘_level611 分钟前
【Kotlin】 基础语法笔记
开发语言·笔记·kotlin
C++忠实粉丝34 分钟前
Linux系统基础-多线程超详细讲解(5)_单例模式与线程池
linux·运维·服务器·c++·算法·单例模式·职场和发展
2401_8771587340 分钟前
什么是垃圾回收(Garbage Collection)?
java·开发语言·算法
Gavin_91543 分钟前
【JavaScript】数组-集合-Map-对象-Class用法一览
开发语言·前端·javascript
DARLING Zero two♡1 小时前
关于我、重生到500年前凭借C语言改变世界科技vlog.15——深入理解指针(4)
c语言·开发语言·科技
混迹网络的权某1 小时前
蓝桥杯真题——三角回文数(C语言)
c语言·开发语言·算法·蓝桥杯·改行学it
爱上语文1 小时前
苍穹外卖 商家取消、派送、完成订单
java·开发语言·spring boot·后端