一.可调用对象
1.可调用对象
可调用对象分为四类:
-
是一个函数指针
int print(int a, double b) {cout << a << b << endl;return 0; } // 定义函数指针 int (*func)(int, double) = print; //使用using起别名来定义函数指针,注意把using起的别名当作一个类,使用时通过构造一个实例对象指向相应的函数 using func_ptr = int (*)(int, double)
知识点1:函数指针的格式,
返回值类型 (*指针名)(参数类型列表)
知识点2:使用
uisng
和typedef
起别名 -
是一个具有operator()成员函数的类对象(仿函数)
#include <iostream> #include <string> #include <vector> using namespace std;struct Test {// ()操作符重载void operator()(string msg){cout << "msg: " << msg << endl;} };int main(void) {Test t;t("我是要成为海贼王的男人!!!"); // 仿函数return 0; }
知识点1:重载操作符
()
(仿函数) -
是一个可被转换为函数指针的类对象
#include <iostream> #include <string> #include <vector> using namespace std;using func_ptr = void(*)(int, string); struct Test {static void print(int a, string b){cout << "name: " << b << ", age: " << a << endl;}// 将类对象转换为函数指针operator func_ptr(){return print;} };int main(void) {Test t;// 对象转换为函数指针, 并调用t(19, "Monkey D. Luffy");return 0; }
知识点1:
operator
不仅可以重载操作符,还可以表示类型转化的意思,这时候其语法为operator 目标类型() const;
因为这里的目标类型是一个函数指针,所以返回一个函数。
注意点1:这里并不是重载
()
而构成的仿函数注意区分。 -
是一个类成员函数指针或者类成员指针
#include <iostream> #include <string> #include <vector> using namespace std;struct Test {void print(int a, string b){cout << "name: " << b << ", age: " << a << endl;}int m_num; };int main(void) {// 定义类成员函数指针指向类成员函数void (Test::*func_ptr)(int, string) = &Test::print; //这里的&可有可无,因为函数名本身就是地址// 类成员指针指向类成员变量int Test::*obj_ptr = &Test::m_num;Test t;// 通过类成员函数指针调用类成员函数(t.*func_ptr)(19, "Monkey D. Luffy");// 通过类成员指针初始化类成员变量t.*obj_ptr = 1;cout << "number is: " << t.m_num << endl;return 0; }
知识点1:类成员函数指针或者类成员指针
2.包装器
std::function
是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
std::function
必须要包含一个叫做functional
的头文件,可调用对象包装器使用语法如下:
#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
详细用法:
#include <iostream>
#include <functional>
using namespace std;int add(int a, int b)
{cout << a << " + " << b << " = " << a + b << endl;return a + b;
}class T1
{
public:static int sub(int a, int b){cout << a << " - " << b << " = " << a - b << endl;return a - b;}
};class T2
{
public:int operator()(int a, int b){cout << a << " * " << b << " = " << a * b << endl;return a * b;}
};int main(void)
{// 绑定一个普通函数function<int(int, int)> f1 = add;// 绑定以静态类成员函数function<int(int, int)> f2 = T1::sub;// 绑定一个仿函数T2 t;function<int(int, int)> f3 = t;// 函数调用f1(9, 3);f2(9, 3);f3(9, 3);return 0;
}
在上面的类对象转化为函数指针后,也可以利用包装器进行封装。
3.回调函数
#include <iostream>
#include <functional>
using namespace std;class A
{
public:// 构造函数参数是一个包装器对象A(const function<void()>& f) : callback(f){}void notify(){callback(); // 调用通过构造函数得到的函数指针}
private:function<void()> callback;
};class B
{
public:void operator()(){cout << "我是要成为海贼王的男人!!!" << endl;}
};
int main(void)
{B b;A a(b); // 仿函数通过包装器对象进行包装a.notify();return 0;
}
callback
是一个 std::function<void()>
类型的对象,这意味着它可以存储任何没有参数且返回类型为 void
的可调用对象。在类 A
的构造函数中,callback
被初始化为传入的 function<void()>
参数,这样它就可以在之后被调用。
std::function
的这种能力使得它非常适合用于回调机制,因为它提供了一种统一的方式来处理不同的可调用对象。在您的例子中,B
类定义了一个重载了 ()
运算符的成员函数,这使得 B
的对象可以像函数一样被调用,成为一个仿函数(functor)。当您创建 A
类的对象 a
并将 B
类的对象 b
作为参数传递给 A
的构造函数时,b
被自动转换为 std::function<void()>
类型,并在 A
类的 notify
成员函数中被调用。
4.绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
- 将可调用对象与其参数一起绑定成一个仿函数。
- 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
// 绑定非类成员函数/变量
auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
注意这里bind
函数得到的是一个可调用对象的包装器类型。
绑定器函数使用例子如下:
- 基础:绑定后作为仿函数使用
#include <iostream>
#include <functional>
using namespace std;void output(int x, int y)
{cout << x << " " << y << endl;
}int main(void)
{// 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数bind(output, 1, 2)();bind(output, placeholders::_1, 2)(10);bind(output, 2, placeholders::_1)(10);// error, 调用时没有第二个参数// bind(output, 2, placeholders::_2)(10);// 调用时第一个参数10被吞掉了,没有被使用bind(output, 2, placeholders::_2)(10, 20);bind(output, placeholders::_1, placeholders::_2)(10, 20);bind(output, placeholders::_2, placeholders::_1)(10, 20);return 0;
}
-
绑定全局函数
#include <iostream> #include <functional> using namespace std;void callFunc(int x, const function<void(int)>& f) {if (x % 2 == 0){f(x);} }void output(int x) {cout << x << " "; }void output_add(int x) {cout << x + 10 << " "; }int main(void) {// 使用绑定器绑定可调用对象和参数auto f1 = bind(output, placeholders::_1);for (int i = 0; i < 10; ++i){callFunc(i, f1);}cout << endl;auto f2 = bind(output_add, placeholders::_1);for (int i = 0; i < 10; ++i){callFunc(i, f2);}cout << endl;return 0; }
-
绑定成员函数(变量)
#include <iostream> #include <functional> using namespace std;class Test { public:void output(int x, int y){cout << "x: " << x << ", y: " << y << endl;}int m_number = 100; };int main(void) {Test t;// 绑定类成员函数function<void(int, int)> f1 = bind(&Test::output, &t, placeholders::_1, placeholders::_2);// 绑定类成员变量(公共)function<int&(void)> f2 = bind(&Test::m_number, &t);// 调用f1(520, 1314);f2() = 2333;cout << "t.m_number: " << t.m_number << endl;return 0; }
二.模板补充
前提:已经了解基础的函数模板和类模板
1.嵌套模板
#include <iostream>using namespace std;template<class T>
class Data
{
public:Data(T data) :data(data) {}
private:T data;
};template<class Ty>
class MM
{
public:MM(Ty data):data(data){}void print(){cout << data << endl;}
private:Ty data;
};int main()
{MM<Data<string>> girl(Data<string>("string"));girl.print();return 0;
}
核心知识点:
- 友元函数一般在类外定义,类内声明
- 友元函数如果是一个模板函数,那么不仅类外定义时需要使用
templaye
修饰,而且类内声明时也需要使用templaye
修饰 - 每使用一个类(只要是类模板)都要配合
<>
使用 - 嵌套时,最重要的一个问题时缺乏与之匹配的操作符(==》运算符重载)
上述代码肯定是错的,因为<<
并没有进行重载
#include <iostream>using namespace std;template<class T>
class Data
{
public:Data(T data) :data(data) {}template <class T>friend ostream& operator<<(ostream& out, const Data<T>& object);
private:T data;
};template<class Ty>
class MM
{
public:MM(Ty data):data(data){}void print(){cout << data << endl;}
private:Ty data;
};template <class T>
ostream& operator<<(ostream& out, const Data<T>& object)
{out << object.data;return out;
}int main()
{MM<Data<string>> girl(Data<string>("string"));girl.print();return 0;
}
2.可变参的模板函数
与原来的模板函数相比,它有一个折叠参数:template <class...Args>
展开折叠参数的过程:
-
1:递归的方式展开参数包
#include <iostream>template <class T> void print(const T& t) {std::cout << t << std::endl; }// 递归模板函数 template <class T, class... Args> void printdata(const T& first, const Args&... rest) {print(first); // 打印第一个参数printdata(rest...); // 递归调用,处理剩余的参数 }// 递归终止条件 template <> void printdata() {// 当没有参数时,递归终止 }int main() {printdata(1, 2.3, "hello");return 0; }
在 C++ 中,当您看到一个函数模板的参数列表中包含
const T& first, const Args&... rest
,这表示该函数模板接受至少一个参数,并且可以接受任意数量的额外参数。这里的const T& first
是第一个参数,它是一个常量引用,而const Args&... rest
是一个参数包,它包含剩余的所有参数,这些参数也是常量引用。 -
2:列表的方式展开参数包
#include <iostream>using namespace std;template<class T> void print(T data) {cout << data << "\t"; }template <class...Args> //涉及到逗号表达式 void printdata(Args...args) {int array[] = { (print(args),0)... }; //对args的每一个参数都执行print函数,每一次都返回0把它存储在array中//其中{}是初始化列表符号,重要的是理解内部的逗号表达式 }int main() {printdata(1, "hello word", 2.2f, 'A');return 0; }
简单来谈一下我的理解:class...Args
代表模板参数包(类),而Args...args
实际上就相当于args
是Args
的一个实例化,完全可以Args...x
来实例一个叫x
的函数参数包。
这里,(print(args), 0)
是一个逗号表达式,它首先对每个 args
参数调用 print
函数,然后返回 0
。这个表达式被用在初始化列表中,列表中的 ...
是初始化列表展开运算符,它将模式 (print(args), 0)
应用到参数包 args
中的每个参数上。结果是创建了一个数组,其每个元素都是 0
,数组的大小与 args
参数包中的参数数量相同。
3.逗号表达式
逗号表达式(Comma operator)是一个二元运算符,它用来序列化多个表达式。逗号表达式的值是它的右操作数的值。当逗号表达式作为语句出现时,它的左操作数会被计算,但它的结果会被丢弃,然后计算右操作数,并返回其结果。
expression1, expression2
理解:会从左到右执行每个语句,但是取最后一个语句expression2
作为其返回值。
因此,{ (print(args), 0)... }
实际上是将 (print(args), 0)
这个逗号表达式应用到参数包 args
中的每个参数上,并为数组 array
初始化一个元素为 0
的序列。这个序列的长度与 args
参数包中的参数数量相同。...
表示应用到数据包上。
4.tuple元组
在C++11后,标准库新添加了一种数据容器——tuple
元组,tuple
的用法类似于 pair
,但它可以包含超过两个元素。你可以创建和访问元组,以及使用 std::get
函数模板来获取元组中的元素。此外,tuple
提供了一些辅助函数,如 std::make_tuple
来创建元组,std::tie
来解包元组,以及 std::forward_as_tuple
来创建一个元组,其元素是通过完美转发得到的。
下面是一个简单的 tuple
用法示例:
#include <iostream>
#include <tuple>
#include <string>int main() {// 创建一个元组std::tuple<int, std::string, double> tup(42, "Hello", 3.14);// 访问元组中的元素std::cout << "Integer: " << std::get<0>(tup) << std::endl;std::cout << "String: " << std::get<1>(tup) << std::endl;std::cout << "Double: " << std::get<2>(tup) << std::endl;// 访问元组中的第一个元素(整数)int i = std::get<0>(tup);std::cout << "Integer: " << i << std::endl;// 使用 std::tie 解包元组int i;std::string s;double d;std::tie(i, s, d) = tup;std::cout << "Unpacked Integer: " << i << std::endl;std::cout << "Unpacked String: " << s << std::endl;std::cout << "Unpacked Double: " << d << std::endl;return 0;
}
6.特化模板
在 C++ 中,template <>
是用来特化一个模板的语法。当您看到一个模板声明后面跟着 template <>
,这表示您正在为一个特定的模板实例提供一个专门的实现,这个实现会覆盖默认的模板实例化。
特化模板通常用于以下几种情况:
- 递归终止条件:在处理可变参数模板时,您可能需要一个递归终止条件来结束递归。这通常是通过为一个没有参数的模板提供一个特化来实现。例如:
template <class... Args>
void printdata(Args... args) {// 处理参数printdata(args...); // 递归调用
}template <>
void printdata() {// 递归终止条件,没有更多参数可处理
}
在这个例子中,printdata
是一个可变参数模板函数,它递归地调用自身来处理参数。当没有更多的参数需要处理时,它会调用一个特化的 printdata
版本,这个版本没有参数,作为递归终止条件。
2.特定类型的特殊行为:有时您可能需要为特定类型提供特殊的行为。例如,您可能需要一个模板函数来处理任何类型的数组,但对于 std::string
类型,您想要特殊处理。您可以这样做:
template <class T>
void processArray(T& arr) {// 默认处理
}template <>
void processArray<std::string>(std::string& arr) {// 特殊处理 std::string
}
在这个例子中,processArray
是一个模板函数,它有一个针对 std::string
类型的特化版本,这个版本会提供特殊的行为。