Contents

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

回顾一下上部分的设计目标如下:

  1. 程序存在一个参数,该参数位配置文件文件名。
  2. 当该文件存在时,读取该文件中的配置数据
  3. 当该文件不存在时,创建新文件,使用默认数据,并将默认数据写如对应路径文件名中。

注意,2,3存在一个条件,当不同条件下,会有不同的执行结果。编程中的俗语为“条件判断”。针对不同条件,给出不同的判断结论。随后跳转进入不同的位置进行代码的执行。关于跳转,后续会专门讨论,此处不展开。此处仅讨论“条件判断”。

在for的介绍中,已经有了“条件判断”,i < 100。但条件判断是两件事情。说”判断“前,先说下”比较“,既然是比较自然是二元的。就是存在两个元素进行的操作。

存在二元关系的比较,例如 <,>,<= ,>= ,!= ,==。 而我们比较后,则有个判断。判断怎么描述,其实不需要描述,如同for 的第二部分。此已经包含了判断动作,如同我们前面的写法 for (i = 0 ; i < 3 ;i++)

而通常情况下需要加上(),这是因为有计算优先级的问题,我们后续会展开讨论,有时你并不需要额外加上(),除非可能没有做判断前的对应处理,后面会讨论到。

有一个非常明确的概念,判断并不是只发生在比较中。而是要看其所在的位置。例如 !i,它的结果完全可以判断。这并没有比较过程。 !的意思是非,当i为0时,该操作返回1,当i不为0时,该操作返回0。你完全可以做如下例子

#include <stdio.h>
int main(int argc ,int argv[]){
   int i;
   for (i = 2 ; i  ; i--){
       printf("i = %d, !i = %d\n",i,!i);
   }
   return 0;
}

这个例子告诉我们几个事实 1. i无论是多少。只要不是0,!i 都为0 。 2. 判断和比较没有直接关系。

其原因是因为,对于正常的逻辑描述,程序的执行跳转是通过 Z状态符号位来决定。你可以全当符号位是裁判委员会的成员之一。每个比赛演员结束后,会根据裁判的喜好,打0或打1.而Z符号的意思,是如果这次计算结果是0,则会对该符号为写1,反之为0 。判断他是滚蛋还是继续,完全可以根据刚刚的修改了Z符号位的执行结果来处理,而不需要进行比较。

更准确的说,所有的比较,诸如 i < 3 , i > j 等。在实际机器中,是通过 i - 3 i - j来实现比较。如果我们的C语言是要求条件判断 i < 3,则机器在这个减法后,会通过相关符号位的组合信息来决策,这里不展开,你可以参考一些减法器的设计补充知识。即便是汇编语言,绝大多数也提供,小于跳转,小于等于跳转等直接的汇编指令。但重复强调个事实,比较是比较,判断是判断。否则你不会理解为什么上面for 循环里,独立i也可以做条件判断。

回到设计任务,那么这里针对路径是否能打开有两种方式。由此我们得介绍一下C语言的一种语句,if (){} else{}

鬼话: 1、如果你希望在你写出10000行以上代码的设计时,降低你的BUG数量。那不妨从一开是就听我一句,不要写 诸如 if (i < 3) printf("i < 3 , i = %d\n",i);这样的语句。 请你始终写出 if (i < 3 ){ printf("i < 3 , i = %d\n",i); }else{ } 这样的语句。 else { } 表示条件不成立时,执行的代码。即便这样的代码你并不需要。留着没有害处的。同时哪怕条件中只有一句,也记得和for一样,加上{},记得for的情况,千万别摸一下,成了看一眼。

除非一种情况,在 if 成立下,会跳出当前作用域。例如函数的{},for 的{}。例如 for (i = 0 ; i < 3 ; i++){ if (i < 2) { break; //一种跳出方法,后续介绍 } } 或 int main(int argc,char *argv[]){ if (argc < 2){ return 1; } .... return 0; }

好的。我们现在根据设计要求,把原先代码推导重来。如下:

#include <stdio.h>
void read_param_default(void){
    return;
}
int read_param_from_file(FILE *f){
    return 0;
}
void param_done(void);
int write_param_to_file(FILE *f);
int main(int argc ,char *argv[]){
    FILE *f = 0;
    if (argc < 2){
        printf("please enter the pathname !\n");
        return 1;
    }
    
    if (f = fopen(argv[1],"rt")){
            read_param_from_file(f);
        fclose(f);
    }else{
            read_param_default();
    }
    param_done();
    if (f){
    }else{
        f = fopen(argv[1],"wt");
        write_param_to_file(f);
        fclose(f);
   }        
    return 0;
}
void param_done(void){
     return ;
}
int write_param_to_file(FILE *f){
    return 0;
}

这里有太多需要解释我们依次如下:

1、我们多了两个看不懂的东西,FILE ,和void 。都是类型。 FILE 是什么,在参考文献1,7.21.1.2 中有明确描述。这里强调的是流,以及是一个记录相关BUF的对象,非面向对象的对象。实际是个结构体。而为了有效对流BUF进行控制,通常我们需要的FILE的地址,以方面信息的传递,因此,各个库函数,使用的是 FILE *。

鬼话:这里是个指针,为什么用f,而不是pf,即可以说是历史原因,也可以说是习惯问题。

void的意思是无意义。void的类型本身是无法申请一个空间的。而对于函数的参数而言,其含义即,不存在这样的参数。因此作为入口参数,void则表示,该函数不存在任何输入参数。作为返回参数时,则表示该函数不返回任何值。我们对param_done这个函数,不需要参数,因此使用了void,同时由于返回没有参数,因此只需要 return。

鬼话:甚至,void作为函数返回类型时,你可以不写return;但听句劝,写上不费笔墨。

深入讨论一下,void虽然表示没有意义,但不代表 void * p;不可行。这表示了,我们要申请一个空间,它是个指针,里面的值所指向的空间是没有意义的,这不代表这个值没有意义,自然p这个存储空间也有其价值。如同一个盒子里放了个望远镜,至于你用它看哪,现在并不重要。这个物品是你关心的。

关于文件,初学者第一个反应是在磁盘上。而磁盘实际是一个外部设备。你要想使用,需要通过操作系统将该设备的对应位置的内容传递到内存中。此时在内存中就有了一个流BUF。而这个流BUF以及文件的一些相关信息,会在一个FILE类型的空间内。实际对该流的各种操作,会利用和影响这些信息,因此,我们需要借助库函数,在打开一个文件时,获取这个FILE类型的空间地址,同时在其他文件操作中诸如读取,写入,向对应函数传递这个地址信息。而此时,实际操作的仍然是流BUF。因此,不要怀疑,大多数流BUF的设备,在C里都会使用FILE。毕竟OS帮我们屏蔽了设备之间的差异性。你实际是在一个磁盘文件所对应的内存中的区域进行操作。无非是流方式的操作。你甚至可以把stdout关闭掉。例如

#include <stdio.h>
int main(int argc ,char *argv[]){
    printf("helloworld!\n");
    fclose(stdout);
    printf("helloworld\n");
    return 0;
}

但记得如参考文献 3 12.2所说明的,别这么玩,如果你想重新关闭再打开的话。说这个只是想说明,给显示器文本输出的也被看作流。所以,干脆大家统一一下,只要是流BUF,我们都叫做文件。无非此文件和磁盘文件的文件不是一会事。 文件操作这里出现了两个,fopen 和 fclose。一个是打开,一个关闭,相关介绍在参考文献3里存在。至于哪里,自己尝试找一下。哈。

鬼话:尽可能的保持fopen和 fclose在代码书写的文本里靠在一起。这间接降低里fopen后忘记fclose的风险。尽可能降低fopen和fclose之间的逻辑,也即工作。fopen不到必要时不要打开,fclose一旦发现该文件可以关闭时,尽快关闭。如同我上面的逻辑,如果外部文件能够打开,我们处理读取工作后立刻关闭。哪怕随后到整个程序退出之间的执行时间非常短。这有几个原因。

fopen和fclose发生的尽可能靠近,可以更好的保证你一次完整的打开关闭的动作处理完毕。

外部资源不用即释放。搞事情,就关着门,这样互不干扰。总比你敞开门的要好,你和别人开房间,有敞开门的癖好吗?即不影响别人,也降低别人影响你的可能。例如fopen和fclose执行期间,正好让和一个有缺陷的程序影响到外部文件或f所指向的空间,怎么办?越早关闭文件,越安全。

你会发现 void param_done(void)在main函数前重复了。而且加了个;,并没有{}。这是函数接口声明。如果不写会有警告。但实际你还是可以运行。那么是否这个警告很没有意义呢?其实很有意义。后续我们会给一个例子,让你知道一个warning 引发的错误。

char done(char a,char b,char c){
    if (c != 0){
        return 0;
    }
    return a+b;
    
}

保存到model.c文件。 将如下保存到test.c文件

#include <stdio.h>
int main(int argc,char *argv[]){
    char a = 2;
    char b = 3;
    printf(" %d + %d = %d\n",done(2,3));
    return 0;
}

执行 gcc model.c test.c -o test 此时编译和链接包含了几个动作。对model.c和test.c的分别编译,形成了临时文件model.o和 test.o。随后将这两个临时文件链接为test的可执行文件,最终.o文件会被删除。

gcc在处理多个C文件时,有些警告会不提示,我们可以使用 -Wall来打开所有警告。如下 gcc -Wall model.c test.c -o test 此时你会发现,有警告。但是不代表不能生成执行程序。同时,执行程序一样能运行。可是执行程序却运行不正确。而且这种不正确是随机的。

鬼话:请相信满头绷带的我的话,随机错误是最难DEBUG的。而我见到的随机错误,很多情况是笔误,而这些笔误大多数是代码行文“不规范”造成的。我让他们“规范”通常的反应是,你怎么总把简单的事情搞复杂。

且不谈为什么你知道有3个参数,却只给两个。至少当你如下写代码

#include <stdio.h>
char done(char a,char b,char c);
int main(int argc,char *argv[]){
    char a = 2;
    char b = 3;
    printf(" %d + %d = %d\n",done(2,3));
    return 0;
}

时,在编译时,就不是warning了。而是错误。因为编译器会 1. 先记录已知的各个函数的接口情况,输入输出。 2. 当有调用函数时,会检测此处给入的参数数目和情况是否和已知的相同。如果和已知的不同,会给出错误。 3. 当有调用函数,而目前并没有发现相同名的函数接口情况,则采用当前书写的情况进行实际参数传递。此时并不知道model里需要3个参数。 注意,C语言,编译是以每个C文件为独立对象的。因此我们可以将每个C文件看做最小的模块。跨C文件的内容在编译阶段是相互不可知的。 4. 当我们model.o test.o都存在了(临时文件上述命令处理完后会删除),使用链接器进行链接。发现main中有个名字为done的函数,则通过各个模块的函数名的表发现model.o里有这个函数。于是main对应调用的位置,就开始修正此处跳转的地址,指向model对应的函数所在的代码段。完成函数调用的实际跳转指向问题。

或许你会问,为什么链接时不能修正呢?链接修正已经没有意义了。函数的参数在传递时,会有一定指令的调整,而所有指令的确定,是在编译阶段。除非链接器在查找可以查到的各个库里发现不了done这个函数名,会以“无法找到函数”的提示报错。否则仍然会给你链接。

这里也引发了一个函数重名的问题。在C语言里面,默认所有函数都是外部函数,(希望你能在参考文献 1中找到相关文字。现在就去查,带着目标的翻阅国际标准,比把国际标准当公式表一样背记有效的多。)我们会在讨论static中展开此讨论。

为什么说,上述不正确会随机发生。

虽然char c没有被赋值,但是model.c里的这个函数在编译时,并不知道是否别人正确调用,因此,指令会机械的从编译时规划好的存储空间取值出来。如果这个值恰好不为0 ,则结果正确。如果恰好为0 ,结果则不正确。

这种错误和环境情况有关,有时开发阶段环境很宽松,你则发现不了这个错误。而实际再别的情况下运行则会出错。其实这个错误的本质是没有把参数正确的传递到位,你如果不打开 -Wall选项,如果打开了。却不关注warning ,那么你只有慢慢抓BUG。其实你为什么就不能写好接口函数申明呢?

而另一种情况引发的错误是经常见的。model.c修改为:

char done(char *p,char b){
    
    return p[b];
    
}

test.c不变。

此时运行会有段错误。其实就是指针跑飞的错误。因为你传递给p的值是 2 , 2 这个地址不是你能用的,你访问了一个你不该访问的地址,自然有段错误。

因此,再次重复,良好的编程习惯,杜绝warning,是降低BUG最有效的手段。不要因为能编译链接通过就以为偷懒可以通过。

回到前面的代码,如果我们去掉 void param_done(void); 这句;在仅是在main函数后面就有实现。此时也会产生警告。其原因在于,首先调用param_done的位置,编译器因为到目前为止找不到接口声明(编译器从上到下识别我们的代码文本),就按照默认的方式给出。而等param_done的函数实际实现时与前面的接口声明会有冲突。通常对默认接口方式,入口参数,根据调用位置的各个变量的类型来设置,而返回,使用int。

因此,你如果如下写代码,则不会有两个关于param_done的warning。

int param_done(void){
    return 0;
}

因为这恰好和编译器使用的默认接口一致。此处的例子,你不使用 -Wall 也可以发现编译提示的区别。

说完函数声明的重要性后,我们看下整体代码。

通常,根据设计目标。一口气,至少要写完这么多 。也只要写这么多。

鬼话:保持良好习惯。写代码由整体到细节。先保证整体的步骤正确,具体细节,使用空函数保留即可。

为什么是,至少要写这么多?

鬼话:因为由argv[1]可能引发几种情况,所以都先写吧。同时,包含了数据载入,数据处理,输出输出的三个模块的入口所相关的代码片。 现在我们的代码的函数越来越多,并不方便整理。我们按照MVC的思想,新加3个 C文件。分别存放不同模块的函数,这样对于后期改动会更有效,参见后期讨论。因此重新整理下源码

attr.c 如下

#include <stdio.h>
int main(int argc ,char *argv[]){

    FILE *f = 0;
    if (argc < 2){
        printf("please enter the pathname !\n");
        return 1;
    }
    
    if ((f = fopen(argv[1],"rt"))){
            read_param_from_file(f);
        fclose(f);
    }else{
            read_param_default();
    }
        param_done();
        if (f){
        }else{
            f = fopen(argv[1],"wt");
            write_param_to_file(f);
            fclose(f);
        }        
    return 0;
}

model.c

void param_done(void){
    return ;
}

view.c

#include <stdio.h>
int write_param_to_file(FILE *f){
    return 0;
}

control.c

#include <stdio.h>
void read_param_default(void){
    return;
}
int read_param_from_file(FILE *f){
    return 0;
}

然后保存。编译,链接。如下 gcc control.c view.c model.c attr.c -o attr 运行 ./attr 会打印出 please enter the pathname ! 不错。至少目前,这个程序会要求你强制输入一个地址。

但记得我们前面的讨论吗?如果你使用 gcc -Wall control.c view model.c attr.c -o attr 会有一堆warning。

鬼话:相信我,绝大多数warning 是会影响你代码的质量的。而不是有些“老人”说的,大多数warning不影响程序的运行。他们说的没错。仅表示不影响现在的运行。但会更多情况,会在以后爆发出危机。

为了剔除掉warning,我们需要对attr.c进行修正,如下:

#include <stdio.h>

void param_done(void);
void read_param_default(void);
int read_param_from_file(FILE *f);
int write_param_to_file(FILE *f);
int main(int argc ,char *argv[]){

    FILE *f = 0;
    if (argc < 2){
        printf("please enter the pathname !\n");
        return 1;
    }
    
    if ((f = fopen(argv[1],"rt"))){
            read_param_from_file(f);
        fclose(f);
    }else{
            read_param_default();
    }
        param_done();
        if (f){
        }else{
            f = fopen(argv[1],"wt");
            write_param_to_file(f);
            fclose(f);
        }        
    return 0;
}

现在再执行 gcc -Wall control.c view model.c attr.c -o attr 够清爽了吧。

这里补充讨论一下条件判断。如果你打开-Wall,如果

if ((f = fopen(argv[1],"rt")))

如下写

if (f =fopen(argv[1],"rt")){
....
}

会有一个警告。建议你增加一个()。这通常被初级C程序员忽视。因为有时,简单的赋值,在有些硬件设备上,并不会引起Z状态位的变化。此时做判断会不正确。(f = XXX)会保证对f有一次测试工作,测试工作也很简单,就是一个检测是否f位0的工作,这是会改变Z状态位的数值,从而确保判断工作正确的判断了该判断的情况,再次强调。条件比较是比较,不是判断,条件判断是两个动作,这个理解将有利于你写出更健壮的C代码程序。一个典型的应用就是记得多加(),以后会展开讨论。

那么我们尝试输入一个存在的文件名,和一个不存在的文件名。比如 ./attr ./attr.c ./attr ./attr.c1 都是没有任何结果。我们不知道是否某个分支被跑到了。因此我们需要增加一些测试代码,日后会成为日志添加点,日志会在后续文章中展开讨论。

在view.c model.c control.c的每个函数里增加如下内容 printf("the XX func !\n"); XX表示当前的函数名。例如model.c修改如下

#include <stdio.h>
void param_done(void){
    printf("the param_done func!\n");
    return ;
}

其他两个文件类似每个函数增加,不再展开。编译链接,并执行。 ./attr ./attr.c ./attr ./attr.c1 怎么样,代码确实运行了。

鬼话:写代码,先明确结构,而不要考虑细节。先确保整体框架的大流程正确。而不要考虑实际每个模块里的数据。

鬼话:我们确定了大框架的正确,在考虑模块与模块之间的数据关联,而不是模块内部的细节,除非你的整体框架和大多数模块都已经设计测试完毕,仅是对某个模块的细化设计。

那么现在我们要做的就是先打通各个模块之间的数据关联。需要明确,这种数据,并不是说那些函数的参数。

鬼话:模块在C里面并不是一定需要(通常是不需要)通过函数的调用来实现。例如这个例子,我们存在三个模块,但仅是main函数启动调用。

如果模块之间确实存在数据联系,而数据量有很大的时候,你需要通过main来转运,不是个好方法。推荐几种方式

  1. 模块入口函数的参数指针或存储区域(变量),指针用于指向需要传递的数据地址。例如上面已经有的 FILE *f;
  2. 全局指针或存储空间(变量)。本模块拥有,由外部修改并使用。
  3. 不隶属于任何模块的一个中间缓冲区,每个模块都可以通过指针访问到它。

我们将上面的代码修改一下。在修改之前,我们讨论下MVC。

model.c内的代码好理解,这里未来将处理所有读取上来的数据。

contorl.c的代码也好理解。这里将外部的控制的获取,看作参数文件的读取

view.c这里的代码实际上存在理解错误。错误点并不在于int write_param_to_file(FILE *f)这个函数本身。我们完全可以把f在write_param_to_file 里当作stdout,显示在屏幕上。错误在于main函数的代码,容易让别人将write的文件和read的文件对应起来,从逻辑上认为他们是一个文件。本质上,他们并没有直接关联。只是设计任务认为,当输入参数的文件不存在时,需要将默认参数输出到指定文件中。这是view 的部分,但更多view 的部分应该是实际param_done中处理的实际数据的结果的输出。 虽然上述代码特意在main函数read_param_from_file(f);立刻关闭,并在param_done后,根据情况重新打开,但除了经验丰富,熟知设计目标和任务的工程师外,都会对上述代码存在歧义的理解。

我们修改attr.c和view.c代码如下:

attr.c文件

#include <stdio.h>
#include <string.h>
void param_done(void);
void read_param_default(void);
int read_param_from_file(FILE *f);
void view(int);
extern char v_param[];
int main(int argc ,char *argv[]){

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

view.c文件如下

#include <stdio.h>
char v_param[1024];
int write_param_to_file(void){
    FILE *f;
    printf("write_param_to_file !\n");
    if ((f =fopen(v_param,"wt"))){
        fclose(f);
    }
        
    return 0;
}

void view(int flag){

    if (flag){
        write_param_to_file();
    }else{
    }
    return;
}

编译链接,运行。 ./attr ./attr.c 这个没错。 ./attr ./attr.c1 这个通常就错了。为什么。你 ls一下。很简单,上次我们运行时,没有attr.c1,而我们的代码就自动创建了这个文件。

rm 掉 ./attr.c1,再运行 ./attr ./attr.c1 ,此时,一切和原先一样。

鬼话:注意,记得,调整代码调用逻辑时,先不要在view内增加 printf("this view func !\n"); 这个测试点。这样方便通过对比前后两次的测试输出是否一致来判断函数调用是否有错误。现在你的测试点少。一眼以阅,随着后续规模的增加,函数调用的测试点,是输出到指定文件的。很多很多,通常是否一致是通过文件比较器来做比较。尽可能的让,输出测试文件只有一致才有可能正确,而不要正确有可能导致输出测试文件不一致。

这里简单说一下数组。回顾一下 char c; 这个空间申请,意思是我申请了一个8位宽的空间。这个空间的地址由c表示。c = 0;含义是向c这个地址所在的(不是所指向)空间内放入0. 那么 char v_param[1024]; 的意思是,我们申请1024个8位宽空间。这就是数组。数组我们后面会有专门文章进行详细讨论。这里看下attr.c中多了个

extern char v_param[];

你可以尝试注释掉这行,编译一下。

没错,编译器不能知道v_param究竟是个什么。函数不知道接口是什么,编译器还可以去猜一猜。如果存储空间不知道什么会引发很多问题,想猜也因为太多可能,会导致各种指令实现方式,所以编译器索性不猜了。直接报错。因此,你必须要使用 extern char v_param[]; 告诉他,这是个数组,是个外部空间。并不在我这个C文件里。至于你是否写 extern char v_param[1024]; 当前这个C文件在编译时,并不在意,你有多少空间和编译器没关系反正不是这个C文件,我不需要分配空间给你。我只是使用。而且你就是告诉我1024,我也没空帮你判断是否读取非法。

我们思考一个问题。 如果view.c是一个已经设计好的大模块。非常完美,完美到主管不会给你C的源代码,而直接给你.o或.a文件(有ar完成对多个.o文件进行归档的一个文件,可以称为库,由于会直接链接到执行文件中,如.o那样,因此也叫静态库,你可以说这是内裤,动态链接的是外裤)

此时,你没有.c文件,你就很难做view函数的接口申明,和v_param的空间类型申明(不是申请)。那么不妨我们把这两个写成头文件,这样,当然用到view.c对应的函数的C文件,只要#include一下就可以。即方便,还防止了笔误等错误。

因此我们修改attr.c如下

#include <stdio.h>
#include <string.h>
void param_done(void);
void read_param_default(void);
int read_param_from_file(FILE *f);
#include "view.h"
int main(int argc ,char *argv[]){

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

注意,这里是 "view.h",表示这不是标准库,是在gcc当前可见的路径下,而非利用环境变量所获取的库路径下。 新增文件view.h文件如下: void view(int); extern char v_param[]; 保存,编译,链接。执行。 编译动作如上。 可能你会有如下动作发生。 gcc -Wall view.h view.c model.c attr.c -o attr 虽然没问题,但没有必要。view.h并不会被gcc直接处理。view.h只会在gcc处理attr.c时,由于有了#include "view.h",才会在gcc可见的目录下进行查找加载,加载后,其内容属于attr.c的一部分。否则。难道你还要写上 gcc -Wall stdio.h string.h ....?

鬼话:这里有个很猪头的想法,就是每个 XX.c文件,一定要包含一个 #include "xx.h"。对于别的语言,或许是要的。但对于C语言,.h通常更多多的是给别人用的。而不是给自己同名的C文件用的。特别是C++程序员转学C是要注意。C++里,对应 .cpp经常要加同名头文件,而C语言里,恰恰不需要。

我们尝试一下笔误。都说我喜欢搞鬼了。我们把view.h修改为

#include "view.h" 
void view(int); 
extern char v_param[];

尝试编译一下。出问题了。说嵌套过多。

实际这对于预处理而言,就是处理#include动作的编译器工作,是个死循环。这种猪头的写法并不常见,但更常见的是,a.h里 #include 了b.h 同时b.h 里也#include 了 a.h。这种交叉#include现象。也是同样会出现死循环。

比较常用的解决方法如下,view.h的全文

#ifndef _VIEW_H_
#define _VIEW_H_
void view(int);
extern char v_param[];
#endif

这里多了三个不认识的东西。后续会展开讨论此处只是简单说一下。

#ifndef 和#endif是成对的。表示 #ifndef 这个判断导致的有效截止位置在 #endif处。 
#ifndef XX的意思是,如果XX被定义了。则不预处理执行下面的代码 
#define XX对XX而言没有什么意义,只是告诉预处理程序(算是编译工作的一部分),定义过XX了。 

那么为什么上面的方式不会出嵌套错呢?我们把弱智写法展开一下

#ifndef _VIEW_H_
#define _VIEW_H_
#include "view.h"
void view(int);
extern char v_param[];
#endif

==>

#ifndef _VIEW_H_
#define _VIEW_H_
#ifndef _VIEW_H_ //此处发现已经定义了。则跳到第8行
#define _VIEW_H_
#include "view.h" //这里的 被跳过了。所以不循环嵌套了。
void view(int);
extern char v_param[];
#endif
void view(int);
extern char v_param[];
#endif

由于#ifndef XXX中,XXX是这么关键,所以一般和该头文件的文件名相关联。你想不出更好的办法时,不如采用我的这个方法。 文件名(大写)后缀名_ 你可以通过类似方法,把control.c 和 attr.c 里的main 进行修改。使得模块接口更为清晰。后续下部分。会继续展开讨论。

这里补充一句:一个良好的程序设计方法,就是让测试工作能高频的执行。你的很少的改动都立刻进行测试,将有效保证你及早的发现代码错误。一个习惯是,每次你的代码改动,除非设计到相关函数的对应调整,否则另一个函数没有启动修改前,当前函数的改动需要测试。而存在相关函数时,要尽可能的将改动方式保证,最多两个函数的改动可以增加测试工作。

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

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

comments powered by Disqus