- 本文内容是接续文章C++11(一)。
一、新的类功能
1、移动构造函数和移动赋值运算符重载
(1)介绍
- 在C++11及以后的版本中,移动语义的引入极大地提高了资源管理和性能优化的能力。移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator)是这一特性的核心部分,它们允许对象通过 “窃取” 另一个对象的资源(如动态分配的内存、文件句柄等)来初始化或赋值,而不是复制这些资源。这样做的好处是可以避免不必要的资源复制,从而提高效率。
- 关于类的其他六个默认成员函数参见类与对象(中)。本文关于类的默认成员主要讲C++11版本中新增的移动构造函数和移动赋值运算符重载。
- 移动构造函数是一种特殊的构造函数,它接受一个右值引用(rvalue reference)作为参数,并将参数中的资源 “移动” 到新创建的对象中。移动构造函数通常用于优化资源密集型对象的创建,如vector, string等。
- 移动赋值运算符是一个成员函数,它接受一个右值引用作为参数,并将参数中的资源 “移动” 到当前对象中,同时释放当前对象原先持有的资源。
(2)编译器默认生成条件
- 如果用户没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现了移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果用户没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
- 编译器生成默认移动构造和移动赋值的条件完全类似。
- 如果用户提供了移动构造或者移动赋值,则编译器不会自动提供拷贝构造和拷贝赋值。
(3)注意事项
- 异常安全:移动构造函数和移动赋值运算符应该设计为noexcept(除非有特别的理由不能这样做),这有助于优化性能,并允许标准库中的某些函数(如vector::resize)使用它们。
- 自赋值安全:虽然移动操作通常不会因自赋值而失败,但实现时仍然应该考虑这种情况,以避免潜在的错误。
- 资源状态:在移动操作后,源对象(即参数对象)应该处于有效但可销毁的状态。这通常意味着源对象的资源(如动态分配的内存)应该被置为无效状态(如设置为nullptr),以避免在析构时释放已移动的资源,即避免重复释放。
- 默认和删除:如果类没有可移动的资源,或者移动操作不适合该类的语义,那么可以显式地删除移动构造函数和移动赋值运算符,或者让编译器生成默认的版本(如果适用)。
(4)示例
2、类成员变量初始缺省值
(1)介绍
- 从C++11开始,允许在类声明时直接对成员变量进行初始化,这是设置成员变量初始缺省值的一种直接方式。这是通过在成员变量声明的末尾添加等号(=)后跟初始值来完成。
(2)示例代码
class TestClass
{
public:
TestClass()
{}
public:
string _s = "snowdragon";
};
int main()
{
TestClass t1;
cout << t1._s << endl;
return 0;
}
(3)运行结果
3、default和delete关键字
(1)介绍
- 在C++中,default和delete关键字在成员函数声明中扮演着特殊角色,特别是在控制构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符的自动生成行为时。
- 这两个关键字提供了一种显式的机制来指示编译器如何(或是否)为这些特殊成员函数生成默认实现。
(2)default
- 当用户希望编译器生成特殊成员函数的默认实现时,但他可能已经定义了其他成员函数(如自定义的构造函数或析构函数),这些自定义成员函数的存在会阻止编译器自动生成这些特殊成员函数。
- 使用default关键字紧随在特殊成员函数的声明之后,用于 “告诉” 编译器,即使我已经为其他成员函数提供了自定义实现,我也希望编译器为我生成这个特殊成员函数的默认实现。
(3)delete
- 当用户想要防止类的对象被拷贝或移动时,使用delete关键字来删除拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符的默认实现。这有助于实现更安全的资源管理,并防止潜在的错误。
- 与default关键字作用相反,使用delete关键字紧随在特殊成员函数的声明之后,用于 “告诉” 编译器,我不想让这个特殊成员函数被定义,任何尝试调用它的代码都应该被编译器拒绝,并给出一个编译时错误。
(4)示例代码
class Person
{
public:
Person()
{}
Person& operator=(const Person& p)
{
if(this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
// 不让编译器生成默认
//Person(Person&& p) = delete;
// 强制编译器生成默认
Person(Person&& p) = default;
Person(const Person& p) = default;
};
4、override与final关键字
参见C++中的多态。
二、可变参数模板
1、介绍
- 可变参数模板(Variable parameter template)是模板编程中的一个重要特性,它允许模板参数的个数可变。这种特性在C++11标准中首次引入,并在D语言中也有支持。
- 定义:可变参数模板指的是模板参数(template parameter)的个数可变的情形。在C++中,这通过三个点(…)来实现,称为模板参数包(template parameter pack)或函数参数包(function parameter pack)。
- 特性:可变参数模板能表示0到任意个数、任意类型的参数,极大地增强了模板的灵活性和泛化能力。
2、递归函数方式展开参数包
(1)介绍
- 在C++中,使用递归函数来展开参数包是一种常见的做法。这种方法依赖于函数模板的递归调用来逐个处理参数包中的每个参数。
- 递归的终止条件通常是参数包为空,此时函数不再递归调用自身。
(2)示例代码
void ShowParameter()
{
cout << endl;
}
template<class T, class ...Args>
void ShowParameter(const T& first, Args... args)
{
cout << first << " ";
ShowParameter(args...);
}
int main()
{
ShowParameter(1);
ShowParameter(1, 2, 3);
ShowParameter(1, 2.3, 'x', "snowdragon");
return 0;
}
(3)实现原理
- ShowParameter函数模板接受至少一个参数(first),并且可选地接受一个或多个额外的参数(args…)。
- 函数首先打印出first参数,然后递归地调用自己,将剩余的参数(args…)作为新的参数包传递。这个过程一直持续到参数包为空,此时会调用没有参数的ShowParameter函数版本(递归终止条件),该版本什么也不做,输出换行只是为了使输出结果更加美观。
(4)运行结果
(5)注意事项
- 这个递归版本在参数包为空时不会直接调用一个带有可变参数模板的函数,而是定义了一个没有参数的ShowParameter函数作为递归的终止条件。这是因为当参数包为空时,尝试调用带有可变参数模板的函数会导致编译错误,因为编译器无法确定如何实例化一个没有任何模板参数的模板。
- 此外,递归方法虽然直观易懂,但在处理大量参数时可能会导致栈溢出,因为每次递归调用都会消耗一定的栈空间。在实际应用中,如果参数数量可能非常大,则需要考虑使用迭代或其他非递归的方法来处理参数包。然而,对于大多数常见的用例来说,递归方法已经足够高效且易于实现。
3、emplace接口
(1)介绍
- insert接口通常用于将已存在的元素(或元素范围)插入到容器的指定位置。这意味着在调用insert之前,元素(或元素的值)必须已经被构造出来。
- emplace接口允许在容器内部直接构造元素,而不需要先构造再复制或移动。这通过传递构造元素所需的参数给emplace函数来实现,而不是传递一个已经构造好的元素。emplace会利用这些参数在容器内部直接调用元素的构造函数来构造新元素。
- 优点:可以减少不必要的复制或移动操作,提高性能。特别是当插入大型对象或需要复杂构造的对象时,这一优势尤为明显。
- 缺点:代码可能相对复杂一些,因为需要传递构造元素所需的参数而不是整个元素。此外,对于不支持在容器内部直接构造的容器类型(如array),emplace接口不可用。
- 当涉及到大型对象或需要复杂构造的对象时,emplace系列接口通常比insert接口更高效。然而,insert接口由于其简单直观的特性,在插入小型对象或简单对象时仍然是一个很好的选择。
(2)示例
三、lambda表达式
1、介绍
- C++中的lambda表达式是一种定义匿名函数对象的简洁方式。它们可以捕获它们所在作用域中的变量,并且可以作为函数对象使用。即它们可以被赋值给函数指针、存储于标准函数对象中,或作为算法(如sort、for_each等)的参数传递。Lambda表达式自C++11起被引入标准。
- lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
2、语法
[capture](parameters) mutable -> return-type { body }
3、参数说明
- capture:捕获列表,指定哪些外部变量在lambda表达式的函数体内是可用的。捕获列表可以是值捕获(变量通过值传递),也可以是引用捕获(变量通过引用传递),即捕捉列表能够捕捉上下文中的变量供lambda函数使用。编译器根据[]来判断接下来的代码是否为lambda函数。
- parameters:参数列表,与普通函数的参数列表类似。如果lambda表达式不接受任何参数,则可以省略该部分(包括圆括号)。
- mutable:这个关键字是可选的,用于指明lambda表达式体内的代码可以修改被捕获的变量的值(这些变量默认是const的)。使用该修饰符时,参数列表不可省略(即使参数为空)。
- return-type:返回类型,可以是任何合法的返回类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- body:函数体,包含了lambda表达式所执行的代码。
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
4、捕获列表说明
(1)值捕获
- 使用:[=]或[var1, var2, …]或[this]
- 使用[=]时,表示值传递方式捕获所有父作用域中的变量(包括this)。
- 可以通过添加指定变量( [var]),显式指定要值捕获的变量。
- [this]:表示值传递方式捕捉当前的this指针。
- 值捕获意味着在lambda表达式体内修改这些变量的副本不会影响原变量。
(2)引用捕获
- 使用:[&] 或 [&var1, &var2, …]
- 使用[&]时,表示引用传递捕捉所有父作用域中的变量(包括this)。
- [&var]:表示引用传递捕捉变量var
- 引用捕获意味着在lambda表达式体内对这些变量的修改会影响原变量。
(3)混合捕获
- 捕获列表中可以混合使用值捕获和引用捕获,即语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
- 比如[=, &var]表示捕获所有外部变量的副本,但var是通过引用捕获的;[&,var, this]表示值传递方式捕捉变量var和this,引用方式捕捉其他变量。
(4)注意事项
- 父作用域指包含lambda函数的语句块。
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。例如,在[=, a]中,=已经以值传递方式捕捉了所有变量,而捕捉a则是重复捕捉。
- 在块作用域以外的lambda函数捕捉列表必须为空;在块作用域中的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同。
- 实际在底层,编译器对于lambda表达式的处理方式完全就是按照函数对象的方式处理的。即,如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()。
5、示例代码
int main()
{
auto f1 = [](int x)
{
cout << x << endl;
return 0;
};
f1(1);
cout << typeid(f1).name() << endl;
cout << endl;
cout << "x, y" << endl;
int x = 0, y = 1;
cout << &x << "\n" << &y << "\n" << endl;
cout << "[=]()" << endl;
auto f2 = [=]() {
cout << &x << "\n" << &y << "\n" << endl;
};
f2();
cout << "[&]()" << endl;
auto f3 = [&]() {
cout << &x << "\n" << &y << "\n" << endl;
};
f3();
cout << "[=, &x]()" << endl;
auto f4 = [=, &x]() {
cout << &x << "\n" << &y << "\n" << endl;
};
f4();
cout << "[&, x]()" << endl;
auto f5 = [&, x]() {
cout << &x << "\n" << &y << "\n" << endl;
};
f5();
return 0;
}
6、运行结果
四、包装器
1、介绍
- 在C++中,function是一个模板类,它是一个通用、多态的函数包装器。
- function的实例可以存储、复制和调用任何Callable目标,如函数对象、lambda表达式、bind表达式或者其他函数对象,还有指向成员函数指针和指向数据成员指针。
- function是一个非常有用的工具,用于编写通用的代码,尤其是当需要在不同的函数或者可调用对象间灵活切换和需要回调或延迟执行时。
- 函数对象可以被复制和移动,并且可以用于直接调用具有指定调用签名的可调用对象(有重载运算符())。
- 函数对象也可以处于没有目标可调用对象的状态。在这种情况下,它们被称为空函数,调用它们会抛出bad_function_call异常。
2、function类
3、示例代码
void SwapFunc(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
struct Swap
{
void operator()(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
};
int main()
{
int x = 0, y = 1;
auto swapLambda = [](int& r1, int& r2) {
int tmp = r1;
r1 = r2;
r2 = tmp;
};
cout << x << " " << y << "\n" << endl;
function<void(int&, int&)> f1 = SwapFunc;
f1(x, y);
cout << x << " " << y << "\n" << endl;
function<void(int&, int&)> f2 = Swap();
f2(x, y);
cout << x << " " << y << "\n" << endl;
function<void(int&, int&)> f3 = swapLambda;
f3(x, y);
cout << x << " " << y << "\n" << endl;
return 0;
}
4、运行结果
五、bind
1、介绍
- 在C++中,bind 是一个函数适配器,它可以将一个可调用的对象(如函数、成员函数或 lambda 表达式)转换成一个新的可调用对象,同时可以改变参数的绑定方式。
- bind通常用于回调、线程函数或者需要特定参数绑定的场景。
2、基本语法
auto bound_function = std::bind(callable, arg_list...);
- callable是一个可调用对象,它可以是一个函数指针、成员函数指针或者一个可调用对象。
- arg_list… 是传递给callable的参数列表。可以使用占位符placeholders::_1, placeholders::_2, … 来表示bind返回的可调用对象在被调用时应接收的参数。
3、示例代码1
(1)代码
class Plus
{
public:
static int PlusI(int x, int y)
{
return x + y;
}
double PlusD(double x, double y)
{
return x + y;
}
};
void TestBindFunction()
{
function<int(int, int)> f1 = Plus::PlusI;
cout << f1(10, 20) << endl;
/*function<double(double, double)> f2 = &Plus::PlusD;
cout << f2(10.1, 20.2) << endl;*/
function<double(Plus*, double, double)> f3 = &Plus::PlusD;
Plus pl;
cout << f3(&pl, 10.1, 20.2) << endl;
function<double(Plus, double, double)> f4 = &Plus::PlusD;
cout << f4(Plus(), 10.1, 20.2) << endl;
function<double(double, double)> f5 = bind(&Plus::PlusD, Plus(), placeholders::_1, placeholders::_2);
cout << f5(10.1, 20.2) << endl;
}
(2)运行结果
4、示例代码2
(1)代码
int Sub(int x, int y)
{
return x - y;
}
void TestBind()
{
function<int(int, int)> f1 = Sub;
cout << f1(20, 10) << endl;
function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(20, 10) << endl;
function<int(int)> f3 = bind(Sub, 30, placeholders::_1);
cout << f3(10) << endl;
}
(2)运行结果
六、相关文章
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕