作者主页:进击的1++
专栏链接:【1++的C++进阶】
在C++03之前,我们的默认成员函数有6个,我们在类与对象这篇中有过详细的讲解。C++11中又增加了两个默认成员函数—移动构造与移动赋值重载,其底层原理以及优势我们在上节已经有过描述。但是针对这两个默认成员函数,我们还需要注意以下说明:
我们以以下代码为例:
class string
{
public:
string(const char* str = "")
:_str(nullptr)
{
cout << "string(const char* str)" << endl;
}
//拷贝构造
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
std::swap(_str, tmp._str);
//....
cout << "string(const string& s)" << endl;
}
//移动构造
string(string&& s)
{
std::swap(s._str, _str);
cout << "string(string&& s)" << endl;
}
//赋值重载
string& operator=(string& s)
{
std::swap(s._str, _str);
cout << "string& operator=(string& s)" << endl;
return *this;
}
//移动赋值
string& operator=(string&& s)
{
std::swap(s._str, _str);
return *this;
}
private:
char* _str;
};
template<class T>
class A
{
public:
A(T&& s)
:_a(0)
,_s(std::forward<T>(s))
{
cout << "A" << endl;
}
A(T& s)
:_a(0)
, _s(s)
{
cout << "A" << endl;
}
private:
int _a;
T _s;
};
void test1()
{
hyp::string s2 = ("234");
A<string> a3(s2);
A<string> a4(move(a3));
}
通过上述结果我们可以发现,对于自定义成员,其在没有自己实现析构函数,拷贝构造,赋值重载时,会自动调用自定义成员的移动构造。
当我们在类A中自己实现析构函数,拷贝构造,赋值重载任意一个时,结果如下:
其就不再自动调用自定义类型成员的移动构造,而是调用拷贝构造。
当我们添加A的移动构造后,编译器便不会再生成拷贝构造和赋值重载,而且我们也没有写,编译器便会报错。
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。
强制生成默认函数的关键字default:
当有了移动构造后,便不会再生成拷贝构造,因此我们可以使用default当强制生成拷贝构造。
禁止生成默认函数的关键字delete:
C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧。
我们在这里只进行简单的了解,下面我将演示两种能够获取到参数包中参数的方法。
方法一—递归
template<class T>
void Showlist(const T val)//递归终止条件
{
cout << val << endl;
}
template<class T,class ...Args>
void Showlist(T val, Args... args)
{
cout << val << " ";
Showlist(args...);
}
方法二–逗号表达式
template<class T>
void printargs(T t)
{
cout << t << " ";
}
template<class ...Args>
void Getargs(Args ...args)
{
int arr[] = { (printargs(args),0)... };
}
为什么要有lambda表达式?
假设我们现在需要对一个集合进行排序,(我们用std::sort进行排序)当我们要排升序时则需要传一个升序规则的仿函数,要降序时,则传一个降序规则的仿函数,当要元素类型不同时,则又需要该这个仿函数。比较麻烦,而lambda表达式可以避免这个麻烦,因此在C++11中就有了lambda表达式的出现。
lambda表达式的格式:
[捕捉列表] (参数列表) mutable -> 返回值类型 { 函数体}。
捕捉列表: 该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
参数列表: 与普通函数的参数列表一致,如果不需要参数传递,则可以
连同()一起省略。
mutable: 默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
返回值类型: 用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
函数体: 在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
我们来小总结一下:在lambda表达式中,参数列表,返回值类型,mutable是可以选择的。因此我们就有了一个最简单的lambda表达式:[ ]{}。但该lambda表达式不能做任何事情。
关于捕获列表:
捕捉列表描述了上下文中哪些变量能够被lambda,是传值使用还是引用使用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
lambda表达式之间是不能够相互赋值的,但是可以进行拷贝构造,可以将其赋值给一个相同类型的函数指针。
明明是一样的两个lambda表达式,为什么却显式不能赋值呢?
我们会在后面进行说明。
可以像函数一样使用的对象有三种:函数指针;仿函数,又叫函数对象;lambda表达式。
我们以以下代码为例:
void test5()
{
int val = 5;
Test t(val);
t(val);
auto ret = [=](int tt) {return tt + val; };
ret(val);
cout << ret(val) << endl;
cout << t(val) << endl;
}
我们再观察其汇编代码。
通过观察我们发现仿函数先是会调用其构造函数,构造出一个对象。
lambda表达式也通过捕获列表将捕获到的值用于初始化会,构造出一个对象。每一个lambda构造出的对象都是不同的,因此其看似两个相同的lambda,却不能够赋值。
并且,接下来他们都调用了operator()!!!!
因此实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。