【C++】C++11<包装器没写>
文章目录
- 一、初始化列表的统一
- 1.列表初始化
- 2.initializer_list
- 二、声明
- 1.auto
- 2.decltype
- 3.nullptr
- 三、范围for
- 四、智能指针
- 五、STL中的变化
- 1.新容器
- array
- forward_list
- 2.接口
- 六、右值引用
- 1.左值引用和右值引用
- 2.右值引用的使用场景和意义
- 3.左值引用和右值引用的价值和场景
- 4.完美转发
- 七、可变参数模板
- 1.可变参数模板
- 求参数个数
- 取参数值:
- 2.emplace系列
- 八、lambda表达式
- 引子
- 语法
- 九、包装器
- 1.functional
- 2.bind
- 十、新增成员函数
一、初始化列表的统一
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++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,一切皆可使用{}初始化,使用{}初始化时,可添加等号(=),也可不添加。
而且使用这样的{}会去调用构造函数
同时这样的话就可以应用于new中
int* ptr1 = new int[3]{ 1, 2, 3 };
Point* ptr2 = new Point[2]{ p0,p1 };
Point* ptr3 = new Point[2]{ {1,1},{0,0} };
struct Point
{
//explicit Point(int x, int y)
// :_x(x)
// , _y(y)
//{
// cout << "Ponint(int x, int y)" << endl;
//}
Point(int x, int y)
:_x(x)
, _y(y)
{
cout << "Ponint(int x, int y)" << endl;
}
int _x;
int _y;
};
int main()
{
int x = 1;
int y = { 2 };
int z{ 3 };
int a1[] = { 1,2,3 };
int a2[]{ 1,2,3 };
//都是在调用构造函数
Point p0(1, 2);
//C++11
Point p1 = { 1, 2 };
Point p2{ 1,2 };
const Point& q = { 1,2 };
int* ptr1 = new int[3]{ 1, 2, 3 };
Point* ptr2 = new Point[2]{ p0,p1 };
Point* ptr3 = new Point[2]{ {1,1},{0,0} };
return 0;
}
建议:日常定义,不要去掉=,显得奇怪,降低代码可读性。
其实上面这些用{}的本质是一个多参数的隐式类型转换,因为之前string中的单参数的隐式类型转换
string s = "xxxxx";
explicit关键字,可以防止隐式类型转换;若加在构造函数前面,那么就无法使用{}这样进行初始化了
再比如,我们可以使用引用进行验证,如果没有explicit关键字,这个引用还可以编译通过
这里必须要加const,因为隐式类型转换要产生一个临时对象,这个临时对象具有常性。
2.initializer_list
下面两段代码是同一个语法吗?
struct Point
{
Point(int x, int y)
:_x(x)
,_y(y)
{
cout << "Ponint(int x, int y)" << endl;
}
int _x;
int _y;
};
int main()
{
vector<int> v = { 1,2,3,4,5,6,7 };
Point p = { 1,2 };
return 0;
}
其实不是的,对于vector,它后面的花括号参数是可以改变的,而对于Point,它后面的花括号参数是不可以改变的。
所以说,这两个其实是利用不同的规则进行初始化的。
第二个我们好理解,就是多参数的隐式类型转换。
那么第一个是咋回事呢?其实C++11做了这样一件事。它新增了一个类型,叫做initializer_list
那么initializer_list是如何实现的呢?
template<class T>
class initializer_list
{
private:
const T* _start;
const T* _finish;
}
然后我们赋值时候所给的数组其实是存储在常量区的,当我们赋值的时候,这两个指针其实一个指向常量区的开始,一个指向常量区的结尾
所以当我们打印这个类型的大小的时候,我们会发现,在32位下是8字节,64位是16字节
里面的类型会自己推导
回到上面的话题:vector为什么可以直接接收initializer_list的类型呢?
其实本质上是vector写了一个构造函数,即支持使用initializer_list初始化的构造函数。
所以现在也解释了为什么vector看上去使用{}初始化可以有任意个类型,其实是两次构造函数得到的。
vector(initializer_list<value_type> il)
{
reserve(il.size());
for(auto& e : il)
{
push_back(e);
}
}
如下是在我们原本的vector容器中进行改造的:
不仅仅是vector中可以这样使用,在map中也有initializer_list初始化:
这样在map中这样用其实比较有点意思,首先map的插入需要的是pair类型,所以实际上里层的两个花括号是多参数的隐式类型转换,将两个字符串转化为pair类型,然后外层的花括号就是initializer_list了。
二、声明
1.auto
实现自动类型推断;当一个类型过于长,就可使用auto提高效率,但是对于不熟悉代码的人,会影响可读性;
详解:在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
2.decltype
decltype可先认为是typeid的升级版(不仅会提取出类型名称,还可以使用提取出的类型);但这里还和aotu不一样,auto是根据值得类型去推端出来的,而decltype是根据传的参数已经定好的。
而且decltype还可以推导表达式的类型:
在函数类模板中:
3.nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
看输出的第二行,就发现不是预期的,而nullptr很好的解决了该问题。
三、范围for
本质就是迭代器,有begin和end两个成员函数,在STL已经详解。
四、智能指针
详解
五、STL中的变化
1.新容器
标记的为新容器,最有用的是unordered_map和unordered_set。
array
array对标的其实就是普通的数组,它两在用法上几乎没有任何区别,甚至是字节大小都一样,唯一不同的就是他们两个的类型不同。这俩个都是静态的数组:
虽然这两个用起来没有任何区别,但是array对于[]运算符重载要比普通的更严格一些
1
.C语言数组越界检查,越界读基本检查不出来,越界写是抽查,因为其本质是指针的解引用
2
.array越界后会强制报错,主要原因就是它的[]运算符本质是operator[]函数的调用,内部会有检查的
不过总体说array还是比较鸡肋的,因为我们更喜欢使用vector,而且它还可以初始化,
vector<int> v(10,0);
补充:array开的空间过大,导致栈溢出。
forward_list
接口:
它只支持头插和头删除,因为尾插尾删效率太低。
如果非要用可以使用insert和erase。但是这两个是往该节点后面插入或删除的。
本质就是一个单链表加了一些功能
2.接口
容器内部变化:
1.都支持initializer_list构造,支持列表初始化
2.增加了cbegin、cend系列接口(鸡肋)
3.移动构造和移动赋值
4.右值引用参数的插入
六、右值引用
1.左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以之前的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值
是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址
,一般可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边,而右值不能出现在赋值符号左边,只能出现在赋值符号的右边。所以左边的一定是左值,右边的不一定是右值;
左值一般可以对它进行赋值,有几个例外就是定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
。
还有字符串常量、字符串上每一个元素都是左值
什么是右值,什么是右值引用?
右值
也是一个表示数据的表达式,如:字面常量(字符)、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
。右值引用就是对右值的引用,给右值取别名。
0
0
总之,记住一句话:
左值一定可以取地址,右值无法取出地址
左值引用就是给左值取别名
右值引用就是给右值取别名
语法:右值引用就是使用两个&&即可
int main()
{
double x = 1.1, y = 2.3;
//右值不能取地址
10;
x + y;
//以下是右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
return 0;
}
左值引用给右值取别名,只需要加上一个const就可以了,但是绝不可以直接使用左值引用去引用右值,这会直接报错:
int main()
{
const int& r = 0;
return 0;
}
右值引用给左值取别名,不可以直接使用,但是可以给左值加上move就可以了,但是move可能会对这个左值造成一些其他影响:
int main()
{
//左值引用给右值取别名
const int& r = 0;
//右值引用给左值取别名
int a = 0;
int&& r1 = move(a);
return 0;
}
总结:
- 左值和右值的区别就是能否取地址,左值可以取地址,右值不可以取地址
- 左值引用可以给左值取别名,不可以引用右值;右值引用可以给右值取别名,但是不可以引用左值
- const左值引用可以给右值取别名,右值引用可以给move以后的左值取别名
2.右值引用的使用场景和意义
先分析左值引用价值:
引用的价值:减少拷贝、增加效率
左值引用解决的问题:
1.做参数:减少拷贝、做输出型参数;
2.做返回值:减少拷贝、引用返回(可修改返回对象如operator[])
然后左值引用有一种场景还没有解决:那就是返回局部对象不可以使用左值引用。即下面的场景,我们只能去使用传值返回,不能传左值引用返回,因为无论是左值引用返回还是const左值引用返回其实本质上都是引用那块空间,而这块空间出了作用域就销毁了。销毁了以后我们还拿到这个别名的话,那么问题就大了,因为就相当于野指针了。我们没有权限去访问。
所以这个地方在之前只能使用传值返回
而传值返回的话,如果编译器不加任何优化,那么s返回的时候要产生一个临时对象,这是一次拷贝构造,然后用这个临时对象在拷贝构造给ans,这又是一次拷贝构造,代价实在太大了。如果这个str字节有十几万的话,代价很大的。所以编译器将这里给优化为了一次拷贝构造。
这里为了方便标记,我们使用自己在学习STL写的string类
namespace my
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
cout << "默认构造 string(const char* str = "")" << endl;
}
//现代写法
//s2(s1)
void swap(string& tmp)
{
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
cout << "深拷贝 string(const string& s)" << endl;
}
string& operator=(const string& s)
{
if (&s != this)
{
string tmp(s);
swap(tmp);
}
cout << "深拷贝 string& operator=(const string& s))" << endl;
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
const char* c_str() const
{
return _str;
}
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
char& operator[](size_t pos)const
{
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(const char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
void append(const string& s)
{
append(s._str);
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
//满需扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + len + pos);
_size -= len;
}
}
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator<(const string& s)const
{
/*return strcmp(_str, s._str) < 0;*/
return !(*this > s);
}
bool operator>=(const string& s)const
{
return *this >(s) || operator==(s);
}
bool operator<=(const string& s)const
{
return *this <(s) || operator==(s);
}
bool operator!=(const string& s)const
{
return !(*this==s);
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
size_t find(const char* sub, size_t pos = 0)const
{
assert(pos < _size);
assert(sub);
//kmp算法/bm算法
const char* ptr = strstr(_str + pos, sub);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//"hello world"
string substr(size_t pos, size_t len = npos)const
{
assert(pos <= _size);
size_t reallen = len;
if (npos == len || pos + len > _size)
{
reallen = _size - pos;
}
string tmp;
for (size_t i = 0; i < reallen; i++)
{
tmp += _str[pos + i];
}
return tmp;
}
void resize(size_t n, char ch = ' ')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
while (_size != n)
{
_str[_size] = ch;
_size++;
}
_str[n] = '\0';
}
}
private:
char* _str;
size_t _capacity;
size_t _size;
public:
const static size_t npos = -1;
};
ostream& operator<<(ostream& out,const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
const size_t N = 32;
char buff[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
buff[i] = '\0';
s += buff;
return in;
}
}
来看第一个框,在函数s中str被返回,会被拷贝给临时变量,临时变量拷贝给ans又是一次拷贝构造,但这里为何只有一次呢,实则是编译器做了优化;我们打开vs2019的终端采用命令行模式加入-fno-elide-constructors
参数去掉优化,重新编译运行,发现果然如此。
好,现在我们先暂放这里,接着往下看:
下面的代码是否构成函数重载?
很明显构成函数重载,参数不同的类型;
那这个呢?
其实也会的。
但是这里const左值引用是可以引用右值,而下面的也能引用右值。那么当我们写出下面的这段代码的时候,会发生什么呢?
他们会走向最匹配的函数
而且如果没有下面的,编译器也是可以跑的
所以在这里,如果有右值引用的版本,就会走右值引用版本,会走最匹配的
然后现在我们再去回过头来看前面的代码,我们知道,前面的代码在编译器不优化的情况下,代价有点太大了
在这里一共涉及到了三次深拷贝,第一次是str创建的时候,第二次是str返回的时候会产生一个临时对象,第三次是将这个临时对象给s时候,还要发生一次深拷贝。
然而其中有两块空间可以说是浪费掉了,如下图打×的部分都是被浪费掉了
那么有没有什么办法可以进行优化呢?
其实关于右值:
我们可以把它分为内置类型的右值和自定义类型的右值
而内置类型的右值我们一般也称为纯右值
,自定义类型的右值一般也称为将亡值
而函数返回值,表达式返回值也是一个右值。并且对于我们上面s字符串的操作,比如说s1+s2,to_string,其实本质都是函数调用,(s1+s2是一个运算符重载,其实本质也是函数调用),而这些函数调用返回的都是将亡值。
换句话说:
s = 左值 //只能老老实实进行深拷贝
s = 右值将亡值 //可以进行移动拷贝
这里的移动拷贝其实就是交换资源,如下所示,就是移动拷贝
来分析一下,由于func函数返回的是一个临时对象,这个临时对象就是一个右值,既然是右值,那么我们就使用右值引用,正好交换资源。即可,相比使用const左值引用要减少了一次深拷贝。
使用const左值引用的话,const对象无法被修改,所以只能去使用一次拷贝构造去创建一个可以交换资源的对象,然后再进行交换。而对于右值引用,就不存在无法被修改的问题了。所以可以直接去交换资源
而且由于编译器会自动走最匹配的,所以对于右值会走向第二个函数,只有当第二个函数不存在的时候,才会走向第一个函数。
如下图所示,第一次深拷贝是func函数中要返回一个临时对象所造成的,第二次拷贝是移动拷贝所必须的,但是这里的移动拷贝里面仅仅只是交换资源,几乎没有消耗。
所以使用了移动拷贝的话,那么就只剩下两次消耗了,一次是func函数中要开一个str字符串,一次是要返回一个临时对象,两次消耗,但是只有一次深拷贝。在赋值这里就没有任何消耗了。而原来的就是三次消耗,即需要两次深拷贝。
原来:
现在:
那么上面的场景是我们不让编译器优化的场景,那么如果让编译器优化呢?
我们可以在利用右值引用写出这样的拷贝构造函数:
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
反正右值都是将亡值,没什么用的值,那么不妨直接用来作为资源即可,即直接交换,直接就可以几乎没有任何开销了,没有深拷贝了
这样的,原来的const左值引用,虽然可以引用右值,但是由于const,导致我们无法直接利用这个将亡值的资源,我们只能眼睁睁看着这个将亡值自己消亡,却无法直接拿走他的资源。所以只能自己去利用它创造一个对象,用这个新的对象去交换资源,这样就多了一次深拷贝了。
可见直接0开销了,而前面编译器优化后还有1次开销呢,可见充分利用了右值的将亡特性。
不过在这里还有一些疑惑的点就是,s要返回的时候,str是一个左值,那么他就必须得用来拷贝构造来构造一个临时对象,这个临时对象确实可以零开销的进行拷贝构造了,但是这里应该还有一次拷贝构造啊?为什么打印结果里没有呢?
其实本身编译器就会做出一些优化:即连续的构造、拷贝构造会进行合二为一,甚至是合三为一。而编译器在这里做出的优化其实就是直接用str去拷贝构造ret。即不需要借助中间的临时变量了。那么func返回的其实就是str本身。而str虽然是一个左值,但是他本身符合将亡值的特征。因为出了作用域,它即将销毁,所以编译器此时做出了第二个优化:把str本身识别为右值。相当于给move了一下,右值引用去引用左值。
总的来说编译器直接进行了两次优化:
1.连续的构造、拷贝构造合二为一(不需要临时变量了,只有传值返回才可以)
2.编译器把str识别为了右值(因为str虽然是左值,但是符合将亡值特征,相当于进行了一次特殊处理)
而且还需要注意的是,这里千万不可以写传引用返回
首先就是传引用返回的话,那么这块空间已经被销毁了,就出现了野指针的问题了。其次只有传值返回的时候,编译器才会进行优化,如果不传值返回,编译器就不会进行上面的优化了。
而且传值返回所造成的第一个优化,即不需要临时变量的本质其实就是把拷贝放在了str还没有销毁的时候,即在函数内部。而传引用返回就一定不可以了
在上面如果个str加上一共move,相当于我们也将他认为是右值了,这样其实也是可以的
一旦我们加上了拷贝构造的右值引用,那么对于编译器无法第一种优化的场景也可以使用第二次种优化。
在这里因为我们并不是连续的拷贝和拷贝构造。而是一次拷贝构造和一次赋值运算符重载。
拷贝构造是由于func要返回一个临时对象,但是这个我们可以将str识别为将亡值,就可以使用移动拷贝了。将str的资源转移到临时对象中去
然后这个临时对象又进一步的使用赋值运算符重载,这里又是一次移动拷贝,因为刚好这个临时对象是一个右值。又一次的转移资源。
最终整个过程没有任何的消耗,仅仅只是两次转移资源,代价极低
3.左值引用和右值引用的价值和场景
对于右值引用的移动拷贝,实际上我们一般只将其用于自定义类型中,尤其是深拷贝的场景,比如vector<vecor>这种拷贝代价极大的场景,而对于内置类型,对其使用右值引用的移动拷贝其实意义并不是很大,或者说没有任何意义。并不能带来一丝的优化。甚至对于浅拷贝的自定义类型也没有任何价值。只有深拷贝的自定义类型才有价值。
左值引用的核心价值就是减少拷贝,提高效率
而右值引用的核心就是价值就是进一步减少拷贝,弥补左值引用没有解决的场景。如:传值返回。
那么右值引用的场景有哪些呢?
场景一:自定义类型中深拷贝的类,且必须传值返回的场景
我们可以同时对比满足和不满足的场景
此时str的地址后四位是0fe8
当出了移动拷贝结束后,此时ans的地址后四位是0fe8;
所以最终结果为交换了资源,而下面这个是不会交换资源的
像下面第一种种move也是不会转移走ret的资源的,但是第二种move这样使用会转移走资源;
我们可以理解为,当一个值被move修饰后作为参数传入其他函数,它的内容就会被掏空,然后自己拿到的是函数里别人给他的值;
场景二:容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝
如下图所示:
不难发现第二种和第三种才是最高效的,但第二种操作之后,str1的值就没了,被掏空了;一般我们采用第三种,相当于创造了个临时对象(或匿名对象),具有常性的。是一个将亡值,就会导致它会去调用右值引用。
如果没有右值引用的话,即没有移动构造的话,那么三个都将是拷贝+拷贝构造。(我们这里只是没有将拷贝打印出来而已)
即便是在STL的其他容器中,基本都是有右值引用的。用来提高效率
4.完美转发
万能引用
(引用折叠)
下面代码是一个模板,在这个模板中,有一个看上去像右值引用的存在,但是实际上,它并不是右值引用,而是万能引用。
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);
}
万能引用:它既可以接收左值,又可以接收右值
当实参为左值的时候,它就是左值引用,我们也称为引用折叠
当实参为右值的时候,它就是右值引用
所以对于下面的代码,我们就可以知道,这些实际调用的都是右值,左值,右值,const左值,const右值。他们调用的实际上不是同一个函数
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;
}
但是当我们运行的时候
运行结果为如下所示:
我们会发现结果其实不符合我们的预期
这是什么情况?难道全折叠了?理论上应该不可能的吧。
我们先用下面这段代码来观察一下
这里为何是左值引用?
我们可以用下面这段代码来发现一些问题。我们发现虽然
b0左值引用了a,b1右值引用了a,但是他们两个本身却是左值,因为他们可以取出地址。
我们知道,右值有两个属性:第一个是不可以取地址,第二个是不可以修改。
而这里b1不仅可以取地址,还可以进行修改。
而右值引用似乎却可以进行修改?其实它也必须得修改,如果它不支持修改,那么它就出问题;
来看自己实现的右值引用:
我们会发现,我们在当函数s返回的这个临时变量对象进行调用移动拷贝的时候,参数s是右值引用了str,但是s居然可以被修改,而且这个s还可以传递给一个左值引用去修改。
所以说s是一个左值;
基于此我们再做一个简单的问题复现:
变量 int&& j = 10; 声明了一个右值引用 j,即 j 是一个右值引用类型的变量。尽管 j 是一个右值引用类型的变量,但 j 本身是一个左值,因为它是一个命名变量,可以被赋值和取址。这是C++语言中的一个特性:尽管 j 是右值引用类型,但 j 本身是左值。
该解释初听可能有点绕,小伙伴们多多体会。
此时最初的疑惑可解:
那我们如何让它按最初的想法实现它本应有的功能呢?
C++在这里搞出来了一个完美转发
当t是左值的时候,保持左值属性
当t是右值的时候,保持右值属性
关键词:forward
想到完美转发,我们可以突然意识到前面有一个问题似乎编译器底层应该用的就是完美转发了,即下面的val本来是左值,但是我们需要它的右值属性,所以可以使用完美转发
然后我们可以去尝试修改一下我们之前所是实现的链表,如下是之前的链表:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string.h>
#include<assert.h>
using namespace std;
namespace my
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
// cout << "默认构造 string(const char* str = "")" << endl;
}
//现代写法
//s2(s1)
void swap(string& tmp)
{
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
cout << "深拷贝构造 string(const string& s)" << endl;
}
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
cout << "移动拷贝构造 string(string&& s)" << endl;
}
string& operator=(const string& s)
{
if (&s != this)
{
string tmp(s);
swap(tmp);
}
cout << "深拷贝赋值拷贝 string& operator=(const string& s))" << endl;
return *this;
}
string& operator=(string&& s)
{
swap(s);
cout << "移动赋值拷贝 string& operator=(string&& s)" << endl;
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
const char* c_str() const
{
return _str;
}
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
char& operator[](size_t pos)const
{
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(const char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
void append(const string& s)
{
append(s._str);
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
//满需扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + len + pos);
_size -= len;
}
}
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator<(const string& s)const
{
/*return strcmp(_str, s._str) < 0;*/
return !(*this > s);
}
bool operator>=(const string& s)const
{
return *this >(s) || operator==(s);
}
bool operator<=(const string& s)const
{
return *this <(s) || operator==(s);
}
bool operator!=(const string& s)const
{
return !(*this==s);
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
size_t find(const char* sub, size_t pos = 0)const
{
assert(pos < _size);
assert(sub);
//kmp算法/bm算法
const char* ptr = strstr(_str + pos, sub);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//"hello world"
string substr(size_t pos, size_t len = npos)const
{
assert(pos <= _size);
size_t reallen = len;
if (npos == len || pos + len > _size)
{
reallen = _size - pos;
}
string tmp;
for (size_t i = 0; i < reallen; i++)
{
tmp += _str[pos + i];
}
return tmp;
}
void resize(size_t n, char ch = ' ')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
while (_size != n)
{
_str[_size] = ch;
_size++;
}
_str[n] = '\0';
}
}
private:
char* _str;
size_t _capacity;
size_t _size;
public:
const static size_t npos = -1;
};
ostream& operator<<(ostream& out,const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
const size_t N = 32;
char buff[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
buff[i] = '\0';
s += buff;
return in;
}
}
而对于STL库里里面的代码来说就是正常的移动拷贝了
主要原因就是因为,list的push_back接口只有const左值引用版本,为了解决这个问题,我们只能去使用一个右值引用版本的来处理
所以我们现在来进行修改list
改造后:
除此之外,我们还可以这样做,这样做的话,也就是说是,只需要使用一个万能引用就可以了。不过这个函数我们必须加上模板,不然的话对于const类型是无法进行构造的。
七、可变参数模板
1.可变参数模板
里的三个点就代表了,可以写任意个参数
这里面其实就相当于有一个数组把这个实参存起来,然后printf会依次访问数组里面的元素。
以上就是函数的可变参数
而模板参数和函数参数是很类似的,模板参数传递的是类型,函数参数传递的是对象。函数的可变参数是传多个对象,而模板的可变参数就是可以传多个类型
Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数。
求参数个数
template<class T,class ...Args>
void showList(T val, Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55);
return 0;
}
template<class ...Args>
void showList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55);
return 0;
}
看到这里正常返回参数个数;
取参数值:
template<class T>
void showList(T val)
{
cout << val << endl;
}
template<class T,class ...Args>
void showList(T val, Args... args)
{
cout << typeid(val).name() << val << endl;
showList(args...);
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55, 2, 85, 69);
return 0;
}
可清晰地看出它每次将n个参数拆分为1 和n-1;当n=0时,当单个参数传入,不在形成参数包;
这样看着很奇怪,要想查看哪些参数是一组的还得自己去处理,怎样给他区分一下呢?
那就给他加一个参数包进去
但显示他根本就没有调用新增的函数,分析一下,原来它会默认的将参数包放入2号中去拆分,当剩一个参数的时候就说明是该包完了,去换行即可,之前新增的删掉。
template<class T>
void showList(T val)
{
cout << val << endl;
}
template<class T,class ...Args>
void showList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << " ";
showList(args...);
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55, 2, 85, 69);
return 0;
}
注意:参数包不可当做数组去访问
它这里其实就用了一个编译时的递归。
一开始会将第一个参数传给T,然后剩下的参数包都传给下一层函数。最上面就是结束条件。
在库里面就有一个类似的接口
不过它的参数只有一个参数包,那么应该如何传递呢?我们也搞一个:
template<class T>
void showList(T val)
{
cout << val << endl;
}
template<class T,class ...Args>
void showList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << " ";
showList(args...);
}
template<class ...Args>
void showList(Args... args)
{
cout << "lllllllllllllllllllllllll" << endl;
showList(args...);
}
//`注意:参数包不可通过遍历数组去访问
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55, 2, 85, 69);
return 0;
}
我们发现它就不调用参数包形式的参数,查文档发现多个参数它会首先去适配1 与n-1的参数值写法,那我们给他再变一下,写成子函数形式:
template<class T>
void _showList(T val)
{
cout << val << endl;
}
template<class T,class ...Args>
void _showList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << " ";
_showList(args...);
}
template<class ...Args>
void showList(Args... args)
{
cout << "lllllllllllllllllllllllll" << endl;
_showList(args...);
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55, 2, 85, 69);
return 0;
}
成功!
代码简化:
void _showList()
{
cout << endl;
}
template<class T,class ...Args>
void _showList(T val, Args... args)
{
cout << val << " ";
_showList(args...);
}
template<class ...Args>
void showList(Args... args)
{
_showList(args...);
}
int main()
{
showList(1.1);
showList(1.1, 2);
showList(1.1, 85u, 90.3f);
showList(22, 55, 2, 85, 69,"asjxkasjck");
return 0;
}
这也符合参数包的概念,参数个数可以为0~n;其实给本质就是递归调用。
这里的核心逻辑就是,在Showlist中,会将参数包传给PrintArg这个函数,这个函数只会解析第一个参数,后面的逗号是一个逗号表达式,用于初始化数组,后面的三个点就是有几个参数就会相当于调用了几次PrintArg这个函数
void Showlist()
{
cout << endl;
}
template<class T>
void PrintArg(T t)
{
cout << t << " ";
}
template<class ...Args>
void Showlist(Args... args)
{
int a[] = { (PrintArg(args),0)... };
cout << endl;
}
int main()
{
Showlist(1);
Showlist(1, 1.1);
Showlist(1, 1.1, 1.2);
Showlist(1, 1.1, 1.3, 1.2, string("xxxxx"));
return 0;
}
2.emplace系列
在C++11以后,很多库里面都加上了emplace系列接口
正常尾插:
int main()
{
std::list< std::pair<int, char> > mylist;
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以后,我们就可以下面的写法了。
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效率稍高一些,但是其实还好,因为并没有太大差距,因为移动拷贝的效率很低
要是真要说的,反倒是内置类型和浅拷贝的效率可以提高一些
,因为深拷贝的量级基本在一个量级
。而浅拷贝是就显得差距比较大了
其中最为核心的原因就是emplace可以传参数包,这就导致了它可以传对象,可以传对象过去。而push_back只能传对象
八、lambda表达式
引子
当我们想要对一个类中的数据进行排序的时候,我们想要使用sort的话,显然我们是无法直接进行排序的,当然我们可以使用运算符重载来支持直接排序,但是这里会出现一个问题,那就是一个类有很多的成员,我们如果想要对这个成员排序完成以后,还想要对其他成员进行排序,这个时候我们就只能使用仿函数了,来进行各种各样的排序,如下代码所示:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareEvaluateGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate > gr._evaluate;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess()); //价格升序
sort(v.begin(), v.end(), ComparePriceGreater()); //价格降序
sort(v.begin(), v.end(), CompareEvaluateGreater()); //评价降序
}
但是上面代码还有一些问题,那就是假如一个命名不规范等问题出现的时候,会非常麻烦
有没有更好的办法呢?当然有,那就是lambda表达式
如下所示,就是一个lambda表达式的简单例子:
总结:
函数指针 ------能不用就不用
仿函数---------类重载operator(),对象可以像函数一样使用
lambda表达式------匿名函数对象,函数内部可以直接定义使用
语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分说明
-
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量提供lambda函数使用。
-
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
-
mutable:默认情况下,
lambda函数总是一个const函数
,mutable可以取消其常性
。使用该修饰符时,参数列表不可省略(即使参数为空)
。 -
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可
省略
。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。 -
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
int main()
{
int a = 0;
int b = 2;
auto add1 = [](int x, int y) ->int {return x + y; };//没有明确的名称
auto add2 = [](int x, int y) {return x + y; };
cout << add1(a, b) << endl;
cout << add2(a, b) << endl;
return 0;
}
再改之前的代码
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; }); //价格升序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {return g1._price > g2._price; }); //价格降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {return g1._evaluate > g2._evaluate; }); //评价降序
return 0;
}
接下类再看捕获的用法:
[var]:表示值传递方式捕捉变量var,捕捉后为const类型
[=]:表示值传递方式捕获所有父作用域中的变量(包括this),捕捉后为const类型
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意事项:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
捕捉列表出来的是不可以修改的;如果真的想修改捕捉到的变量,可以加上mutable
如果想修改外面的,可以使用引用捕捉
而且引用捕捉是可以捕捉const变量的。只不过捕捉以后无法修改,但是可以进行访问,他们的地址都是一样的
示例:
不可赋值:
我们会发现其实这两个对象的类型其实是不一样的。所以当然无法赋值。
其实lambda表达式的底层就是仿函数,这里的add1,add2,add3都是一些仿函数对象,只不过他们的类型是编译器自己生成的。我们看不到而已。
测试代码:
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
return 0;
}
唯一的标识符为uuid;
所以lambda表达式底层其实就是仿函数,就像范围for的底层是迭代器一样
九、包装器
1.functional
2.bind
十、新增成员函数
默认成员函数
c98:6个
C++11:8个
若自己全没写析构、赋值构造、拷贝构造函数的话,会默认生成移动构造;
=default;强制生成默认类型函数;
=delete;强制是对象不能被拷贝;
override:检查虚函数是否被重写,没重写会报错
final:修饰虚函数不能重写且类不能被继承;
面试:用delete关注字实现一个类只能在堆上创建;
在不使用delete关键字时:
1.私有析构函数
2.私有构造
使用delete:
制动析构函数,
~Heap() = delete;
Heap* ptr = new Heap;
operator delete(this);