一、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中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
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 };
创建对象时也可以使用列表初始化方式调用构造函数初始化。
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将变量的类型声明为表达式指定的类型。相当于它接收之前的变量类型来声明新的变量。
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修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
因此,我们区分是否是左值的方法就是看其是否可以取地址。
// 以下的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;
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,常量临时对象,匿名对象
10;
x + y;
string("11111");
左值引用就是我们之前熟悉的引用(给左值取别名),那右值引用同理,就是给右值取别名,但语法规则有些不同。
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
发现与左值引用相比,右值引用就是多了一个&进行区分。
那左值引用能否给右值取别名呢?即
int &x=10;
const int &y=0;
运行结果我们发现,第一行不成立,第二行成立,也就是说左值引用不能直接给右值取别名,但const左值引用可以。
那反过来,右值引用能不能给左值取别名呢?答案是不能直接引用,需要move(左值)后才可引用。这个move是std中的一个函数,它会返回一个右值引用的值实现引用。(其本质是强制类型转换)
int &&rx1=b;
int &&rx2=move(b);
第一行就会报错,第二行就能正常引用。
2、右值引用的使用场景和意义
我们知道,引用的意义就是减少拷贝提高效率,那么右值引用出现的原因就是有些情况是左值引用没有解决的。左值引用的作用有传参和传返回值,但传返回值的情况并没有完全解决。比如:有些场景只能用传值返回。
返回的是一个局部变量,出了这个作用域就会销毁,如果用左值引用就是野引用了。且传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造,拷贝构造的优化,之前提到过)。为了解决这个问题,引入一个新概念——移动构造。它们的函数参数有些不一样。
string(const string&s) //拷贝构造
string(string &&s)//移动构造
如果不写移动构造的话,传左值和右值都会走拷贝构造,但如果有移动构造的话,传左值会走拷贝构造,传右值会走移动构造。
刚才在上面列举了右值的几种常见类型,但大致分类两类:纯右值:内置类型右值。将亡值:类类型的右值。(比如匿名对象等临时创建的对象,用完就要销毁)。移动构造的思路就是:反正你出了作用域也没有了,不如把这个资源给我,我就不用再拷贝了。所以移动构造是一种抢夺资源的行为。
我们看一下有移动构造的情况下的优化
优化前:
str先拷贝成一个右值的临时对象然后再通过移动构造赋给s1
优化后:
中间不产生临时对象,直接把str隐式move成右值。
注意:只有深拷贝的类,移动拷贝才有意义。
除此之外,还有一个概念——移动赋值
道理和移动构造类似。
不仅是传值返回,C++11在push_back中也使用了移动构造
其原理与上面的相似。insert函数也如此。
接下来我们来分析一种情况:
//假设我们已经写好了左值和右值的insert
void push_back(T&&x)
{
insert(end(),x);
)
当我们想验证结果时,发现其使用了右值的插入函数,这是我们的预期,但它又走了左值的insert。
这是因为,右值的右值引用其本质是左值。为什么会有这种退化式的设计呢?因为,如果其还是右值,那么我在进行资源交换的时候就无法实现,为了解决这一问题,我们可以用move
void push_back(T&&x)
{
insert(end(),move(x));
)
注意,**x的本质未变,只是为了让编译器识别此处传的是右值。**这种操作可以方便我们一步步传参时保持右值的属性,减少拷贝。
但并不是所有情况move都可以解决问题,下面我们介绍一个新的概念。
3.完美转发
我们通过刚才函数的左值和右值版本发现,貌似构造,插入等函数想提高效率都需要单独写一个右值版本的函数。但C++11时就有一个提问:以后所有函数都要写两个版本吗?会不会太麻烦了?所以,C++在模板部分在此有了一些调整:
template<class T>
void func(T&&x)
{
//....
}
其中“&&”并不是右值引用版本,而被称为万能引用,从结构上看,只是多了一个模板,别的好像并没有太大区别。但虽然这个地方像右值引用,但我可以通过你传的参数进行推导,这样就不用写两个版本了,你传左值他就会生成左值版本的,右值也同理。我们也称引用折叠。
如果模板实例化是左值引用,保留属性直接进行下一步传参;模板实例化是右值引用,右值引用属性会退化成左值,需要转换成右值再进行下一步传参。
但是,编译器是不支持用语法判断所传的类型,比如上面的实例,我们不可能用T==“&”。因此,完美转发的作用就来了。语法如下:
template<class T>
void func(T&&x)
{
Fun(forward<T>(t));
}
传的是左值则不变,若传的是右值但退化成左值,完美转发就会把t重新以右值的身份传给Fun函数。完美转发适用于我不知道传入的是左值还是右值,不像我们前面明确知道是左右值的情况下可以用move,上面这个情况如果我写成move那么如果传入左值就达不到想要的结果了。他会一律处理成右值。
五、类的新功能
我们在类与对象部分提到,类有6个默认成员函数,但C++又新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
但一般打破这种规则的时候都是深拷贝(有资源要释放),那么需要我们自己写拷贝、析构、赋值重载,当然移动构造和移动赋值也要自己写了。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
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修饰的函数为删除函数。