自第一台计算机诞生,其最小存储单元就被永久的定格了——一个由8个比特(bit)组成的称为字节(byte)的单位。计算机的所有内存以字节数组的方式进行编址。 当一个逻辑上长于一个字节的整形数据放置在内存中时(比如16位,32位,和64位的整数),计算机设计者需要考虑这些字节的存储顺序。一些体系结构的设计者选择了将字节的逻辑顺序与物理顺序一致,即将逻辑上较低的字节放置在物理上较低的字节上;另外一些设计者则选择了将字节的逻辑顺序与物理顺序相反,即将逻辑上较低的字节放置在物理上较高的字节上。前者被称为“little endian”,比如Intel x86系列;后者则被称为“big endian”,比如Motorola的PowerPC以及Sun Sparc。还有一些平台同时支持两种方案,由开发者决定使用哪一种。 两种选择为底层开发者带来了一定的困扰。比如,两个字节顺序不一致的平台之间进行通信,或者在两个字节顺序不一致的平台之间移植系统。这都是跨平台的例子,对于这些情况,字节顺序的问题是不能回避的。对于仅仅在一种平台上进行开发的程序员而言,如果它能够避免强制类型转换(比如将字节数组强制转换为一个长整数),一贯的以逻辑顺序来操作大于一个字节的整数,应该可以回避这个问题。但由于C语言是一种非常灵活的语言,有时候通过强制类型转换可以让代码非常精简,甚至达到非常巧妙的效果,所以,要求C程序员完全回避这个问题,几乎是不现实的。 由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。 两派的支持者争论不休,正像他们所支持名词(big endian和little endian)的典故所讲述的那样:Little Endian和Big Endian这两个名词来源于Jonathan Swift的《格利佛游记》其中交战的两个派别无法就应该从哪一端--小端还是大端--打开一个半熟的鸡蛋达成一致。:) 在那个时代,Swift是在讽刺英国和法国之间的持续冲突,Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了(摘自《深入理解计算机系统》)。 需要特别指出的是,通常所提到的Little Endian和Big Endian仅仅指字节顺序。在硬件设计者的术语中,对于一个字节内部的bit顺序也分Little Endian和Big Endian,但对于程序员而言,这些bit顺序的不同是透明的,也就是说,程序员只需要按照逻辑顺序来看待和操作字节内部的bit即可。 Endian的不同不仅仅带来字节顺序的不同,还有更多的问题。如果C程序员在定义一个结构体时,使用了bitwise的域定义,比如: struct foo { 这个结构体的一个对象会占用4个字节。由于a,b,c,d的类型都是int,所以他们都在以int32为单位的整数上分配bit,另外,由于他们的bit数量正好等于int32的bit数,所以,它们都分配于一个int所占用的空间。关键问题在于这些字节在这4个字节内是分配顺序是怎么样的? 对于little endian,其分配顺序与逻辑顺序是一致的,即在byte[0]的bit[0~2]上分配a,在byte[0]的bit[3,7]以及byte[1]的bit[0,1]上分配b,依次类推。 对于big endian,其方案会带来很大的问题。其分配顺序为: 字节物理顺序:从低到高; 也就是说,big endian在bitwise的分配方案上,从字节顺序到bit顺序都反过来了(因为其正向存储顺序为:字节从高到底,bit从低到高(从程序员的观点看))。换句话说:big endian的bit分配顺序为,按照bit的逻辑顺序,从高到底进行分配。
|--------|--------|--------|--------| Bitwise allocation |-a-|---b---|------c------|----d----| 请注意,并不是硬件平台使用的这种方案,而是C语言编译器。这是一种荒谬的方案,我想可能是C语言编译器的早期开发者希望通过编译器屏蔽掉big endian和little endian在bitwise allocation上的差异,而都与物理存储顺序一致。但由于其采用了bit order的反向分配,反而加剧了这种差异,随后的编译器为了保持兼容,也只好将错误延续了下来。 基于这种原因,在C语言中直接使用bitwise的方式定义结构体是一种危险的方式,因为这些代码是平台依赖的。当进行跨平台移植的时候必须重新定义这些结构体。 有两种方式可以消除这种风险: 1、使用逻辑移位的方式来操作bit;以上面的例子为例,我们可以这么做: struct foo { #define SET_A(f,a) do { (f) |= ((a)&0x7); } while(0) #define GET_A(f) ((f)&0x7) 2、对于big endian,我们可以使用相反的顺序来声明bitwise fields。仍然以上例为例: #if LITTLE_ENDIAN struct foo { 对于little endian,逻辑顺序与物理顺序一致,只需要按照原样定义;而对于big endian,由于其整体的bit顺序恰好与逻辑顺序是相反的,所以,我们将顺序反过来,使其bit的分配顺序与逻辑顺序一致即可。 |
触类旁通 举一反三
我们从中学习到的就是:
(1)将不同的画面类比为WIN32中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;
(2)给各个画面提供一个功能键"消息"处理函数,该函数接收按键信息为参数;
(3)在各画面的功能键"消息"处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。
/* 将窗口元素、消息处理函数封装在窗口中 */
struct windows
{
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE keyvalue);
…
};
/* 消息处理函数 */
void messageFunction(BYTE keyvalue)
{
BYTE i = 0;
/* 获得焦点元素 */
while ( (element [i].ID!= currentFocus)&& (i < ELEMENT_NUM) )
{
i++;
}
/* "消息映射" */
if(i < ELEMENT_NUM)
{
switch(keyvalue)
{
case OK:
element[i].OnOk();
break;
…
}
}
}
在窗口的消息处理函数中调用相应元素按键函数的过程类似于"消息映射",这是我们从WIN32编程中学习到的。编程到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的"拿来主义"。
在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC中处理MESSAGE_MAP的方法,我们也可以学习MFC定义几个精妙的宏来实现"消息映射"。
处理数字键
用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x坐标,y坐标)。此外,程序还需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起:
/* 用户数字输入结构体 */
typedef struct tagInputNum
{
BYTE byNum; /* 接收用户输入赋值 */
BYTE xPos; /* 数字输入在屏幕上的显示位置x坐标 */
BYTE yPos; /* 数字输入在屏幕上的显示位置y坐标 */
}InputNum, *LPInputNum;
那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:
InputNum inputElement[NUM_LENGTH]; /* 接收用户数字输入的数组 */
/* 数字按键处理函数 */
extern void onNumKey(BYTE num)
{
if(num==0|| num==1) /* 只接收二进制输入 */
{
/* 在屏幕上显示用户输入 */
DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, "%1d", num);
/* 将输入赋值给数组元素 */
inputElement[currentElementInputPlace].byNum = num;
/* 焦点及光标右移 */
moveToRight();
}
}
将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。
整理用户输入
继续第2节的例子,在第2节的onNumKey函数中,只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬如要转化为有效的XXX数据,其方法是:
/* 从2进制数据位转化为有效数据:XXX */
void convertToXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
XXX += inputElement[i].byNum*power(2, NUM_LENGTH - i - 1);
}
}
反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:
/* 从有效数据转化为2进制数据位:XXX */
void convertFromXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
inputElement[i].byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;
}
}
当然在上面的例子中,因为数据是2进制的,用power函数不是很好的选择,直接用"<< >>"移位操作效率更高,我们仅是为了说明问题的方便。试想,如果用户输入是十进制的,power函数或许是唯一的选择了。
是思想而不是语言本身
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!
笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
要求以键盘上的"← →"键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下OK键 */
void onOkKey()
{
/* 判断在什么焦点菜单上按下Ok键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnOk();
break;
case MENU2:
menu2OnOk();
break;
…
}
}
/* 按下Cancel键 */
void onCancelKey()
{
/* 判断在什么焦点菜单上按下Cancel键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnCancel();
break;
case MENU2:
menu2OnCancel();
break;
…
}
}
终于有一天,我这样做了:
/* 将菜单的属性和操作"封装"在一起 */
typedef struct tagSysMenu
{
char *text; /* 菜单的文本 */
BYTE xPos; /* 菜单在LCD上的x坐标 */
BYTE yPos; /* 菜单在LCD上的y坐标 */
void (*onOkFun)(); /* 在该菜单上按下ok键的处理函数指针 */
void (*onCancelFun)(); /* 在该菜单上按下cancel键的处理函数指针 */
}SysMenu, *LPSysMenu;
当我定义菜单时,只需要这样:
static SysMenu menu[MENU_NUM] =
{
{
"menu1", 0, 48, menu1OnOk, menu1OnCancel
}
,
{
" menu2", 7, 48, menu2OnOk, menu2OnCancel
}
,
{
" menu3", 7, 48, menu3OnOk, menu3OnCancel
}
,
{
" menu4", 7, 48, menu4OnOk, menu4OnCancel
}
…
};
OK键和CANCEL键的处理变成:
/* 按下OK键 */
void onOkKey()
{
menu[currentFocusMenu].onOkFun();
}
/* 按下Cancel键 */
void onCancelKey()
{
menu[currentFocusMenu].onCancelFun();
}
程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。
面向对象,真神了!
用C开发的小知识
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中p++后的结果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的结果等同于:p = p+sizeof(int),而p-(或-p)的结果是p = p-sizeof(int)。
同理,若执行:
long int *p = (long int *)0xF000FF00;
则p++(或++p)的结果等同于:p = p+sizeof(long int) ,而p-(或-p)的结果是p = p-sizeof(long int)。
记住:CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作内存是相当重要的。
函数指针
首先要理解以下三个问题:
(1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于"调转指令+参数传递处理+回归位置入栈",本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以"调用"一个根本就不存在的函数实体,晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:
typedef void (*lpFunction) ( ); /* 定义一个无参数、无返回类型的 */
/* 函数指针类型 */
lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定义一个函数指针,指向*/
/* CPU启动后所执行第一条指令的位置 */
lpReset(); /* 调用函数 */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起到了"软重启"的作用,跳转到CPU启动后第一条要执行的指令的位置。
记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!
关键字const
const意味着"只读"。区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲哀:
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
(1) 关键字const的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const关键字意味着这个参数在函数体内不会被修改,属于"输入参数"。在有多个形参的时候,函数的调用者可以凭借参数前是否有const关键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。
(2)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样可以减少bug的出现。
const在C++语言中则包含了更丰富的含义,而在C语言中仅意味着:"只能读的普通变量",可以称其为"不能改变的变量"(这个说法似乎很拗口,但却最准确的表达了C语言中const的本质),在编译阶段需要的常数仍然只能以#define宏定义!故在C语言中如下程序是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:编译阶段不能用到变量 */
=======================================
关键字volatile
C语言编译器会对用户书写的代码进行优化,譬如如下代码:
int a,b,c;
a = inWord(0x100); /*读取I/O空间0x100端口的内容存入a变量*/
b = a;
a = inWord (0x100); /*再次读取I/O空间0x100端口的内容存入a变量*/
c = a;
很可能被编译器优化为:
int a,b,c;
a = inWord(0x100); /*读取I/O空间0x100端口的内容存入a变量*/
b = a;
c = a;
但是这样的优化结果可能导致错误,如果I/O空间0x100端口的内容在执行第一次读操作后被其它程序写入新值,则其实第2次读操作读出的内容与第一次不同,b和c的值应该不同。在变量a的定义前加上volatile关键字可以防止编译器的类似优化,正确的做法是:
volatile int a;
volatile变量可能用于如下几种情况:
(1) 并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);
(2) 一个中断服务子程序中会访问到的非自动变量(也就是全局变量);
(3) 多线程应用中被几个任务共享的变量。
CPU字长与存储器位宽不一致处理
在背景篇中提到,本文特意选择了一个与CPU字长不一致的存储芯片,就是为了进行本节的讨论,解决CPU字长与存储器位宽不一致的情况。80186的字长为16,而NVRAM的位宽为8,在这种情况下,我们需要为NVRAM提供读写字节、字的接口,如下:
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函数功能:读NVRAM中字节
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字节值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
return *lpAddr;
}
/* 函数功能:读NVRAM中字
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
WORD wTmp = 0;
LPBYTE lpAddr;
/* 读取高位字节 */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
wTmp += (*lpAddr)*256;
/* 读取低位字节 */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 为什么偏移要×2? */
wTmp += *lpAddr;
return wTmp;
}
/* 函数功能:向NVRAM中写一个字节
*参数:wOffset,写入位置相对NVRAM基地址的偏移
* byData,欲写入的字节
*/
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{
…
}
/* 函数功能:向NVRAM中写一个字 */
*参数:wOffset,写入位置相对NVRAM基地址的偏移
* wData,欲写入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
…
}
子贡问曰:Why偏移要乘以2?
子曰:请看图1,16位80186与8位NVRAM之间互连只能以地址线A1对其A0,CPU本身的A0与NVRAM不连接。因此,NVRAM的地址只能是偶数地址,故每次以0x10为单位前进!
[ 相关贴图 ]
子贡再问:So why 80186的地址线A0不与NVRAM的A0连接?
子曰:请看《IT论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。
内存的问题
今天想了我们模块合在现在机顶盒软件上的问题,主要还是内存分配的问题,我想模块之间要尽量独立,尽量不能让别的模块的问题牵扯进来
我首先想到的一个办法是
/* allocate File data buffer */
BufferData= (char*) memory_allocate( SystemPartition, (U32) FileSize);
原来是在SystemPartition 上分配 。ST_Partition_t *SystemPartition = &TheSystemPartition;
初始的堆栈为 partition_init_heap(&TheSystemPartition,
(U8 *) SystemPartitionBase,
SystemPartitionSize);
那我现在可以自己定义一个MyUserPartition,先在alliref_common.cfg 中做一下修改
## Addresses & sizes of cached & non-cached areas of External Memory.
## The area defined by NCACHE_BASE and NCACHE_SIZE will contain the
## 'Non-cached' memory partition. The size of this area may be reduced if
## required (NB subject to hardware limitations!)
## IMPORTANT: These definitions reflect the constants of the same
## name in mb361.h. If one of these files is modified then the
## other must be manually updated accordingly. */
NCACHE_SIZE = (1*M)
NCACHE_BASE = EXTMEM_BASE
CACHED_SIZE = (EXTMEM_SIZE - NCACHE_SIZE - TH_SIZE - TRACE_BUFFER_SIZE-UDATA_SIZE)
CACHED_BASE = (EXTMEM_BASE + NCACHE_SIZE+UDATA_SIZE)
USERDATA_SIZE = (4500*K)
USERDATA_BASE = (EXTMEM_BASE + NCACHE_SIZE)
## divide the stack and heap
stack EXTERNAL (64*K)
heap EXTERNAL (800*K)
define NcachePartitionBase "NCACHE_BASE"
define NcachePartitionSize "NCACHE_SIZE"
define InternalPartitionBase "(addressof INTERNAL) + (sizeused INTERNAL)"
define InternalPartitionSize "(sizeof INTERNAL) - (sizeused INTERNAL)"
define UserDataPartitionBase "USERDATA_BASE"
define UserDataPartitionSize "USERDATA_SIZE"
define SystemPartitionBase "(addressof EXTERNAL) + (sizeused EXTERNAL)"
define SystemPartitionSize "(sizeof EXTERNAL) - (sizeused EXTERNAL)"
define CacheBaseAddress "SMI_CACHED_BASE"
define CacheSize "SMI_CACHED_SIZE"
define AVMEM_BASE_ADDRESS "AVMEM_SMI_BASE"
##define AVMEM_SMI_BASE "AVMEM_SMI_BASE"
define AVMEM_SMI_SIZE "AVMEM_SMI_SIZE"
partition_init_heap(&TheUserPartition,
(U8 *) UserDataPartitionBase,
UserDataPartitionSize);
ST_Partition_t *MyUserPartition= &TheUserPartition;
typedef struct
{
ST_Partition_t* DriverPartition;
}User_Handle_t;
User_Handle_t *UserHandle;
UserHandle = (User_Handle_t*) memory_allocate(MyUserPartition,1024*1024*4);
//分出4M空间,如果分不出的话直接使用 MyUserPartition 当然MyUserPartition 都用不起来就没有办法了
if( UserHandle ==NULL )
{
if ( MyUserPartition !=NULL)
{
(UserHandle)->DriverPartition = MyUserPartition ;
}
else
{
return(MALLOC_FAIL);
}
}
然后定义一个#define MEMALLOC(_size_) memory_allocate(UserHandle->DriverPartition, _size_)
以后就可以这样分配内存了,这里面最大的危险就是我并不能确定 MyUserPartition 能不能用起来除非别的模块也用这个MEMALLOC
第二种办法就是和别的模块共用内存
EVENT_INFO_STRUCT pastSchEvtInfoTable[EIT_PROGRAM_NUM][EIT_DAY_NUM][EIT_EVENT_NUM_PER_DAY]; /*[ProgNo][Day][Event]*/
EVENT_INFO_STRUCT pastPfEvtInfoTable[EIT_PROGRAM_NUM][2];
EVENT_INTRO_STRUCT pastEvtIntroTable[EIT_SHORT_EVENT_NUM];
这三个用了4M内存
我用union 的结构共用内存
typedef struct
{
EVENT_INFO_STRUCT pastSchEvtInfoTable1[EIT_PROGRAM_NUM][EIT_DAY_NUM][EIT_EVENT_NUM_PER_DAY];
}STRUCT_1;
typedef struct
{
EVENT_INFO_STRUCT pastPfEvtInfoTable2[EIT_PROGRAM_NUM][2];
}STRUCT_2;
typedef struct
{
EVENT_INTRO_STRUCT pastEvtIntroTable3[EIT_SHORT_EVENT_NUM];
}STRUCT_3;
union
{
struct
{
STRUCT_1 pastSchEvtInfoTable1;
STRUCT_2 pastPfEvtInfoTable2;
STRUCT_3 pastEvtIntroTable3;
}vedio;
struct
{
ST_Partition_t DriverPartition;
}UserHandle;
}myBss;
STRUCT_1 * myStruct1 = &myBss.vedio.pastSchEvtInfoTable1;
STRUCT_2 * myStruct2 = &myBss.vedio.pastPfEvtInfoTable2;
STRUCT_3 * myStruct3 = &myBss.vedio.pastEvtIntroTable3;
#define pastSchEvtInfoTable myStruct1->pastSchEvtInfoTable1
#define pastPfEvtInfoTable myStruct2->pastPfEvtInfoTable2
#define pastEvtIntroTable myStruct3->pastEvtIntroTable3
#define MEMALLOC(_size_) memory_allocate(myBss.UserHandle.DriverPartition, _size_)
这个办法不知道可不可以? 我总觉得这个union的结构怪怪的,好象绕了一圈又绕回原来的地方了
用 C# 读取二进制文件
![]() |
在 C 和 C++ 中,读取二进制文件还是很容易的。除了一些开始符(carriage return)和结束符(line feed)的问题,每一个读到C/C++中的文件都是二进制文件。事实上,C/C++ 只知道二进制文件,以及如何让二进制文件像文本文件一样。当我们使用的语言越来越抽象时,我们最后使用的语言就不能直接、容易的读取创建的文件了。这些语言想要用它们自己独特的方式来自动处理输出数据。
问题的所在
在许多计算机科学领域,C 和 C++ 仍旧直接依照数据结构来储存和读取数据。在C和C++中,依照内存中的数据结构来读取和写文件,是十分简单的。在C中,你只需要使用fwrite()函数,并提供下列参数:一个指向你的数据的指针,告诉它有多少个数据,一个数据有多大。这样,就直接用二进制格式把数据写成文件了。
如上所述的那样把数据写成文件,同时如果你也知道其正确的数据结构的话,那么也就意味着读取文件也很容易。你只要使用 fread() 函数,并提供下列参数:一个文件句柄,一个指向数据的指针,读取多少个数据,每一个数据的长度。 fread() 函数帮你把其余的事都做了。突然,数据又回到了内存中。没有采用解析以及也没有对象模型的方式,它只是把文件直接的读到内存中。
在C和C++中,最大的两个问题就是数据对齐(structure alignment)和字节交换(byte swapping)。数据对齐指的是有时编译器会跳过数据中间的字节,因为如果处理器访问到那些字节,就不再处于最优化状态下了,要花费更多的时间(一般情况,处理器访问未对齐数据花费的时间是访问对齐数据的两倍),花费更多的指令。因此,编译器要为了执行速度而进行优化,跳过了那些字节并重新进行排序。另一方面,字节交换指的是:由于不同处理器对字节排序的方式不同,需要对数据的字节重新排序的过程。
数据对齐
因为处理器能够一次处理更多的信息(在一个时钟周期内),所以它们希望它们所处理的信息能以一种确定的方式排列。大多数的 Intel 处理器使整数类型(32位的)的储存首地址能被4除尽(即:从能被4除尽的地址上开始储存)。如果内存中的整数不是储存在4的倍数的地址上的话,它们是不会工作的。编译器知道这些。因此当编译器遇到一个可能引起这种问题的数据时,它们就有下面三种选择。
第一种,它们可以选择在数据中添加一些无用的白空格符,这样可以使整数的开始地址能被4除尽。这是一种最普遍的做法。第二种,它们可以对字段重新排序,以便使整数处于4位的边界上。因为这样会造成其它有趣的问题,因此,这种方式较少使用。第三种选择是,允许数据中的整数不处于4位的边界上,但是把代码复制到一个合适的地方从而使那些整数处于4位的边界上。这种方式需要一些额外的时间花费,但是,如果必须压缩的话,那么它就很有用了。
以上所说的这些大都是编译器的细节问题,你用不着过多的担心。如果你对写数据的程序和读数据的程序使用同样的编译器,同样的设定,那么,这些就不成其为问题了。编译器用同样的方法来处理同样的数据,一切都OK。但是当你涉及到跨平台文件转换问题时,用正确的方式来排列所有数据就显得很重要了,这样才能保证信息能被转换。另外,一些程序员还了解怎样让编译器不用理睬他们的数据。
字节交换(byte swapping):高位优先(big endians)和低位优先(little endians)
![]() |
高位优先和低位优先,指的是两种不同的方式,把整数储存在计算机中的的方式。因为整数是多于一个字节的,那么,问题在于:最重要的字节是否应该首先被读写。最不重要的字节是变化的最频繁的。这就是,如果你不断给一个整数加一,最不重要的字节要改变256次,次不重要的字节才只变化一次。
不同的处理器用不同的方式储存整数。Intel 处理器一般用低位优先方式来储存整数,换句话说,低位首先被读写。大多数其它处理器用高位优先方式来储存整数。因此,当二进制文件在不同平台上读写时,你就有可能不得不对字节重新排序以便得到正确的顺序。
在 UNIX 平台上,还有一种特殊的问题,因为UNIX可以在Sun Sparc处理器、HP处理器、IBM Power PC、Inter的芯片等多种处理器上运行。当从一种处理器转移到另一种处理器上时,就意味着那些变量的字节排列顺序必须翻转,以便于它们能满足新处理器所要求的顺序。
用 C# 处理二进制文件
用 C# 处理二进制文件的话,就会有另外两项新的挑战。第一项挑战是:所有的 .NET 语言都是强类型的。因此,你不得不从文件中的字节流转换为你所想要的数据类型。第二项挑战就是:一些数据类型比它们表面上要复杂的多,需要某种转换。
类型破坏(type breaking)
因为 .NET 语言,包括 C#,都是强类型的,你不能只是任意的从文件中读取一段字节,然后塞到数据结构中就一切OK了。因此当你要破坏类型转换规则时,你就不得不这样做了,首先读取你所需要的字节数到一个字节数组中,然后把它们从头到尾的复制到数据结构中。
在 Usenet (注:世界性的新闻组网络系统)的文档中搜寻,你会找到几个构架在 microsoft.public.dotnet层次上的一组程序,它们可以容许你把任何对象转换为一系列字节,并可以重新转换回对象。它们可以在下面地址找到 Listing A
复杂的数据类型
在 C++ 中,你明白什么是对象,什么是数组,什么既不是对象又不是数组。但是在 C# 中,事情并不像看起来的那样简单。一个字符串(string)就是一个对象,因此也是一个数组。因为在 C# 中,既没有真正的数组,许多对象也没有固定尺寸,因此一些复杂数据类型并不适合成为固定尺寸的二进制数据。
幸好, .NET 提供了一种方式来解决这种问题。你可以告诉 C# ,你想怎样处理你的字符串(string)和其它类型的数组。这将通过 MarshalAs 属性来完成。下面这个例子,就是在 C# 中使用字符串,这属性必须要在所控制的数据使用之前被使用:
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
你想要从二进制文件中读取,或者储存到二进制文件中的字符串(string)的长度就决定了参数 SizeConst 的大小。这样就确定了字符串长度的最大值。
解决以前的问题
![]() |
现在,你知道了 .NET 引入的问题是怎样被解决的了。那么,在后面,你就可以了解到,解决前面所遇到的二进制文件问题是那么的容易。
包装(pack)
不用麻烦的去设定编译器来控制如何排列数据。你只需使用 StructLayout 属性就可以使数据依照你的意愿来排列或打包。当你需要不同的数据有着不同的包装方式的时候,这就显得十分有用了。这就像装扮你的汽车一样,任你的喜好。使用 StructLayout 属性就像你很小心的决定是否把每一个数据都紧凑包装或者还是只将它们随便打发,只要它们能够被重新读出来就行了。 StructLayout 属性的使用如下面所示:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
这样做可以使数据忽略边界对齐,让数据尽可能的紧凑包装。这个属性应当和你从二进制文件中读取的任何数据的属性都保持一致(即:你写到文件中的属性应和从文件读出来属性保持不变)。
你也许会发现,即使给你的数据加上了这个属性后,也没有完全解决问题。在某些情况下,你可能不得不进行沉闷冗长的反复实验。由于不同计算机和编译器在二进制层次上的有着不同的运行处理方式,这就是引起上述问题的原因。特别是在跨平台时,我们都必须特别小心的处理二进制数据。 .NET 是个好工具,适合其它二进制文件,但是也并不是一个完美的工具。
字节排列顺序的翻转(endian flipping)
读写二进制文件的经典问题之一就是:某些计算机首先是储存最不重要的字节(如:Inter),而另外一些计算机是首先储存最重要的字节。在 C 和 C++ 中,你不得不手动处理这个问题,而且只能是一个字段一个字段的翻转。而 .NET 框架的优点之一就是:代码可以在运行时访问类型的元数据(metadata),你也就能够读取信息,并使用它来自动解决数据中每一段的字节排列顺序问题。在 Listing B 上可以找到源代码,你可以了解是如何处理的。
一旦你得知对象的类型,你能够获得数据里的每个部分,并开始检查每一个部分,并确定其是否是一个16位或32位的无符号整数。在任何一种上述情况下,你都可以改变字节的排序顺序,而且不会破坏数据。
注意:你不是用字符串类(string)来完成所有的事。是采用高位优先还是低位优先,并不会影响到字符串类。那些字段是不受翻转代码的影响。你也只是要注意无符号整数而已。因为,负数在不同的系统上,并不是使用同一种表示方式的。负数可以只用一个记号(一位字节)表示,但是更常用的,却是使用两个记号(两位字节)表示。这使得负数在跨平台时有些更困难。幸运的是,负数在二进制文件中极少使用。
这只是多说几句了,同样的,浮点数有时并不是用标准方式表示的。尽管大多数系统是以IEEE格式为基础来设置浮点数的,但是还是有一小部分老的系统使用了其它的格式来设置浮点数的。
克服困难
尽管 C# 还是有一些问题,但是你依旧能够使用它来读取二进制文件。实际上,由于 C# 所使用的那种用来访问对象的元数据(metadata)的方式,使它成为一种能够更好读取二进制文件的语言。因此, C# 能够自动解决整个数据的字节交换(byte swapping)问题。
对几组sizeof信息的分析
对几组sizeof信息的分析
在列举这几个例子前需要说明以下几点:
1、在Win32平台上,指针长度都是 4字节, char *、 int *、 double *如此,vbptr ( virtual base table pointer )、vfptr ( virtual function table pointer )也是如此;
2、对于结构体 (或类 ),编译器会自动进行成员变量的对齐,以提高运算效率。自然对齐 (natural alignment )也称默认对齐方式是按结构体的成员中size最大的成员对齐的,强制指定大于自然对齐大小的对齐方式是不起作用的。
3、不推荐强制对齐,大量使用强制对齐会严重影响处理器的处理效率。
范例 1: (一个简单的C语言的例子 )
void f ( int arr [])
{
cout << "sizeof(arr) = " << sizeof (arr ) << endl ; //当被作为参数进行传递时,数组失去了其大小信息
}
void main ()
{
char szBuf [] = "abc" ;
cout << "sizeof(szBuf) = " << sizeof (szBuf ) << endl ; //输出数组占用空间大小
char * pszBuf = szBuf ;
cout << "sizeof(pszBuf) = " << sizeof (pszBuf ) << endl ; //输出的是指针的大小
int iarr [ 3 ]; iarr ;
cout << "sizeof(iarr) = " << sizeof (iarr ) << endl ; //输出数组占用空间大小
f (iarr );
int * piarr = iarr ;
cout << "sizeof(piarr) = " << sizeof (piarr ) << endl ; //输出指针的大小
}
范例 2: (一个涉及alignment的例子 )
struct DATA1
{
char c1 ; //偏移量0,累积size = 1
char c2 ; //偏移量1,累积size = 1 + 1 = 2
short si ; //偏移量2,累积size = 2 + 2
};
struct DATA2
{
char c1 ; //偏移量0,累积size = 1
short si ; //偏移量1 + (1),累积size = 1 + (1) + 2 = 4
char c2 ; //偏移量4,累积size = 4 + 1 = 5,但按最大长度sizeof(short) = 2对齐,故最后取6
};
struct DATA3
{
char c1 ; //偏移量0,累积size = 1
double d ; //偏移量1 + (7),累积size = 1 + (7) + 8 = 16
char c2 ; //偏移量16,累积size = 16 + 1 = 17,但按最大长度sizeof(double) = 8对齐,故最后取24
};
#pragma pack(push,1) //强制1字节对齐
struct DATA4
{
char c1 ; //偏移量0,累积size = 1
double d ; //偏移量1,累积size = 1 + 8 = 9
char c2 ; //偏移量9,累积size = 9 + 1 = 10
};
#pragma pack(pop) //恢复默认对齐方式
struct DATA5
{
char c1 ;
double d ;
char c2 ;
};
void main ()
{
cout << "sizeof(DATA1) = " << sizeof (DATA1 ) << endl ;
cout << "sizeof(DATA2) = " << sizeof (DATA2 ) << endl ;
cout << "sizeof(DATA3) = " << sizeof (DATA3 ) << endl ;
cout << "sizeof(DATA4) = " << sizeof (DATA4 ) << endl ;
cout << "sizeof(DATA5) = " << sizeof (DATA5 ) << endl ;
}
范例 3: (C ++语言特征对 sizeof的影响 )
class CA
{
};
class CB : public CA
{
public :
void func () {}
};
class CC : virtual public CA
{
};
class CD
{
int k ; //私有成员
public :
CD () {k = - 1 ;}
void printk () { cout << "k = " << k << endl ; }
};
class CE : public CD
{
};
class CF
{
virtual void func () {}
};
void main ()
{
cout << "sizeof(CA) = " << sizeof (CA ) << endl ; //为了区分不包含任何成员的类的不同的元素,编译器会自动为类添加一个匿名元素
cout << "sizeof(CB) = " << sizeof (CB ) << endl ; //与上面类似,编译器也为CB添加了一个匿名元素
cout << "sizeof(CC) = " << sizeof (CC ) << endl ; //虚拟继承中vbptr(virtual base table pointer)占用4个字节
cout << "sizeof(CD) = " << sizeof (CD ) << endl ;
cout << "sizeof(CE) = " << sizeof (CE ) << endl ; //访问权限控制是在编译期间由编译器控制的,所以虽然不能访问CD类的成员k,这里仍然占用了sizeof(int)大小的空间
//下面的代码进一步说明上述观点,由于在复杂的类层次结构中,当涉及到虚函数或者虚拟继承等时,有些信息是运行期动态生成的,故请勿效仿以下方法对对象进行修改
CE e ;
e .printk ();
memset (&e , 0 , sizeof (CE ));
e .printk (); //从这里可以看出,上面的memset操作修改了CD类的私有成员k
cout << "sizeof(CF) = " << sizeof (CF ) << endl ; //虚函数表指针占有4个字节
}
结构体对齐的具体含义(#pragma pack)
结构体对齐的具体含义(#pragma pack)
作者:panic 2005年4月2日
还是来自csdn的帖子:
主 题: 探讨:内存对齐
作 者: typedef_chen ((名未定)(我要骗人))
等 级:
信 誉 值: 100
所属论坛: C/C++ C++ 语言
问题点数: 50
回复次数: 1
发表时间: 2005-04-02 22:53:27
朋友帖了如下一段代码:
#pragma pack(4)
class TestB
{
public:
int aa;
char a;
short b;
char c;
};
int nSize = sizeof(TestB);
这里nSize结果为12,在预料之中。
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class TestC
{
public:
char a;
short b;
char c;
};
int nSize = sizeof(TestC);
按照正常的填充方式nSize的结果应该是8,为什么结果显示nSize为6呢?
事实上,很多人对#pragma pack的理解是错误的。
#pragma pack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。
具体解释
#pragma pack(4)
class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。
如果
#pragma pack(2)
class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
//所以 sizeof(TestB)是10。
最后看原贴:
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class TestC
{
public:
char a;//第一个成员,放在[0]偏移的位置,
short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
char c;//第三个,自身长为1,放在[4]的位置。
};
//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6
//所以sizeof(TestC)是6。
感谢 Michael 提出疑问,在此补充:
当数据定义中出现__declspec( align() )时,指定类型的对齐长度还要用自身长度和这里指定的数值比较,然后取其中较大的。最终类/结构的对齐长度也需要和这个数值比较,然后取其中较大的。
可以这样理解, __declspec( align() ) 和 #pragma pack是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最大值,两者同时出现时,前者拥有更高的优先级。
__declspec( align() )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma pack规定的方式填充的,这时候类/结构的实际大小和内存格局的规则是这样的:
在__declspec( align() )之前,数据按照#pragma pack规定的方式填充,如前所述。当遇到__declspec( align() )的时候,首先寻找距离当前偏移向后最近的对齐点(满足对齐长度为max(数据自身长度,指定值) ),然后把被指定的数据类型从这个点开始填充,其后的数据类型从它的后面开始,仍然按照#pragma pack填充,直到遇到下一个__declspec( align() )。
当所有数据填充完毕,把结构的整体对齐数值和__declspec( align() )规定的值做比较,取其中较大的作为整个结构的对齐长度。
特别的,当__declspec( align() )指定的数值比对应类型长度小的时候,这个指定不起作用。
引自:http://blog.vckbase.com/panic/archive/2005/04/02/4340.aspx
sizeof和内存对齐
struct sample1
{
char a; /// sizeof(char) = 1
double b; /// sizeof(double) = 8
};
///default( 缺省#pragam pack(8) ——VC6和VC71,其它编译器,个人未知 )
///1+8 = 9 —> 16( 8 < 9 < 16 )
#pragma pack( 4 )
///1+8 = 9 —> 12( 8 < 9 < 12 )
#pragma pack( 2 )
///1+8 = 9 —> 10( 8 < 9 < 10 )
#pragma pack( 1 )
///1+8 = 9 —> 9
#pragma pack( 16 )
///1+8 = 9 —> 16( 16—>8 ---- 8 < 9 < 16 )
struct sample2
{
char a; ///1
int b; ///4
};
#pragma pack( 8 )
/// 1 + 4 = 5 —> 8( 8 —> 4 )
#pragma pack( 16 )
/// 1 + 4 = 5 —> 8( 16 —> 4 )
说明:#pragma pack告诉编译器进行内存边界对齐,一般都是采用编译器的设置对整个项目采用同一对齐方案,而且通常为缺省8字节对齐。
sizeof(联合)这个值是怎么计算的 内存管理
解惑:sizeof(联合)这个值是怎么计算的
[不要只做技术]在论坛上问如下代码结果为什么是24?
union DATE { char a; int i[5]; double b; }; DATE max; cout<< sizeof(max) << endl;
这个问题很好回答,并且我把这个问题归结于基本概念题(就是入门书必须介绍的)。我想一般来说,做过内存管理的,对这个语言特性肯定不会陌生。
摘几句The C Programming Language里面讲述这个问题的原话,以说明读书还是必要的:
①联合就是一个结构,②它的所有成员相对于基地址的偏移量都为0,③此结构空间要大到足够容纳最“宽”的成员,④并且,其对齐方式要适合于联合中所有类型的成员。
怕有的兄弟还不明白,特附图一个帮助理解:
char a; | => | x | |||||||||||||||||||||||
int i[5]; | =>> | x | x | x | x | x | x | ||||||||||||||||||
double b; | => | x |
|
|
该结构要放得下int i[5]必须要至少占4×5=20个字节。如果没有double的话20个字节够用了,此时按4字节对齐。但是加入了double就必须考虑double的对齐方式,double是按照8字节对齐的,所以必须添加4个字节使其满足8×3=24,也就是必须也是8的倍数,这样一来就出来了24这个数字。综上所述,最终联合体的最小的size也要是所包含的所有类型的基本长度的最小公倍数才行。(这里的字节数均指winnt下的值,平台、编译器不同值也有可能不同。)
联合在存储分配的时候用的机会最多,因为很少有像存储分配这样需要给多种不同类型的变量分配空间而又打算尽可能的节约内存的,这很适合联合的特性。上述对齐的方式有个很有趣的用法也就常在存储分配里面使用。(下面依旧用 The C Programming Language中的例子作答)
typedef long Align; union header { struct { union header *ptr; unsigned size; } s; Align x; }
这里的Align有什么用?作用只有一个,就是强迫分配的结构体按long的长度对齐。
C语言的异常机制 setjump longjump函数
C语言的异常机制
usr_root : c99支持异常吗? 周星星 : 当然不 usr_root : 为什么是当然不?异常机制和c无缘? 七猫 : 当然,到现在还有很多人不喜欢异常。连有些C++的标准库都支持不带异常的。 周星星 : :)俺就是这样的人 usr_root : 哦,我是你的影子汗!
![](http://spaces.msn.com/Emoticons/QQ/icon18.gif)
/* ------------------------------------------------------------------------- ** File : cexcept.c * ** Coder: Spark Song. * ** Note : Use the example code from 《C Interfaces and Implementations》 * ** ------------------------------------------------------------------------- */ #include <setjmp.h> #include <stdlib.h> #include <stdio.h> #include <assert.h> int Allocation_handled = 0; jmp_buf Allocate_Failed; void *allocate(unsigned n) { void * new = (void *)malloc(n); if (new) return new; if (Allocation_handled) /* 如果实例化了异常处理程序的话... */ longjmp(Allocate_Failed, 1); /* 产生异常,并抛出 */ assert(0); /* 如果没有实例化异常处理程序,则此断言会报出运行期的错误 */ } int main(int argc, char *argv[]) { char *buf = 0; int count = 0; Allocation_handled = 1; /* 设置实例化异常的标志,设为1表示实例化了异常处理程序 */ if (setjmp(Allocate_Failed)) /* 实例化异常 */ { fprintf(stderr, "EXCEPT: Couldn't allocate the buffer/n"); exit(EXIT_FAILURE); } while(1) /* 测试代码:一直分配内存,直到没有内存为止。没有内存时会触发异常 */ { buf = (char *)allocate(4096000); printf("Allocate successs, the count is: %d/n", count++); } Allocation_handled = 0; return 0; }
上面这个例子在MingW下通过,编译时使用了-std=c89 -pedantic的编译开关(强制使用c89的语法检查)和-std=iso9899:199409 -pedantic(强制使用c99的语法检查)。运行结果如下:
Allocate successs, the count is: 1 ... ... Allocate successs, the count is: 447 Allocate successs, the count is: 448 Allocate successs, the count is: 449 Allocate successs, the count is: 450 EXCEPT: Couldn't allocate the buffer
简要讲述一下代码的流程:setjmp用来实例化异常处理程序,在这里我们的异常处理程序就是往stderr输出一个字符串并退出应用程序。setjmp会返回2次值(颇有些fork()的味道)。setjmp第一次返回值是在应用代码(这里就是main函数里面)调用setjmp的地方,这时候它实例化了异常处理程序,并返回0,所以异常处理程序的代码并没有被执行。在allocate中调用longjmp的时候,会引起setjmp第二次值的返回,此时的返回值由longjmp的第二个参数所决定。文中我们调用longjmp的时候,传给它的第二个参数是1,所以setjmp返回时会执行if中的异常处理程序。
这个例子就是最最简单的C语言处理异常的原型,我们完全可以利用它来构造出一整套的异常处理体系,一点也不比C++之类的高级语言差。为什么不把异常加入语言本身?我想这是由C语言的设计理念和设计目的决定的。C语言是面向底层和系统开发的较低级的语言,所以语言本身并不复杂,强大的功能完全可以通过函数库来实现。
欲更深入的了解C语言的异常处理体系的设计,可以参考David R.Hanson的C Interfaces and Implementations(中文版《C语言接口与实现》)。
Anyone know of a tasteful LGPL HTML parser in C?
> On Thursday 25 November 2004 06:53, Jeff Johnson wrote:
>
>
>> I'm certainly willing to listen to other ideas, the above is what
>> makes sense to me.
>>
>>
>
> For whatever it's worth, I'd rather just see a quick expat parse to
> extract what you need. You certainly don't want DOM and full SAX API
> is debatable. I think libxml2 brings a lot of baggage to solve
> globbing in html urls. I've used libxml2 in several projects, and it's
> really nice, mem is good, api is sane. But, if you're just grepping
> out URLs from <a>, then expat. Shoot, a regex hack would be quick and
> easy.
>
Yah, I'm rapidly discovering just how much HTML sucketh mightily.
I have several alternative fallbacks, including grabbing the quite
predictable and
easy to parse ls-lR file, or sticking with the already implemented
WebDAV Glob()
instead of grepping HTML directly. I do know that users will not be able to
tell when servers are WebDAV enabled, and I cannot force WebDAV on any
web mistresses who maintain package repositories.
There's also a chance that I might get permission to use the wget
html-parse.c,
which is rather tidy and appealing compared to other solutions I've
looked at.
So I'm trying to give it a shot, but if the solution is really pugly, I
will bail in a hurry.
>
> Not sure about your b) and d) reasons, though. Don't forget about all
> the crappy HTML out there ... it ain't pretty.
>
Luckily, the presentation of package repositories is difficult to
prettify, so perhaps
the HTML subset necessary to parse from package repositories is not so bad.
Dunno yet, just kicking tires so far.
b) There is info regarding XAR at
http://www.opendarwin.org/mailman/listinfo/xar.
There's a lot right with XAR format including
a) XML for metadata with DTD, and version, so additions to metadata
are perhaps easier
than with existing rpm Headers. The rpmlib API really really sux
(well, not as bad as HTML
parsers ;-) and it's the only way to get at metadata.
b) XAR format is similar to *.rpm packages, i.e. all the metatdata
is contiguous at the front, with payload behind.
c) XAR can represent extended attr's from several operating systems,
including linux, a non-trivial implementation.
d) XAR is a leading contendor for OpenDarwin's package format.
So an XAR like format may well be next generation *.rpm format. I know
of no better atm.
d) DV (aka Daniel Veillard) has been immensely helpful with rpm for
years, see http://rpmfind.net for one
useful service, DistURL: within *.spec syntax for another useful idea
(an xml URI used for package discovery).
But your warnings are noted and appreciated.
73 de Jeff
LYNX-DEV pre-announcing a new Lynx SGML.c parser
I have finished modifying the first stage "SGML" parsing in lynx
to be somewhat closer to a real SGML parser. Essentially, I have
extended the per-tag information (given in HTMLDTD.c) to include
more of the content model info of a real DTD, and done away with the
special treatment of some tags in SGML.c. end_element and also
start_element in SGML.c now do partial stack wind-downs, depending
on whether an element is "allowed" to close another one (and, in some
cases, whether the other element's end tag can be legally omitted).
This delivers to the next stage (HTML.c) a series of
HTML_{start,end}_element which are always correctly ordered, for all
elements which are not declared as SGML_EMPTY, and I have removed the
SGML_EMPTY flags from a number of tags that were specially treated
before, including P and (recently) FORM. Note that I haven't made
*any* changes to HTML.c to accomodate the changes in SGML.c and
HTMLDTD.c. It works with the unchanged HTML.c, which is great and
shows that these modules have remained reasonably independent of
each other; it does however not always give identical results
(screen appearance) even for valid HTML, which shows that sometimes
HTML.c is relying on specific hacks for specific elements in SGML.c
and the old "DTD". (for example, declaring P as SGML_EMPTY *and*
converting </P> to <P>).
I would like to have HTML.c in a form that it could deal equally with
being called from the modified SGML.c parser, as well as from the
old-style parser (with a, possibly increasing, number of hacks). This
would allow testing of recovery heuristics with the new parser and
comparison with the old way, without each time having to modify HTML.c.
Fote, I would appreciate your help here :). It would help if you at
least did not make changes to HTML.c that depend on new hacks introduced
in SGML.c and the HTMLDTD. (I am not saying that you *did* make such
changes recently; this is just a just-in-case request, I still haven't
checked whether the recent me->inUnderline changes fall in this
category. Your clarification sounded a bit like it, but I am not sure
so will have to check the code.)
(Also, I know and accept that you don't want to be considered an
"active developer" at this point. However, as long as you are often
the first to make required and/or useful changes, and make them
available, you'll have to accept that your mods continue to be at least
an important source of input for our development code :). Given that,
my request above could help cut down on my [not your] time.)
I will make the code available as a more-experimental-than-usual update
to the devel code, as soon as I have considered some other misc. unrelated
changes. Still without adapting HTML.c, 'cause I want this to get out
the door now, and would like people to test it... THe first goal then is
to reproduce Lynx's current behavior (as far as it is correct :) ) for
valid HTML, tweaking the recovery heuristics for invalid HTML will come
later. I am not sure whether there is any screwed-up HTML out there where
my approach *already* gives better results, or whether it finally can be
made sophisticated enough to generally improve treatment of bad HTML (over
that already done by Fote's latest hacks). Maybe a combination of
approaches will finally give best results.
解析Html页面:HTML Parser的试用
最近在研究lucene的全文检索,在很多地方需要解析或者说分析Html内容或者Html页面,Lucene本身的演示程序中也提供了一个Html Parser,但是不是纯Java的解决方案.于是到处搜索,在网上找到了一个"HTMLParser".
网址是: http://htmlparser.sourceforge.net ,当前版本为1.5.
下载下来,试用一番,感觉不错,完全能满足lucene解析Html的需求.
过几天贴出lucene进行全文检索的代码.(检索本站的文章等).
试用代码如下,供大家参考:
package com.jscud.test; import java.io.BufferedReader; import org.htmlparser.Node; import com.jscud.util.LogMan; //一个日志记录类 /** public class ParseHtmlTest public static void main(String[] args) throws Exception String content = readTextFile(aFile, "GBK"); test1(content); test2(content); test3(content); test4(content); test5(aFile); //访问外部资源,相对慢 } /** //设置编码 HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println(textInPage); /** HtmlPage visitor = new HtmlPage(myParser); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getTitle(); System.out.println(textInPage); /** TextExtractingVisitor visitor = new TextExtractingVisitor(); myParser.visitAllNodesWith(visitor); String textInPage = visitor.getExtractedText(); System.out.println(textInPage); /** myParser = Parser.createParser(content, "GBK"); NodeFilter textFilter = new NodeClassFilter(TextNode.class); //暂时不处理 meta OrFilter lastFilter = new OrFilter(); nodeList = myParser.parse(lastFilter); Node[] nodes = nodeList.toNodeArray(); for (int i = 0; i < nodes.length; i++) String line = ""; line = linknode.getLink(); if (isTrimEmpty(line)) System.out.println(line); /** myParser = Parser.createParser(content, null); nodes = myParser.extractAllNodesThatAre(TextNode.class); //exception could be thrown here for (int i = 0; i < nodes.length; i++) } /** try String dataLine = ""; ins.close(); return sbStr.toString(); /** /** } |
A Lexical Analyzer for HTML and Basic SGML
Specs, Drafts, and Reports
See also: abstracts of MIMESGML drafts
In reverse order, by publication/revision date:
-
A Lexical Analyzer for HTML and Basic SGML
-
W3C Tech Report on SGML low-level parsing details. Includes
flex spec,
test file,
change log, and source distribution (which is missing
filter.c):
-rw-rw-r-- 1 connolly 69 50650 Feb 7 11:59 sgml-lex-19960207.tar.gz -rw-rw-r-- 1 connolly 69 57182 Feb 7 12:00 sgml-lex-19960207.zip 21f7b70ec7135531bc84fd4c5e3cdf3d sgml-lex-19960207.tar.gz (pgp sig) 083e21759d223b1005402120cdbf8169 sgml-lex-19960207.zip (pgp sig)
HTML 2.0 Specification Review Materials
Toward a Formalism for Communication on the Web)
- unpublished draft by Dan Connolly
- 引自: http://www.w3.org/MarkUp/SGML/
- 可以做为文献查阅
HTML Parser and Generator Implementations
This is a sort of "Family Tree" of HTML parser implementations, annotated with notes on features and bugs.
I'm working on updating the HTML parser in our reference code. See: A Lexical Analyzer for HTML and Basic SGML.
See also: HTML Testing and Certification
-
SGML.c in LibWWW
-
The first HTML parser ever released was in the library/linemode distribution back in '92 or so. It supported broken markup such as:
<xmp>... </foo> ... </xmp> <a href=http://foo.bar/>...</a>
- NCSA Mosaic 2.4 -- didn't use CERN code, but was inspired by it.
- Spyglass Mosaic -- re-write of NCSA code
- Netscape -- re-implementation of NCSA code
- MS IE -- inspired by Netscape
htmllib.py used in
grail
- NCSA Mosaic 2.4 -- didn't use CERN code, but was inspired by it.
- Based on regexps. Guido wrote the first web spider, I believe. This parser treats P, LI, DT, DD as empty elements. Nifty formatter code. SGML Lexical Analyzer
Tools that Write HTML
-
LaTeX2HTML
- Creates documents with missing quotes around the attribute values.
PHP的XML分析函数
| |
HTML解析器
相关连接:
http://sourceforge.net/projects/jerichohtml/ projectpage
http://jerichohtml.sourceforge.net/ homepage
http://freshmeat.net/projects/jerichohtml/ projectpage
http://jerichohtml.sourceforge.net/api/index.html apidoc
作者也是在苦于其他的htmlparser无法满足自己的需求情况下开发的一个,感觉非常不错.
看样子也应该比较成熟了,于是下载看看能否适应我的需求.发现一共只有50个类左右(大部分都是数据bean,比如emlment,tag之类的),不依赖于其他三方的lib,所以可以直接就使用了.看了一下使用的sample:
截取部分代码如下:
import au.id.jericho.lib.html.*; import java.util.*; import java.io.*; import java.net.*; public class DisplayAllElements { public static void main(String[] args) throws Exception { String sourceUrlString="data/test.html"; if (args.length==0) System.err.println("Using default argument of /""+sourceUrlString+'"'); else sourceUrlString=args[0]; if (sourceUrlString.indexOf(':')==-1) sourceUrlString="file:"+sourceUrlString; URL sourceUrl=new URL(sourceUrlString); String htmlText=Util.getString(new InputStreamReader(sourceUrl.openStream())); Source source=new Source(htmlText); source.setLogWriter(new OutputStreamWriter(System.err)); // send log messages to stderr for (Iterator i=source.findAllElements().iterator(); i.hasNext(); ) { Element element=(Element)i.next(); System.out.println("---------------------------------" +"----------------------------------------------"); System.out.println(element.getDebugInfo()); System.out.println(element); } } }
可以看得出来处理过程先通过一个uri获取到html的字符串,其中uri可能是本地的协议(file://)也可能是其他的协议:http,ftp等方式的.html串以后就把该串传入一个Source类构造出实例来,这个Source的实例有几个方法可以进行内容的访问和标签的遍历(类的方法和api用法见http://jerichohtml.sourceforge.net/api/au/id/jericho/lib/html/Source.html).上述例子里面是使用了:findAllElements(),找出所有的elment.父类Segment里面还有一些可以找到注释等的函数.
其实这个html paser的处理方式类似于dom的解析方式,把整个字串都全部读入内存中,可以随机访问内容的每一个元素和内容.但是针对于海量的html恐怕速度上面会有些问题.
其他的几个开源html parser:
JavaCC HTML Parser by Quiotix Corporation (http://www.quiotix.com/downloads/html-parser/)
GNU GPL licence, expensive licence fee to use in commercial application. Does not support document structure (parses into a flat node stream).
Demonstrational HTML 3.2 parser bundled with JavaCC. Virtually useless.
JTidy (http://jtidy.sourceforge.net/)
Supports document structure, but by its very nature it "tidies" up anything it doesn't like in the source document. On first glance it looks like the positions of nodes in the source are accessible, at least in protected start and end fields in the Node class, but these are pointers into a different buffer and are of no use.
javax.swing.text.html.parser.Parser
Comes standard in the JDK. Supports document structure. Does not track the positions of nodes in the source text, but can be easily modified to do so (although not sure of legal implications of modifications). Requires a DTD to function, but only comes with HTML3.2 DTD which is unsuitable. Even if an HTML 4.01 DTD were found, the parser itself might need tweaking to cater for the new element types. The DTD needs to be in the format of a "bdtd" file, which is a binary format used only by Sun in this parser implementation. I have found many requests for a 4.01 bdtd file in newsgroups etc on the web, but they all reamain unanswered. Building it from scratch is not so easy.
Kizna HTML Parser v1.1 (http://htmlparser.sourceforge.net/)
GNU LGPL licence. Version 1.1 was very simple without support for document structure. I have since revisited this project at sourceforge (early 2004), where version 1.4 is now available. There are now two separate libraries, one with and one without document structure support. It claims to now also be capable of reproducing source text verbatim.
CyberNeko HTML Parser (http://www.apache.org/~andyc/neko/doc/html/index.html)
Apache-style licence. Supports document structure. Based on the very popular Xerces XML parser. At the time of evaluation this parser didn't regenerate the source accurately enough.
昨天听朋友讨论网络穿透的问题
问题分析:公司内部通常采用192。168。0。12 这类私有地址,然后通过一台路由器与internet相连(先不考虑防火墙)
我192.168.0.12向internet请求。www.google.com这个页面,那么路由器会开一个端口来和我192.168.0.12关联,并且它也是通过这个端口 来与www.google.com的http服务器交换数据。最终,网内机器->代理网关->HTTP服务器,在一个会话期间,各自的端口保持了映射关系,特别是代理网关(就是那台路由器)和网内机器的端口映射,使得代理网关不会把接收到的数据向网内转发时,发错了机器。好,现在问题归结为局域网内的机器在网关处,是靠什么技术来实现映射端口,并以此来实现Internet连接。
答案是NAT(Network Address Translators),网络地址转换 (Rfc1631 详见http://www.watersprings.org/pub/rfc/rfc1631.txt)
Stun协议(Rfc3489、详见http://www.watersprings.org/pub/rfc/rfc3489.txt)将NAT粗略分为4种类型,即 Full Cone、Restricted Cone、Port Restricted Cone和Symmetric。举个实际例子(例1)来说明这四种NAT的区别:
A机器在私网(192.168.0.4)
NAT服务器(210.21.12.140)
B机器在公网(210.15.27.166)
C机器在公网(210.15.27.140)
现在,A机器连接过B机器,假设是 A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8000)-> B(210.15.27.166:2000)。
同时A从来没有和C通信过。
则对于不同类型的NAT,有下列不同的结果:
Full Cone NAT:C发数据到210.21.12.140:8000,NAT会将数据包送到A(192.168.0.4:5000)。因为NAT上已经有了192.168.0.4:5000到210.21.12.140:8000的映射。
Restricted Cone:C无法和A通信,因为A从来没有和C通信过,NAT将拒绝C试图与A连接的动作。但B可以通过210.21.12.140:8000与A的 192.168.0.4:5000通信,且这里B可以使用任何端口与A通信。如:210.15.27.166:2001 -> 210.21.12.140:8000,NAT会送到A的5000端口上。
Port Restricted Cone:C无法与A通信,因为A从来没有和C通信过。而B也只能用它的210.15.27.166:2000与A的192.168.0.4:5000通信,因为A也从来没有和B的其他端口通信过。该类型NAT是端口受限的。
Symmetric NAT:上面3种类型,统称为Cone NAT,有一个共同点:只要是从同一个内部地址和端口出来的包,NAT都将它转换成同一个外部地址和端口。但是Symmetric有点不同,具体表现在:只要是从同一个内部地址和端口出来,且到同一个外部目标地址和端口,则NAT也都将它转换成同一个外部地址和端口。但如果从同一个内部地址和端口出来,是到另一个外部目标地址和端口,则NAT将使用不同的映射,转换成不同的端口(外部地址只有一个,故不变)。而且和Port Restricted Cone一样,只有曾经收到过内部地址发来包的外部地址,才能通过NAT映射后的地址向该内部地址发包。
现针对Symmetric NAT举例说明(例2):
A机器连接过B机器,假使是 A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8000)-> B(210.15.27.166:2000)
如果此时A机器(192.168.0.4:5000)还想连接C机器(210.15.27.140:2000),则NAT上产生一个新的映射,对应的转换可能为A(192.168.0.4:5000)-> NAT(转换后210.21.12.140:8001)-> C(210.15.27.140:2000)。此时,B只能用它的210.15.27.166:2000通过NAT的210.21.12.140: 8000与A的192.168.0.4:5000通信, C也只能用它的210.15.27.140:2000通过NAT的210.21.12.140:8001与A的192.168.0.4:5000通信,而 B或者C的其他端口则均不能和A的192.168.0.4:5000通信。
以上引自 http://linux.chinaunix.net/doc/netconf/2005-01-18/817.shtml
所以我们靠NAT来映射端口并实现Internet连接,因此,NAT也直接被称为“端口映射”。端口映射之后,在一个会话期间保持,对于TCP连接是直到连接断开才销毁,而对于UDP,却存在一个不定的生存期,例如2秒(不知道对不对)。
好,下面总结一下
因为企业内部通常采用私有地址,所以会启用地址转换NAT/PAT功能。NAT是将私有地址和公有地址一一对应,并在一定时间内保持这种对应关系。对于UDP通信,有两种类型的NAT。PAT则是将私有地址+TCP/UDP端口转换并为一个公有地址+一个TCP/UDP端口,通常会将多个私有地址对应一个公有地址,用不同的TCP/UDP端口进行区分。一些通信协议在穿越NAT/PAT时都会出现问题,因为通常NAT/PAT只对IP包头中的源IP地址/源端口号进行转换,不能相应地修改这些协议包内的源地址,这样协议在回传响应时都按其协议包内的源地址发送,结果就造成无法建立通信。
好,现在问题归结为如何穿越NAT实现P2P的通信 这个网上的资料比较多
觉得最后价值的是以下几篇
http://serious-code.net/moin.cgi/NatAndP2pApplications
http://www.brynosaurus.com/pub/net/p2pnat/
http://www.watersprings.org/pub/id/draft-takeda-symmetric-nat-traversal-00.txt
P2P之UDP穿透NAT的原理与实现(附源代码) http://www.ppcn.net/n1306c2.aspx
P2P之UDP穿透NAT的原理与实现--增强篇(附源代码) http://www.ppcn.net/n2422c38.aspx