07 类型标识:虚拟机支持对象自检的基础设施
你好,我是海纳。
前边两节课我们实现了控制流的功能。在实现控制流的时候,遇到使用基本数据类型的情况时,我们都采用了一些手段回避过去了。但是在进一步实现其他功能之前,我们必须把对象体系建立起来。这节课我们就从虚拟机中的基本数据类型入手,补全之前的功能,从而实现基本的对象体系。
Klass-Oop 二元结构
到目前为止,虚拟机里只有一个 HiObject 类,Integer 和 String 都是继承自这个类。我们回顾一下 Integer 的 equal 方法。
HiObject* HiInteger::equal(HiObject* x) {
if (_value == ((HiInteger*)x)->_value)
return Universe::HiTrue;
else
return Universe::HiFalse;
}
代码里的参数 x,如果它的类型是 Integer,equal 函数就可以正确执行。如果 x 的实际类型不是 Integer,这段代码就不能正常工作了。
我们需要一种机制,来判断某个 HiObject 对象的实际类型到底是什么。在编程语言虚拟机中,最常用的解决办法就是使用 Klass-Oop 二元结构。Klass 代表一种具体的类型,它是“类”这个概念的实际体现。例如,Integer 类在虚拟机里就有一个 IntegerKlass 与之对应,所有的整数都是 IntegerKlass 的实例。Oop是 Ordinary object pointer 的缩写,代表一个普通的对象。每一个对象都有自己的 Klass ,同一类对象是由同一个 Klass 实例化出来的。
类与类之间有继承关系,类里还会封装其他的属性和方法。这些信息都会保存在 Klass 结构中。
使用这种二元结构,还有一个原因是,我们不希望在普通对象里引入虚函数机制,因为虚函数会在对象的开头引入虚表指针,而虚表指针会影响对象的属性在对象中的偏移。
所以,我们就把类的方法定义和实现都放到 Klass 中了,而在 HiObject 里只需要调用相应的 Klass 中的函数。
我们先来定义 Klass 类。
class Klass {
private:
HiString* _name;
public:
Klass() {};
void set_name(HiString* x) { _name = x; }
HiString* name() { return _name; }
virtual void print(HiObject* obj) {};
virtual HiObject* greater (HiObject* x, HiObject* y) { return 0; }
virtual HiObject* less (HiObject* x, HiObject* y) { return 0; }
virtual HiObject* equal (HiObject* x, HiObject* y) { return 0; }
virtual HiObject* not_equal(HiObject* x, HiObject* y) { return 0; }
virtual HiObject* ge (HiObject* x, HiObject* y) { return 0; }
virtual HiObject* le (HiObject* x, HiObject* y) { return 0; }
virtual HiObject* add(HiObject* x, HiObject* y) { return 0; }
virtual HiObject* sub(HiObject* x, HiObject* y) { return 0; }
virtual HiObject* mul(HiObject* x, HiObject* y) { return 0; }
virtual HiObject* div(HiObject* x, HiObject* y) { return 0; }
virtual HiObject* mod(HiObject* x, HiObject* y) { return 0; }
};
目前的 Klass 类只包含一个属性,_name 代表了这个类的名称,它是一个字符串。
Klass 类中最重要的是上述代码中出现的 12 个虚函数。前面我们分析过,虚函数应该从对象中搬到 Klass 中去。在代码中,这 12 个函数都是空的,我们先这样做,等 HiObject 重构完以后,再来重点实现它们。
有了 Klass 定义,HiObject 的定义也要进行相应地修改,在 HiObject 类里增加一个属性,是一个指向 Klass 的指针,用于表示这个对象的类型。
由于我们已经把虚函数都搬到了 Klass 中,HiObject 中原来定义的函数就都不必是虚函数了。这样 HiObject 就变成了一个没有虚表的普通对象,更有利于我们清晰地把握它的内存布局。
接下来我们再把 HiObject 中的函数都实现为转向调用自己所对应的 Klass 中的函数。
HiObject 的定义变为下面这种形式:
// object/hiObject.hpp
class HiObject {
private:
Klass* _klass;
public:
Klass* klass() { assert(_klass != NULL); return _klass; }
void set_klass(Klass* x) { _klass = x; }
void print();
HiObject* add(HiObject* x);
HiObject* sub(HiObject* x);
HiObject* mul(HiObject* x);
HiObject* div(HiObject* x);
HiObject* mod(HiObject* x);
HiObject* greater (HiObject* x);
HiObject* less (HiObject* x);
HiObject* equal (HiObject* x);
HiObject* not_equal(HiObject* x);
HiObject* ge (HiObject* x);
HiObject* le (HiObject* x);
};
// object/hiObject.cpp
void HiObject::print() {
klass()->print(this);
}
HiObject* HiObject::greater(HiObject * rhs) {
return klass()->greater(this, rhs);
}
// other comparision methods.
// ...
HiObject* HiObject::add(HiObject * rhs) {
return klass()->add(this, rhs);
}
// other arithmatic methods.
// ...
到这里,Klass 和 Oop 的二元结构就搭建完成了。接下来,我们要分别审视各种类型的具体实现。先从整数开始。
重构整数
原来的系统里已经实现了整数。在 Klass-Oop 二元结构下,整数类也需要做相应的修改。首先是头文件中,HiInteger 类里的虚函数声明都不再需要了,HiInteger 的方法均继承自HiObject,由于 HiObject 中已经实现了所有函数,所以 HiInteger 类就变得很简洁了。
class HiInteger : public HiObject {
private:
int _value;
public:
HiInteger(int x);
int value() { return _value; }
};
接下来的步骤是实现 IntegerKlass,用于表示 Integer 类型。如下图所示,系统中的所有 Integer 对象,它的 Klass 指针(继承自HiObject)都应该指向同一个 Klass 对象,就是我们现在要实现的 IntegerKlass。可见,IntegerKlass 在整个系统中只需要有一个就够了。符合这种特点的对象,我们往往采取单例模式来实现。
定义IntegerKlass
由于 IntegerKlass 类应该实现成单例模式,所以,我们可以这样定义IntegerKlass:
// object/hiInteger.hpp
class IntegerKlass : public Klass {
private:
IntegerKlass();
static IntegerKlass* instance;
public:
static IntegerKlass* get_instance();
virtual void print(HiObject* obj);
virtual HiObject* greater (HiObject* x, HiObject* y);
virtual HiObject* less (HiObject* x, HiObject* y);
virtual HiObject* equal (HiObject* x, HiObject* y);
virtual HiObject* not_equal(HiObject* x, HiObject* y);
virtual HiObject* ge (HiObject* x, HiObject* y);
virtual HiObject* le (HiObject* x, HiObject* y);
virtual HiObject* add(HiObject* x, HiObject* y);
virtual HiObject* sub(HiObject* x, HiObject* y);
virtual HiObject* mul(HiObject* x, HiObject* y);
virtual HiObject* div(HiObject* x, HiObject* y);
virtual HiObject* mod(HiObject* x, HiObject* y);
};
// object/hiInteger.cpp
IntegerKlass* IntegerKlass::instance = NULL;
IntegerKlass::IntegerKlass() {
}
IntegerKlass* IntegerKlass::get_instance() {
if (instance == NULL)
instance = new IntegerKlass();
return instance;
}
HiInteger::HiInteger(int x) {
_value = x;
set_klass(IntegerKlass::get_instance());
}
在上述代码清单里,IntegerKlass 被实现成了一个单例类。Integer 对象在创建的时候会把自己的 klass 设置成 IntegerKlass。做完这一步,Integer 类的二元结构就改造完成了,最后只需要在 Klass 中实现相应的虚函数即可,这里我们以 equal 方法来举例说明。
HiObject* IntegerKlass::equal(HiObject* x, HiObject* y) {
if (x->klass() != y->klass())
return Universe::HiFalse;
HiInteger* ix = (HiInteger*) x;
HiInteger* iy = (HiInteger*) y;
assert(ix && (ix->klass() == (Klass *)this));
assert(iy && (iy->klass() == (Klass *)this));
if (ix->value() == iy->value())
return Universe::HiTrue;
else
return Universe::HiFalse;
}
在 equal 方法的开头部分,提前判断了 x 和 y 的类型,如果它们的类型不一样,那就可以直接返回 False 了。接下来,验证 x 和 y 都是整数类型,最后再取它们的 value 进行比较。在大于或者小于的判断中,如果类型不符合期望,标准的 Python 虚拟机的动作是抛出异常,但现在还没有实现异常机制,在那之前,我们就先使用 assert 让程序崩溃吧。
其他的比较运算和数学运算,这里我们就不一一列出了,你可以自己动手补齐,并与工程里的源代码进行比较。
在修改完整数类型以后,下面这个测试用例就可以正确地执行了。
因为这个例子只使用了整数,完全没有使用变量。如果程序中使用了变量的话,就不能运行了,因为变量所依赖的局部变量表依赖于字符串的比较操作。所以,接下来就要重构字符串。
重构字符串
与 IntegerKlass 相似,我们也使用单例模式来实现字符串的 StringKlass,并在字符串的构造函数里把 klass 属性设为 StringKlass。
// object/hiString.hpp
class StringKlass : public Klass {
private:
StringKlass() {}
static StringKlass* instance;
public:
static StringKlass* get_instance();
virtual HiObject* equal (HiObject* x, HiObject* y);
virtual void print(HiObject* obj);
};
// object/hiString.cpp
StringKlass* StringKlass::instance = NULL;
StringKlass* StringKlass::get_instance() {
if (instance == NULL)
instance = new StringKlass();
return instance;
}
HiString::HiString(const char* x) {
_length = strlen(x);
_value = new char[_length];
strcpy(_value, x);
set_klass(StringKlass::get_instance());
}
可以看到,StringKlass 的设计思路和 IntegerKlass 的设计思路是十分相似的。这里就不过多解释了。字符串类里比较重要的两个方法print 和 equal,是要重点实现的,我们先来实现 equal 方法。
HiObject* StringKlass::equal(HiObject* x, HiObject* y) {
if (x->klass() != y->klass())
return Universe::HiFalse;
HiString* sx = (HiString*) x;
HiString* sy = (HiString*) y;
assert(sx && sx->klass() == (Klass*)this);
assert(sy && sy->klass() == (Klass*)this);
if (sx->length() != sy->length())
return Universe::HiFalse;
for (int i = 0; i < sx->length(); i++) {
if (sx->value()[i] != sy->value()[i])
return Universe::HiFalse;
}
return Universe::HiTrue;
}
在 equal 的实现中,先比较 x 和 y 的类型,如果它们的类型不一样,就直接返回 False。接着,在验证过类型以后,再比较 x 和 y 的长度,如果长度不一样,就直接返回False。最后才是逐个字符进行比较,只要有一个字符不相等,就会返回 False。只有这些检查全部通过了,才会返回 True。
然后我们再实现 print 方法。
void StringKlass::print(HiObject* obj) {
HiString* str_obj = (HiString*) obj;
assert(str_obj && str_obj->klass() == (Klass*)this);
for (int i = 0; i < str_obj->length(); i++) {
printf("%c", str_obj->value()[i]);
}
}
print 方法里不能使用 %s
直接格式化输出,因为中间有可能会出现字符 '\0'
,所以这里只能逐个字符往外打印。当字符串也修改完了,局部变量表才能正常地起作用。你可以通过运行 Fibonacci 的例子来进行测试。
对象系统的重构就到此为止了。我们为对象系统打下了非常好的基础,在这套体系下,虚拟机可以实现自定义 class。但在那之前,还要再实现 Python 中最重要的一个机制:函数。所以从下一节课开始,我们就来实现函数机制。
总结
这节课我们重点重构了整个虚拟机的内建对象系统。最核心的目标是把内建对象改成不带虚函数的普通对象,这么做是为了保证每个对象的内存布局完全可控,而不是依赖编译器的实现。
把原来的 HiObject 修改成普通对象,那么它的多态性就要由相应的 Klass 对象来实现。所以 HiObject 的实现全部被转向调用 Klass 对象中的实现。
在这个基础上我们重新实现了整数和字符串,从而支持变量功能,最终可以运行 Fibonacci 的例子。虽然从功能上看,我们并没有实现新的特性,但是经过重构,对象系统已经打好了基础,可以继续实现更加强大的功能。
思考题
请你思考字符串和整数相加的例子,在Python、C++、Java、JavaScript等语言中各自会有怎样的结果?欢迎你把你对比后的内容分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!
知识拓展:单例模式
单例模式是设计模式的一种,可以保证一个类在全局只能生成一个对象。这个模式的关键技术点有两个,一是单例类要有一个私有的构造函数,从而保证对象不能被任意代码随时创建。例如:
class Singleton {
private:
Singleton() {}
public:
void say_hello();
};
void Singleton::say_hello() {
printf("hello world\n");
}
// this is wrong.
Singleton* singleton = new Singleton();
这样一来,我们就无法正常地通过访问构造函数来构造一个 Singleton 的实例了,杜绝了这个类被随意实例化的可能。可是这样就相当于这个类没有用了,我们还要找办法给这个类开一个口子,这就是第二个技术要点:static 方法。
static 方法是可以通过类名访问的,而 static 方法又位于类的内部,对类里的任何方法都有访问权限。这就好了,我们给 Singleton 加上一个静态方法,在这个静态方法里创建类就行。
class Singleton {
private:
Singleton() {}
static Singleton* _instance;
public:
static Singleton* get_instance();
void say_hello();
};
// this is important!!! DO NOT FORGET THIS.
Singleton* Singleton::_instance = NULL;
Singleton* Singleton::get_instance() {
if (_instance == NULL)
_instance = new Singleton();
return _instance;
}
我们通过给 Singleton 类增加一个 public static 方法在这个类上开了一个口子,而这个方法对于私有构造函数是有访问权限的。也就是说,如果你想使用 Singleton 的实例,就不能自己通过 new 来创建,只能通过访问 get_instance 来获取。
get_instance 不是每次都去创建一个新的对象,而是先去检查以前有没有创建过,如果没有创建过,就先创建一个再把创建的 object 赋值给 _instance 属性存起来。如果已经创建过了,就直接返回这个对象。这样就保证了,无论应用代码怎么写,整个系统里只能有一个 Singleton 对象,全局唯一。这种只能创建一个实例的技巧就是单例模式。
需要强调的是代码里的第 12 行,静态变量一定要记得定义和初始化,否则就会出现链接错误,这是新手程序员最常见的错误之一。
- qinsi 👍(0) 💬(1)
实现map的时候没有去调用HiObject的equal方法, 而是直接对象比较, 结果测试变量的程序也可以跑过. 因为load_name的参数指向的都是names表中的实例. 那么什么情况下会有问题呢?
2024-05-22 - ifelse 👍(0) 💬(0)
学习打卡
2024-10-22