开发者社区> 问答> 正文

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


    有了指针,数组,字符串的概念,总算可以继续说说 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、检测多个单词是否合法
    6、我们能将对应的数字和对应的正确的单词所指向的参数进行存储。
    当然我们还能测试。
    下面一个工作任务就是对上述工作进行函数名的设计。不过先得听我两句,
鬼话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();       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痛苦。反正我不干这事,累狠了。

未完,待续。。。。

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

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

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

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

    2020-06-11 15:24:11
    赞同 展开评论 打赏
问答标签:
问答地址:
问答排行榜
最热
最新

相关电子书

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