Contents

数组,指针,字符串(下)

有了指针,数组,字符串的概念,总算可以继续说说 MVC了。前面说过。模块化的设计,接口参数针对模式,而数据是通过缓冲来传递的。那么对于MVC 的 C ,control我们也存在缓冲。这里先说一下,control,控制,侠义的控制,如同你的游戏机手柄,或者你的鼠标。但其实还有另外一种控制。例如警察针对嫌疑犯,前者对后者的控制,其实这个控制包含了另一种解释,即,约束和掌握。

因此说,广义的控制,应该是存在主从关系的由主对从的影响力的表现。这话说的有点大和杂了,无非希望你别认为,只有触发和通过你的手才叫控制。当A,使用某种信息,可以影响到时,这其实就是控制。

我们先不说键盘怎么操作,这个很无聊,有两个无聊点: 1. 各种键盘甚至游戏柄都是外设,而不同操作系统下,对这个外设的接口通常有各自的方式。介绍这类问题会把讨论,谈歪了。 2. 很多教科书,如同推销自己的传家宝一样,推销scanf这个函数,那么试想,如果你尝试测试一个软件,需要很多方式的组合,是不是你也手工一个个键盘按一遍?何必呢?把对应的情况,依次传入文件,再有程序读取这些文件,一样。

鬼话:很多设备我们都可以看作流文件,键盘如此,你依次读取的文件内容也可以 。

因此,先说个广义上的控制。这还不是我们傻乎乎的把键盘的按键写到文件里。而是我们读取程序运行的参数。现在重新调整第3篇 MVC模块化编程中,设计到的入口参数(注意,这不是接口参数,而是针对程序的给入参数)。假设我们把 height = 3 width = 5 mode = 1 如上文本保存到名为config_attr这个文件名。我们的control模块的目标就是能正确识别到有3个参数。为了能完成这个目标,我们分析一下需要的基本功能,如下散列(散列的意思是内容没有前后因果逻辑,这比较适合开发小组一开始进行头脑风暴,确认大体的设计任务内容): 1. 我们需要对文本文件进行读取操作。 2. 我们能区分行。 3. 我们能从行中区分单词。 4. 我们能比较单词。 5. 我们能将单词转换为数字。 6. 检测多个单词是否合法 7. 我们能将对应的数字和对应的正确的单词所指向的参数进行存储。 8. 当然我们还能测试。

下面一个工作任务就是对上述工作进行函数名的设计。不过先得听我两句,

鬼话1:有些新手此时会脑袋有点蒙,如果换个任务,我该怎么细分动作?这事,还真没法教你。因为任何一个任务,都可以有不同的细分方式,而不同的任务,也很难有一个套路来细分。这是经验,或者说是阅历,不是什么军规,规则就可以解决的。

鬼话2:书上有的,记得不用背,这是和教学,考试非常相反的思想。有就查,不然你花钱买书做什么。而书上没有的,恰恰是需要你通过反复的实践和练习靠自己沉淀才能获得的。即便是你获得了葵花宝典,认为看了就神功了,我仍然认为,疼痛忍耐力这种神功,还是需要你漫长的忍耐才能练成。

所以,记得多练。我们继续。 read_param_from_file 这个已经存在。目标是把一个文件的内容读取出来。 split_line 目标区分行 split_token 目标区分单词 //比较不用了吧,咱们用strcmp得了。有标准函数不用,是傻瓜。 //单词转数字?其实你可以参考文献1,查查atol之类的函数,对应还有strtol等,你可以对比一下他们的区别,为了统一期间,我们使用atol等函数。 check_grammar 检测每行的数据是否合规 set_param 设置参数 好的,我们开始写代码,先把control.c的代码如下设计

#include <stdio.h>
char filename[1024];
static int split_line(void){
    return 1;
}
static int split_token(void){
    return 1;
}
static int check_grammar(void){
    return 1;
}
static int set_param(void){
    return1;
}
static void read_param_default(void){
    printf("read_param_default func !\n");
    return;
}

static int read_param_from_file(char * filename){
    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;
}

以上是我们设计代码的第一版。这里有几个鬼话要说。

鬼话1:设计、设计,那么就应该对整体任务有个规划,有个逻辑流程,有个切割细分的工作,可能切割成各个小模块,小函数的内部实现你还没有理清楚,但至少,通常你会一次性的设计出几个函数。

鬼话2:对于这些函数,需要一批次写入,先别在意里面怎么写。先把接口框定好。这如同你请美女去吃饭,美女想吃什么,得看心情,所以你通常是先电话预定个位置,而不是把菜都点了。

鬼话3:如果你不清楚这个函数的细节,那么预定位置,我的习惯是返回int ,而入口参数是void,记得默认返回一个错误值。我的习惯是内部函数,错误按照 1来处理,正确按照0来处理。这样可以通过很多非0的返回,来表示不同的错误类型,正确就一种可以了,你的代码能返回更清晰的错误类型,对你的整体模块的稳定性有帮助。

鬼话4:原则上,一个C文件,先写入口函数,如同void control(int flag) 一样,这个入口函数,我的习惯,并不会返回,而其他函数,刚预定位置时,先static。谁知道你吃饭时是否想私密一下,包厢少,先预定咯。而模块(C文件是编译的单位嘛,所以算一个编译器可以看到的最小模块)的返回和入口信息越少,越容易降低模块与模块的耦合性。降低模块的耦合性的意义是,提高模块的适用性,也就是复用。

上面写完,我们发现几个问题。 1. 函数没有加测试点。 2. 函数之间没有流程调用关系。 3. 函数接口不明。

先解决1,2。测试点,我们在model.c中有个define。恩。为什么不考虑让这个define服务大家呢。因此我们现在可以考虑把它放到一个头文件里。这样,不同的C文件都#include这个头文件,岂不方便?放model.h里?难道model.h需要所有C文件使用?或许control.h要争了。凭什么不放它那?谁被引用的多 ,好像谁值钱一样,就是一破头文件,又不是论文。 所以我们干脆,使用一个新的文件如下

#ifndef _DEFINE_ATTR_H_ 
#define _DEFINE_ATTR_H_ 
#define __PRINT_FUNC() do {printf("%s func!\n",__func__);}while(0) 
#endif

保存为define_attr.h. 这个头文件的意思是,只要是我这个attr的模块的C文件,都有可能需要的定义,那么就都放这里。于是乎,你现在需要对,所有包含函数测试打印点的C文件,都#include “define_attr.h"。并对应的将诸如 printf("read_param_from_file func !\n"); 的代码,替换为 _PRINT_FUNC();

为什么我们不使用define.h 。算了,这么好的名字,很容易被别人抢注,何必争呢,多写个_attr也不费事,而且方便以后对不同的C语言对应文件名理解(不是C文件名)。这里只给出 control.c和model.c的代码如下

#include <stdio.h>
#include "define_attr.h"
char filename[1024];
static int split_line(void){
    __PRINT_FUNC();
    return 0;
}
static int split_token(void){
    __PRINT_FUNC();
    return 0;
}
static int check_grammar(void){
    __PRINT_FUNC();
    return 0;
}
static int set_param(void){
    __PRINT_FUNC();
    return 0;
}
static void read_param_default(void){
    __PRINT_FUNC();
    return;
}

static int read_param_from_file(char * filename){
    __PRINT_FUNC();
    return 0;
}

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

model.c的代码如下,此处__PRINT_FUNC()的定义已经被拿掉了。换成了#include "define_attr.h"。

#include <stdio.h>
#include <setjmp.h>
#include "value.h"
#include "define_attr.h"


jmp_buf context_buf;
typedef void F_V_V(void);
static int test = 0;
static void model_init(void);
static void model_done(void);
static void model_exit(void);
#define MODEL_ENTRY_NUM 4
static F_V_V *model_entry[MODEL_ENTRY_NUM] = {model_init,model_done,model_exit,model_exit};

static void model_init(void){
    int t;
    __PRINT_FUNC();
    if ( (t = setjmp(context_buf)) >= 10){
        printf("my god ,i escape! %d\n",t);
        model_exit();
    }
    return;
}
static void model_done(void){
    int i;
    __PRINT_FUNC();
    for (i = 0 ; i < 100; i++){
        printf("touch the dog! \n");
        if (test >= 10){
            printf("the officers come ! run away!\n");
            longjmp(context_buf,test);
        }
        test += 1;
    }
    return;
}

static void model_exit(void){

    
    __PRINT_FUNC();

    return;
}

void model(int status){
    
    status = (status >= MODEL_ENTRY_NUM ) ? MODEL_ENTRY_NUM-1: status;
    model_entry[status]();
 }

编译、连接、运行,看 model_init func! read_param_from_file func! model_done func! 这三行是否存在?不错吧,现在或许你知道.h文件的价值了。一次书写,到处复制。记住,一个头文件被一个C文件#include,则表示头文件的内容被插入到对应的C文件里。那么对应的被多个C文件#include,你可以理解为,复制了N份,每个#include这个头文件的C文件,都被插入了一份。当然你完全可以头文件里#include头文件,含义一样,后者完全的被COPY了一份在前者对应位置。 注意,你在编译时会有4个warning被提示,说虽然写了但没有被使用。那么你尝试,去掉上述4个函数中的某个函数的static,例如int split_token(void),再编译。

啥情况?少了一个警告。而是连接,也试试?还是没有警告说有4个函数。因为外部函数,现在不被使用,不代表不能被其他模块所使用,或许编译好了后,对应的obj文件,被另一个模块调用,连接器又怎么能回绝这种要求呢?既然连接器可能需要,编译器自然不能说,一个没有被调用的外部函数,有问题了。

但内部(局部)函数,static强制后,不好意思,所有可以调用该函数的,必须在同名C文件里,编译器当然可以直接站出来说,啊!!写了不用,太浪费了,你就是做外包,也不能这么骗钱!

鬼话:这是一个方法。什么方法?利用C语言的规则,为你提示你的工作情况。通过编译器的warning,可以提醒你,还有些函数没有被调用过,你小子,是不是忘记这部分工作了。还是那句,你可以不听我野鬼的,因为这些都不是标准,都不是考试题目,都不是教学内容。但,站在我这一边的,就是一句话,解决BUG的最好手段是习惯,良好的设计习惯,思维习惯。

我们先把上面4个warning放一边。没错,野鬼第一次开始容忍warning了?非也,不是容忍,而是时刻提醒我们,还有事情没做完。

鬼话:通常,一个团队,分工设计,最好是当前的新增函数,分配到不同的人中,当天解决他们的逻辑调用。而不是隔夜。你有意见?那你今天的晚饭是不是可以隔夜再吃?当然这不属于本部分的内容。在第四部分,C语言的项目开发管理中,会展开讨论。

现在解决第2个问题。就是上述没有被调用的函数直接调用的流程问题。

我们先看下 split_line,既然是split_line,则我们需要将一个param的文件读取上来,然后再操作。先看个弱弱的做法。至于学院派的那种介绍,我们如何测定一个文件的长度,并根据文件长度,malloc空间,我还是那句

鬼话:malloc,free集中防止。该申请的一次申请够。

因此我们一次性申请4K的BUF,那么如果大于4K的文件长度呢?现在先弱弱的告诉用户,我不伺候。由此我们多出个全局变量。让init_value中,malloc到,在free_value中释放掉。因此value.c的文件修改如下:

#include "value.h"
#include <stdlib.h>
#include "control.h"
#define FREE_S_G_POINT(s,g) do {if (s) {free(s); s = 0; g = 0;} }while (0)
static int init_flag;
static char *s_pmodel = 0;
static char *s_pcontrol_input = 0;
static int init_control(void){
    if (s_pcontrol_input == 0){
        s_pcontrol_input = g_pcontrol_input = (char *)malloc(sizeof(char)*CONTROL_INPUT_SIZE);
        return 1;
    }
    return 0;
}
static void free_control(void){
    FREE_S_G_POINT(s_pcontrol_input,g_pcontrol_input);
}
static int init_model(void){
    if (s_pmodel == 0){

        s_pmodel = g_pmodel = (char *)malloc(sizeof(char)*100);
        
        return 1;
    }
    return 0;
}
static void free_model(void){
    FREE_S_G_POINT(s_pmodel,g_pmodel);
}
static int init_status(void){
    g_status = 1;
    return 1;
}
static void free_status(void){
    return;
}
int init_all(void){
    if (init_flag == 1) {return init_flag;}
    init_flag = 1;
    init_flag = init_flag & init_status();
    init_flag = init_flag & init_model(); <span></span>   
    init_flag = init_flag & init_control();
  return init_flag;
}
int get_init_status(void){
    return init_flag;
}
void free_all(void){
    if (init_flag == 0) {return;}
    init_flag = 0;
    free_control();
 free_model();
    free_status();
    return;
}
int g_status;
char *g_pmodel ;
char *g_pcontrol_input;

对应的value.h 为

#ifndef _VALUE_H_
#define _VALUE_H_
extern int g_status;
// used for model module
extern char *g_pmodel;
//used for control module
extern char *g_pcontrol_input;
int init_all(void);
void free_all(void);
#endif

对应的control.h为

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

这里有几个要注意的。 1. value模块既然为本模块中所有C文件(子模块)服务,那么#include 各个模块的头文件很正常。同时,你将CONTROL_INPUT_SIZE 放到value.c或value.h里定义,显然逻辑上错误。 2. 注意我多了一个#define FREE_S_G_POINT(s,g),这样做是因为不同的函数中间,有相同的代码片,而且逻辑一致,无非是操作的存储空间有差异。虽然你可以写成同一个函数,但你仍然会有 free_model free_control这样的逻辑和必须存在的函数(与init_model init_control函数对应嘛),因此这种代码片,我们可以使用宏。 3. 但对于申请部分我并没有这么做。虽然他们的代码片是相同的。

鬼话:你完全可以不要听我的,但是我仍然拒绝在#define的代码片中,拥有return,或者goto的内容。问我为什么?代码描述逻辑不清楚,只会导致2个结果,设计出错,抓BUG痛苦。反正我不干这事,累狠了。

我们回到control.c文件里。那么我们就可以利用g_pcontrol_input进行读取工作。当然这里有个学院派的做法,就是检测当前文件的长度,毕竟如果这个长度比BUF大,我们得认为不能处理嘛。后面有概念解决掉这个小问题。

鬼话:程序员,会一直和BUG打交道。错误、不足、BUG统称是BUG吧。我没说做斗争,是因为,本身系统设计,人为问题,总有不足,甚至这些不足是开发阶段造成的。例如上面这个典型的例子,我们暂时不支持大于4096的文件长度。什么都尽善尽美了,还有什么后续版本可以做呢?哈。

我们修改control.c的read_param_from_file 函数如下

static int read_param_from_file(char * filename){
    FILE *fp;
    long int file_size;
    size_t read_size;
    __PRINT_FUNC();
    fp = fopen(filename,"rt");
    if (fp == 0) return 1;
    fseek( fp, 0L, SEEK_END );
    file_size = ftell(fp);
    if (file_size >= CONTROL_INPUT_SIZE){
        fclose(fp);
        return 2;
    }
    fseek(fp,0L,SEEK_SET);
    read_size = fread(g_pcontrol_input,sizeof(char),(size_t)file_size,fp);
    printf("file size is %ld,read is %ld !\n",file_size,read_size);
    fclose(fp);
    return 0;
}

这里有几个要讨论的地方 1. 如果你只是替换了上面的代码,你尝试编译,会有很多问题。其实是一些头文件没有加。那么control.c的所有目前所需要的头文件我列表如下,希望你能根据编译的错误,一个个判断,究竟是加哪个头文件,而不是简单的抄我的列表。 * >鬼话: warning的另一个好处就是当你记忆力不行是,提醒你,有哪些逻辑你忘记描述了。#include 某个头文件就是一个逻辑关联嘛。你见到warning ,不指望你如见到亲人那样,但至少要如见到领导那样,重视和感觉亲切。 2. fseek ,ftell的用法,和参数类型,包括返回类型,自己查找参考文献1可以获得。而fread这里的file_size参数为什么要强制转换,其实不这么做,也不会报错,但是养成良好的习惯。 * >鬼话:悄悄的告诉你,实际上,我自己写代码,上述不会引发warning的强制转换,我也会忘了加,通常是在有warning的时候,进行补上。而如果你认为有些warning没必要修改,那么类似可能会引发事故的warning,会淹没在很多warning中。不信你试试,很多BUG,编译器在warning中就已经提示了。 3. 这里增加了一个测试点,printf,打印了文件长度和读取长度。文件有多长不代表一定能读上来,实际你操作内存中,有效数据,是你读取的。你领导答应你这个月额外给个加班费,没到你手上你也认了? 4. fseek(fp,0L,SEEK_SET);是要把文件的指针回到最初的位置。其实我们可以做个宏,我们修改如下

#define GET_FILE_SIZE(size,fp) do {long int pos = ftell(fp); fseek(fp,0L,SEEK_END);size = ftell(fp);fseek(fp,pos,SEEK_SET);}while(0)
static int read_param_from_file(char * filename){
    FILE *fp;
    long int file_size;
    long int read_size;
    __PRINT_FUNC();
    fp = fopen(filename,"rt");
    if (fp == 0) return 1;
    GET_FILE_SIZE(file_size,fp);
    if (file_size >= CONTROL_INPUT_SIZE){
        fclose(fp);
        return 2;
    }
    read_size = fread(g_pcontrol_input,sizeof(char),file_size,fp);
    printf("file size is %ld,read is %ld !\n",file_size,read_size);
    fclose(fp);
    return 0;
}

上面这种做法,可以保证,在你测试过长度后,可以将fp指向原先流文件的位置。

好了。现在数据都在g_pcontorl_input里面了。长度也在read_size里面了。这里有两个问题。先把问题放放,说下一个小细节。

我们使用的空间有CONTROL_INPUT_SIZE个,你管它具体多少个,这就是#define的强大,你可以如同讨论数学问题是,说,这是A,B,C而不用带入任何具体值来描述。那么文件长度如果是正好等于CONTROL_INPUT_SIZE,也可以支持啊。为什么我对错误的判断是>=。

鬼话:我的摔跤的经历告诉我,一句老话,手上有粮,心中不慌,非常正确。习惯性的让你的空间留点,也没坏处。遇到状态位,从16位扩展到32位,你原先预留了1位表示扩展,不是挺好?而这里,对于字符串的BUF,也留个空位,也挺好。哈。

  1. read_size可能要被别人用啊。是的。简单。我们增加一个本C文件内部的存储空间。就是static大头,在文件最上方申请的空间。
  2. 我不守规矩了。在一个局部函数中,竟然使用g_pcontrol_input。应该需要使用局部存储进行引用。

这里对2的问题,我们展开讨论 我们对各种行为习惯,保持坚持,大多数情况下,是应该的,但不是全部情况。如果g_pcontorl_input的目的明确,使用者明确,形而上学的一定要用局部存储空间进行设计,就有点多余。如果我们保证一个全局存储空间的独占性,则可以抵充不坚持习惯带来的不足。

鬼话:就我认为,人的一个很重要的能力,在于抉择。很多情况,不存在最优方案。那么如何权衡利弊,进行决策,这和智商没有关系。和吃过多少亏+记忆力有关系。这里的额外讨论突出重点在此,而不是讨论上述行为习惯的可用不可用问题。

现在修正代码如下:

#include <stdio.h>
#include "define_attr.h"
#include "control.h"
#include "value.h"
#define GET_FILE_SIZE(size,fp) do {long int pos = ftell(fp); fseek(fp,0L,SEEK_END);size = ftell(fp);fseek(fp,pos,SEEK_SET);}while(0)
char filename[1024];
static long int buf_num = 0;
static int split_line(void){
    __PRINT_FUNC();
    return 0;
}
static int split_token(void){
    __PRINT_FUNC();
    return 0;
}
static int check_grammar(void){
    __PRINT_FUNC();
    return 0;
}
static int set_param(void){
    __PRINT_FUNC();
    return 0;
}
static void read_param_default(void){
    __PRINT_FUNC();
    return;
}
static int read_param_from_file(char * filename){
    FILE *fp;
    long int file_size;
    long int read_size;
    __PRINT_FUNC();
    fp = fopen(filename,"rt");
    if (fp == 0) return 1;
    GET_FILE_SIZE(file_size,fp);
    if (file_size >= CONTROL_INPUT_SIZE){
        fclose(fp);
        return 2;
    }
    buf_num = read_size = fread(g_pcontrol_input,sizeof(char),file_size,fp);
    g_pcontrol_input[buf_num] = 0;
    printf("file size is %ld,read is %ld !\n",file_size,read_size);
    fclose(fp);
    return 0;
}

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

这是个完整的control.c代码的改良。需要讨论的是,为什么有了buf_num,仍然有read_size。

鬼话:在你原型设计,不设计算法优化时,每个存储空间均有独立的逻辑描述,反之,每个独立的逻辑描述,均对应独立的存储空间。此处,buf_num是给外部看的,read_size是给打印测试点看的。相信我,这样的代码,绝对比一个存储空间的数据,被不同逻辑描述所对应,要质量高很多。除去算法优化不谈,后者就是在偷工减料。

这里多了个g_pcontrol_input[buf_num] = 0;至少了 if (file_size >= CONTROL_INPUT_SIZE),有啥意义?看后面代码可知道,是为了确保任何情况下,我们的字符串判断不会跑出buf_num所指向的空间。

数据已经都到g_pcontrol_input所指向的空间了。我们对g_pcontorl_input所指向的空间进行行划分。行有几种情况。

\0xa \0xd ,连续两个字符 
\0xd  就它一个 
\0xa 就它一个 

我们要将不同行切割开,不如索性把 \0xa \0xd 都替换成\0x0。这样每行就是一个独立字符串里。岂不是很爽? 那么代码如下

static int split_line(void){
    int i;    

    __PRINT_FUNC();
    i = 0;
    while (i < buf_num){
        if ((g_pcontrol_input[i] == 0xd) || (g_pcontorl_input[i]  == 0xa)){
            g_pcontrol_input[i] = 0;
        }
        i++;
    }
    return 0;
}

我们分离行结束了。确实结束了。就这么简单。但有个问题。有常量出现。0xd ,0xa。

鬼话:相信我,除非是return 仅有 0和1,而且你非常明确,0,1的 含义,否则正常常量均应该使用#define,用一个单词来描述这个常量的含义,便于你后续对已有代码的理解,加快你对新增设计的开发速度,提高新增代码的正确性。

同时还有个引申问题。我们如何确定行呢?不同的系统可是有不同的规定,我们暂且认为,0xa对应的ASCII的换行为标记,同时,我们分离行,和g_pcontrol_input有什么关系,或许以后要使用其他存储空间呢。由此我们调整如下:

#define dNEXT_LINE 0xa
#define dRETURN 0xd    
#define CHECK_LINES(p,i,lines) do {lines += (p[i] == dNEXT_LINE);}while (0)

static int split_line(char *pbuf){
    int i;    
    int lines = 0;
    __PRINT_FUNC();
    i = 0;
        
    while (i < buf_num){
        CHECK_LINES(pbuf,i,lines);
        if ((pbuf[i] == dNEXT_LINE) || (pbuf[i]  == dRETURN)){
            pbuf[i] = 0;
        }
        i++;
    }
    printf("the lines is %d\n",lines);
    return lines;
}

鬼话:通常,和外部打交道的,例如读取文件,其逻辑本身就对资源具备独占性,因此可以直接使用全局存储空间,而内部函数,要保证代码的可复用性,应尽可能的使用局部存储存储空间。这就是为什么在split_line中使用pbuf,在上面读取文件中,使用全局存储空间的原因。 这里需要说明一下,并不是所有的返回都是用0作为正确情况。

鬼话:split_line,这个函数的逻辑,返回为0 ,表示工作错误,错就错了,更关注正确下,实际分解了几行,那么何必纠结。都说了,抉择是个学问,我不介意你坚持return 0是正确,并用其他局部(C文件内可见)存储空间来暂存行数。

那么我们分离单词怎么处理?也用 0 做分割?恐怕不好吧。怎么区分行呢?不用0做分割,也不好吧。后续会有很多分割符的判定逻辑增加进来。怎么办?一个算法优化原则,这里先提出来。即,数据空间换逻辑复杂度。简单打个比方。我们很久没摸狗了。把以前的例子找回来。

if (摸狗次数 >90){ 
    惨烈地, 
} 
if (摸狗次数 > 50){ 
    反抗地 
} 
if (摸狗次数 >= 10){ 
    叫。 
}else{ 
    舒服的哼。 
}

那么我们可以增加100个数组,每个单元里,直接对应不同的内容。例如:

惨烈地反抗地叫。 
反抗地叫 
叫 
舒服的哼。 

const char STR_TABLE[4][16] ={”舒服的哼“,”叫“,"反抗地叫“,"惨烈地反抗地叫"}; 
int str_mode[100] = {0,0,0,0,0,0,0,0,0,1,1.....,2,2,2,2....3,3,3,3}; 

此时可以通过

STR_TABLE[str_mode[摸狗次数]] 

来获取对应字符串。虽然逻辑简单了。没有那么多比较判断,但存储空间大了。但实际使用并没有这么笨的方式。是否还记得我们main函数的参数。 int argc ,char *argv[]

我们再分析下argv,仍然是从右向左读. argv[]是个数组,数组里面的内容是什么? char 啊。很简单。那么指向数组的指针,则是 char (argv)[],因为有()所以把优先级改变了。首先读的是 *argv,这是个指针存储空间,里面存放的内容指向一个数租。 由于当前我们每行识别的信息数量是有限的,例如目前我们只确认3个。那么我们对split_token的处理。

#define CHECK_ALPHA(c) (((c) != dSPACE_KEY) && ((c) != dTAB_KEY))    
static int split_token(char *pline,char *ppos[]){
    int tokens = 0;

    __PRINT_FUNC();
    while (*pline){
        if (CHECK_ALPHA(pline[0])){
            ppos[tokens++] = pline;
                        pline++;
            while (CHECK_ALPHA(pline[0])){
                if (pline[0] == 0){
                    goto LABEL_return_split_token;
                }
                pline++;
            }            
        }
        pline[0] = 0;
        pline++;
    }
    
LABEL_return_split_token:
    printf("the tokens is %d !\n",tokens);    
    return tokens;
}

上面的代码存在goto,当然你可以不使用goto,不过那样的写法,对于while循环内的描述会不清晰。现在描述如下:

  1. 循环检测到0,如果为0 ,则跳转6 这和经过split_line处理后,传入的数据逻辑对应。
  2. 如果检测到是字符,则转3,否则转入 5
  3. 当前位置记录为一个单词的起始位置。
  4. 循环检测当前连续的字符。如果当前存储空间位置为0 则转入6
  5. 设置当前存储空间为0 ,存储空间的指针偏移到下一个位置
  6. 退出

现在我们需要将split_line,split_token连起来,有两种做法。对整个buf先全部做split_line的扫描,我们将所有行对应成字符串,再对每个字符串进行扫描,由此将每个单词对应成字符串。这样的做法有个缺点,每个行都要和外的存储空间来存储字符串的起始地址。而下面的做法是在每次检测到一个完整的行时,就使用split_token。

static int split_line(char *pbuf){
    int i;    
    int lines = 0;
    char *pline = pbuf;
    __PRINT_FUNC();
    i = 0;
        
    while (i < buf_num){

        CHECK_LINES(pbuf,i,lines);
        if ((pbuf[i] == dNEXT_LINE) || (pbuf[i]  == dRETURN)){
            pbuf[i] = 0;
            printf("%s \n",pline);
            split_token(pline,s_ptoken_pos);
            pline = pbuf + i+1;
        }
        i++;
    }
    printf("the lines is %d\n",lines);
    return 0;
}

上面的代码和前面的代码,差异并不是很大。其实你很容易发现一个问题。如果是WIN下的0xd 0xa的模式,split_token会被发现0xd和0xa调用两次。当然第二次,给入split_token的会是个空字符串,为啥?自己分析。 但这样会导致未来s_ptoken_pos出现问题。因此我们采用如下方式实现

static int split_line(char *pbuf){
    int i;    
    int lines = 0;
    char *pline = pbuf;
    __PRINT_FUNC();
    i = 0;
        
    while (i < buf_num){

        CHECK_LINES(pbuf,i,lines);
        if (pbuf[i] == dNEXT_LINE){
            pbuf[i] = 0;
            printf("%s \n",pline);
            split_token(pline,s_ptoken_pos);
            pline = pbuf + i+1;
            
            
        }else  if (pbuf[i]  == dRETURN){
            pbuf[i] = 0;
            pline = pbuf+i+1;
        }
        i++;
    }
    printf("the lines is %d\n",lines);
    return 0;
}

你需要注意, if (pbuf[i] == dRETURN) 仍然存在 pline = pbuf+i+1; ,而不是在 if (pbuf[i] == dNEXT_LINE) 内去尝试各种判断决定。例如可能会有一种写法

if (pbuf[i] == dNEXT_LINE){ 
    pbuf[i] = 0; 
    printf("%s \n",pline); 
    split_token(pline,s_ptoken_pos); 
    if (pbuf[i+1] != dRETURN){ 
        pline = pbuf + i+1; 
    }else{ 
        pline = pbuf + i + 2; 
    } 
}else  if (pbuf[i]  == dRETURN){ 
        pbuf[i] = 0; 
}

这种写法,看似少了点什么,而且对pline集中在dNEXT_LINE中描述,但缺增加了逻辑关联。即当前的字符判断,需要关联下一个,或上一个字符判断,这在设计代码里,是要尽可能回避的。如果 pbuf[i+1] == 0 怎么办?

鬼话:为什么说把简单的事情搞复杂,显得专业,其实就是一个目的,让看似简单的事情,实实在在的分解为更简单的小块进行处理。降低每个步骤或模块以及他们之间的逻辑复杂度。你的代码,尽可能处理为,我面对谁,我就处理谁。而数据之间的逻辑关联能回避就回避,回避不了的虽然需要面对,但也不能多事。

你可以思考一下,为什么是 else if (pbuf[i] == dRETURN) ,这不是逻辑问题?虽然这是个优化问题,但也体现了一个逻辑含义,dNEXT_LINE 和dRETURN是互斥的。针对pbuf内的空间。

在继续的讨论前,我需要说明,这里有个明显的BUG。啥?段出错的BUG。就是传说中的指针跑飞。

鬼话:你问我为什么比较容易看出代码的错误,其实完全是习惯。用习惯去约束代码的设计。从而避免以前摔坑的经历。

哪错了?在split_token里,这一段

while (*pline){
    if (CHECK_ALPHA(pline[0])){
        ppos[tokens++] = pline;
        pline++;
        while (CHECK_ALPHA(pline[0])){
            if (pline[0] == 0){
                goto LABEL_return_split_token;
            }
            pline++;
        }            
    }
    pline[0] = 0;
    pline++;
}

通体没有考虑 ppos的数组大小。其实只要是static的函数,内部逻辑是确认的,当你代码设计完毕,你完全可以根据函数的入口数据来源,来对空间的使用范围进行约会。但上述代码是个例外,上述代码是一类求数量的代码,例如tokens这个存储空间需要保存实际有多少个单词的值。

鬼话:那么当你经验丰富时,就会知道,当一个行,可区分的超过10个单词后,ppos这个数组的访问存储,会出界。从而导致错误。不是吓唬你,通常都是段错误。而且错误点并不是在此,而是由于你对不属于你的空间进行了赋值,影响到另一个函数的运行。你问我什么是经验丰富?在这个讨论范围下,经验丰富就表示,出了很多次错,并记得为什么错。这些都不是书本上可以教的。无非我也仅是给点案例而已。

因此,我们坚持使用 #define ,来杜绝这个问题,有错不可怕,可怕的是不能即时制止。其实很简单,就是找个判断,不过代码设计质量是否良好,有个判断标准就是新的逻辑增加时,对老逻辑的兼容能力,以及代码修改的范围。

#define TOKEN_MAX_NUM 10
static char *s_ptoken_pos[TOKEN_MAX_NUM];
static int split_token(char *pline,char *ppos[]){
    int tokens = 0;

    __PRINT_FUNC();
    while ((*pline) && tokens< TOKEN_MAX_NUM )){
        if (CHECK_ALPHA(pline[0])){
            ppos[tokens++] = pline;
                        pline++;
            while (CHECK_ALPHA(pline[0])){
                if (pline[0] == 0){
                    goto LABEL_return_split_token;
                }
                pline++;
            }            
        }
        pline[0] = 0;
        pline++;
    }
    
LABEL_return_split_token:
    printf("the tokens is %d !\n",tokens);    
    return tokens;
}

ok,这样就行了。我们不考虑算法优化问题,这样是最好的,原型正确后,再考虑算法优化的问题,你可以将(tokens< TOKEN_MAX_NUM )移动到 tokens发生改变的位置。

鬼话:代码在开发的不同阶段,最追不一样。例如你没结婚时,追求的是恋爱,你结婚后,可能追求的是繁衍。如果顺序不对,追求错位,故事很容易变成事故。那么在最初代码阶段,力求是逻辑的清晰、准确的表达,这样可以为代码的逻辑增加和完善,提供方面,模块化设计,包含模块切割和模块内部实现,上述讨论的都是后者。而代码逻辑正确,输入输出框定完毕,你追求的是性能,资源占用量的降低,那是优化的事情,你的逻辑会有精简,映射,替换。不过都是局部的工作。

这里说一下split_token函数的设计过程。此后不再重复,因为实在太占篇幅。 首先是

static int split_token(void){
    __PRINT_FUNC();
    return 0;
}

余下是

static int split_token(char *pline){
    int tokens = 0;
    while (pline){
        pline++;
    }
    return 0;
}

上述步骤是,完成对操作整体的描述,实际方式是通过指针偏移,和退出的判断的描述来实现。再细化,上述的代码先是

static int split_token(void){
    int tokens = 0;
    __PRINT_FUNC();
    return 0;
}

--->

static int split_token(char *pline){
    int tokens = 0;
    __PRINT_FUNC();
    return 0;
}

--->

static int split_token(char *pline){
    int tokens = 0;
    __PRINT_FUNC();
    while (pline){
        
    }
    return 0;
}

--->

static int split_token(char *pline){
    int tokens = 0;
    __PRINT_FUNC();
    while (pline){
        pline++;
    }
    return 0;
}

这样的书写顺序。除了倒数第二部分,是无法做代码测试外,其他都可以做为一个测试断点。

鬼话1:此处的断点,不是在这里加一个标记,用debug让程序可以停在此位置。而是说,一个测试断点,表示可以完整正确编译,链接,运行,且可以输出,供测试。这里包含了正确运行(不是最终逻辑的正确,而是执行的正确),显然倒数第二个,很容易进入死循环。而此处所谓的测试断点,也可以看作你上传代码版本管理器的一个最小分界。

鬼话2:哪怕房子着火,老婆电话,彗星撞地球,这三个必须要挪屁股的事情发生,你也需要把倒数第二步迅速调整到最后一步,存盘。否则,等你第二天上班,发现可以或必须去做什么什么,于是10天半个月后再拿你手上的代码,起步的测试工作都没有办法正确运行结束。

鬼话3:如同程序设计里,有个原子操作一样,代码书写也有个原子操作的概念。上述1,2,都算原子操作,3则不是,一定要和4合起来。但一定一定不能写成如下:

while (pline){ 
    pline++; 
将此为3。 
while (pline){ 
    pline++; 
}

将此为4。

上述做法,别说正确运行了,正确编译都过不去。

我们将上述代码,编译运行。运行如下 ./attr ./config_attr 看看结果。你甚至可以将config_attr 中间增加一行, a b c d e f g h i j k l m n o p q r s t 琢磨琢磨,输出该行单词数量是多少。

现在我们有了每行的单词字符串指针,和数量。那么我们可以进行语法检测了。如下:

static int split_line(char *pbuf){
    int i;    
    int lines = 0;
    char *pline = pbuf;
    __PRINT_FUNC();
    i = 0;
        
    while (i < buf_num){

        CHECK_LINES(pbuf,i,lines);
        if (pbuf[i] == dNEXT_LINE){
            int tokens;
            pbuf[i] = 0;
            printf("%s \n",pline);
            tokens = split_token(pline,s_ptoken_pos);
            printf("%d\n",tokens);
            check_grammar(tokens,s_ptoken_pos);
            pline = pbuf + i+1;
            
            
        }else  if (pbuf[i]  == dRETURN){
            pbuf[i] = 0;
            pline = pbuf+i+1;
        }
        i++;
    }
    printf("the lines is %d\n",lines);
    return 0;
}

static int check_grammar(int argc,char *argv[]){
    int value;
    __PRINT_FUNC();
    if (argc != 3){
        return 1;
    }
    if ((strcmp(argv[0],"height") != 0) ||
         (strcmp(argv[0],"weight") != 0)||
         (strcmp(argv[0],"mode") != 0)){
        return 2;
    }
    if (strcmp(argv[1],"=") != 0){
        return 3;
    }
    value = atoi(argv[2]);
    printf("%s -> %d\n",argv[0],value);
    return 0;
}

你尝试将所有的warning减少到只有一个。即存在一个函数没有被使用。编译链接,运行。看看什么情况? check_grammar并没有输出 printf("%s -> %d\n",argv[0],value);内容。看来我们的逻辑有问题。不怕不怕,这里给个测试方法。修改 check_grammar(tokens,s_ptoken_pos); 为 printf("test check_grammar return %d\n",check_grammar(tokens,s_ptoken_pos)); 然后重新编译链接,执行,你会发现返回了2。

现在知道,return 返回我们定义正确是0,还是正确为非0的重要性了吧。方便以后的实际运行时,对输入内容的检测,也方便现在调试代码。

鬼话:曾经一度非常喜欢IDE下的debug工具。例如VC的F9等。但在经历了大型程序设计时,我彻底否定了IDE的上述测试方法。诸如,需要在运行到某个存储值为104356次被改变后,才会出错。而你需要在104355次时停下来,逐步跟踪代码,而两次之间,又经历了多个函数,多个C文件的来回调用切换,你打算怎么办?一个良好的方法是在release模式下,直接通过增加信息输出的方式,到文件中,进行分析。当然现在的分析内容少,我们仍然输出到屏幕上,但输出内容多到,你不得不再设计个输出内容的判断程序进行自动化定位和分析,你就会喜欢上,把printf定向到指定文件而不是屏幕的方法了。

鬼话2:我挺反感GDB,虽然被很多资深开发人员推崇。除非是涉及指令的逻辑理解,汇编的验证,硬件的正确性,否则我仍然要冒众人的反对意见说GDB只会让你丧失对大系统的测试能力。动态的测试、分析,判断逻辑的能力是一个门槛。坚持release模式下,使用printf的方式,将帮助你跨过去。

鬼话3:动不动通过汇编,指令来判断错误的主,我只能说是黔驴技穷的主。除非他在反向理解执行程序,或者工作的内容之一是判断该芯片是否符合设计规格。对于后者,在算法优化中,我曾经不得不面对,但不希望写C的程序员面对。

现在的问题是,返回了2,恩,看看代码逻辑。显然有错嘛。只要符合任意一个,就行了。怎么能用 ||这个逻辑。改了再试。

你会发现,嘿嘿,height -> 3 和 mode -> 1 打印出来了。

不过还是有问题啊。因为width -> 没有打印出来。自己查代码吧。

首先我们可以判断,是check_grammer没有正确运行,因此你的关注点可以放到这个函数。那么可能有两个原因,输入有问题。其次是内部逻辑有问题。

如果输入有问题,我们在split_line每次都调用了 printf("%s \n",pline);通过打印内容,可以判断表示行切割没问题。由此则输入错误的问题,仅会在split_tokens出现。

你大可以在check_grammer内,或调用前,对tokens个s_ptoken_pos进行打印,观测。 而另一方面,我们看下判断。哈。其实 "weight"写错了。实际应该写为"width"。

虽然错误很明显,但通常出现错误,首先的测试,是将错误发生位置进行缩小,所以你更多的是根据已有输出之间的逻辑不对应,增加信息测试点。在确定足够小的位置时,再进行肉眼扫描。

鬼话:书上永远只教对的。我希望通过有BUG的代码分析,让你明确,测试方法,而不是某种正确的代码。习惯的养成一定是反复的错误处理下形成的。因为你没有错误,那些看似多此一举的习惯或书写风格的价值由何在呢?失败是成功它妈妈,分析错误,是成功它爸爸的一种行为。没有这个行为,就只有妈,也生不出成功这个孩子。不是嘛。

现在有两个问题,我们对参数文件的每行,需要确定是那个参数,被设定了什么值,且不谈一个值不能反复被设定的问题。或者值的范围的问题。同时还有很多常量出现在代码里问题。后者简单,宏,宏,宏。大家一定要喜欢上C里的宏设计方法。

前者,我们需要在check_grammer后,对对应的存储空间(存放参数)进行设置。由此设计set_param,此时我们需要知道,当前字符串指向的是哪个参数,同时当前参数的值。由此代码如下

static int set_param(int mode,int value){
    __PRINT_FUNC();
    switch (mode){
        case HEIGHT_PARAM: s_height_param = value;
        printf("%s -> %d\n",PARAM_STR0,value);
        break;
        case WIDTH_PARAM: s_width_param = value;
         printf("%s -> %d\n",PARAM_STR1,value);
         break;
        case MODE_PARAM:s_mode_param = value;
        printf("%s -> %d\n",PARAM_STR2,value);
         break;
        default:
            printf("mode is error!\n");
    }
    return 0;
}

对应split_token改动如下

static int check_grammar(int argc,char *argv[]){
    int value;
    int mode = -1;
    __PRINT_FUNC();
    if (argc != 3){
        return 1;
    }
    if ((strcmp(argv[0],PARAM_STR0) == 0)){
        mode = 0;
    }else if ((strcmp(argv[0],PARAM_STR1) == 0)){
        mode = 1;
    }else if ((strcmp(argv[0], PARAM_STR2) == 0)){
        mode = 2;
    }else {
        return 2;
    }
    if (strcmp(argv[1],"=") != 0){
        return 3;
    }
    value = atoi(argv[2]);
    set_param(mode,value);
    return 0;
}

新增存储空间和定义如下

#define PARAM_STR0 "height"
#define PARAM_STR1 "width"
#define PARAM_STR2 "mode"
enum{
    HEIGHT_PARAM,
    WIDTH_PARAM,
    MODE_PARAM,
    MAX_PARAM
};
static int s_height_param;
static int s_width_param;
static int s_mode_param;

这里有几个要注意的。

  1. 测试点,被挪动到set_param(int mode,int value)中。
  2. 有了该死的switch。
  3. 我们目前set_param只能支持int的参数
  4. 可能有个易出错的设计,HEIGHT_PARAM等和PARAM_STR0对不上。

这些问题我们放到下一篇介绍。目前咱们已经顺利的完成了对一个文本文件参数的读取工作。我更希望先不要深究代码的性能指令,对于新手,而是多思考,一个任务,如何分解,并逐步实现。

上一篇:数组,指针,字符串(中)

下一篇:任意存储空间结构的设计

comments powered by Disqus