??接下来的几篇博客,我会给大家介绍C++11的相关内容,今天主要介绍一部分基本语法,有一部分C++11的语法在前面的博客中有介绍过,例如:范围for、final和override关键字和nullptr这样的表示空指针关键字等
??博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code
目录
- C++11简介
- 列表初始化
-
- 用法
- initializer_list
- 变量类型推导
-
- auto类型推导
- decltype类型推导
- 运行时类型推导
- 右值引用和移动语义
-
- 右值引用
- 右值引用的移动语义
- move
- STL容器增加了右值引用
- 完美转发和万能引用
- 新的类功能
-
- 新增的两个默认成员函数
- 两个关键字——default和delete
- 可变参数模板
- 总结
C++11简介
在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++98发布后13年来第一次重大修正。列表初始化 用法 在C++98中,
{}
只能够对数组元素进行统一的列表初始化,但是对应自定义类型,无法使用{}
进行初始化,如下所示:// 数组类型
int arr1[] = { 1,2,3,4 };
int arr2[6]{ 1,2,3,4,5,6 };
// 自定义类型(C++98不支持下面这种初始化的方式)
vector v{ 1,2,3 };
在C++11中,扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加,如下所示:
// 内置类型变量
int a{ 2 };
int b = { 3 };
int c = { a + b };
// 动态数组 C++98不支持
int* arr = new int[5]{ 1,2,3,4,5 };
// 容器使用{}进行初始化
// vector v = { 1,2,3 };
vector v{ 1,2,3 };
// 等号可以省略不写
map m{ {1,1},{2,2},{3,3} };
自定义类型的列表初始化:
下面是自己定义的一个类:
class Point
{
public:
Point(int x, int y)
:_x(x)
,_y(y)
{}
void PrintPoint()
{
printf("点的坐标为:(%d, %d)\n", _x, _y);
}
private:
int _x;
int _y;
};
创建一个
Point类
并使用{}
对其进行列表初始化,具体如下:int main()
{
// 自定义类型列表初始化
Point p{ 1, 2 };
p.PrintPoint();
size_t i = 0;
return 0;
}
代码运行结果如下:
文章图片
可以看出,列表初始化对应自定义类型都是支持的。
initializer_list
文章图片
initializer_list
一般是作为构造函数的参数,C++11对STL中的不少容器就增加initializer_list
作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值对应多个对象的列表初始化,必须支持一个带有initializer_list
类型参数的构造函数。注意: 这种类型的对象由编译器根据初始化列表声明自动构造,初始化列表声明是用
{}和,
容器使用
initializer_list
作为构造函数的参数的例子:- vector
文章图片
文章图片
- list
文章图片
- map
文章图片
template
class Vector
{
public:
Vector(initializer_list l)
:_size(0)
,_capacity(l.size())
{
_a = new T[_capacity];
for (auto e : l)
{
_a[_size++] = e;
}
}
Vector& operator=(initializer_list l)
{
delete _a;
_size = 0;
_capacity = l.size();
_a = new T[_capacity];
for (auto e : l)
{
_a[_size++] = e;
}return *this;
}
private:
T* _a;
size_t _size;
size_t _capacity;
};
int main()
{
Vector v1 = { 1,2,3 };
Vector v2 = { 3,5,7,9 };
v2 = { 1,2,3 };
return 0;
}
变量类型推导 auto类型推导
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。注意: auto声明的类型必须要进行初始化,否则编译器无法推导出auto的实际类型。auto不能作为函数的参数和返回值,且不能用来直接声明数组类型
实例演示:
int main()
{
int a = 10;
auto pa = &a;
auto& ra = a;
// 声明引用类型要加& cout << typeid(a).name() << endl;
cout << typeid(pa).name() << endl;
cout << typeid(ra).name() << endl;
return 0;
}
代码运行结果如下:
文章图片
decltype类型推导 decltype是根据表达式的实际类型推演出定义变量时所用的类型。且还可以使用推导出来的类型进行变量声明。
实例演示:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
// 用decltype自动推演a+b的实际类型
decltype(a + b) c = 10;
cout << typeid(c).name() << endl;
// 不带参数,推导函数类型
cout << typeid(decltype(Add)).name() << endl;
// 带参数,推导函数返回值类型,注意这里不会调用函数
cout << typeid(decltype(Add(1,1))).name() << endl;
return 0;
}
代码运行结果:
文章图片
注意: decltype不可以作为函数的参数,编译时推导类型
运行时类型推导 C++98中确实已经支持RTTI(运行时类型识别):
- typeid只能查看类型不能用其结果类定义类型(上面两个例子都使用了)
- dynamic_cast只能应用于含有虚函数的继承体系中(后面会详细介绍)
- 运行时类型识别的缺陷是
降低程序运行的效率
- typeid只能推导类型,但是不能使用类型声明和定义变量
在之前的博客中,我已经介绍过了引用的语法,那里的引用都是左值引用。C++11新增了右值引用的语法特性,给右值取别名。左值引用和右值引用都是给对象取别名,只不过二者对象的特性不同。注意: 左值引用用符号
&
,右值引用的符号是&&
左值、左值引用。右值和右值引用:
专有名词 | 概念 |
---|---|
左值 | 一个表示数据的表达式,可以取地址和赋值,且左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边,例如:普通变量、指针等,const修饰后的左值不可以赋值,但是可以取地址,所以还是左值 |
左值引用 | 给左值的引用,给左值取别名 ,例如:int& ra = a; |
右值 | 一个表示数据的表达式,右值不能取地址,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等 |
右值引用 | 给右值的引用,给右值取别名,例如:int&& ra = Add(a,b) |
int Add(int x, int y)
{
return x + y;
}int main()
{
int a = 10;
// 左值,可以取地址
int& ra = a;
// 左值引用 int&& ret = Add(3, 4);
// 函数的返回值是一个临时变量,是一个右值 return 0;
}
总结1:
- 左值
- 普通类型的变量,可以取地址
- const修饰的常量,可以取地址,也是左值
- 如果表达式运行结果或单个变量是一个引用,则认为是右值
- 右值
- 纯右值:基本类型的常量或临时对象,如:a+b,字面常量
- 将亡值:自定义类型的临时对象用完自动完成析构,如:函数以值的方式返回一个对象
int main()
{
int a = 10;
int& r1 = a;
// 左值引用
//int& r2 = 10;
// error,左值引用不可以引用右值(这是因为权限放大了,所以不行)
const int& r2 = 10;
// const的左值引用可以引用右值(这是因为权限不变,所以可以) return 0;
}
总结2:
- 左值引用不可以引用右值
- 加了const的左值引用既可以引用左值也可以引用右值
int main()
{
int&& r1 = 10;
// 对字面常量进行引用(右值引用)
r1 = 20;
// 10原本是一个字面常量,无空间存储,右值引用后会开一块空间把值存起来,可以取地址
cout << &r1 << endl;
int a = 10;
// int&& r2 = a;
// error,右值引用不可以引用左值
int&& r2 = move(a);
// move后的左值可以引用,a的属性不变,引用的是move的返回值 return 0;
}
总结3:
- 右值引用不可以引用左值
- 右值引用可以引用move后的左值
问题提出:
先看下面简单的移动代码:
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
, _size(0)
{
strcpy(_str, str);
_str[_size] = '\0';
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
, _size(s._size)
{
cout << "深拷贝" << endl;
strcpy(_str, s._str);
}
String& operator=(String& s)
{
if (this != &s)
{
cout << "深拷贝" << endl;
delete _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
_size = s._size;
_str[_size] = '\0';
}return *this;
}
~String()
{
delete _str;
}
private:
char* _str;
size_t _size;
};
String func(String& str)
{
String tmp(str);
return tmp;
}
int main()
{
String s1("123");
//
String s2(s1);
String s3(func(s1));
return 0;
}
代码运行结果如下:
文章图片
分析结果:
第一次深拷贝是因为s1拷贝构造s2,这里都不难理解。主要看后两次,s1传参过程不发生深拷贝,因为这里是传引用,接着就是str拷贝构造tmp,这里会发生一次深拷贝,紧接着就是返回tmp,tmp会先拷贝构造一个临时对象(这里会发生一次深拷贝),然后临时对象拷贝构造给s3(这里会发生一次深拷贝),连续两次拷贝构造会被编译器优化成一次,这也就是我们上面看到的两次深拷贝。
分析问题:
在上面的代码中,可以发现,func中的tmp、返回是构造的临时对象和s3都有一个独立的空间,且内容是相同的,这里相当于创建了3个内容完全相同的对象,这是一种极大的浪费,且效率也会降低。
如何解决?
移动语义来解决
文章图片
这里解决其实就是进行资源的转移,这里加上一份移动构造的代码,如下:
String(String&& s)
:_str(s._str)
{
// 对于将亡值,内部做移动拷贝
cout << "移动拷贝" << endl;
s._str = nullptr;
}
代码运行结果如下:
文章图片
因为返回的临时对象是一个右值,所以会调用上面的移动构造的代码对返回的临时对象进行构造,本质是资源进行转移,此时tmp指向的是一块空的资源。最后返回的临时对象拷贝构造给s3时还是调用了移动构造,两次移动构造被编译器优化为一个。可以看出的是这里解决的是减少接受函数返回对象时带来的拷贝,极大地提高了效率。
除了移动构造,我们还可以增加移动赋值,具体如下:
String& operator=(String&& s)
{
cout << "移动赋值" << endl;
_str = s._str;
_size = s._size;
s._str = nullptr;
return *this;
}
演示:
int main()
{
String s1("123");
String s2("ABC");
s2 = func(s1);
return 0;
}
运行结果如下:
文章图片
可以看出的是func返回的临时对象是通过移动赋值给s2的,也是一次资源的转义
注意:
- 移动构造和移动赋值函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造和移动赋值,该移动构造和移动赋值为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造和移动赋值。
右值引用和左值引用减少拷贝的场景:
- 作参数时: 左值引用减少传参过程中的拷贝。右值引用解决的是传参后,函数内部的拷贝构造
- 作返回值时: 如果出了函数作用域,对象还存在,那么可以使用左值引用减少拷贝。如果不存在,那么产生的临时对象可以通过右值引用提供的移动构造生成,然后通过移动赋值或移动构造的方式将临时对象的资源给接受返回值者
move: 当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。注意:
- 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量value不会被销毁。
- STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
int main()
{
String s1("123");
String s2(move(s1));
return 0;
}
代码运行结果如下:
文章图片
需要知道的是,move后的s1变成了一个右值,所以会调用移动构造去构造s2,这样s1的资源就被转移给了s2,s1本身也没有资源了,如下:
文章图片
STL容器增加了右值引用 列举一部分:
- list的尾插
文章图片
- vector的尾插
文章图片
完美转发和万能引用
- 完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
- 万能引用: 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
如下:
void Fun(int& x) { cout << "左值引用" << endl;
}
void Fun(const int& x) { cout << "const 左值引用" << endl;
}
void Fun(int&& x) { cout << "右值引用" << endl;
}
void Fun(const int&& x) { cout << "const 右值引用" << endl;
}
// std::forward(t)在传参的过程中保持了t的原生类型属性。
template
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10);
// 右值
int a;
PerfectForward(a);
// 左值
PerfectForward(std::move(a));
// 右值
const int b = 8;
PerfectForward(b);
// const 左值
PerfectForward(std::move(b));
// const 右值
return 0;
}
代码运行结果如下: 属性丢失
文章图片
解决: 使用完美转发能够在传递过程中保持它的左值或者右值的属性
template
void PerfectForward(T&& t)
{
Fun(std::forward(t));
}
代码运行结果如下: 属性保持了
文章图片
总结: 右值引用在传参的过程中移动要进行完美转发,否则会丢失右值属性
新的类功能 新增的两个默认成员函数 在类和对象的博客中,已经介绍了类的6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
- 移动构造函数
- 移动赋值运算符重载
- 如果没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个
默认移动构造
。 - 默认生成的移动构造函数,对于
内置类型成员
会按照字节序进行浅拷贝,自定义类型成员
,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造
,没有实现就调用拷贝构造
。 - 如果没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个
默认移动赋值
。 - 默认生成的移动构造函数,对于
内置类型成员
会按照字节序进行浅拷贝,自定义类型成员
,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值
,没有实现就调用拷贝赋值
。
实例演示: 为了方便观察,这里我使用自己简单模拟实现的string来进行演示。拿析构函数做演示,有析构函数和没有析构函数,两种情况下,使用右值对象构造一个对象和使用右值对象给一个对象赋值,观察会调用哪个函数
namespace Simulation
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
// 不包含最后做标识的\0
};
}
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{}
~Person()
{}
/*~Person()
{}*/
private:
Simulation::string _name;
int _age;
};
int main()
{
Person p1("xiaoming", 20);
Person p2("xiaohong", 19);
// 拷贝构造or移动构造
Person p3 = std::move(p1);
// 拷贝复制or移动赋值
p2 = std::move(p3);
return 0;
}
有自己实现的析构函数时,代码运行结果如下:
文章图片
无自己实现的析构函数时,代码运行结果如下:
文章图片
验证的结果和我们预想是一样的,有自己实现的构造函数时,编译器不会生成一个默认的移动构造和移动赋值,此时会调用Person的默认拷贝构造和默认移动赋值函数,string就会去调用自己的拷贝构造和拷贝赋值。如果无自己实现的析构函数,编译器会自动生成一个默认的移动构造和移动赋值,此时会调用Person的默认的移动构造和移动赋值,string就会去调用自己的移动构造和移动赋值。
如果自己实现了上述的其他的几个默认成员函数(析构函数 、拷贝构造、拷贝赋值重载),也是不会自动生成默认的移动赋值和移动构造的,具体大家自己也可以去验证,效果是一样的。
两个关键字——default和delete C++11可以让你更好的控制要使用的默认函数。你可以强制生成某个默认成员函数,也可以禁止生成某个默认成员函数,分别用的的关键字是——default和delete
如下:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{}
Person(Person&& p) = default;
// 强制生成默认的
Person& operator=(Person&& p) = default;
Person(Person& p) = delete;
// 强制删除默认的
~Person()
{}
private:
Simulation::string _name;
int _age;
};
可变参数模板 C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
语法规范如下:
template
void fun(Args ...args)
{}
说明几点:
- Args和args前面有省略号,所以它们是可变参数,带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数
- Args是一个
模板参数包
,args是一个函数形参参数包
- 递归函数展开参数包
递归调用ShowList,当参数包中的参数个数为0时,调用无参的ShowList,这算是一个递归终止的函数,args中第一个参数作为value传参,参数包中剩下的参数作为新的参数包传参
void ShowList()
{
cout << endl;
}template
void ShowList(T value, Args ...args)
{
cout << value << " ";
// 递归调用ShowList,当参数包中的参数个数为0时,调用上面无参的ShowList
// args中第一个参数作为value传参,参数包中剩下的参数作为新的参数包传参
ShowList(args...);
}
int main()
{
ShowList(1, 'A');
ShowList(3, 'a', 1.23);
ShowList('a', 4, 'B', 3.3);
return 0;
}
运行结果如下:
文章图片
- 逗号表达式展开参数包
如果一个参数包中都是一个的类型,那么可以使用该参数包对数组进行列表初始化,参数包会展开,然后对数组进行初始化。先看下面一个例子:
void ShowList(Args ...args)
{
int arr[] = { args... };
// 列表初始化
}
int main()
{
ShowList(1, 2, 3, 4, 5);
return 0;
}
如果参数包中的参数不是同一类型就会报错。但是我们可以利用列表初始化数组时,展开参数包的特性,再与一个逗号表达式结合使用,可以展开都会表达式中的参数,如下:
template
void PrintArg(T value)
{
cout << value << " ";
}
template
void ShowList(Args ...args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
都会表达式会按顺序执行,先执行第一个函数,然后把最后的0值赋给数组,这样就把参数包展开了
展开
(PrintArg(arg1), 0),(PrintArg(arg2), 0),(PrintArg(arg3), 0),(PrintArg(arg4), 0)
计算参数包大小:
sizeof...(args)
emplace系列接口:
文章图片
文章图片
上面的
&&
是万能引用,实参是左值,参数包的这个形参就是左值引用,实参是右值,参数包的这个形参就是右值引用emplace_back和push_back进行对比:
- emplace_back可以支持可变参数包,然后调用定位new,使用参数包对空间进行初始化,push_back不可以如下:
list> lt;
lt.emplace_back(1, "hehe");
- push_back可以使用初始化列表进行类似的操作,但emplace_back不可以,因为push_back知道传进来的参数的类型,使用初始化列表初始化pair,然后插入节点。emplace_back接受的是一个参数包,参数不能够匹配,所以无法使用,如下:
list> lt;
lt.push_back({ 2, "haha" });
- 对于右值对象,emplace_back是先构造一个对象,移动构造,push_back也是如此
- 对于左值对象,都是先构造一个左值对象,再拷贝构造
- 对于参数包,emplace_back直接使用参数包用定位new构造
文章图片
推荐阅读
- JavaSE|【JavaSE】枚举类——规范的类
- Java|JVM 类加载机制与加载过程
- #|HashSet源码学习
- 暑期实训|1.4.C语言基础进阶——红黑树
- python-海洋数据处理|python 基于metpy计算位涡平流项(水平)
- 如何在C ++中打印Pascal三角形
- 如何解决C++错误C4996’getch’(不建议使用此项目的POSIX名称。而是使用符合ISO C和C ++的名称:_getch)
- 感悟|Java后端学习体系(韩顺平)
- 笔记|初识Java Bean