在 C 的世界之外

本文为「C 的容器、环境与算法」的续篇

本文的大的背景见「基于 m4 的 C 代码模板化

A:「密集型」的运算过程,还可以用 void * 进行抽象吗?

B:不可以,除非不在意自己的 C 程序在性能上输给 C++ 模板程序。如果这种密集型的运算过程仅仅是对某些数据类型有所依赖,此时可以用宏进行抽象。

A:「宏」,似乎我看到了一幅可怕的景象。

B:也不是很难,可以从简单的一点一点写起。有了感觉之后再去写复杂一些的。像「C 的容器、环境与算法」中的代码,都能忍受,还有什么不能忍受的?看到宏代码,应该立刻会觉得小清新无限……

A:小星星都不见了?

B:建立一个 array.h 文件,其内容为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

然后,再建立一份 main.c 文件,其内容为:

#include <stdio.h>
#include <stdlib.h>

#define T float
#include "array.h"

int main(void)
{
        size_t n = 3;
        Array *a = array_alloc(n);
        a->data[0] = 1.0f; a->data[1] = 2.0f; a->data[2] = 3.0f;
        Array *b = array_alloc(n);
        b->data[0] = -1.0f; b->data[1] = -2.0f; b->data[2] = -3.0f;
        
        Array *v = point_sub(a, b);
        for (size_t i = 0; i < v->n; i++) {
                if (i < v->n - 1) printf("%f ", v->data[i]);
                else printf("%f\n", v->data[i]);
        }
        
        array_free(v);
        array_free(b);
        array_free(a);
        
        return 0;
}

A:这些代码似曾相识,星星果然少多了!

B:T 表示 arrao_allocpoint_sub 等运算所需要的数据类型。它是一个宏,要想让 array.h 中的代码能够通过编译,必须在 #include "array.h" 之前定义 T

A:既然用宏可以让代码干净许多,那为啥还要用 void * 呢?

B:array.h 如果被多个 .c 文件 include,那么 array.h 中的代码会被 C 编译器重复编译为目标代码,这样便会导致程序体积膨胀。

A:这个世界不完美。

B:C++ 的模板,其思路与上述的宏代码很相似,结果也很相似——程序的体积会膨胀,不过 C++ 的编译器提供了优化功能,开了 -O2 之后,那些重复的目标代码会被清除。

A:C++ 更完美……

B:那是因为 C++ 编译器的开发者们把这些脏活累活替大家做了,导致 C++ 编译器的复杂程度跟 C 编译器不是一个数量级。当我使用 CGAL 中的 kd 树对 400,000 个数据点进行 k 近邻检索,程序的执行时间只需 2.8 秒,但是 g++ 编译这个程序所用的时间需要 18 秒。我用 C 实现的 kd 树,用尽我的洪荒之力,也只能将其 k 近邻检索的运算时间降至 4.7 秒,但是 gcc 编译我的 C 程序只需 0.5 秒。

A:这个世界不完美!

B:你要享受现代科技,那就只能多吸点雾霾了。

A:……跑步去

B:我觉得,许多人说 C++ 的模板比 C 的宏更好,这是正确的观点。但是要说 C++ 模板比宏更好,这是错误的观点。

A:是 C 的宏太弱了,衬托出了 C++ 的模板更好?

B:是这样的。如果我们跳出 C 的世界,来看容器与算法所依赖的那些数据类型,它们不过是非常普通的文本而已。既然如此,我们为什么非要在编程语言自身的体系内左右互搏来解决这个问题呢?

A:我觉得……你在说一些我并不擅长的话题……

B:我觉得二维世界里的人会觉得一维世界里的人无比可怜,三维世界里的人看到二维世界里的人因为吃饭而导致自己的身体被分成两半也会觉得很可怜。四维世界里的人怎么看待我们,我难以想象。不过,对于 C 程序而言,它仅仅是个一维世界里的事物而已。void * 也好,宏也好,这些努力都是企图在一维世界里去解决二维问题。

A:你到底想表达什么?

B:我们试试 m4。

A:HA,HA,HA……你暴露了年龄,以前打 CS 时,我最喜欢用的武器!

B:貌似是你暴露了年龄。我都不知道你说的是什么。我说的 m4 是一种语言。

A:老夫……

B:将 array.h 改造为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

A:恕我老眼昏花,愣是没看出来改了何处。

B:你没看错,原样照抄,丝毫未变。试试下面这条命令:

$ m4 -D T=float array.h > array_float.h

A:请允许我做个吃惊的表情,array_float.h 的内容如下:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        float *data;
} Array;

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(float));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

#endif

B:现在你明白了我的意思了吧?

A:似懂非懂。

B: 现在将之前的代码文件都删除,新建 array.h_T,其内容为:

#ifndef ARRAY_H
#define ARRAY_H

typedef struct {
        size_t n;
        T *data;
} Array;

Array * array_alloc(size_t n);
void array_free(Array *x);
Array * point_sub(Array *a, Array *b);

#endif

再建立 array.c_T 文件,其内容为:

#include <stdio.h>
#include <stdlib.h>
`#'include `"'array_`'T`'.h`"'

Array * array_alloc(size_t n)
{
        Array *v = malloc(sizeof(Array));
        v->n = n;
        v->data = malloc(n * sizeof(T));
        return v;
}

void array_free(Array *x)
{
        free(x->data);
        free(x);
}

Array * point_sub(Array *a, Array *b)
{
        if (a->n != b->n) fprintf(stderr, "两个不同维度的点无法构成向量!");
        Array *v = array_alloc(a->n);
        for (size_t i = 0; i < a->n; i++) v->data[i] = a->data[i] - b->data[i];
        return v;
}

注意,上面这两份文件的末尾都要留出一个空行。

main.c 文件内容为:

#include <stdio.h>
#include <stdlib.h>
#include "array_float.h"

int main(void)
{
        size_t n = 3;
        Array *a = array_alloc(n);
        a->data[0] = 1.0f; a->data[1] = 2.0f; a->data[2] = 3.0f;
        Array *b = array_alloc(n);
        b->data[0] = -1.0f; b->data[1] = -2.0f; b->data[2] = -3.0f;
        
        Array *v = point_sub(a, b);
        for (size_t i = 0; i < v->n; i++) {
                if (i < v->n - 1) printf("%f ", v->data[i]);
                else printf("%f\n", v->data[i]);
        }
        
        array_free(v);
        array_free(b);
        array_free(a);
        
        return 0;
}

然后在 Bash 中执行以下命令:

$ ls
array.c_T  array.h_T  main.c

$ for i in array.h array.c ; do m4 -D T=float ${i}_T > ${i%%.*}_float.${i##*.}; done

$ ls
array.c_T  array_float.c  array_float.h  array.h_T  main.c

$ gcc -std=c11 -pedantic -Werror array_float.c main.c -o array-test

$ ./array-test
2.000000 4.000000 6.000000

A:我是不是需要对 m4 有所了解方能看懂下面这样的咒语?

`#'include `"'array_`'T`'.h`"'

是不是需要对 Bash 有所了解,方能看懂:

$ for i in array.h array.c ; do m4 -D T=float ${i}_T > ${i%%.*}_float.${i##*.}; done

B:然。我已经为你写了一份 m4 教程,详见「让这世界再多一份 GNU m4 教程」。至于 Bash,我也只是会用个 for 循环,而且还是临时翻书。


这里可能不会再更新了。

5.9k 声望
1.9k 粉丝
0 条评论
推荐阅读
ConTeXt 蹊径
大概是 2009 年,初学 ConTeXt 时,曾经写了一份笔记,内容颇为粗陋,当时 CTeX 论坛的朋友协助打包上传到了 CTAN。2011 年我对该笔记作了一些修改,并在文中许诺在当年年底作一番大修,然而我食言了。很多年后,...

garfileo阅读 632评论 3

Linux终端居然也可以做文件浏览器?
大家好,我是良许。在抖音上做直播已经整整 5 个月了,我很自豪我一路坚持到了现在【笑脸】最近我在做直播的时候,也开始学习鱼皮大佬,直播写代码。当然我不懂 Java 后端,因此就写写自己擅长的 Shell 脚本。但...

良许1阅读 2.1k

C++编译器和链接器的完全指南
C++是一种强类型语言,它的编译和链接是程序开发过程中不可或缺的两个环节。编译器和链接器是两个非常重要的概念。本文将详细介绍C++中的编译器和链接器以及它们的工作原理和使用方法。

小万哥2阅读 1.1k

封面图
比cat更好用的命令!
但 cat 命令两个很重大的缺陷:1. 不能语法高亮输出;2. 文本太长的话无法翻页输出。正是这两个不足,使得 cat 只能用来查看行数不多的小文件。

良许2阅读 1k

完了,良许直播中删库了……
大家好,我是良许。今天跟大家聊个尴尬的事,大家可以本着看热闹不嫌事大的心态来听我唠唠。经常来我直播间(视频号+抖音)的小伙伴都知道,我最近一直都在直播间手把手现场写 Shell 脚本。就在前天晚上,我写 Sh...

良许1阅读 1.3k

Go官方推出了Go 1.18的2个新教程
2022年1月14日,Go官方团队的Katie Hockman在Go官方博客网站上发表了一篇新文章,主要介绍了Go 1.18的2个新教程,涉及Go泛型和Go Fuzzing。

coding进阶阅读 1.5k

封面图
【新增功能】招投标信息查询——方便快捷查询企业招投标信息
招投标信息查询是集简云的一款内置应用,可以方便快捷地查询企业招投标信息,包括招标标题、招标单位、中标单位、代理机构、招标详情等信息。用户可通过集简云快速集成其他应用,实现招投标信息订阅和推送。

集简云阅读 1.4k

这里可能不会再更新了。

5.9k 声望
1.9k 粉丝
宣传栏