写在前面
今天我们谈谈C++的一些语法,这些语法是C++11标准下新增的.有的人感觉学C++很难,那么C++11标准出来之后你会发现学习的成本又增加了.C++11增添了很多特性,有有用的,有"无用"的,有的人甚至调侃C++11标准出来后,C++越来越感觉像一门新语言.这个博客我们简单的谈一谈C++11比较常用的一些特性.
初始化列表
首先我们要说明这个和类和对象构造函数那里的初始化列表没有半毛钱的关系.在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定.对数组初始化我们是常用的.
struct Person
{
char name[20];
int age;
char sex[10];
};
int main()
{
int arr[10] = { 1,2,3,4,5,6 };
struct Person per = { "张三",18,"男" };
return 0;
}
C++11标准出来后,我们可以用{}干更多的事情,例如使用{}对内置类型和自定义类型进行初始化,这个就涉及到初始化列表.下面三个变量的初始化的值是一样的.
int main()
{
int x = 10;
int y = { 10 };
int z{ 10 };
return 0;
}
甚至我们在C++98那里初始化方式里面的等号也可以省略.
struct Person
{
char name[20];
int age;
char sex[10];
};
int main()
{
int arr[10]{ 1,2,3,4,5,6 };
struct Person per{ "张三",18,"男" };
return 0;
}
有的人可能会感到疑惑,我们这么些好像麻烦了,这里我们之前的方法不是挺好的吗?现在倒是多此一举了.实际上语法上支持这样做是为了后面的方法.
在我们谈动态内存管理的时候,我们谈过对于只new一个对象可以初始化,如果new多个对象这里我们就没有办法了,只能看对象的对应类的默认构造函数了.C++11的初始化列表解决了这个问题.
int main()
{
int* p1 = new int(1);
delete p1;
return 0;
}
我们先来看看如何new多个对象并初始化的,这个也支持不完全初始化和完全初始化.
int main()
{
int* p1 = new int[3]{ 0 };
int* p2 = new int[3]{ 1,2,3 };
delete[] p1;
delete[] p2;
return 0;
}
这里我们需要看一下什么是不完全初始化,和我们之前不够的补零是不是一样?
struct A
{
A(int a = 1)
:_a(a)
{}
int _a;
};
int main()
{
A* p = new A[3]{ 0 };
delete p;
return 0;
}
这里我们分析一下,对于不完全初始化,剩下的调用自己的默认构造函数.这里和我们C++98的语法是相匹配的.我们知道在C++中内置类型也被封装成类了,那么int的默认构造函数的的初始化值是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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1{ 2000, 1, 2 };
Date d3 = { 2000, 1, 3 };
Date* p1 = new Date[3]{ { 2022, 1, 2 },{ 2022, 1, 2 },{ 2022, 1, 2 } };
delete p1;
return 0;
}
这里我们先解释一下单参数的构造函数支持隐式类型转换.这里的本质是 { 2000, 1, 2 }先构造一个临时对象,后面再进行拷贝构造.这里编译器给优化了.我们这试一下不允许单参数的隐式类型转换.
class Date
{
public:
explicit 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;
};
int main()
{
Date d1 = { 2000, 1, 2 };
return 0;
}
initializer_list
我们需要解释一下我为何C++11可以支持上面的用法.实际上我们把{}封装成成一个类了,这个类就是initializer_list.
我们需要好好分析一下,这个究竟是什么.
int main()
{
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
这我们可以这么理解,我们把{}看作一个initializer_list类对象的数组,里面存储的一些对象,这些对象里面存储这我们需要的数据,编译器可以根据这些对象来推导出对应的数据类型.
int main()
{
auto il = { 10, 20, 30 };
auto it = il.begin();
while (it != il.end())
{
cout << *it << endl;
++it;
}
return 0;
}
我们已经知道了这个原理了,这我们需要看一看STL里面C++11出现后新增的构造函数.这里以vector为例子.
我们先来用一下,后面简单的说一下模拟实现就可以了.
int main()
{
vector<int> v = { 1,2,3,4,5,6 };
return 0;
}
这里我们需要简单的解释下.
我们这里模拟实现一下vector支持initializer_list的构造函数,这里倒是挺简单的,反正它支持迭代器,这里直接使用范围for
namespace bit
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
for (auto& data : l)
push_back(data);
}
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;
}
void push_back(const T& data)
{
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
auto
这个是新增的关键字,主要是让编译器自动推导变量的类型,前面我们已经谈过了,这里就不浪费大家的时间了.
int main()
{
map<string, string> m;
m.insert(make_pair("left", "左边"));
map<string, string>::iterator it1 = m.begin();
auto it2 = m.begin(); // 简单点
return 0;
}
范围for
这个也不谈了,支持迭代器就支持范围for.范围for的底层还是调用了迭代器.
int main()
{
vector<int> v = { 1,2,3,4,5,6 };
for (int val : v)
{
cout << val << " ";
}
return 0;
}
final override 和 array
final与override这是C++11增加的两个关键字.作用我们都谈过了.其中array也是槽点满满.
decltype
这个关键字可以通过一些表达式来自动推导变量的类型,而且还可以在变量命名的时候使用.
int main()
{
decltype(1) a = 10;
decltype(1.0*a) b = 1.00;
return 0;
}
右值引用
这个是C++11更新的很好的特性.我们需要重点谈谈.这个特性提高了程序的效率,但是在一定程度上增加了学习的成本.
右值 VS 左值
在C语言的时候我就想和大家分享,但是那时候知识储备还不够,自己的思路还没有整理出来,就放弃了.这里我也是从最简单的结论开始谈起.
首先我们要分析一下什么是左值什么是右值?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边定义时const修饰符后的左值,不能给他赋值,但是可以取地址.
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址.
这里我们需要完善一下自己的结论.可以取地址的叫做左值,不能取地址叫右值.这个是我们最终的结论.
右值引用
之前我们用的所有都是左值引用,这里就不谈了.所谓的右值引用就是给右值取别名,方法和左值一样,要求也和左值差不多.
int main()
{
10;
int&& a = 10; // 右值引用
return 0;
}
这里问题就来了,我们好象之前也对10进行过左值引用吧.这里我们先得到几个结论.
- 左值引用只能引用左值
- 右值引用只能引用右值
- const修饰的左值引用可以引用右值
我们还可以得到一个现象,我们不能对右值取地址,但是可以对右值引用取地址.
int main()
{
10;
int&& a = 10;
cout << &a << endl;
return 0;
}
右值引用的意义
这里我们就需要好好的分析一下了,前面我们说的是右值和右值引用的基本特点.这里我们需要谈一谈右值引用究竟有什么好处.
我们知道,右值引用是不能引用左值的,但是下面的方法可以使左值属性变成右值.这里我们就可以看到move只是改变了左值的属性,不会做其他的事情.
int main()
{
int x = 111;
int&& y = move(x);
return 0;
}
左值引用的缺点
我们开始好奇,为何会出现右值引用,这是由于左值引用有照顾不到的地方.我们知道左值引用的出现在引用传参的时候避免了深拷贝,提高了效率.但是我们知道有一个场景我们使用左值引用需要慎重考虑.我们返回引用的时候需要仔细思虑一下,不要返回函数内部的局部变量,这个出了函数是要被销毁的.那么我们只能值返回,这就需要拷贝构造,甚至需要深拷贝.这效率可就低了去了.
看一下杨辉三角的代码,你返回试试尤其当数据大的时候,这个效率更低.
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i<numRows; i++)
{
vv[i].resize(i + 1, 0);
}
// 遍历 vv
for (int i = 0; i<numRows; i++)
{
for (int j = 0; j<vv[i].size(); j++)
{
if (j == 0 || j == vv[i].size() - 1)
{
vv[i][j] = 1;
}
else
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
}
return vv;
}
};
这个时候就出现了右值引用了.我们先来看一下效果.
int main()
{
string s1("hello");
string s2 = move(s1);
return 0;
}
移动构造
移动构造就是我们使用右值引用重新写一个构造函数.当我们传入左值的时候就使用左值的构造函数,右值的就匹配右值的.这就有点意思了,我们这里用自己模拟实现的string来和大家验证.
namespace bit
{
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string && s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
~string()
{
delete[] _str;
_str = nullptr;
}
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)
{
push_back(ch);
return *this;
}
private:
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
//这个实现是不对的
bit::string to_string(int val)
{
bit::string str;
while (val > 0)
{
int x = val % 10;
val /= 10;
str += ('0' + x);
}
return str;
}
}
这里我们使用那个不正确的to_string来验证.我们先把右值引用的构造函数给屏蔽掉.
int main()
{
bit::string str = bit::to_string(123);
return 0;
}
这里我需要解释一下.上面代码的原理实际上是下面图显示的,这里是需要两次拷贝构造的,而且是深拷贝.编译器一看,这两次连续了,就把他们优化成一次了,也就是我们上面得到一次打印的结果.
这里我们就要看看把移动构造放开,这里只调用了移动构造,这是有原因的.
int main()
{
bit::string str = bit::to_string(123);
return 0;
}
我先说一下原因,我们首先构造一个拷贝构造出一个临时变量,要知道里临时变量具有常性,这里我们可以认为是右值.这里使用移动拷贝构造.
原理是是上面谈的,这里编译器会发生优化,直接进行移动构造,这里出现了问题,str可是地地道道的左值,移动拷贝里面的参数是右值引用,接受不了左值.但是实际上我们需要这样分析,右值又可以分为两种.
- 纯右值 例如10,20,'a'...
- 将亡值 函数返回值,临时对象...
这里我们就会看看到to_string中str是一个左值.这里编译器发现你这个变量出来了就会被销毁,资源就浪费了,编译器就会把他move成一个右值,这个右值是一个将亡值.它的作用就是为了资源转移.
int main()
{
bit::string s1 = "hello";
bit::string s2(s1);
bit::string s3 = "word";
bit::string s4(move(s3));
return 0;
}
移动赋值
移动赋值这就不用说了,作用和之前一样.
// 移动赋值
string& operator=(string && s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
这里测试一下就可以了.
int main()
{
bit::string s1("hello");
bit::string s2;
s2 = move(s1);
return 0;
}
总结
现在我们需要总结一下.C++98的时候我们谈到了类面存在六个默认的函数,今天要再加上两个了,就是上面的移动构造和移动赋值.不过这两个默认的函数编译器自动生成还有一定的要求.
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造.
完美转发
完美转发是底层的原理,这里我们需要了解一下,有的选择题可能会考.
万能引用
我们先来看一下下面的例子.这个结果会让你大跌眼见(不知道成语).
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;
}
template<typename T>
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<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
完美转发
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发.我们这里开始想对于万能引用,传入左值还好说,就是右值有点难搞,我们不能对传入的值都move掉,这样有都变成了右值了.
template<typename T>
void PerfectForward(T&& t)
{
Fun(move(t));
}
完美转发很好的解决了这个场景,我们可以使用完美转发是的参数恢复它的右值还是左值属性.
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
应用场景
C++11之后STL里面的增加右值的插入,这些是对于那些资源进行转移的.
完美转发是主要在底层使用的,我们里模拟实现的list为例.我们这里给右值的push_back和右值的insert
namespace bit
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _data(val)
{}
};
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
//return &(operator*());
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const self& it)
{
return _node != it._node;
}
bool operator==(const self& it)
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
list()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& xx)
{
//insert(end(), std::forward<T>(xx));
insert(end(), xx);
}
// 插入在pos位置之前
iterator insert(iterator pos, const T& x)
{
Node* newNode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
iterator insert(iterator pos, T&& xx)
{
//Node* newNode = new Node(std::forward<T>(xx));
Node* newNode = new Node(xx);
Node* cur = pos._node;
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
private:
Node* _head;
};
}
这里我们测试一下,你会发现结果和我们预料的不一样.
int main()
{
bit::list<bit::string> lt;
bit::string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
这里面的原因就是万能引用,参数无论是左值还是右值,这里会退化成左值.这里push_back和insert都要使用完美转发.
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& xx)
{
insert(end(), std::forward<T>(xx));
}
// 插入在pos位置之前
iterator insert(iterator pos, const T& x)
{
Node* newNode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
iterator insert(iterator pos, T&& xx)
{
Node* newNode = new Node(std::forward<T>(xx));
Node* cur = pos._node;
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
我们这里需要再次测试下这个是不是正确的.
这是有原因的,我们在new 节点间的是时候也是传入了引用,节点的构造函数也是万能引用.
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _data(val)
{}
list_node(T&& val)
:_next(nullptr)
, _prev(nullptr)
, _data(std::forward<T>(val))
{}
};
default&delete
这是两个关键字里面的内容都是挺简单的
- 强制生成默认函数的关键字default
- 止生成默认函数的关键字delete
// 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;
};
// 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;
};
可变参数模板
这个看起来非常的恶心,看起来非常不C++.这个起源是C语言的printf.对于这个知识点我们只要求理解,有可能选择题会涉及到.
相对于printf.可变参数模板更加的恶心,编译甚至不知道参数的类型.
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
参数个数
这里我们要谈两个方面,计算模板参数的大小和打印模板参数对应的数据.
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1, 2, 3, 4, 5);
return 0;
}
打印数据还是有点困难的,这里我们需要先看一下做法,后面和大家分析.
template <class T>
void ShowList(const T& val)
{
cout << val <<"->"<<typeid(val).name()<<" end"<< endl;
}
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << sizeof...(args) << endl;
cout << val <<" -> "<<typeid(val).name() << endl;
ShowList(args...);
}
int main()
{
ShowList(1, 2, 3, 4, 5);
return 0;
}
这里我们用两个参数简单的分析一下.这里剩最后一个参数的时候,编译器看到有一个现成,直接用了.
有的人可能还用下面的方法.这里是因为参数包允许是零个.
void ShowList()
{
}
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
cout << sizeof...(args) << endl;
cout << val <<" -> "<< typeid(val).name() << endl;
ShowList(args...);
}
int main()
{
ShowList(1, 2);
return 0;
}
这里就有点问题了,一般情况下,我们只有模板参数这一个参数,前面是没有另外的参数的.
template <class ...Args>
void ShowList(Args... args)
{
}
这里我们可以给出另外一种方法.这里我们不做解释.
template <class T>标签:11,return,string,右值,int,左值,特性,C++,prev From: https://blog.51cto.com/byte/5734503
int PrintArg(const T& t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1, 'x', 1.1, string("hello world"));
cout << endl;
ShowList(1, 2, 3, 4, 5);
return 0;
}