对象的回归
最后更新于: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 *` 指针进行类型转换的繁琐过程。