前言
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯意性的将两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准十年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含越140个新的特性,以及对C++03标准中约600缺陷的修正,这使得C++11像是从C++98/03中孕育出来的新语言。相比较而言,C++11能更好的用于系统开发和库开发,语言跟简单化和泛化,更加稳定和安全,不仅功能更加强大,而且能提升程序员的开发效率。公司实际项目开发中也用到的较多,C++11增加的语法特性篇幅非常的多,我们这里无法一一讲解,所以只会介绍一些比较实用的语法特性。
目录
1.统一的初始化列表
1.1{}初始化
1.2std::initializer_list
2.变量类型推导
3.范围for循环
4.final和override
5.智能指针
6.新增的容器
7.默认成员函数控制
8.右值引用
8.1左值引用和右值引用
8.2右值引用的使用场景和意义
8.3右值引用引用左值及其一些更深入的使用场景的分析
8.4 完美转发
9.lambda表达式
10.线程库
在C++98中允许使用{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct point
{
int _x;
int _y;
};
int main()
{
/在c++98中标准允许对结构体和数组使用{}进行初始化
int arrar[] = { 1,2,3,4,5 };
point p1 = { 1,2 };
return 0;
}
C++11扩大了用大括号括起来的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户的自定义类型,使用初始化列表时,可以添加等号(=),也可以不添加。
#include
#include
#include
#include
在创建对象时也可以使用列表初始化方式来调用构造函数初始化。
//创建对象时也可以使用初始化列表方式来调用构造函数的初始化
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year{year}
,_month{month}
,_day{day}
{ }
private:
int _year;
int _month;
int _day;
};
int main()
{
Test1();
Date d1{ 2023,9,5 };
Test2();
return 0;
}
std::initializer_list是什么类型:如下
void Test2()
{
auto li = { 1,2,3 };
cout << typeid(li).name() << endl;
}
std::initializer_list一般作为构造函数的参数,C++11对STL中的不少容器就增加了std::initializer_list作为参数的构造函数,这样初始化容器就更加方便了。也可以作为operator=的参数,这样就可以用大括号来进行赋值。
例如:
void Test1()
{
//在c++98中标准允许对结构体和数组使用{}进行初始化
int arrar[] = { 1,2,3,4,5 };
point p1 = { 1,2 };
//统一的列表初始化
//对于自定义类型
vector v1 = { 1,2,3,4,5 };
vector v2{ 6,7,8,9,10 };
list l1 = { 8,9,10,11,12,13 };
std::map m1 = { {"西瓜",1},{"菠萝", 3},{"苹果", 5} };
}
简单的让模拟实现的vector也支持{}初始化和赋值。
namespace qyy
{
template
class vector
{
public:
typedef T* iterator;
vector(std::initializer_list l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list ::iterator lit = l.begin();
//将std::initializer_list 的数据插入到vector中
while (lit != l.end())
{
*vit++ = *lit++;
}
}
vector& operator=(initializer_list l) {
vector tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
在c++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是在局部域里面定义的变量默认就是自动存储类型,所以auto就没有什么价值了,c++11中废弃了auto原来的用法,将其用于自动推断类型。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
void Test3()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map m = { {"sort","排序"},{"insert","插入"} };
auto it = m.begin();
std::map ::iterator it = m.begin();//两种写法是类似的但是使用auto可以简化代码
}
需要注意的是auto不能作为函数的参数,和返回值。
nullptr
由于C语言中NULL被定义为字面值常量0,这样会带来一些问题,0既能为指针常量,又能表示整形常量。所以出于清晰和安全的考虑,c++11中新增了nullptr,用于表示空指针。
NULL实际上是一个宏,在传统的c文件中stddef.h),可以看到如下的代码:
#ifndef NULL
#ifdef _cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
decltype
decltype可以识别对象的类型并且通过识别的对象的类型定义同类型的新对象。
类型推导,属于RTTI(run time type Identification),当程序运行起来的时候对对象的类型进行识别。
int main()
{
int a = 10;
double e = 3.14;
auto c = a + e;
//拿到对象名字的字符串
cout << typeid(c).name() << endl;
string s;
cout << typeid(s).name() << endl;
//假设现在我想定义一个和c类型一样的变量但是我不知道c的类型,那么我就可以用对象去推类型
decltype(c) d;
decltype(s) s1;
cout << typeid(d).name() << endl;
cout << typeid(s1).name() << endl;
return 0;
}
详见范围for 范围for在底层也是使用迭代来遍历容器的
final修饰类的时候,这个类被称为最终类,这个类不能再被其它类继承,final修饰虚函数时,这个虚函数不能被重写。
override是检查子类继承的虚函数是否完成了重写。 不满足重写则报错
详见 智能指针
用红色圈起来的是c++11增加的新容器,array是定长数组,也就是说它的大小是固定的,并且空间是开辟在栈空间上的。forward_list是单向链表,单向链表有的缺陷它也存在,所以不是很常用。
unordered_map和unordered_set的效率很高,详见
容器的一些新方法
如果我们再仔细去看看会发现每个容器中都增加了一些c++11的方法,但是其实很多都用得比较少。
比如提供了cbegin和cend()方法返回const迭代器等等,但是实际上用的都不多,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
实际上C++11更新以后 ,容器中增加的新方法 最后用于插入接口函数的右值引用版本。
这些接口可以提高效率。如何提高效率的请接着向下看。
map的右值引用
vector的右值引用
如何指定显示生成默认的构造函数,要用到关键字default。 下面这种情景,我们已经自己实现了拷贝构造函数,但是这时候想要使用默认的构造函数已经不可能了,所以这时候就要用到default。
class A
{
public:
A() = default;
A(const A& a)
:_a(a._a)
{ }
private:
int _a = 10;
};
int main()
{
A a1;
A a2(a1);
return 0;
}
如果要求对象不能拷贝和赋值,C++98的做法是:对象的拷贝构造函数和operator=只声明不实现,并且加private访问限定符。
class B
{
public:
B() = default;//指定显示生成默认的构造函数
//只声明不实现
private:
B(const B& bb);
B& operator=(const B& bb);
private:
int _b = 10;
};
C++11给出了更为简便的方法采用关键字delete.
class B
{
public:
B() = default;//指定显示生成默认的构造函数
//只声明不实现
B(const B& bb) = delete;
B& operator=(const B& bb) = delete;
private:
int _b = 10;
};
传统的C++语法中就有引用的语法存在,而C++11中新增了右值引用的的语法特性,所以从现在开始之前C++中的引用就叫做左值引用。无论是左值引用还是右值引用都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或者解引用的指针名),我们可以获取它的地址,也可以对它进行赋值。左值出现在赋值符号的左边,右值不能出现在赋值符号的左边。定义const修饰后的左值,也不能对它进行赋值,但是可以对它进行取地址,左值引用就是对左值的引用,对左值取别名。
int main()
{
//以下都是左值
int* p = new int{0};
int b = 1;
const int c = 2;
double e = 3.14;
//对左值的引用
int*& pp = p;
int& rb = b;
const int& rc = c;
//对临时变量的引用
const int& re = e;
return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如字面值常量,表达式返回值,函数返回值(这个不能是左值引用)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值引用不能取地址,右值引用就是对右值的引用,对右值取别名。
int main()
{
double x = 1.1;
double y = 2.2;
//以下都是常见的右值
x + y;
10;
fmin(x, y);
//以下是对右值的引用
int&& r1 = 10;
double&& d = x + y;
double&& r3 = fmin(x, y);
//以下会报错
10 = 1;
x + y = 2;
fmin(x, y) = 2.2;
return 0;
}
简答的理解就是左值就是在赋值符号的左边,右值是在赋值符号的右边,左值是可以被修改的,它相当于一块存储变量的空间,而右值是不能被修改的。但是左值也可以在赋值符号的右边对其它的值进行赋值,const修饰的左值是具有常属性的是不可以被修改的。
左值不可以直接引用右值,但是加了const修饰的左值是可以引用右值的,右值也不可以直接引用左值,但是经过移动(move)的左值是可以被右值引用的。
int main()
{
const int& x = 10;
int y = 9;
int&& ry = move(y);
return 0;
}
右值分为纯右值和将亡值。 纯右值就是内置类型的字面值常量,而将亡值就是自定义类型的临时对象。
string fun1()
{
string s1;
return s1;//这里这个函数返回的就是右值中的将亡值
}
int fun2()
{
int a = 10;
return a;//这里函数返回的就是右值中的纯右值
}
string的移动构造和移动赋值
左值引用的使用场景:
做参数和做返回值都可以提高效率。
void Func1(qyy::string s)
{
}
void Func2(const qyy::string& s)
{
}
int main()
{
qyy::string s1("hello world");
//Func1和Func2的调用我们可以看到引用做参数可以减少拷贝次数,提高效率的场景和使用价值
Func1(s1);
Func2(s1);
//string operator+=(char ch)传值返回存在深拷贝
//string &operator+=(char ch)传左值引用没有拷贝提高效率
s1 += '!';
return 0;
}
左值引用的短板:
当函数的返回值是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:
qyy::string Fun3(const qyy::string& s)
{
qyy::string s1(s);
return s1;//s1是一个局部的对象出了作用域就不存在了,所以不能用左值引用返回
}
Func3函数的返回值是一个右值,如果没有移动构造,调用的就是拷贝构造,因为const左值引用可以引用右值,这里就是一个深拷贝,但是有移动拷贝就会解决这个问题,就不会出现深拷贝。
不调用移动构造的结果:
右值引用和移动语义解决上述的问题:
在qyy::string中增加移动构造,移动构造的本质是窃取参数右值的资源占为己用,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
增加移构造的结果:
有移动构造的时候编译器会根据左值和右值的不同去匹配最适合的构造函数。
不仅仅移动构造可以解决很多问题,移动赋值也可以解决问题: 比如:使用临时对象构造给string进行赋值,如果没有移动赋值,就会调研拷贝赋值,就会是一次深拷贝,但是如果有移动赋值就可以避免深拷贝。
qyy::string Fun3(qyy::string &&s1 )
{
qyy::string s(s1);
return s;
}
int main()
{
/*qyy::string s("hello world");
qyy::string ret = Func3(s);*/
qyy::string s1;
s1 = Fun3(qyy::string("hello qyy"));
return 0;
}
或者是在vector
在STL中的容器都增加了移动拷贝和移动赋值。
https://cplusplus.com/reference/string/string/string/
https://cplusplus.com/reference/vector/vector/vector/
emplace 开头的都是使用了右值引用作为函数的参数来提升效率,所有有人建议同样的接口应该优先使用带emplace开头的版本,比如在vector中的接口push_back和emplace_back,如果按照上述说的应该优先使用emplace_back,但是实际上这种说法是不够准确的,因为在push_back的实现中也是有实现右值 引用作为参数函数重载的实现:
所以在支持C++11的编译器上使用push_back也是可以提升效率的,真正不同的是emplace开头的函数实现了右值引用的可变参数的形式:
使用起来更加灵活方便。
按照语法,右值引用只能引用右值,但是右值引用一定不能引用左值吗?因为在有些场景下,可能存在右值引用引用左值实现移动语义的情况。当需要右值去引用一个左值的时候,可用通过move函数左值转化为右值。C++11中std::move()函数位于头文件中,该函数的名字具有迷惑性,它并不是搬运东西,唯一的功能是将一个左值强制转化为一个右值,然后实现移动语义。
右值在多次函数传参调用的过程中它的性质可能会发生变化,变为左值,例如:
void Fun(int& x)
{
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << "const 左值引用" << endl;
}
void Fun(int&& x)
{
cout << "右值引用" << endl;
Fun(x);//这里再次调用Fun函数时,x就会变成一个左值
}
void Fun(const int&& x)
{
cout << "右值引用" << endl;
}
int main()
{
int a = 0;
Fun(move(a));//调用Fun的右值引用
return 0;
}
模版中&&不代表右值引用而是万能引用,其既能接受左值也能接受右值,模版中的万能引用只是提供了接受左值和右值的能力,但是引用类型的唯一作用就是限制接受的类型,在后续的使用中都退化成了左值,我们希望能在传递过程中保持它的左值或者右值属性,就需要用到完美转发了。
void Fun(int& x)
{
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << "const 左值引用" << endl;
}
void Fun(const int&& x)
{
cout << "右值引用" << endl;
}
void Fun(int&& x)
{
cout << "右值引用" << endl;
}
//模版中&&不代表右值引用而是万能引用,其既能接受左值也能接受右值
//模版中的万能引用只是提供了接受左值和右值的能力
//但是引用类型的唯一作用就是限制接受的类型,在后续的使用中都退化成了左值
//我们希望能在传递过程中保持它的左值或者右值属性,就需要用到完美转发了
template
void PerfectForward(T&& t)
{
Fun1(std::forward(t));
}
int main()
{
PerfectForward(10);//右值
int a = 10;
PerfectForward(a);//左值
PerfectForward(std::move(a));//右值
const int b = 9;
PerfectForward(b);//const左值
PerfectForward(std::move(b));//const右值
return 0;
}
我们可以使用forward来达到我们的目的。
lambda表达式的介绍
线程库介绍