开发者社区> 问答> 正文

第6篇 指针数组字符串(下)补充:报错

我们回到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对不上。

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


展开
收起
kun坤 2020-06-08 11:02:03 1665 0
1 条回答
写回答
取消 提交回答
  • 有点没得要领

    其实指针,以及C语言的其他基本类型都一回事儿吧,就是一段内存,再加上对内存使用方法的解释。
    类型转换就是转换一下内存使用的方法。

    只不过指针里面保存的是内存地址而已。抛开一些表面的概念,以底层一点的角度去理解,还算简单的。

    当然具体使用的时候,有什么技巧,什么陷阱,容易弄错的地方,经验还是很有用的。

    ######

    引用来自“zhcosin”的答案

    这些代码不都是 Java 的么,哪儿是 C 语言嘛。
    抽烟给呛着了。。。不知道这算最大的否定,还是最大的肯定。。哈哈。。。。
    ######

    引用来自“刘冲”的答案

    有点没得要领

    其实指针,以及C语言的其他基本类型都一回事儿吧,就是一段内存,再加上对内存使用方法的解释。
    类型转换就是转换一下内存使用的方法。

    只不过指针里面保存的是内存地址而已。抛开一些表面的概念,以底层一点的角度去理解,还算简单的。

    当然具体使用的时候,有什么技巧,什么陷阱,容易弄错的地方,经验还是很有用的。

    你说的没错,我很赞同,前面的篇幅已经讨论过你说的观点了。这边主要是做扩展例子。原本打算独立一篇如何测试的文章,后来觉得干脆放在各个例子里,虽然散列,但给出的信息可能更多些更直观些。
    ######支持老鬼的C说书!!!######又有东西看了,倒杯茶,舒舒服服地品品。######出书了一定买啊~######这些代码不都是 Java 的么,哪儿是 C 语言嘛。
    ######

    引用来自“中山野鬼”的答案

    引用来自“zhcosin”的答案

    这些代码不都是 Java 的么,哪儿是 C 语言嘛。
    抽烟给呛着了。。。不知道这算最大的否定,还是最大的肯定。。哈哈。。。。
    哈哈,
    2020-06-08 15:35:44
    赞同 展开评论 打赏
问答分类:
问答标签:
问答地址:
问答排行榜
最热
最新

相关电子书

更多
低代码开发师(初级)实战教程 立即下载
冬季实战营第三期:MySQL数据库进阶实战 立即下载
阿里巴巴DevOps 最佳实践手册 立即下载