本篇博客基于可运行源码,完整保留源码注释与运行日志,代码块在前,知识点解析在后,自底向上梳理面试核心考点。
测试实体类 Int
cpp
#include<iostream>
#include<functional>
using namespace std;
class Int
{
private:
int value; //heap;
public:
Int() :value(0)
{
cout << "defaul create " << this << " " << value << endl;
}
Int(int x) :value{ x }
{
cout << "create Int(int) " << this << " " << value << endl;
}
~Int()
{
cout << "destroy Int : " << this << " " << value << endl;
value = -1;
}
Int(const Int& it) :value(it.value)
{
cout << this << " copy create Int " << &it << endl;
}
Int& operator=(const Int& it)
{
if (this != &it)
{
this->value = it.value;
}
cout << this << " operator= " << &it << endl;
return *this;
}
void Print() const
{
cout << "this: " << this << " value: " << value << endl;
}
Int(Int&& it) :value(it.value)
{
cout << this << " move create Int " << &it << endl;
it.value = -1;
}
Int& operator=(Int&& it)
{
if (this != &it)
{
this->value = it.value;
it.value = -1;
}
cout << this << " move operator=() " << &it << endl;
return *this;
}
int& Value() { return value; }
const int& Value() const { return value; }
void operator()(int x) const
{
cout << "Int: " << x << " this:" << this << " " << value << endl;
}
};
void func(Int x)
{
cout << "func(int x): " << endl;
x.Print();
}
struct Foo {
void operator()(Int x) const {
cout<<"Foo value of x is "<<x.Value()<<endl;
}
};
struct Bar {
using pFun = void(*)(Int);
static void func(Int x) {
cout<<"Static value of x is "<<x.Value()<<endl;
}
operator pFun() const {
return func;
}
};
class Test {
public:
int a_;
double dx;
char ch;
int val;
public:
Test(int x=0) : a_(x),dx(0.0),ch('a'),val(x+10) {}
void m_func(int x) {
cout<<"x is "<<x<<endl;
}
};
1.仿函数基础
仿函数是重载了()操作符的类或结构体对象,使得可以像调用函数一样调用该对象。
2.函数指针类型定义
这定义了一个名为 pFun 的函数指针类型,指向一个接受 int 参数并返回 void 的函数。
3.类静态成员函数特性
静态函数,没有所谓的this指针。静态函数属于类而非类的实例,不需要类的实例即可调用。 静态函数只能访问:同类的其他静态成员函数、同类的静态成员变量、作用域内的局部变量、全局变量(不推荐)。 特点:
- 不依赖对象状态:没有this指针,不能访问非静态成员
- 共享性:所有实例共享同一个静态函数
- 生命周期:随类存在,不随对象创建和销毁
- 访问限制:只能访问静态成员变量和函数
- 内存占用:不会为每个对象实例创建副本
4.对象隐式转换为函数指针
这个重载允许将 Bar 类型的对象隐式转换为 pFun 类型的func函数指针,即返回func函数的指针。这个重载就是将仿函数指定为具体函数的调用。
5.类成员变量指针
我们也可以这么理解,这个定义是创建一个专门服务于Test类的一个变量,这个变量为所有的Test类服务,也就是家族的管家。 在这里它起到的作用是,储存Test类中的整形成员相对于这个类的起始地址的偏移量,并让Test类对象可以通过它访问到对应的成员变量。 因为它是为类服务的,所以只要是这个类类型的对象都可以直接去使用它,因为它本事是为Test类服务的,Test实际对象可将它当作自己中的成员变量进行访问。 只能用实例对象名+.*访问,不可以用.去查看它的偏移量,因为p并不是类的成员,管家是服务test对象,但它仍是外人,不是test类的一家人。 也不可以缺少实例对象名,因为他只知道偏移量,不知道具体类对象的起始地址。
优势 它作为一个变量,可以储存相同类型不同成员变量的偏移量,并让Test类对象通过它访问到对应的成员变量,具有强大的灵活性。
使用场景
- 需要运行时动态访问成员
- 实现序列化 / 反序列化框架
- 构建反射系统
- 实现数据驱动的配置系统
- 需要高性能的反射机制
- 回调机制中需要访问特定成员
6.成员变量指针与普通指针区别
指针指向类模板里面的成员,函数,不是指向已经存在的类的对象中的成员变量。
7.类成员函数指针
我们从下面的例子就知道,方法的实现有很多种,例如普通函数,成员函数,lambda表达式,函数对象,函数指针等等。 function的作用就是一个通用的多态函数包装器,它提供了强大的函数封装能力。
8.多种调用语法辨析
pfunc(x) C++写法;(*pfunc)(x) C的写法,二者实现的效果是一样的。 函数对象调用,等价于foo.operator()(x)。 函数对象调用,等价于bar.operator()(x)。
std::bind 默认值传递,会产生对象副本
cpp
#if 0
int main() {
Int a(10);
cout<<"\n----------------------------------------------\n";
auto fr1 = std::bind(func, a);
cout<<"\n----------------------------------------------\n";
fr1();
cout<<"\n----------------------------------------------\n";
return 0;
}
#endif
运行日志
cpp
create Int(int) 012FF9FC 10
----------------------------------------------
012FF9F0 copy create Int 012FF9FC //创建绑定器中要拷贝一份
----------------------------------------------
012FF51C copy create Int 012FF9F0 //调用绑定器,func函数传参也还要拷贝一份
func(int x) :
this : 012FF51C value : 10
destroy Int : 012FF51C 10
----------------------------------------------
destroy Int : 012FF9F0 10
destroy Int : 012FF9FC 10
知识点
创建绑定器中要拷贝一份;调用绑定器,func函数传参也还要拷贝一份。
std::bind绑定类普通成员函数,传值与传地址对比
cpp
class Base {
private:
Int value;
public:
Base(Int x=0) :value(x) {}
void func() {
cout<<"Base func()"<<this<<endl;
value.Print();
value.Value()=100;
value.Print();
}
};
#if 0
int main() {
Base base(20);
std::function<void(void)> fr1 = std::bind(&Base::func, &base);//使用绑定器,传值参传的是副本,传地址,不会创建副本
/*在C++中使用std::bind绑定类成员函数时,第一个参数必须写成 & ClassName::成员函数的形式。
因为这里调用function的目的,就像那个家族管家一样,同类的所有对象都可以去直接使用这个公有接口直接调用func函数。
而我们的类刚开始只是一个模板,没有具体实例,就跟一张建筑图纸一样,不在前面加一个作用域,要么系统找不到这个函数,因为没有被创建出来,
要么就找到非类里面的同名函数,为了解决这个问题
我们要调用这个函数,就只能去记住这个函数距离该类对象的基地址的距离,也就是偏移量
和之前的例子:
int Test::* p = nullptr;
p=&Test::a_;
一模一样,只不过这里是一个函数指针,指向的是类成员函数,而不是成员变量。
第二个参数就是实际类对象成员,可以是右值也可以是左值,作用就是提供基准地址,这样起始地址加上偏移量就可以找到我们想调用的类的成员方法。
另外,由于偏移量也叫相对地址,所以要加&地址符。
*/
fr1();
//调用的是副本的func函数
cout<<"\n----------------------------------------------\n";
base.func();
//调用的是原对象的func函数
return 0;
}
#endif
不加取地址符运行日志
cpp
create Int(int) 010FF920 20
010FFA64 copy create Int 010FF920
destroy Int : 010FF920 20
010FF950 copy create Int 010FFA64
014236D8 move create Int 010FF950
destroy Int : 010FF950 - 1
Base func()014236D8
this : 014236D8 value : 20
this : 014236D8 value : 100
----------------------------------------------
Base func()010FFA64
this : 010FFA64 value : 20
this : 010FFA64 value : 100
destroy Int : 014236D8 100
destroy Int : 010FFA64 100
加取地址符运行日志
cpp
create Int(int) 001DF818 20
001DF964 copy create Int 001DF818
destroy Int : 001DF818 20
Base func()001DF964
this : 001DF964 value : 20
this : 001DF964 value : 100
----------------------------------------------
Base func()001DF964
this : 001DF964 value : 100
this : 001DF964 value : 100
destroy Int : 001DF964 100
知识点
使用绑定器,传值参传的是副本,传地址,不会创建副本。 在C++中使用std::bind绑定类成员函数时,第一个参数必须写成 & ClassName::成员函数的形式。 因为这里调用function的目的,就像那个家族管家一样,同类的所有对象都可以去直接使用这个公有接口直接调用func函数。 而我们的类刚开始只是一个模板,没有具体实例,就跟一张建筑图纸一样,不在前面加一个作用域,要么系统找不到这个函数,因为没有被创建出来,要么就找到非类里面的同名函数,为了解决这个问题。 我们要调用这个函数,就只能去记住这个函数距离该类对象的基地址的距离,也就是偏移量。 和之前的例子:int Test::* p = nullptr; p=&Test::a_; 一模一样,只不过这里是一个函数指针,指向的是类成员函数,而不是成员变量。 第二个参数就是实际类对象成员,可以是右值也可以是左值,作用就是提供基准地址,这样起始地址加上偏移量就可以找到我们想调用的类的成员方法。 另外,由于偏移量也叫相对地址,所以要加&地址符。 fr1()调用的是副本的func函数;base.func()调用的是原对象的func函数。
std::bind占位符placeholders使用
cpp
void funcc(int a, int b) {
cout<<"func(int a, int b)"<<a<<","<<b<<endl;
}
#if 0
int main() {
auto fr1 = std::bind(funcc,12,13);//使用绑定器,可以实现有参函数的无参调用
fr1();
auto fr2= std::bind(funcc,12,placeholders::_1);//这里的placeholders::1为占位符,
//占位符在bind绑定器中的参数位置,决定了与函数相对应位置的形式参数绑定。
//占位符后面的后缀,决定了使用第几个位置的实际参数传入到对应的形式参数。
//占位符最多有20个
fr2(13);
auto fr3 = std::bind(funcc,placeholders::_1,13);
fr3(23);
auto fr33 = std::bind(funcc,placeholders::_2,13);
fr33(11,23);
auto fr4 = std::bind(funcc,placeholders::_1,placeholders::_2);
fr4(23,24);
auto fr5 = std::bind(funcc,placeholders::_2,placeholders::_1);
fr5(23,24);
return 0;
}
#endif
知识点
使用绑定器,可以实现有参函数的无参调用。 这里的placeholders::_1为占位符。 占位符在bind绑定器中的参数位置,决定了与函数相对应位置的形式参数绑定。 占位符后面的后缀,决定了使用第几个位置的实际参数传入到对应的形式参数。 占位符最多有20个。
std::bind与std::function组合
cpp
#if 0
int main() {
// 正确使用std::bind
auto fr1 = std::bind(funcc,12,13);//使用绑定器,可以实现有参函数的无参调用
fr1(); // 调用bind函数
std::function<void(void)> fx;
fx = std::bind(funcc,12,13);
fx();//它的实际作用是将可调用对象与其参数绑定成仿函数
return 0;
}
#endif
知识点
使用绑定器,可以实现有参函数的无参调用。 它的实际作用是将可调用对象与其参数绑定成仿函数。
std::function回调封装类示例
cpp
class A {
std::function<void()> callback_;
public:
A(const std::function<void()>& f) : callback_(f) {}
void setCallback(const std::function<void()>& f) {
callback_ = f;
}
void notify() {
callback_();
}
};
知识点
无额外注释,用于演示std::function作为回调成员的封装写法。
std::function接收仿函数对象
cpp
#if 0
int main() {
Foo foo;
A a(foo);
a.notify();
//a.setCallback(func);
a.notify();
return 0;
}
#endif
知识点
无额外注释,演示仿函数传入std::function回调。
std::function赋值仿函数对象,拷贝与析构行为
cpp
#if 0
int main() {
std::function<void(int)> fr1;
Int x(10),y(20);
cout<<"---------------------------------------\n";
fr1 = x;//赋值的过程本质上会调用拷贝构造函数,生成一个副本,实际上我们调用的是副本的仿函数。
cout<<"---------------------------------------\n";
fr1(10);
cout<<"---------------------------------------\n";
fr1 = y;//当functional对象被重新赋值时,会先调用旧对象的析构函数,会将老旧的副本进行析构,再调用新对象的拷贝构造函数。
cout<<"---------------------------------------\n";
fr1(20);
cout<<"---------------------------------------\n";
return 0;
};
#endif
运行日志
cpp
create Int(int) 0099F718 10
create Int(int) 0099F70C 20
-------------------------------------- -
00D536CC copy create Int 0099F718
-------------------------------------- -
Int: 10 this : 00D536CC 10
-------------------------------------- -
00D53314 copy create Int 0099F70C
destroy Int : 00D536CC 10 ///原先副本被析构掉
-------------------------------------- -
Int : 20 this : 00D53314 20
-------------------------------------- -
destroy Int : 0099F70C 20
destroy Int : 0099F718 10
destroy Int : 00D53314 20
知识点
赋值的过程本质上会调用拷贝构造函数,生成一个副本,实际上我们调用的是副本的仿函数。 当functional对象被重新赋值时,会先调用旧对象的析构函数,会将老旧的副本进行析构,再调用新对象的拷贝构造函数。 原先副本被析构掉。
std::function支持的可调用对象类型
cpp
#if 0
int main() {
std::function<void(int)> fr1;
Foo foo;
fr1 = foo;//将函数对象foo赋值给fr1
//普通函数,成员函数,lambda表达式,函数对象,函数指针等等。都可以直接赋值给它
fr1(10);//调用方式统一
}
#endif
知识点
将函数对象foo赋值给fr1。 普通函数,成员函数,lambda表达式,函数对象,函数指针等等。都可以直接赋值给它。 调用方式统一。
std::function包装普通函数、静态成员函数
cpp
#if 0
int main() {
Int x(10);
cout << "\n---------------------------------------\n";
std::function<void(Int)> fr1 = func;
cout << "\n---------------------------------------\n";
fr1(x);//函数对象调用,等价于fr1.operator()(x)
cout << "\n---------------------------------------\n";
fr1 = Bar::func;
cout << "\n---------------------------------------\n";
fr1(x);
return 0;
}
#endif
知识点
函数对象调用,等价于fr1.operator()(x)。
std::function模板参数写法
cpp
#if 0
int main() {
std::function<void(int)> frl;//<>里面写的时函数去掉名字之后剩余的声明部分
//std::function<void(*)> fr2;函数指针不可以包装。
std::function<void* (int)> fr3;
std::function<void*(int*)> fr4;
}
#endif
知识点
<>里面写的是函数去掉名字之后剩余的声明部分。 函数指针不可以包装。