C 语言和 OOP
Cfront编译器
语言之间的转换:C++语言 --> C语言 --> 汇编 --> 机器指令
- 汇编器:将汇编语言翻译成机器指令
- C编译器:将C语言翻译成汇编语言
- C++编译器:将C++语言翻译成C语言
广义上的 C 编译器,指的是把 C 语言翻译成机器码(程序)的编译器。但严格意义上讲,是指把 C 语言翻译成汇编语言,然后再由汇编器把汇编语言翻译成机器码。 看到 C++ 编译器时,你可能会有疑问,C++编译器是将C++语言翻译成C语言?是不是在扯犊子,C++语言和C语言不是两种不同的语言吗?怎么能由一种语言翻译成另一种语言呢?这时候来看下 C++ 原始的编译器:CFront 编译器的描述
Cfront was the original compiler for C++ (then known as "C with Classes") from around 1983, which converted C++ to C; developed by Bjarne Stroustrup at AT&T Bell Labs.
Cfront 是原始的 C++ 编译器,作用是把 C++ 代码转成 C 代码
可能你对把 “C++代码转成 C 代码”这个问题没有疑问了。但又会冒出新的问题:
- C++代码都转成什么样的 C 代码了?
Class
转成 C 的时候变成了什么?- C++的引用在 C 里面是怎么实现的?
public
和private
关键字怎么实现?this
关键字到底是怎么来的?- 方法重载怎么实现?
- etc
不过这里不会一一讨论这些细节,主要是讲和 OOP 面向对象相关的知识点。有兴趣可以移步CFront 的源代码查阅:https://www.softwarepreservation.org/projects/c_plus_plus/cfront/release_1.0/src/cfront/
函数和方法
如果你只写过 C,那你可能只听过 Function 函数这个词, 但如果你学了其它支持面向对象范式的语言,如 C++、Java、Golang 之类,会给你说,Class 里面的函数叫方法 Method。那今天来做一个总结:
- 原本只有函数 Function
- 而方法 Method 是函数的特例、子集,它其实也是函数
有什么证据来支持你这个结论吗?往下看 this 关键字。
this关键字
this
关键字“官方”一点的解释:this
是当前类的实例。不知道在用 this 关键字的时候有没有过疑问:
this
关键字怎么来的?你也没有声明和定义,就能直接用了- 如果他是个变量,不都应该有作用域吗?
第一个疑问:编译器给你生成的。实际上现代的编译器能做的更多:修改代码、插入指令、编译时计算、即时编译 Jit、边翻译边执行(解释器)、etc。对于后者作用域,你大概知道它的作用域:在方法 Method 里面都能用。那它实现的方式:应该是这个方法里面的“一个函数的参数”或者是“函数内的一个局部变量”,所以才能去使用this
变量。
看到这里,你可能还是有点懵,下面图中写了三段代码:左边是 C 语言、中间是 Rust 语言、右边是 C++语言。代码功能含义都一样:定义了一个 Person 的类,定义了构造函数和它的一些方法。不同于 C++、Java这类语言,会完全的隐藏this
关键字,Rust 和Python会保留了一个 self
关键字在函数的参数里面。那现在有一个大概的结论:this
就是“方法”里面的一个参数。而“恰好”C++版本的 this
可以通过->
操作符去访问它的成员,这不就是 C 语言中:结构体指针通过->
操作符去访问成员 的方式一模一样吗。所以说:方法是一种特殊的函数,隐藏了一个当前类实例 this
参数的函数。
类的实例去调用它的方法实际就是个语法糖,实际上类的实例是方法的第一个参数,Go 则使用了把这个参数写在函数名前面,这样看起来更像是实例调用了它的方法,其实也是 this 的作用;
package main
type Rectangle struct {
width, height float64
}
// A method with a receiver parameter
func (r Rectangle) Area() float64 {
return r.width * r.height
}
func main() {
rect := Rectangle{width: 10, height: 5}
// Calling a method with a receiver parameter
area := rect.Area()
}
另外一问题,Java和C++的类用大括号把成员和函数包起来,其实就是个编译器语法糖,看起来像是“方法是类的一部分”;而现代一点的语言 Go、Rust 选择了把类的成员变量单独写(和 C 类似),个人觉得这样更加整洁(成员全都在一块,可读性更高)。
方法重载怎么实现
函数重载只需要编译器增加支持:编译器在编译时去改代码,给函数进行重命名(根据参数类型)。
#include <stdio.h>
__attribute__((overloadable)) void foo_(int x) { printf("foo_with_int: %d\n", x); }
__attribute__((overloadable)) void foo_(float x) { printf("foo_with_float: %f\n", x); }
__attribute__((overloadable)) void foo_(double x) { printf("foo_with_double: %lf\n", x); }
__attribute__((overloadable)) void foo_(char* x) { printf("foo_with_string: %s\n", x); }
int main(int argc, char* argv[]) {
foo_(10);
foo_(10.1f);
foo_(10.1);
foo_("10.1");
return 0;
}
编译:
# 使用用clang编译器 编译程序
$ clang main.c
$ ./a.out
foo_with_int: 10
foo_with_float: 10.100000
foo_with_double: 10.100000
foo_with_string: 10.1
# mn: list symbols from object files
$ nm a.out
0000000100003eb8 T __Z4foo_Pc
0000000100003e84 T __Z4foo_d
0000000100003e4c T __Z4foo_f
0000000100003e14 T __Z4foo_i
0000000100000000 T __mh_execute_header
0000000100003eec T _main
U _printf
TODO: 代码解释,重命名 symbol;还有一点很有意思,在演变的过程中,由编译器提供的特性变成语言提供的特性
封装:
封装从字面上来理解就是包装的意思,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。两个关键点:封装和隐藏;封装:就是把结构体和函数捆绑在一起,看成一个整体,上面 this
关键字部分,专业点就是信息隐藏,
封装:
TODO:curl、sqlite3、dbus、Glib接口设计
隐藏:成员和函数
一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员进行更精确的控制。
- 隐藏信息,实现细节。
TODO:dbus
继承:
用 C 实现继承有两种方法:
- 拓展:父类用
void*
去拓展子类 - 组合:子类嵌套父类结构体
拓展:父类用 void*
去拓展子类
#include <stdio.h>
#include <stdlib.h>
// 继承:通过使用拓展实现
typedef struct object {
int x;
int y;
int width;
int height;
void* sub_obj;
} object_t;
// 创建父类对象
object_t* object_new(int x, int y, int width, int height) {
object_t* obj = malloc(sizeof(object_t));
obj->x = x;
obj->y = y;
obj->width = width;
obj->height = height;
obj->sub_obj = NULL;
return obj;
}
typedef struct img {
char* src;
} img_t;
// 创建子类对象
object_t* img_new(int x, int y, int width, int height) {
object_t* obj = object_new(x, y, width, height);
obj->sub_obj = malloc(sizeof(img_t));
return obj;
}
// 设置子类对象的成员
void img_set_src(object_t* obj, char* src) {
img_t* img = (img_t*)obj->sub_obj;
img->src = src;
}
// 获取子类对象的成员
char* img_get_src(object_t* obj) {
img_t* img = (img_t*)obj->sub_obj;
return img->src;
}
int main(int argc, char* argv[]) {
// 创建子类对象
object_t* obj = img_new(0, 0, 100, 200);
img_set_src(obj, "bar.webp");
printf("obj width: %d\n", obj->width);
printf("obj heith: %d\n", obj->height);
printf("img src: %s\n", img_get_src(obj));
return 0;
}
组合:子类嵌套父类结构体
把父类结构体放在子类的结构成员的第一个,
#include <stdio.h>
#include <stdlib.h>
typedef struct object {
int x;
int y;
int width;
int height;
} object_t;
// 继承:通过使用组合实现
typedef struct img {
struct object obj;
char* src;
} img_t;
img_t* img_new(int x, int y, int width, int height, char* src) {
img_t* img = malloc(sizeof(img_t));
img->obj.x = x;
img->obj.y = y;
img->obj.width = width;
img->obj.height = height;
img->src = src;
return img;
}
typedef struct label {
struct object obj;
char* text;
} label_t;
int main(int argc, char* argv[]) {
// 创建子类对象
img_t* img = img_new(0, 0, 100, 200, "foo.webp");
// 子类对象转换为父类对象
object_t* obj = (object_t*)img;
// 访问父类对象的成员
printf("obj width: %d\n", obj->width);
printf("obj heith: %d\n", obj->height);
return 0;
}
多态:
多态在面向对象语言里面有两种实现方式:抽象类实现、接口实现。但从结果上讲,他们解决的问题是:最后去调谁的实现函数的问题,是调用 A 子类的实现,还是调用 B 子类的实现。所以最后本质问题是,调用哪个函数的问题,对于C语言来说,就是一个函数指针变量赋值的问题。你对这个函数指针变量赋值了 A 类的函数,最终就会调用 A 类的实现;对这个函数指针变量赋值了 B 类的函数,最终就会调用 B 类的实现。
TODO:举例 fs?
面向对象 & 面向过程 怎么选?
- 当然是:全都要
- 建模时、整体结构上,使用面向对象
- 处理流程、细节上,使用面向过程
TODO