面向对象编程(OOP)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想。
OOP表现出来的三个最基本的特性就是封装、继承与多态。
在本文中我们将讨论C语言如何简单地实现OOP的基础功能,并且关注C++是如何实现OOP的,将两者进行对比。
对于C++的讨论不涉及过多的语法细节,我们不关注public,private,protected修饰的区别,无论是对类的方法还是继承关系的修饰,统一使用public。
我们不关注多继承、菱形继承以及虚继承等复杂情景,只考虑简单的单继承情景,不考虑模板类的处理。
封装
封装就是在形式上将数据和操作数据的方法打包在一起,然后提供部分接口给外部访问,隐藏内部的实现细节。
但是C语言没有直接提供将内部信息隐藏的机制,我们只能使用其它的间接方案:
- 君子协定,约定只通过固定的接口进行访问;
- 利用动态库的符号部分导出的特点将动态库的部分信息隐藏;
- 利用
static变量和函数对源文件外部不可见的性质,将部分信息隐藏
下面我们只关注数据和操作方法的打包,因为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 32 33 34 35 36 37 38
| #include <stdio.h>
typedef struct Point { double x; double y; void (*show)(const struct Point *const); void (*add)(struct Point *const, const struct Point *const); } Point;
void show_point(const Point *const self) { printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void add_point(Point *const self, const Point *const obj) { self->x += obj->x; self->y += obj->y; }
Point make_point(double x, double y) { Point obj; obj.x = x; obj.y = y; obj.show = show_point; obj.add = add_point; return obj; }
int main() { Point p1 = make_point(1.0, 2.0); p1.show(&p1);
Point p2 = make_point(3.0, 4.0);
p1.add(&p1, &p2); p1.show(&p1);
return 0; }
|
运行结果为
1 2
| (x, y) = (1.000000, 2.000000) (x, y) = (4.000000, 6.000000)
|
这里我们定义了Point对象,提供了一个它的构造函数make_point,在其中进行对象的初始化,并且自动用成员函数指针指向对应的函数,
然后就可以通过p.add(...)和p.show(...)来调用对象的方法函数,就像访问对象的数据一样。
值得注意的是,我们必须显式地将当前对象自身的地址&p传递过去,才能保证在调用时不会产生对象的复制,修改始终是针对当前对象自身的。
我们也可以不在结构内中定义成员函数指针,放弃.风格的方法调用形式,直接使用普通的函数调用形式,例子改写如下
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
| #include <stdio.h>
typedef struct Point { double x; double y; } Point;
void show_point(const Point *const self) { printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void add_point(Point *const self, const Point *const obj) { self->x += obj->x; self->y += obj->y; }
Point make_point(double x, double y) { Point obj; obj.x = x; obj.y = y; return obj; }
int main() { Point p1 = make_point(1.0, 2.0); show_point(&p1);
Point p2 = make_point(3.0, 4.0);
add_point(&p1, &p2); show_point(&p1);
return 0; }
|
将两个版本的C语言实现对比,我们可以发现它们其实各有优缺点:
- 第一个版本,每一个对象在内存中都存储了函数指针(实际上是调用函数表),这可能导致额外的空间占用:如果实例化 $m$ 个对象,每个对象有 $n$ 个成员函数,那么就要占用 $8mn$ 的内存。但是空间上的代价带来的好处是:我们可以直接通过修改对象的函数指针达到动态多态的目的,这甚至比C++的虚函数和虚表更灵活,C++的虚表是针对每一个类型的,但是这里的调用函数表是针对每一个对象的。
- 第二个版本,放弃在每一个对象中存储函数指针,这节约了空间占用,但是从语法的角度来说,并没有实现数据和方法的严格绑定,对数据和方法的访问形式是不一样的,对方法的访问就和普通函数调用一样,只是在第一个参数传递了指向对象自身的指针。
在实践中两种方案都有被采用,也可能混合使用。
那么C++到底是怎么做的呢?C++在设计上遵顼Zero overhead 原则,这意味着在提供某种特性、功能或抽象的同时,不对程序或系统引入额外的性能开销或资源消耗。
显然第一个版本不符合这个原则,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 25 26 27 28 29 30
| #include <iostream>
class Point { public: double x; double y;
Point(double x, double y) : x(x), y(y) {}
void show() const { std::cout << "(x, y) = (" << x << ", " << y << ")\n"; }
void add(const Point &other) { x += other.x; y += other.y; } };
int main() { Point p1(1.0, 2.0); p1.show();
Point p2(3.0, 4.0);
p1.add(p2); p1.show();
return 0; }
|
从使用者的角度来看,这更像C语言实现的第一个版本
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { Point p1 = make_point(1.0, 2.0); p1.show(&p1);
Point p2 = make_point(3.0, 4.0);
p1.add(&p1, &p2); p1.show(&p1);
return 0; }
|
只是C++在语法上提供了直接的构造函数,并不需要我们提供专门的make_point函数,
并且C++的普通成员函数调用会自动将this指针传递给方法,并不需要显式传递&p,并且通过引用传递进一步隐藏了指针参数的使用。
但是在编译器实现的角度,这更像C语言实现的第二个版本
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { Point p1 = make_point(1.0, 2.0); show_point(&p1);
Point p2 = make_point(3.0, 4.0);
add_point(&p1, &p2); show_point(&p1);
return 0; }
|
在编译器看来,类的成员函数和普通的外部函数实质没什么区别,除了成员函数总是会自动将指向当前对象自身的this指针作为真正意义上的第一个参数传递给函数。这里this既不需要出现在参数列表中,也不需要出现在调用语句中。
继承
为了简化问题的讨论,我们只考虑单继承关系。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <stdio.h>
typedef struct Point { double x; double y; void (*show)(const struct Point *const); void (*add)(struct Point *const, const struct Point *const); } Point;
void show_point(const Point *const self) { printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void add_point(Point *const self, const Point *const obj) { self->x += obj->x; self->y += obj->y; }
Point make_point(double x, double y) { Point obj; obj.x = x; obj.y = y; obj.show = show_point; obj.add = add_point; return obj; }
typedef struct WeightedPoint { Point p; double w; void (*show)(const struct WeightedPoint *const); void (*add)(struct WeightedPoint *const, const struct WeightedPoint *const); } WeightedPoint;
void show_weighted_point(const WeightedPoint *const self) { printf("(x, y, w) = (%f, %f, %f)\n", self->p.x, self->p.y, self->w); }
void add_weighted_point(WeightedPoint *const self, const WeightedPoint *const obj) { self->p.add(&(self->p), &(obj->p));
self->w += obj->w; }
WeightedPoint make_weighted_point(double x, double y, double w) { WeightedPoint obj; obj.p = make_point(x, y); obj.w = w; obj.show = show_weighted_point; obj.add = add_weighted_point; return obj; }
int main() { Point p1 = make_point(1.0, 2.0); p1.show(&p1);
Point p2 = make_point(3.0, 4.0);
p1.add(&p1, &p2); p1.show(&p1);
WeightedPoint wp1 = make_weighted_point(1.0, 2.0, 3.0); wp1.show(&wp1);
WeightedPoint wp2 = make_weighted_point(3.0, 4.0, 5.0);
wp1.add(&wp1, &wp2); wp1.show(&wp1);
return 0; }
|
运行结果如下
1 2 3 4
| (x, y) = (1.000000, 2.000000) (x, y) = (4.000000, 6.000000) (x, y, w) = (1.000000, 2.000000, 3.000000) (x, y, w) = (4.000000, 6.000000, 8.000000)
|
解释一下这里的新内容:
- 我们定义了
WeightedPoint对象,它包含一个Point对象,这相当于继承关系:WeightedPoint继承了Point。
- 我们提供了派生类的构造函数
make_weighted_point,在其中调用了基类的构造函数make_point,并且加上了对额外的权重数据的初始化。
- 我们还重写了
p.add(...)和p.show(...)方法,对于show()方法直接完全重写,对于add()方法则调用了基类的同名方法,在此基础上添加了权重的相加。
第二个版本的实现也是类似的,移除所有的成员函数指针,直接通过普通函数调用
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #include <stdio.h>
typedef struct Point { double x; double y; } Point;
void show_point(const Point *const self) { printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void add_point(Point *const self, const Point *const obj) { self->x += obj->x; self->y += obj->y; }
Point make_point(double x, double y) { Point obj; obj.x = x; obj.y = y; return obj; }
typedef struct WeightedPoint { Point p; double w; } WeightedPoint;
void show_weighted_point(const WeightedPoint *const self) { printf("(x, y, w) = (%f, %f, %f)\n", self->p.x, self->p.y, self->w); }
void add_weighted_point(WeightedPoint *const self, const WeightedPoint *const obj) { add_point(&(self->p), &(obj->p)); self->w += obj->w; }
WeightedPoint make_weighted_point(double x, double y, double w) { WeightedPoint obj; obj.p = make_point(x, y); obj.w = w; return obj; }
int main() { Point p1 = make_point(1.0, 2.0); show_point(&p1);
Point p2 = make_point(3.0, 4.0);
add_point(&p1, &p2); show_point(&p1);
WeightedPoint wp1 = make_weighted_point(1.0, 2.0, 3.0); show_weighted_point(&wp1);
WeightedPoint wp2 = make_weighted_point(3.0, 4.0, 5.0);
add_weighted_point(&wp1, &wp2); show_weighted_point(&wp1);
return 0; }
|
使用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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include <iostream>
class Point { public: double x; double y;
Point(double x, double y) : x(x), y(y) {}
void show() const { std::cout << "(x, y) = (" << x << ", " << y << ")\n"; }
void add(const Point &other) { x += other.x; y += other.y; } };
class WeightedPoint : public Point { public: double weight;
WeightedPoint(double x, double y, double weight) : Point(x, y), weight(weight) {}
void show() const { std::cout << "(x, y, w) = (" << x << ", " << y << ", " << weight << ")\n"; }
void add(const WeightedPoint &other) { Point::add(other); weight += other.weight; } };
int main() { Point p1(1.0, 2.0); p1.show();
Point p2(3.0, 4.0);
p1.add(p2); p1.show();
WeightedPoint wp1(1.0, 2.0, 3.0); wp1.show();
WeightedPoint wp2(3.0, 4.0, 5.0);
wp1.add(wp2); wp1.show();
return 0; }
|
这与C语言实现的第二个版本在原理上仍然是一致的。
多态
我们主要关注动态多态的C语言模拟和C++实现,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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <iostream>
class Point { public: double x; double y;
Point(double x, double y) : x(x), y(y) {}
virtual void show() const { std::cout << "show from point\n"; std::cout << "(x, y) = (" << x << ", " << y << ")\n"; }
void hello() const { std::cout << "hello from point\n"; } };
class WeightedPoint : public Point { public: double w;
WeightedPoint(double x, double y, double w) : Point(x, y), w(w) {}
void show() const override { std::cout << "show from weighted point\n"; std::cout << "(x, y, w) = (" << x << ", " << y << ", " << w << ")\n"; }
void hello() const { std::cout << "hello from weighted point\n"; } };
void test(const Point *base) { std::cout << "test:\n"; base->hello(); base->show(); }
int main() { Point *base = new Point(1.0, 2.0); test(base); delete base;
WeightedPoint *derived = new WeightedPoint(1.0, 2.0, 3.0); test(derived); delete derived;
return 0; }
|
运行结果如下
1 2 3 4 5 6 7 8
| test: hello from point show from point (x, y) = (1, 2) test: hello from point show from weighted point (x, y, w) = (1, 2, 3)
|
注意这里的hello()和show()得到的结果是不一样的,前者是静态绑定,后者是动态绑定(因为show()是虚函数),这里不展开讨论。
下面分别从两个角度对这个例子使用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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <stdio.h> #include <stdlib.h>
typedef struct Point { double x; double y; void (*hello)(const struct Point *const); void (*show)(const struct Point *const); } Point;
void show_point(const Point *const self) { printf("show from point\n"); printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void hello_point(const Point *const self) { printf("hello from point\n"); }
void init_point(Point *const self, double x, double y) { self->x = x; self->y = y;
self->hello = hello_point; self->show = show_point; }
typedef struct WeightedPoint { Point p; double w; void (*hello)(const struct WeightedPoint *const); void (*show)(const struct WeightedPoint *const); } WeightedPoint;
void show_weighted_point(const WeightedPoint *const self) { printf("show from weighted point\n"); printf("(x, y, w) = (%f, %f, %f)\n", self->p.x, self->p.y, self->w); }
void hello_weighted_point(const WeightedPoint *const self) { printf("hello from weighted point\n"); }
void virtualoverwrite_show(const Point *const self) { ((WeightedPoint *)self)->show((WeightedPoint *const)self); }
void init_weighted_point(WeightedPoint *const self, double x, double y, double w) { init_point(&(self->p), x, y); self->w = w;
self->hello = hello_weighted_point;
self->show = show_weighted_point; self->p.show = virtualoverwrite_show; }
void test(Point *base) { printf("test:\n"); base->hello(base); base->show(base); }
int main() { Point *base = (Point *)malloc(sizeof(Point)); init_point(base, 1.0, 2.0); test((Point *)base); free(base);
WeightedPoint *derived = (WeightedPoint *)malloc(sizeof(WeightedPoint)); init_weighted_point(derived, 1.0, 2.0, 3.0); test((Point *)derived); free(derived);
return 0; }
|
运行结果与C++的代码一致。
解释一下,这里我们对hello()和show()进行了不同的处理:
- 对于
hello()方法,base->hello的调用效果完全取决于当前的base是Point *指针还是Derived *指针。
- 对于
show()方法,我们不仅将WeightedPoint版本的实现绑定到派生类的*show指针,还修改了派生类包含的基类对象中的*show指针,将其指向一个类型转换接口:virtualoverwrite_show,在其中将Point *指针完全转换为WeightedPoint *指针再进行处理,达到基类指针调用派生类方法的目的。
事实上在这种方案下,我们完全不需要继承关系就可以实现运行时多态的效果,例如
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #include <stdio.h> #include <stdlib.h>
typedef struct Point { double x; double y; double w; void (*hello)(const struct Point *const); void (*show)(const struct Point *const); } Point;
void show_point(const Point *const self) { printf("show from point\n"); printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void hello_point(const Point *const self) { printf("hello from point\n"); }
void init_point(Point *const self, double x, double y) { self->x = x; self->y = y; self->w = 0;
self->hello = hello_point; self->show = show_point; }
void show_weighted_point(const Point *const self) { printf("show from weighted point\n"); printf("(x, y, w) = (%f, %f, %f)\n", self->x, self->y, self->w); }
void hello_weighted_point(const Point *const self) { printf("hello from weighted point\n"); }
void init_weighted_point(Point *const self, double x, double y, double w) { self->x = x; self->y = y; self->w = w;
self->hello = hello_point; self->show = show_weighted_point; }
void test(Point *base) { printf("test:\n"); base->hello(base); base->show(base); }
int main() { Point *tmp1 = (Point *)malloc(sizeof(Point)); init_point(tmp1, 1.0, 2.0); test(tmp1); free(tmp1);
Point *tmp2 = (Point *)malloc(sizeof(Point)); init_weighted_point(tmp2, 1.0, 2.0, 3.0); test(tmp2); free(tmp2);
return 0; }
|
这里对hello()和show()的处理其实是一致的,在初始化函数中自由地调整函数指针即可
1 2 3 4 5 6 7 8
| self->hello = hello_point; self->show = show_weighted_point;
self->hello = hello_weighted_point; self->show = show_weighted_point;
|
基于类型的虚函数表
基于第二个方案,我们没有将普通函数的调用通过结构体内部的函数指针实现,而是为每一个对象加上一个虚表指针vptr,指向当前类型的虚表,在涉及到虚函数动态调用时,通过当前类型的虚函数表进行间接调用。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| #include <stdio.h> #include <stdlib.h>
typedef struct Point { const struct Point_vftable *vptr; double x; double y; } Point;
typedef struct Point_vftable { void (*show)(const Point *const); } Point_vftable;
void hello_point(const Point *const self) { printf("hello from point\n"); }
void show_point(const Point *const self) { printf("show from point\n"); printf("(x, y) = (%f, %f)\n", self->x, self->y); }
void init_point(Point *const self, double x, double y) { static const Point_vftable point_vftable = {show_point}; self->vptr = &point_vftable;
self->x = x; self->y = y; }
typedef struct WeightedPoint { Point base; double w; } WeightedPoint;
void hello_weighted_point(const WeightedPoint *const self) { printf("hello from weighted point\n"); }
void show_weighted_point(const WeightedPoint *const self) { printf("show from weighted point\n"); printf("(x, y, w) = (%f, %f, %f)\n", self->base.x, self->base.y, self->w); }
void init_weighted_point(WeightedPoint *const self, double x, double y, double w) { init_point(&(self->base), x, y);
static const Point_vftable weightedPoint_vftable = { (void (*)(const Point *const))show_weighted_point}; self->base.vptr = &weightedPoint_vftable;
self->w = w; }
void test(Point *base) { printf("test:\n"); hello_point(base); base->vptr->show(base); }
int main() { Point *base = (Point *)malloc(sizeof(Point)); init_point(base, 1.0, 2.0); test(base); free(base);
WeightedPoint *derived = (WeightedPoint *)malloc(sizeof(WeightedPoint)); init_weighted_point(derived, 1.0, 2.0, 3.0); test((Point *)derived); free(derived);
return 0; }
|
解释一下:
- 首先,我们定义了结构体
Point,含有数据成员和虚表指针
- 然后定义了继承
Point的结构体WeightedPoint,它将前者作为自己的成员,并且含有额外的数据成员,它仍然使用基类的虚表指针
- 两个类型的虚表通过函数的静态局部变量的形式被存放在初始化函数中,确保具有唯一的实例和静态生命周期
- 在C++编译器的具体实现中,除了虚表指针,类型识别信息和偏移量实际上也会被塞进结构体中,这里为了简化将它们移除了
hello()方法的调用必然是静态的:我们只提供了hello_point()和hello_weighted_point两个明确的接口
show()方法支持静态调用:我们提供了show_point和show_weighted_point两个明确的接口,也支持动态调用,通过虚表的调用实现:base->vptr->show(base)
为了尽可能和C++的实现保持一致,上面所有的C语言代码中的指针类型在允许的情况下都加上了很繁琐的const修饰,有时加了两层const,看着非常繁琐,可以进行简化。