对象的回归

最后更新于:2022-04-01 06:25:49

## 对象的回归 先摆出 `create_chain_node` 函数: ~~~ struct chain_node * create_chain_node(void) { struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5)); struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5)); struct pair *holes = pair(left_hole, right_hole); struct pair *body = pair_for_double_type(10.0, 1.0); struct pair *shape = pair(body, holes); struct chain_node *ret = malloc(sizeof(struct chain_node)); ret->prev = NULL; ret->next = NULL; ret->shape = shape; return ret; } ~~~ `create_chain_node` 函数可以创建链节,它是借助很抽象的 `pair` 结构体将很多种类型的数据层层封装到了 `chain+node`结构体中,那么我们如何从 `chain_node` 结构体中提取这些数据,并使之重现它们所模拟的现实事物? 例如,我们怎样从 `chain_node` 结构体中获取一个 `left_hole` 的信息?显然,下面的代码 ~~~ struct *t = create_chain_node(); struct pair *shape = t->shape; struct pair *holes = shape->second; struct pair *left_hole = holes->first; ~~~ 并不能解决我们的问题,因为 `left_hole` 中只是两个 `void *` 指针,而我们需要知道的是 `left_hole` 的中心与半径。那么我们继续: ~~~ struct pair *center = left_hole->first; double radius = *((double *)(left_hole->second)); ~~~ 依然没有解决我们的问题,因为我们想要的是 `left_hole` 的中心,而不是一个包含着两个 `void *` 指针的 `center`,所以需要继续: ~~~ double center_x = *((double *)(center->first)); double center_y = *((double *)(center->second)); ~~~ 最后我们得到了三个 `double` 类型的数据,即 `center_x`, `center_y`, `radius`,于是似乎我们的任务完成了,但是你如何将上述过程写成一个函数 `get_left_hole`? C 语言中的函数只能有一个返回值。如果通过函数的参数来返回一些值,那么`get_left_hole` 是能写出来的,例如: ~~~ void get_left_hole(struct chain_node *t, double *x, double *y, double *r) { struct pair *shape = t->shape; struct pair *holes = shape->second; struct pair *left_hole = holes->first; struct pair *center = left_hole->first; *x = *((double *)(center->first)); *y = *((double *)(center->second)); *r = *((double *)(left_hole->second)); } ~~~ 但是,如果你真的这么写了,那只能说明再好的编程语言也无法挽救你的品味。 我们应该继续挖掘指针的功能,像下面这样定义 `get_left_hole`会更好一些: ~~~ struct point { double *x; double *y; }; struct hole { struct point *center; double *radius; }; struct hole * get_left_hole(struct chain_node *t) { struct pair *shape = t->shape; struct pair *holes = shape->second; return holes->first; } ~~~ 好在哪?我们充分利用了 C 编译器对数据类型的隐式转换,这实际上就是 C 编译器的一种编译期计算。这样做可以避免在代码中出现 `*((double *)(...))` 这样的代码。`void *` 指针总是能通过赋值语句自动转换为左值的,前提是你需要保证左值的类型就是 `void *` 的原有类型。这是 C 语言的一条清规戒律,不能遵守这条戒律的程序猿,也许再好的编程语言也无法挽救他。 C++ 这个叛徒,所以无论它有多么强大,也无法拯救那些无法保证左值的类型就是 `void *` 原有类型的程序猿。用 C++ 编译器迫使程序猿必须将 ~~~ struct pair *shape = t->shape; struct pair *holes = shape->second; ~~~ 写成: ~~~ struct pair *shape = (struct pair *)(t->shape); struct pair *holes = (struct pair *)(shape->second); ~~~ 否则代码就无法通过编译。这样做,除了让代码更加混乱之外,依然无法挽救那些无法保证左值的类型就是 `void *` 原有类型的程序猿,只会让他们对裸指针以及类型转换这些事非常畏惧,逐渐就走上了惟类型安全的形而上学的道路。C++ 11 带来了新的智能指针以及右值引用,希望他们能得到这些新 C++ 式的拯救吧。 当我们用面向对象的思路实现了 `get_left_hole` 之后,就可以像下面这样使用它: ~~~ struct *t = create_chain_node(); struct hole *left_hole = get_left_hole(t); printf("%lf, %lf, %lf\n", *(left_hole->center->x), *(left_hole->center->y), *(left_hole->radius)); ~~~ 一切都建立在指针上了,只是在最后要输出数据的需用 `*` 对指针进行解引用。 上述代码中有个特点,`left_hole` 并不占用内存,它仅仅是对 `t` 所引用的内存空间的再度引用。可能有人会担心`left_hole` 具有直接访问 `t` 所引用的内存空间的能力是非常危险的……有什么危险呢?你只需要清楚 `left_hole` 只是对其他空间的引用,而这一点自从你用了指针之后就已经建立了这样的直觉了,你想修改 `left_hole` 所引用的内存空间中的数据,就可以 do it,不想修改就不去 do it,这有何难?如果自己并不打算去修改 `left_hole` 所引用的内存空间中的数据,但是又担心自己或他人会因为失误而修改了这些数据……你应该将这些担心写到 `get_left_hole` 的注释里! 对于只需要稍加注意就可以很大程度上避免掉的事,非要从编程语言的语法层面来避免,这真的是小题大作了。如果我们在编程中对于 `void *` 指针的隐式类型正确转换率高达 99%,为何要为 1% 的失误而修改编程语言,使之充满各种巧妙迂回的技巧并使得代码愈加晦涩难懂呢? 《C 陷阱与缺陷》的作者给出了一个很好的比喻,在烹饪时,你用菜刀的时候是否失手切伤过自己的手?怎样改进菜刀让它在使用中更安全?你是否愿意使用这样一把经过改良的菜刀?作者给出的答案是:我们很容易想到办法让一个工具更安全,代价是原来简单的工具现在要变得复杂一些。食品加工机一般有连锁装置,可以保护使用者的手指不会受伤。然而菜刀却不同,如果给菜刀这种简单、灵活的工具安装可以保护手指的装置,只能让它失去简单性与灵活性。实际上,这样做得到的结果也许是一台食品加工机,而不再是一把菜刀。 我成功的将本节的题目歪到了指针上。现在再歪回来,我们来谈谈对象。其实已经没什么好谈的了,`get_left_hole` 返回的是泛型指针的类型具化,借助这种类型具化的指针我们可以有效避免对 `pair` 中的 `void *` 指针进行类型转换的繁琐过程。
';