Contents

从 MVC 开始模块化编程(下)

我们继续利用上一篇,参考 MVC 开始模块化编程(中)对 VIEW 的方法,我们也对 control 模块的接口进行设计,并修改 attr.c 文件。如下:

attr.c

#include <stdio.h>
#include <string.h>
void param_done(void);

#include "view.h"
#include "control.h"
int main(int argc ,char *argv[]){

    FILE *f = 0;
    int view_mode = 0;
    int control_mode = 0;
    if (argc < 2){
        printf("please enter the pathname !\n");
        return 1;
    }
    control_mode = ((f = fopen(argv[1],"rt")) != 0);
    if (control_mode){
        strcpy(filename,argv[1]);
        fclose(f);
        view_mode = 0;
    }else{
        filename[0] = 0;
        view_mode = 1;
        strcpy(v_param,argv[1]);
    }
    control(control_mode);
    
        param_done();
        view(view_mode);
    
    return 0;
}

control.c

#include <stdio.h>
char fliename[1024];
static void read_param_default(void){
    printf("read_param_default func !\n");
    return;
}
static int read_param_from_file(FILE *f){
    printf("read_param_from_file func !\n");
    return 0;
}

void control(int flag){
    if (flag){
        read_param_from_file(filename);
    }else{
        read_param_default();
    }
    return;
}

新增文件 control.h

#ifndef _CONTROL_H_
#define _CONTROL_H_
void control(int);
extern char filename[];
#endif

保存上述文件,然后 我们先编译一下attr.c 文件,如下 gcc -Wall -c attr.c -o attr.o 没有任何错误。

如果我们一不小心命令写成如下 gcc -Wall attr.c -o attr.o 会有一堆错误,意思是找不到对应的函数或存储空间 (变量)。

鬼话:重复强调下,如果直接使用gcc或以后使用makefile 而后者没有正确描述,缺失-c,会导致上面的命令要求编译以及链接生成一个attr.o的执行文件。这是新手经常犯的错误。如果你发现 undefine reference to "XXX",这是链接出了问题,要么是没有找到对应库,要么是你使用了那个外部函数,也即本C文件中使用的函数实际在另一个C文件中实现,出现了名称不匹配。通常是调用位置名称写错 。后者的概率更大。

我们现在编译下新的代码control.c gcc -Wall -c control.c -o control.o 有错了。这里的提示为意思是filename没有声明。其实原因很简单,仔细看control.c中的第2行,我把filename错误的拼写为了fliename。

但为什么attr.c在编译阶段,没有错呢?因为attr.c使用filename仅是编译,只要在control.h中,存在 extern char filename[]; 那么对于attr.c的编译而言,就已经确认外部有此存储空间。反过来,如果我们在control.h的第4行,写为fliename,而在control.c的第2行写为filename,那么如果只是对上述两个C文件进行编译的话,attr.c会错,而control.c不会错。

由此你应该理解,编译器,是各编各的,在编译阶段,不会考虑其他C文件的内容。而再次强调,attr.c #include 了control.h,此时不代表attr.c和control.c这个文件有了关联。而此时的control.h仅是attr.c的一部分。

鬼话一下:相信我,再老的鸟,写C代码,笔误都会存在,诸如上述的错误,均会发生,这和编程经验与能力无关,和细心度有关,因此不要害怕类似错误。至少我,到目前阶段,很少出现一次编译通过的情况,经常发生笔误。没什么大不了的。只要你能找到错误点。

我们将错误改正,只需要改control.c中 fliename 变为filename,即可。注意,这里针对control.c的编译仍然有个WARNING问题。而且这个问题比较容易引发错误,请读者自行查找原因,并解决掉。

运行如下命令, gcc -Wall control.c view.c attr.c model.c -o attr 记得rm attr.c1 运行 ./attr attr.c ./attr attr.c1 看是否打印内容和以前一致。如果一致,至少表示两种情况运行的最终函数顺序正确。

鬼话:无论你如何替换,修改你的函数,要确保替换和修改后,至少可以通过测试点输出的数据的一致否,来判断替换,修改后,对应未变逻辑实现的正确性。如果有新增逻辑,那么新增逻辑的测试打印点,一定要等待原有测试打印点在修改后验证无误后,才能增加。否则又要开始理解自己写的代码了。如我上述代码,虽然现在新增了函数,但并没有增加测试点。只是逻辑转移了。

鬼话:写代码设计本身并不是难点,难点是在如何构造测试方法和测试数据。为什么这么说?我们写代码的目的是要实现设计目标。而设计目标是否有效实现和测试系统的设计有极大的关联。测试方法不合理,测试数据不完备,那么就会埋藏很多 BUG,这些BUG,通常是逻辑上的,和设计目标无法正确实现有关联,BUG越在后期发现,修改的难度越大。 这里讨论一下我上面gcc后续跟的文件顺序。你会发现,attr.c并不是最后一个文件。你需要明确。此处由于没有-c的参数,则gcc会完成编译和链接两个工作。因此,首先是把所有的输入文件进行编译,生成对应的.o文件,所以不存在attr.c的顺序问题。虽然我们知道attr.c中存在main函数。但连接器可不管这么多,给入的.o文件中会自动查找。为什么上述命令后,ls看不到对应.o文件。我前面已经讨论过了。

我们回顾下已经修改过的代码。先看attr.c这里有 strcpy(filename,argv[1]);filename[0] = 0; 两个语句。在本篇(中)里已经有strcpy,这里展开讨论下字符串和strcpy操作。

先说strcpy函数,其是标准函数,因此你在参考文献1和参考文献3中都可以找到相关内容。你可以发现,需要 #include <string.h> 这个头。

这里给出一个strcpy 的实现逻辑如下

char *strcpy(char *restrict s1,const char *restrict s2){
    int i;
    for (i = 0;s2[i] != 0;i++){
        *(s1+i) = s2[i];
    }
    s1[i] = 0;
    return s1;
}

以上 restrict涉及到 C99 标准,对于老的 C89 标准,并不支持。如果你想尝试让上述代码在某个C文件里能通过,需要如下操作 gcc -std=gnu99 这里对restrict的引入讨论仅是说一下C99标准如何通过gcc的参数进行打开。而restrict实际是为了指针优化使用。优化部分暂时目前不进行展开讨论。

上面的代码我特意写的很不规范。我想说明,s1,s2都是指针存储空间,用于指向个8位宽空间。这里先不考虑char的符号类型问题,如同我们例子中int仅先讨论其是32位宽。因为诸如在ARM的一些编译器下,char是无符号的,而在X86的一些编译器下,char是有符号的。

对于s1 指向的地址我们可以通过两种方式,进行带偏移量的实际8位宽空间的获取。*(s1+i)和s1[i],他们的操作结果是相同的。你可以如下理解

*(s1+i),为我们指向一个空间,该地址是 s1存储区域里存放的地址值加上 i 个 8位宽地址量。而计算机系统里,相邻两个地址之间有8位宽,也即一个byte(但这个只是通常情况下)。因此你全可以认为是 加上 i。

s1[i],为,我们指向一个空间,该空间是s1存储区里存放的地址所指向空间的针对8位宽位单元的第i个。最终结果没有差异,仅是在计算地址顺序时有不同。

需要注意,这里我始终没有说 *(s1+i)为 s1 存储区里存放的地址值加上i后,所指向的8位宽存储区内的值。因为如下

char a = *(s1+i); 
*(s1+i) = a; 

第一个解释是,我们取一个8位宽的存储区(其地址多少如上面描述)里的值,存放到a这个8位宽的存储区。

第二个解释是,我们将a这个8位宽的存储区里的值,取出,存放到另一个8位宽的存储区里(其地址多少如上面描述)。 对于通常大家描述的变量,其值是取,还是存,得根据代码来判断,而不能直接针对诸如(s1+i)来描述,因此 (s1+i),不妨你全当访问到一个8位宽的存储区而已。

这里展开讨论一下。假设 char *s1 = (char *)1000; (10进制,1000强制转换为8位宽存储空间的指针类型) 那么 s1+7 则为 1007。基于当前编译器下,char 我们认为是8位宽。

而对于 int *s1 = (int*)1000; 那么 s1 + 7 则为 1028.基于当前编译器下,int 我们认为是32位宽。因为一个int的存储空间有32位,所以自然占用4个8位宽,也即4个 bytes。所以相邻两个int之间的地址差了4.因此实际 s1 的地址加上了 7 * 4。但当你明确申请了 s1存储区内的地址是指向int类型的存储区,那么编译器也自然知道,由此编译器会自动对s1+7做好实际74的工作。 而无需你担心 int s1;*(s1+7)是否真的能访问到第7个int宽度的存储区域,(记得一切从0开始,我们存在第0个有效的存储区域,第7个,实际表示第8个。哈)

for循环整体的理解,即我们从s2这个指针所指向的存储区开始,直到s2指向的这个存储区里的值为0,则结束。每次计算完毕,我们将偏移量i进行累加。而循环执行,仅是个复制的过程。也即字符COPY。

而for循环结束后,你会发现,s1[i]被设置了0。这是又C语言对字符串的定义所约束的。其字符串的存储空间最后一个,需要保存为0。你可以在参考文献1的6.4.5中获取字符串的更全面的信息。

那么如果一个指向字符位宽的指针所指向的第一个存储空间中设置为0,这表示什么?表示一个不包含任何有效信息的字符串,也即空字符串。那么 filename[0] = 0; 就可以理解了。

那么这里就要讨论下模块与模块接口的问题。以及MVC的模块化设计的问题。

首先,模块接口大多数情况下,需要包含两类信息,一个是当前外部给予的控制模式及状态信息,例如control_mode , view_mode,而你实际看view和control这两个函数,入口 参数的名称为 flag,我想告诉你,名字是什么不重要,mode 还是flag都行,不过通常flag用于表述状态信息,而mode 用于表述控制模式信息,此处设计尽可能简答,我就两个单词干一类事了。而另一个是模块的数据缓冲。 (这些名称也有别的方式,没有标准的,唯一可做评判对是开发团队内部的习惯)

需要注意并非所以模块都需要有数据缓冲区。而目前的模块数据缓冲是,control模块,有一个文件路径名的字符数组的数据缓冲。用于外部给入文件路径名,而view模块也类似。而目前model模块我们尚为加上数据缓冲区。

同时,不代表一个模块仅有一个数据缓冲区。但不同类型的数据缓冲区的总数也尽可能的少,以降低模块接口的复杂度。 其次,MVC等模块设计方式,有一个思想,把一个整系统,切割成不同的部分,每个部分分别干自己的事情,而每个部分直接不存在相互调用问题,因此他们更可以看作是平级单位。而平级单位之间的调用,由一个上层代码来组织,目前最简答啦,用main函数。正常的MVC方式,会用多进程(此处不考虑线程和进程的差异)的方式实现。以实现上述三样工作,能异步执行。异步同步,你可以参考通讯原理的概念。

简单的举例:公检法。公安,检察院,法院。公安就负责抓人,执法,检察院负责侦察和公诉,法院负责判案。那么我们假设,检察院接到举报,说有人贪污,那么检察院就查找一堆资料,OK,可以立案了,把犯罪嫌疑人的资料丢给公安。公安忙,不好意思,等等再抓,当然如果很重要,那么把其他事情放放,立刻抓。抓完了,扔给法院,法院等手上事情处理完,着手审理案件。

MVC类似,不代表,公安是由检察院管理,检察院来调用公安。而同时检察院会告诉公安有事情做咯,这个很重要,需要立刻抓捕,这个属于模块的接口参数,那么实际犯罪嫌疑人资料呢?这个数据数据缓冲区内的操作数据。而对于公安抓到人了。反正望系统里一丢信息,说抓到了。这是接口参数的事情,但人还在公安手上拘留着,未必数据已经丢出去。等法院判了,无罪,那就放了。有罪,那么可能就关押。但也不代表人这个数据要丢给要法院进行看管。

上面的例子说明了MVC等模块设计的一些特点。

  1. 接口是接口,接口用于模块之间状态和模式信息的传递。
  2. 数据是数据,虽然两个模块可能联动,但很有可能数据并没有来回传递。
  3. 没有谁管谁,谁调用谁的。只能说,谁触发了一个事件,理应对于模块进行处理,而对应模块忙完自身工作后,检查是否有事情要做,再做对接。

特别是第3条,导致,接受触发事件的模块,很有可能需要等待一下,而发起者有没时间等,因此接受模块需要开放出一个存储空间,临时放置那些需要处理的数据,如同我们的control中的 filename,和view中的v_param。这就是数据缓冲区存在的价值。

而无论在control还是view的入口函数中,都存在一个模式或状态判断比较的动作。这里用最简单的方式实现,就是一个if,实际设计,通常有多种选择。前面也说了。多模块设计,往往每个模块都始终存在,无非没有事情做时睡大觉,每睡一会,醒来看看工作表,是否有事。那么系统要退出怎么办?其实也简单。我们多设置一个退出状态,在睡了一会后,发现,系统说退休了。。。那么爽歪歪,终于可以放假等死了。。由于本部分只是C语言基础部分,所以我们不展开上述随眠,多进程的设计。在第三部分会进行展开讨论。

而对于模块而言,可能一个大模块有多个C文件组成(原则上因只有一个)。但对外的接口函数应当同意在一个C文件中。而这个模块,恰恰良好的做法是同名.h文件不要被本模块内各个C文件#include,以保证借口封装的干净利索。而非接口函数,因以static前缀开头,这表示是局部函数,C文件以外的代码禁止访问。除了这个好处,static开头的函数在不同C文件中,是可以重名的。C语言模块化编程,外部函数重名,经常会碰到,这和模块切割的关联不大。但和模块的接口,以及开放哪些函数有关联。说到这里,你就记得把view.c里,除了void view(int)以外的函数均加上static。虽然方法太语法原始化了。但这的确就是个封装的动作。模块也需要封装。

上一篇:从 MVC 开始模块化编程(中)

下一篇:从 goto 说起

comments powered by Disqus