1. lambda表达式
在C++中,lambda表达式是一种用于创建匿名函数的语法。它允许我们在需要的地方定义一个临时的函数,而不需要显式地声明一个函数。
想象一下,你是一个小朋友,你喜欢画画。有一天,你的奶奶给你一个任务,让你根据不同的主题画不同的画。你不想每次都写一个新的函数来完成这个任务,因为这样会很麻烦。你希望能够在需要的时候,快速地定义一个临时的函数来完成任务。在C++中,lambda表达式就是用来解决这个问题的。
lambda表达式的语法如下:
[capture list] (parameter list) -> return type { function body }
- capture list:用于捕获外部变量。可以是空的[],表示不捕获任何外部变量;也可以是[&],表示以引用方式捕获所有外部变量;还可以是[=],表示以值方式捕获所有外部变量。还可以使用具体的外部变量名来指定捕获方式,比如[x]表示以值方式捕获变量x。
在C++中,[]符号用于指定lambda表达式的捕获列表(Capture List)。捕获列表用于在lambda表达式中访问外部变量。
捕获列表有以下几种形式:
- []:不捕获任何外部变量。
- [变量名]:捕获指定的外部变量,可以是多个变量,用逗号分隔。
- [=]:以值的方式捕获所有外部变量,即复制一份外部变量的值到lambda表达式中。在lambda表达式中修改捕获的变量不会影响外部变量。
- [&]:以引用的方式捕获所有外部变量,即直接引用外部变量。在lambda表达式中修改捕获的变量会影响外部变量。
- [变量名1, 变量名2, …]:以值的方式捕获指定的外部变量,可以是多个变量,用逗号分隔。
- [&变量名1, &变量名2, …]:以引用的方式捕获指定的外部变量,可以是多个变量,用逗号分隔。
下面是一个示例代码,演示了lambda表达式中捕获列表的使用:
int main()
{
int x = 10;
int y = 20;
// 捕获x和y,以值的方式
auto lambda1 = [=]() {
std::cout << "x + y = " << x + y << std::endl;
std::cout << "x = " << x << ", y = " << y << std::endl; // 输出: x = 10, y = 20
};
// 捕获x和y,以引用的方式
auto lambda2 = [&]() {
x++;
y++;
std::cout << "x + y = " << x + y << std::endl;
std::cout << "x = " << x << ", y = " << y << std::endl;// 输出: x = 11, y = 21
};
lambda1(); // 输出: x + y = 30
lambda2(); // 输出: x = 11, y = 21
return 0;
}
在这个例子中,我们定义了两个lambda表达式lambda1和lambda2,它们分别捕获了外部变量x和y。
lambda1以值的方式捕获了x和y,在lambda表达式中只能读取这些变量的值,不能修改它们。
lambda2以引用的方式捕获了x和y,在lambda表达式中可以读取和修改这些变量的值,对x和y的修改会反映到外部变量。
通过运行这段代码,我们可以看到lambda表达式根据捕获列表的不同,对外部变量的访问和修改也不同。
总之,[]符号用于指定lambda表达式的捕获列表,通过捕获外部变量,lambda表达式可以访问和修改这些变量的值。不同的捕获方式会导致不同的访问和修改行为。
-
parameter list:用于指定lambda函数的参数列表。和普通函数的参数列表一样,可以包含参数名和类型。
-
return type:用于指定lambda函数的返回类型。可以省略,编译器会自动推导返回类型。
-
function body:用于定义lambda函数的具体实现。
下面是一个lambda表达式的例子:
int main()
{
int x = 5;
int y = 10;
auto sum = [](int a, int b)
{
return a + b;
};
int result = sum(x, y);
cout << "Sum: " << result << endl;
return 0;
}
输出结果:
在这个例子中,我们定义了一个lambda表达式来计算两个整数的和。在这个例子中,参数列表是(int a, int b),函数体是return a + b;。
我们使用关键字auto来声明一个变量sum,并将lambda表达式赋值给它。这样,sum就成为了一个可以调用的函数对象,可以像普通函数一样使用。
在main函数中,我们调用了sum函数,并将结果赋给变量result。然后,我们使用std::cout打印出了结果。
lambda表达式的优点是它可以在需要的地方快速定义一个临时的函数,避免了显式地声明一个函数。它可以捕获外部变量,让我们在函数体内部使用外部变量。它还可以作为参数传递给其他函数,使代码更加灵活和可读。
总之,lambda表达式是C++中的一种匿名函数,可以在需要的地方快速定义一个临时的函数。它的语法简洁,可以捕获外部变量,并可以作为参数传递给其他函数。
2. 移动构造函数和移动赋值运算符重载
当我们在C++中使用对象时,有时候需要进行对象的拷贝或赋值操作。通常情况下,拷贝构造函数和拷贝赋值运算符会被调用来完成这些操作。但是,在某些情况下,拷贝操作可能会比较耗时,特别是当对象的数据较大时。为了提高效率,C++引入了移动构造函数和移动赋值运算符。移动构造函数和移动赋值运算符允许将一个对象的资源(比如内存)从一个对象转移到另一个对象,而不是进行深拷贝。这样可以避免不必要的内存分配和释放,提高程序的性能。
针对这两个函数,需要注意的点是:
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
假设奶奶有一本书,她想把这本书给小学生看。如果奶奶使用拷贝操作,她需要先复制这本书的内容,然后再给小学生。这样做会浪费时间和资源。
但是,如果奶奶使用移动操作,她只需要将这本书直接给小学生,而不需要复制内容。这样就更加高效。
class Book
{
public:
Book()
{
std::cout << "默认构造函数" << std::endl;
data = new int[1000000]; // 假设data是一个大型的数据结构
}
Book(const Book& other)
{
std::cout << "拷贝构造函数" << std::endl;
data = new int[1000000];
std::copy(other.data, other.data + 1000000, data);
}
Book(Book&& other)
{
std::cout << "移动构造函数" << std::endl;
data = other.data;
other.data = nullptr;
}
Book& operator=(const Book& other)
{
std::cout << "拷贝赋值运算符" << std::endl;
if (this != &other) {
delete[] data;
data = new int[1000000];
std::copy(other.data, other.data + 1000000, data);
}
return *this;
}
Book& operator=(Book&& other)
{
std::cout << "移动赋值运算符" << std::endl;
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
~Book() {
std::cout << "析构函数" << std::endl;
delete[] data;
}
private:
int* data;
};
int main()
{
Book book1; // 调用默认构造函数
Book book2 = book1; // 调用拷贝构造函数
Book book3(std::move(book1)); // 调用移动构造函数
book2 = book3; // 调用拷贝赋值运算符
book3 = std::move(book2); // 调用移动赋值运算符
return 0;
}
运行结果:
3. default关键字
当我们在C++中定义一个类的成员函数时,有时候我们希望使用默认的实现,而不需要自己去编写具体的实现。C++提供了一个default关键字,可以用来指示编译器生成默认的实现。
使用default关键字可以让编译器自动生成以下几种函数的默认实现:
- 默认构造函数:当我们没有定义任何构造函数时,编译器会自动生成一个默认构造函数。
- 拷贝构造函数:当我们没有定义拷贝构造函数时,编译器会自动生成一个默认的拷贝构造函数。
- 移动构造函数:当我们没有定义移动构造函数时,编译器会自动生成一个默认的移动构造函数。
- 拷贝赋值运算符:当我们没有定义拷贝赋值运算符时,编译器会自动生成一个默认的拷贝赋值运算符。
- 移动赋值运算符:当我们没有定义移动赋值运算符时,编译器会自动生成一个默认的移动赋值运算符。
- 析构函数:当我们没有定义析构函数时,编译器会自动生成一个默认的析构函数。
下面是一个示例代码,演示了如何使用default关键字来生成默认的构造函数和拷贝构造函数:
class MyClass
{
public:
MyClass() = default; // 默认构造函数
MyClass(const MyClass& other) = default; // 拷贝构造函数
void print()
{
std::cout << "Hello, World!" << std::endl;
}
};
int main()
{
MyClass obj1; // 调用默认构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数
obj1.print(); // 输出: Hello, World!
obj2.print(); // 输出: Hello, World!
return 0;
}
运行结果:
在这个例子中,我们定义了一个名为MyClass的类。**我们使用default关键字来生成默认的构造函数和拷贝构造函数。**然后,我们创建了两个MyClass对象obj1和obj2,并调用了它们的print函数。
通过运行这段代码,我们可以看到两个对象的构造和拷贝构造函数被正确地调用,并且print函数输出了"Hello, World!"。
总之,default关键字可以让编译器自动生成默认的函数实现,避免我们手动编写这些函数的实现。这样可以简化代码,并且确保这些函数的行为符合预期。
4. delete关键字
在C++中,delete关键字用于删除某些特殊函数的默认实现。通过使用delete关键字,我们可以告诉编译器不要生成特定函数的默认实现。
使用delete关键字可以实现以下几种效果:
- 禁用默认构造函数:通过将构造函数声明为delete,我们可以禁用默认构造函数,使得对象不能被默认构造。
- 禁用拷贝构造函数和拷贝赋值运算符:通过将拷贝构造函数和拷贝赋值运算符声明为delete,我们可以禁用对象的拷贝操作。
- 禁用移动构造函数和移动赋值运算符:通过将移动构造函数和移动赋值运算符声明为delete,我们可以禁用对象的移动操作。
下面是一个示例代码,演示了如何使用delete关键字来禁用默认构造函数和拷贝构造函数:
class MyClass
{
public:
MyClass() = delete; // 禁用默认构造函数
MyClass(const MyClass& other) = delete; // 禁用拷贝构造函数
void print()
{
std::cout << "Hello, World!" << std::endl;
}
};
int main()
{
MyClass obj1; // 编译错误,禁用了默认构造函数
MyClass obj2 = obj1; // 编译错误,禁用了拷贝构造函数
return 0;
}
编译错误:
在这个例子中,我们定义了一个名为MyClass的类。我们使用delete关键字来禁用默认构造函数和拷贝构造函数。然后,我们尝试创建一个MyClass对象obj1和使用obj1初始化另一个对象obj2。
由于我们将默认构造函数和拷贝构造函数声明为delete,所以编译器会报错,提示我们禁用了这些函数,无法使用它们来创建对象。
总之,delete关键字可以用来禁用特定函数的默认实现,从而限制对象的某些操作。这样可以提高代码的安全性和可读性,确保对象的行为符合预期。
5. 可变参数模板
可变参数模板是C++11引入的一种模板特性,它允许我们定义一个可以接受任意数量和任意类型参数的模板函数或模板类。简单来说,它就像是一个可以接受不确定数量和类型的参数的函数。
举个例子来说明可变参数模板的使用。假设我们想要实现一个函数sum,可以计算任意数量的参数的和。我们可以使用可变参数模板来实现这个功能。
#include <iostream>
// 基本情况:没有参数时,返回0
int sum() {
return 0;
}
// 递归情况:计算第一个参数和剩余参数的和
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
int main() {
int result = sum(1, 2, 3, 4, 5);
std::cout << "Sum: " << result << std::endl; // 输出: Sum: 15
return 0;
}
在这个例子中,我们定义了一个可变参数模板函数sum。它有两个重载版本,一个是基本情况,当没有参数时返回0;另一个是递归情况,计算第一个参数和剩余参数的和。
在递归情况中,我们使用了模板参数包(template parameter pack)Args…来表示可变数量的参数。通过使用递归调用和展开参数包的方式,我们可以逐个计算参数的和。
在main函数中,我们调用sum函数,并传递了一系列整数作为参数。sum函数会计算这些整数的和,并将结果存储在result变量中,最后输出结果。
需要注意的是,可变参数模板还可以与其他模板特性(如模板参数推导、完美转发等)结合使用,以实现更加灵活和通用的功能。
**当没有第一个形参时,我们可以使用递归基准情况来处理。**这里举一个没有第一个形参的例子,假设我们想要实现一个函数print,可以打印出任意数量的参数。
#include <iostream>
// 递归基准情况:没有参数时,终止递归
void print() {
std::cout << std::endl;
}
// 递归情况:打印第一个参数,然后递归打印剩余参数
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
int main() {
print(1, "hello", 3.14, 'a');
// 输出: 1 hello 3.14 a
return 0;
}
在这个例子中,我们定义了一个可变参数模板函数print。它有两个重载版本,一个是递归基准情况,当没有参数时终止递归;另一个是递归情况,它打印第一个参数,然后递归打印剩余参数。
在main函数中,我们调用print函数,并传递了一系列不同类型的参数。print函数会逐个打印这些参数,最后输出结果。
需要注意的是,递归基准情况在没有第一个形参时起到了终止递归的作用。这样,当参数传递完毕时,递归调用会停止,避免无限递归的问题。
这个例子展示了如何使用可变参数模板来实现一个可以打印任意数量参数的函数。通过递归调用和展开参数包的方式,我们可以逐个处理参数,实现更加灵活和通用的代码。
总之,可变参数模板是C++中的一种模板特性,允许我们定义可以接受任意数量和任意类型参数的模板函数或模板类。通过使用可变参数模板,我们可以实现更加灵活和通用的代码。
6. push_back和emplace_back的区别
当我们使用C++中的容器时,有两个常用的函数push_back和emplace_back,它们用于在容器的末尾插入元素。
**push_back函数接受一个参数,该参数是要插入的元素的副本。它将创建一个新的元素,并将其添加到容器的末尾。**例如,如果我们有一个vector容器,我们可以使用push_back函数将一个整数添加到容器的末尾。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
for (int i : vec) {
std::cout << i << " ";
}
// 输出: 1 2 3
return 0;
}
**emplace_back函数与push_back函数类似,但它接受多个参数,并将这些参数传递给容器中的元素类型的构造函数。这意味着我们可以直接在容器中构造元素,而不需要创建副本。这在某些情况下可以提高性能。**例如,如果我们有一个vector容器,其中Person类有一个带有名称和年龄参数的构造函数,我们可以使用emplace_back函数直接在容器中构造一个新的Person对象。
#include <iostream>
#include <vector>
#include <string>
class Person {
public:
Person(const std::string& name, int age) : name_(name), age_(age) {}
void Print() {
std::cout << "Name: " << name_ << ", Age: " << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
std::vector<Person> vec;
vec.emplace_back("Alice", 25);
vec.emplace_back("Bob", 30);
vec.emplace_back("Charlie", 35);
for (const Person& p : vec) {
p.Print();
}
// 输出:
// Name: Alice, Age: 25
// Name: Bob, Age: 30
// Name: Charlie, Age: 35
return 0;
}
区别总结:
- push_back函数接受一个参数,该参数是要插入的元素的副本。
- emplace_back函数接受多个参数,并将这些参数传递给容器中的元素类型的构造函数,直接在容器中构造新的元素,而不需要创建副本。
- 使用emplace_back函数可以避免创建副本,提高性能。
emplace_back并不总是比push_back更高效。它们的性能取决于具体的情况。
emplace_back的优势在于它可以直接在容器中构造对象,而不需要创建副本。这意味着它可以避免额外的对象复制或移动操作,从而提高性能。
**然而,emplace_back的效果取决于对象的构造函数。如果对象的构造函数非常复杂或开销很大,那么emplace_back可能会比push_back更慢。**因为emplace_back需要在容器中直接构造对象,而构造函数的复杂性可能会导致更多的计算和内存分配。
另外,emplace_back和push_back对于容器的内存分配也有不同的影响。emplace_back可以利用容器的内存,避免不必要的内存分配和拷贝,从而提高效率。而push_back则需要在容器中分配新的内存,并将对象复制或移动到新的内存中。
因此,要确定哪种方法更高效,需要根据具体的情况进行评估。在大多数情况下,emplace_back通常会更高效,特别是当对象的构造函数相对简单且开销较小时。但是在某些情况下,push_back可能会更适合,特别是当对象的构造函数比较复杂或开销较大时。
综上所述,emplace_back和push_back都有各自的优势和适用场景,具体使用哪种方法取决于具体的情况和需求。
6.1. emplace_back的优势
当我们在容器中使用emplace_back或push_back添加对象时,如果对象是通过移动构造函数构造的,将会更高效。移动构造函数可以避免不必要的数据复制,直接将资源从一个对象转移到另一个对象。
下面是一个示例,演示了如何在Person类中添加移动构造函数,并在构造函数中打印信息:
class Person {
public:
Person(const std::string& name, int age)
: name_(name), age_(age) {
std::cout << "构造函数: " << name_ << ", " << age_ << std::endl;
}
Person(const Person& other)
: name_(other.name_), age_(other.age_) {
std::cout << "拷贝构造: " << name_ << ", " << age_ << std::endl;
}
Person(Person&& other)
: name_(std::move(other.name_)), age_(other.age_) {
std::cout << "移动构造: " << name_ << ", " << age_ << std::endl;
}
void Print() const {
std::cout << "Name: " << name_ << ", Age: " << age_ << std::endl;
}
private:
std::string name_;
int age_;
};
int main() {
std::vector<Person> vec;
vec.emplace_back("Alice", 25);
vec.emplace_back("Bob", 30);
vec.emplace_back("Charlie", 35);
std::cout << std::endl;
vec.push_back(Person("Dave", 40));
vec.push_back(Person("Eve", 45));
vec.push_back(Person("Frank", 50));
for (const Person& p : vec) {
p.Print();
}
return 0;
}
当运行上面的代码时,输出结果如下:
构造函数: Alice, 25
构造函数: Bob, 30
构造函数: Charlie, 35
移动构造: Dave, 40
构造函数: Dave, 40
移动构造: Eve, 45
构造函数: Eve, 45
移动构造: Frank, 50
构造函数: Frank, 50
Name: Alice, Age: 25
Name: Bob, Age: 30
Name: Charlie, Age: 35
Name: Dave, Age: 40
Name: Eve, Age: 45
Name: Frank, Age: 50
输出结果说明了对象的构造和移动过程:
- 首先,通过emplace_back方法添加了三个对象,分别是Alice、Bob和Charlie。这些对象是通过构造函数直接在容器中构造的,没有进行拷贝或移动操作。
- 然后,通过push_back方法添加了三个对象,分别是Dave、Eve和Frank。这些对象是通过移动构造函数从临时对象中移动构造而来的。
- 最后,通过循环遍历容器中的对象,打印了每个对象的名称和年龄。
从输出结果可以看出,使用emplace_back方法构造对象时,并没有触发拷贝构造函数的调用,而使用push_back方法构造对象时,触发了移动构造函数的调用。
移动构造函数通过使用std::move来将资源从一个对象移动到另一个对象,避免了不必要的数据复制,提高了性能。
7. 包装器
在C++中,包装器是一种功能强大的工具,它可以将不同类型的可调用对象(如函数、函数指针、lambda表达式等)统一封装成一个对象,从而实现动态的函数调用。
想象一下你有三个不同的函数,分别是加法、减法和乘法。你希望能够在运行时动态地选择和调用这些函数。这时,你可以使用C++中的包装器来实现。
**首先,你需要使用std::function来声明一个变量,该变量可以存储任意具有相同参数和返回类型的可调用对象。**在我们的例子中,我们声明了一个std::function<int(int, int)>类型的变量operation,它可以存储两个整数参数并返回一个整数结果的可调用对象。
然后,你可以将不同的函数赋值给operation,以实现动态选择和调用不同的函数。在我们的例子中,我们分别将加法、减法和乘法函数赋值给operation。
最后,你可以使用operation来调用所选择的函数,并得到结果。在我们的例子中,我们分别使用加法、减法和乘法函数进行运算,并打印结果。
下面是相应的代码示例:
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
// 使用add函数进行加法运算
std::function<int(int, int)> operation = add;
std::cout << "加法运算: " << operation(5, 3) << std::endl;
// 使用subtract函数进行减法运算
operation = subtract;
std::cout << "减法运算: " << operation(5, 3) << std::endl;
// 使用multiply函数进行乘法运算
operation = multiply;
std::cout << "乘法运算: " << operation(5, 3) << std::endl;
return 0;
}
运行结果:
在这个例子中,我们定义了三个不同的函数add、subtract和multiply,它们都接受两个整数参数并返回一个整数结果。
然后,我们声明了一个std::function<int(int, int)>类型的变量operation,它可以存储任意具有相同参数和返回类型的可调用对象。
通过将不同的函数赋值给operation,我们可以在运行时动态选择和调用不同的函数。在主函数中,我们分别使用加法、减法和乘法函数进行运算,并打印结果。
通过使用包装器,我们可以在不改变代码结构的情况下,灵活地切换和组合不同的函数。这种灵活性对于实现策略模式、回调函数等场景非常有用。
7.1. 包装器的语法格式
当使用std::function包装器时,你需要指定可调用对象的签名(参数类型和返回类型),并将其作为模板参数传递给std::function类模板。语法格式如下:
std::function<返回类型(参数类型1, 参数类型2, ...)> 变量名;
其中,返回类型是可调用对象的返回类型,参数类型1, 参数类型2, …是可调用对象的参数类型,变量名是你给包装器变量取的名称。
下面是一些std::function包装器的常见用法:
- 将函数赋值给std::function变量:
std::function<int(int, int)> operation = add;
在这个例子中,我们将add函数赋值给了名为operation的std::function变量。这样,operation就可以被调用,并执行加法运算。
- 使用std::function调用可调用对象:
int result = operation(5, 3);
在这个例子中,我们使用operation调用了可调用对象(在这里是add函数),并将结果赋值给result变量。
- 重新赋值std::function变量:
operation = subtract;
在这个例子中,我们将subtract函数赋值给了之前的operation变量。这样,operation就可以被调用,并执行减法运算。
通过重新赋值std::function变量,我们可以在运行时动态地选择和调用不同的函数。
总而言之,std::function包装器提供了一种灵活的方式来存储和调用不同类型的可调用对象。通过使用std::function,我们可以实现动态选择和调用函数,从而提高代码的灵活性和可维护性。