手把手教你用C写游程编码

来源:互联网 发布:eclipse加载jar包源码 编辑:程序博客网 时间:2024/05/15 20:36

手把手教你用C写游程编码


(原创作品,作者Shawn, 转载请声明)

相信大家对游程编码的概念不会陌生。如果用C语言亲自实现一遍游程编码的话,会发现综合运用到了很多C语言的知识和要避免踩到坑!

游程编码:

给定一串数据,如:
0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22
编码后,得到:
0x11,0x01,0x22,0x01,0x11,0x03,0x34,0x02,0x22,0x01

废话少说,立马手把手教你用C写游程编码!这里用到函数分层的思想,即模块化处理:
输入的数据–>用函数处理–>返回输出数据
即数据的处理完全由函数内部解决。

敲黑板划重点!!!
C语言就是玩转内存空间
C语言就是玩转内存空间!!
C语言就是玩转内存空间!!!

因此要时刻谨记和扪心自问三个哲学问题:

1. 要操作的对象是什么?(大小,即数据类型)

2. 分配的内存空间在哪里?(内存的哪个段)

3. 处理后的内存空间要去哪里?(注意指针的坑)


下面展开来说。

要操作的对象是什么?

  • 根据对象的需要,分配相应的数据类型。数据类型可以分为两种:
    1. 内置数据类型:比如,int, float, double.
    2. 自定义数据类型:比如,结构体, 数组(本质是内置数据类型的组合)。结构体适用于不同内置数据类型的组合,例如应用在通信协议的设计;数组适用于也必须用于相同数据类型的组合。

先看原始数据

原始数据是1字节的序列,即大小为1字节的(char)、连续的内存空间。可见,输入数据可使用数组表示:

char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};

如果你愿意更严谨一些,可以表示为

unsigned char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};

以此来向别人表明这些数据是不考虑正负号的。
但其实两种写法都可以,原则上都ok(因为编译器才是老大,它说ok就妥妥的)。
此处为了方便,采用char raw_data[]的写法。


原始数据的内存空间应该分配在哪里呢?

这引出了新的问题,分配的内存在哪几个段(segment)?

内存分布图

具体如下:

地址值 段 备注1 备注2 高地址 stack 栈 动态内存分配 heap 堆 动态内存分配 bss 未初始化全局变量或静态(全局或局部)变量 可读可写* 静态内存分配 data 已初始化全局变量或静态(全局或局部)变量 可读可写* 静态内存分配 低地址 text 代码段+字符串常量 只读 静态内存分配

* 也存在只读变量,此处不展开说明。

此处,我们简单粗暴地把原始数据空间定义在main函数里,也就是说,是在栈空间,因为定义在main函数内部,且没有static修饰符,所以既不是全局变量、也不是静态变量。
详情可参考该链接


再看结果数据:

此处我要用一个名为runLenEncoding的函数(run-length encoding游程编码)来处理输入数据。
那么现在问题来了,在真正调用该函数,传入raw_data数据之前,该函数根本不知道需要为结果数据分配多少内存空间(因为这里假定所有数据处理都在该函数内部,所以不能在进入该函数前就处理数据,从而无法预先得知结果数据空间的大小!)。

好纠结,纠结不如敲几行代码:

#include<stdio.h>void runLenEncoding(void);int main(){    //原始数据空间,这里简单粗暴地放在栈空间即可    char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};    //结果数据空间。应该是啥?    //执行函数,进行游程编码,返回什么给结果数据空间?    result_data=runLengEncoding();    return 0;}

结果数据的内存空间应该分配在哪里呢?

考虑到在runLenEncoding函数中,经过一定数据处理,才能得知结果空间的大小。也就是说,不可能是静态内存分配,函数还没处理,我怎么知道需要为结果数据分配多大空间?
剩下两个动态内存分配:
1. 。还是不靠谱:
- 函数一结束返回,则栈空间的数据就销毁,返回的地址就没有意义了——因为此时该地址的数据已经被系统回收了,不存在了。
- 更何况,类似静态内存分配那样,在编写代码时就需要确定空间的大小,因而同样无法满足函数需要动态增加内存的需求
2. 。妥妥的。堆具有两个优势:
- 堆空间的生命周期由程序员说了算,由程序员编写代码创造和毁灭。也就是说,通过malloc()函数创造的堆空间,在用了free()函数后,该空间才会被销毁。因此,不存在栈空间的缺点:在函数返回后,栈空间则销毁(数据/资源被系统回收)。
- 堆空间可以不是在编写程序时就规定好的。也就是,可以根据程序实际需要,真正地实现按需分配空间大小

因此,在runLenEncoding函数内部创建堆空间,即结果数据空间,返回该数据空间的首地址。

处理后的内存要去哪里?

main函数用一个result_data指针接受该地址。


妥,继续敲几行。

#include<stdio.h>char* runLenEncoding(const char*);int main(){    //原始数据空间,这里简单粗暴地放在栈空间即可    char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};    //结果数据空间(实质是指向结果数据空间的指针)。初始化指向NULL    char *result_data=NULL;    //调用函数,进行游程编码,返回堆空间(结果数据空间)的首地址给main函数中的结果指针?    result_data=runLenEncoding(raw_data);    //返回指针后,对指针进行检查    if(result_data==NULL){        printf("Get result_data failed!\n");        return -1;    }    return 0;}

打印,检查输入和输出

仔细想想,输出的数据我要打印出来检查一下对不对呀。要操作的对象是数组,脑海里立马是for循环。既然要打印输出数据,那把输入数据也打印吧,正好对比着看,那就写一个check_data函数,两个都可以调用(模块化设计哦!)。

用到了for循环,那条件中的结束标志当然是数组中的数据个数,因此要分别定义两个变量raw_dataresult_data

raw_data可以简单粗暴地求出来:

raw_num=sizeof(raw_data)/sizeof(raw_data[0]);

反向更新

result_data不可以事先确定(因为正如前文所说,假定数据处理都放在runLenEncoding函数中)。所以,只能让runLenEncoding子函数帮帮忙,即为反向更新
result_data首地址作为参数传入runLenEncoding函数参数列表中。

runLenEncoidng(raw_data, &result_num);

就像是main函数拜托runLenEncoding函数:
“儿子啊,爹靠你了。result_data的地址给你了,靠你帮爹处理好,爹不管了,你开心就好。”


代码更新如下:

#include<stdio.h>char* runLenEncoding(const char*, const int*);void check_data(const char*, int);int main(){    //原始数据空间,这里简单粗暴地放在栈空间即可    char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};    //结果数据空间(实质是指向结果数据空间的指针)。初始化指向NULL    char *result_data=NULL;    //原始数据的个数    int raw_num=0;    //结果数据的个数    int result_num=0;    //求出原始数据的个数    raw_num=sizeof(raw_data)/sizeof(raw_data[0]);    printf("raw_num is %d\n",raw_num);    printf("raw_data is \n");    //打印原始数据    check_data(raw_data, raw_num);    //调用函数,进行游程编码,返回堆空间(结果数据空间)的首地址给main函数中的结果指针?    result_data=runLenEncoding(raw_data, &raw_num);    //返回指针后,对指针进行检查    if(result_data==NULL){        printf("Get result_data failed!\n");        return -1;    }    //打印结果数据    printf("result_data is \n");    check_data(result_data, result_num);    return 0;}void check_data(const char* data, int num){    for(int i=0;i<num;i++){        printf("0x%02x\t",data[i]);    }    printf("\nCheck_data complete!\n");}

防止指针越界

仔细想想,传入的是原始数据raw_data的首地址,runLenEncoding函数在函数内部用result指针接收后,如果不知道原始数据空间的长度,胡乱地指向,越界就可怕了——访问并修改了其他变量数据,那还有王法吗?!
于是,应该为runLenEncoding函数的参数列表添加raw_num,以此告诉子函数raw_data的长度。

runLenEncoding(raw_data, raw_num, &result_num);

好,我改改改。

#include<stdio.h>char* runLenEncoding(const char*, int,int*);void check_data(const char*, int);int main(){    //原始数据空间,这里简单粗暴地放在栈空间即可    char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};    //结果数据空间(实质是指向结果数据空间的指针)。初始化指向NULL    char *result_data=NULL;    //原始数据的个数    int raw_num=0;    //结果数据的个数    int result_num=0;    //求出原始数据的个数    raw_num=sizeof(raw_data)/sizeof(raw_data[0]);    printf("raw_num is %d\n",raw_num);    //打印原始数据    printf("raw_data is \n");    check_data(raw_data, raw_num);    //调用函数,进行游程编码,返回堆空间(结果数据空间)的首地址给main函数中的结果指针?    result_data=runLenEncoding(raw_data, raw_num, &result_num);    //返回指针后,对指针进行检查    if(result_data==NULL){        printf("Get result_data failed!\n");        return -1;    }    //打印结果数据    printf("result_data is \n");    check_data(result_data, result_num);    return 0;}char *runLenEncoding(const char* raw_data, int raw_num, int* result_num){//用以指向结果数据空间的指针    char* result=NULL;//返回结果数据空间的首地址    return result;}void check_data(const char* data, int num){    for(int i=0;i<num;i++){        printf("0x%02x\t",data[i]);    }    printf("\nCheck_data complete!\n");

运行一下代码:

raw_num is 8raw_data is 0x11    0x22    0x11    0x11    0x11    0x34    0x34    0x22    Check_data complete!Get result_data failed!

此处Get result_data failed! 是因为runLenEncoding函数返回的reuslt是一个空指针。

程序调试小技巧

那我对runLenEncoding函数内部做一点小修改,作为调试使用,查看目前为止能否完整运行程序的框架

char *runLenEncoding(const char* raw_data, int raw_num, int* result_num){    //char* result=NULL;    //作为调试,任意定义一个6个元素的数组作为结果数据空间    static char result[]={0x11,0x22,0x33,0x44,0x55,0x66};    //反向更新result_num的个数,此处为6    *result_num=6;    return result;}

static修饰符

又多了一个坑:static修饰符。因为是在函数内部定义的、用于调试的result数组变量,而该变量在栈空间,想起前文提到的栈空间(局部变量)在函数结束返回后就销毁了吗?
为填这个坑,于是加static修饰符,让变量成为静态局部变量,则该变量不会被销毁,生命周期是直至程序结束。

指针与*修饰符

//通过子函数result_num指针,反向更新main函数的result_num变量*result_num=6;

指针是一个大坑,足够另外写N篇文章。
参考自《让你不再害怕指针》,链接里面有详细说明。
- 先抛出指针最核心的四个问题
1. 指针的类型
2. 指针所指向的类型
3. 指针的值(指针所指向的地址)
4. 指针本身所占据的地址
-再简单阐释一下*修饰符——
*p,该运算结果多种多样。本质是“p所指向的东西”。
*p的特点是:
1. *p的类型为p所指向的类型;
2. *p所占用的地址是p所指向的地址。
runLenEncoding函数中,result_num是一个指针,则*result_num即为result_num所指向的东西——main函数中的result_num变量
main函数中的result_num等于6.

运行结果为:

raw_num is 8raw_data is 0x11    0x22    0x11    0x11    0x11    0x34    0x34    0x22    Check_data complete!result_data is 0x11    0x22    0x33    0x44    0x55    0x66    Check_data complete!

可见,程序的大体框架可以运行了。把调试的代码注释掉。

输入数据和输出数据都搞定了,接下来是重头戏 ——写runLenEncoding函数,真正实现游程编码的函数。

大家可以看到,目前为止,我只是写了runLenEncoding函数的声明,并没有写它的实现。写代码是应该先写框架,梳理了输入和输出,才能好好着手写实现的函数


游程编码实现函数

再抄一遍runLenEncoding函数的框架:

char *runLenEncoding(const char* raw_data, int raw_num, int* result_num){    char* result=NULL;    return result;}

需要多大结果数据空间

终于不得不面对,要给结果数据分配多大的内存空间的问题。
结果数据空间:我该如何地存在?!
回看游程编码的问题:

给定一串数据,如:
0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22
编码后,得到:
0x11,0x01,0x22,0x01,0x11,0x03,0x34,0x02,0x22,0x01

这个问题等价为:在原始空间中,从第一个元素开始逐个比较计数。像是玩连连看,连续相同的元素则视为只占一个位置,求需要多少个位置?

相同/不相同的比较问题,显然是用if语句处理。
- 涉及到逐个比较,需要定义一个中间变量(此处是tmp);
- 涉及到计数,需要定义一个变量(此处是malloc_num)来数数。
实际上,一个萝卜占两个坑。因此有:

//结果空间中:第一个坑是放原始数据,第二个坑是放相应的原始数据有多少个*result_num= malloc_num*2;

代码如下:

//找出结果数据空间所需大小    char tmp=0;//比较时候用到的中间变量    int malloc_num=1;//至少要占一个位置    tmp=raw[0];//把第一个元素赋给中间变量tmp    for(int i=1;i<raw_num;i++){        if(tmp!=raw[i])//若tmp和下一个元素不相等        {            malloc_num+=1;//则计数变量加1            tmp=raw[i];//更新中间变量        }    }    *result_num=malloc_num*2;    printf("The result_num is %d\n",*result_num);

堆空间与malloc函数

终于可以为结果数据空间分配内存了。malloc一下,马上出发。
使用malloc函数,需要在源代码前加上头文件

#include<stdlib.h>

目测一下malloc函数原型:

//输入参数数据类型为size_t整型,吩咐malloc给老子弄多少个字节的堆空间。//返回该堆空间的首地址//没有指定接收的指针应该怎么读,因此指针接收的时候要根据需要进行强制类型转换void* malloc(size_t);

妥,那我用一个result指针去接收这个地址吧!

char* result=NULL;//强制类型转换为char*类型result=(char *)malloc(*result_num);

释放堆空间

malloc函数和free函数是配套使用的。前文提到,堆空间的生命周期是程序员说了算的,所以要记得使用完堆空间后及时释放,不然它一直占用系统资源,会出现内存泄漏!

//有借有还,再借不难//malloc后,堆空间用完就要free,释放资源//此处放在main函数中对result_data进行check_data函数后,即在main函数的最后,函数返回之前,free(result);

检查指针

每次接受地址,记得检查一下指针指对没有哦!

//malloc函数堆空间动态内存分配    result=(char*)malloc(*result_num);    //检查result结果指针    if(result==NULL){        printf("malloc failed!\n");        return NULL;    }    printf("malloc successfully!\n");

万事具备,只差对结果数据空间的填充啦!


结果数据空间的填充

回看游程编码的问题:

给定一串数据,如:
0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22
编码后,得到:
0x11,0x01,0x22,0x01,0x11,0x03,0x34,0x02,0x22,0x01

与前文找结果数据空间大小类似的,此处同样要对原始数据进行逐个比较计数。不过稍微复杂一些,需要对结果数据空间也要操作。

别怕,有神图护体

具体流程如下所示:
- 把原始空间的第一个元素0x11放进结果空间第一个位置;

result[0]=raw[0];

图0.PNG-11.7kB

  • 把原始空间的第一个元素0x11放进中间变量tmp;
tmp=raw[0];

图1.PNG-13.2kB

  • 原始空间中,第一个元素和第二个元素相同吗?0x11和0x22不相等,则结果空间第二个位置填count;count重新置为1;
//这里需要一个result_offset变量来作为结果数据空间的索引//result_offset=1;初始化为1if(tmp!=raw[i]){    result[result_offset]=count;    //count完成阶段使命后,重新置为1    count=1;    //打辅助的result_offset,在count赋值后,自增+1续一秒    result_offset++;}

图2.PNG-17kB

  • 把0x22放入结果空间第三个位置;
if(tmp!=raw[i]){    result[result_offset]=count;    count=1;    result_offset++;    result[result_offset]=raw[i];    //result_offset完成一次辅助,则自增+1    result_offset++;}

可以改改代码,写得更精简一些:

if(tmp!=raw[i]){    result[result_offset++]=count;    count=1;    result[result_offset++]=raw[i];}

图3.PNG-18.2kB

  • 0x22和下一个元素0x11相等吗?0x22和0x11不相等,则结果空间第四个位置填count;
    图4.PNG-18.3kB

  • 把0x11放入结果空间第五个位置,把0x11放入中间变量tmp;

if(tmp!=raw[i]){    result[result_offset++]=count;    count=1;    result[result_offset++]=raw[i];//有发现前面代码少了什么吗?对,发现两个元素不一样要更新tmp!//tmp用来更新被比较数,用以与下一个元素比较!!!    tmp=raw[i];}

图5.PNG-17.2kB

  • 0x11和下一个元素0x11相等吗? 相等,则count计数加1,此处变为2.
if(tmp!=raw[i]){    result[result_offset++]=count;    count=1;    result[result_offset++]=raw[i];    tmp=raw[i];}//相等就是else, count自增+1else    count++;

图6.PNG-16kB

  • 0x11和下一个元素相等吗?
  • ……
  • 以此类推
//以此类推,怎么能少得了for循环?for(int i=1;i<raw_num;i++){        if(raw[i]!=tmp){            result[result_offset++]=count;            count=1;            result[result_offset++]=raw[i];            tmp=raw[i];        }        else            count++;    }
  • 最后,不要忘了做收尾处理!不要忘了做收尾处理!不要忘了做收尾处理!
//容易被遗忘的收尾处理for(int i=1;i<raw_num;i++){        if(raw[i]!=tmp){            result[result_offset++]=count;            count=1;            result[result_offset++]=raw[i];            tmp=raw[i];        }        else            count++;    }//在for循环退出后,把count中的值赋给result最后一位result[result_offset]=count;

收.PNG-19.3kB


到此为止,已经完成了结果数据空间的填充!

完整的runLenEncoding函数

char *runLenEncoding(const char* raw,int raw_num,int* result_num){    char* result=NULL;    char tmp=0;    int malloc_num=1;    int count=1;    int result_offset=1;//之前用于测试的代码行    //static char result[]={0x11,0x22,0x33,0x44,0x55,0x66};    printf("Start to encode!\n");//找出结果数据空间所需大小    tmp=raw[0];    for(int i=1;i<raw_num;i++){        if(tmp!=raw[i])        {            malloc_num+=1;            tmp=raw[i];        }    }    *result_num=malloc_num*2;    printf("The result_num is %d\n",*result_num);//通过malloc分配结果数据空间    result=(char*)malloc((size_t)*result_num);    //check result pointer    if(result==NULL){        printf("malloc failed!\n");        return NULL;    }    printf("malloc successfully!\n");//结果数据空间的填充    tmp=raw[0];    result[0]=raw[0];    for(int i=1;i<raw_num;i++){        if(raw[i]!=tmp){            result[result_offset++]=count;            count=1;            result[result_offset++]=raw[i];            tmp=raw[i];        }        else            count++;    }//收尾处理    result[result_offset]=count;    printf("Complete encoding!Start to back to main function!\n");//之前用于测试的代码行    //*result_num=6;    return result;}

完整代码

经过锲而不舍的努力,终于码好代码啦!整合起来!

#include<stdio.h>#include<stdlib.h>char* runLenEncoding(const char*, int,int*);void check_data(const char*,const int);int main(){    //原始数据空间,这里简单粗暴地放在栈空间即可    char raw_data[]={0x11,0x22,0x11,0x11,0x11,0x34,0x34,0x22};    //结果数据空间(实质是指向结果数据空间的指针)。初始化指向NULL    char *result_data=NULL;    //原始数据的个数    int raw_num=0;    //结果数据的个数    int result_num=0;    //求出原始数据的个数    raw_num=sizeof(raw_data)/sizeof(raw_data[0]);    printf("raw_num is %d\n",raw_num);    //打印原始数据    printf("raw_data is \n");    check_data(raw_data, raw_num);    //调用函数,进行游程编码,返回堆空间(结果数据空间)的首地址给main函数中的结果指针?    result_data=runLenEncoding(raw_data, raw_num, &result_num);    //返回指针后,对指针进行检查    if(result_data==NULL){        printf("Get result_data failed!\n");        return -1;    }    //打印结果数据    printf("result_data is \n");    check_data(result_data, result_num);    //释放堆空间    free(result);    return 0;}char *runLenEncoding(const char* raw,int raw_num,int* result_num){    char* result=NULL;    char tmp=0;    int malloc_num=1;    int count=1;    int result_offset=1;//之前用于测试的代码行    //static char result[]={0x11,0x22,0x33,0x44,0x55,0x66};    printf("Start to encode!\n");//找出结果数据空间所需大小    tmp=raw[0];    for(int i=1;i<raw_num;i++){        if(tmp!=raw[i])        {            malloc_num+=1;            tmp=raw[i];        }    }    *result_num=malloc_num*2;    printf("The result_num is %d\n",*result_num);//通过malloc分配结果数据空间    result=(char*)malloc((size_t)*result_num);    //check result pointer    if(result==NULL){        printf("malloc failed!\n");        return NULL;    }    printf("malloc successfully!\n");//结果数据空间的填充    tmp=raw[0];    result[0]=raw[0];    for(int i=1;i<raw_num;i++){        if(raw[i]!=tmp){            result[result_offset++]=count;            count=1;            result[result_offset++]=raw[i];            tmp=raw[i];        }        else            count++;    }//收尾处理    result[result_offset]=count;    printf("Complete encoding!Start to back to main function!\n");//之前用于测试的代码行    //*result_num=6;    return result;}void check_data(const char* data,const int num){    for(int i=0;i<num;i++){        printf("0x%02x\t",data[i]);    }    printf("\nCheck_data complete!\n");}

运行结果如下:

raw_num is 8raw_data is 0x11    0x22    0x11    0x11    0x11    0x34    0x34    0x22    Check_data complete!Start to encode!The result_num is 10malloc successfully!Complete encoding!Start to back to main function!result_data is 0x11    0x01    0x22    0x01    0x11    0x03    0x34    0x02    0x22    0x01    Check_data complete!

妥妥的!


printf函数

在我的代码中,加入了很多printf()函数,目的是为了方便调试debug。有这样的习惯和意识,方便debug时候快速定位bug在哪里!
等代码调试完全ok后,把不必要的、调试用的printf()和其他函数、注释删除即可。


手把手教你用C写游程编码。作为菜鸟的我也只能帮到这儿了。别看那么多了,代码敲起来才是王道!!!代码敲起来才是王道!!!代码敲起来才是王道!!!

0 0
原创粉丝点击