并行Html解析算法的分析

并行Html解析作为一个研究性课题来说,十分具有挑战性。因为html特有的上下文关联特性,使得它很难被一般的自动机解析,即使是支持大部分编程语言的下推自动机,也很难描述。

本文,就从html的特点分析入手,解释html语法的特点,和将其并行化的一些思考。

Hello Ubuntu 'Xenial Xerus'

今天,Canonical发布了Ubuntu 16.04 LTS ‘Xenial Xerus’,作为一个长期支持版,确实很让人期待。

Ubuntu 16.04提供了很多激动人心的新功能。

例如新的内核升级到Linux4.4,大量的软件版本更新,而且Ubuntu 16.04将会是最后一个支持32位架构的Ubuntu系统,日后将会是64位的时代。

LLVM函数的调用时声明插入

如果调用一个未声明的函数,我们知道肯定是不正确的,但符号表中,可能预先存有该函数的 FunctionType,这时即使未扫描到该函数,我们也可以用Module中的getOrInsertFunction方法,获取或插入一个函数。

Constant Module::getOrInsertFunction (
StringRef Name,
FunctionType
T,
AttributeSet AttributeList
)

其行为是这样的:

  1. 如果不存在,创建一个原型
  2. 存在,但是一个static的局部函数,那么创建一个新的全局函数替换之
  3. 存在,而且类型正确,返回当前函数
  4. 存在,类型不匹配,那么会在外层包一个constantexpr cast的语句,转换到正确的类型上

是不是很方便呢?
这样应该可以减少一次函数的声明遍历。

C和C++的面向对象专题(2)——C语言也能实现面向对象

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

二、C语言也能实现面向对象

今天要为大家介绍C语言的面向对象设计方法,正如题记上面所说,面向对象是一种思想,而并非是一种语言,我们将会介绍C语言实现的面向对象开发方式。

简单实用的C语言面向对象设计思路

众所周知,C++中的面向对象十分方便,但在C中,并没有类,但我们可以通过C的结构体来实现,然后,手动将this指针传入
目前这个方法,应该是C语言设计中,简便易用的方式,而且能较好的体现面向对象的设计思路,然而遗憾的是,没有继承和多态。

例如,我们这样一个C++类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class test {
public:
test(char* str, int k) {
this->str = str;
this->k = k;
}
void ShowMessage() {
for (int i = 0; i < k; ++i)
printf("%s\n", str);
}
private:
int k;
char* str;
};

那么我们可以这样转换为一个C类

1
2
3
4
5
6
7
8
9
/* test.h */

typedef struct _test {
int k;
char* str;
} test;

test* TestCreate();
void ShowMessage(test*);
1
2
3
4
5
6
7
8
9
10
11
12
/* test.c */

test* TestCreate() {
return (test*) malloc(sizeof(test));
}

void ShowMessage(test* t) {
int i;
for (i = 0; i < t->k; ++i) {
printf("%s\n", t->str);
}
}

其实思路也很清晰,思路简单易懂,实现也很清新明快,在各类C工程中使用极为广泛。

复杂的基于GObject的面向对象程序设计

如果你希望学习C语言的GUI程序设计,那么,必定要学习的就是GObject的类实现方式。
GObject相当于从C层面上模拟了一个C++的类对象模型,实现当然相对复杂的多。

下面我们来实际看一下一个GTK的窗口类,这是GTK+-3.0的一段样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* appwin.h */
#ifndef APPWIN_H
#define APPWIN_H

#include "app.h"
#include <gtk/gtk.h>

/* 该类的类型定义以及类型转换宏 */
#define APP_WINDOW_TYPE (app_window_get_type())
#define APP_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), APP_WINDOW_TYPE, AppWindow))

/* 该类分成两部分,一部分是成员,一部分是类本身 */
typedef struct _AppWindow AppWindow;
typedef struct _AppWindowClass AppWindowClass;

GType app_window_get_type (void);
AppWindow* app_window_new (App *app);
void app_window_open (AppWindow *win, GFile *file);

#endif // APPWIN_H

而其真实的定义是在.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

struct _AppWindow
{
GtkApplicationWindow parent;
};

struct _AppWindowClass
{
GtkApplicationWindowClass parent_class;
};


typedef struct _AppWindowPrivate AppWindowPrivate;

struct _AppWindowPrivate
{
GSettings *settings;
GtkWidget *stack;
GtkWidget *search;
GtkWidget *searchbar;
GtkWidget *searchentry;
GtkWidget *gears;
GtkWidget *sidebar;
GtkWidget *words;
GtkWidget *lines;
GtkWidget *lines_label;
};

G_DEFINE_TYPE_WITH_PRIVATE(AppWindow, app_window, GTK_TYPE_APPLICATION_WINDOW);

/* 后面有具体的实现方法,这里就不一一列举 */

我们发现,这种定义方式比C++中的其实更有优势,封装的更加彻底。为何这样说呢?首先,我们的声明文件十分的简洁,如果公开方法不修改的话,那么将其余内容如何改动,都不会影响我们的外部接口。

其次,由于需要显示的向GObject注册,那么动态进行类注册就成为可能,这样的设计优势表现在哪里呢?多语言的互通性就很好了,因为很多动态语言,是支持类的动态加载以及反射加载的。

另外,vala语言就是基于GObject类型的,他是一门新兴的编译时语言,但其也有很多动态语言的特性,用其开发gtk程序,比C具有明显的优势。

C和C++的面向对象专题(3)——C++中的不优雅特性

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

三、C++中的不优雅特性

今天来说一说C++中不优雅的一些问题,C++虽然是面向对象的设计语言,但也有很多缺陷和弊病,我们将会讨论如何通过良好的设计解决这些问题。

C++编译缓慢

C++编译慢已经成为了业界共识,一个大型C++项目甚至要用专用的服务器编译好久才能完成,Java和.net作为大型开发平台, 却也没发现编译如此缓慢的问题,那么究竟是什么,导致了C++编译难的问题呢?

模板的纠结

C++中模板有个很神奇的问题,就是实现和声明都必须被使用者引用,这段模板代码才有效,也就是说,模板是在编译时展开的代码生成机制。

我们不妨做个实验,这是类的声明:

1
2
3
4
5
6
7
8
9
10
template<class T>
class CObject
{
public:
CObject(T k) {obj = k;}
~CObject() {}
T getObj();
private:
T obj;
};

下面是类的实现:

1
2
3
4
5
6
#include "CObject.h"

template<class T>
T CObject<T>::getObj(){
return this->obj;
}

主函数中调用:

1
2
3
4
5
6
7
8
9
10
11
#include <cstdio>
#include "CObject.h"

using namespace std;

int main(){
CObject<int> Obj(10);
int k = Obj.getObj();
printf("%d\n", k);
return 0;
}

一切看起来是那么的顺利,但是!我的电脑给我显示如下错误信息:

1
2
3
4
5
6
7
8
9
10
Scanning dependencies of target template_test
[ 50%] Building CXX object CMakeFiles/template_test.dir/src/CObject.cpp.o
[100%] Building CXX object CMakeFiles/template_test.dir/src/main.cpp.o
Linking CXX executable template_test
CMakeFiles/template_test.dir/src/main.cpp.o:在函数‘main’中:
main.cpp:(.text+0x22):对‘CObject<int>::getObj()’未定义的引用
collect2: error: ld returned 1 exit status
make[2]: *** [template_test] 错误 1
make[1]: *** [CMakeFiles/template_test.dir/all] 错误 2
make: *** [all] 错误 2

链接器告诉我,我们找不到一个叫做‘CObject::getObj()’的函数,恩?为何,我们不是将类实现链接进来了么?

如果你这样想就错了,上网查找解决方案,得到的回复居然是这样:
#include "CObject.h" => #include "CObject.cpp"

omg,那我还不如把两个文件写成一个hpp来的方便呢,其实C++也是推荐你这样做的,理由就是——模板是编译时,在用到的时候进行代码展开得到的
如果不这样做,链接器是不会找到对应的代码的。

那么也找到了很多大型工程如boost库,为何编译缓慢的直接原因,大量的模板展开消耗了巨大的资源,而且模板展开是很不利于代码复用的,同样的算法,换一种类型,必须全部编译,生成新的代码,并且这类模板生成的代码,不能提前编译成二进制库,这样的结果就是,项目哪里改动一点,好多文件重复编译,造成编译十分缓慢。

封装的问题

C++的类并没有很好的将代码封起来,这和上次讲到的GObject对比可以发现,C++的私有变量是一同放置在类的声明中,而我们知道,一个类的声明,是会被很多其他类引用的。
那么,思考我们的C++编译过程,很多类都引用了一个.h文件,那么这个.h文件一旦发生更改,那么所有引用这个文件的cpp文件都将被触发重复编译,而我们在实现一个类时,对类的成员函数小修小补是很平常的,但由于封装的不彻底,那么我们的项目又将被反复编译,带来编译的速度缓慢。

而且,如果是库的话,那么私有成员的更新甚至还会影响用户使用,非常麻烦。
例如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public:
Test();
~Test();

void Show();

private:
std::string message;
int pointer;
void formatMessage(std::string&);
};

很明显,一般的C++类,私有成员都会比公开成员多,那么私有成员修改一点,哪怕只是一不小心多了个空格,都会带来这个文件的更新,触发makefile的重编译,带来了低效率。

缺乏反射机制

最新的C++11,引入了众多的新特性,包括好用的auto关键字以及模板元编程特性等,但这些,还是不能弥补反射机制缺失带来的影响。反射是对象串行化、GUI界面事件响应和根据数据动态调用代码等技术的核心,缺乏反射机制,会使得C++很多地方十分的不便。

很多大型软件,如firefox,在实现中,往往搭建了反射框架,供系统使用。但由于C++本身语法的问题,缺乏反射依旧会使得类书写变得困难。

跨平台困难

C++的跨平台性真的不好,甚至很多编译器上都会出现匪夷所思的问题,例如在不同平台上,基本类型的大小会随CPU字长而变化,如果有跨平台需求的软件,最好使用跨平台定义的类型。
C++的结构体中数据往往有内存对齐的问题,有些编译器还能通过编译器指令对其设置,这些问题最好还是能避开就避开。

跨平台时,还应小心异常处理的代码,因为有些版本的C++编译器对抛出的异常规格并不很遵守规范。
另外,不同平台的宽字符集也是大问题,往往并不能轻松统一,另外MinGW里貌似就没有宽字符- -

C和C++的面向对象专题(4)——解决封装,避免接口

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

四、解决封装,避免接口

恩,今天我们来讨论,如何通过设计,解决C++中的不优雅特性,改进项目的结构,改善编译速度。

上次我们提到,如果一个类的封装不好,容易导致种种不便,那么如何设计能够避免这种现象呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
class test {
public:
void print() {
printf("Hello\n");
}

void print2() {
printf("K : %d\n", k);
}

private:
int k;
};

简要的改进,将函数的实现移动到类cpp实现文件中

最简单的想法就是将实现和声明分开,这也是C++提倡的,这样虽然文件会增多,但编译速度和代码的清晰度会提升。

1
2
3
4
5
6
7
8
/* test.h */
class test {
public:
void print();
void print2();
private:
int k;
};
1
2
3
4
5
6
7
8
9
/* test.cpp */
#include "test.h"
void test::print() {
printf("Hello\n");
}

void test::print2() {
printf("K : %d\n", k);
}

很明显的,这样我们改动cpp文件中,.h文件不会受到影响,但假若我的private方法增加了,那么还是需要改动.h文件,进而会影响所有引用我的部分,为了避免这种情况出现,有什么好设计方法么?

使用接口降低代码耦合

一种标准的设计模式是使用接口,这在很多库的设计时也被经常采用,核心思想是通过多态调用的方式,避免内部方法的暴露。

接口一般就是C++的多态类:

1
2
3
4
5
6
7
8
/* Itest.h */
class Itest {
public:
virtual void print() = 0;
virtual void print2() = 0;
};

extern ITest* createItest(); // 类似工厂的方式为你构建类
1
2
3
4
5
6
/* Itest.cpp */
#include "test.h"

ITest* createItest() {
return new test();
}

让test从这个接口继承出来:

1
2
3
4
5
6
7
8
/* test.h */
class test : public Itest {
public:
virtual void print();
virtual void print2();
private:
int k;
};

这样的好处当然十分明显了,将类转成接口的形式,就能方便的修改下面的实现类,无论实现类如何改动,都在模块范围内,接口不变。
但这样做的坏处也很明显,如果C++大量使用这样的方式实现内部封装,那么很多情况下效率比较低,而且代码复杂度就上来了,需要添加很多的接口类。

轻便的成员类封装

下面介绍一种简单的方式来实现类封装性的提升,首先还是看这个test类,我们将其提示为test2:

1
2
3
4
5
6
7
8
/* test2.h */
class test2 {
public:
void print();
void print2();
private:
int k;
};

这里的k实际上并不需要写在这里,我们需要的是将private的部分整体的封装成一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* test2.h */
class test2_private;

class test2 {
public:
test2();
test2(int);
~test2();

void print();
void print2();

protected:
test2_private* that;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* test2.cpp */
#include "test2.h"

class test2_private {
public:
int k;
};

test2::test2() {
that = new test2_private();
that->k = 0;
}


test2::test2(int k) {
that = new test2_private();
that->k = k;
}

test2::~test2() {
delete that;
}

这时我们发现,这种封装可以很有效的解决类的接口不便的问题,而由于只使用了类指针,所以我们并不需要前向声明这个私有类,于是这个类可以方便的被修改,从而避免了接口和多态调用的问题。

这种设计还有一个用途,假若你有另外的代码生成器生成的代码,需要和已有的类嵌入使用,那么推荐使用这种方式,Qt中就是这样做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();

private:
Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
}

MainWindow::~MainWindow()
{
delete ui;
}

我们发现这里有一个神奇的代码

1
2
3
namespace Ui {
class MainWindow;
}

其实这只是另外一个类,和本类并不同名,Ui::MainWindow是qt设计器帮忙生成的类,用来标注UI界面生成的一些代码,为了让代码很好的和我们自己的类统一起来,他们用了这种方式。

C和C++的面向对象专题(5)——合理使用模板,避免代码冗余

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

五、合理使用模板,避免代码冗余

下面我们来讨论一下,如何解决模板的不易封装的问题。

我们提供这样一种思路,对于链表一类的通用类型,我们尽量采取强制类型转换的方式,尽量避免模板的滥用。

同样,我们应该避免对结构体的直接存储,尽量使用类似java的指针传递方式来传递对象。

我们首先来写一个单类型的list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef LIST_C_H
#define LIST_C_H

class list_c_private;
struct list_c_node;

class list_c {
public:
list_c();
~list_c();

void insert(void*);
int size();
void* get(int);

protected:
list_c_private* priv;
};

#endif // LIST_C_H

这里我们使用了上面讲到的封装方式,降低了类间的耦合度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "list_c.h"

class list_c_private
{
public:
int size;
list_c_node* head;
};

struct list_c_node
{
void* data;
list_c_node* next;

list_c_node() {
data = next = nullptr;
}
};


list_c::list_c() {
priv = new list_c_private();
priv->head = new list_c_node();
}

list_c::~list_c() {
delete priv;
}

void list_c::insert(void* data) {
list_c_node* p;
for (p = priv->head; p->next != nullptr; p = p->next) {}
p->next = new list_c_node();
p->next->data = data;
}

int list_c::size() {
return priv->size;
}

void* list_c::get(int k) {
int t; list_c_node* p;
for (p = priv->head->next, t = 0; p != nullptr && t != k ; p = p->next, ++t) {}
return p->data;
}

这是一个简单的链表,只是作为示例使用,写了插入和获取的两个方法。

而为了通用性支持,我们写一个模板,进行类型的强制转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef LIST
#define LIST

#include "list_c.h"

template<typename T>
class list {
public:
list() { clist = new list_c(); }
~list() { delete clist; }

void insert(T data) {
clist->insert((void*)data);
}

int size() { return clist->size(); }

T get(int k) {
return (T)clist->get(k);
}

protected:
list_c* clist;
};

#endif // LIST

这样,带来的好处有,首先能够将模板封装操作,其次,能够在封装类中,动态的调整内部实例。
对于一个传入的类型,你可以判断一下,是否适合当前的模板,如果不适合,可以在其中动态的报错。

最后是模板的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "list"
using namespace std;

int main(){
list<long> testlist;
testlist.insert(10);
testlist.insert(20);

long k = testlist.get(1);
printf("%d\n", k);
return 0;
}

C和C++的面向对象专题(6)——C++也能反射

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

六、C++也能反射

今天我们来探讨C++的反射问题,缺乏反射机制一直是C++的大问题,很多系统的设计时,需要根据外部资源文件的定义,动态的调用内部的函数和接口,如果没有反射,将很难将外部的数据,转换为内部的方法。

Java和.net的反射机制很容易实现,由于其动态语言的特性,在编译时就存储了大量的元数据,而在动态装载时,也是根据这些元数据载入的模块。由于C++缺乏这些信息,往往并不能很好的动态装载和链接。操作系统为了实现C和C++的动态装载功能,特意设计了动态链接库,将符号表保存在动态库中,运行时重定位代码,然后进行链接操作。而这是操作系统实现的,并不能很好的被用在用户工程中,所以我们有必要自己构建一套元数据集合,保存反射所需的内容。

反射的原理

反射的核心就是根据字符串名字,创建对应的类或者调用对应类的方法,为此,我们使用C++中的map

1
std::map<std::string, meta_class*>

meta_class 是保存一个类中的关键元数据用的类,可以支持反射构造,反射调用函数等功能。

meta_func 是保存一个方法的关键信息类,但由于方法有不定的参数和返回类型,我们使用模板的方式,将一个抽象存储的成员函数指针,转换为我们确定类型的成员函数指针,然后再去调用,达到动态调用的目的:

1
2
3
4
5
6
template <typename T, typename R, typename... Args>
R Call(T* that, Args... args) {
R (T::*mp)();
mp = *((R (T::**)())func_pointer);
return (that->*mp)(args...);
}

这里的代码十分混乱,如果你没学过C的函数指针的话,建议先去补习一下函数指针的定义和用法。

这里涉及到的是成员函数指针的传递,一会儿将会详细讲解如何传递任意一个函数指针。

反射类对象

首先,我们肯定要为类对象建立meta_class的模型,但每个meta_class,应该都能够构建本类的对象,为了实现这一特点,我们想到了模板:

1
2
3
4
5
6
7
template<typename T>
class MetaClass : public IMetaClass {
public:
virtual void* CreateObject() {
return new T();
}
};

为了让每个类都能有统一的创建方法,我们将使用IMetaClass接口进行多态调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class IMetaClass {
public:
virtual void* CreateObject() = 0;

template<typename T>
T* Create() {
return (T*) CreateObject();
}

void AddFunc(const std::string s, MetaFunc* c) { func_map[s] = c; }
MetaFunc* GetFunc(const std::string s) {
if (func_map.find(s) != func_map.end()) return func_map[s];
else return NULL;
}
private:
std::map<const std::string, MetaFunc*> func_map;
};

这里我们在接口类中编写了方法和成员函数,我觉得这是C++的优势,不像Java,为了安全性,而取消了这么简单好用的功能。

接口统一实现相同的类对象构建方式,避免了在实现类中反复编写的困难。

这样,我们只要在每个类的定义时,向我们的类注册器注册该MetaClass对象就可以了

但问题是,如何才能在类定义时编写代码呢?我们的C和C++可是只能在函数中调用代码,不能像脚本一样随时随地执行。

利用全局对象的构造函数执行代码

我们发现,C++有一个很有趣的特性,有些代码是可以在main函数执行前就执行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class test
{
public:
test() {
printf("%s\n","Hello Ctor!");
}
};

test show_message();

int main(){
printf("Main function run!\n");
return 0;
}

执行代码,哦?好像不大对,貌似我们的对象并没有启动,这有可能是被编译器优化掉了。。。= =!
控制台的显示:

1
2
sxf@sxf-PC:~/data/workspace/C++/OObyCpp/testCppRunCode$ ./main
Main function run!

稍加改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdio>
using namespace std;

class test
{
public:
test(const char* msg) {
printf("%s\n",msg);
}
};

test show_message("Hello Ctor!");

int main(){
printf("Main function run!\n");
return 0;
}

好的,我们发现构造函数运行在了main函数之前,也就是我们的类型定义的构造期。

1
2
3
sxf@sxf-PC:~/data/workspace/C++/OObyCpp/testCppRunCode$ ./main
Hello Ctor!
Main function run!

具体想详细了解C++的运行环境的细节,推荐看一本英文的开源书:
【How to Make a Computer Operating System】
这本书讲解如何利用C++开发了一个小型操作系统,而在C++运行时的导入过程中,就介绍了C++全局对象构造函数的运行过程,可以清楚的看出,C++的主函数在汇编层的调用流程是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
start:
push ebx

static_ctors_loop: ; 全局构造函数初始化
mov ebx, start_ctors
jmp .test
.body:
call [ebx]
add ebx,4
.test:
cmp ebx, end_ctors
jb .body

call kmain ; 调用主函数

static_dtors_loop: ; 全局对象的析构函数调用
mov ebx, start_dtors
jmp .test
.body:
call [ebx]
add ebx,4
.test:
cmp ebx, end_dtors
jb .body

于是我们可以这样编写一个类,专门用来注册一个类:

1
2
3
4
5
6
7
class reflector_class {
public:
reflector_class(const char* name, IMetaClass* meta_class) {
ClassRegister::Add(name, meta_class);
printf("define class: %s\n", name);
}
};

这个类的对象在构造时,会去调用ClassRegister类中的静态方法,向其中添加类名和类元数据

利用宏定义处理类的注册

我们希望每个类对象能够方便的找到自己的meta_class,最简单的方式就是将其添加为自己的成员,为何不用继承机制呢?首先继承较为复杂,并且父类也同样可能拥有meta_class, 我们希望每个类型都能方便的找到meta_class,那么可以建一条Reflectible宏,让大家写在class中

1
2
3
4
5
6
#ifndef Reflectible
#define Reflectible \
public:\
static IMetaClass* meta_class;\
private:
#endif

为了避免放置在最上面时,影响下面成员的private的默认定义,所以写成这样。

我们在写一个宏,让用户添加到类的cpp文件中,真正定义该meta_class对象:

1
2
3
4
5
#ifndef ReflectClass
#define ReflectClass(class_name) \
IMetaClass* class_name :: meta_class = new MetaClass< class_name >(); \
reflector_class class_name##reflector_class( #class_name , class_name::meta_class)
#endif

这里我们用到了两个宏技巧:

## 表示将两个符号连接在一起,由于词法分析中,宏是按照词的顺序分隔的,如果直接连接,往往会造成符号分析不清。
#something 表示将该内容展开成字符串的形式 => "something data",所以我们可以很方便的用这个宏将宏符号转为字符串传入到函数中。

反射成员函数

首先编写一个能调用成员函数的模板类,根据我们的反射原理,将一个函数指针转换为成员函数的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MetaFunc {
public:
MetaFunc(void* p) { func_pointer = p; }
void setFuncPointer(void* p) { func_pointer = p; }
template <typename T, typename R, typename... Args>
R Call(T* that, Args... args) {
R (T::*mp)();
mp = *((R (T::**)())func_pointer);
return (that->*mp)(args...);
}
private:
void* func_pointer;

};

我在这里使用了C++11的新特性,可变参数的模板,这样可以更方便的接受目标参数
如果我们直接对成员函数取地址,返回的是一个return_type (ClassName::)(args)这样的成员函数指针。
注意,成员函数指针不能直接被传递,成员函数指针由于包含了很多其他数据信息,并不能被被强制类型转换成void
,一个显而易见的例子是,成员函数指针,往往比较大,最大的指针甚至可以达到20byte。

为了能够传递函数指针,我们可以将成员函数指针赋值给一个该成员函数指针类型的对象,然后再对这个指针对象取地址

1
2
auto p = &test::print;
&p //这个地址可以被轻松传递

这个地址是一个指针的指针return_type (ClassName::**)(args)
于是就有了我们前面代码中,强制类型转换的方法

利用C语言的可变参数函数来定义函数

我们目前要将地址传递过来,但是我们并不知道每个类中有多少个函数,所以我们要使用C语言的宏,对可变参数进行处理。

下面将reflector_class进行一下修改,支持多个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class reflector_class {
public:
reflector_class(const char* name, IMetaClass* meta_class, ...) {
ClassRegister::Add(name, meta_class);
printf("define class: %s\n", name);

va_list ap;
va_start(ap, meta_class);
for (int arg = va_arg(ap, int); arg != -1; arg = va_arg(ap, int) )
{
std::string name(va_arg(ap, const char*));
void* p = va_arg(ap, void*);
if (arg == 0) {
printf("\tdefine func: %s\n", name.c_str());
MetaFunc* f = new MetaFunc(p);
meta_class->AddFunc(name, f);
} else {
printf("\tdefine prop: %s\n", name.c_str());
}
}
va_end(ap);
}
};

va_list ap; 可变参数列表

va_start(ap, meta_class);  这里的第二个参数,是当前函数的最后一个固定参数位置

void* p = va_arg(ap, void*); 可以用来获得一个固定类型的参数

使用过后释放资源:

va_end(ap);  

为了支持函数和属性两种声明,我们定义如下宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef DefReflectFunc
#define DefReflectFunc(class_name, func_name) \
auto func_name##_function_pointer = &class_name::func_name
#endif

#ifndef ReflectClass
#define ReflectClass(class_name) \
IMetaClass* class_name :: meta_class = new MetaClass< class_name >(); \
reflector_class class_name##reflector_class( #class_name , class_name::meta_class ,
#endif

#ifndef ReflectFunc
#define ReflectFunc(func_name) \
0, #func_name, _F(func_name##_function_pointer) ,
#endif

#ifndef ReflectProp
#define ReflectProp(prop_names) \
1, #prop_names, _F(prop_names) ,
#endif

#ifndef _F
#define _F(x) reinterpret_cast<void*>(&x)
#endif

#ifndef End
#define End -1 )
#endif

这样我们在cpp中使用这些宏时只需要:

1
2
3
4
DefReflectFunc(test2,print2);
ReflectClass(test2)
ReflectFunc(print2)
End;

类的全局注册难题

好的,关键的部分已经都清楚了,但目前我们还欠缺一个很重要的类,就是类的全局注册器。

1
2
3
4
5
6
7
8
class ClassRegister
{
public:
static std::map<const std::string, IMetaClass*> class_map;
static void Add(const std::string s, IMetaClass* k) {
class_map[s] = k;
}
}

但这个类有一个严重的漏洞,会造成程序崩溃,我们在接下来的章节中,将会介绍这个尴尬的问题的发生原因。

C和C++的面向对象专题(7)——单例模式解决静态成员对象和全局对象的构造顺序难题

本专栏文章列表

一、何为面向对象

二、C语言也能实现面向对象

三、C++中的不优雅特性

四、解决封装,避免接口

五、合理使用模板,避免代码冗余

六、C++也能反射

七、单例模式解决静态成员对象和全局对象的构造顺序难题

八、更为高级的预处理器PHP

九、Gtkmm的最佳实践

本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/

七、单例模式解决静态成员对象和全局对象的构造顺序难题

上回书说道,我们的程序有一个隐藏的漏洞,如果ClassRegister这个类所在的.o文件,如果在所有.o文件中是第一个被链接的的,那么就不会出问题。
这么说太抽象了,让我们画个图表

1
2
3
4
5
ClassRegister.o
--------------------
Meta.o
--------------------
Main.o

这样的结构,也就是链接顺序要这样指定

1
gcc -o main ClassRegister.o Meta.o Main.o

就不会出问题,但如果调换一下顺序就可能出问题。

思考一下原因,ClassRegister中的map对象,是一个全局对象,而我们注册类的时候,也使用了全局对象的构造函数,两个谁先执行呢?这个就不得而知了,C++并未说明两个谁先谁后,而一般链接器,都是从前往后链接代码,而构造函数的执行顺序,也往往和链接时的顺序有关。

但这样实现就很不好,我们的系统居然要靠链接器的顺序才能正确编译执行,太不可靠了,万一用户没注意到这一点,直接编译链接,就会出现未知的错误。

那么如何避免这种情况呢?

C++的单例模式

C++中有一种极好的设计模式很适合这种情况,那就是用单例,单例模式也很容易理解,核心就是推迟构造,如果没有使用时,就不会被构造,被用到时,对象就会构造,并且仅一次,最常见的写法就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CSingleton  
{
private:
CSingleton() //构造函数是私有的
{
}
static CSingleton *m_pInstance;
public:
static CSingleton * GetInstance()
{
if(m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};

当然,我们这里并没有考虑多线程,因为多线程时单例模式一般要加锁来保障不会多次构造引发冲突。

于是经过简要修改,就能用单例模式设计一个类注册器了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ClassRegister
{
private:
ClassRegister() { printf("register\n"); } //构造函数是私有的
std::map<const std::string, IMetaClass*> class_map;
public:
static ClassRegister * GetInstance() {
static ClassRegister instance; //局部静态变量
return &instance;
}
static void Add(const std::string s, IMetaClass* k) {
GetInstance()->class_map[s] = k;
}

static IMetaClass* Get(const std::string s) {
std::map<const std::string, IMetaClass*>& m = GetInstance()->class_map;
if (m.find(s) != m.end()) return m[s];
else return NULL;
}
};

这个类注册器简单实用,采用的设计方式和指针的模式稍有不同,这里用到了局部静态变量的概念。
局部静态变量能够改变对象的生存周期,这样就能很好的符合我们的要求。