C++ 可调用实体 (详解 一站式)

目录

可调用实体

函数对象

函数指针

成员函数指针

空指针的使用(了解)


可调用实体

讲到调用这个词,我们首先能够想到普通函数函数指针,在学习了类与对象的基础知识后,还增加了成员函数,那么它们都被称为可调用实体。事实上,根据其他的一些不同的场景需求,C++还提供了一些可调用实体,它们都是通过运算符重载来实现的。

普通函数执行时,有一个特点就是无记忆性。一个普通函数执行完毕,它所在的函数栈空间就会被销毁,所以普通函数执行时的状态信息,是无法保存下来的,这就让它无法应用在那些需要对每次的执行状态信息进行维护的场景。大家知道,我们学习了类与对象以后,有了对象的存在,对象执行某些操作之后,只要对象没有销毁,其状态就是可以保留下来的。

函数对象

想让对象像一个函数一样被调用

class FunctionObject{
    //...
};

void test0(){
    FunctionObject fo;
    fo();  //让对象像一个函数一样被调用
}

上面的代码看起来很奇怪,如果我们从运算符的视角出发,就是函数调用运算符()要处理FunctionObject对象,只需要实现一个函数调用运算符重载函数即可。

函数调用运算符必须以成员函数的形式进行重载

class FunctionObject{
    void operator()(){
        cout << "void operator()()" << endl;
    }
};

void test0(){
    FunctionObject fo;
    fo();  //ok
}

在定义 "()" 运算符的语句中,第一对小括号总是空的,因为它代表着我们定义的运算符名称,第二对小括号就是函数参数列表了,它与普通函数的参数列表完全相同。对于其他能够重载的运算符而言,操作数个数都是固定的,但函数调用运算符不同,它的参数是根据需要来确定的, 并不固定

重载了函数调用运算符的类的对象称为函数对象,由于参数列表可以随意扩展 ,所以可以有很多重载形式(对应了普通函数的多种重载形式)

#include<iostream>
using namespace std;

class FunctionObject
{
public:
	//对函数调用运算符--()进行重载
	//第一个括号是和operator连在一起的作为函数名
	//第二个括号是参数列表,只不过此时定义时是一个无参的成员函数
	void operator()()
	{
		cout << "operator()函数一" << endl;
	 }

	//函数调用运算符重载比较特殊
	//其操作数个数可以为任意个数(this指针必须有)
	int operator()(int x)
	{
		cout << "operator()函数二" << endl;
		return x;
	}
	double operator()(double x, int y)
	{
		cout << "operator()函数三" << endl;
		return x + y;
	}
};
void func1() {
	cout << "hello" << endl;
}
int func2(int x)
{
	return x;
}
double func3(double x, int y)
{
	return x + y;
}

void test0()
{
	FunctionObject fo;
	//fo对象可以作为函数对象使用
	fo();
	fo.operator()();//本质

	cout << fo(5) << endl;
	cout << fo.operator()(5) << endl;

	cout << fo(3.5, 7) << endl;
	cout << fo.operator()(3.5, 7) << endl; 
}
int main()
{
	test0();
	return 0;
}

class FunctionObject{
public:
    void operator()(){
        cout << "FunctionObject operator()()" << endl;
        ++ _count;
    }

    int operator()(int x, int y){
        cout <<"operator()(int,int)" << endl;
        ++ _count;
        return x + y;
    }
    
    int _count = 0;//携带状态
};

void test0(){
    FunctionObject fo;
  
    cout << fo() << endl;
    cout << fo.operator()() << endl;//本质

    cout << fo(5,6) << endl;
    cout << fo.operator()(5,6) << endl;//本质

    cout << "fo._count:" << fo._count << endl;//记录这个函数对象被调用的次数
}

函数对象相比普通函数的优点:

可以携带状态(函数对象可以封装自己的数据成员、成员函数,具有更好的面向对象的特性)

如上,可以记录函数对象被调用的次数,而普通函数只能通过全局变量做到(全局变量不够安全)。

除此之外,函数对象作为STL的六大组件之一而存在,可以做很多定制化的行为。后面的章节中会学到。

下面为测试代码,可以自行测试

#include<iostream>
using namespace std;

class FunctionObject
{
public:
	//对函数调用运算符--()进行重载
	//第一个括号是和operator连在一起的作为函数名
	//第二个括号是参数列表,只不过此时定义时是一个无参的成员函数
	void operator()()
	{
		_cnt++;
		cout << "operator()函数一" << endl;
	 }

	//函数调用运算符重载比较特殊
	//其操作数个数可以为任意个数(this指针必须有)
	int operator()(int x)
	{
		_cnt++;
		cout << "operator()函数二" << endl;
		return x;
	}
	double operator()(double x, int y)
	{
		_cnt++;
		cout << "operator()函数三" << endl;
		return x + y;
	}
	//const 将this指针变为双重const定义的指针,指针指向不可变,
	//指针指向的值也不可以改变,就是对_cnt进行了限制
	int getcount() const
	{
		return _cnt;
	}
private:
	int _cnt = 0;
};
int cnt = 0;
void func1() {
	cnt++;
	cout << "hello" << endl;
}
int func2(int x)
{
	cnt++;
	return x;
}
double func3(double x, int y)
{
	cnt++;
	return x + y;
}

void test0()
{
	FunctionObject fo;
	//fo对象可以作为函数对象使用
	fo();
	fo.operator()();//本质

	cout << fo(5) << endl;
	cout << fo.operator()(5) << endl;

	cout << fo(3.5, 7) << endl;
	cout << fo.operator()(3.5, 7) << endl;

	//如果我想记录一下函数调用的次数,我可以怎么办呢?
	//1.对于普通函数我可以设置全局变量,但是这个全局变量我可以随时修改,不安全
	//2.那么对于函数对象我们就可以,在类中添加私有数据成员,在类外不可以访问和修改,但是我想
	//打印就要通过成员函数的形式进行获取
	
	//对于普通函数
	//只能通过全局变量
	//不够安全
	//func1();
	//func2(3);
	//func3(3.5, 5);
	不会对我进行限制可以修改,不够安全
	//cnt = 0;
	//cout << cnt << endl;


	//对于函数对象,想要记录调用的次数
	//可以通过数据成员的方式进行记录
	//比较安全

	cout << fo.getcount() << endl;
	//_cnt为私有成员无法直接访问,需要通过成员函数的形式进行获取
	//cout << fo._cnt << endl;
}
int main()
{
	test0();
	return 0;
}

函数指针

既然对象可以像一个函数一样去调用,那函数可不可以像一个对象一样去组织?

如果可以,那函数类型由什么决定呢,也就是说,如果把函数看作对象,如何从这些“对象”抽象出类来?

在C的阶段就学习过函数指针,定义函数指针时要明确使用这个指针指向一个什么类型的函数(返回类型、参数类型都要确定)

void print(int x){
    cout << "print:" << x << endl;
}

void display(int x){
    cout << "display:" << x << endl;
}

int main(void){
    //省略形式
    void (*p)(int) = print;
    p(4);
    p = display;
    p(9);
    
    //完整形式
    void (*p2)(int) = &print;
    (*p2)(4);
    p2 = &display;
    (*p2)(9);
}

定义函数指针p后,可以指向print函数,也可以再指向display函数,并通过函数指针调用函数(两种方式——完整/省略);

——那么其实可以抽象出一个函数指针类,这个类的对象就是这个特定类型的函数指针

p和p2可以抽象出一个函数指针类型void(*)(int) —— 逻辑类型,不能在代码中直接以这种形式写出

以前我们使用typedef可以定义类型别名,这段程序中函数指针p、p2的类型是void (*) (int),但是C++中是没有这个类的(我们可以这样理解,但是代码不能这么写)

可以使用typedef定义这样的一个新类型

可以理解为是给void ( * ) (int) 取类型别名为Function

typedef void(*Function)(int);

Function类的“对象”可以这样使用,这个类的“对象”都是特定类型的函数指针,只能指向一种函数(这种函数的类型在定义函数指针类时就决定了)

   Function f;
    f = print;
    f(19);
    f = display;
    f(27);

成员函数指针

函数指针的用法熟悉后,顺势思考一个问题:成员函数能否也使用这种形式?如果可以,应该怎样定义一个成员函数指针

比如有这样一个类FFF,包含两个成员函数

class FFF
{
public:
    void print(int x){
        cout << "FFF::print:" << x << endl;
    }

    void display(int x){
        cout << "FFF::display:" << x << endl;
    }
};

定义一个函数指针要明确指针指向的函数的返回类型、参数类型,那么定义一个成员函数指针还需要确定的是这个成员函数是哪个类的成员函数(类的作用域)

与普通函数指针不一样的是,成员函数指针的定义和使用都需要使用完整写法,不能使用省略写法,定义时要完整写出指针声明,使用时要完整写出解引用(解出成员函数后接受参数进行调用)。

另外,成员函数需要通过对象来调用,成员函数指针也需要通过对象来调用

void (FFF::*p)(int) = &FFF::print;
FFF ff;
(ff.*p)(4);

类比来写,也可以使用typedef来定义这种成员函数指针类,使用这个成员函数指针类的”对象“调用FFF类的成员函数print

这里有一个要求 —— 成员函数指针指向的成员函数需要是FFF类的公有函数

typedef void (FFF::*MemberFunction)(int); //定义成员函数类型MemberFunction

MemberFunction mf = &FFF::print; //定义成员函数指针
FFF fff;  
(fff.*mf)(15);//通过对象调用成员函数指针

此时就出现了一个新的运算符 ".*" —— 成员指针运算符的第一种形式。

FFF类对象还可以是一个堆上的对象

FFF * fp = new FFF();

(fp->*mf)(65);//通过指针调用成员函数指针

又引出了新的运算符 "->*" —— 成员指针运算符的第二种形式。

成员函数指针的意义:

  1. 回调函数:将成员函数指针作为参数传递给其他函数,使其他函数能够在特定条件下调用该成员函数;

  2. 事件处理:将成员函数指针存储事件处理程序中,以便在特定事件发生时调用相应的成员函数;

  3. 多态性:通过将成员函数指针存储在基类指针中,可以实现多态性,在运行时能够去调用相应的成员函数。

下面为测试代码可自行测试

#include <iostream>
using namespace std;
typedef void(*function)(int);

class FFF
{
public:
    void print(int x) {
        cout << "FFF::print:" << x << endl;
    }

    void display(int x) {
        cout << "FFF::display:" << x << endl;
    } 
    void func() {
        cout << "func()" << endl;
    }
    int func2(int x)
    {
        cout << "func2()" << endl;
    }
    static void show(int x)
    {
        cout << "show():" << x << endl;
    }
};
//要将这个写到类的下面
//对一类特定的成员函数指针给出新的类型名称
//menfunction 类型的变量就是这种类型的成员函数指针变量
typedef void(FFF ::* menfunction)(int);
void test()
{
    //这里为什么将成员函数设置为静态就可以使用
    // 因为静态成员函数可以通过类名直接调用
    //和普通函数一样只是找到地址就可以调用,不需要特定对象调用
    function f1 = FFF::show;
    f1(10);

    //普通成员函数不能指向一个类的非静态成员函数
    //f1 = FFF::display;//error

    //需要定义一个成员函数指针才能指向一个累的非静态成员指针
    //注意:
    //(1)在指针名前面也加上类作用域限定
    //(2)必须使用完整形式
    //定义成员函数指针时就已经确定了能够指针的成员函数
    // 返回类型 函数参数信息  类信息
    void (FFF::*f2)(int) = &FFF::print;

    //想要使用普通的成员函数,要通过对象来使用,因为他本身就是需要通过对象来使用的
    //所以创建一个对象
    FFF fff;
    //因为定义的时候就是使用的完整的形式,所以使用时也要使用完整的形式
    //  .*成员指针运算符之一
    (fff.*f2)(26);

    f2 = &FFF::display;
    (fff.*f2)(37);

    //下面两种情况类型不一样
    //f2 = &FFF::func;//error
    //f2 = &FFF::func2;//error
    //f2 = &DDD::func3;//error

    void (FFF:: * f3)(int);
    f3 = &FFF::display;
    (fff.*f3)(18);

    menfunction f4;
    f4 = &FFF::print;
    (fff.*f4)(63);

    // .和->都是成员访问运算符
    FFF* ft = new FFF();
    // 成员指针运算符之二
    //运算符名称中的指针说明f4是一个指针
    (ft->*f4)(74);
}
int main()
{
    test();
	return 0;
}

空指针的使用(了解)

接着上面的例子,我们来看一段比较奇怪的代码

fp = nullptr;
(fp->*mf)(34);

发现竟然是可以通过的并输出了正常的结果。难道空指针去调用成员函数指针没有问题吗?

事实上,空指针去调用成员函数也好、成员函数指针也好,只要不涉及到访问该类数据成员,都是可以的。

class Bar{
public:
    void test0(){ cout << "Bar::test0()" << endl; }
    void test1(int x){ cout << "Bar::test1(): " << x << endl; }
    void test2(){ cout << "Bar::test2(): " << _data << endl; }

    int _data = 10;
};

void test0(){
    Bar * fp = nullptr;
    fp->test0();//ok
    fp->test1(3);//ok
    fp->test2(); //error
}

结合内存图来分析

空指针没有指向有效的对象。对于不涉及数据成员的成员函数,不需要实际的对象上下文,因此就算是空指针也可以调用成功。对于涉及数据成员的成员函数,空指针无法提供有效的对象上下文,因此导致错误。

自己写代码别这么写,看到这样的代码知道为什么不报错就行了。

总结:

C++中普通函数、函数指针、成员函数、成员函数指针、函数对象,可以将它们概括为可调用实体。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值