Huffman编码与解码_C语言实现

实验目的:

1.        掌握熵编码的原理和方法

2.        掌握霍夫曼编码的原理

3.        了解霍夫曼编码的优缺点

4.        掌握和熟悉C


一、背景知识及相关公式

1.熵,又称为“信息熵”(Entropy)

1.1         在信息论中,熵是信息的度量单位。信息论的创始人Shannon在其著作《通信的数学理论》中提出了建立在概率统计模型上的信息度量。他把信息定义为“用来消除不确定性的东西”。

1.2         一般用符号 H 表示,单位是比特。对于任意一个随机变量 X,它的熵定义如下:

1.3         变量的不确定性越大,熵也就越大。换句话说,了解它所需要的信息量也就越大。

2.  Huffman编码

1.4         Huffman Coding (霍夫曼编码)是一种无失真编码的编码方式,Huffman编码是可变字长编码(VLC)的一种。

1.5         Huffman编码基于信源的概率统计模型,它的基本思路是,出现概率大的信源符号编长码,出现概率小的信源符号编短码,从而使平均码长最小。

1.6         在程序实现中常使用一种叫做树的数据结构实现Huffman编码,由它编出的码是即时码。

3.  Huffman 编码的方法

1.7         统计符号的发生概率;

1.8         把频率按从小到大的顺序排列

1.9         每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;

1.10      重复3,直到最后得到和为1的根节点;

1.11      将形成的二叉树的左节点标0,右节点标1,把从最上面的根节点到最下面的叶子节点途中遇到的0,1序列串起来,就得到了各个符号的编码。


二、数据结构

1.huffman树节点

typedef struct huffman_node_tag
{
    unsigned char isLeaf;  //是否是叶节点
    unsigned long count;  //字母出现的频率
    struct huffman_node_tag *parent; //父节点指针
 
    union //联合体:如果是叶节点,则只能有symbol,如果是非叶节点,只能有左右孩子指针
    {
        struct
        {
            struct huffman_node_tag *zero, *one;  //左右孩子指针
        };
        unsigned char symbol; //该节点对应的字母
    };
} huffman_node;

2.huffman码字节点

typedef struct huffman_code_tag
{
    //以位为单位的码字长度
unsigned long numbits;
 
/*码字(二进制):码字的第1位位于bits[0]的第1位;
                  码字的第2位位于bits[0]的第2位
                      ……
                  码字的第8位位于bits[0]的第8位
                  码字的第9位位于bits[1]的第1位 */
    unsigned char *bits;
} huffman_code;


3.输出缓冲结构体

typedef struct buf_cache_tag /*内存编码时,结构体存放输出内存及缓存的指针*/
{
//cache:缓存作用
//如果待存入数据大小合适,则放入*cache;
/*如果待存入数据与*cache中原有数据大小之和超出cache_len,则将原有数据与待存入数据一起放入输出内存*pbufout,最后将*cache内容清空*/
unsigned char *cache;
 
//缓存区*cache的大小,本程序将其设为1024字节
unsigned int cache_len;
 
//缓冲区*cache当前已缓存数据的大小(当前已缓存大小)
unsigned int cache_cur;
 
//最终所有输出数据存放的内存区域,即输出内存的二级指针
unsigned char **pbufout;
 
//最终所有输出数据的大小之和,即*pbufout所指向的内存大小
    unsigned int *pbufoutlen;
} buf_cache;

 

思考

         为什么使用pbufout二级指针?输出内存**pbufout是通过malloc后多次realloc获得,malloc后内存地址一定会变,realloc后内存地址有时会变有时不变(MSDN上说,*realloc returns a void pointer to the reallocated (and possiblymoved) memory block.),所以输出内存地址(指向输出内存的指针)是不断变化的,即指针内容会发生改变,因此要想通过函数改变指针内容,并使该内容可以被函数外环境使用,只能操作二级指针。

         为什么使用pbufoutlen指针?要想通过函数改变输出内存大小的值,并使该内容可以被函数外环境使用,只能操作指针。


三、主函数分析

1.getopt()分析命令行参数

 

头文件:#include<unistd.h>   (unix standard header缩写  unix 标准头文件)

 

原型:

int getopt(int argc,char * const argv[],const char * optstring);

参数argc和argv是由main()传递的参数个数和内容。参数optstring 则代表预处理选项字符串。

 

什么是选项?什么是参数?

字符串optstring可以下列元素

1.单个字符,表示选项。

2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。

3.单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。

 

 

调用原理:

调用一次,返回一个选项。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,char* optarg指向该参数。在命令行选项参数再也检查不到optstring中包含的选项时,返回-1,同时optind储存第一个不包含选项的命令行参数。

 

相关变量:

optarg是char*型变量,会指向此额外参数。

 

返回值:

getopt()每次调用会逐次返回命令行中符合的选项。

 当没有参数的最后的一次调用时,getopt()将返回-1。

 当解析到一个不在optstring里面的参数,或者一个必选值参数不带值时,返回'?'。

 

注意三点:

(1). 不带值的参数可以连写,象1和a是不带值的参数,它们可以-1-a分开写,也可以-1a或-a1连写。

(2). 参数不分先后顺序,'-1a -ccvalue -ddvalue'和'-d -c cvalue -a1'的解析结果是一样的。

(3). 要注意可选值的参数的值与参数之间不能有空格,必须写成-ddvalue这样的格式,如果写成-d dvalue这样的格式就会解析错误。


本程序应用:


(1).文件编码:


getopt处理以'-’开头的命令行参数,如图optstring=”i:o:cdhvm”,命令行为huff_run.exe–i test.doc –o 1.huf –c。在这个命令行参数中,-i、-o和-c就是选项元素,去掉'-',i、o和c就是选项。test.doc是i的参数,1.huf是o的参数。其中顺序可以改变,增加了程序的灵活性。


(2).文件解码:


(3).内存编码:


(4).内存解码:



2.main()函数分析

int main(int argc, char** argv)  //argc:命令行参数个数
//argv:字符指针数组:命令行参数
{
    char memory = 0;  //memory缺省值为0,即默认为文件编解码,而非内存
    char compress = 1;  //compress缺省值为1,即默认为编码
    int opt;  //接收getopt()返回值,为选项或-1
const char *file_in = NULL, *file_out = NULL;  //输入输出文件路径及文件名
                                       //缺省目录则表明为当前目录
    FILE *in = stdin;  //缺省值为标准输入文件
    FILE *out = stdout;  //缺省值为标准输出文件

    //得到命令行参数
    while((opt = getopt(argc, argv, "i:o:cdhvm")) != -1)
    {
        switch(opt)  //opt为iocdhvm字母之一
        {
        case 'i':   //-i后接输入文件
            file_in = optarg;  //optarg为选项参数缩写,该变量存放参数
//注意:optarg无须另设
            break;
        case 'o':  //-o后接输出文件
            file_out = optarg;
            break;
        case 'c':  //-c表明程序功能为文件压缩
            compress = 1;
            break;
        case 'd':  //-d表明程序功能为文件解压
            compress = 0;
            break;
        case 'h':  //-h表明需要显示help使用方法:
               /*fputs("Usage: huffcode [-i<input file>] [-o<output file>] [-d|-c]\n"
		         "-i - input file (default is standard input)\n"
		         "-o - output file (default is standard output)\n"
		         "-d - decompress\n"
		         "-c - compress (default)\n"
  "-m - read file into memory, compress, then write to file 
(not default)\n", out);*/
            usage(stdout);  //输出上述信息到屏幕上
            return 0;
        case 'v':
            version(stdout);  //输出版本版权信息
            return 0;
        case 'm':   //-m表明为内存编码或内存解码
            memory = 1;
            break;
        default:   //如果是其他情况,则将使用方法信息送到标准错误文件
            usage(stderr);
            return 1;
        }
    }

    //如果给出输入文件,则打开该文件
    if(file_in)
    {
        in = fopen(file_in, "rb");
        if(!in)
        {
            fprintf(stderr,"Can't open input file '%s': %s\n",file_in, strerror(errno));   //strerror(errono);返回值为错误的字符串信息
            return 1;
        }
}

	//如果输出文件名给出,则创建该文件
    if(file_out)
    {
        out = fopen(file_out, "wb");
        if(!out)
        {
            fprintf(stderr,
                    "Can't open output file '%s': %s\n", file_out, strerror(errno));
            return 1;
        }
    }
	// memory为1时,说明是内存编解码
    if(memory)
    {
        return compress ?   //compress为1时内存编码,为0时内存解码
            memory_encode_file(in, out) : memory_decode_file(in, out);
    }
	//若执行到此,说明是文件编解码
    return compress ?   //compress为1时文件编码,为0时文件解码

        huffman_encode_file(in, out) : huffman_decode_file(in, out);
}

3.  errno变量和strerror()函数

 

3.1  errno:(Error No. 的缩写)

概念:是一个int型变量------记录系统最后一次错误代码。

头文件:#include<errno.h>

部分输出错误原因定义:

#define EPERM 1  //Operation not permitted

#define ENOENT 2  //No such file or directory

#define ESRCH 3  //No such process

……



3.2  strerror():

函数作用:获取系统错误信息,将单纯的标号转为字符串描述。

头文件:#include<string.h>

补充:常配合errno使用,即strerror(errno)

举例:



四、huffman_encode_file

1.文件编码流程


2. 代码分析

         2.1 文件编码总流程的代码实现

//SymbolFrequencies的类型为数组名,数组元素为huffman树叶节点指针
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];

//SymbolEncoder的类型为数组名,数组元素为huffman码字节点指针
typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];

int huffman_encode_file(FILE *in, FILE *out)
{
    SymbolFrequencies sf;   //sf是数组,数组元素为huffman树叶节点的指针
SymbolEncoder *se;     //se为指向数组的指针(注意不能看做二级指针)
                      //数组元素为码字节点的指针
    huffman_node *root = NULL;   //huffman树的根节点指针
    int rc;   //return count缩写,返回值
    unsigned int symbol_count;   //输入文件的总字符数量

//第一次扫描文件,得到每一个符号对应的树叶节点,节点指针顺序存储
//(按照符号的ASCII码)sf数组中,在节点中储存着符号频率
//函数返回值为输入文件字符总数
    symbol_count = get_symbol_frequencies(&sf, in);

//1.建立huffman树,根节点指针为sf[0]
//2.由huffman树建立所有的码字节点,码字节点指针存储在se中
    se = calculate_huffman_codes(&sf);
    root = sf[0];

	//第一次扫描信源文件时,文件指针位于文件末尾,重新定位至文件开始,
rewind(in);
//根据码字节点指针数组se,在输出文件中,写出各符号的码表
    rc = write_code_table(out, se, symbol_count);
    if(rc == 0)   //函数返回值为0,说明写码表成功
        rc = do_file_encode(in, out, se);   //第二次扫描信源文件,
//根据扫描到的符号,依次查表,将对应码字写入输出文件

    free_huffman_tree(root);   //释放huffman树
    free_encoder(se);   //释放码字节点指针数组se
return rc;  
}


2.2第一次扫描信源文件

         2.2.1扫描信源文件并创建每个符号的huffman树叶节点,最多256个,下表对应符号的ASCII码

         2.2.2根据符号出现频率,将各符号的频率保存在叶节点count中

static unsigned int
get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)
{
    int c;
    unsigned int total_count = 0;  //记录扫描到的符号数
    
init_frequencies(pSF);   //初始化*pSE数组所有元素为NULL

    while((c = fgetc(in)) != EOF)   //扫描输入文件
    {
        unsigned char uc = c;
        if(!(*pSF)[uc])    //如果是一个新符号,则建立一个新的叶节点
         (*pSF)[uc] = new_leaf_node(uc);   //下标为该符号

        ++(*pSF)[uc]->count;   //频率累加1
        ++total_count;   //总符号数累加1
    }

    return total_count;   //返回输入文件的总符号数
}

static huffman_node*
new_leaf_node(unsigned char symbol)
{  //sizeof()中可以是类型名,也可以是变量名
    huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));
    p->isLeaf = 1;   //新建的是叶节点,所以isLeaf设为1
    p->symbol = symbol;
    p->count = 0;
    p->parent = 0;
    return p;
}

2.3  建立huffman树, 由huffman树建立码字节点

         2.3.1 建立huffman树,根节点指针为(*pSF)[0]    

static SymbolEncoder*
calculate_huffman_codes(SymbolFrequencies * pSF)
{
    unsigned int i = 0;
    unsigned int n = 0;
huffman_node *m1 = NULL, *m2 = NULL;   //m1为左孩子指针
                                          // m2为右孩子指针
  	SymbolEncoder *pSE = NULL;   //*pSE存放码字节点指针
    
	//以(*pSE)为排序依据,把*pSF数组元素升序排列
    qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);

	//得到输入文件的符号种类数
    for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n);

	//建立huffman树,需要合并n-1次,故循环n-1次
    for(i = 0; i < n - 1; ++i)
    {
   		//将m1、m2设置为频率最低的数组元素
        m1 = (*pSF)[0];
        m2 = (*pSF)[1];

		//合并m1、m2为非叶节点,count为二者count之和
		//并将该非叶节点的左右孩子设为m1、m2
       //将左右孩子的父节点指向该非叶节点
		//将(*pSF)[0]指向该非叶节点,将(*pSF)[1]置空
        (*pSF)[0] = m1->parent = m2->parent =
            new_nonleaf_node(m1->count + m2->count, m1, m2);
        (*pSF)[1] = NULL;
        
		//将*pSF数组重新按照升序排序,即将{m1,m2}合并后与其他元素排序
        qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);
    }   //最终(*pSF)[0]为根节点,其count为输入文件大小(单位:字节)

/* Build the SymbolEncoder array from the tree. */
/*分配码字节点指针数组的内存空间(注意:pSE不是二级指针,而是指向整个数组的指针,参考如下:	http://www.360doc.com/content/12/0313/18/4186481_194063111.shtml 
此外:sizeof(数组名)为整个数组的大小*/
    pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
    memset(pSE, 0, sizeof(SymbolEncoder));   //初始化该数组
    build_symbol_encoder((*pSF)[0], pSE);   //由huffman树建立所有的码字节点
    return pSE;   //返回码字节点指针数组的地址
}

qsort()函数:  ----quickly sort

头文件:#include<stdlib.h>

功能:使用快速排序例程进行排序

原型:void qsort(void*base,int nelem,int width,int (*fcmp)(const void *,const void *));

参数:1 待排序数组首地址

2 数组中待排序元素数量

3 各元素的占用空间大小

4 指向函数的指针,用于确定排序的顺序

compare函数原型:

compare( (void *) & elem1, (void *)& elem2 );

Compare 函数的返回值

描述

< 0

elem1将被排在elem2前面

0

elem1 等于 elem2

> 0

elem1 将被排在elem2后面


举例

(1)    对一个长为1000的数组进行排序时,int a[1000]; 那么base应为a,num应为 1000,width应为 sizeof(int),comp函数随自己的命名。

qsort(a,1000,sizeof(int),comp);

其中comp函数应写为:(注意:参数a,b是指向数组元素的指针。函数体内,需要的是,数组中的元素,所以要加*)

int comp(const void*a,const void*b)
{
    return *(int*)a-*(int*)b;
}

上面是由小到大排序,return*(int *)b - *(int *)a; 为由大到小排序。


(2)对一维数组的排序实例(从小到大排序):

int array[5]={4,2,63,1,10};
qsort(array,5,sizeof(int),comp);
int comp(const void*a,const void*b)
{
return *(int*)a-*(int*)b;
}


(3)对字符串进行排序:

int Comp(const void*p1,const void*p2)
{
    return strcmp((char*)p2,(char*)p1);
}
int main()
{
    char a[MAX1][MAX2];
    initial(a);
    qsort(a,lenth,sizeof(a[0]),Comp);
}


(4) 按结构体中某个关键字排序(对结构体一级排序):

structNode
{
    double data;
    int other;
}s[100];
int Comp(constvoid*p1,constvoid*p2)
{
    return(*(Node*)p2).data>(*(Node*)p1).data?1:-1;
}
qsort(s,100,sizeof(s[0]),Comp);

(5)按结构体中多个关键字排序(对结构体多级排序)[以二级为例]:

struct Node
{
    int x;
    int y;
}s[100];
//按照x从小到大排序,当x相等时按y从大到小排序
int Comp(const void*p1,const void*p2)
{
    struct Node*c=(Node*)p1;
    struct Node*d=(Node*)p2;
    if(c->x!=d->x)returnc->x-d->x;
    else return d->y-c->y;
}

(6)对结构体中字符串进行排序:

struct Node
{
    int data;
    char str[100];
}s[100];
//按照结构体中字符串str的字典序排序
int Comp(const void*p1,const void*p2)
{
    return strcmp((*(Node*)p1).str,(*(Node*)p2).str);
}
qsort(s,100,sizeof(s[0]),Comp);

huffman编码中应用:

qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);


static int
SFComp(const void *p1, const void *p2)
{
    const huffman_node *hn1 = *(const huffman_node**)p1;
    const huffman_node *hn2 = *(const huffman_node**)p2;

    //把所有的值为NULL的数组元素排到最后
    if(hn1 == NULL && hn2 == NULL)
        return 0;
    if(hn1 == NULL)
        return 1;
    if(hn2 == NULL)
        return -1;
    //由小到大排列
    if(hn1->count > hn2->count)
        return 1;     //返回值为正,参数2排在前
    else if(hn1->count < hn2->count)
        return -1;    //返回值为负,参数1排在前
    return 0;        //返回值为0,参数1==参数2
}

static huffman_node*
new_nonleaf_node(unsigned long count, huffman_node *zero, huffman_node *one)
{
    huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));
    p->isLeaf = 0;
    p->count = count;
    p->zero = zero;
    p->one = one;
    p->parent = 0;
    
    return p;
}    //新建非叶节点

2.3.2递归遍历huffman树,建立所有的码字节点,码字节点指针存储在*pSF数组中

(1)  递归遍历huffman树,找到symbol所对应的叶节点

static void
build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF)  {
    if(subtree == NULL)
        return;   /*检查首次传递的参数root是否为NULL,是则返回。若root不为NULL,则后面递归永远不会再次运行这一语句。*/

    if(subtree->isLeaf)  /*如果是1,则说明到达叶节点,建立新节点,并且此次函数调用结束*/
        (*pSF)[subtree->symbol] = new_code(subtree);
    else
{
/*递归——前序遍历:遍历各节点的isLeaf,并判断是否为1。若为1,则建立新节点,并结束该次函数调用。*/
        build_symbol_encoder(subtree->zero, pSF);
	build_symbol_encoder(subtree->one, pSF);
    }
}

(2)  根据找到的symbol,找到相应的码字节点(*pSF)[symbol];从叶节点开始,向上“爬树”直到“树顶”,“途中”位操作取二进制码,写入码字节点(*pSF)[symbol]中

static huffman_code*
new_code(const huffman_node* leaf)
{
    unsigned long numbits = 0;  //实时记录每次“爬行”的长度(单位:位)
    unsigned char* bits = NULL;   //编码
    huffman_code *p;  //码字节点

while(leaf && leaf->parent)/*当leaf==NULL:当前字符为空,无法编码。 
当leaf->parent==NULL: 已经到达树根root,此字符编码结束。*/
    {
        huffman_node *parent = leaf->parent;

		//cur_bit为在当前bits[cur_byte]的位置
        unsigned char cur_bit = (unsigned char)(numbits % 8);
        unsigned long cur_byte = numbits / 8;   //cur_byte为第几个字节

        /* cur_bit为0代表已写满一个字节,需要下一个数组元素 */
        if(cur_bit == 0)
        {
            size_t newSize = cur_byte + 1;
            bits = (char*)realloc(bits, newSize);  /*注意两点:1. 扩容后内存地址可能改变,也可能不变,所以bits=不能省略。2.扩容后返回的是void*指针,所以要强制类型转换*/
            bits[newSize - 1] = 0; /* 初始化新字节为0 */
        }

		/*如果leaf是其parent的左孩子,则无需改变bits[cur_byte]中的二进制数字,因为初始化值是0. 如果是右孩子,则需要位运算,在bit[cur_byte]相应位置变为1*/
        if(leaf == parent->one)
            bits[cur_byte] |= 1 << cur_bit;

        ++numbits;   //增加已“爬行”的长度(单位:位)
        leaf = parent;   //置为父节点
}
    if(bits)
        reverse_bits(bits, numbits);   //反转bits数组中二进制数

    p = (huffman_code*)malloc(sizeof(huffman_code));
    p->numbits = numbits;   //码位数赋值给码字节点中numbits
    p->bits = bits;   //码字赋值给码字节点中bits
    return p;   //返回码字节点
}

 reverse_bits(bits,numbits);之前:



reverse_bits(bits,numbits);之后:



思考:为什么要reverse_bits(bits,numbits);?

因为,解码时,读码顺序是,从bit[0]的最右一位向左读,再从bit[1]的最右一位向左读……直到bit[numbytes-1]的最左一位。而解码时,要从树根节点root开始向下遍历,如果读到1,则继续读右孩子,否则读左孩子。所以为了保持一致,先读的bit[0]的最右一位应该是根节点到下一节点的二进制码值,最后读的bit[numbytes-1]的最左一位应该是到叶节点的二进制码值。


static void
reverse_bits(unsigned char* bits, unsigned long numbits)
{
	//将numbits除以8上取整,得到numbytes
unsigned long numbytes = numbytes_from_numbits(numbits); 

	//分配临时字符指针,alloca是在栈(stack)上申请空间,用完马上就释放
unsigned char *tmp =        
        (unsigned char*)alloca(numbytes);
    unsigned long curbit;   //记录即将要反转的二进制码的位置
    long curbyte = 0;  //记录即将要反转的二进制码所在的的数组下标
    
    memset(tmp, 0, numbytes);   //初始化tmp[numbytes]所有元素为0

    for(curbit = 0; curbit < numbits; ++curbit)
    {
        unsigned int bitpos = curbit % 8;   //要向左移动几位

        if(curbit > 0 && curbit % 8 == 0)
            ++curbyte;   /*如果已反转的二进制数字已达到8的倍数,则到下一字节*/

        /*get_bit()第二个参数是0时,则为bit[0]最右一位;为numbits-curbit-1时,则为bit[numbytes-1]的最左一位。向左移位,使二进制数对准相应位置*/
        tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);
    }

    memcpy(bits, tmp, numbytes);   //将tmp临时数组内容拷贝到bits数组中
}


2.4  在输出文件开始处,写入:字符种类数,输入文件字节数,各字符及其码字长度、对应码字

static int
write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count)
{
    unsigned long i, count = 0;
    
    /* 计算字符种类数 */
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        if((*se)[i])
            ++count;
    }

    /* 主机字节序(host)变成(to)网络字节序(network)(即intel字节序变成motorola字节序) l代表long,参数为32位数字。对应地:htons,s代表short,参数为16位整数*/
    i = htonl(count);
    if(fwrite(&i, sizeof(i), 1, out) != 1)
        return 1;   //将字符种类数写入文件

    //将输入文件字符数写入输出文件
    symbol_count = htonl(symbol_count);
    if(fwrite(&symbol_count, sizeof(symbol_count), 1, out) != 1)
        return 1;

	//写码字
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        huffman_code *p = (*se)[i];
        if(p)
        {
            unsigned int numbytes;
            /* 写入符号(1字节) */
            fputc((unsigned char)i, out);
            /*写入该符号的码字长度(1字节) */
            fputc(p->numbits, out);
            /* 写入码字(numbytes字节) */
			/*这里区别将所有字符编码时,码字之间无间距*/
            numbytes = numbytes_from_numbits(p->numbits);
            if(fwrite(p->bits, 1, numbytes, out) != numbytes)
                return 1;
        }
    }

    return 0;
}



2.5  第二次扫描文件,对各字符查表*se,将所有字符的码字写入输出文件,完成文件编码

static int
do_file_encode(FILE* in, FILE* out, SymbolEncoder *se)
{
    unsigned char curbyte = 0;
    unsigned char curbit = 0;
    int c;
    
    while((c = fgetc(in)) != EOF)   //遍历每一个字符
    {
        unsigned char uc = (unsigned char)c;
        huffman_code *code = (*se)[uc];   //查表
        unsigned long i;
        
        for(i = 0; i < code->numbits; ++i)   /*循环完成,则成功写入一个字符的码字,但是不一定被输出*/
        {
            /*将curbyte字节对应位置变成相应二进制数*/
            curbyte |= get_bit(code->bits, i) << curbit;

			/*每次写进一位二进制数后,都要判断当前字节curbyte是否写满*/
            if(++curbit == 8)  /*写满则输出,当前字节初始化为0,当前位的位								   置为0*/
            {
                fputc(curbyte, out);
                curbyte = 0;
                curbit = 0;
            }
        }
    }

	/*当最后一个curbyte没有写满时(写满时curbit==0),不会写入文件。所以当curbit>0时,将最后一个curbyte写入文件*/
    if(curbit > 0)
        fputc(curbyte, out);

    return 0;
}   /*注意各码字都是无间距的(之间没有空的二进制位),可能一个字节中前半部分是上个码字,后半部分是下一码字,原因之一就是huffman编码是可变字长编码(VLC)*/


五、huffman_decode_file

1.文件解码流程



2.代码分析

2.1 文件解码总流程的代码实现

int huffman_decode_file(FILE *in, FILE *out)
{
    huffman_node *root, *p;
    int c;
    unsigned int data_count;
    
    /* 读输入文件起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树 */
    root = read_code_table(in, &data_count);
    if(!root)
        return 1;  //建huffman树失败

    /* 解码开始:遍历输入文件得到各字符的码字,根据码字,从huffman树根节点root开始,向下直至叶节点,从而获得相应symbol */
p = root;

//data_count>0逻辑上仍有数据,(c = fgetc(in)) != EOF文件中仍有数据
    while(data_count > 0 && (c = fgetc(in)) != EOF)
    {  
        unsigned char byte = (unsigned char)c;
        unsigned char mask = 1;   //mask用来逐位读出二进制数字

		//循环一次,mask由0移位至溢出为0,即此byte每一位都已被遍历
        while(data_count > 0 && mask) 
/*data_count>0再次被判断的原因:输入文件的最后一个字节,很有可能前半段是编码,后半段因为所有编码完成而全部为0。此时,若没有data_count>0判断,因mask没有溢出为0,所以此次循环无法终止,但是联合体内p->one和p->zero都不存在(被symbol代替)而无法被访问,从而程序出现错误。*/
        {
            p = byte & mask ? p->one : p->zero;   
			/*loop1:mask=00000001,取byte第1位
		     loop2:mask=00000010,取byte第2位
			  ……
			  loop8:mask=10000000,取byte第8位 */
 
            mask <<= 1;   //loop8以后,mask移位溢出为00000000

            if(p->isLeaf)  //每次p向下移动后,都要判断其是否为叶节点
            {
                fputc(p->symbol, out);  //是叶节点,则输出symbol
                p = root;   //重新将p置为根节点,因为要重新从根部向下遍历
                --data_count;   //还需要被解码的符号个数
            }
        }
    }

    free_huffman_tree(root);  //解码结束,释放huffman树
    return 0;
}



2.2  读输入文件起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树

static huffman_node*
read_code_table(FILE* in, unsigned int *pDataBytes)
{
    huffman_node *root = new_nonleaf_node(0, NULL, NULL);
    unsigned int count;
    
    /*读取符号种类数(存储为网络字节序)*/
    if(fread(&count, sizeof(count), 1, in) != 1)
    {
        free_huffman_tree(root);
        return NULL;
    }

	//将网络字节序变为主机字节序
    count = ntohl(count);

    /*读取字符总数*/
    if(fread(pDataBytes, sizeof(*pDataBytes), 1, in) != 1)
    {
        free_huffman_tree(root);
        return NULL;
    }

	//将网络字节序变为主机字节序
    *pDataBytes = ntohl(*pDataBytes);


    /*读取符号、对应码位数、码字*/
    while(count-- > 0)
    {
        int c;
        unsigned int curbit;
        unsigned char symbol;
        unsigned char numbits;
        unsigned char numbytes;
        unsigned char *bytes;
        huffman_node *p = root;
        
        if((c = fgetc(in)) == EOF)   //读取符号(1字节)
        {
            free_huffman_tree(root);
            return NULL;
        }
        symbol = (unsigned char)c;  //fgetc的返回值是int,所以要强制类型转换
        
        if((c = fgetc(in)) == EOF)   //读取码位数(1字节)
        {
            free_huffman_tree(root);
            return NULL;
        }
        
        numbits = (unsigned char)c;
        numbytes = (unsigned char)numbytes_from_numbits(numbits);
        bytes = (unsigned char*)malloc(numbytes);

		/* 读取对应码字(numbytes字节)*/
        if(fread(bytes, 1, numbytes, in) != numbytes)
        {
            free(bytes);
            free_huffman_tree(root);
            return NULL;
        }

		//顺着码字建树:当前读取位为0时,则建左孩子;为1时,则建右孩子
        for(curbit = 0; curbit < numbits; ++curbit)
        {
            if(get_bit(bytes, curbit))  //当前读取位为1时
            {
			 /*如果p->one为NULL,则可以建立树节点。如果不是NULL,
			 说明之前有码字(前半段与当前码字相同)建立过此树节点*/
               if(p->one == NULL)                  
				{
				  /* curbit == (unsigned char)(numbits - 1)说明已经到达叶节点					处,建立叶节点。否则未到达,建立非叶节点*/
                    p->one = curbit == (unsigned char)(numbits - 1)
                        ? new_leaf_node(symbol)
                        : new_nonleaf_node(0, NULL, NULL);
                    p->one->parent = p;  //建立双向指针
                }
                p = p->one;  //p下移至右孩子
            }
            else   //当前读取位为0时。算法同上
            {
                if(p->zero == NULL)
                {
                    p->zero = curbit == (unsigned char)(numbits - 1)
                        ? new_leaf_node(symbol)
                        : new_nonleaf_node(0, NULL, NULL);
                    p->zero->parent = p;
                }
                p = p->zero;
            }
        }
        
        free(bytes);   //huffman树已经建成,可以释放掉码字
    }

    return root;  //返回huffman树的根节点指针
}


六、memory_encode_file

1.内存编码流程



2.代码分析

2.1 总流程的代码实现:将输入文件读入内存,将内存编码得到输出文件内存,将输出文件内存写入输出文件

static int
memory_encode_file(FILE *in, FILE *out)
{
	/*buf指向放置输入文件的内容的内存(以下简称输入内存),
	bufout指向放置输出文件的内容的内存(以下简称输出内存)*/
    unsigned char *buf = NULL, *bufout = NULL; 
    unsigned int len = 0, cur = 0, inc = 1024, bufoutlen = 0;

	/* assert的作用是计算括号内表达式,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。*/
    assert(in && out);   //断言:输入文件、输出文件都被成功打开

    /*将输入文件读入输入内存*/
    while(!feof(in))   //文件读取没有结束
    {
        unsigned char *tmp;  //临时指针,用来判断内存是否够用
        len += inc;  //内存重新分配大小,递增1kb
        tmp = (unsigned char*)realloc(buf, len);  /*内存分配扩大1kb,注意realloc扩容后内存地址可能改变,也可能不变*/
        if(!tmp)  //判断重新分配是否成功,即判断内存是否充足
        {
            if(buf)   //如果内存不够,则释放buf
                free(buf);
            return 1;
        }

        buf = tmp;   //buf指向重新分配的内存
        cur += fread(buf + cur, 1, inc, in);   /*以1kb为单位,读取输入文件内容。cur的最终值为输入文件大小*/
    }

    if(!buf)   //如果buf为空,则返回1
        return 1;

/* 对输入内存内容进行编码:buf指向输入内存,bufout指向输出内存,cur为输入文件的大小(也是输入内存的大小),bufoutlen为输出文件的大小(也是输出内存的大小)*/
/*传入bufout指针的地址原因:bufout所指内存是通过malloc后多次realloc获得,malloc后内存地址一定会变,realloc后内存地址有时会变有时不变,所以bufout的指向是不断变化的,即指针内容会发生改变,因此要传入指针地址。
传入bufoutlen的地址原因:要改变bufoutlen的值*/
    if(huffman_encode_memory(buf, cur, &bufout, &bufoutlen))
    {
        free(buf);
        return 1;
    }   //内存编码完成

    free(buf);   /*内存编码完成后,输入内存(由buf指向)使用完毕,可以释放*/

    /* 将输出内存(由bufout指向)中的内容写人输出文件*/
    if(fwrite(bufout, 1, bufoutlen, out) != bufoutlen)
    {
        free(bufout);
        return 1;
    }

    free(bufout);   /*写入完成,输出内存(由bufout指向)使用完毕,可以释放*/

    return 0;
}

2.2 将输入文件内存编码,得到输出文件内存,以下为编码流程

#define CACHE_SIZE 1024   //宏定义缓存大小为1kb

int huffman_encode_memory(const unsigned char *bufin,
                          unsigned int bufinlen,
                          unsigned char **pbufout,
                          unsigned int *pbufoutlen)
{
    SymbolFrequencies sf;
    SymbolEncoder *se;
    huffman_node *root = NULL;
    int rc;
    unsigned int symbol_count;
    buf_cache cache;

    /* 确保参数的正确性 */
    if(!pbufout || !pbufoutlen)
        return 1;

	/*cache是输出文件的缓存(指针)+缓存区大小+当前已缓存大小+内存(二级指针)+内存大小(指针)的结构体*/
	/*init_cache是将cache结构体内的元素都设为初始值:malloc分配CACHE_SIZE大小的缓存,缓存区大小设为CACHE_ SIZE,当前已缓存大小设为0,内存的二级指针设为输出文件内存的二级指针,内存大小为0*/
    if(init_cache(&cache, CACHE_SIZE, pbufout, pbufoutlen))
        return 1;

    /* 第一次遍历输入内存,得到输入文件大小(也是输入内存大小)(其实就是bufinlen),并建立huffman树叶节点*/
    symbol_count = get_symbol_frequencies_from_memory(&sf, bufin, bufinlen);

    /*同文件编码:根据频率,构建huffman树,根据huffman树,得到各符号的码字节点*/
    se = calculate_huffman_codes(&sf);
    root = sf[0];   //huffman树根节点

    /*将符号种类数,输入文件大小,各符号,以及对应符号的码位数、码字写入输出内存开始部分*/
rc = write_code_table_to_memory(&cache, se, symbol_count);

	/*第二次扫描输入内存,通过查表找到对应字符的码字,按位写入输出内存,确保每一码字直接无二进制位间隔*/
    if(rc == 0)
        rc = do_memory_encode(&cache, bufin, bufinlen, se);

    /* 将缓冲区内容放入输出内存*/
    flush_cache(&cache);
    

    free_huffman_tree(root);    /*释放huffman树所用内存 */
    free_encoder(se);   //释放所有码字节点所用内存
    free_cache(&cache);    //释放输出内存(缓冲区+内存)
    return rc;
}

2.3 初始化输出内存缓冲区cache

static int init_cache(buf_cache* pc,
                      unsigned int cache_size,
                      unsigned char **pbufout,
                      unsigned int *pbufoutlen)
{
    assert(pc && pbufout && pbufoutlen);  /*断言:pc、pbufout、pbufoutlen都不是NULL*/
    if(!pbufout || !pbufoutlen)
        return 1;
    
    pc->cache = (unsigned char*)malloc(cache_size);   /*开辟缓存,大小为cache_size,本程序是1024*/
    pc->cache_len = cache_size;   //缓存大小设为cache_size
    pc->cache_cur = 0;   //当前已缓存大小设为0
    pc->pbufout = pbufout;   //输出内存的二级指针
    *pbufout = NULL;   //输出内存的指针指向NULL
    pc->pbufoutlen = pbufoutlen;   //输出内存大小指针
    *pbufoutlen = 0;  //输出内存大小设为0

    return pc->cache ? 0 : 1;   //缓存开辟成功时,返回0,否则返回1
}

2.4 第一次遍历输入内存,得到输入文件大小(也是输入内存大小),并建立huffman树叶节点

static unsigned int  /*代码与原理跟get_symbol_frequencies()函数十分类似,区别就是扫描输入文件时是fgetc,扫描输入内存时,直接用下标法取元素。*/
get_symbol_frequencies_from_memory(SymbolFrequencies *pSF,
                                   const unsigned char *bufin,
                                   unsigned int bufinlen)
{
    unsigned int i;
    unsigned int total_count = 0;
    
   init_frequencies (pSF);
    
    for(i = 0; i < bufinlen; ++i)
    {
        unsigned char uc = bufin[i];
        if(!(*pSF)[uc])   
            (*pSF)[uc] = new_leaf_node(uc);
        ++(*pSF)[uc]->count;  
        ++total_count;     //其实就是bufinlen
    }

    return total_count;
}

2.5 将符号种类数,输入文件大小,各符号,以及对应符号的码位数、码字写入输出内存开始部分

static int  /*代码与原理跟write_code_table()函数十分类似,不同之处在于本函数使用write_cache()向输出内存写入(下面已标注)*/
write_code_table _to_memory(buf_cache *pc,
                           SymbolEncoder *se,
                           unsigned int symbol_count)
{
    unsigned long i, count = 0;

    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        if((*se)[i])
            ++count;
    }

    i = htonl(count);
    
	//将字符种类数写入输出内存中
    if(write_cache(pc, &i, sizeof(i)))
        return 1;

symbol_count = htonl(symbol_count);

//将输入文件大小写入输出内存
    if(write_cache(pc, &symbol_count, sizeof(symbol_count)))
        return 1;

    
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        huffman_code *p = (*se)[i];
        if(p)
        {
            unsigned int numbytes;
            unsigned char uc = (unsigned char)i;
            /* 将符号写入输出内存(1字节) */
            if(write_cache(pc, &uc, sizeof(uc)))
                return 1;
        
            uc = (unsigned char)p->numbits;
			/* 将码位数写入输出内存(1字节)*/
            if(write_cache(pc, &uc, sizeof(uc)))
                return 1;

            /*将码字写入输出内存(numbytes字节)*/
			/*这里区别将所有字符编码时,码字之间无间距*/
            numbytes = numbytes_from_numbits(p->numbits);
            if(write_cache(pc, p->bits, numbytes))
                return 1;
        }
    }

    return 0;
}

2.6 第二次扫描输入内存,通过查表找到对应字符的码字,按位写入输出内存,确保每一码字直接无二进制位间隔

static int  /*代码与原理跟do_file_encode十分相似,不同之处在于do_file_encode是写入文件,这里是写入内存*/
do_memory_encode(buf_cache *pc,
                 const unsigned char* bufin,
                 unsigned int bufinlen,
                 SymbolEncoder *se)
{
    unsigned char curbyte = 0;
    unsigned char curbit = 0;
    unsigned int i;
    
    for(i = 0; i < bufinlen; ++i)
    {
        unsigned char uc = bufin[i];
        huffman_code *code = (*se)[uc];
        unsigned long i;
        
        for(i = 0; i < code->numbits; ++i)
        {
            curbyte |= get_bit(code->bits, i) << curbit;

            if(++curbit == 8)
            {   //将码字写入输出内存
                if(write_cache(pc, &curbyte, sizeof(curbyte)))
                    return 1;
                curbyte = 0;
                curbit = 0;
            }
        }
    }

    return curbit > 0 ? write_cache(pc, &curbyte, sizeof(curbyte)) : 0;
}

2.7将数据写入输出内存的函数

static int write_cache(buf_cache* pc,
                       const void *to_write,
                       unsigned int to_write_len)
{
    unsigned char* tmp;

	//如果pc和to_write至少一个为NULL的话,则终止程序进行
assert(pc && to_write);
//如果已缓存数据量大于缓存区大小的话,则终止程序进行
    assert(pc->cache_len >= pc->cache_cur);
    
/*如果将要写入的数据量与已缓存的数据量之和大于缓存区大小,那么就先flush缓存区,然后再将要写入的数据直接写入输出内存,即不使用缓存区。
否则,使用缓存区缓存将要写入的数据,待以后缓存区将满时,flush到输出内存*/
    if(to_write_len > pc->cache_len - pc->cache_cur)
    {
        unsigned int newlen;
        flush_cache(pc);   // flush缓存区中的内容到输出内存

		//计算输出内存的原有数据量与将要写入的数据量之和,作为newlen
        newlen = *pc->pbufoutlen + to_write_len;

		//扩大输出内存空间,大小为newlen
        tmp = realloc(*pc->pbufout, newlen);
        if(!tmp)
            return 1;

		/*将要写入的数据复制到输出内存中,注意从tmp + *pc->pbufoutlen位置(新空间开始处)开始,长度为将要写入的数据量*/
        memcpy(tmp + *pc->pbufoutlen, to_write, to_write_len);
        *pc->pbufout = tmp;
        *pc->pbufoutlen = newlen;  //输出内存空间大小置为newlen
    }

    else
    {
        /* 把将要写入的数据拷贝到缓存中,注意从原缓存数据量开始,长度为将要写入的数据量*/
        memcpy(pc->cache + pc->cache_cur, to_write, to_write_len);

		/*已缓存位置设为原缓存数据量与刚写入数据量之和*/
        pc->cache_cur += to_write_len;  
    }

    return 0;
}

2.8 将缓冲区内容放入输出内存

static int flush_cache(buf_cache* pc)
{
    assert(pc);   //如果pc为NULL,则终止程序运行
    
    if(pc->cache_cur > 0)  /*如果当前已缓存数据量为0,则不无需flush,直接return 0;*/
{
		//计算已缓存数据量与输出内存中的数据量之和,作为新长度newlen
        unsigned int newlen = pc->cache_cur + *pc->pbufoutlen;
		
		//扩大输出内存空间,大小为newlen
        unsigned char* tmp = realloc(*pc->pbufout, newlen);
        if(!tmp)
            return 1;

		/*将缓冲区内容拷贝到新增加的内存空间上,注意从tmp+*pc->pbufoutlen位置(新空间开始处)开始拷贝,拷贝长度为原缓存数据量*/
        memcpy(tmp + *pc->pbufoutlen, pc->cache, pc->cache_cur);

        *pc->pbufout = tmp;
        *pc->pbufoutlen = newlen;
        pc->cache_cur = 0;   //已缓存数据量置零
    }

    return 0;
}



七、memory_decode_file

1.内存解码流程



2.代码分析

2.1  内存解码总流程的代码实现

static int   /*代码与memory_encode_file(FILE *in,FILE *out)基本相同,不同之处在于本函数调用的是huffman_decode_memory()函数*/
memory_decode_file(FILE *in, FILE *out)
{
    unsigned char *buf = NULL, *bufout = NULL;
    unsigned int len = 0, cur = 0, inc = 1024, bufoutlen = 0;
    assert(in && out);

    while(!feof(in))
    {
        unsigned char *tmp;
        len += inc;
        tmp = (unsigned char*)realloc(buf, len);
        if(!tmp)
        {
            if(buf)
                free(buf);
            return 1;
        }

        buf = tmp;
        cur += fread(buf + cur, 1, inc, in);
    }

    if(!buf)
        return 1;

    if(huffman_decode_memory(buf, cur, &bufout, &bufoutlen))
    {
        free(buf);
        return 1;
    }

    free(buf);

    if(fwrite(bufout, 1, bufoutlen, out) != bufoutlen)
    {
        free(bufout);
        return 1;
    }

    free(bufout);

    return 0;
}

2.2 对输入内存进行解码,解码内容存入输出内存

int huffman_decode_memory (const unsigned char *bufin,
                          unsigned int bufinlen,
                          unsigned char **pbufout,
                          unsigned int *pbufoutlen)
{
    huffman_node *root, *p;
    unsigned int data_count;
    unsigned int i = 0;
    unsigned char *buf;
    unsigned int bufcur = 0;

    /*确保参数的合法性*/
    if(!pbufout || !pbufoutlen)
        return 1;

    /*读输入内存起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树 */
    root = read_code_table_from_memory(bufin, bufinlen, &i, &data_count);
    if(!root)
        return 1;

    buf = (unsigned char*)malloc(data_count);

    /* 下面内容与huffman_decode_file()函数中解码部分基本相同,不同之处已标明*/
    p = root;
    for(; i < bufinlen && data_count > 0; ++i) 
    {
        unsigned char byte = bufin[i];
        unsigned char mask = 1;
        while(data_count > 0 && mask)
        {
            p = byte & mask ? p->one : p->zero;
            mask <<= 1;

            if(p->isLeaf)
            {
                p = root;
				//下标法取出输出内存中的元素,并将字符存入其中
                buf[bufcur++] = p->symbol;
                --data_count;
            }
        }
    }

    free_huffman_tree(root);
    *pbufout = buf;
    *pbufoutlen = bufcur;
    return 0;
}

2.3 读输入内存起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树

static huffman_node*
read_code_table _from_memory(const unsigned char* bufin,
                            unsigned int bufinlen,
                            unsigned int *pindex)

{略。与read_code_table()函数基本相同,只是读取数据的方式不同,本函数使用的是memread函数,具体解析如下。}


2.4  memread函数:

static int
memread(const unsigned char* buf,
        unsigned int buflen,

		/*用来记录已读取的数据量,标记读取位置。使用指针的原因是,要使不断改变的读取位置,被本次函数调用之后,仍能被函数环境中其他元素使用*/
        unsigned int *pindex,
        void* bufout,
        unsigned int readlen)
{
	/*如果buf、pindex、bufout中存在为NULL,则终止程序运行*/
assert(buf && pindex && bufout);

	/*如果读取位置大于被读取内存大小,则终止程序运行*/
    assert(buflen >= *pindex);
    if(buflen < *pindex)
        return 1;

	/*如果读取位置与将读取的数据量之和大于被读取内存大小,则返回1*/
    if(readlen + *pindex >= buflen)
        return 1;

	/*将被读取内存读取位置之后的内容,按照所要求大小,读入外部内存*/
memcpy(bufout, buf + *pindex, readlen);

	/*读取位置右移刚读取数据量大小*/
    *pindex += readlen;
    return 0;
}


八、发现的问题及实验改进


问题1:

郭远航的实验报告中描述了一个问题:“结构体huffman_code_tag建立时,码字长度的数据类型为unsignedlong,占用4byte。在将码表写入文件时,用的是函数fputc()(解码读出时用fgetc(),即写入(/读出)1byte的码长。这就造成了前后的不匹配,存在错误隐患。”

         具体错误隐患为:产生的问题就是getchar时,会取低内存的字节,如果主机采用motorola字节序,则造成码位数取值为0,从而造成错误。

实验报告中指出解决方案:“在结构体huffman_code_tag建立时,将码字长度的数据类型改为unsignedchar,占用1byte。”

但是问题在于,这种解决方案忽略了一个很小的细节。当各字符频率排序后,出现类似于1,1,2,4,8,16,……这种数列中任意一个数字都大于等于前面所有数字之和的情况时,建成huffman树以后,会出现极不平衡的二叉树(如图)。


这种情况下,如果文件中每一种可能的字符都出现,即2^8个符号全部都有相应编码时,huffman树的高度就会是2^8。所以numbits应该为256。按照上述解决方案,将numbits的类型改为unsigned char时,numbits最大为255,无法存储256. 即使赋值numbits=256;(实际numbits会溢出为0),--numbits;后,numbits的值会变成255,仍然具有实际意义,但是不可忽略如下情况:当码位数为256时,numbits溢位为0.



上述情况都会出错。

         所以最好的解决办法不是在结构体中将numbits的类型由unsigned long改为unsigned char,而是仍保留unsigned long类型(因为可以存储数字256),并在write_code_table中写码位数时,将fputc()改为fwrite();read_code_table中,fgetc()改为fread()。在已编码文件起始处的code_table部分,码位数始终占据4字节空间。这样即解决了由于cpu字节序引发的问题,又解决了程序中多次使用造成的256数字无法存储的问题。



问题2:

         输入文件(内存)中字符总数symbol_count总是采用unsignedint类型,unsigned int最多表示2^32个数字,也就是说,最多压缩2^32字节(4G)的文件(准确地说应该是4G-1字节的文件)。

         如果要压缩4G或者4G以上的文件,那么此程序无法正确运行。

解决办法:

使用unsigned__int64或者unsigned long long类型来定义symbol_count,最大压缩2^64字节(2^34G)的文件。相应的不能用htonl()/ntohl()来转换字节序,因为其中参数必须是32位的数字。所以应该定义函数htonll()/ntohll()如下,可以转换64位的字节序。

(注:vc6.0中无法使用unsigned long long,可以使用unsigned __int64。用vc6.0时,将以下unsigned long long全部改为unsigned __int64)


unsigned long long ntohll(unsigned long long val)
{
        return (((unsigned long long )htonl((int)((val << 32) >> 32))) << 32) | (unsigned int)htonl((int)(val >> 32));
}

unsigned long long htonll(unsigned long long val)
{
        return (((unsigned long long )htonl((int)((val << 32) >> 32))) << 32) | (unsigned int)htonl((int)(val >> 32));
}

举例分析函数ntohll():

假设64位数字var=0X ff 66 ee 55 dd 44 cc 33(以下均为十六进制)

内存中:33 cc 44 dd 55 ee 66 ff(左:低内存;右:高内存)

或“|”左边内存变化:

1)      val<<32: 00 00 00 00 33 cc 44 dd

2)      val>>32: 33 cc 44 dd 00 00 00 00

3)      int强制类型转换: 33 cc 44 dd

4)      htonl改变字节序: dd 44 cc 33

5)      unsigned long long 强制类型转换:dd 44 cc 33 00 00 00 00

6)      <<32:  00 00 00 00dd 44 cc 33

或“|”右边内存变化:

1)      val>>32:  55 ee 66ff 00 00 00 00

2)      int强制类型转换:55 ee 66 ff

3)      htonl改变字节序:ff 66 ee 55

4)      unsigned int 强制类型转换:ff 66 ee 55


最后结果:ff 66 ee 55 dd 44 cc 33(左:低内存;右:高内存)


九、实验结果分析


文件类型

原文件大小

压缩后文件大小

压缩效率

word文档

31k

14k

54.83%

ppt文档

209k

152k

27.3%

mp3音乐

4567k

4558k

0.20%

exe应用程序

56694k

56694k

0

MP4视频

9964k

9957k

0.07%

wma音乐

3457k

3429k

0.81%

excel文档

342k

158k

53.8%

WinRAR压缩文件

2038k

2039k

-0.05%

HTML文档

157k

114k

27.4%

avi视频

81k

56k

30.9%

 

(写在后面)

这篇文百分之九十九的内容都是两年前的写的,想起那时可以把整个程序都默写下来,心里还是挺多感触的,好好珍惜剩下不多的一年多的时光,加油共勉。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值