实验目的:
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% |
(写在后面)
这篇文百分之九十九的内容都是两年前的写的,想起那时可以把整个程序都默写下来,心里还是挺多感触的,好好珍惜剩下不多的一年多的时光,加油共勉。