欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析(3)
目录
- 👉🏻 新的类功能
- 类成员变量初始化
- 强制生成默认函数的关键字default
- 禁止生成默认函数的关键字delete
- 继承和多态中的final与override关键字
- 👉🏻可变参数模板
- empalce相关接口函数
- 👉🏻Lambda表达式
- 注意要点
- 函数对象与lambda表达式
- 👉🏻包装器
- function包装器
- function包装器与map的配合使用
- 👉🏻bind函数
- 绑定函数指针和参数
- 绑定函数对象和参数
- 绑定成员函数和对象指针
- 绑定函数对象和引用参数
👉🏻 新的类功能
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
🗣 针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
- 如果你
没有自己实现移动构造函数
,且没有实现析构函数
、拷贝构造
、拷贝赋值重载
中的任
意一个,即四没。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。 - 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似) - 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值,如果需要,要自己写了老弟。
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
继承和多态中的final与override关键字
这个在【C++】多态中,不再赘述
👉🏻可变参数模板
C++11引入了可变参数模板,它允许函数或类模板接受可变数量的参数。使用可变参数模板可以编写更加灵活和通用的代码。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变
参数,所以我们的用一些奇招来一一获取参数包的值
下面是一个示例,展示了用递归函数方式展开参数包
进行使用可变参数模板:
#include <iostream>
// 基本情况,递归终止条件
void print() {
std::cout << std::endl;
}
// 可变参数模板,递归调用打印每个参数
template<typename T, typename... Args>
void print(const T& first, const Args&... args) {
std::cout << first << " ";
print(args...); // 递归调用,传递剩余参数
}
int main() {
print(1, 2, 3, "hello", 4.5);
return 0;
}
在上面的示例中,我们定义了一个可变参数模板函数print
,它可以接受任意数量的参数。基本情况是一个空函数print()
,当没有参数时输出一个换行符。递归情况使用模板参数包(typename... Args
)来接受可变数量的参数,并使用递归调用打印每个参数。在main
函数中,我们调用print
函数,传递了整数、字符串和浮点数作为参数,它们都被打印出来。
使用逗号表达式展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
//int arr[] = { PrintArg(args)...};
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
empalce相关接口函数
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, bit::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}
emplace_back
和push_back
都是向容器中添加元素的成员函数,但它们有一些重要的区别。
push_back
函数会创建一个临时对象,并将其复制或移动到容器中。如果容器存储的是对象而不是指针,那么这将涉及到对象的复制或移动构造函数和析构函数的调用。这个额外的开销可能会导致效率降低,特别是在频繁插入元素的情况下。
相比之下,emplace_back
函数直接在容器的末尾就地构造一个新的元素(直接构造),而不是先创建一个临时对象。这意味着我们可以避免创建临时对象、复制或移动和析构操作,从而提高性能。实际上,emplace_back
通常比push_back
更快。
在使用emplace_back
时,我们需要注意以下几点:
-
emplace_back
参数应该与容器中元素的构造函数相匹配。 -
如果容器存储的是指针,则必须传递指向堆中分配的对象的指针。否则,在容器释放时可能会出现内存泄漏。
-
对于
std::vector
和std::string
等容器,emplace_back
和push_back
的语义是相同的。
总之,emplace_back
是一个更加高效的添加元素的方法,可以避免创建临时对象并提高性能。
👉🏻Lambda表达式
C++11引入了lambda表达式,它是一种方便的方式来创建匿名函数。lambda表达式可以在需要函数对象的地方使用,并且可以捕获周围作用域中的变量。
一个基本的lambda表达式的语法如下:
[capture list](parameters)mutable -> return_type {
// 函数体
}
其中:
capture list
:捕获列表,用于指定lambda表达式访问的外部变量。parameters
:参数列表,可选地指定输入参数。return_type
:返回类型,可选地指定返回值类型。{}
:函数体,包含实际的操作和代码逻辑。mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性,mutable关键字用于在lambda表达式中标记捕获的变量可以被修改。使用该修饰符时,参数列表不可省略(即使参数为空)。
下面是一个简单的示例,展示了lambda表达式的用法:
#include <iostream>
int main() {
int x = 5;
int y = 10;
auto sum = [](int a, int b) -> int {
return a + b;
};
auto result = sum(x, y);
std::cout << "Sum: " << result << std::endl;
return 0;
}
在上述示例中,我们定义了一个lambda表达式 sum
,它接受两个整数参数并返回它们的和。然后,我们使用该lambda表达式计算变量 x
和 y
的和,并将结果存储在变量 result
中。
lambda表达式还支持捕获外部变量。捕获列表可以指定要在lambda表达式内部使用的外部变量。可以通过值捕获([x]
)、引用捕获([&x]
)或混合捕获([x, &y]
)来捕获变量。
下面是一个使用捕获列表的示例:
#include <iostream>
int main() {
int x = 5;
int y = 10;
auto sum = [x](int a, int b) -> int {
return a + b + x;
};
auto result = sum(y, 15);
std::cout << "Sum: " << result << std::endl;
return 0;
}
在上述示例中,我们使用值捕获 [x]
来捕获变量 x
,并在lambda表达式内部使用它计算结果。注意,在捕获列表中指定的变量将会被复制到lambda表达式的闭包中。
lambda表达式还可以省略参数列表和返回类型,让编译器自动推断。如果lambda表达式没有参数,则可以使用空括号 ()
表示。如果返回类型可以被推断出来,则可以省略返回类型。
除了上述基本用法,lambda表达式还可以与标准库算法(例如std::for_each
、std::transform
等)一起使用,以提供更强大的功能。
🍓捕获列表说明
- [var]:表示值传递方式捕捉变量var
- [=]:表示
值传递
方式捕获所有父作用域中的变量(包括this) - [&var]:表示引用传递捕捉变量var,传引用不用multable,可以修改
- [&]:表示
引用传递
捕捉所有父作用域中的变量(包括this) - [this]:表示值传递方式捕捉当前的this指针
注意要点
-
父作用域指包含lambda函数的语句块
-
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 -
捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 -
在块作用域以外的lambda函数捕捉列表必须为空。
-
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都
会导致编译报错。 -
lambda表达式之间不能相互赋值,即使看起来类型相同
函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{ return money * _rate * year;}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year;
};
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可
以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
👉🏻包装器
C++11引入了一系列包装器(Wrappers),用于在编程中提供更多的灵活性和功能。这些包装器是标准库中的模板类,用于封装和操作其他类型的对象。
以下是C++11中的几个常见的包装器:
-
std::pair
:std::pair
是一个模板类,用于存储两个不同类型的值。它提供了方便的方式来将两个值组合在一起,并且可以通过first
和second
成员访问其中的值。 -
std::tuple
:std::tuple
是一个模板类,用于存储多个不同类型的值。与std::pair
类似,std::tuple
允许将多个值组合在一起,并且可以使用std::get
函数按索引或类型访问其中的值。 -
std::function
:std::function
是一个通用的函数包装器,可以存储可调用对象(如函数、函数指针、lambda表达式等)。它提供了一种机制来传递和存储不同类型的可调用对象,并且可以像普通函数一样进行调用。 -
std::reference_wrapper
:std::reference_wrapper
是一个模板类,用于包装对另一个对象的引用。它提供了一种非拥有(non-owning)引用的方式,允许将引用作为值进行传递和存储,而不是像指针一样对对象进行拥有。
这些包装器提供了更高层次的抽象,使得在编程中可以更灵活地处理不同类型的数据和对象。它们有助于提高代码的可读性、可维护性和重用性,并且在许多情况下可以简化编程任务。
function包装器
std::function
是一个通用的函数包装器,它可以存储可调用对象(如函数名(函数指针)、函数对象、lambda表达式、 类的成员函数等)。使用std::function
可以将任何可调用对象视为具有相同签名的函数,并使用相同的方法进行调用。
🍔以下有案例使用:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
类的成员函数有点特殊,要&取地址和给出类域。
function包装器与map的配合使用
std::function
和std::map
可以很好地配合使用,以实现基于字符串的事件处理程序或回调函数的映射。具体来说,可以将不同的字符串映射到不同的std::function
对象上,然后根据字符串查找相应的函数并调用它。
例如,假设需要实现一个简单的命令行工具,用户可以输入不同的命令,然后程序会执行相应的操作。可以使用std::map
将不同的命令字符串映射到相应的std::function
对象上,然后在用户输入命令时查找相应的函数并调用它。
以下是一个示例代码:
#include <iostream>
#include <map>
#include <functional>
void print_help() {
std::cout << "Usage: command [args...]\n"
<< "Available commands:\n"
<< " help - Print this help message\n"
<< " echo - Print the input arguments\n";
}
void echo(const std::vector<std::string>& args) {
for (const auto& arg : args) {
std::cout << arg << " ";
}
std::cout << std::endl;
}
int main() {
std::map<std::string, std::function<void(const std::vector<std::string>&)>> commands{
{"help", [](const std::vector<std::string>& args){ print_help(); }},
{"echo", echo}
};
std::string command;
while (true) {
std::cout << "> ";
std::getline(std::cin, command);
std::istringstream iss(command);
std::string name;
iss >> name;
auto it = commands.find(name);
if (it != commands.end()) {
std::vector<std::string> args(std::istream_iterator<std::string>{iss},
std::istream_iterator<std::string>{});
it->second(args);
}
else {
std::cout << "Unknown command: " << name << std::endl;
}
}
return 0;
}
在上面的代码中,我们定义了两个命令函数print_help
和echo
,分别用于打印帮助信息和输出输入的参数。然后,我们将这两个函数分别存储在std::function
对象中,并使用std::map
将命令字符串映射到相应的函数上。
在main
函数中,我们使用一个无限循环来等待用户输入命令。每次输入命令后,我们根据空格拆分输入字符串,并将第一个单词作为命令名称。然后,我们在commands
中查找与命令名称匹配的函数,并将剩余的参数传递给它进行处理。
需要注意的是,我们在std::map
中使用了std::function
对象作为值类型,并指定了函数签名为void(const std::vector<std::string>&)
,以便与命令处理函数进行匹配。此外,我们还使用了C++11中的初始化列表语法来方便地初始化commands
对象。
👉🏻bind函数
std::bind
是一个函数,它可以把一个可调用对象和一些参数绑定在一起,返回一个新的可调用对象。这个新的可调用对象可以像原来的可调用对象一样调用,但是其参数已经固定了。
std::bind
的语法比较复杂,需要注意以下几点:
- 第一个参数是要绑定的可调用对象,可以是函数指针、函数对象、成员函数指针等。
- 之后的参数是要绑定的参数,可以是任意类型的值或引用(包括占位符
_1
、_2
等)。 - 返回值是一个新的可调用对象,其参数个数和类型与原来的可调用对象不同,但可以通过占位符进行传递。
下面是一些常见的用法:
绑定函数指针和参数
#include <functional>
#include <iostream>
void foo(int a, int b) {
std::cout << a << " + " << b << " = " << (a + b) << std::endl;
}
int main() {
auto f = std::bind(foo, 2, 3);
f(); // 输出 2 + 3 = 5
return 0;
}
在上面的代码中,我们使用std::bind
将函数foo
和参数2、3绑定在一起,生成一个新的可调用对象f
。然后,我们调用f()
就会执行foo(2, 3)
。
绑定函数对象和参数
#include <functional>
#include <iostream>
struct Bar {
void operator()(int a, int b) const {
std::cout << a << " - " << b << " = " << (a - b) << std::endl;
}
};
int main() {
Bar bar;
auto f = std::bind(bar, 5, std::placeholders::_1);
f(3); // 输出 5 - 3 = 2
return 0;
}
在上面的代码中,我们定义了一个函数对象Bar
,并重载了其括号运算符。然后,我们使用std::bind
将函数对象bar
和参数5、占位符_1
绑定在一起,生成一个新的可调用对象f
。在调用f(3)
时,实参3会替换掉占位符_1
,最终会执行bar(5, 3)
。
绑定成员函数和对象指针
#include <functional>
#include <iostream>
struct Baz {
void hello(int n) const {
std::cout << "hello " << n << std::endl;
}
};
int main() {
Baz baz;
auto f = std::bind(&Baz::hello, &baz, std::placeholders::_1);
f(42); // 输出 hello 42
return 0;
}
在上面的代码中,我们定义了一个类Baz
,其中包含一个成员函数hello
。然后,我们创建一个Baz
对象baz
,并使用std::bind
将其成员函数hello
和对象指针&baz
、占位符_1
绑定在一起,生成一个新的可调用对象f
。在调用f(42)
时,实参42会替换掉占位符_1
,最终会执行baz.hello(42)
。
需要注意的是,在绑定成员函数时,需要使用成员函数指针,同时还需要传入一个对象指针作为第一个参数(可以使用&
取地址符)。
绑定函数对象和引用参数
#include <functional>
#include <iostream>
struct Foo {
void operator()(int& n) const {
n += 10;
}
};
int main() {
int x = 5;
Foo foo;
auto f = std::bind(foo, std::ref(x));
f(); // 修改 x 的值为 15
std::cout << x << std::endl;
return 0;
}
在上面的代码中,我们定义了一个函数对象Foo
,其中包含一个括号运算符,接受一个整数引用,并将其加上10。然后,我们创建一个整数变量x
,并使用std::bind
将函数对象foo
和整数引用x
绑定在一起,生成一个新的可调用对象f
。在调用f()
时,x
的值会被修改为15。
需要注意的是,在绑定引用参数时,需要使用std::ref
函数将其转换为引用。如果直接绑定一个值,那么修改的只是副本,而不是原来的变量。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长