第一次接触对象的序列化与反射,理解尚浅,有什么不对的虚心接受指正。
在项目中,之前做过一个小功能,在系统开启后,会弹出一个进度条,是为了从数据库中将系统需要的数据读出来放到到内存,然后每读一部分,进度条会实时增加。之后的一次机会同事说,这个进度条可以再进行优化,像这样每次加载进度条都从数据库中读取,如果数据库繁忙,进度条会很慢,之后就提到的对象的反射。比如在网络通信中,因为不允许传递对象,可以将类的对象,方法以及属性等通过字符串的形式传递过去(因为Tcp等传输传的都是字符串),然后在另一端解析字符串。像在Qt中,那些ui文件,其实打开都是XML文件的格式,将那些UI控件以XML文件的形式存储就叫对象的序列化,然后通过XML文件展示控件就叫对象的反射。
先抛出一个问题:
如何通过类的名称字符串来生成类的对象。比如有一个类Class A,那么如何通过类名称字符串”A”来生成类的对象呢?
C++是不支持通过类名称字符串”XX”来生成对象的,也就是说我们可以使用XX* object =new XX;
来生成对象,但是不能通过
XX* object=new "XX";
来生成对象。
那该如何解决这个问题?我们就可以通过反射来解决这个问题。
1、什么是反射
官方给出的定义:反射是程序可以访问、检测和修改它本身状态或行为的一种能力。
定义说的有点抽象,我的理解就是程序在运行的过程中,可以通过类名称创建对象,并获取类中申明的成员变量和方法。
2、设计思路
(1)为需要反射的类中定义一个创建该类对象的一个回调函数;
(2)设计一个工厂类,类中有一个std::map,用于保存类名和创建实例的回调函数。通过类工厂来动态创建类对象;
(3)程序开始运行时,将回调函数存入std::map里面,类名做为map的key值;
流程如下:
3、具体实现
(1)定义一个函数指针类型,用于指向创建类实例的回调函数。
typedef void* (*PTRCreateObject)(void);
(2)定义和实现一个工厂类,用于保存保存类名和创建类实例的回调函数。工厂类的作用仅仅是用来保存类名与创建类实例的回调函数,所以程序的整个证明周期内无需多个工厂类的实例,所以这里采用单例模式来涉及工厂类。
//工厂类的定义
class ClassFactory
{
private:
map m_classMap ;
ClassFactory(){}; //构造函数私有化
public:
void* getClassByName(string className);
void registClass(string name, PTRCreateObject method) ;
static ClassFactory& getInstance() ;
};
//工厂类的实现
//获取工厂类的单个实例对象
ClassFactory& ClassFactory::getInstance()
{
static ClassFactory sLo_factory;
return sLo_factory ;
}
//通过类名称字符串获取类的实例
void* ClassFactory::getClassByName(string className)
{
map::const_iterator iter;
iter = m_classMap.find(className) ;
if ( iter == m_classMap.end() )
{
return NULL ;
}
else
{
return iter->second() ;
}
}
//将给定的类名称字符串和对应的创建类对象的函数保存到map中
void ClassFactory::registClass(string name, PTRCreateObject method)
{
m_classMap.insert(pair(name, method)) ;
}
(3)比较重要的一步,将定义的类注册到工厂类中。也就是说将类名的字符串和创建类的实例的回调函数保存到工厂类的map中。这里我们又需要完成两个工作,第一个是定义一个创建类实例的回调函数,第二个就是将类名称字符串和我们定义的回调函数保存到工厂类的map中。假设我们定义了一个TestClassA
class TestClassA
{
public:
void m_print()
{
cout<<"hello TestClassA"<
到此完了第一个工作,定义了一个创建类实例的回调函数。下面思考一下如何将这个回调函数和对应的类名称字符串保存到工厂类的map中。提供一个想法是创建一个全局变量,在创建这个全局变量时,调用的构造函数内将回调函数和对应的类名的字符串保存到工厂类的map中。在这里,这个全局变量的类型我们定义为RegisterAction
//注册动作类
class RegisterAction
{
public:
RegisterAction(string className,PTRCreateObject ptrCreateFn)
{
ClassFactory::getInstance().registClass(className,ptrCreateFn);
}
};
有个这个注册动作类,在每个类定义完成之后,就创建一个全局的注册动作类的对象,通过注册动作类的构造函数将我们定义的类名和回调函数注册到工厂类的map中。可以在程序的任何一个源文件中创建注册动作类的对象,但是在这里,放在回调函数后面创建。后面你就知道为什么这么做了。创建一个注册动作类的对象如下:
RegisterAction g_creatorRegisterTestClassA("TestClassA",(PTRCreateObject)createObjTestClassA);
到这里,就完成将类名称和创建类实例的回调函数注册到工厂类的map。下面再以另外一个类TestClassB为例,重温一下上面的步骤:
class TestClassB
{
public:
void m_print()
{
cout<<"hello TestClassB"<
现在应该发现,如果再定义一个类C、类D等等,需要写大量的相似度极高的代码。如何让代码变得简洁,提高效率呢。这时想到了宏。其实,包括回调函数的定义和注册动作的类的变量的定义,每个类的代码除了类名外其它都是一模一样的,那么就可以用下面的宏来替代这些重复代码。
#define REGISTER(className) \
className* objectCreator##className()
{ \
return new className; \
} \
RegisterAction g_creatorRegister##className( \
#className,(PTRCreateObject)objectCreator##className)
有了上面的宏,就可以在每个类后写一个REGISTER(ClassName)
就完成了注册的功能。
4、测试
这里已经完成了C++反射的部分功能(为什么是部分功能,后面再另外说明),测试一下,是否解决了上面我们提到的问题:如何通过类的名称字符串来生成类的对象。测试代码如下:
#include
结果:
通过类名的字符串创建类的实例,其实需要用到类名进行强制类型转换,既然有了类名,为什么还要花费心思实现反射的功能,直接用类名创建实例不就行了吗?
其实,上面实现的反射只是解决了一开始抛出的问题。那么在实际的项目中,假设有一种场景:就是定义好了基类,给客户给继承,但是我不知道客户继承了基类之后的类名。我们可以通过配置文件说明客户实现的具体类名称,这样我们就可以通过类名称字符串来创建客户自定义类的实例了。
(5)其他注册方法
上面是通过实现C++的反射来通过类名的字符串创建类的实例。在对需要反射的类进行注册的时候,用到了一个注册动作类的全局变量,来达到注册的功能。
通过全局对象的构造函数将类的创建实例的函数注册到工厂类中,其实是利用了全局对象的初始化执行的构造函数是在程序进入main函数之前执行的,这个问题就可以抽象为C/C++中如何在main()函数之前执行一条语句。
主要有以下几种方法:
(1)全局变量的构造函数
就是上面说的的通过全局对象的构造函数是实现在进入main函数之前执行的。很明显的副作用就是定义了一个不从使用的全局变量。在项目中全局这种东西大家都很不喜欢吧。
(2)全局变量的赋值函数。
跟上面的方法有异曲同工之妙,但也同样有着上面的副作用
#include
using namespace std;
int foo(void);
int i=foo();
int foo(void)
{
cout<<"before main"<
(3)使用GCC的话,可以通过attribute关键字声明constructor和destructor分别规定函数在main函数之前执行和之后执行。
#include
__attribute((constructor)) void before_main()
{
printf("%s/n",__FUNCTION__);
}
__attribute((destructor)) void after_main()
{
printf("%s/n",__FUNCTION__);
}
int main( int argc, char ** argv )
{
printf("%s/n",__FUNCTION__);
return 0;
}
(4)指定入口点,入口点中调用原来的入口点
在使用gcc编译C程序时,我们可以使用linker指定入口,使用编译选项-e指明程序入口函数。
#include
int main(int argc, char **argv)
{
printf("main\n");
return 0;
}
int xiao(int argc, char **argv)
{
printf("xiao\n");
return main(argc, argv);
}
编译语句可以为:gcc -e xiao test.c
上面是往上提出的方法,但是在测试的时候,运行到main函数中,总是会出现段错误。C++程序,使用g++如法炮制,编译可以通过,但也是执行到main函数时却是中抛出Segmentation fault (core dumped)。有兴趣的读者可以尝试一下,编译的时候记得给新的入口函数添加extern “C”说明,以防g++编译时改变了函数签名。
(5)可以用main调用main实现在main前执行一段代码,如下:
#include
#include
int main(int argc, char **argv)
{
static _Bool firstTime = true;
if(firstTime)
{
firstTime = false;
printf("BEFORE MAIN\n");
return main(argc, argv);
}
printf("main\n");
return 0;
}
6、总结
先解释一下上文提出的一个问题,为什么只是完成了C++反射的部分功能,因为在上面并没有完整的实现C++的反射机制,只能实现了反射机制中的一个功能模块而已,即通过类名称字符串创建类的实例。除此之外,反射机制所能实现的功能还有通过类名称字符串获取类中属性和方法,修改属性和方法的访问权限等。
为什么需要反射机制。由于在 Java 和.NET 的成功应用,反射技术以其明确分离描述系统自身结构、行为的信息与系统所处理的信息,建立可动态操纵的因果关联以动态调整系统行为的良好特征,已经从理论和技术研究走向实用化,使得动态获取和调整系统行为具备了坚实的基础。当需要编写扩展性较强的代码、处理在程序设计时并不确定的对象时,反射机制会展示其威力,这样的场合主要有:
(1)序列化(Serialization)和数据绑定(Data Binding)。
(2)远程方法调用(Remote Method Invocation RMI)。
(3)对象/关系数据映射(O/R Mapping)。
当前许多流行的框架和工具,例如 Castor(基于 Java 的数据绑定工具)、Hibernate(基于 Java 的对象/关系映射框架)等,其核心都使用了反射机制来动态获得类型信息。因此,能够动态获取并操纵类型信息,已经成为现代软件的标志之一。
下面是本文用到的完整代码,均写在一个源文件中,可以根据实际应用,将不同功能的代码写在不同的文件中。也可以在此基础上,进行功能扩充和改良。
#include