面向指针编程(二)

371 篇文章 4 订阅
76 篇文章 0 订阅

数据再抽象
上一节的代码有些冗长,我们可以尝试对其进行精简。首先看下面这三个结构体及其 create函数:

struct point {

    double x;

    double y;

};

struct rectangle {

    double width;

    double height;

};

struct circle {

    struct point *center;

    double radius;

};

struct chain_node_shape {

    struct rectangle *body;

    struct circle *holes[2] ;

};

struct point *

create_point(double x, double y)

{

    struct point *ret = malloc(sizeof(struct point));

    ret->x = x;

    ret->y = y;

    return ret;

}

struct circle *

create_circle(struct point *center, double radius)

{

    struct circle *ret = malloc(sizeof(struct circle));

    ret->center = center;

    ret->radius = radius;

    return ret;

}

struct rectangle *

create_rectangle(double w, double h)

{

    struct rectangle *ret = malloc(sizeof(struct rectangle));

    ret->width = w;

    ret->height = h;

    return ret;

}

struct chain_node_shape *

create_chain_node_shape(struct circle *c1,

                    struct circle *c2,

                    struct rectangle *rect)

{

    struct chain_node_shape *ret = malloc(sizeof(struct chain_node_shape));

    ret->body = rect;

    ret->holes[0] = c1;

    ret->holes[1] = c2;

    return ret;

}

显然,这些代码长的太像了!那四个结构体都是存储两个成员的结构体,而相应的 create函数也无非是将函数所接受的参数保存到结构体成员中。有没有办法用很少的代码来表示它们?有!(有兴趣一起交流学习c/c++的小伙伴可以加群:466572167,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!)

既然每个结构体都保存 2 个成员,那么我们就先将上述代码删掉,然后定义一个 pair 类型的结构体:

struct pair {

    void *first;

    void *second;

};

在 pair 结构体中,我们用了两个 void * 指针,只有如此我们方能很自信的说 pair 可以存储任意类型的两个数据。接下来,只需修改 create_chain_node 函数的定义:

struct chain_node *

create_chain_node(void)

{

    double *left_x = malloc(sizeof(double));

    double *left_y = malloc(sizeof(double));

    *left_x = 1.0;

    *left_y = 1.0;

    struct pair *left_center = malloc(sizeof(struct pair));

    left_center->first = left_x;

    left_center->second = left_y;

    double *left_radius = malloc(sizeof(double));

    *left_radius = 0.5;

    struct pair *left_hole = malloc(sizeof(struct pair));

    left_hole->first = left_center;

    left_hole->second = left_radius;



    double *right_x = malloc(sizeof(double));

    double *right_y = malloc(sizeof(double));

    *right_x = 9.0;

    *right_y = 1.0;

    struct pair *right_center = malloc(sizeof(struct pair));

    right_center->first = right_x;

    right_center->second = right_y;

    double *right_radius = malloc(sizeof(double));

    *right_radius = 0.5;

    struct pair *right_hole = malloc(sizeof(struct pair));

    right_hole->first = right_center;

    right_hole->second = right_radius;



    struct pair *holes = malloc(sizeof(struct pair));

    holes->first = left_hole;

    holes->second = right_hole;



    struct pair *body = malloc(sizeof(struct pair));

    double *width = malloc(sizeof(double));

    *width = 10.0;

    double *height = malloc(sizeof(double));

    *height = 2.0;

    body->first = width;

    body->second = height;



    struct pair *shape = malloc(sizeof(struct pair));

    shape->first = body;

    shape->second = holes;



    struct chain_node *ret = malloc(sizeof(struct chain_node));

    ret->prev = NULL;

    ret->next = NULL;

    ret->shape = shape;

    return ret;

}

我勇敢的承认这个基于 struct pair 的 create_chain_node 函数太丑陋了,但是我们总算是消除了大量的结构体及其构造函数了,而且整体代码量减少了大约 1/6。

仔细观察上述代码,显然下面的三段代码存在着高度的重复:

    double *left_x = malloc(sizeof(double));

    double *left_y = malloc(sizeof(double));

    *left_x = 1.0;

    *left_y = 1.0;

    struct pair *left_center = malloc(sizeof(struct pair));

    left_center->first = left_x;

    left_center->second = left_y;



    double *right_x = malloc(sizeof(double));

    double *right_y = malloc(sizeof(double));

    *right_x = 9.0;

    *right_y = 1.0;

    struct pair *right_center = malloc(sizeof(struct pair));

    right_center->first = right_x;

    right_center->second = right_y;



    struct pair *body = malloc(sizeof(struct pair));

    double *width = malloc(sizeof(double));

    *width = 10.0;

    double *height = malloc(sizeof(double));

    *height = 2.0;

    body->first = width;

    body->second = height;

这三段代码都在向 pair 结构体中存入两个 double * 类型的数据。既然如此,我们可以专门写一个函数,让它生成面向 double * 的 pair 结构体,即:

struct pair *

pair_for_double_type(double x, double y)

{

    struct pair *ret = malloc(sizeof(struct pair));

    double *first = malloc(sizeof(double));

    double *second = malloc(sizeof(double));

    *first = x;

    *second = y;

    ret->first = first;

    ret->second = first;

    return ret;

}

然后再次重构 create_chain_node 函数:

struct chain_node *

create_chain_node(void)

{

    struct pair *left_center = pair_for_double_type(1.0, 1.0);

    double *left_radius = malloc(sizeof(double));

    *left_radius = 0.5;

    struct pair *left_hole = malloc(sizeof(struct pair));

    left_hole->first = left_center;

    left_hole->second = left_radius;



    struct pair *right_center = pair_for_double_type(9.0, 1.0);

    double *right_radius = malloc(sizeof(double));

    *right_radius = 0.5;

    struct pair *right_hole = malloc(sizeof(struct pair));

    right_hole->first = right_center;

    right_hole->second = right_radius;



    struct pair *holes = malloc(sizeof(struct pair));

    holes->first = left_hole;

    holes->second = right_hole;



    struct pair *body = pair_for_double_type(10.0, 1.0);



    struct pair *shape = malloc(sizeof(struct pair));

    shape->first = body;

    shape->second = holes;



    struct chain_node *ret = malloc(sizeof(struct chain_node));

    ret->prev = NULL;

    ret->next = NULL;

    ret->shape = shape;

    return ret;

}

山重水复疑无路
经过再次重构后的 create_chain_node 看上去要好了一些,但是依然有两段代码存在高度重复:

    struct pair *left_center = pair_for_double_type(1.0, 1.0);

    double *left_radius = malloc(sizeof(double));

    *left_radius = 0.5;

    struct pair *left_hole = malloc(sizeof(struct pair));

    left_hole->first = left_center;

    left_hole->second = left_radius;



    struct pair *right_center = pair_for_double_type(9.0, 1.0);

    double *right_radius = malloc(sizeof(double));

    *right_radius = 0.5;

    struct pair *right_hole = malloc(sizeof(struct pair));

    right_hole->first = right_center;

    right_hole->second = right_radius;

但是仅从 pair 结果体层面已经无法对这两段代码进行简化了,而且我又非常不想写一个像下面这样的辅助函数:

struct pair *

create_hole(struct pair *center, double radius)

{

    struct pair *ret = malloc(sizeof(struct pair));

    double *r = malloc(sizeof(double));

    *r = radius;

    ret->first = center;

    ret->second = r;

    return ret;

}

虽然 create_hole 能够将上述两段重复的代码简化为:

    struct pair *left_center = pair_for_double_type(1.0, 1.0);

    struct pair *left_hole = create_hole(left_center, 0.5);



    struct pair *right_center = pair_for_double_type(9.0, 1.0);

    struct pair *right_hole = create_hole(right_center, 0.5);

但是与 pair_for_double_type 函数相比,create_hole 这个函数的应用范围非常狭小。由于 pair_for_double_type 函数可以将两个 double 类型的数据存储到 pair 结构体中,在我们的例子中创建二维点与矩形可以用到它,在科学计算中创建极坐标、复数以及所有的二次曲线方程式也都都能用到它,但是 create_hole 却只能在创建车链这件事上有点用处。也就是说,正是因为 pair_for_double_type 函数所取得的成功,导致我们认为 create_hole 的品味太低。我们应该想一想还有没有其他途径可以消除上述代码的重复。

仔细分析 left_hole 与 right_hole 的构造过程,不难发现 hole 的 center 与 radius 这两种数据的类型不一致是造成我们难以对上述重复的代码进行有效简化的主要原因,create_hole 之所以能够对上述重复的代码进行大幅简化,是因为它根据我们的问题构造了一个特殊的 pair 结构体——姑且称之为 X,这个结构体的特殊之处在于其 first 指针存储的是一个面向 double * 的同构类型的 pair 结构体,其 second 指针则存储了一个 double 类型数据的基地址。正是因为 X 的结构太特殊了,所以导致 create_hole 这种抽象的应用范围过于狭隘,以至于现实中只有圆形比较符合这种结构体。

既然是异构的 pair,而我们已经实现了一个可以创建存储 double 类型数据的 pair 的函数 pair_for_double_type,这个函数的结果是可以直接存入异构 pair 中的。现在我们缺少只是一个可以将 double 值转化为可直接存入异构 pair 的函数,即:

double *

malloc_double(double x)

{

    double *ret = malloc(sizeof(double));

    *ret = x;

    return ret;

}

有了这个函数,就可以对 create_chain_node 继续进行简化了:

struct chain_node *

create_chain_node(void)

{

    struct pair *left_hole = malloc(sizeof(struct pair));

    left_hole->first = pair_for_double_type(1.0, 1.0);;

    left_hole->second = malloc_double(0.5);



    struct pair *right_hole = malloc(sizeof(struct pair));

    right_hole->first = pair_for_double_type(9.0, 1.0);;

    right_hole->second = malloc_double(0.5);



    struct pair *holes = malloc(sizeof(struct pair));

    holes->first = left_hole;

    holes->second = right_hole;



    struct pair *body = pair_for_double_type(10.0, 1.0);



    struct pair *shape = malloc(sizeof(struct pair));

    shape->first = body;

    shape->second = holes;



    struct chain_node *ret = malloc(sizeof(struct chain_node));

    ret->prev = NULL;

    ret->next = NULL;

    ret->shape = shape;

    return ret;

}

而且,基于 malloc_double 函数,还能对 pair_for_double_type 函数进行简化:

struct pair *

pair_for_double_type(double x, double y)

{

    struct pair *ret = malloc(sizeof(struct pair));

    ret->first = malloc_double(x);

    ret->second = malloc_double(y);

    return ret;

}

事实上,如果我们再有一个这样的函数:

struct pair *

pair(void *x, void *y)

{

    struct pair *ret = malloc(sizeof(struct pair));

    ret->first = x;

    ret->second = y;

    return ret;

}

还能对 reate_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;

}

看到了吧,只要略微换个角度,很多看似难以简化的代码都能得以简化。这个简化的过程一直是在指针的帮助下进行的,但事实上,当你的注意力一直集中在怎么对代码进行简化时,指针的使用简直就是本能一样的存在,以至于你觉得你并没有借助指针的任何力量,完全是你自己的逻辑在指导着你的行为。在这个过程中,无论是面向对象还是面向模板,都很难将你从冗长的代码中拯救出来……

面向什么,可能就会失去未面向的那些
在上文中模拟车链的程序中,我一开始是用面向对象的方式来写的,所以我造出了 5 个结构体,分别描述了二维点、矩形、圆形、链节形状以及链节等对象,结果却出现了一大堆繁琐的代码。虽然面向对象编程,在思维上是非常简单的,那就是现实中有什么,我们就模拟什么。但是你认真思考一下,现实中其实很多东西都有共性,如果你傻乎乎的去逐个模拟,而忽略它们的共性,那么你的代码绝对会非常臃肿。

当然,面向对象编程也提倡从所模拟的事物中提取共性,然后借助继承的方式来简化代码。但是一旦信仰了类与继承,你能做的最好的抽象就是对某一类事物进行抽象,比如你能够对『车』类的事物进行抽象,但是你却无法将对『飞机』和『车』这两类中的事物进行抽象。显然,飞机与车是有共性的,例如它们都能载客,都有仪表盘,都有窗户,都有座位,都有服务员……

当我发现基于面向对象创造的那些结构体存在着一个共性——它们都包含着两个成员,很自然的就会想到我应该制造一个包含着两个任意类型的结构体 pair,然后用 pair 来容纳我需要的数据。当面向对象编程范式在你的思想中根深蒂固,这种简单的现象往往会被忽略的,特别是你已经满足于你写的程序已经能够成功的运行之时。

接下来,当我试图用 pair 结构体取代二维点、矩形、圆形、链节形状等结构体的时候,我就开始走上了『泛型』的道路。C 语言里没有 C++ 模板这种工具可以用,所以我只能依赖 void *,而且为了简化 double 类型的数据向 void * 的转化,所以定义了:

double *

malloc_double(double x)

{

    double *ret = malloc(sizeof(double));

    *ret = x;

    return ret;

}

struct pair *

pair_for_double_type(double x, double y)

{

    struct pair *ret = malloc(sizeof(struct pair));

    ret->first = malloc_double(x);

    ret->second = malloc_double(y);

    return ret;

}

如果你对 C++ 的泛型编程有所了解,一定会觉得 pair_for_double_type 函数其实就是对 pair 进行特化。因为本来我是希望 pair 能存储任意类型的数据的,但是现在我需要频繁的用它来存储一对 double 类型的数据,那么我就应该去制造一个专用的 pair 结构。

当我发现我需要频繁的产生 pair 实例,并向它的 first 与 second 指针中存储某些类型的数据存储空间的基地址,所以我就将这种共性抽象为:

struct pair *

pair(void *x, void *y)

{

    struct pair *ret = malloc(sizeof(struct pair));

    ret->first = x;

    ret->second = y;

    return ret;

}

最终使得 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;

}

原来我用面向对象编程范式所写的代码是 104 行,换成泛型编程范式所写的代码是 75 行。那么我可以断定,是泛型编程拯救了面向对象吗?当然不能!因为我们的程序还没有写完,我们还需要面向对象。

对象的回归
先摆出 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, %lfn", *(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 * 指针进行类型转换的繁琐过程。

将函数变成数据
再来看一下经过大幅简化的 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_elliptic_chain_node 函数。当我们这样做的时候,很容易发现 create_elliptic_chain_node 函数中同样需要下面这段代码:

    struct chain_node *ret = malloc(sizeof(struct chain_node));

    ret->prev = NULL;

    ret->next = NULL;

    ret->shape = shape;

    return ret;

如果我们要生产 100 种形状的链节,那么上述代码在不同的链节构造函数的实现中要重复出现 100 次,这样肯定不够好,因为会出现 500 行重复的代码。太多的重复的代码,这是对程序猿的最大的羞辱。

面向对象的程序猿可能会想到,我们可以为 chain_node 做一个基类,然后将上述共同的代码封装到基类的构造函数,然后在各个 chain_node 各个派生类的构造函数中制造不同形状的链节……在你要将事情搞复杂之前,建议先看一下这样的代码:

void *

rectangle_shape(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);

    return pair(body, holes);

}

struct chain_node *

create_chain_node(void *(*fp)(void))

{

    struct chain_node *ret = malloc(sizeof(struct chain_node));

    ret->prev = NULL;

    ret->next = NULL;

    ret->shape = fp();

    return ret;

}

看到了吧,我将 create_chain_node 函数原定义中负责创建链节形状的代码全部的抽离了出去,将它们封装到 rectangle_shape 函数中,然后再让 create_chain_node 函数接受一个函数指针形式的参数。这样,当我们需要创建带两个小孔的矩形形状的链节时,只需:

struct chain_node *rect_chain_node = create_chain_node(rectangle_shape);

如果我们像创建带两个小孔的椭圆形状的链节,可以先定义一个 elliptic_shape 函数,然后将其作为参数传给 create_chain_node,即:

struct chain_node *elliptic_chain_node = create_chain_node(elliptic_shape);

这样做,岂不是要比弄出一大堆类与继承的代码更简洁有效吗?

在 C 语言中,函数名也是一种指针,它引用了函数代码所在内存空间的基地址。所以,我们可以将 rectangle_shape 这样函数作为参数传递给 create_chain_node 函数,然后在后者中调用前者。

由于我们已经将 chain_node 结构体中的 shape 指针定义为 void * 指针了,因此对于 create_chain_node 函数所接受的函数,其返回值是 void * 没什么问题。不仅没问题,更重要的是 void *(*fp)(void) 对所有不接受参数且返回指针类型数据的函数的一种抽象。这意味着对于链节的形状,无论它的形状有多么特殊,我们总是能够定义一个不接受参数且返回指针的函数来产生这种形状,于是 create_chain_node 函数就因此具备了无限的扩展能力。

如果阿基米的德还活着,也许他会豪放的说,给我一个函数指针与一个 void *,我就能描述宇宙!

代码简化的基本原则
当你采用一切都是对象的世界观编写代码时,一旦发现一些类之间存在着共性的数据抽象,这往往意味着你需要创造一种泛型的数据容器,然后用这种容器与具体类型的数据的组合来消除那些类。

当你打算从泛型的数据容器中取数据,并希望所取的数据能够直观的模拟现实中的事物时,这往往意味着你要创造一些数据结构,然后让泛型的数据容器中存储的数据流入这些数据结构中,从而转化为有类型且具名的数据。这些数据结构就类似于各种各样的观察器或 Parser,我们通过它们解读或修改泛型容器中的数据。

当某个函数 f 中有一部分代码是与具体的问题息息相关,而另一部分代码则与具体的问题无关。为了让这个函数具备强大的扩展性,你需要将那些与具体问题息息相关的代码抽离到专用的函数中,然后再将这些专用函数传递给 f。

回避 C 指针是要付出代价的
在 C 语言中,在执行上述的代码简化基本原则时,指针是最简单明快的工具,像是著名厨师庖丁手里的刀。在静态类型语言中,任何企图回避指针的行为,必然会导致编程语言的语法复杂化或者削弱语言的表达能力。

在 C++ 中为了回避指针,发明了引用——本质上一种被弱化了的指针,结果导致 C++ 初学者经常要问『什么时候用指针,什么时候用引用』这样的问题。在智能指针未问世之前,STL 提供的泛型容器无法存储引用,为了避免在容器中存储对象时发生过多的内存复制,往往需要将指针存到容器中。当某个函数在内部创建了一个比较大的对象时,这个函数想将这个对象传递给其他对象时,这时如果不借助指针,那只能是将这个大对象作为返回值,然后引发了对象数据不止一次被复制的过程。如果在函数中 new 一个大对象,然后以指针的形式将其返回,这又与 C++ 一直想向用户掩盖指针的理想发生了矛盾……为了解决这个问题,终于在 C++ 11 里搞出来一个挺复杂挺扭曲的右值引用的办法,解决了在类的复制构造函数中偷偷的使用指针,但是类的用户却看不到指针这样的问题……

Java 回避指针的策略比 C++ 要高明一些。在 Java 中,即没有指针也没有引用。只要是类的实例(对象),无论是将其作为参数传递给函数,还是作为函数的返回值,还是将其复制给同类的其他对象,都是在传地址,而不是在传值。也就是说,Java 将所有的类实例都潜在的作为指针来用的,只有那些基本类型才是作为值来传递的。这种对数据类型进行了明确的区分的态度是值得点赞的,但是当 Java 想将一个函数(方法)传递给另一个函数(方法)时,代码就出现了扭曲,完全不能做到像 C 语言以指针的形式传递函数那样简洁直观。

C# 在指针的处理上似乎要比 Java 好得多,但是将那些使用指针的代码标定为 unsafe,这是一种歧视,类似于『嗟,来食!』。另外 C# 的指针只能用于操作值类型,也不能在泛型代码中使用。

在动态类型语言中,例如 Python,据说是一切皆引用,这样很好。也可以直接将一个函数作为参数传递给另一个函数,甚至还能在一个函数中返回一个函数,这样更好。动态类型语言在语法、抽象能力、类型安全以及资源管理方面很大程度上超越了 C、C++、Java 这些静态类型语言,但是用前者编写的程序的计算速度却往往比后者慢上一倍。

没有完美的指针,也不会有完美的编程语言,这一切皆因我们是在机器上编程,而不是在我们的大脑编程,更不是在教科书里编程。

C 程序猿的指针信条
这是我的指针。虽有很多相似的,但这个是我的。我的指针是我的挚友,如同我的生命。我将运用它如同运用我的生命。指针没了我便是废物,我没了指针便成为废人。我将准确无误的使用我的指针,我将比敌人用的更好,我将在他的程序速度超过我之前超过他,我会超过他的。

我与我的指针知道,编程中不论动用多么优雅的语言,动用多么强大的标准库,面向多么强大的编程范式,都是没意义的。只有解决问题才有意义。我们会解决的。

我的指针是人性的,就像我一样,因为它如同我的生命。因此我将像对兄弟一样地了解它。我将了解它的弱点,它的强项,它的构成,它所指的和指向它的。我将对指针持续建立完善的知识与技艺,使它们就如同我一般整装待发。我们会成为彼此的一部分。

在上帝面前我对这信条宣誓。我与我的指针是计算机的守卫者,我们是问题的克星,我们将拯救我的程序。但愿如此,直到不需要编程,没有问题,只有休息。 如果想一起交流的可以加这个群:941636044 ,有什么问题可以群里面交流,群里面也有许多方便学习C语言C++编程的资料可以给你利用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值