第6章,数组,指针,字符串

中山野鬼 发布于 2012/11/02 03:07
阅读 6K+
收藏 102
    绝大多数执行语句语法其实已经说完了。无非还有些零散的操作以后谈。几乎常用的代码,你都可以实现,不过仅是说语句语法部分,诸如
    i+3
    j < 5
    for(i = 0 ; i < 4 ; i++){
    }
    while (1){
    }
    do {
    }while(0)
    if (){}else{}
    goto
    等等。其实绝大多数的你所能见到的程序,无非有四种东西,就可以实现其全部逻辑了。
    操作,诸如+,- ,* ,/ ,%,>> ,<< 等等。
    比较 , 诸如  < ,> ,!= ,== ,<=,>=等等。
    判断,
    跳转 if (){} else()
    
鬼话:相信我,只要有上面4个部分,你的任何见到的程序逻辑都可以描述了。无非有时会比较笨拙,因此有了for , while 等。不过我们现在还是没办法把MVC继续完善下去。因为存储空间,我们还停留在位宽的概念上,这不够。
    前面提到过指针,指针是C语言里的一个大学问,为了突出这个“大”字,所以我对指针尽可能少用教科书的语言,而是反复的用存储空间的值,和指向两个概念来谈。如下:
    char ****p;
    这是啥意思,咱们需要复读机,按行分,跟着我读,记得从右向左读:
    *p 这是一个存储空间,申请空间的名称为p,存放的是一个指针,这个指针指向一个空间,哪个?下面这个。 (打断下,这里不需要你跟读,我说的是*p,不是char *p;)
    * *p 这是一个存储空间,存放的是一个指针,这个指针指向一个空间,哪个?下面这个。
    * **p 这是一个存储空间,存放的是一个指针,这个指针指向一个空间,哪个?下面这个。

    * ***p 这是一个存储空间,存放的是一个指针,这个指针指向一个空间,哪个?下面这个。

    char ,这个存储空间大家就不用读了吧。

    别怀疑我排版有问题。我可不是混行数的。但注意,只有第一行有 “申请空间的名称为p”。因此这里你需要确保了解2个事实。
    1、char ****p,别数有多少个*就认为这有多少空间。实际只有一个空间,位宽是指针类型,这个空间的名字在代码和编译器看到的,就是p。
    2、存储空间里的内容指向哪并不重要,重要的是指向的空间对应的类型。此处的类型有两层含义,一、是“是否是指针”,二、最终指向的空间是什么类型。
    关于第二点,你可以如此想一下,如同俄罗斯的套娃,一层层的套,总算最终有个娃了,但至少每层模样相同,外面是美女,里面最终的娃也得是一样的美女,不能外面是关羽,里面是项羽。
    为什么要这样,编译器在最终指向实际类型空间时需要检测类型宽度。下面说说怎么利用p。为了不绕口令,我们用3级指针,别搞4级指针了。
鬼话:通常数组将维度,二维,三维,指针我叫做“级”,级联的级。你问我是哪篇英文大作的哪个英文单词的翻译?拜托我野鬼自创的,只要能说明问题,为什么非要翻译英文。顺带说个冷笑话
    问,为什么C的入口函数不是entry,而是,main?
    答一:是因为发明C做实例的家伙,忘记entry怎么拼写了。
    答二:错了,其实是因为main 比entry 少了个字母。
        答三:你们,有没有学问?是叫主函数。而不叫入口函数。 主函数翻译成英文就是main,entry那叫入口,没文化真可怕,国人就这素质!
        这个笑话的意思是说,何必认真于名词,除了老师和形而上学的研究C而不是利用C的。标准和潜规则叫什么就是什么。默默的享受吧。
        回到正题,三级字符类型指针定义就如下:
        char ***ppp;
        对比下,我们前面曾经有过类似的例子
        char *p;
        那么这是一级的,一级的指针p,存放的是个指针,指针指向一个char位宽的空间。那么按照这个位宽顺着数i个char位宽的位置所对应的char 位宽的空间,我们就叫做 p[i],注意,p[i] 和 char a;一样,两个都是个存储空间,不是值也不是变量。
        对应的, ppp[i]是什么呢?一个char 位宽的空间?非也,非也。是一个 类似char **p;的空间。**p是二级的,显然是个存放指针的空间。而对应 ppp[i]则是,ppp这个存储空间里的值所指向的空间,我们顺着数i个指针位宽所对应的指针位宽的空间。
        你不信?那么我们看 ,ppp[0] 和 ppp[1]总表示不同的内容吧。好的,
        ppp[0]的存储空间的地址是什么?就是在 ppp这个存储空间里存放的值。如果ppp[1]对应的空间地址和ppp[0]对应的空间地址的差*8,小于一个指针的位宽(例如32位),那么ppp[0]的存放的数据不就和ppp[1]存放的数据打架了吗?
        那么你也可以想,为什么ppp[1]存储空间的地址,不能大于和 ppp[0]存储空间地址差*8呢?你高富帅,内存买得起。你可以查查发明C时,电脑价格和内存价格。当然,由于电脑确实越来越便宜,特别是内存(实际是外存,CPU外面的),因此从算法优化的角度,在一些特殊情况,例如不是4的倍数,8的倍数的存储结构,扩展到这些倍数,以提高数据获取的效率,这得另谈。
        由此你可以理解,ppp[i] 和  ppp[i+j]两个存储空间差了多少位? sizeof(void *)*8*j ,存储空间的地址差了多少?sizeof(void *)*j
        这里扩展介绍个玩意,sizeof,在参考文献1 6.5.3.4 当然兼顾需要看6.2.3.1,对应 7.19.2中给出了sizeof返回类型,是个size_t 类型。一个无符号的整型。位宽多少,你的自己看咯。
        sizeof是一个操作,和 + , <= 等一样,是个操作(全称是计算操作),特殊的是,他是个个编译器帮你算好的操作。有两种用法,通常如我上面的方式,sizeof(type name),就是()里放个类型。为什么是void *,与其你不记得是个什么指针类型,不如但凡是指针的地址宽度的计算,你都使用void *,一个指向没有意义的空间(其实可能有意义,无非你在看这个指针本身时不需要考虑指向的空间是什么类型和位宽)。
        这里突出强调sizeof的利用,是因为,在C语言里,对于位宽,以及地址偏移等,经常使用sizeof,多写点sizeof不仅体现你的水平,更体现你的品味。
        但需要记得,sizeof返回的是针对地址空间不是位。而计算机的地址,每个地址空间有8位,所以要乘以8。
        那么我们看一下,ppp[0][3] 和 ppp[0][4]这两个存储空间的地址他们之间差了多少?很显然,还是sizeof(void *) ,我特意没有写sizeof(void***)就是希望强调,
鬼话:指针都一样,如果我们不考虑他存储的空间里的值,所指向的空间是什么类型,那么就是指针。信我,工程上方便你思考问题,不信我,信研究派,那你可以出论文。
    但 ppp[0][0][3] 和 ppp[1][1][4],这两存储空间的地址差了多少?问我?回答:不知道!没错,不单单我不知道,编译器也不知到。为什么?我们回顾一下ppp[0]和ppp[1]里放的是什么?
    废话,当然是指针咯。好,我们回顾一下,char *p; 对应的 p[0],p[1],这是两个存储否?是的。是否相邻?显然。什么类型?char型。存什么? 不知到。对了。同样的道理, ppp[0] ,ppp[1],他们是两个存储空间,他们也是相邻,但他们里面存放的值,你并不知到。由此,ppp[0]和ppp[1]里面存放的内容可以完全没联系,甚至相等。那么自然,ppp[0][1] 的空间,应该是 ppp[0]里存储的内容(一个指针)所指向的空间地址(假设是 A)加上 sizeof(void *),,而ppp[1][1]的空间,应该是 ppp[0]所指向的空间地址(假设是 B)加上 sizeof(void *)。
    A和B是不确定的。你怎么敢说 ppp[0][1] 和ppp[1][1]里存的内容的关系是确定的?这不确定,你又怎么敢说 ppp[0][1][3] 和 ppp[1][1][4]的两个存储空间的地址的关系是确定的?
    级联的指针,就有这个问题。不单单有这个问题,还有所指向空间在哪的问题,这个和后面我讲多维数组的差异就非常大。因此我特别喜欢说,char ****pppp;是4级指针,而不是4维指针。
鬼话:哪个老师说后者,你可以大声的带我问他,你写过程序吗?你吃过BUG的苦吗?
    当然,ppp[i][j],甚至ppp[i][j][k]是完全符合编译标准的。不过类似 char *p;我们曾经有过介绍, p[i] 和 *(p+i)的存储空间是完全一样的。注意反复强调
    *(p+i) = 3; 意思是,把3放到 *(p+i)对空间里,该空间的地址是 p这个存储空间里的值所指向的地址+3个char 所对应的空间。
    p[j] = *(p+i),意思是,将*(p+i)的空间里的值取出来,再放到p[j]里的空间。至于这个空间的地址,我就不复读机般的再解释了。但你始终还要知道,*(p+i)不是变量,不是值,而是一个存储空间。*这个取地址空间内容的操作对应的*p,意思是取出p这个存储空间的值(它是个地址),指向另一个 存储空间。
    回到前面说的一个问题,char ***ppp;定义了。那么ppp[0], ppp[1][2],ppp[3][4][5]指向哪?首先,ppp[0]就是个空白支票,你爱写哪写哪。而且和ppp[1]还没关系,因此,你要知道 p[3][4][5]指向哪,你必须用代码明确,ppp[3]指向哪,随后,明确,ppp[3][4]指向哪,最后明确 了ppp[3][4][0] 在哪。
    注意,我可没说一定要明确 ppp[3][4][5]指向哪。为什么呢? 记得,ppp是三级指针,ppp[i][j][0]已经指向了实际存储空间,此时这个空间存放的是char 类型的数据。而不在是个指针。既然我们定义了 char ,这可不是白写的。那么实际已经约束了ppp[i][j][0]和ppp[i][j][1]之间的地址差异。就是sizeof(char)
    那么 char ***ppp;申请的是一个存储空间,ppp[0],ppp[1][2],ppp[3][4][5]这些空间哪呢?显然,编译器不会帮你分配,爱指哪指哪,不过更多情况,你可能需要一个独立的空间来使用并指向。于是乎。。。。
    我要抄起锣,“哐,哐,哐.."的敲无数下,大喊“瞧一瞧,看一看啦。。。”,当然不是看猴子,我也长的不像猴子,而是“隆重“的介绍C语言的两个标准函数,malloc和free,你可以在参考文献7.22.3.4&7.22.3.3 中间找到他们,当然你可以对应把7.22.3都看一下,对比下malloc和其他几个函数的区别。
    之所以如此隆重,是因为,几乎我现在写代码,没有这两玩意,都不知道怎么写了。先说下malloc
    函数接口,也即原型如下:
    void *malloc(size_t size);
    size_t前面介绍过了。那么size 表示空间数量。你可以理解你的酒量。总要有点面子嘛,和你兄弟去喝酒,你可以大喊,小二,来2瓶二锅头,哦,不是2两装的,要一斤的那种,或者,小二,5箱啤酒,记得大支的,12一箱的,或者,小二,给我2立方白开水(如果你实在没酒量)。。。“您这是喝酒呢?还是搓澡呢?”,不怕不怕。你可以很牛的摔两个一元的钢蹦,说“咱有的是钱,买得起内存,只怕你操作系统支持不得,哦错了,怕这小店里没那么大的鱼缸”
    注意两点。
    1、malloc之类,是动态分配内存的。啥意思?程序运行到malloc这个函数,这个函数是标准库实现的,所以有对应的代码,去向操作系统要空间。这个空间是按照堆的方式管理的(堆是啥方式?自己到相关部门索取资料,操作系统的内存管理)。但记得,这个空间是动态获取的,所以也经常叫堆空间。和堆栈空间,一般为了区别,更多叫栈空间不一样,和编译器的数据段的空间也不一样,后两者是编译器分配的。包含在了程序的执行代码里面,程序进入运行,形成进程时,就已经对应存在了。
    2、size是要申请的存储空间大小,但是里面放的内容,通常我们按照单位空间*数量的方式来描述。你问我为什么。我就问你了,为啥你要说大支装的,5箱?谁去酒家要两立方的水喝啊?
    一种学院派的做法如下,我们仍然使用model.c文件。完整如下:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
jmp_buf context_buf;
#if 0
static int test = 0;

static void model_done(void){
    int i;
    printf("param_done func !\n");
    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;
}
#else
static void model_done(void){
    char *p;
    int i,j;
    p = (char *)malloc(sizeof(char)*100);
    for (i = 0 ;  i < 100 ;i++){
        p[i] = i;
    }
    for (i = 0 ; i  < 100 ; i++){
        if (p[i] != i){
            printf("why?\n");
            return ;
        }else{
            j += i;
        }
    }
    printf("the total from 0 to 99 is %d\n",j);
    return;
    
}
#endif


static void model_exit(void){

    
    printf("param_exit func !\n");

    return;
}

void model(int status){
    
    int t;
    int flag = 0;
    do {
        switch (status){
            case 0:
                if ( (t = setjmp(context_buf)) >= 10){
                     printf("my god ,i escape! %d\n",t);
                 }
                break;
            case 1:
                model_done();
                break;
            case 2:
                model_exit();
                break;
            default:
                printf("error ! the status value is illegal!\n");
                status = 2;
                flag = 1;
        }
    }while (flag);
    

     return;

 }

    编译链接,运行,打印出了
    the total i from 0 to 99 is 4950
    计算正确。表示确实至少有100个空间了。否则,肯定有某个空间被重新覆盖了,会导致计算的结果不对。至于是否实际空间有多少是这个连续的块?这事,我不告诉你,因为需要看实际怎么分配内存的,同时,我建议你,别想歪点子,你就认为只有100个空间。
    先注意下两个,
    1、#include <stdlib.h>这个你看标准的标准库的函数时,一定要注意,某个函数属于对应的哪个头文件进行函数接口定义的,否则又有warning,有这个warning一般不会出故事,不过要出,通常都是事故。
    2、p = (char *)malloc(sizeof(char)*100); 中,(char *)是什么意思?注意,malloc的原型是 void *,你不写,也没错,如同我前面说的例子,指针类,sizeof(void *)足以,因为指针类型,本身就是指针,不在乎你指向哪。但这个根据编译器不同,会有不同warning。当然这个warning不会出故事。不过个人建议,加上(char*)。(type)的意思是,将后面的内容,强制转换成type类型。不加,通常会有warning,不过除了两头都是指针外,有时(不是一般咯)会出事故,而不单单是故事。此放后面展开讨论。
    为什么说是学院派。通常学院派的性子都比较急,认为写代码和改作业一样,如果一段时间不出成果就会受不了,你要让他规划个3年开发计划,多半会崩溃,所以空间一拿到,就想用。
    而庸俗的工程派会怎么样呢?malloc后,最多在这个函数里做初始化工作,而用在别的地方用。而且通常一个空间从系统开始就申请,在系统释放时在丢弃。没办法,庸俗啊,好不容易发了点工资,先存着,该花时再花。
    ?这里有个丢弃的动作。对了,这就是free。free函数也很简单。只要告诉它曾经申请到的空间的地址,当然这个地址被一个指针类型的空间存放,那么就可以告诉操作系统,这个指针的空间里存放的值所指向的那个地址,可是你曾经给我那个块的第0个位置,你总该有记录吧,我还给你,不要了。
    工程派这么做有什么好处呢?如下:
    1、任何一个系统都有边界。因为硬件资源是有限的。你不可能无休止的获取资源。所以设计系统时,有一点非常强调,我支持的处理能力,这就对应了,我实际最大能处理的工作任务所需要的存储空间。而如果这个存储空间你一开始不申请足了,真的工作任务来了怎么办?难道说,客户问题,都有哪些服务?你说,怎么样都行,真得开始了,你左一个“哎呀,今天不方便”,右一个,“哎呀,没地了”你让客户怎么想?
    2、如果用时在申请,操作系统虽然大多数时间不是很忙,但将心比心,一个人动不动让你做同样的事情,你烦不烦?你做事情也要成本,操作系统申请空间也要时间,如果你在一个循环里,申请,利用,释放,何苦呢?别以为一个函数里,你不会这么做,经常学院派的高雅写法是在计算过程中,循环调用包括申请,利用,释放的函数。
    3、工程设计,强调一个稳定性,其中包括资源的稳定性。稳定的地址空间,可以为有效的保证计算性能提供一定帮助。
    因此,我们应该如下写,先给出value.h ,value.c的代码
#include "value.h"
#include <stdlib.h>
static int init_flag;
static char *s_pmodel = 0;
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){
    if (s_pmodel){
        free(s_pmodel);
        s_pmodel = 0;
        g_pmodel = 0;
    }
}
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();
    return init_flag;
}
int get_init_status(void){
    return init_flag;
}
void free_all(void){
    if (init_flag == 0) {return;}
    init_flag = 0;
    free_model();
    free_status();
    return;
}
int g_status;
char *g_pmodel ;


value.h的内容如下
#ifndef _VALUE_H_
#define _VALUE_H_
extern int g_status;
extern char *g_pmodel;
int init_all(void);
void free_all(void);
#endif

model.c的 model_done函数修改如下:
#else
static void model_done(void){
    char *p = g_pmodel;
    int i,j;

    for (i = 0 ;  i < 100 ;i++){
        p[i] = i;
    }
    for (i = 0 ; i  < 100 ; i++){
        if (p[i] != i){
            printf("why?\n");
            return ;
        }else{
            j += i;
        }
    }
    printf("the total from 0 to 99 is %d\n",j);
    return;
    
}
#endif

这里先说说value.c里的几个改动。
1、多了  static int init_model(void) 和 static void free_model(void),并分别在init_all()和 free_all里面被调用。
鬼话:一定要养成习惯,涉及malloc申请空间的函数和其对应的释放函数尽可能的靠近。同样,init_model,和free_model是针对模块的,你可以将一个模块的所有动态数据申请,在这里申请个够。
2、切记,申请的函数,和释放的函数,要使用,局部函数定义static来约束。也就是其他C代码不可见。这就是模块封装的一个精神。但和面向对象的所谓私有函数保持举例,咱是面向模块,别串,串了你,哪天你摔粪坑里可别说是挖的。因为free的正确释放需要对一个指针所指向的地址正确的获取,这也是为什么我增加 static char *s_pmodel = 0;的原因,s_pmodel是不会被外部改变的。而g_pmodel可能会被外部改变。
鬼话:或许又要有人说了。你累不累啊,搞两个存储空间,空间不要钱啊,我只想说,设计代码的行为习惯和常用手法,都是我无数次摔坑的经验总结。等哪天你苦命的抓指针的问题时,或许能理解我,另可多要个空间,简单的事情搞复杂,也不图学院派的高雅,潇洒,咱就是一占满臭粪的庸俗野鬼。
3、s_pmodel在定义时设置了0,确保了第一次init动作可以执行空间,且不会s_pmodel不会被连续两次申请,导致第一次申请的空间地址被丢失,从而无法还给操作系统。虽然操作系统在进程退出时,会帮你清理空间,但是写代码要有原则,好借好还。
4、注意     free_model();
    free_status();
    和
    init_flag = init_flag & init_status();
    init_flag = init_flag & init_model();
    的顺序。申请空间,有时不一定都能申请到,那么你可以关闭部分功能模块,而保留系统最基础的服务。这种栈式写法,则有助于在你增加了些辅助信息时,能有效的释放该释放的空间,即便你可以通过一堆的s_pxxx是否不为0,来判断是否释放。但一个行为习惯,最好坚持,否则摇摆不定,我只会送你一句话,骑墙的主,通常某些地方会疼。
5、重复讨论下,g_pmodel,和 s_pmodel,这里建议,通常的指针,前缀p,是否下划杠隔离后面的设计空间名称,看习惯。虽然g_pmodel会因为外部代码的逻辑错误被改写,也导致指针乱飞,内存出错的问题,但是这个问题,和s_pmodel如果错误导致内存出错的问题的性质不一样。你甚至可以不定期的比较g_pmodel 和s_pmodel是否一致,做自我检测,这对抓代码BUG都是有帮助,这在后面BUG定位方法中会展开讨论。

    好了。其他没什么了。value.h里记得加上外部存储空间标记。就是extern char *g_pmodel;
    这就好了。真的好了。或许有人会问,你空间什么时候申请的 ?init_all啊。init_all在系统刚启动,不就折腾了吗?这就是结构化某块化编程的好处。各忙各的事,model你做你的逻辑操作。空间申请的事情别乱操心。
    扩展讨论下,一个好的模块化设计是什么?各有标准。就我的理解如下:
鬼话:好的模块化设计,无非是方便抓BUG,方便加逻辑。对于后者就是,团队一致性的惯性思维,什么动作该什么模块做,那么仅修改该模块就可以搞定。当然,模块内部有子模块,因此也可以说,几个动作组合的业务需求,你可以很容易且非常精确的分散到各个模块中,用最少的代码实现,这就是好模块的设计。这中间有个经验,不是可以教的,是要自己悟的,就是,业务逻辑在模块化设计中的逻辑投影问题,也就是如何拆分一个需求成各个明确的模块内部动作。对了,怎么悟?很简单。和我一样,多摔几次粪坑。
    这里说了一级指针的空间申请。那么对于二级,三级指针呢?我们先回顾下,三级指针。
    char ***ppp;
    这个ppp的空间,有了。编译器帮你分配好了。但是 ppp[0]的空间没有,不妨假设你p里面存放的指针,向指向的空间及其随后的空间可以粗放5个char **pp的内容。那么至少有 5个 指针型。申请完了,再依次对ppp[0]到ppp[4]里的存储内容进行设置,就是再次申请空间5次,分别保存到ppp[0]到ppp[4]里。因此应该有如下逻辑。假设三级指针分别对应了5,6,7的空间大小,注意,是7个char类型的空间。
  
char ***ppp;
    int i,j;
    ppp = (char ***)malloc(sizeof(void *)*5);
    for (i = 0 ; i < 5 ; i++){
        ppp[i] = (char **)malloc(sizeof(void*)*6);
    }
    for (i = 0 ;  i< 5; i++){
        for (j = 0 ; j < 6 ; j++){
            ppp[i][j] = (char *)malloc(sizeof(char)*7);
        }
    }

    此时,对应ppp[1][2]里面存放了一个指针,这个指针指向了一个地址,由该地址开始,连续的7个char 类型的位宽对应的空间被申请到。这个和 char *p = malloc(sizeof(char)*100);类似。
鬼话:不同级的指针不谈,但对于诸如 (type *)malloc开头的,你需要仔细检查是否是sizeof(type)。嘿嘿,其实我个人习惯是
    ppp[i] = (char **)malloc(sizeof(char*)*6);
    ppp[i][j] = (char*)malloc(sizeof(char)*7);
    这样检查起来更方便。
    为什么我仍然坚持告诉你如果是指针类型,管他指向的什么类型都用void *呢。因为上面方法被我淘汰了。因为开发经验的积累和方法的学习,我更喜欢用另一个方法,如下,修改,value.c,例如三级指针,描述一个三维空间(注意我开始使用维度用语,因为说维度,通常可以联想到正交空间,每个维度不相关,因此申请起空间来,是个方方正正的家伙),分别对应5,6,100。
static char ***s_pmodel = 0;
static int init_model(void){

    if (s_pmodel == 0){
#if 1    
        int i;
        char **pp;
        s_pmodel = (char ***)malloc((5 + 5*6)*sizeof(void *) + 5*6*100 *sizeof(100));
        s_pmodel[0] = (char **)s_pmodel + 5;
        for (i = 1 ; i < 5 ; i++){
            s_pmodel[i] = s_pmodel[i-1] + 6;
        }    
        pp = s_pmodel[4] + 6;
        for (i = 0 ; i < 5 * 6 ; i++){
            pp[i] = (char *)(s_pmodel[4] + 6) + 100* i;
        }
        g_pmodel = s_pmodel[0][0];
#else
        s_pmodel = g_pmodel = (char *)malloc(sizeof(char)*100);
#endif        
        return 1;
    }
    return 0;
}

    记得  #if 1的用法,如果你尝试替换一段代码,做临时使用,或者尚未通过测试和决定,它有存在价值,记得#if 起来,确保老代码存在。
    注意上面的函数中几个细节
    1、只有一个malloc,此处把,5个char **类型的存储空间,5*6个char *类型的存储空间,和5*6*100个char 类型的存储空间一次申请够。有啥好处?你看我没有贴出free_model的函数,表示,free_model的函数不需要修改。
    鬼话:而使用前面的例子,记得有几个malloc显示的写出来,那么就应该有几个free也显示的写出来。那么我需要修改两个函数。
    同时,所有申请的空间是连续的。是否物理连续放一边不谈。至少逻辑空间连续,可以更好的确保数据的连续性。这个在算法优化CACHE和内存访问调度策略中会展开讨论。
    2、s_pmodel[0] = (char **)s_pmodel + 5; 为什么加5.我们继续复读机一下。
    s_pmodel此时是一个存储空间,里面存放的内容是个指针,指针指向的存储空间是个二级指针类型,即char **。根据空间要求,这样的有5个,而
    s_pmodel[0]此时是一个存储空间,里面存放的内容是个指针,指针指向的存储空间是个一级指针类型,即char *。根据空间要求,每组这样的空间有6个。
    s_pmodel[0][0]此时是一个存储空间,里面存放的内容是个指针,指针指向的存储空间是个字符类型,即char 。根据空间要求,每组这样的空间有100个。
    所以s_pmodel[0]里面放的是char *,而自身是一个char **的存储空间。这样的空间有5个。自然s_pmodel[0]需要指向char *,我们已经占用了5个,所以要加5。
    有啥不理解的,可以把这些存储空间所在的地址打印出来。一个存储空间所在的地址是什么值?取地址操作啊,&.注意*是取当前地址空间的内容所指向的存储空间。
    这样有什么好处?连续啊。连续有什么好处,除了快点。我还可以明确知道s_pmodel[0][3]和s_pmodel[1][4]的存储空间内的值的差异啊。而且,如果你尝试要对上述三维空间的所有内容做统一的设置,例如设置-1那么你可以如下:
    char *p = s_pmodel[0][0];
    for (i = 0 ; i < 5 * 6 * 100 ; i++){
        p[i] = -1;
    }
    这不比
for (i = 0 ; i  < 5; i++){
        for (j = 0 ; j < 6;  j++){
            for (k = 0 ; k < 100; k++){
                s_pmodel[i][j][k] = -1;        
            }
        }
    }

    清爽多了?
    且慢,三维空间不是5,6,100吗?你这个i明显出界了你最大可以取到 2999的值。野鬼你不学好啊,教大家如何指针溢出,跑飞指针?
    显然不是。注意p的含义,是什么,char *,我们对三维空间的实际char的申请,是统一的,因此此时char *p,p表示一级指针,谁说是三维空间了。我就不能把10瓶2两二锅头倒一个碗里,让你一口喝完啊?
鬼话:编写C语言,一旦设计到指针,你要有个概念,它的含义是什么,它指向的空间大小是什么,如同闭上眼睛,你能想到里家里每个房间的尺寸大小,形状。若问我,我那么大一个系统呢,怕什么?初入一个城市,你会晕,待个10年8春秋的,各个路还不都熟悉了?给你点时间,你会习惯的。
    虽然你完全可以这么写
for (i = 0 ; i < 5 * 6 * 100 ; i++){
        s_pmodel[0][0][i] = -1;
    }

    类型没有任何问题,编译器会很认同,学院派会给出10种证明方式,证明s_pmodel[0][0][i]和p[i]是一会事,但我仍然要说,一个重要的代码设计习惯,对我的团队甚至是要求
鬼话:代码要逻辑正确,而不单单是编译器认可的逻辑,这个逻辑包括业务逻辑,空间逻辑。
    扩展的说下#define 。方法前面已经简答的介绍过了。为什么说他?注意我们代码
s_pmodel = (char ***)malloc((5 + 5*6)*sizeof(void *) + 5*6*100 *sizeof(100));
        s_pmodel[0] = (char **)s_pmodel + 5;
        for (i = 1 ; i < 5 ; i++){
            s_pmodel[i] = s_pmodel[i-1] + 6;
        }    
        pp = s_pmodel[4] + 6;
        for (i = 0 ; i < 5 * 6 ; i++){
            pp[i] = (char *)(s_pmodel[4] + 6) + 100* i;
        }

    如果你的领导,10分钟后,说,不行,我们需要 6*7*180的数据,你改吧。漏了一个地方,例如:
        s_pmodel = (char ***)malloc((6 + 6*7)*sizeof(void *) + 6*7*100 *sizeof(180));
        s_pmodel[0] = (char **)s_pmodel + 6;
        for (i = 1 ; i < 6 ; i++){
            s_pmodel[i] = s_pmodel[i-1] + 7;
        }    
        pp = s_pmodel[4] + 7;
        for (i = 0 ; i < 6 * 7 ; i++){
            pp[i] = (char *)(s_pmodel[4] + 7) + 180* i;
        }        
    相信我,你的代码有很大情况下,会出事故,不单单是故事。你找找哪错了。
    那么如果我们用#define 就很方便了。可以如下写value.h的前半部分
#include "value.h"
#include <stdlib.h>
#define S_PMODEL_3 6
#define S_PMODEL_2 7
#define S_PMODEL_1 180
static int init_flag;
static char ***s_pmodel = 0;
static int init_model(void){

    if (s_pmodel == 0){
#if 1    
        int i;
        char **pp;
        s_pmodel = (char ***)malloc((S_PMODEL_3 + S_PMODEL_3* S_PMODEL_2)*sizeof(void *) +S_PMODEL_3* S_PMODEL_2* S_PMODEL_1 *sizeof(char));
        s_pmodel[0] = (char **)s_pmodel + S_PMODEL_3;
        for (i = 1 ; i < S_PMODEL_3 ; i++){
            s_pmodel[i] = s_pmodel[i-1] +  S_PMODEL_2;
        }    
        pp = s_pmodel[S_PMODEL_3 - 1] +  S_PMODEL_2;
        for (i = 0 ; i < S_PMODEL_3 *  S_PMODEL_2 ; i++){
            pp[i] = (char *)(s_pmodel[S_PMODEL_3 - 1] +  S_PMODEL_2) +  S_PMODEL_1* i;
        }
        g_pmodel = s_pmodel[0][0];
#else

    感觉何如?其实我知道,我带的新人,嘴巴上会说,哦,这样也可以,你真牛。其实心里说,不行了,看得想吐了。吐就吐吧,一堆宏定义,吐吐就习惯了。    
    有啥好处?领导让你改,不行我们得改回去,还是5,6,100这样比较好看。那么你只要改动3个地方如下
#define S_PMODEL_3 5
#define S_PMODEL_2 6
#define S_PMODEL_1 100    
    就OK了。宏定义,有一个最重要的优势,不是文本的便捷替换,而是逻辑的含义明确。对于上述代码中,任何设计到100的,当前都是表示一个逻辑描述含义,就是三维空间的最低维的大小,那么你就应该用宏替换掉。因为,如果你只看到了文本替换的方便,你可能会不理解,为什么我实际代码会如下写
#define S_PMODEL_3 6
#define S_PMODEL_2 7
#define S_PMODEL_1 180
#defein S_PMODEL_P_NUM  (S_PMODEL_2 * S_PMODEL_3)

并且将诸如
    for (i = 0 ; i < S_PMODEL_3 *  S_PMODEL_2 ; i++){
    改成
    for (i = 0 ; i < S_PMODEL_P_NUM ; i++){
    这不是让你吐的程度减轻。而是存在明确的含义,表示1级指针的数量。而S_PMODEL_1表示第0维的空间大小。
    补充一点,需要注意,但凡宏定义中,存在操作,无论什么操作,诸如 * + - 比较,强制自己写上(),诸如
#define TEST(a,b,c,d,e) ((((a * b) + c) >>d) != e)
你写成
#define TEST(a,b,c,d,e) (a * b + c >>d != e)
恩,你可以自信漫漫的认为操作优先级,你考试满分。
鬼话:听我一句劝,好记性不如好习惯,任何操作都按照逻辑要求加上(),比你熟记优先级要有效的多。重点不是优先级问题,重点是非常明确的描述了逻辑关系。代码易读性大大增强。所以说,但凡问我优先级的问题,和详细解释优先级的书,我都不理不睬,纯粹没事挖坑的主,我摔的够多了。不想再摔了。
    对了。你找到上面的错了吗?如果没找到,我这列个答案。是忘了吧
    pp = s_pmodel[4] + 7;
    修改为
    pp = s_pmodel[6 - 1] + 7;
    了。
    又有谁,不知道 6 - 1 == 5呢?可要改的代码一多,有又谁能保证写上5呢?不是你计算功底不行,而是你在一堆1,2,3,4,5中间很难理解都是什么含义。
鬼话:当你看到了宏,你需要看到的不是宏,而是逻辑描述或逻辑关系,当然你要问我,它究竟是什么?废话,还是宏啊。

    未完,字符串和数组还没讲呢。
加载中
3
泡不烂的凉粉
泡不烂的凉粉

浏览器真糟糕. 辛苦打了半天,一个 误操作,让辛苦打的内容全部消失了.

野鬼辛苦了. 不过我还是觉得你的叙述很绕口.
不能很好的说明 数组,指针,以及字符串之间的关系.

为避免犯下之前的错误. 我想在跟帖的时候,多跟几次贴,每次输入一小段,提交成功后再次编辑..

对于 你文中叙述的内容,我觉得切入方式不够好, 切入前奏,还可以更充分点. 深入部分也不必这么早引入动态申请内存以及释放等等这些.

我的叙述方式: 

计算机是按照指令进行操作的. C语言与汇编比算高级语言, 但它仍然很"低级".
计算机操作的都是数字, 差别只是 直接操作与间接操作. C仍然可以用这些概念来了解 数组与指针的关系.
比如: int i; i=0; i++; 这3条语句就是属于直接操作, 操作数是 i, 操作的目标也是i;
 另外一种 int *p; *p=0; (*p)++; 这3条语句属于间接操作,操作数是p,操作目的(*p);
记住一点,计算机操作的都是数字,也就是 i, 和 p 都是数字, 他们都可以用 printf("%d %d",i,p);来看看具体是什么数字. 这里的差别只是i是一个具体的被操作数,而p只是一个指针,例子中改变的不是p这个数,而是p对应的内容. p所对应的内容是一个int 类型. 所以声明 用的是int *p;
另外,(*p)++; 这个括号不可以少, 否则会变成 p这个指针自增,然后取内容.p自增后指向那里.不确定,当然是野指针,会出问题的.后话,以后会讨论, 里面有戏法,这个是C的优点.善加利用会有好处.

居然有人顶我的回帖,这给我增加了信心. 好吧,我会继续完善我的跟帖,帮助与我同一起跑线的朋友一起学习C语言. 这里声明一点,我也是学习C语言的新手. 跟帖内容是我的理解, 并非是正确的解释. 具体正确解释还是请大家听取野鬼的指导文章. 也欢迎给位朋友指出我表述中存在的问题,好帮助我更准确的学习这门语言. 愿我与大家共同进步.

之前表述内容是了解 计算机在处理信息方面采用的方式, 重点还是"一切都是数字". 
接下来说说所有变量都有存储位置. 当然,这个是相对来讲的.程序编译以及优化过程中这些概念不一定合适, 但是,对与学习这门语言来说.还是认为这句话是对的比较好.(虽然这句话本身就是错的).
 继续给例子, int i; 这个语句声明了一个变量i(我也不知道如何说明这个i, 姑且认为他是一个变量的存储空间).  i就表示一个存储空间, 他的大小 足够放的下一个 int 内容. int 是什么? 是一种类型.这些概念之前有讲过, 可以看之前内容加深理解.  这里可以大体上理解为 "宽度" 如果以8位表示一个字节的话, char 类型就是占用一个字节, int 类型占用 sizeof(int) 个字节, sizeof(int) 是什么?  sizeof 是一个操作符. 用来返回 目标 占用的存储空间大小, 以字节为单位表示. sizeof(char) 返回肯定是1. 这个就叫标准. sizeof(int) 会返回什么? 标准中没规定具体数值. 根据编译器以及对编译目标平台来决定. sizeof用来干什么? 备用. 
之后说说 数组.  char a[9];  这个语句就是定义一个数组. 数组名是a,  数组大小是9个char 大小. 占用空间 计算方式为 9*(sizeof(char)). sizeof(char) =1 (这个是规定). 所以 9*sizeof(char) =9; 所以 char a[9]  会占用9个字节的存储空间. char a[8] 会占用8个字节的存储空间. 所以 char a[9] 是用来声明 a 地址内会有 9 个字节的空间可以用以程序使用.  short a[9] 会有 18个字节的空间, 此时a 的大小有 9*sizeof(short) = 18.  sizeof(short) = 2 也是规定.   在这里记住一点, a 也是有数值的. 所有操作都是用来操作数字的. a可以理解为一个 存储空间的首地址. 一切皆为数字.

再来说一下指针与数组的关系. 
 看例子. char a[9]; char *pa; pa=a; 这3条语句比较好理解. 首先 声明了一个 大小为 9*sizeof(char) 的存储空间, 入口地址是 a , 之后声明了一个 存储空间, 存放pa, 它是一个指针.大小是指针的存储空间--> sizeof(void *);  类型不同啊. 管他呢.  指针大小在同一平台下是 不变的.   最后 pa=a ; 意思是将第二条 申请的空间内容 存储成 a 的值. a 是什么?  a其实就是一个数字, 是 数组存储位置的首地址.  下面要说的就是神奇的地方. 指针运算.

变戏法, 指针运算
依然看上面的例子.  a 表示一个长度为9的存储空间, a也是个数字, a也可以表示为9个存储空间的首地址. 由此可以理解为, 数组,是一段连续空间的表示形式. 数组名是空间首地址.
在这里需要引入一个概念, 数组指针, 数组指针是区别与指针数组的. 数组指针实际上是一个指针, 声明的时候会分配一个存储空间给它,分配给他的存储空间长度为 指针的长度, 内容是一段指向连续空间的存储空间首地址.
先来说一下指针,数组之间的魔法. 例子中 pa 是一个指针, 类型是 char . a 是一个数组, 类型是 char 的数组,  pa=a; 后,会发生什么呢, pa 是一个char 类型的指针, 指向的内容类型也就是一个 char, 所以 *pa 就是一个char 类型. 来看下:
char a[9]="9742"; /*声明并赋值*/ 
char *pa;
pa=a;/*这段可以简单写成 char *pa=a; 声明并赋值*/
printf("%c\n", *pa);/* 这段会输出 字符 9 并换行. */
pa++; /* 指针运算,自增 */
printf("%c\n", *pa);  /* 这段会输出字符 7, 并换行*/

其实,上面这段是一个小魔术, 不过这个魔术会在C语言中随处可见. 原因很简单. C代码当中,到处都是指针. 为了代码的优美, 指针遍布各个角落, 这样的魔术也遍布个个角落. 相对于数组, 指针可以通过修改一个数字来操作改变后的内容,相对于数组只能通过下标的不同来修改要方便的.
为加深印象. 查看下面的代码.
 short a[9]; /*声明数组*/
a[0] = 9;
a[1] = 1;
a[2] = 7;
a[3] = 4;
a[4] = 8;
short *pa; pa=a; 
printf("%d\n", *pa);
pa++;
printf("%d\n", *pa);
printf("%d\n", *(pa+1)); 
printf("%d\n", *(pa+2));  
当然,在这里, 也可以用printf("%d\n", *(a+1)); 来实现第二个输出语句所实现的目的. 但是却没办法执行 a++; 这样的操作.  或许会有疑问, 既然可以*(a+1) 为什么还要 *(pa+1);
这就引出更多的内容. tyepdef

我不是万恶之源,我只是一粒沙.我是 typedef
为什么要这么说. 因为我不是让代码变得 凌乱的 罪魁祸首, 我只是一个让一切变得好起来的帮手.我是typedef. 请好好待我.

还是例子:
typedef char ta[5];/* 一个5字节的连续空间 */
int fa() { 
ta a;
return 0;
}
定义了一个函数 fa, 返回类型是 int ,  里面声明了一个变量 a;
a 其实就是一个 数组,  和 char b[5]; 效果是一样的. 这没什么好稀奇的.
使用的时候可以 a[0],a[1],a[2]; 类似的, char b[5]在使用的时候也可以是 b[0],b[1],b[2],b[3]

ta a[3];  a[3] 的效果和 char b[3][5]; 是一样的. 二维数组. 没错.就是他, 可以更简单一些.
使用的时候, 可以 a[0][0]... a[2][4]; 是这样. 那有什么优势呢?  魔术,魔法. 指针是很神奇的东西.



抱歉,我写不下去了. 接下来就是 关于typedef 内部使用的内容. 涉及到 指针访问, 指针传递, 参数, 内存申请,内存释放. 能牵扯到的内容太多了.能引出 指针的精华.可我写不下去了.

野鬼的连载勾起了我的兴趣. 写内容本来是一件简单的事情. 我认为我能更清晰的表述出重点.于是我尝试这么做了. 过程并没那么有趣, 简单的事情做好并容易.  当我尝试写下去的时候.发现有太多内容可写. 显然在这里跟帖是不合适. 所以决定将内容转入自己的博文中.后续内容以博文形式更新. 在此,向辛勤耕耘的 野鬼 表示感谢,感谢你为我们写学习文章. 相信你后出的书一定会成为激发爱好者的利器. 无论从技术上还是从心理上. 

左建光
左建光
不错 看看
f
fsssosei
还没仔细读,光看排版挺清晰。随便扫了两眼,这语言风格我习惯 明天有空继续读
泡不烂的凉粉
泡不烂的凉粉
我写的也很辛苦, 并且还不知道会不会召来野鬼的指责,也不确信有没有人喜欢我写的内容. 我还要不要继续写下去?给点动力. 给点支持. 你们的支持才是我继续下去的动力.
0
RainJ
RainJ

野鬼这是要逆天啊,这么半夜了来讲C。

身体重要!~~~~~

0
中山野鬼
中山野鬼

引用来自“Quark”的答案

野鬼这是要逆天啊,这么半夜了来讲C。

身体重要!~~~~~

白天忙白天的事情,晚上还要加班干活。写书,每章几乎都要花上我7到9个小时。。。。
langrey
langrey
鬼哥,记得亲情价
navyblue
navyblue
佩服!
loki_lan
loki_lan
啥时候出版了我第一个去买
稻草鸟人
稻草鸟人
能便宜卖不~~^_^
皮总
皮总
出版了我们一定支持鬼哥
0
朱__朱
朱__朱
野鬼大哥辛苦了
0
景德真人
景德真人
搬个板凳,细细品尝。
0
一刀
一刀
野鬼果然是野鬼,都是神经半夜起来发表的,,惊悚啊~~
0
棋有此理
棋有此理
"野鬼"经常是在半夜神出鬼没的,符合鬼的名号.我工作四年多都用C,还没有你理解的深刻!惭愧.
polly
polly
@中山野鬼 @中山野鬼 这篇连载有楼梯吗?上不来下不去,也没放博客里。关注一下。
中山野鬼
中山野鬼
回复 @恋恋美食 : 哈。不惭愧不惭愧。。。。
恋恋美食
恋恋美食
@中山野鬼 这你怎么看,好像把你当刚学C的小朋友了
0
铂金蛋蛋
铂金蛋蛋
常事儿~注意你好久了!
0
linuxhunter
linuxhunter
兄弟,注意身体呀。
0
byhard
byhard
支持,牛人就是这样炼成的啊。
返回顶部
顶部