目录
一、新的类功能
1.1 移动构造函数和移动赋值运算符重载
在以前的C++时,我们知道C++的类有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
比较重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
移动构造函数和移动赋值运算符重载有一些需要注意的点:
- 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造。如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
注:
- 如果想要使用默认生成的移动构造/移动赋值重载,那么析构函数、拷贝构造、赋值重载都不能自己定义实现。但可以实现构造函数。
- 一般析构函数、拷贝构造、赋值重载作为一个整体出现。比如一般出现析构函数,就说明类的内部实现了开辟空间请求资源,析构函数是为了释放资源。既然申请了资源,拷贝时就要进行深拷贝,于是就有了拷贝构造和赋值拷贝。
下面的示例中只实现了构造函数,mystring是上一篇中定义的类在此不作赘述。main函数要使用Person默认生成的拷贝构造和移动构造,之前讲过,默认生成的拷贝构造对内置类型浅拷贝,对自定义类型用它自己的拷贝构造(如果它有的话)。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
mystring::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // 默认的拷贝构造
Person s3 = std::move(s1); // 默认的移动构造
return 0;
}
如果我们定义了析构函数、拷贝构造、赋值重载其中的任意一个,那么移动构造就不会默认生成,比如定义析构函数,就不会生成和使用默认的移动构造,而是使用默认的拷贝构造。
~Person()
{
cout << "~Person()" << endl;
}
1.2 强制生成默认函数的关键字default
如果我们既想生成析构函数、拷贝构造、赋值重载其中几个,又想生成默认的移动构造/移动赋值重载。可以使用default关键字强制生成默认的某些函数。
比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。(也要强制生成默认的拷贝构造)
// 强制编译器生成
Person(Person&& p) = default;
Person(const Person& p) = default;
如果不强制生成默认的拷贝构造函数会报错,因为强制生成移动构造后,就不会生成拷贝构造了。
编译器生成默认的拷贝构造函数和拷贝赋值运算符的条件是:
- 类中没有定义拷贝构造函数或拷贝赋值运算符。
- 类中没有定义移动构造函数或移动赋值运算符。
- 类中没有定义任何构造函数(除了默认构造函数)。
1.3 禁止生成默认函数的关键字delete
在 C++98 中,如果想要阻止编译器生成某个函数的默认版本,可以将该函数声明为私有,并且只声明不实现。这样,除了类内部之外,外部无法访问这个函数,从而阻止了编译器生成默认版本,其他人想要调用就会报错。
在C++11中更简单,只需在该函数声明加上=delete即可,这将告诉编译器不要生成该函数的默认版本,从而防止它被外部调用。称=delete修饰的函数为删除函数。
Person(Person&& p) = delete;
1.4 其它的类功能
1.类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这
个我们在类和对象默认就讲了,这里就不再细讲了。
2. 继承和多态中的final与override关键字
这个我们在继承和多态部分已经进行了详细讲解这里就不再细讲。只是强调两点:
final 关键字
用途:final 关键字用于修饰类或者函数,表示不允许被继承或者重写。
类修饰:当 final 修饰一个类时,它表示该类不能被其他类继承。
函数修饰:当 final 修饰一个函数时,它表示该函数不能被派生类重写。override 关键字
用途:override 关键字用于修饰虚函数,表示该虚函数是在派生类中重写的基类虚函数。
条件:override 修饰的虚函数必须在派生类中有一个基类虚函数与之匹配。
错误:如果 override 修饰的虚函数在派生类中没有重写基类虚函数,或者重写的函数不符合匹配条件,编译器会报错。
二、可变参数模板
C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。我们现阶段掌握一些基础的可变参数模板特性就够我们用了。
下面是一个可变参数的函数模板:
- Args是一个模板参数包,args是一个函数形参参数包
- 声明一个参数包Args... args,这个参数包中可以包含0到任意个模板参数。
- sizeof...(args) 来获取参数包中参数的数量
template <class... Args>
void ShowList(Args... args)
{
//cout << sizeof(args) << endl;//错误用法
//sizeof...(args) 来获取参数包中参数的数量
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1);
ShowList(1, 2, 3);
return 0;
}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
void _ShowList()
{
cout << endl;
}
// 编译时的递归推演
// 第一个模板参数依次解析获取参数值
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
ShowList(1, 2.2, 'x', 3.3);
return 0;
}
简单解释过程:
- 例如ShowList(1, 2, 3);函数把参数1,2,3传给_ShowList,在这个函数参数(const T& val, Args... args)里,1被看作第一个参数val,后面的所有参数2和3看作一个参数包。
- 打印val,再递归调用_ShowList(val, args)。这个_ShowList函数里参数2被看作第一个参数val,后面的所有参数3看作一个参数包。
- 打印val,再递归调用_ShowList(val,args)。这个_ShowList函数里参数3被看作第一个参数val,后面没有参数,args参数为空。
- 打印val,因为args参数为空,递归调用_ShowList()。输出换行。
方法2:
template <class T>
int PrintArg(T&& t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class... Args>
void ShowList(Args&&... args)
{
// 要初始化arr,强行让编译器解析参数包,参数包有几个参数,PrintArg就依次推演生成几个
int arr[] = { PrintArg(args)... };
cout << endl;
}
- 使用模板参数包推演来初始化一个数组 arr,并且让编译器解析参数包,生成与参数包中参数数量相等的 PrintArg 调用。这是通过使用 ... 运算符在数组初始化表达式中实现的。
- ... 运算符在 C++ 中被称为展开运算符,它用于将模板参数包中的每个参数依次传递给函数或模板。在这个例子中,PrintArg(args)... 表达式会依次调用 PrintArg 函数,每个参数调用一次。
- 在 ShowList 函数中,使用 { PrintArg(args)... } 表达式来初始化数组 arr。这个表达式会依次调用 PrintArg 函数,每个参数调用一次。由于 PrintArg 函数返回 int 类型,所以数组 arr 中的每个元素都是 int 类型的,但它们的值是 PrintArg 函数的返回值,即 0。
三、STL容器中的empalce相关接口函数
list::emplace_back - C++ Reference (cplusplus.com)
vector::emplace_back - C++ Reference (cplusplus.com)
template <class... Args>
void emplace_back (Args&&... args);
emplace 相关接口函数在 C++ 中提供了一种高效的方式来在容器中插入元素。
int main()
{
std::list<mystring::string> lt;
mystring::string s1("1111");
lt.push_back(s1);
lt.push_back(move(s1));
cout << endl;
mystring::string s2("1111");
lt.emplace_back(s2);
lt.emplace_back(move(s2));
cout << endl;
lt.push_back("xxxx");
cout << endl;
lt.emplace_back("xxxx");
return 0;
}
empalce_back与push_back的对比:
- emplace_back 和 push_back 是 C++ 中用于在容器末尾插入元素的两个方法。
- 它们的效率在插入自定义对象时差不多,都可以使用相应的深拷贝和移动拷贝。
- 但是在使用变量传参发生隐式类型时,push_back要进行构造加移动拷贝,而emplace_back直接将这个值一步步传给内部的构造函数构造。省略了移动拷贝的过程。
- 所以emplace_back() 在插入大量元素时比 push_back() 更高效。但实际使用时差距也不是很大。大家可以根据自己情况选择使用。
四、lambda表达式
4.1 lambda的引入
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。但是如果待排序元素为自定义类型,需要用户定义排序时的比较规则。例如一个类中有不同的值可以排序,就要写不同的排序方法。如商品的名称、价格、评分。
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式
4.2 lambda表达式语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
4.2.1 lambda表达式各部分说明
- [capture-list] : 捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable:取消常量性。可以省略。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,一般可以省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
- 在lambda函数定义中,有参数列表、返回值类型还有函数体,与函数的不同就是多加了方括号[]和mutable。
- lambda可以简单理解为匿名函数对象。该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
4.2.2 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 传值方式捕捉的变量不能被修改,捕捉后就成了类仿函数内部的成员变量,但是这个变量是const修饰的。如果需要修改,就需要在表达式加上mutable。
- 传值方式捕捉的变量和外部的变量不是同一个,即使内部可以修改,也不会影响外部的变量值。
- [&var] 不是取地址,而是以引用的方式捕捉对象。不需要再加mutable。想加上也可以加。
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量,捕捉this就可以访问成员变量。 - 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
- lambda表达式之间不能相互赋值,即使看起来类型相同
在 C++ 中,当您创建一个 lambda 函数时,您可以选择捕捉外部作用域中的变量。如果您的 lambda 函数在块作用域(如循环、函数或模板参数包)之外定义,那么它不能捕捉任何变量。这被称为“封闭规则”或“封闭性规则”。
具体来说,这意味着:
- 块作用域:如果 lambda 函数定义在一个块作用域内(如在循环中),它可以捕捉该块作用域内的变量。
- 函数作用域:如果 lambda 函数定义在一个函数内,它可以捕捉该函数作用域内的变量。
- 全局作用域:如果 lambda 函数定义在一个全局作用域内,或者在块作用域之外定义,它不能捕捉任何变量。
这主要是因为当 lambda 函数在全局作用域或块作用域之外定义时,它没有明确的“封闭”作用域。如果它捕捉了外部变量,那么这些变量必须被保持不变,以便 lambda 函数在它的生命周期内能够访问它们。然而,这通常是不可能的,因为这些变量可能在 lambda 函数创建后不久就被销毁或更改。
class AA
{
public:
void func()
{
/*auto f1 = [this]() {
cout << a1 << endl;
cout << a2 << endl;
};*/
//lambda可以在成员函数内部
//不可以使用a1,a2,因为捕捉列表是捕捉父作用域的变量,a1a2不是父作用域的变量
auto f1 = [a1, a2]() {
cout << a1 << endl;
cout << a2 << endl;
};
//捕捉使用this,编译器做了优化处理,不用写this->a1,this->a2
auto f1 = [=]() {
cout << a1 << endl;
cout << a2 << endl;
};
f1();
}
private:
int a1 = 1;
int a2 = 1;
};
4.3 函数对象与lambda表达式
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。
class Rate
{
public:
Rate(double rate)
:_rate(rate)
{}
double operator()(double money, int year)
{
return money * year * _rate;
}
private:
double _rate;
};
int main()
{
double rate = 0.13;
//使用函数对象
Rate r1(rate);
int tmp = r1(10000, 2);
//使用lambda表达式
auto r2 = [=](double money, int year)
{
return money * year * rate;
};
int tmp2 = r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可
以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的。
即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
名字是<lambda_uuid>,即lambda+uuid,随机生成 。
五、包装器
5.1 function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
包装器包装的是可调用对象:函数指针、函数对象、仿函数、lambda表达式。
为什么要引入function包装器?
之前学习的函数指针、仿函数、lambda表达式在使用时都有各自的缺点,例如:
- 函数指针的缺点:类型不好写。(void (*funcName)(int x, int y);)
- 仿函数的缺点:类型好写,但是每次使用都需要写一个类,比较笨重.
- lambda缺点:简洁,但是不好描述类型。不同机器的UUID不同,类型结果不同,即类型是相对匿名的。decltype可以获得类型。
function包装器的表达式语法:
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
void swap_func(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
class Swap
{
public:
void operator()(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
};
int main()
{
int x = 1, y = 2;
cout << x << " " << y << endl;
auto swap_lambda = [](int& r1, int& r2) {
int tmp = r1;
r1 = r2;
r2 = tmp;
};
function<void(int&, int&)> f1 = swap_func;
f1(x, y);
cout << x << " " << y << endl;
function<void(int&, int&)> f2 = Swap();
f2(x, y);
cout << x << " " << y << endl;
function<void(int&, int&)> f3 = swap_lambda;
f3(x, y);
cout << x << " " << y << endl;
map<string, function<void(int&, int&)>> Option =
{
{"函数指针", swap_func},
{"仿函数", Swap()},
{"lambda表达式",swap_lambda}
};
Option["函数指针"](x, y);
cout << x << " " << y << endl;
Option["仿函数"](x, y);
cout << x << " " << y << endl;
Option["lambda表达式"](x, y);
cout << x << " " << y << endl;
return 0;
}
如果function包装的是类的成员对象,
- 需要在对象前面加上类域和& ,&用来取地址,因为普通函数的函数名就是地址,静态的成员函数可以不加,但是推荐加上。
- 包装非静态成员函数,需要在Args…(被调用函数的形参)中指定类的实例化对象的地址。因为非静态成员函数的参数还有一个隐含的this指针。
- 编译器进行优化,可以在Args中实例化匿名对象,不用再实例化出对象传址。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 成员函数取地址,比较特殊,要加一个类域和&
function<int(int, int)> f1 = &Plus::plusi;
cout << f1(1, 2) << endl;
//function<double(double, double)> f2 = &Plus::plusd;//会报错,因为非静态成员函数的参数还有一个隐含的this指针。
function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus ps;
cout << f2(&ps, 1.1, 2.2) << endl;
//编译器进行优化,不用再实例化出对象传址
function<double(Plus, double, double)> f3 = &Plus::plusd;
cout << f3(Plus(), 1.1, 2.2) << endl;
return 0;
}
5.2 bind包装器
function处理类的成员函数时,每次都要在参数上加上类名或者类的指针。为了减少操作,引入了bind,用来调节可调用对象的参数列表。
std::bind 是头文件functional的一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象和可选的参数绑定列表,生成一个新的可调用对象来“适应”原对象的参数列表。这个新的可调用对象可以被用来调用原始的可调用对象,并且可以改变参数的顺序或绑定一些参数的值。
原型如下:
bind的主要作用:
- 调整参数顺序
- 调整参数个数,有些参数可以绑定(bind)时写死。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的可调用对象callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符(placeholders),表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
int Sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
// 调整参数顺序
function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
// 调整参数个数,有些参数可以绑定(bind)时写死
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout << f3(5) << endl;
return 0;
}
function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
cout << f4(1.11, 2.22) << endl;