本节目标
- c++11简介
- 列表初始化
- 变量类型推导
- 范围for循环
- 新增加容器
- 右值
- 新的类功能
- 可变参数模板
1. c++11简介
在2003年标准委员会提交了一份计数勘误表(简称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中孕育出来的新语言。相比较而言,c++11能更好的用于系统开发和库开发、语法更加泛化和简单化,更稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用的比较多,所以要作为一个重点去学习。c++11增加的语法特性篇幅很多,没办法一一讲解
https://en.cppreference.com/w/cpp/11
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++03的下一个版本的时候,一开始计划2007年发布,所以最初这个标准叫C++07。但是到06年的时候,官方觉得2007年肯定完不成C++07,而且官方觉得2008年可能也完不成。最后干脆叫C++0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11
2. 统一的列表初始化
2.1 {} 初始化
在c++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如
struct Point
{
int _x;
int _y;
};
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
c++11扩大了用大大括号的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义类型,使用初始化列表时,可添加等号(=),也可不添加
int x1 = 1;
int x2{ 2 };
//new也可以列表初始化
int* pa = new int[4] {0};
创建对象u额可以用列表初始化方式调用构造函数初始化,但有本质区别,是先用括号的内容构造一个临时对象,再拷贝构造给初始化对象,会优化为直接构造
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;
}
private:
int _year;
int _month;
int _day;
};
//创建对象列表初始化
Date d1( 2022, 1 ,1 ); //旧初始化方式
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
//单参数构造函数支持隐士类型转换
string str = "xxxxx";
//证明,这里不能优化,所以转不了,必须加const
const Date& d4 = { 2023, 11 , 5 };
2.2 std::initializer_list
介绍文档
http://www.cplusplus.com/reference/initializer_list/initializer_list/
是什么类型:
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
Date d1( 2022, 1 ,1 );
vector<int> v1 = { 0, 1, 2, 3, 4 };
可以使用迭代器遍历
initializer_list<int> l2 = { 0, 1, 2, 3, 4 };
initializer_list<int>::iterator it = l2.begin();
while (it != l2.end())
{
cout << *it << " ";
it++;
}
这里的v1和d1不一样,上面的是构造的对象,下面是先构造的initializer_list类型,v1的参数个数可以是随意的,d1只能是三个
使用场景
std::initializer_list一般是作为构造函数的参数,c++11对stl中的不少容器增加了它作为参数的构造函数,这样初始化容器就方便多了。也可以作为operator=的参数,就可以用大括号赋值
让vector也支持{}初始化和赋值
vector(std::initializer_list<T> lt)
{
Reserve(lt.size());
for (auto& e : lt)
{
PushBack(e);
}
}
vector<T>& operator=(initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
当参数的个数和构造函数匹配时会识别为对象,不匹配时会认为是initializer_list类型
3. 声明
c++11通了多种简化声明的方式,尤其是在使用模板时
3.1 auto
c++98中auto是一个存储类型的说明符,表明变了是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。c++11废除auto原来用法,将其用于实现自动类型判断,这样要求必须显示初始化,让比那一期将定义对象的类型设置为初始化值的类型
typeid可以获得变量的类型字符串
int i = 10;
auto p = i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
3.2 decltype
上面可以推导类型,但推导的类型不能用来创建变量,如果想根据某个变量类型推导并创建变量,可以用decltype,将变量的类型声明为表达式指定的类型
double y = 2.2;
decltype(y) ret = 3.3;
cout << typeid(ret).name() << endl;
3.3 nullptr
由于c++中NULL被定义为字面量0,这样就可能带来一些问题,0既指针常量,又表示整形常量。所以出于清晰和安全的角度考虑,c++11新增了nullptr,表示空指针
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4.范围for循环
略
5. stl一些变化
圈起来的是几个新容器,但是实际最有用的是unordered_map和unordered_set。
array和内置数组相比,越界访问会报错
容器的新方法
增加的新方法都用的比较少。比如提供了cbegin和cedn方法返回const迭代器等待,但意义不大,begin和end也可以返回const迭代器,属于锦上添花的操作
插入接口函数增加了右值版本
http://www.cplusplus.com/reference/vector/vector/emplace_back/
意义在哪,说能提高效率,如何提高的
6. 右值引用和移动语义
6.1 左值引用和右值引用
传统c++语法就有引用,c++11新增了右值引用特性,无论是左值还是右值引用,都是给对象取别名
左值是一个表示数据的表达式(变量名或解引用的指针),可以获取它的地址,可以赋值,左值可以出现在赋值符号左边,右值不能出现在赋值符号左边。定义时const修饰后的左值,不能赋值,但可以取地址。左值引用就是给左值的引用,取别名
int* p = new int(0);
int b = 1;
const int c = 2;
//以下都是左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
什么是右值,什么是右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值、函数返回值(这个不能是左值引用返回)等待,右值可以出现在赋值符号的右边,不能出现在左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
double x = 1.1, y = 2.2;
//以下是常见的右值
10;
x + y;
fmin(x, y);
//以下几个都是右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
//编译会报错,error c2106, "=":左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
6.2 左值引用和右值引用比较
左值引用总结:
1.左值引用只能引用左值,不能引用右值,但是const左值引用既可以引用左值,也可以引用右值
2.右值引用不能引用左值,move的可以
//左值引用不能给右值取别名,const左值可以
int& r1 = 10;
const int& r2 = 10;
//右值引用不能给左值取别名,move可以
int i = 10;
int&& rr3 = i;
int&& rr4 = move(i);
6.3 左值引用使用场景和意义
左值做参和返回值都可以提高效率,减少了拷贝
#pragma once
#include <string>
#include <iostream>
#include <assert.h>
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
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)
{
::std::swap(_str, s._str);
::std::swap(_size, s._size);
::std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 深拷贝" << std::endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
左值引用的短板
当函数返回对象是一个局部变量,除了函数作用域就不存在了,不能使用左值引用返回,只能传值返回。例如:string to_string(int value)函数中可以看到,这里只能传值返回,传值返回会导致至少一次拷贝构造(如果旧一点的编译器可能是两次拷贝构造)
string to_string(int value)
{
string ret;
while (value)
{
int x = value % 10;
value /= 10;
ret += '0' + x;
}
std::reverse(ret.begin(), ret.end());
return ret;
}
旧编译器会产生两次拷贝构造,返回的ret对象拷贝一次,赋值的时候也会调用一次赋值重载。既然ret已经是一个要销毁的对象了,多次拷贝就会造成资源的浪费。下面第一个是连续的构造和拷贝构造都可以优化一次拷贝构造
下面这个无法优化
右值引用和移动语义解决上述问题
右值可以分为:
1.纯右值,内置类型右值
2.将亡值,自定义的右值
移动构造本质是将参数右值的资源窃取过来,占为己有,不做深拷贝,叫它移动构造,就是窃取别人的资源构造自己。所以可以实现拷贝和赋值的移动版本
编译器会选择最匹配的调用,to_string返回的是右值,如果既有拷贝又有右值,就会匹配移动构造
//移动拷贝
string(string&& s)
{
std::cout << "string(string&& s) -- 移动语义" << std::endl;
swap(s);
}
//移动赋值
string& operator=(string&& s)
{
std::cout << "string& operator=(string s) -- 移动语义" << std::endl;
swap(s);
return *this;
}
s和ret的字符串是同一个
运行后调用了一次移动构造和移动赋值,因为如果用一个已经存在的对象接收,编译器没办法优化,to_string函数中先用str生成构造一个临时对象,但是可以看到,编译器把str识别成了右值,调用了移动构造,然后把临时对象作为to_string函数调用的 返回值赋值给ret1,调用的移动赋值
stl容器都增加了移动构造和移动赋值
string s1("hello");
string s2 = s1;
string s3 = std::move(s1);
6.4 右值引用左值及一些深入的使用场景
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?有些场景下,需要用右值去引用左值实现移动语义。当需要右值引用左值时,可以通过move函数将左值转化为右值,c++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,并不搬移任何东西,唯一的功能是将一个左值强制转换为右值使用,实现移动语义
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
//move会改为右值,s1的资源置空,转移给了s3
string s1("hello");
string s2 = s1;
string s3 = std::move(s1);
//move的返回值是右值,并不改变变量本身
string s4 = s1;
stl容器也加入了右值引用版本
std::list<string> l1;
string s1 = "hello";
//左值
l1.push_back(s1);
//右值
l1.push_back(to_string(1234));
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
修改list
在前面的list中加入右值插入的版本,先把pushback函数加入右值,这时还是会有深拷贝
右值被右值引用以后得属性是左值,编译器设计因为右值引用需要被修改
这里x传入下层又变为了左值,需要传给inert的右值版本move后的,insert函数里创建节点也需要再次move
__list_node(T&& x)
: _prev(nullptr)
, _next(nullptr)
, _data(std::move(x))
{}
void push_back(T&& x)
{
Insert(end(), std::move(x));
}
void Insert(iterator pos, T&& x)
{
node* new_node = new node(std::move(x));
//记录前后节点
node* pre = pos.node_iterator->_prev;
node* cur = pos.node_iterator;
//连接
pre->_next = new_node;
new_node->_prev = pre;
new_node->_next = cur;
cur->_prev = new_node;
}
list<string> l1;
string s1 = "hello";
l1.push_back(s1);
l1.push_back(to_string(1234));
6.5 完美转发
模板中的&&万能引用
void fun(int& x) { std::cout << "左值引用" << std::endl; };
void fun(const int& x) { std::cout << "const 左值引用" << std::endl; };
void fun(int&& x) { std::cout << "右值引用" << std::endl; };
void fun(const int&& x) { std::cout << "const 右值引用" << std::endl; };
// 模板的&&不是右值引用,是万能引用,既能接收左值,也能右值
// 引用类型的唯一作用是限制了接收的类型,后续使用都退化成了左值
// 想要保持左值和右值的属性,要使用完美转发
template <typename T>
void PerfectForward(T&& t)
{
fun(t);
};
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(std::move(a)); //右值
const int b = 8;
PerfectForward(b); //左值
PerfectForward(std::move(b)); //右值
上面的t后续都退化成了左值,想要保持传入的属性,就要加入std::forward保留属性
fun(std::forward<T>(t));
使用场景
容器的插入等可以直接使用完美转发,代替左值和右值两个版本
7. 新的类功能
默认成员函数
原来c++类中,有6个默认成员函数:
1.构造函数
2.析构函数
3.拷贝构造函数
4.拷贝赋值重载
5.取地址重载
6.const取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的
c++11新增了两个:移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果没有实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员汇之星逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
- 如果没有实现移动赋值重载,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员汇之星逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动,没有实现就调用拷贝构造
- 如果提供了移动构造或者移动赋值,编译器就不会自动提供拷贝构造和拷贝赋值
类成员变量初始化
c++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化
强制生成默认函数的关键字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即可,指示编译器不生成对应函数的默认版本,修饰的为删除函数
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
}
继承和多态中的final与override关键字
final修饰类或虚函数,表示不可被继承或重写。override检测虚函数是否完成重写
8. 可变参数模板
c++11的新特性可变参数模板能够创建可以接收可变参数的函数模板和类模板,相比c++98/03,类模板和函数模板中只能含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。然而由于可变模板参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。掌握一些基础的可变模板参数特性就可以了
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模板参数,把带省略号的参数称为“参数包”,里面包含了0到N(N>0)个模板参数。无法直接获取参数包args中的每个参数,只能通过展开参数包的方式获取每个参数,这时使用可变模板参数的一个主要特点,也是最大的难点,如何展开可变模板参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们用一些特殊方式一一获取参数包
递归函数展开参数包
// 递归终止函数
void _ShowList()
{
std::cout << std::endl;
}
//展开函数
template <class T, class ...Args>
void _ShowList(const T& value, Args ...args)
{
std::cout << value << " ";
_ShowList(args...);
}
template <class ...Args>
void ShowList(Args ...args)
{
_ShowList(args...);
}
int main()
{
ShowList(1, 2, 'x');
ShowList(1, 2, 3.5);
return 0;
}
逗号表达式展开
这种方式展开参数包,不需要通过递归终止函数,是直接在expand函数体中展开的,printarg不是一个递归终止函数,指示一个处理参数包每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。逗号表达式会按顺序执行,返回最后一个
expand函数中的逗号表达式也(printarg(args),0),也是按照这个执行顺序,限制性printarg(args),在得到逗号表达式的结果0,同时还用到了c++11的另外一个特性–初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0}将会展开成(printarg(arg1), 0),(printarg(arg2), 0), (printarg(arg3), 0), etc…),最终会创建一个元素值都为0的数组Int arr[sizeof…(args)]。由于是逗号表达式,在创建数组的过程中回显执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个参数的目的纯粹是为了在数组构造的过程展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
stl容器中empalce相关接口
template <class... Args>
void emplace_back (Args&&... args);
emplace系列接口,支持模板的可变参数,并且万能引用。那么相对insert优势在哪里
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
emplace是由模板参数包直接传入参数,不会拷贝一个临时对象。而pushback需要拷贝构造或移动构造。移动构造的消耗也不是很高
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, bit::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}