C++11(三)
目录
lambda表达式
捕捉列表
lambda表达式底层探究
默认成员函数控制
C++11中的新增的默认成员函数
默认成员函数控制
default关键字
delete关键字
emplace_back和push_back
本期我们将继续展开C++11知识点的学习。
lambda表达式
在C++的算法中,右sort的排序方法。
我们发现此算法的前两个参数迭代器类型,最后一个参数为Com类型的仿函数对象。但是其实这个对象我们不单单可以传递仿函数对象,只要是可调用对象都可以传递,那么问题来了,什么是可传递对象呢?
可传递对象分为:
- 函数指针。
- 仿函数对象。
- lambda表达式(本期主角)。
那么究竟什么是lambda表达式呢?
先通过一段代码为大家说明。
1.通过函数指针进行sort排序。
bool Compare(const Goods& gl, const Goods& gr)
{
return gl._evaluate < gr._evaluate;
}
2.通过仿函数进行sort排序。
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
3.通过lambda表达式进行sort排序。
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price < g2._price; });
lambda表达式格式:[capture-list] (parameters) mutable -> return-type { statement }
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分 可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
- 注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分(lambda表达式不需要参数时可以不写参数列表,返回值任何时候都可以不用写,编译器会自己推导),而捕捉列表和函数体不可以省略。 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕捉列表
捕捉列表有以下规则。
- [var]:表示值传递方式捕捉变量。
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)。
- [&var]:表示引用传递捕捉变量var。
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)。
- [this]:表示值传递方式捕捉当前的this指针。
注意:不能一次将同个变量捕捉多次,且只能捕捉当前作用域中的变量。
图示如下。
除此之外,lambda表达式也只能捕捉同意作用域的变量。
lambda表达式底层探究
我们知道lambda表达式和仿函数都是可调用对象。这两者有区别吗?我们通过底层汇编代码一探究竟。
struct Add
{
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
int a = 10, b = 20;
Add add1;
add1(a, b);
auto add2 = [](int a, int b){ return a + b; };
add2(a, b);
return 0;
}
我们用仿函数和lambda表达式来实现两个数相加。
通过底层的汇编代码不难看出,lambda表达式本质上就是一个lambda_加上一个字符串(这个字符串我们称之为uuid)表示的类,最终使用lambda表达式类对象进行lambda表达式的调用时,其实也是调用了lambda_字符串类的内部的operator()成员函数。
所以,lambda表达式底层就是仿函数。
默认成员函数控制
C++11中的新增的默认成员函数
在之前我们学习C++98的初期阶段,我们学习了C++98的一些默认成员函数,如默认构造函数,默认拷贝构造函数,默认赋值运算符重载,默认析构函数,默认&和const&重载。
在C++11中,我们又引入了两个默认构造函数,默认移动构造和默认移动赋值,那么这两个的函数的生成条件和之前的五个默认生成函数的生成条件是一样子的吗?当然是不一样的。我们给定这两个默认成员函数的生成条件。
默认移动构造:在我们没有显示定义移动构造的时候,且没有显示定义析构函数,拷贝构造函数和赋值运算符重载时,编译器会生成默认移动构造函数,生成的默认移动构造对内置类型进行值拷贝,对于自定义类型调用它们的移动构造,如果这个自定义类型没有实现移动构造,就调用这个自定义类型的拷贝构造。
默认移动赋值:在我们没有显示定义默认移动赋值的时候,且没有显示定义析构函数,拷贝构造函数和赋值运算符重载时,编译器会生成默认移动赋值函数,生成的默认移动赋值函数对于内置类型进行值拷贝,对于自定义类型调用它们的移动赋值,如果这个自定义类型没有实现移动赋值,就调用这个自定义类型的赋值运算符重载 。
通过示例代码为大家验证,以模拟实现的string为例。
namespace yjd
{
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);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
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;
this->swap(s);
}*/
// 移动赋值
/* string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 转移资源" << endl;
swap(s);
return *this;
}*/
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
//cout << "~string()" << endl;
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;
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
class Person
{
private:
yjd::string name;
int age = 10;//这里是缺省参数,并不是初始化
};
int main()
{
Person p1;
Person p2(move(p1));
Person p3;
p3 = move(p1);
return 0;
}
我们定义了一个Person类,让其符合编译器默认生成拷贝构造和移动构造的条件,与此同时,我们分别给出string类的移动构造和拷贝构造以及注释掉string类的默认构造和移动构造,看其调用的函数是什么。
没有注释掉string类的移动构造和移动赋值,那么在创建了Person类的时候,进行赋值和拷贝时,对于其内部的成员string类而言,就调用了string的移动构造和移动赋值。
在注释掉string类的移动构造和移动赋值后,那么在创建了Person类的时候,进行赋值和拷贝时,对于其内部的成员string类而言,就调用了string的拷贝构造和赋值运算符重载。
默认成员函数控制
default关键字
在创建一个类时,我们知道在没有显示的定义构造函数时,编译器会自动生成一个默认的构造函数,但是一旦我们定义了构造函数或者拷贝构造函数(拷贝构造函数也是构造函数),那么编译器就不会默认生成构造函数,有没有什么强制的方法让编译器生成,有的就是default关键字。
在没有编译器没有生成默认构造函数时,此时调用默认构造函数会出错。图示如下。
我们给默认构造函数加上default关键字之后,编译器就会强制默认生成。
delete关键字
有这样一个情景,我们想让一个类无法被拷贝,我们会把这个类的拷贝构造设置为私有,并且不实现这个拷贝构造函数,只声明即可。
但是我们发现,仅仅将拷贝构造函数设置成私有就已经无法实现拷贝,但是我们为什么还要只声明不实现呢?这是因为我们在某些极端情况下会在类内实现拷贝。
如图所示,即使我们将拷贝构造函数设置成了私有,也依旧在类内访问到了拷贝构造函数,实现了对象的拷贝。所以,我们必须将拷贝构造既声明成私有的,又只声明不实现。
但是在C++11中delete关键字,就可以轻松搞定这个问题。
emplace_back和push_back
在C++11中,我们会看到有很多容器有emplace_back接口和push_back接口,这两个接口都是元素的插入接口,有什么区别吗?
这两个函数接口,通过形参就可以看出来,push_back的接口的形参就是容器的元素的类型,但是emplace_back接口的形参它是一个万能引用的可变参数模板,什么是可变参数模板,可变参数模板就是一个参数包,就是有多个模板,通过传递的实参自动推导每个模板的类型。
我们通过一段代码为大家看看这两个接口到底有什么区别。
其实这两个接口实质上也没有太大的差别,emplace_back和push_back在插入左值时,进行的操作都是类似的,但是在插入右值时,emplace_back只是在刚开始调用了构造函数进行隐式类型转换,在最终通过定位new进行元素的插入时并不会去调用移动构造,但是push_back不仅仅在刚开始调用了构造函数,并且在最终定位new进行元素的插入时, 还会调用移动构造,进行资源转移。
好了,以上便是本期的所有内容.
本期内容到此结束^_^