查看文章

16位CRC校验原理与算法分析

2007-12-14 09:37

这里，不讨论CRC的纠错原理以及为什么要选下面提及的生成多项式，只是针对以下的生成多项式，如何获得CRC校验码，作一个比较详细的说明。

标准CRC生成多项式如下表：

名称 生成多项式 简记式* 标准引用
CRC-4 x4+x+1 3 ITU G.704
CRC-8 x8+x5+x4+1 0x31
CRC-8 x8+x2+x1+1 0x07
CRC-8 x8+x6+x4+x3+x2+x1 0x5E
CRC-12 x12+x11+x3+x+1 80F
CRC-16 x16+x15+x2+1 8005 IBM SDLC
CRC16-CCITT x16+x12+x5+1 1021 ISO HDLC, ITU X.25, V.34/V.41/V.42, PPP-FCS
CRC-32 x32+x26+x23+...+x2+x+1 04C11DB7 ZIP, RAR, IEEE 802 LAN/FDDI, IEEE 1394, PPP-FCS
CRC-32c x32+x28+x27+...+x8+x6+1 1EDC6F41 SCTP

生成多项式的最高位固定的1，故在简记式中忽略最高位1了，如0x1021实际是0x11021。
I、基本算法（人工笔算）：
以CRC16-CCITT为例进行说明，CRC校验码为16位，生成多项式17位。假如数据流为4字节：BYTE[3]、BYTE[2]、BYTE[1]、BYTE[0]；
数据流左移16位，相当于扩大256×256倍，再除以生成多项式0x11021，做不借位的除法运算（相当于按位异或），所得的余数就是CRC校验码。
发送时的数据流为6字节：BYTE[3]、BYTE[2]、BYTE[1]、BYTE[0]、CRC[1]、CRC[0]；

II、计算机算法1（比特型算法）：
1)将扩大后的数据流（6字节）高16位（BYTE[3]、BYTE[2]）放入一个长度为16的寄存器；
2)如果寄存器的首位为1，将寄存器左移1位(寄存器的最低位从下一个字节获得)，再与生成多项式的简记式异或；
否则仅将寄存器左移1位(寄存器的最低位从下一个字节获得)；
3)重复第2步，直到数据流（6字节）全部移入寄存器；
4)寄存器中的值则为CRC校验码CRC[1]、CRC[0]。

III、计算机算法2（字节型算法）：256^n表示256的n次方
把按字节排列的数据流表示成数学多项式，设数据流为BYTE[n]BYTE[n－1]BYTE[n－2]、、、BYTE[1]BYTE[0]，表示成数学表达式为BYTE[n]×256^n+BYTE[n-1]×256^(n-1)

+...+BYTE[1]*256+BYTE[0]，在这里+表示为异或运算。设生成多项式为G17（17bit），CRC码为CRC16。
则，CRC16＝(BYTE[n]×256^n+BYTE[n-1]×256^(n-1)+...+BYTE[1]×256+BYTE[0])×256^2/G17，即数据流左移16位，再除以生成多项式G17。
先变换BYTE[n-1]、BYTE[n-1]扩大后的形式，
CRC16＝BYTE[n]×256^n×256^2/G17+BYTE[n-1]×256^(n-1)×256^2/G17+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
＝(Z[n]+Y[n]/G17)×256^n+BYTE[n-1]×256^(n-1)×256^2/G17+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
＝Z[n]×256^n+{Y[n]×256/G17+BYTE[n-1]×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
＝Z[n]×256^n+{(YH8[n]×256+YHL[n])×256/G17+BYTE[n-1]×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
＝Z[n]×256^n+{YHL[n]×256/G17+(YH8[n]+BYTE[n-1])×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
这样就推导出，BYTE[n-1]字节的CRC校验码为{YHL[n]×256/G17+(YH8[n]+BYTE[n-1])×256^2/G17}，即上一字节CRC校验码Y[n]的高8位（YH8[n]）与本字节BYTE[n-1]异或，

该结果单独计算CRC校验码（即单字节的16位CRC校验码，对单字节可建立表格，预先生成对应的16位CRC校验码），所得的CRC校验码与上一字节CRC校验码Y[n]的低8位（YL8[n]）

乘以256（即左移8位）异或。然后依次逐个字节求出CRC，直到BYTE[0]。
字节型算法的一般描述为：本字节的CRC码，等于上一字节CRC码的低8位左移8位，与上一字节CRC右移8位同本字节异或后所得的CRC码异或。
字节型算法如下：
1)CRC寄存器组初始化为全"0"(0x0000)。（注意：CRC寄存器组初始化全为1时，最后CRC应取反。）
2)CRC寄存器组向左移8位,并保存到CRC寄存器组。
3)原CRC寄存器组高8位（右移8位）与数据字节进行异或运算，得出一个指向值表的索引。
4)索引所指的表值与CRC寄存器组做异或运算。
5)数据指针加1，如果数据没有全部处理完，则重复步骤2)。
6)得出CRC。

unsigned short GetCrc_16(unsigned char * pData, int nLength)
//函数功能：计算数据流* pData的16位CRC校验码，数据流长度为nLength
{
unsigned short cRc_16 = 0x0000; // 初始化

while(nLength>0)
{
cRc_16 = (cRc_16 << 8) ^ cRctable_16[((cRc_16>>8) ^ *pData) & 0xff]; //cRctable_16表由函数mK_cRctable生成
nLength--;
pData++;
}

return cRc_16;
}

void mK_cRctable(unsigned short gEnpoly)
//函数功能：生成0－255对应的16CRC校验码，其实就是计算机算法1（比特型算法）
//gEnpoly为生成多项式
//注意，低位先传送时，生成多项式应反转(低位与高位互换)。如CRC16-CCITT为0x1021，反转后为0x8408
{
unsigned short cRc_16=0;
unsigned short i,j,k;

for(i=0,k=0;i<256;i++,k++)
{
cRc_16 = i<<8;
for(j=8;j>0;j--)
{
if(cRc_16&0x8000) //反转时cRc_16&0x0001
cRc_16=(cRc_16<<=1)^gEnpoly; //反转时cRc_16=(cRc_16>>=1)^gEnpoly
else
cRc_16<<=1; //反转时cRc_16>>=1
}
cRctable_16[k] = cRc_16;
}
}


ASCII码表

信息在计算机上是用二进制表示的，这种表示法让人理解就很困难。因此计算机上都配有输入和输出设备，这些设备的主要目的就是，以一种人类可阅读的形式将信息在这些设备上显示出来供人阅读理解。为保证人类和设备，设备和计算机之间能进行正确的信息交换，人们编制的统一的信息交换代码，这就是ASCII码表，它的全称是“美国信息交换标准代码”。

八进制 | 十六进制 | 十进制 | 字符 | 八进制 | 十六进制 | 十进制 | 字符
00 | 00 | 0 | nul | 100 | 40 | 64 | @
01 | 01 | 1 | soh | 101 | 41 | 65 | A
02 | 02 | 2 | stx | 102 | 42 | 66 | B
03 | 03 | 3 | etx | 103 | 43 | 67 | C
04 | 04 | 4 | eot | 104 | 44 | 68 | D
05 | 05 | 5 | enq | 105 | 45 | 69 | E
06 | 06 | 6 | ack | 106 | 46 | 70 | F
07 | 07 | 7 | bel | 107 | 47 | 71 | G
10 | 08 | 8 | bs | 110 | 48 | 72 | H
11 | 09 | 9 | ht | 111 | 49 | 73 | I
12 | 0a | 10 | nl | 112 | 4a | 74 | J
13 | 0b | 11 | vt | 113 | 4b | 75 | K
14 | 0c | 12 | ff | 114 | 4c | 76 | L
15 | 0d | 13 | er | 115 | 4d | 77 | M
16 | 0e | 14 | so | 116 | 4e | 78 | N
17 | 0f | 15 | si | 117 | 4f | 79 | O
20 | 10 | 16 | dle | 120 | 50 | 80 | P
21 | 11 | 17 | dc1 | 121 | 51 | 81 | Q
22 | 12 | 18 | dc2 | 122 | 52 | 82 | R
23 | 13 | 19 | dc3 | 123 | 53 | 83 | S
24 | 14 | 20 | dc4 | 124 | 54 | 84 | T
25 | 15 | 21 | nak | 125 | 55 | 85 | U
26 | 16 | 22 | syn | 126 | 56 | 86 | V
27 | 17 | 23 | etb | 127 | 57 | 87 | W
30 | 18 | 24 | can | 130 | 58 | 88 | X
31 | 19 | 25 | em | 131 | 59 | 89 | Y
32 | 1a | 26 | sub | 132 | 5a | 90 | Z
33 | 1b | 27 | esc | 133 | 5b | 91 | [
34 | 1c | 28 | fs | 134 | 5c | 92 | \
35 | 1d | 29 | gs | 135 | 5d | 93 | ]
36 | 1e | 30 | re | 136 | 5e | 94 | ^
37 | 1f | 31 | us | 137 | 5f | 95 | _
40 | 20 | 32 | sp | 140 | 60 | 96 | '
41 | 21 | 33 | ! | 141 | 61 | 97 | a
42 | 22 | 34 | " | 142 | 62 | 98 | b
43 | 23 | 35 | # | 143 | 63 | 99 | c
44 | 24 | 36 | $ | 144 | 64 | 100 | d
45 | 25 | 37 | % | 145 | 65 | 101 | e
46 | 26 | 38 | & | 146 | 66 | 102 | f
47 | 27 | 39 | ` | 147 | 67 | 103 | g
50 | 28 | 40 | ( | 150 | 68 | 104 | h
51 | 29 | 41 | ) | 151 | 69 | 105 | i
52 | 2a | 42 | * | 152 | 6a | 106 | j
53 | 2b | 43 | + | 153 | 6b | 107 | k
54 | 2c | 44 | , | 154 | 6c | 108 | l
55 | 2d | 45 | - | 155 | 6d | 109 | m
56 | 2e | 46 | . | 156 | 6e | 110 | n
57 | 2f | 47 | / | 157 | 6f | 111 | o
60 | 30 | 48 | 0 | 160 | 70 | 112 | p
61 | 31 | 49 | 1 | 161 | 71 | 113 | q
62 | 32 | 50 | 2 | 162 | 72 | 114 | r
63 | 33 | 51 | 3 | 163 | 73 | 115 | s
64 | 34 | 52 | 4 | 164 | 74 | 116 | t
65 | 35 | 53 | 5 | 165 | 75 | 117 | u
66 | 36 | 54 | 6 | 166 | 76 | 118 | v
67 | 37 | 55 | 7 | 167 | 77 | 119 | w
70 | 38 | 56 | 8 | 170 | 78 | 120 | x
71 | 39 | 57 | 9 | 171 | 79 | 121 | y
72 | 3a | 58 | : | 172 | 7a | 122 | z
73 | 3b | 59 | ; | 173 | 7b | 123 | {
74 | 3c | 60 | < | 174 | 7c | 124 | |
75 | 3d | 61 | = | 175 | 7d | 125 | }
76 | 3e | 62 | > | 176 | 7e | 126 | ~
77 | 3f | 63 | ? | 177 | 7f | 127 | del


BCD码

定义：用4位二进制数来表示1位十进制数中的0~9这10个数码，简称BCD码

编码本身都是为了在计算机中运算使用。实质上，在计算机中只有以高低电平来表示的二进制。至于，BCD码是一种8421编码，它将一个字节分两部分，高4位代表一个数字，低4位代表一个数字。也就是说，计算机怎么识别一个字节到底是二进制码还是BCD码，这是由程序代码来运算的。

我的理解可由16进制数简化理解：每2个BCD码构成一字节，接收到每字节二进制数据需处理成16进制的字符形式。仅是由00-99的范围。而非00-FF范围。同样发送的10进制数以每2个BCD码组成1个16进制数形成二进制的一个字节发送来达到目的。

    即BCD代码。Binary-Coded Decimal‎，简称BCD，称BCD码或二-十进制代码，亦称二进码十进数。是一种二进制的数字编码形式，用二进制编码的十进制代码。这种编码形式利用了四个位元来储存一个十进制的数码，使二进制和十进制之间的转换得以快捷的进行。这种编码技巧，最常用于会计系统的设计里，因为会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点式记数法，采用BCD码，既可保存数值的精确度，又可免却使电脑作浮点运算时所耗费的时间。此外，对于其他需要高精确度的计算，BCD编码亦很常用。

    由于十进制数共有0、1、2、……、9十个数码，因此，至少需要4位二进制码来表示1位十进制数。4位二进制码共有2^4=16种码组，在这16种代码中，可以任选10种来表示10个十进制数码，共有N=16！/（16-10）！约等于2.9乘以10的10次方种方案。常用的BCD代码列于末。

    常用 BCD编码方式

    最常用的BCD编码，就是使用"0"至"9"这十个数值的二进码来表示。这种编码方式，在中国大陆称之为“8421码”。除此以外，对应不同需求，各人亦开发了不同的编码方法，以适应不同的需求。这些编码，大致可以分成有权码和无权码两种：

    有权BCD码，如：8421(最常用)、2421、5421…

    无权BCD码，如：余3码、格雷码…

    以下为三种常见的BCD编码的比较。

    十进数 8421-BCD码余3-BCD码 2421-A码

    (M10) ＤＣＢＡ C3 C2 C1 C0 a3 a2 a1 a0

    ０００００００１１００００

    １０００１０１０００００１

    ２００１００１０１００１０

    ３００１１０１１０００１１

    ４０１０００１１１０１００

    ５０１０１１００００１０１

    ６０１１０１００１０１１０

    ７０１１１１０１００１１１

    ８１０００１０１１１１１０

    ９１００１１１００１１１１

    常用 BCD码

    十进制数 8421码 5421码 2421码余3码余3循环码

    0 0000 0000 0000 0011 0010

    1 0001 0001 0001 0100 0110

    2 0010 0010 0010 0101 0111

    3 0011 0011 0011 0110 0101

    4 0100 0100 0100 0111 0100

    5 0101 1000 1011 1000 1100

    6 0110 1001 1100 1001 1101

    7 0111 1010 1101 1010 1111

    8 1000 1011 1110 1011 1110

    9 1001 1100 1111 1100 1010

    -----------------------

    特点：

    8421编码直观，好理解。

    5421码和2421码中大于5的数字都是高位为1，5以下的高位为0。

    余3码是8421码加上3，有上溢出和下溢出的空间。

    格雷码相邻2个数有三位相同，只有一位不同。

    ————————————————————

    什么是BCD码2006-3-19 13:24:45

    bcd码也叫8421码就是将十进制的数以8421的形式展开成二进制，大家知道十进制是0～9十个数组成，着十个数每个数都有自己的8421码：

    0＝0000

    1＝0001

    2＝0010

    3＝0011

    4＝0100

    5＝0101

    6＝0110

    7＝0111

    8＝1000

    9＝1001

    举个例子：

    321的8421码就是

    3 2 1

    0011 0010 0001

    原因:0011=8x0+4x0+1x2+1x1=3 0010=8x0+4x0+2x1+1x0=2. 0001=8x0+4x0+2x0+1x1=1

    具体:

    bcd码是四位二进制码, 也就是将十进制的数字转化为二进制, 但是和普通的转化有一点不同, 每一个十进制的数字0-9都对应着一个四位的二进制码,对应关系如下: 十进制0 对应二进制0000 ;十进制1 对应二进制0001 ....... 9 1001 接下来的10就有两个上述的码来表示 10 表示为00010000 也就是BCD码是遇见1001就产生进位,不象普通的二进制码,到1111才产生进位10000

    举例:

    某二进制无符号数11101010，转换为三位非压缩BCD数，按百位、十位和个位的顺序表示，应为<U>__C</U>__。

    A.00000001 00000011 00000111 　　　B. 00000011 00000001 00000111

    C.00000010 00000011 00000100 　　　D. 00000011 00000001 00001001

    解:(1)11101010转换为十进制:234

    (2)按百位、十位和个位的顺序表示，应为<U>__C</U>__。

    附注：压缩BCD码与非压缩BCD码的区别—— 压缩BCD码的每一位用4位二进制表示，一个字节表示两位十进制数。例如10010110B表示十进制数96D；非压缩BCD码用1个字节表示一位十进制数，高四位总是0000，低4位的0000~1001表示0~9.例如00001000B表示十进制数8.


BCD码转换

unsigned int ui;//(16位无符号二进制数)

unsigned char uc[5];//(BCD码表示的5位十进制数)

uc[4]=(ui/10000)%10;

uc[3]=(ui/1000)%10;

uc[2]=(ui/100)%10;

uc[1]=(ui/10)%10;

uc[0]=ui%10;

_____________________________________________________________________

今天有人在问我BCD码如何转换成10进制码的问题

其实很简单

BCD 的表示法是 用4个二进制位来表示一个十进制位，也就是一个字节能够表示2个十进制数位，这是一种压缩的十进制表示方式，应用很广，最常见的就是CMOS里的时间存放格

式就是BCD码格式，更具体的表示方法大家可以去百度一下就知道了不过其实知道我上面说的就差不多了。

明白了BCD是怎么表示后转换也就容易了，低4位和高4位分别转换然后合并起来就是了！

如15用BCD码表示为 0001 0101

低四位的值就是实现十进制数的低四，高4位就是高四位的值，这样我们有很高效的转换方法

低四位＋高四位×10（上面是2位十进制数的）

用C语言表达一下可能更能明白我上面说的意思

#include <stdio.h>

int main(void)

{

int bcd = 0x15;

int dec;

dec = (bcd & 15) + ((bcd>>4) * 10);

printf("%d\n",dec);

return 0;

}

所以实际转换可以定义成一个宏

#define BCDTODEC(bcd)

如

#include <stdio.h>

#define BCDTODEC(bcd) ((bcd) = ((bcd) & 15) + ((bcd)>>4) * 10)

int main(void)

{

int bcd = 0x15;

BCDTODEC(bcd);

printf("%d\n",bcd);

return 0;

}

______________________________________________________________________________________

#define BCD2BIN(val) (((val) & 0x0f) + ((val)>>4)*10)

#define BIN2BCD(val) ((((val)/10)<<4) + (val)%10)

////////////////////////////////////////////////////////////////////////////////////////////////

//函数名:ZH_Data();

//返回参数:有.返回为十进制或十进制BCD码.

//输入参数:State=0 输入为十进制BCD码输出为十进制.

//输入参数:State=1 输入为十进制,输出为十进制BCD码.

//设计人:wang1jin.

//设计版本:V1.0.

//////////////////////////////////////////////////////////////////////////////////////////////

uint8 ZH_Data(bit State,uint8 Value)

{

if(State==0) //判断工作模式.

{

Value=(Value/16)*10+(Value%16); //把8421BCD码转为十进制

return Value; //返回;

}

else //把十进制转为8421BCD码;

{

Value=(Value/10)*16+Value%10; //把十进制转为8421BCD码.

return Value; //返回;

}

}

______________________________________________________________________________________

//16进制转bcd码

unchar HEXBCD2(unchar bcd_data) //hex转为bcd子程序

{

unchar temp;

temp=(bcd_data/16*10+bcd_data%16);

return temp;

}

unchar BCD2HEX(unchar hex_data) //BCD转为HEX子程序

{

unchar temp;

temp=(hex_data/10*16+hex_data%10);

return temp;

}

______________________________________________________________________________________

long str16to10(unsigned char *s)

{

//s字符串最大为"FFFFFFFF"

//调用示例:

// long lRet; lRet=str16to10("a10"); // -- (结果: lRet=2576)

longlRet;

unsigned char ch;

lRet=0;

while (*s)

{

ch=*s;

if ((ch >= '0') && (ch <= '9')) ch -= '0';

else if ((ch >= 'A') && (ch <= 'F')) ch -= '7';

else if ((ch >= 'a') && (ch <= 'f')) ch -= 'W';

//lRet=lRet*16+ch;

lRet=(lRet<<4)+ch;

s ++;

}

return lRet;

}

____________________________________________________________________________________________

sprintf(char,"%x",int);//将10进制转换成16进制

sprintf(char,"%o",int);//将10进制转换成8进制


C++中的数据类型转换方法

从int到char*，或者反过来从char*到int，在C/C++中到底有多少种转换方法呢？符合标准的大概有四种。即C数据转换函数族、sprintf/snprintf/sscanf函数族、字符串流std::stringstream、std::strsteam。不符合标准却又广为使用的包括CString和boost::lexical_cast。本文只讨论符合标准的转换方法，其中std::strstream由于已经被C++标准委员为指定为不推荐使用的（deprecated），所以不予考虑了。下面重点讨论三种标准转换方法之间的优劣。

1. Int和char*或std::string之间的转换

C数据转换函数族

C数据转换函数族即包括itoa、atoi等数据类型转换函数在内的一大批C函数，在C语言时代曾经被大量使用。源代

码如下：

int i = 10;

char szBuf[10] = "";

itoa(i, szBuf, 10);

cout<<"itoa: szBuf = "<<szBuf<<endl;

i = 0;

i = atoi(szBuf);

cout<<"atoi: i = "<<i<<endl;

使用还是比较简单的。一个最大的问题是：没有进行char*的越界检查，可能会造成数组溢出。

snprintf/sscanf

sprintf是用来格式化字符串的一个C函数，sscanf则是从字符串中读取值的一个C函数。由于Herb Sutter（Exceptional C++系列著作的作者）教导我们“永远也不要使用sprintf”，所以这里我们只使用snprintf。由于snprintf进入C标准较晚，所以在你的编译器中也许只能使用非标准的_snprintf（例如我的VC6平台）。源代码

如下：

int i = 20;

char szBuf[10] = "";

memset(szBuf, 0, sizeof(szBuf));

_snprintf(szBuf, sizeof(szBuf), "%d", i);

cout<<"_snprintf: szBuf = "<<szBuf<<endl;

i = 0;

sscanf(szBuf,"%d",&i);

cout<<"sscanf: i = "<<i<<endl;

使用很简单，而且，似乎没有什么内存泄露或者数组越界。

std::stringstream

对流很熟悉的人可能会更快适应std::stringstream的解决方案：

#include <sstream>

using namespace std;

int i = 30;

string strRel;

ostringstream oss;

oss<<i;

strRel = oss.str();

cout<<"ostringstream: strRel = "<<strRel<<endl;

i = 0;

istringstream iss(strRel);

iss>>i;

cout<<"istringstream: i = "<<i<<endl;

使用较为复杂，而且，还使用了两个临时变量oss和iss，这必然带来性能上的开销。

2. double和char*或std::string之间的转换

1. C数据转换函数族

当开始进行double和char*之间的转换时，C数据转换函数的缺点暴露无疑。先看源代码：

double d = 3.1415926;

char szBuf[18] = "";

_gcvt(d, 9, szBuf);

cout<<"_gcvt: szBuf = "<<szBuf<<endl;

d = 0;

char* stopstring;

d = strtod(szBuf, &stopstring);

cout<<"strtod: d = "<<d<<endl;

首先转换函数的名字就让人大吃一惊，与itoa对应的不是我们想象的dtoa，而是_gcvt，而与atoi对应的是strtod。其次它们的参数很奇怪，没有msdn是不可能明白的。其次，数组越界依然存在。至此我想我们可以抛弃这组函数了。当然，更加无奈的理由在后面。

snprintf/sscanf

snprintf/sscanf表现不错，源代码如下：

double d = 3.1415926;

char szBuf[18] = "";

memset(szBuf, 0, sizeof(szBuf));

_snprintf(szBuf, sizeof(szBuf), "%f", d);

cout<<"sprintf: szBuf = "<<szBuf<<endl;

sscanf(szBuf, "%f", &d);

cout<<"sscanf: d = "<<d<<endl;

很好，很强大！

std::stringstream

std::stringstream的代码似乎没有任何改动，除了一个int类型改成了double类型：

double d = 9.1415926;

string strRel;

ostringstream oss;

oss<<d;

strRel = oss.str();

cout<<"ostringstream: strRel = "<<strRel<<endl;

d = 0;

istringstream iss(strRel);

iss>>d;

cout<<"istringstream: d = "<<d<<endl;

写到这里，我似乎看到模板函数在向我招手。

3. 复杂的转换

考虑一个经典的场景，从一个int，一个double和一个string中读出值，然后拼凑为一个输出的字符串。最后，从这个字符串中再将这几个值读出来。

C数据转换函数族

直接看代码：

int iAge = 25;

float fPayment = 3.25;

string strName ="Wang";

char szBuf[100] = "";

char szTemp[100];

strcpy(szBuf,"Age= ");

itoa(iAge, szTemp, 10);

strcat(szBuf, szTemp);

strcat(szBuf," ,Payment= ");

_gcvt(fPayment, 4, szTemp);

strcat(szBuf,szTemp);

strcat(szBuf," ,Name= ");

strcat(szBuf, strName.c_str());

cout<<"szBuf = "<<szBuf<<endl;

以上代码的表现真是惨不忍睹，费了几鼻子的劲好歹是转为目标字符串了。转换回来的代码也没有写。也许有，不过那个复杂程度，我看还是算了。而且，strcpy、strcat和几个转换函数都是危险的API，不检查越界的。

snprintf/sscanf

主要看看sscanf的表现：

int iAge = 25;

float fPayment = 3.25;

string strName ="Wang";

char szBuf[100] = "";

memset(szBuf, 0, sizeof(szBuf));

_snprintf(szBuf, sizeof(szBuf), "Age = %d, Payment = %f, Name = %s",iAge,fPayment,strName.c_str());

cout<<"sprintf: szBuf = "<<szBuf<<endl;

iAge = 0;

fPayment = 0.0;

memset(szTemp, 0, sizeof(szTemp));

sscanf(szBuf,"Age = %d, Payment = %f, Name = %s",&iAge,&fPayment,&szTemp);

strName = szTemp;

cout<<"sscanf: Age = "<<iAge<<",Payment="<<fPayment<<",name="<<strName<<endl;

snprintf表现还是一如既往的强大。sscanf的表现简直就是perfect，但是要注意，sscanf的样式字符串一定要和snprintf中的样式字符串一模一样，否则其后果是不可预计的。例如，我稍微改动了几个字符，最后的strname就读取错误了。

std::stringstream

std::stringstream的代码很长很长：

int iAge = 25;

float fPayment = 3.25;

string strName ="Wang";

string strRel;

ostringstream oss;

oss<<"Age = "<<iAge<<", Payment = "<<fPayment<<", Name = "<<strName;

strRel = oss.str();

cout<<"ostringstream: strRel = "<<strRel<<endl;

iAge = 0;

fPayment = 0.0;

strName = "";

istringstream iss(strRel);

string strTemp;

iss>>strTemp>>strTemp>>iAge>>strTemp>>strTemp>>strTemp>>fPayment

>>strTemp>>strTemp>>strTemp>>strName;

cout<<"istringstream: Age = "<<iAge<<",Payment="<<fPayment<<",name="<<strName<<endl;

ostringstream的表现还是不错的，比snprintf毫不逊色，甚至更好一点，因为它不用记样式符。但是看到istringstream的这几行代码，估计大部分人要吐血了：

string strTemp;

iss>>strTemp>>strTemp>>iAge>>strTemp>>strTemp>>strTemp>>fPayment

>>strTemp>>strTemp>>strTemp>>strName;

为什么中间有那么多strTemp？因为每当istringstream每当遇到由一个空格或者非数字字符包围的字符串时就必须输入到一个string中，因此例如“，”或者“=”都必须占用一个string来输入。总之，当字符串很复杂时，很麻烦。

4. 小结

从易用性、安全性和效率三个方面来考察以上方法。

易用性按从好到坏排列依次是：snprintf/sscanf、std::stringstream、C数据转换函数族。

安全性按从好到坏排列依次是：std::stringstream、snprintf/sscnaf、C数据转换函数族。

效率按从好到坏排列依次是：snprintf/sscanf、C数据转换函数族、std::stringstream。

到此我们可以抛弃C数据转换函数族了，接下来从其他方面来考察剩下的方法。

5. 模板函数

考虑写一个从任何内置数据类型到字符串的转换函数。此时只能使用std::stringstream了。

template<typename T> string to_str(T val)

{

ostringstream oss;

oss<<val;

return oss.str();

}

string strFromInt = to_str(10);

cout<<strFromInt<<endl;

string strFromDouble = to_str(3.1415926);

cout<<strFromDouble<<endl;

换句话说，也就是std::stringstream是类型安全的，而snprintf不是。

6. 精度和格式

前面的例子都忽略了精度，事实上要进行精度和格式的设置，snprintf和std::stringstream的学习时间是差不多的。这里就不一一叙述了。

7. 再谈安全性

snprintf是安全的，因为它会检查数组越界并阻止这种行为。但是当目标字符数组长度小于需求时，其结果是不正确的。sscanf也是同样，当它的样式字符串不匹配时，其结果是未知的。

std::stringstream是更安全的。由于它采用了内存自动管理机制。无论在任何时候，只要它没有抛出异常，其结果总是正确的。

8. 总结

在易用性和效率方面，snprintf/sscanf都比std::stringstream强，因此只要程序员能够保证目标字符数组长度够用时，都可以放心使用snprintf。sscanf也是同理，只要程序员保证其样式字符串正确，其结果也总是正确的。而std::stringstream的最大优点是具有模板亲和力，可以用于泛型编程。在效率不是很重要的情况下，建议使用std::stringstream。若你是一位非常谨慎的程序员，建议你总是使用std::stringstream，因为它是最安全的。


一、其它数据类型转换为字符串

短整型(int)

itoa(i,temp,10);///将i转换为字符串放入temp中,最后一个数字表示十进制

itoa(i,temp,2); ///按二进制方式转换

长整型(long)

ltoa(l,temp,10);

浮点数(float,double)

用fcvt可以完成转换,这是MSDN中的例子:

int decimal, sign;

char *buffer;

double source = 3.1415926535;

buffer = _fcvt( source, 7, &decimal, &sign );

运行结果:source: 3.1415926535 buffer: "31415927" decimal: 1 sign: 0

decimal表示小数点的位置,sign表示符号:0为正数，1为负数

CString 变量

str = "2008北京奥运";

buf = (LPSTR)(LPCTSTR)str;

BSTR变量

BSTR bstrValue = ::SysAllocString(L"程序员");

char * buf = _com_util::ConvertBSTRToString(bstrValue);

SysFreeString(bstrValue);

AfxMessageBox(buf);

delete(buf);

CComBSTR变量

CComBSTR bstrVar("test");

char *buf = _com_util::ConvertBSTRToString(bstrVar.m_str);

AfxMessageBox(buf);

delete(buf);

_bstr_t 变量

_bstr_t类型是对BSTR的封装，因为已经重载了=操作符，所以很容易使用

_bstr_t bstrVar("test");

const char *buf = bstrVar;///不要修改buf中的内容

AfxMessageBox(buf);

通用方法(针对非COM数据类型)

用sprintf完成转换

char buffer[200];

char c = "1";

int i = 35;

long j = 1000;

float f = 1.7320534f;

sprintf( buffer, "%c",c);

sprintf( buffer, "%d",i);

sprintf( buffer, "%d",j);

sprintf( buffer, "%f",f);

二、字符串转换为其它数据类型

strcpy(temp,"123");

短整型(int)

i = atoi(temp);

长整型(long)

l = atol(temp);

浮点(double)

d = atof(temp);

CString变量

CString name = temp;

BSTR变量

BSTR bstrValue = ::SysAllocString(L"程序员");

...///完成对bstrValue的使用

SysFreeString(bstrValue);

CComBSTR变量

CComBSTR类型变量可以直接赋值

CComBSTR bstrVar1("test");

CComBSTR bstrVar2(temp);

_bstr_t变量

_bstr_t类型的变量可以直接赋值

_bstr_t bstrVar1("test");

_bstr_t bstrVar2(temp);

三、其它数据类型转换到CString

使用CString的成员函数Format来转换,例如:

整数(int)

str.Format("%d",i);

浮点数(float)

str.Format("%f",i);

字符串指针(char *)等已经被CString构造函数支持的数据类型可以直接赋值

str = username;

对于Format所不支持的数据类型，可以通过上面所说的关于其它数据类型转化到char *的方法先转到char *，然后赋值给CString变量。

四、BSTR、_bstr_t与CComBSTR

CComBSTR 是ATL对BSTR的封装，_bstr_t是C++对BSTR的封装,BSTR是32位指针,但并不直接指向字串的缓冲区。

char *转换到BSTR可以这样:

BSTR b=_com_util::ConvertStringToBSTR("数据");///使用前需要加上comutil.h和comsupp.lib

SysFreeString(bstrValue);

反之可以使用

char *p=_com_util::ConvertBSTRToString(b);

delete p;

具体可以参考一，二段落里的具体说明。

CComBSTR与_bstr_t对大量的操作符进行了重载，可以直接进行=,!=,==等操作，所以使用非常方便。

特别是_bstr_t,建议大家使用它。

五、VARIANT 、_variant_t 与 COleVariant

VARIANT的结构可以参考头文件VC98\Include\OAIDL.H中关于结构体tagVARIANT的定义。

对于VARIANT变量的赋值：首先给vt成员赋值，指明数据类型，再对联合结构中相同数据类型的变量赋值，举个例子：

VARIANT va;

int a=2001;

va.vt=VT_I4;///指明整型数据

va.lVal=a; ///赋值

对于不马上赋值的VARIANT，最好先用Void VariantInit(VARIANTARG FAR* pvarg);进行初始化,其本质是将vt设置为VT_EMPTY,下表我们列举vt与常用数据的对应关系:

Byte bVal; // VT_UI1.

Short iVal; // VT_I2.

long lVal; // VT_I4.

float fltVal; // VT_R4.

double dblVal; // VT_R8.

VARIANT_BOOL boolVal; // VT_BOOL.

SCODE scode; // VT_ERROR.

CY cyVal; // VT_CY.

DATE date; // VT_DATE.

BSTR bstrVal; // VT_BSTR.

DECIMAL FAR* pdecVal // VT_BYREF|VT_DECIMAL.

IUnknown FAR* punkVal; // VT_UNKNOWN.

IDispatch FAR* pdispVal; // VT_DISPATCH.

SAFEARRAY FAR* parray; // VT_ARRAY|*.

Byte FAR* pbVal; // VT_BYREF|VT_UI1.

short FAR* piVal; // VT_BYREF|VT_I2.

long FAR* plVal; // VT_BYREF|VT_I4.

float FAR* pfltVal; // VT_BYREF|VT_R4.

double FAR* pdblVal; // VT_BYREF|VT_R8.

VARIANT_BOOL FAR* pboolVal; // VT_BYREF|VT_BOOL.

SCODE FAR* pscode; // VT_BYREF|VT_ERROR.

CY FAR* pcyVal; // VT_BYREF|VT_CY.

DATE FAR* pdate; // VT_BYREF|VT_DATE.

BSTR FAR* pbstrVal; // VT_BYREF|VT_BSTR.

IUnknown FAR* FAR* ppunkVal; // VT_BYREF|VT_UNKNOWN.

IDispatch FAR* FAR* ppdispVal; // VT_BYREF|VT_DISPATCH.

SAFEARRAY FAR* FAR* pparray; // VT_ARRAY|*.

VARIANT FAR* pvarVal; // VT_BYREF|VT_VARIANT.

void FAR* byref; // Generic ByRef.

char cVal; // VT_I1.

unsigned short uiVal; // VT_UI2.

unsigned long ulVal; // VT_UI4.

int intVal; // VT_INT.

unsigned int uintVal; // VT_UINT.

char FAR * pcVal; // VT_BYREF|VT_I1.

unsigned short FAR * puiVal; // VT_BYREF|VT_UI2.

unsigned long FAR * pulVal; // VT_BYREF|VT_UI4.

int FAR * pintVal; // VT_BYREF|VT_INT.

unsigned int FAR * puintVal; //VT_BYREF|VT_UINT.

_variant_t是VARIANT的封装类，其赋值可以使用强制类型转换，其构造函数会自动处理这些数据类型。

使用时需加上#include <comdef.h>

例如：

long l=222;

ing i=100;

_variant_t lVal(l);

lVal = (long)i;

COleVariant的使用与_variant_t的方法基本一样，请参考如下例子：

COleVariant v3 = "字符串", v4 = (long)1999;

CString str =(BSTR)v3.pbstrVal;

long i = v4.lVal;

六、其它一些COM数据类型

根据ProgID得到CLSID

HRESULT CLSIDFromProgID( LPCOLESTR lpszProgID,LPCLSID pclsid);

CLSID clsid;

CLSIDFromProgID( L"MAPI.Folder",&clsid);

根据CLSID得到ProgID

WINOLEAPI ProgIDFromCLSID( REFCLSID clsid,LPOLESTR * lplpszProgID);

例如我们已经定义了 CLSID_IApplication,下面的代码得到ProgID

LPOLESTR pProgID = 0;

ProgIDFromCLSID( CLSID_IApplication,&pProgID);

...///可以使用pProgID

CoTaskMemFree(pProgID);//不要忘记释放

七、ANSI与Unicode

Unicode称为宽字符型字串,COM里使用的都是Unicode字符串。

将ANSI转换到Unicode

(1)通过L这个宏来实现，例如: CLSIDFromProgID( L"MAPI.Folder",&clsid);

(2)通过MultiByteToWideChar函数实现转换,例如:

char *szProgID = "MAPI.Folder";

WCHAR szWideProgID[128];

CLSID clsid;

long lLen = MultiByteToWideChar(CP_ACP,0,szProgID,strlen(szProgID),szWideProgID,sizeof(szWideProgID));

szWideProgID[lLen] = "\0";

(3)通过A2W宏来实现,例如:

USES_CONVERSION;

CLSIDFromProgID( A2W(szProgID),&clsid);

将Unicode转换到ANSI

(1)使用WideCharToMultiByte,例如:

// 假设已经有了一个Unicode 串 wszSomeString...

char szANSIString [MAX_PATH];

WideCharToMultiByte ( CP_ACP, WC_COMPOSITECHECK, wszSomeString, -1, szANSIString, sizeof(szANSIString), NULL, NULL );

(2)使用W2A宏来实现,例如:

USES_CONVERSION;

pTemp=W2A(wszSomeString);

八、其它

对消息的处理中我们经常需要将WPARAM或LPARAM等32位数据（DWORD)分解成两个16位数据（WORD),例如：

LPARAM lParam;

WORD loValue = LOWORD(lParam);///取低16位

WORD hiValue = HIWORD(lParam);///取高16位

对于16位的数据(WORD)我们可以用同样的方法分解成高低两个8位数据(BYTE),例如:

WORD wValue;

BYTE loValue = LOBYTE(wValue);///取低8位

BYTE hiValue = HIBYTE(wValue);///取高8位

两个16位数据（WORD）合成32位数据(DWORD,LRESULT,LPARAM,或WPARAM)

LONG MAKELONG( WORD wLow, WORD wHigh );

WPARAM MAKEWPARAM( WORD wLow, WORD wHigh );

LPARAM MAKELPARAM( WORD wLow, WORD wHigh );

LRESULT MAKELRESULT( WORD wLow, WORD wHigh );

两个8位的数据(BYTE)合成16位的数据(WORD)

WORD MAKEWORD( BYTE bLow, BYTE bHigh );

从R(red),G(green),B(blue)三色得到COLORREF类型的颜色值

COLORREF RGB( BYTE byRed,BYTE byGreen,BYTE byBlue );

例如COLORREF bkcolor = RGB(0x22,0x98,0x34);

从COLORREF类型的颜色值得到RGB三个颜色值

BYTE Red = GetRValue(bkcolor); ///得到红颜色

BYTE Green = GetGValue(bkcolor); ///得到绿颜色

BYTE Blue = GetBValue(bkcolor); ///得到兰颜色

九、注意事项

假如需要使用到ConvertBSTRToString此类函数,需要加上头文件comutil.h,并在setting中加入comsupp.lib或者直接加上#pragma comment( lib, "comsupp.lib" )


你所遇到的这几个宏是为了进行条件编译

你所遇到的这几个宏是为了进行条件编译。一般情况下，源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译，也就是对一部分内容指定编译的条件，这就是“条件编译”。有时，希望当满足某条件时对一组语句进行编译，而当条件不满足时则编译另一组语句。

条件编译命令最常见的形式为：

#ifdef 标识符

程序段1

#else

程序段2

#endif

它的作用是：当标识符已经被定义过(一般是用#define命令定义)，则对程序段1进行编译，否则编译程序段2。

其中#else部分也可以没有，即：

#ifdef

程序段1

#endif

这里的“程序段”可以是语句组，也可以是命令行。这种条件编译可以提高C源程序的通用性。如果一个C源程序在不同计算机系统上系统上运行，而不同的计算机又有一定的差异。例如，我们有一个数据类型，在Windows平台中，应该使用long类型表示，而在其他平台应该使用float表示，这样往往需要对源程序作必要的修改，这就降低了程序的通用性。可以用以下的条件编译：

#ifdef WINDOWS

#define MYTYPE long

#else

#define MYTYPE float

#endif

如果在Windows上编译程序，则可以在程序的开始加上

#define WINDOWS

这样则编译下面的命令行：

#define MYTYPE long

如果在这组条件编译命令之前曾出现以下命令行：

#define WINDOWS 0

则预编译后程序中的MYTYPE都用float代替。这样，源程序可以不必作任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况，可以根据此思路设计出其它的条件编译。

例如，在调试程序时，常常希望输出一些所需的信息，而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段：

#ifdef DEBUG

print (\"device_open(%p)\\n\", file);

#endif

如果在它的前面有以下命令行：

#define DEBUG

则在程序运行时输出file指针的值，以便调试分析。调试完成后只需将这个define命令行删除即可。有人可能觉得不用条件编译也可达此目的，即在调试时加一批printf语句，调试后一一将printf语句删除去。的确，这是可以的。但是，当调试时加的printf语句比较多时，修改的工作量是很大的。用条件编译，则不必一一删改printf语句，只需删除前面的一条“#define DEBUG”命令即可，这时所有的用DEBUG作标识符的条件编译段都使其中的printf语句不起作用，即起统一控制的作用，如同一个“开关”一样。 [Page]

有时也采用下面的形式：

#ifndef 标识符

程序段1

#else

程序段2

#endif

只是第一行与第一种形式不同：将“ifdef”改为“ifndef”。它的作用是：若标识符未被定义则编译程序段1，否则编译程序段2。这种形式与第一种形式的作用相反。

以上两种形式用法差不多，根据需要任选一种，视方便而定。

还有一种形式，就是#if后面的是一个表达式，而不是一个简单的标识符：

#if 表达式

程序段1

#else

程序段2

#endif

它的作用是：当指定的表达式值为真（非零）时就编译程序段1，否则编译程序段2。可以事先给定一定条件，使程序在不同的条件下执行不同的功能。

例如：输入一行字母字符，根据需要设置条件编译，使之能将字母全改为大写输出，或全改为小写字母输出。

#define LETTER 1

main()

{

char str[20]=\"C Language\",c;

int i=0;

while((c=str[i])!=’\\0’){

i++;

#if LETTER

if(c>=’a’&&c<=’z’) c=\"c-32\";

#else

if(c>=’A’&&c<=’Z’) c=\"c\"+32;

#endif

[NextPage]

printf(\"%c\",c);

}

}

运行结果为：C LANGUAGE

现在先定义LETTER为1，这样在预处理条件编译命令时，由于LETTER为真（非零），则对第一个if语句进行编译，运行时使小写字母变大写。如果将程序第一行改为：

#define LETTER 0

则在预处理时，对第二个if语句进行编译处理，使大写字母变成小写字母（大写字母与相应的小写字母的ASCII代码差32）。此时运行情况为：

c language

有人会问：不用条件编译命令而直接用if语句也能达到要求，用条件编译命令有什么好处呢？的确，此问题完全可以不用条件编译处理，但那样做目标程序长（因为所有语句都编译），而采用条件编译，可以减少被编译的语句，从而减少目标的长度。当条件编译段比较多时，目标程序长度可以大大减少。 [Page]

文件中的#ifndef

头件的中的#ifndef，这是一个很关键的东西。比如你有两个C文件，这两个C文件都include了同一个头文件。而编译时，这两个C文件要一同编译成一个可运行文件，于是问题来了，大量的声明冲突。

还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用，你都要加上这个。一般格式是这样的：

#ifndef <标识>

:}G\"{d}Guest#define <标识>

......

}!Dg+t0il4rGuest......

#endif

<标识>在理论上来说可以是自由命名的，但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写，前后加下划线，并把文件名中的“.”也变成下划线，如：stdio.h

#ifndef _STDIO_H_

y6NW5GesGuest#define _STDIO_H_

......

#endif

2.在#ifndef中定义变量出现的问题（一般不定义在#ifndef中）。

#ifndef AAAEETOP专业博客---电子工程师自己的家园+_8RY&W)@ OC8WH

#define AAA

:G{#d:i)J-Ep XGuest...

)Ahzo N8`B9YGuestint i;EETOP专业博客---电子工程师自己的家园_;^T usl

...EETOP专业博客---电子工程师自己的家园S t0L$\\]REm9?

#endif

0o3u_s2kz7h0V8G[CsGuest里面有一个变量定义

+c%G~,n?ZlGuest在vc中链接时就出现了i重复定义的错误，而在c中成功编译。

结论：

(1).当你第一个使用这个头的.cpp文件生成.obj的时候，int i 在里面定义了当另外一个使用这个的.cpp再次[单独]生成.obj的时候，int i 又被定义然后两个obj被另外一个.cpp也include 这个头的，连接在一起，就会出现重复定义.

(2).把源程序文件扩展名改成.c后，VC按照C语言的语法对源程序进行编译，而不是C++。在C语言中，若是遇到多个int i，则自动认为其中一个是定义，其他的是声明。

(3).C语言和C++语言连接结果不同，可能（猜测）时在进行编译的时候，C++语言将全局

RFX’r/G\"JrGuest变量默认为强符号，所以连接出错。C语言则依照是否初始化进行强弱的判断的。（参考）

解决方法：

(1).把源程序文件扩展名改成.c。

(2).推荐解决方案：

5k[ En E4RGuest.h中只声明 extern int i;在.cpp中定义

<x.h>EETOP专业博客---电子工程师自己的家园 x3l~&^|BP

#ifndef __X_H__EETOP专业博客---电子工程师自己的家园Nv9H#\\Y._A:GL+Q&XI

#define __X_H__EETOP专业博客---电子工程师自己的家园}9\\3x7Ik9xIKnW

extern int i;EETOP专业博客---电子工程师自己的家园1}UxW(z M }1d&o

#endif //__X_H__

zGe b*ExmGuest<x.c>

ixc:t$X zGuestint i;

注意问题：

(1).变量一般不要定义在.h文件中。

一般情况下，源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译，也就是对一部分内容指定编译的条件，这就是“条件编译”。有时，希望当满足某条件时对一组语句进行编译，而当条件不满足时则编译另一组语句。

X1g2J QV3|+QnGuest条件编译命令最常见的形式为： EETOP专业博客---电子工程师自己的家园7e n7w[]8G3[ [Page]

#ifdef 标识符

}$?*xz TR?[JGuest 程序段1 EETOP专业博客---电子工程师自己的家园^t6D_ BJ

#else

X(|XDv G(@,SfGuest 程序段2 EETOP专业博客---电子工程师自己的家园.M%~:Yke1|Z;K8Q

#endif EETOP专业博客---电子工程师自己的家园1?%m%I@q5Y

BC6dG6I(OGuest 它的作用是：当标识符已经被定义过(一般是用#define命令定义)，则对程序段1进行编译，否则编译程序段2。 EETOP专业博客---电子工程师自己的家园9?2[tx8EKZ

其中#else部分也可以没有，即： EETOP专业博客---电子工程师自己的家园K%L#Z @h,Op[)Rn

#ifdef

q-A#G6sd%TGuest 程序段1

o*X1l6aa1mGuest #denif EETOP专业博客---电子工程师自己的家园{X)u+n3_A

EETOP专业博客---电子工程师自己的家园 y|.@KNS%\\tS

这里的“程序段”可以是语句组，也可以是命令行。这种条件编译可以提高C源程序的通用性。如果一个C源程序在不同计算机系统上系统上运行，而不同的计算机又有一定的差异。例如，我们有一个数据类型，在Windows平台中，应该使用long类型表示，而在其他平台应该使用float表示，这样往往需要对源程序作必要的修改，这就降低了程序的通用性。可以用以下的条件编译：

[NextPage]

Oo0?(^b’suGuest #ifdef WINDOWS

K[{1C%CD.U.i4OGuest #define MYTYPE long EETOP专业博客---电子工程师自己的家园 X-|#f2u*D+j }xG

#else EETOP专业博客---电子工程师自己的家园8c\"Y/[$n!QVb

#define MYTYPE float

H’Q/N@4vGuest #endif EETOP专业博客---电子工程师自己的家园;p6`jd3OP0Se8V

?JTIn’AYoGuest 如果在Windows上编译程序，则可以在程序的开始加上

0HOpZ0CH)hBGuest #define WINDOWS

Kj;qXO v8ykGuest EETOP专业博客---电子工程师自己的家园 ey J$r,U

这样则编译下面的命令行： EETOP专业博客---电子工程师自己的家园uK Amkx

#define MYTYPE long EETOP专业博客---电子工程师自己的家园bS3_)zREYv+l*?

EETOP专业博客---电子工程师自己的家园 cUe^.QCp;e

如果在这组条件编译命令之前曾出现以下命令行： EETOP专业博客---电子工程师自己的家园!Rb~n9Y&gA\"x+jB6\\&L [Page]

#define WINDOWS 0 EETOP专业博客---电子工程师自己的家园? f%hS#xsP

EETOP专业博客---电子工程师自己的家园1I%O\"HA.q&}/e%?

则预编译后程序中的MYTYPE都用float代替。这样，源程序可以不必作任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况，可以根据此思路设瞥?a href=\"javascript:;\" onClick=\"tagshow(event, ’%C6%E4%CB%FC’);return false;\">其它的条件编译?

tRX1w9G a{0{AGuest 例如，在调试程序时，常常希望输出一些所需的信息，而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段： EETOP专业博客---电子工程师自己的家园GRd*IR

#ifdef DEBUG

)k[?^7LGuest print (\"device_open(%p) \", file); EETOP专业博客---电子工程师自己的家园:k*P.U}\"d#YdZ9[

#endif

4LoC3DDn.]Guest

j\"Q5` q{Guest 如果在它的前面有以下命令行： EETOP专业博客---电子工程师自己的家园)t!Z1g~U4h3@$z

#define DEBUG

by#K y*nz9g0TyvGuest EETOP专业博客---电子工程师自己的家园wJu:h)a,q ^ Q

则在程序运行时输出file指针的值，以便调试分析。调试完成后只需将这个define命令行删除即可。有人可能觉得不用条件编译也可达此目的，即在调试时加一批printf语句，调试后一一将printf语句删除去。的确，这是可以的。但是，当调试时加的printf语句比较多时，修改的工作量是很大的。用条件编译，则不必一一删改printf语句，只需删除前面的一条“#define DEBUG”命令即可，这时所有的用DEBUG作标识符的条件编译段都使其中的printf语句不起作用，即起统一控制的作用，如同一个“开关”一样。 EETOP专业博客---电子工程师自己的家园0F4D@1PK(LGcI+L8C

有时也采用下面的形式：

l#U.@y8p*Wnw!NY jGuest #ifndef 标识符 EETOP专业博客---电子工程师自己的家园1Z)?3X hV j~

程序段1

{z DX/xq;kGuest #else EETOP专业博客---电子工程师自己的家园Sov@-J&NV

程序段2 EETOP专业博客---电子工程师自己的家园9b(~ ]GM }

#endif EETOP专业博客---电子工程师自己的家园*_%I`X,Rv\\6x

EETOP专业博客---电子工程师自己的家园:I9yU[B

只是第一行与第一种形式不同：将“ifdef”改为“ifndef”。它的作用是：若标识符未被定义则编译程序段1，否则编译程序段2。这种形式与第一种形式的作用相反。 EETOP专业博客---电子工程师自己的家园+z9f},mL:Z}P [Page]

以上两种形式用法差不多，根据需要任选一种，视方便而定。 EETOP专业博客---电子工程师自己的家园F&S0@3v _

还有一种形式，就是#if后面的是一个表达式，而不是一个简单的标识符：

4^$w+J Vd#\\mGuest #if 表达式 EETOP专业博客---电子工程师自己的家园.]-l,^]Y

程序段1 EETOP专业博客---电子工程师自己的家园)}IlI:B K)C

#else

&[(a$h/Q#CP,mnqGuest 程序段2 EETOP专业博客---电子工程师自己的家园%UJcX4[5D

#endif EETOP专业博客---电子工程师自己的家园(Ll/ydH1l bJG r

EETOP专业博客---电子工程师自己的家园 ? S0Wlb\\

它的作用是：当指定的表达式值为真（非零）时就编译程序段1，否则编译程序段2。可以事先给定一定条件，使程序在不同的条件下执行不同的功能。

作用范围就是当前文件啊。因为编译是以cpp或c文件位单位的嘛。还以这个为例：

[NextPage]

//正常代码EETOP专业博客---电子工程师自己的家园C Y_W ].Tu3O:V;A

#ifdef _DEBUG

.F,`\"CclGuest TRACE(\"Some infomation\");EETOP专业博客---电子工程师自己的家园+f/Z ^3g-Y~{/~Fg^

#elseEETOP专业博客---电子工程师自己的家园(RLkS2p0frb

//Now is release version,so do nothing

Q2{ht3G&S5`5_G.|Guest#endifEETOP专业博客---电子工程师自己的家园?MY(U/bI da/N

//正常代码

编译时是先把所有的预编译处理展开（比如宏）再编译，所以Debug模式下，编译时的代码是：EETOP专业博客---电子工程师自己的家园’F-Sv(B/O.M7B)J

//正常代码EETOP专业博客---电子工程师自己的家园m(T/Of)E8XT$]O’@

TRACE(\"Some infomation\");

kh|L at|Guest//正常代码

Release模式下的代码是：EETOP专业博客---电子工程师自己的家园d(s8?2Os{ f-m9A

//正常代码EETOP专业博客---电子工程师自己的家园u%xpN?bg}5{[

//正常代码

预处理就是在进行编译的第一遍词法扫描和语法分析之前所作的工作。说白了，就是对源文件进行编译前，先对预处理部分进行处理，然后对处理后的代码进行编译。这样做的好处是，经过处理后的代码，将会变的很精短。

关于预处理命令中的文件包含（#include），宏定义（#define），书上已经有了详细的说明，在这里就不详述了。这里主要是对条件编译（#ifdef,#else,#endif,#if等）进行说明。以下分3种情况：

1：情况1：

#ifdef _XXXX

...程序段1...

#else

...程序段2...

#endif

这表明如果标识符_XXXX已被#define命令定义过则对程序段1进行编译；否则对程序段2进行编译。

例： [Page]

#define NUM

.............

.............

.............

#ifdef NUM

printf(\"之前NUM有过定义啦！:) \\n\");

#else

printf(\"之前NUM没有过定义！:( \\n\");

#endif

}

如果程序开头有#define NUM这行，即NUM有定义，碰到下面#ifdef NUM的时候，当然执行第一个printf。否则第二个printf将被执行。

我认为，用这种，可以很方便的开启/关闭整个程序的某项特定功能。

2:情况2：

#ifndef _XXXX

...程序段1...

#else

...程序段2...

#endif

这里使用了#ifndef，表示的是if not def。当然是和#ifdef相反的状况（如果没有定义了标识符_XXXX，那么执行程序段1，否则执行程序段2）。例子就不举了。

3：情况3：

#if 常量

...程序段1...

#else

...程序段2...

#endif

这里表示，如果常量为真（非0，随便什么数字，只要不是0），就执行程序段1，否则执行程序段2。

我认为，这种方法可以将测试代码加进来。当需要开启测试的时候，只要将常量变1就好了。而不要测试的时候，只要将常量变0。

#if语句要判断是宏的值，宏的值只有通过#define或者在命令行-DXXX的形式定义。

用const int UP = 1；形式定义了一个整型常量，而不是一个宏。

对于宏的替换，是在预编译阶段就进行的，会把所有的宏替换为实际的值。而对const int UP的解析，却是在编译阶段。

在预编译阶段，只认#define定义的宏，不会理会const int 定义出来的常量，所以#if永远为假。

【fallinleave】:

#if寻找UP宏，你用const只是定义常量，并没有UP这个宏，而#define 是定义的宏，所以成立。


C语言包含5个基本数据类型: void, int, float, double, 和 char

C语言包含5个基本数据类型: void, int, float, double, 和 char. （另：C++ 定义了另外两个基本数据类型: bool 和 wchar_t. 一些基本数据类型能够被 signed, unsigned, short, 和 long 修饰。所以short,long等等都不算基本数据类型。

这是书上说的，所以C++是7种基本数据类型。空值型是一种，但枚举型不算。原因就是枚举型可分的，所以不够基本。

不过不同的书也有不同的规定，比如C++Primer上就说是bool,char,wchar_t,short,int,long,float,double,long double和void，这个暂时没有定论。)

============================================================

基本类型：包括字节型（char）、整型（int）和浮点型（float/double）。

定义基本类型变量时，可以使用符号属性signed、unsigned（对于char、int），和长度属性short、long（对于int、double）对变量的取值区间和精度进行说明。

下面列举了Dev-C++下基本类型所占位数和取值范围：

符号属性 | 长度属性 | 基本型 | 所占位数 | 取值范围 | 输入符举例 | 输出符举例
 |  | char | 8 | -2^7 ~ 2^7-1 -128~127 | %c %c | %d、%u
signed |  | char | 8 | -2^7 ~ 2^7-1 -128~127 | %c %c | %d、%u
unsigned |  | char | 8 | 0 ~ 2^8-1 0~255 | %c %c | %d、%u
signed | short | [int] | 16 | -2^15 ~ 2^15-1 | %hd | %hd
unsigned | short | [int] | 16 | 0 ~ 2^16-1 | %hu、%ho、%hx | %hu、%ho、%hx
signed |  | int | 32 | -2^31 ~ 2^31-1 | %d | %d
unsigned |  | [int] | 32 | 0 ~ 2^32-1 | %u、%o、%x | %u、%o、%x
signed | long | [int] | 32 | -2^31 ~ 2^31-1 | %ld | %ld
unsigned | long | [int] | 32 | 0 ~ 2^32-1 | %lu、%lo、%lx | %lu、%lo、%lx
signed | long long | [int] | 64 | -2^63 ~ 2^63-1 | %I64d | %I64d
unsigned | long long | [int] | 64 | 0 ~ 2^64-1 | %I64u、%I64o、%I64x | %I64u、%I64o、%I64x
 |  | float | 32 | +/- 3.40282e+038 | %f、%e、%g | %f、%e、%g
 |  | double | 64 | +/- 1.79769e+308 | %lf、%le、%lg | %f、%e、%g
 | long | double | 96 | +/- 1.79769e+308 | %Lf、%Le、%Lg | %Lf、%Le、%Lg

几点说明：

1. 注意! 表中的每一行，代表一种基本类型。“[]”代表可省略。

例如：char、signed char、unsigned char是三种互不相同的类型；

int、short、long也是三种互不相同的类型。

可以使用C++的函数重载特性进行验证，如:

void Func(char ch) {}

void Func(signed char ch) {}

void Func(unsigned char ch) {}

是三个不同的函数。

2. char/signed char/unsigned char型数据长度为1字节；

char为有符号型，但与signed char是不同的类型

注意! 并不是所有编译器都这样处理，char型数据长度不一定为1字节，char也不一定为有符号型。

3. 将char/signed char转换为int时，会对最高符号位1进行扩展，从而造成运算问题。

所以,如果要处理的数据中存在字节值大于127的情况，使用unsigned char较为妥当。

程序中若涉及位运算，也应该使用unsigned型变量。

4. char/signed char/unsigned char输出时，使用格式符%c（按字符方式）；

或使用%d、%u、%x/%X、%o，按整数方式输出；

输入时，应使用%c，若使用整数方式，Dev-C++会给出警告，不建议这样使用。

5. int的长度，是16位还是32位，与编译器字长有关。

16位编译器（如TC使用的编译器）下，int为16位；32位编译器（如VC使用的编译器cl.exe）下，int为32 位。

6. 整型数据可以使用%d（有符号10进制）、%o（无符号8进制）或%x/%X（无符号16进制）方式输入输出。

而格式符%u，表示unsigned，即无符号10进制方式。

7. 整型前缀h表示short，l表示long。

输入输出short/unsigned short时，不建议直接使用int的格式符%d/%u等，要加前缀h。

这个习惯性错误，来源于TC。TC下，int的长度和默认符号属性，都与short一致，

于是就把这两种类型当成是相同的，都用int方式进行输入输出。

8. 关于long long类型的输入输出：

"%lld"和"%llu"是linux下gcc/g++用于long long int类型(64 bits)输入输出的格式符。

而"%I64d"和"%I64u"则是Microsoft VC++库里用于输入输出__int64类型的格式说明。

Dev-C++使用的编译器是Mingw32，Mingw32是x86-win32 gcc子项目之一，编译器核心还是linux下的gcc。

进行函数参数类型检查的是在编译阶段，gcc编译器对格式字符串进行检查，显然它不认得"%I64d"，

所以将给出警告“unknown conversion type character `I' in format”。对于"%lld"和"%llu"，gcc理所当然地接受了。

Mingw32在编译期间使用gcc的规则检查语法，在连接和运行时使用的却是Microsoft库。

这个库里的printf和scanf函数当然不认识linux gcc下"%lld"和"%llu"，但对"%I64d"和"%I64u"，它则是乐意接受，并能正常工作的。

9. 浮点型数据输入时可使用%f、%e/%E或%g/%G，scanf会根据输入数据形式，自动处理。

输出时可使用%f（普通方式）、%e/%E（指数方式）或%g/%G（自动选择）。

10. 浮点参数压栈的规则：float(4 字节)类型扩展成double(8 字节)入栈。

所以在输入时，需要区分float(%f)与double(%lf)，而在输出时，用%f即可。

printf函数将按照double型的规则对压入堆栈的float(已扩展成double)和double型数据进行输出。

如果在输出时指定%lf格式符，gcc/mingw32编译器将给出一个警告。

11. Dev-C++(gcc/mingw32)可以选择float的长度，是否与double一致。

12. 前缀L表示long（double）。

虽然long double比double长4个字节，但是表示的数值范围却是一样的。

long double类型的长度、精度及表示范围与所使用的编译器、操作系统等有关。

====================================================================
（一）32位平台：
分为有符号型与无符号型。
有符号型：
short 在内存中占两个字节，范围为-2^15~（2^15-1)
int 在内存中占四个字节，范围为-2^31~（2^31-1）
long在内存中占四个字节，范围为-2^31~2^31-1
无符号型：最高位不表示符号位
unsigned short 在内存中占两个字节，范围为0~2^16-1
unsigned int 在内存中占四个字节，范围为0~2^32-1
unsigned long在内存中占四个字节，范围为0~2^32-1
实型变量:
分单精度 float 和双精度 double 两种形式：
float:占四个字节，提供7~8位有效数字。
double: 占八个字节，提供15~16位有效数字。

（二）16位平台:
1)整型(基本型)：类型说明符为int，在内存中占2个字节。
2)短整型：类型说明符为short int或short。所占字节和取值范围均与整型(基本型)相同。
3)长整型：类型说明符为long int或long，在内存中占4个字节。
无符号型：类型说明符为unsigned。
无符号型又可与上述三种类型匹配而构成：
各种无符号类型量所占的内存空间字节数与相应的有符号类型量相同。但由于省去了符号位，故不能表示负数。
实型变量:
分为单精度（float型）、双精度（double型）和长双精度（long double型）三类。
单精度型占4个字节（32位）内存空间，其数值范围为3.4E-38～3.4E+38，只能提供七位有效数字。
双精度型占8 个字节（64位）内存空间，其数值范围为1.7E-308～1.7E+308，可提供16位有效数字。
长双精度型16 个字节（128位）内存空间，可提供18-19位有效数字。
============================================================================
C语言中5种基本数据类型的存储空间长度的排列顺序:

char = signed char = unsigned char < short int = unsigned short int <= int = unsigned int <= long int = unsigned long int <= long long int = unsigned long long int

float <= double <= long double

用short修饰int时,short int表示短整型,占2字节的数据位.用long修饰int时,long int表示长整型,占4字节的数据位,在vc++6.0中int的数据类型也是占用了4字节的数据位, 那么应用int型与long型有什么区别呢?下面就让我们来解答这个问题.

我们知道现在的C++开发工具非常的多,int型在不同的系统中有可能占用的字节数会不一样,而short与long修饰的int型的字节数是固定的,任何支持标准C++编译系统中都是如此.所以如果需要编写可移植性好的程序,应该将整型数据声明为short与long修饰的int型。


2

2.1 C 语言的数据类型

C语言的数据类型具体分类如下：

基本类型：分为整型、实型（又称浮点型）、字符型和枚举型四种。q

构造类型：分为数组类型、结构类型和共用类型三种。q

指针类型q

空类型q

2.2 常量和变量

2.2.1 常量

1、常量的概念：在程序运行过程中，其值不能被改变的量称为常量

2、常量的分类：

整型常量q

实型常量q

字符常量q

符号常量q

3、常量的类型，可通过书写形式来判别。

2.2.2 变量

1、变量的概念：在程序运行过程中，其值可以被改变的量称为变量

2、变量名：每个变量都必须有一个名字——变量名，变量命名遵循标识符命名规则

3、变量值：在程序运行过程中，变量值存储在内存中。在程序中，通过变量名来引用变量的值

4、标识符命名规则：

只能由字母、数字和下划线组成，且以字母或下划线开头q

有效长度：随系统而异，但至少前8个字符有效。如果超长，则超长部分被舍弃q

C语言的关键字不能用作变量名q

注意：C语言对英文字母的大小敏感，即同一字母的大小写，被认为是两个不同的字符。F

习惯上，变量名和函数名中的英文字母用小写，以增加可读性。

5、变量的定义与初始化

在C语言中，要求对所有用到的变量，必须先定义、后使用；且称在定义变量的同时进行赋初值的操作为变量初始化。

（1）变量定义的一般格式

数据类型 变量名[, 变量名2……];

例如，float radius, length, area;

（2）变量初始化的一般格式

数据类型 变量名[=初值][, 变量名2[=初值2]……];

例如，float radius=2.5, length, area;

2.3 整型数据

2.3.1 整型变量

1、分类

根据占用内存字节数的不同，整型变量又分为4类：

（1）基本整型（类型关键字为int）。

（2）短整型（类型关键字为short [int]）。

（3）长整型（类型关键字为long [int]）。

（4）无符号整型。无符号型又分为无符号基本整型（unsigned [int]）、无符号短整型（unsigned short）和无符号长整型（unsigned long）三种，只能用来存储无符号整数。

2、占用内存字节数与值域

上述各类型整型变量占用的内存字节数，随系统而异。

在 16位操作系统中，一般用2字节表示一个int型变量，且long型（4字节）≥int型（2字节）≥short型（2字节）。

显然，不同类型的整型变量，其值域不同。占用内存字节数为n的（有符号）整型变量，其值域为：-2n*8-1~（2n*8-1-1）；无符号整型变量的值域为：0~（2n*8-1）。

例如，PC机中的一个int型变量，其值域为-22*8-1~（22*8-1-1），即-32768~32767；一个unsigned型变量的值域为：0~（22*8-1），即0~65535。

实型数据

2.4.1 实型变量

C语言的实型变量，分为两种：

（1）单精度型：类型关键字为float，一般占 4字节（32位）、提供7位有效数字。

（2）双精度型：类型关键字为double，一般占 8个字节、提供15~16位有效数字。

2.4.2 实型常量

1、表示形式

实型常量即实数，在C语言中又称浮点数，其值有两种表达形式：

（1）十进制形式：例如3.14、9.8。

（2）指数形式：<尾数>E（e）<整型指数>。例如3.0E+5等。

2、关于类型

实型常量不分float型和double型。一个实型常量，可以赋给一个实型变量（float型或double型）。

2.5 字符型数据

2.5.1 字符常量

1、字符常量的定义

用一对单引号括起来的单个字符，称为字符常量。

例如，‘A’、‘1’、‘＋’等。

2、转义字符

C语言还允许使用一种特殊形式的字符常量，就是以反斜杠“\ ”开头的转义字符。

注意：如果反斜杠或单引号本身作为字符常量，必须使用转义字符：‘\\’、‘\’‘。

[案例2.1] 用转义字符输出可打印字符和不可打印字符。

main()

{

printf(“\x4F\x4B\x21\n”); /* 等价于printf(“OK!\n”); */

printf(“\x15 \xAB\n”);

}

程序运行结果如下：

OK！

2.5.2 字符变量

字符变量的类型关键字为char，一般占用1字节内存单元。

1.变量值的存储

字符变量用来存储字符常量。将一个字符常量存储到一个字符变量中，实际上是将该字符的ASCII码值（无符号整数）存储到内存单元中。

例如，

char ch1, ch2; /*定义两个字符变量：ch1，ch2*/

ch1=’a’; ch2=’b’; /*给字符变量赋值*/

2.特性

字符数据在内存中存储的是字符的ASCII码—— 一个无符号整数，其形式与整数的存储形式一样，所以C语言允许字符型数据与整型数据之间通用。

（1）一个字符型数据，既可以字符形式输出，也可以整数形式输出。

[案例2.2] 字符变量的字符形式输出和整数形式输出。

main()

{

char ch1,ch2;

ch1='a'; ch2='b';

printf(“ch1=%c,ch2=%c\n”,ch1,ch2);

printf(“ch1=%d,ch2=%d\n”,ch1,ch2);

}

程序运行结果：

ch1=a,ch2=b

ch1=97,ch2=98

（2）允许对字符数据进行算术运算，此时就是对它们的ASCII码值进行算术运算。

[案例2.3] 字符数据的算术运算。

main()

{

char ch1,ch2;

ch1=‘a’; ch2=‘B’;

printf(“ch1=%c,ch2=%c\n”,ch1-32,ch2+32); /*字母的大小写转换*/

/*用字符形式输出一个大于256的数值*/

printf("ch1+200=%d\n", ch1+200);

printf("ch1+200=%c\n", ch1+200);

printf("ch1+256=%d\n", ch1+256);

printf("ch1+256=%c\n", ch1+256);

}

程序运行结果：

ch1=A,ch2=b

ch1+200=297

ch1+200=)

ch1+256=353

ch1+256=a


条件编译是指在编译源程序前，根据指定的条件确定编译的范围

条件编译是指在编译源程序前，根据指定的条件确定编译的范围。条件编译通过if系列预处理指令完成。预处理指令#if簇的用法在很多情况下与if控制语句类似，差别是if语句控制着某些语句是否被执行，某些语句是否被编译。

条件编译预处理指令有以下几种形式。

1．#ifdef标识符

程序段1

#else

程序段2

#end

当标识符已经被定义过，则对程序段1进行编译，否则对程序段2进行编译。

和if—else语句一样，#else子句可以缺省，缺省后的形式为：

#ifdef标识符

程序段1

#endif

另可增加#elif子句构成嵌套。

2．#ifndef标识符

程序段1

#else

程序段2

#endif

与第一种形式正好相反，当标识符未被定义过时，则对程序段1进行编译，否则，对程序段2进行编译。

3．#if表达式

程序段1

#else

程序段2

#endif

当表达式之值为真(非零)时，编译程序段1，否则编译程序段2。

例如：

#if DEBuG==1

调试代码

#endif

开发程序时，：DEBUG定义为1，插入调试代码帮助调试程序。调试完成，将DEBI．JG定义为o，再编译一次，即可去掉调试代码。

第一、 二种形式编译的控制条件为定义的标识符，第三种方式编译的控制条件为表达式，当然可用符号常量。

预处理过程扫描源代码，对其进行初步的转换，产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。

在C语言中，并没有任何内在的机制来完成如下一些功能：在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作，就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序，但通常认为它们是独立于编译器的。预处理过程读入源代码，检查包含预处理指令的语句和宏定义，并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字，在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令，该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令：

指令 用途

# 空指令，无任何效果

#include 包含一个源代码文件

#define 定义宏

#undef 取消已定义的宏

#if 如果给定条件为真，则编译下面代码

#ifdef 如果宏已经定义，则编译下面代码

#ifndef 如果宏没有定义，则编译下面代码

#elif 如果前面的#if给定条件不为真，当前条件为真，则编译下面代码

#endif 结束一个#if……#else条件编译块

#error 停止编译并显示错误信息

一、文件包含

#include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的，也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。

预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时，通过给定编译时的条件来达到不同的效果。例如：

#define AAA

#include "t.c"

#undef AAA

#include "t.c"

为了避免那些只能包含一次的头文件被多次包含，可以在头文件中用编译时条件来进行控制。例如：

/*my.h*/

#ifndef MY_H

#define MY_H

……

#endif

在程序中包含头文件有两种格式：

#include <my.h>

#include "my.h"

第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件，如果找不到，再搜索编译器自带的头文件。

采用两种不同包含格式的理由在于，编译器是安装在公共子目录下的，而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件，也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。

二、宏

宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏，这样的宏可以象函数一样被调用，但它是在调用语句处展开宏，并用调用时的实际参数来代替定义中的形式参数。

1.#define指令

#define预处理指令是用来定义宏的。该指令最简单的格式是：首先神明一个标识符，然后给出这个标识符代表的代码。在后面的源代码中，就用这些代码来替代该标识符。这种宏把程序中要用到的一些全局值提取出来，赋给一些记忆标识符。

#define MAX_NUM 10

int array[MAX_NUM];

for(i=0;i<MAX_NUM;i++) /*……*/

在这个例子中，对于阅读该程序的人来说，符号MAX_NUM就有特定的含义，它代表的值给出了数组所能容纳的最大元素数目。程序中可以多次使用这个值。作为一种约定，习惯上总是全部用大写字母来定义宏，这样易于把程序红的宏标识符和一般变量标识符区别开来。如果想要改变数组的大小，只需要更改宏定义并重新编译程序即可。

宏表示的值可以是一个常量表达式，其中允许包括前面已经定义的宏标识符。例如：

#define ONE 1

#define TWO 2

#define THREE (ONE+TWO)

注意上面的宏定义使用了括号。尽管它们并不是必须的。但出于谨慎考虑，还是应该加上括号的。例如：

six=THREE*TWO;

预处理过程把上面的一行代码转换成：

six=(ONE+TWO)*TWO;

如果没有那个括号，就转换成six=ONE+TWO*TWO;了。

宏还可以代表一个字符串常量，例如：

#define VERSION "Version 1.0 Copyright(c) 2003"

2.带参数的#define指令

带参数的宏和函数调用看起来有些相似。看一个例子：

#define Cube(x) (x)*(x)*(x)

可以时任何数字表达式甚至函数调用来代替参数x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中，而且参数也包含在括号中，这样就保证了宏和参数的完整性。看一个用法：

int num=8+2;

volume=Cube(num);

展开后为(8+2)*(8+2)*(8+2);

如果没有那些括号就变为8+2*8+2*8+2了。

下面的用法是不安全的：

volume=Cube(num++);

如果Cube是一个函数，上面的写法是可以理解的。但是，因为Cube是一个宏，所以会产生副作用。这里的擦书不是简单的表达式，它们将产生意想不到的结果。它们展开后是这样的：

volume=(num++)*(num++)*(num++);

很显然，结果是10*11*12,而不是10*10*10;

那么怎样安全的使用Cube宏呢？必须把可能产生副作用的操作移到宏调用的外面进行：

int num=8+2;

volume=Cube(num);

num++;

3.#运算符

出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。例如：

#define PASTE(n) "adhfkj"#n

main()

{

printf("%s\n",PASTE(15));

}

宏定义中的#运算符告诉预处理程序，把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15。

4.##运算符

##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子：

#define NUM(a,b,c) a##b##c

#define STR(a,b,c) a##b##c

main()

{

printf("%d\n",NUM(1,2,3));

printf("%s\n",STR("aa","bb","cc"));

}

最后程序的输出为:

123

aabbcc

千万别担心，除非需要或者宏的用法恰好和手头的工作相关，否则很少有程序员会知道##运算符。绝大多数程序员从来没用过它。

三、条件编译指令

条件编译指令将决定那些代码被编译，而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。

1.#if指令

#if指令检测跟在制造另关键字后的常量表达式。如果表达式为真，则编译后面的代码，知道出现#else、#elif或#endif为止；否则就不编译。

2.#endif指令

#endif用于终止#if预处理指令。

#define DEBUG 0

main()

{

#if DEBUG

printf("Debugging\n");

#endif

printf("Running\n");

}

由于程序定义DEBUG宏代表0，所以#if条件为假，不编译后面的代码直到#endif，所以程序直接输出Running。

如果去掉#define语句，效果是一样的。

3.#ifdef和#ifndef

#define DEBUG

main()

{

#ifdef DEBUG

printf("yes\n");

#endif

#ifndef DEBUG

printf("no\n");

#endif

}

#if defined等价于#ifdef; #if !defined等价于#ifndef

4.#else指令

#else指令用于某个#if指令之后，当前面的#if指令的条件不为真时，就编译#else后面的代码。#endif指令将中指上面的条件块。

#define DEBUG

main()

{

#ifdef DEBUG

printf("Debugging\n");

#else

printf("Not debugging\n");

#endif

printf("Running\n");

}

5.#elif指令

#elif预处理指令综合了#else和#if指令的作用。

#define TWO

main()

{

#ifdef ONE

printf("1\n");

#elif defined TWO

printf("2\n");

#else

printf("3\n");

#endif

}

程序很好理解，最后输出结果是2。

6.其他一些标准指令

#error指令将使编译器显示一条错误信息，然后停止编译。

#line指令可以改变编译器用来指出警告和错误信息的文件号和行号。

#pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。


有些程序在调试、兼容性、平台移植等情况下可能想要通过简单地设置一些参数就生成一个不同的软件，这当然可以通过变量设置，把所有可能用到的代码都写进去，在初始化时配置，但在不同的情况下可能只用到一部分代码，就没必要把所有的代码都写进去，就可以用条件编译，通过预编译指令设置编译条件，在不同的需要时编译不同的代码。

（一）条件编译方法

条件编译是通过预编译指令来实现的，主要方法有：

1、#if， #elif， #else， #endif

#if　条件　1
  代码段　1
#elif 条件　2
代码段　2
...
#elif 条件　n
代码段　n
#else
代码段 n+1
#endif

即可以设置不同的条件，在编译时编译不同的代码，预编译指令中的表达式与C语言本身的表达式基本一至如逻辑运算、算术运算、位运算等均可以在预编译指令中使用。之所以能够实现条件编译是因为预编译指令是在编译之前进行处理的，通过预编译进行宏替换、条件选择代码段，然后生成最后的待编译代码，最后进行编译。

#if的一般含义是，如果#if后面的常量表达式为true，则编译它所控制的代码，如条件1成立时就代码段1，条件1不成立再看条件2是否成立，如果条件2成立则编译代码段2，否则再依次类推判断其它条件，如果条件1－N都不成力则会编译最后的代码段n+1.

2、#ifdef， #else， #endif或#ifndef， #else， #endif

条件编译的另一种方法是用#ifdef与#ifndef命令，它们分别表示“如果有定义”及“如果无定义”。有定义是指在编译此段代码时是否有某个宏通过 #define 指令定义的宏，#ifndef指令指找不到通过#define定义的某宏，该宏可以是在当前文件此条指令的关面定义的，也可以是在其它文件中，但在此指令之前包含到该文件中的。

#ifdef的一般形式是：

#ifdef macro_name
代码段　1
#else
代码段　2
#endif

或
#ifdef的一般形式是：

#ifndef macro_name
代码段　2
#else
代码段　1
#endif

这两段代码的效果是完全一样的。

3、通过宏函数defined（macro_name）

参数为宏名（无需加""），如果该macro_name定义过则返回真，否则返回假，用该函数则可以写比较复杂的条件编译指令如

#if defined(macro1) || (!defined(macro2) && defined(macro3))
...
#else
...
#endif

（二）条件编译技巧与示例

（1）#ifdef和#defined（）比较

首先比较一下这两种方法，第一种方法只能判断一个宏，如果条件比较复杂实现起来比较烦锁，用后者就比较方便。如有两个宏MACRO_1，MACRO_2只有两个宏都定义过才会编译代码段A，分别实现如下：

#ifdef MACRO_1
#ifdef MACRO_2
代码段　A
#endif
#endif

或者
#if defined(MACRO_1) && defined(MACRO_2)
#endif

同样，要实现更复杂的条件用#ifdef更麻烦，所以推荐使用后者，因为即使当前代码用的是简单的条件编译，以后在维护、升级时可能会增加，用后者可维护性较强。旧的编译器可能没有实现#defined（）指令，C99已经加为标准。要兼容老的编译器，还需用#ifdef指令。

2、#if与 #ifdef或#if　defined（）比较

比如自己写了一个printf函数，想通过一个宏MY_PRINTF_EN实现条件编译，用#if可实现如下

#define MY_PRINTF_EN 1

#if MYS_PRINTF_EN == 1
int printf(char* fmt, char* args, ...)
{
...
}
#endif

如果宏MY_PRINTF_EN定义为1则编译这段代码，如果宏定义不为1或者没有定义该宏，则不编译这段代码。同样也可以通过#ifdef或者#defined（）实现，如

#define MY_PRINTF_EN 1

#if defined(MY_PRINTF_EN)
int printf(char* fmt, char* args, ...)
{
...
}
#endif

在这种情况下两种方法具有异曲同工之妙，但试想如果你为了节约代码写了两个printf函数，在不同情况下使用不同的printf函数，一个是精简版一个是全功能标准版，如：

#define MY_PRINTF_SIMPLE

#ifdef MY_PRINTF_SIMPLE
void printf(*str) // 向终端简单地输出一个字符串
{...
}
#endif
#ifdef MY_PRINTF_STANDARD
int printf(char* fmt, char* args, ...)
{...
}
#endif

同样可以用#if defined()实现
#define MY_PRINTF_SIMPLE

#if defined(MY_PRINTF_SIMPLE)
void printf(*str) // 向终端简单地输出一个字符串
{
...
}
#elif defined(MY_PRINTF_STANDARD)
int printf(char* fmt, char* args, ...)
{
...
}
#endif

两种方法都可以实现，但可见后者更方便。但试想如果你有三个版本，用前者就更麻烦了，但方法相似，用后者就更方便，但仍需三个宏进行控制，你要住三个宏，改进一下就用#if可以用一个宏直接控制N种情况如：

#define MY_PRINTF_VERSION 1

#if MY_PRINTF_VERSION == 1
void printf(*str) // 向终端简单地输出一个字符串
{
...
}
#elif MY_PRINTF_VERSION == 2
int printf(char* fmt, char* args, ...)
{
...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
...
}
#else
默认版本
#endif

这样，你只需修改一下数字就可以完成版本的选择了

看来好像用#if 比较好了，试想如下情况：你写了一个配置文件叫做config.h用来配置一些宏，通过这些宏来控制代码，如你在config.h的宏

#define MY_PRINTF_EN 1

来控制是否需要编译自己的printf函数，而在你的源代码文件printf.c中有如下指令

＃i nclude "config.h"
#if MY_PRINTF_EN == 1
int printf(char* fmt, char* args, ...)
{
...
}
#endif

但这样也会有一个问题，就是如果你忘了在config.h中添加宏MY_PRINTF_EN，那么自己写的printf函数也不会被编译，有些编译器会给出警告：MY_PRINTF_EN未定义。如果你有两个版本的想有一个默认版本，可以在printf.c中这样实现

#incldue "config.h"
#if !defined(MY_PRINTF_VERSION)
#define MY_PRINTF_VERSION 1
#endif

#if MY_PRINTF_VERSION == 1
void printf(*str) // 向终端简单地输出一个字符串
{
...
}
#elif MY_PRINTF_VERSION == 2
int printf(char* fmt, char* args, ...)
{
...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
...
}
#endif

这种情况下还得用到#ifdef或#if　defined（），你可以不用动主体的任何代码，只需要修改printf.c文件中MY_RPINTF_VERSION宏的数字就可以改变了，如果用前面那种方法还得拖动代码，在拖动中就有可能造成错误。

再试想，如果软件升级了，或者有了大的改动，原来有三个版本，现在只剩下两个版本了，如

#if MY_PRINTF_VERSION == 2
int printf(char* fmt, char* args, ...)
{
...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
...
}
#endif

因为这些核心代码不想让使用这些代码的人关心，他们只需要修改config.h文件，那就要在printf.c中实现兼容性。如果以前有人在config.h配置宏MY_PRINTF_VERSION为1，即有

#define MY_PRINTF_VERSION 1

而现在没有1版本了，要想兼容怎么办？那当然可以用更复杂的条件实现如：

#if MY_PRINTF_VERSION == 2 || MY_PRINTF_VERSION == 1
int printf(char* fmt, char* args, ...)
{
...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
...
}
#endif

不过还有另外一种方法，即使用#undef命令

#if MY_PRINTF_VERSION == 1
#undef MY_PRINTF_VERSION
#define MY_PRINTF_VERSION 2
#endif
#if MY_PRINTF_VERSION == 2
int printf(char* fmt, char* args, ...)
{
...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
...
}
#endif

用#if还有一个好处，如果你把宏名记错了，把MY_PRINTF_EN定义成了MY_PRINT_EN，那么你用#ifdef MY_PRINTF_EN或者#if defined（MY_PRINTF_EN）控制的代码就不能被编译，查起来又不好查，用#if MY_PRINTF_EN ==1控制就很好查，因为你把MY_PRINTF_EN定义成MY_PRINT_EN，则MY_PRINTF_EN实际上没有定义，那么编译器会给出警告#if MY_PRINTF_EN　== 1中的MY_PRINTF_EN没有定义，但错就比较快。


Data Types

Value | Meaning
ATOM | Atom. For more information, see Atoms.
BOOL | Boolean variable (should be TRUE or FALSE).
BOOLEAN | Boolean variable (should be TRUE or FALSE).
BYTE | Byte (8 bits).
CALLBACK | Calling convention for callback functions.
CHAR | 8-bit Windows (ANSI) character. For more information, see Character Sets Used By Fonts.
COLORREF | Red, green, blue (RGB) color value (32 bits). See COLORREF for information on this type.
CONST | Variable whose value is to remain constant during execution.
CRITICAL_SECTION | Critical-section object. For more information, see Critical Section Objects.
DWORD | 32-bit unsigned integer.
DWORD_PTR | Unsigned long type for pointer precision. Use when casting a pointer to a long type to perform pointer arithmetic. (Also commonly used for general 32-bit parameters that have been extended to 64 bits in 64-bit Windows. )
DWORD32 | 32-bit unsigned integer.
DWORD64 | 64-bit unsigned integer.
FLOAT | Floating-point variable.
HACCEL | Handle to an accelerator table.
HANDLE | Handle to an object.
HBITMAP | Handle to a bitmap.
HBRUSH | Handle to a brush.
HCONV | Handle to a dynamic data exchange (DDE) conversation.
HCONVLIST | Handle to a DDE conversation list.
HCURSOR | Handle to a cursor.
HDC | Handle to a device context (DC).
HDDEDATA | Handle to DDE data.
HDESK | Handle to a desktop.
HDROP | Handle to an internal drop structure.
HDWP | Handle to a deferred window position structure.
HENHMETAFILE | Handle to an enhanced metafile.
HFILE | Handle to a file opened by OpenFile, not CreateFile.
HFONT | Handle to a font.
HGDIOBJ | Handle to a GDI object.
HGLOBAL | Handle to a global memory block.
HHOOK | Handle to a hook.
HICON | Handle to an icon.
HIMAGELIST | Handle to an image list.
HIMC | Handle to input context.
HINSTANCE | Handle to an instance.
HKEY | Handle to a registry key.
HKL | Input locale identifier.
HLOCAL | Handle to a local memory block.
HMENU | Handle to a menu.
HMETAFILE | Handle to a metafile.
HMODULE | Handle to a module. The value is the base address of the module.
HMONITOR | Handle to a display monitor.
HPALETTE | Handle to a palette.
HPEN | Handle to a pen.
HRGN | Handle to a region.
HRSRC | Handle to a resource.
HSZ | Handle to a DDE string.
HWINSTA | Handle to a window station.
HWND | Handle to a window.
INT | 32-bit signed integer.
INT_PTR | Signed integral type for pointer precision. Use when casting a pointer to an integer to perform pointer arithmetic.
INT32 | 32-bit signed integer.
INT64 | 64-bit signed integer.
LANGID | Language identifier. For more information, see Locales.
LCID | Locale identifier. For more information, see Locales.
LCTYPE | Locale information type. For a list, see Locale and Language Information.
LONG | 32-bit signed integer.
LONG_PTR | Signed long type for pointer precision. Use when casting a pointer to a long to perform pointer arithmetic.
LONG32 | 32-bit signed integer.
LONG64 | 64-bit signed integer.
LONGLONG | 64-bit signed integer.
LPARAM | Message parameter.
LPBOOL | Pointer to a BOOL.
LPBYTE | Pointer to a BYTE.
LPCOLORREF | Pointer to a COLORREF value.
LPCRITICAL_SECTION | Pointer to a CRITICAL_SECTION.
LPCSTR | Pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
LPCTSTR | An LPCWSTR if UNICODE is defined, an LPCTSTR otherwise.
LPCVOID | Pointer to a constant of any type.
LPCWSTR | Pointer to a constant null-terminated string of 16-bit Unicode characters. For more information, see Character Sets Used By Fonts.
LPDWORD | Pointer to a DWORD.
LPHANDLE | Pointer to a HANDLE.
LPINT | Pointer to an INT.
LPLONG | Pointer to a LONG.
LPSTR | Pointer to a null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
LPTSTR | An LPWSTR if UNICODE is defined, an LPSTR otherwise.
LPVOID | Pointer to any type.
LPWORD | Pointer to a WORD.
LPWSTR | Pointer to a null-terminated string of 16-bit Unicode characters. For more information, see Character Sets Used By Fonts.
LRESULT | Signed result of message processing.
LUID | Locally unique identifier.
PBOOL | Pointer to a BOOL.
PBOOLEAN | Pointer to a BOOL.
PBYTE | Pointer to a BYTE.
PCHAR | Pointer to a CHAR.
PCRITICAL_SECTION | Pointer to a CRITICAL_SECTION.
PCSTR | Pointer to a constant null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
PCTSTR | A PCWSTR if UNICODE is defined, a PCSTR otherwise.
PCWCH | Pointer to a constant WCHAR.
PCWSTR | Pointer to a constant null-terminated string of 16-bit Unicode characters. For more information, see Character Sets Used By Fonts.
PDWORD | Pointer to a DWORD.
PFLOAT | Pointer to a FLOAT.
PHANDLE | Pointer to a HANDLE.
PHKEY | Pointer to an HKEY.
PINT | Pointer to an INT.
PLCID | Pointer to an LCID.
PLONG | Pointer to a LONG.
PLUID | Pointer to a LUID.
POINTER_32 | 32-bit pointer. On a 32-bit system, this is a native pointer. On a 64-bit system, this is a truncated 64-bit pointer.
POINTER_64 | 64-bit pointer. On a 64-bit system, this is a native pointer. On a 32-bit system, this is a sign-extended 32-bit pointer.
PSHORT | Pointer to a SHORT.
PSTR | Pointer to a null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
PTBYTE | Pointer to a TBYTE.
PTCHAR | Pointer to a TCHAR.
PTSTR | PWSTR if UNICODE is defined, a PSTR otherwise.
PTBYTE | Pointer to a TBYTE.
PTCHAR | Pointer to a TCHAR.
PTSTR | A PWSTR if UNICODE is defined, a PSTR otherwise.
PUCHAR | Pointer to a UCHAR.
PUINT | Pointer to a UINT.
PULONG | Pointer to a ULONG.
PUSHORT | Pointer to a USHORT.
PVOID | Pointer to any type.
PWCHAR | Pointer to a WCHAR.
PWORD | Pointer to a WORD.
PWSTR | Pointer to a null-terminated string of 16-bit Unicode characters. For more information, see Character Sets Used By Fonts.
REGSAM | Security access mask for registry key.
SC_HANDLE | Handle to a service control manager database. For more information, see SCM Handles.
SC_LOCK | Handle to a service control manager database lock. For more information, see SCM Handles.
SERVICE_STATUS_HANDLE | Handle to a service status value. For more information, see SCM Handles.
SHORT | Short integer (16 bits).
SIZE_T | The maximum number of bytes to which a pointer can point. Use for a count that must span the full range of a pointer.
SSIZE_ T | Signed SIZE_T.
TBYTE | A WCHAR if UNICODE is defined, a CHAR otherwise.
TCHAR | A WCHAR if UNICODE is defined, a CHAR otherwise.
UCHAR | Unsigned CHAR.
UINT | Unsigned INT.
UINT_PTR | Unsigned INT_PTR.
UINT32 | Unsigned INT32.
UINT64 | Unsigned INT64.
ULONG | Unsigned LONG.
ULONG_PTR | Unsigned LONG_PTR.
ULONG32 | Unsigned LONG32.
ULONG64 | Unsigned LONG64.
ULONGLONG | 64-bit unsigned integer.
UNSIGNED | Unsigned attribute.
USHORT | Unsigned SHORT.
VOID | Any type.
WCHAR | 16-bit Unicode character. For more information, see Character Sets Used By Fonts.
WINAPI | Calling convention for system functions.
WORD | 16-bit unsigned integer.
WPARAM | Message parameter.


给你一个操作INI文件的类，使用起来会很方便：

class CIniReader

{

public:

// method to set INI file name, if not already specified

void setINIFileName(CString strINIFile);

// methods to return the lists of section data and section names

CStringList* getSectionData(CString strSection);

CStringList* getSectionNames();

// check if the section exists in the file

BOOL sectionExists(CString strSection);

// updates the key value, if key already exists, else creates a key-value pair

long setKey(CString strValue, CString strKey, CString strSection);

// give the key value for the specified key of a section

CString getKeyValue(CString strKey,CString strSection);

// default constructor

CIniReader()

{

m_sectionList = new CStringList();

m_sectionDataList = new CStringList();

}

CIniReader(CString strFile)

{

m_strFileName = strFile;

m_sectionList = new CStringList();

m_sectionDataList = new CStringList();

}

~CIniReader()

{

delete m_sectionList;

delete m_sectionDataList;

}

private:

// lists to keep sections and section data

CStringList *m_sectionDataList;

CStringList *m_sectionList;

CString m_strSection;

long m_lRetValue;

// ini file name

CString m_strFileName;

};

CString CIniReader::getKeyValue(CString strKey,CString strSection)

{

char ac_Result[255];

// Get the info from the .ini file

m_lRetValue = GetPrivateProfileString((LPCTSTR)strSection,(LPCTSTR)strKey,

"",ac_Result, 255, (LPCTSTR)m_strFileName);

CString strResult(ac_Result);

return strResult;

}

// Used to add or set a key value pair to a section

long CIniReader::setKey(CString strValue, CString strKey, CString strSection)

{

m_lRetValue = WritePrivateProfileString (strSection, strKey,

strValue, m_strFileName);

return m_lRetValue;

}

// Used to find out if a given section exists

BOOL CIniReader::sectionExists(CString strSection)

{

char ac_Result[100];

CString csAux;

// Get the info from the .ini file

m_lRetValue = GetPrivateProfileString((LPCTSTR)strSection,NULL,

"",ac_Result, 90, (LPCTSTR)m_strFileName);

// Return if we could retrieve any info from that section

return (m_lRetValue > 0);

}

// Used to retrieve all of the section names in the ini file

CStringList* CIniReader::getSectionNames() //returns collection of section names

{

char ac_Result[2000];

m_sectionList->RemoveAll();

m_lRetValue = GetPrivateProfileSectionNames(ac_Result,2000,(LPCTSTR)m_strFileName);

CString strSectionName;

for(int i=0; i<m_lRetValue; i++)

{

if(ac_Result[i] != '\0')

{

strSectionName = strSectionName + ac_Result[i];

}

else

{

if(strSectionName != "")

{

m_sectionList->InsertAfter(m_sectionList->GetTailPosition(),strSectionName);

}

strSectionName = "";

}

}

return m_sectionList;

}

// Used to retrieve all key/value pairs of a given section.

CStringList* CIniReader::getSectionData(CString strSection)

{

char ac_Result[2000]; //change size depending on needs

m_sectionDataList->RemoveAll();

m_lRetValue = GetPrivateProfileSection((LPCTSTR)strSection, ac_Result, 2000, (LPCTSTR)m_strFileName);

CString strSectionData;

for(int i=0; i<m_lRetValue; i++)

{

if(ac_Result[i] != '\0')

{

strSectionData = strSectionData + ac_Result[i];

}

else

{

if(strSectionData != "")

{

m_sectionDataList->InsertAfter(m_sectionDataList->GetTailPosition(),strSectionData);

}

strSectionData = "";

}

}

return m_sectionDataList;

}

void CIniReader::setINIFileName(CString strINIFile)

{

m_strFileName = strINIFile;

}


INI文件是一种文本格式的文件，其中的数据组织格式为：

;注释

[小节名]

关键字=值

...

INI文件允许有多个小节，每个小节又允许有多个关键字，"="后面是该关键字的值。

值的类型有三种：字符串、整型数值和布尔值。其中字符串存贮在INI文件中时没有引号，布尔真值用1表示，布尔假值用0表示。

注释以分号";"开头。

;注释

[Section1 Name]

KeyName1=value1

;注释

KeyName2=value2

…

[Section2 Name]

KeyName1=value1

KeyName2=value2

…

INI文件中可以存在多个小节（Section），每个小节的开始用包括在一对方括号中的小节名称指定，不同的小节不能重名，一个小节的内容从小节名称的下一行开始，直到下一个小节开始为止。用户程序可以按照自己的需求建立多个小节。

在每个小节中可以定义多个键（Key），每一个键由一个“键名＝键值”格式的字符串组成，并独自占用一行。在同一个小节中不能存在同名的键，但是在不同的小节中可以存在同名的键。

如果需要在INI文件的某些地方加注释，可以将注释放在单独的一行中，行首以分号开始，注释行出现的地方并没有什么限制，既可以出现在文件的最前面，也可以出现在文件的任何一行中。

一般来说，如果在自己开发的应用程序中使用系统定义的INI文件，如Win.ini等，由于文件中已经存在多个小节，那么自己建立一个独立的小节比较合适，然后在这个小节中定义不同的键值，比如，下面是笔者的计算机上Win.ini文件的片断：

…

[MCI Extensions]

asf=MPEGVideo

asx=MPEGVideo

m3u=MPEGVideo

mp2v=MPEGVideo

mp3=MPEGVideo

mpv2=MPEGVideo

wma=MPEGVideo

wmv=MPEGVideo

[Hex Workshop]

Path=C:\PROGRA~1\BREAKP~1\HEXWOR~1.1\hworks32.exe

CurrentVersion=3.11

…

其中的“MCI Extensions”小节是Windows系统自身使用的小节，Windows在这里定义了一些媒体文件的关联方式，而“Hex Workshop”小节是安装了HexWorkshop16进制编辑器后由软件创建并使用的，编辑器在小节中用“Path”键定义了软件的安装目录、在“CurrentVersion”键中定义了软件的版本号。

如果觉得往系统INI文件中写数据显得不是那么“绿色环保”，那么应用程序可以建立一个独立的INI文件。如本节的例子文件就在自己运行的目录中建立了一个Option.ini文件，并在“Windows Position”小节的“X”，“Y”键中保存窗口的位置，以便在下一次运行的时候将窗口移动到上一次退出时所处的位置，内容如下：

[Windows Position]

X=194

Y=162

…

Windows系统提供了一系列函数对INI文件进行操作，其中包括读取和设置键值，获取小节名称列表及获取和删除整个小节内容等函数。下面的例子演示了这些功能的使用方法。

在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入INI文件中,程序初始化时再读入.具体应用如下:

一.将信息写入.INI文件中.

    1.所用的WINAPI函数原型为:

BOOL WritePrivateProfileString(

LPCTSTR lpAppName,

LPCTSTR lpKeyName,

LPCTSTR lpString,

LPCTSTR lpFileName

);

其中各参数的意义:

      LPCTSTR lpAppName 是INI文件中的一个字段名.

      LPCTSTR lpKeyName 是lpAppName下的一个键名,通俗讲就是变量名.

      LPCTSTR lpString 是键值,也就是变量的值,不过必须为LPCTSTR型或CString型的.

      LPCTSTR lpFileName 是完整的INI文件名.

2.具体使用方法:设现有一名学生,需把他的姓名和年龄写入 c:\stud\student.ini 文件中.

CString strName,strTemp;

int nAge;

strName="张三";

nAge=12;

::WritePrivateProfileString("StudentInfo","Name",strName,"c:\\stud\\student.ini");

    此时c:\stud\student.ini文件中的内容如下:

      [StudentInfo]

3.要将学生的年龄保存下来,只需将整型的值变为字符型即可:

strTemp.Format("%d",nAge);

::WritePrivateProfileString("StudentInfo","Age",strTemp,"c:\\stud\\student.ini");

二.将信息从INI文件中读入程序中的变量.

1.所用的WINAPI函数原型为:

DWORD GetPrivateProfileString(

LPCTSTR lpAppName,

LPCTSTR lpKeyName,

LPCTSTR lpDefault,

LPTSTR lpReturnedString,

DWORD nSize,

LPCTSTR lpFileName

);

其中各参数的意义:

      前二个参数与 WritePrivateProfileString中的意义一样.

      lpDefault : 如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量.

      lpReturnedString : 接收INI文件中的值的CString对象,即目的缓存器.

      nSize : 目的缓存器的大小.

      lpFileName : 是完整的INI文件名.

2.具体使用方法:现要将上一步中写入的学生的信息读入程序中.

CString strStudName;

int nStudAge;

GetPrivateProfileString("StudentInfo","Name","默认姓名",strStudName.GetBuffer(MAX_PATH),MAX_PATH,"c:\\stud\\student.ini");

执行后 strStudName 的值为:"张三",若前两个参数有误,其值为:"默认姓名".

3.读入整型值要用另一个WINAPI函数:

UINT GetPrivateProfileInt(

LPCTSTR lpAppName,

LPCTSTR lpKeyName,

INT nDefault,

LPCTSTR lpFileName

);

这里的参数意义与上相同.使用方法如下:

nStudAge=GetPrivateProfileInt("StudentInfo","Age",10,"c:\\stud\\student.ini");

三.循环写入多个值,设现有一程序,要将最近使用的几个文件名保存下来,具体程序如下:

1.写入:

CString strTemp,strTempA;

int i;

int nCount=6;

file://共有6个文件名需要保存

for(i=0;i {strTemp.Format("%d",i);

strTempA=文件名;

file://文件名可以从数组,列表框等处取得.

::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,

"c:\\usefile\\usefile.ini");

}

strTemp.Format("%d",nCount);

::WritePrivateProfileString("FileCount","Count",strTemp,"c:\\usefile\\usefile.ini");

file://将文件总数写入,以便读出.

2.读出:

nCount=::GetPrivateProfileInt("FileCount","Count",0,"c:\\usefile\\usefile.ini");

for(i=0;i {strTemp.Format("%d",i);

strTemp="FileName"+strTemp;

::GetPrivateProfileString("CurrentIni",strTemp,"default.fil", strTempA.GetBuffer(MAX_PATH),MAX_PATH,"c:\\usefile\\usefile.ini");

file://使用strTempA中的内容.

}

另外要注意以下三点：

1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.

2.文件名的路径中必须为 \\ ,因为在VC++中, \\ 才表示一个 \ .

3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".\\student.ini".


INI文件读写技术及程序自动加载应用剖析

INI文件是Windows系统中一类比较重要的文件，通常用来存放系统或者应用程序的配置信息，以方便系统或者应用程序在初始化时再次读入。比如Windows系统中的配置文件win.ini和system.ini，它们就主要存放系统启动或用户登陆时的系统信息。这项功能在方便了系统配置的同时，也为非法程序的自动运行提供了可乘之机。显然，这类文件的重要性应该引起我们的重视。但是对于这样的ini文件的读写操作却与普通文本文件有着种种的不同，尤其体现在编程实现上。笔者曾经尝试用手动更改的方法在文件中加入一些项，使得自己的程序能够在初始化时自动运行，但是却没有成功，最后还是藉由编程的方法来实现了。这里主要涉及到一些API函数，而这些函数又往往不被人们所熟知，本文的任务就是在介绍这些函数的同时，用简单的程序作了示例，下面我们言归正传。

先来看几个往配置文件中写入信息的函数：

（1）WritePrivateProfileSection()用来在ini文件中直接向指定区域写入键和值的信息，其原型如下：

BOOL WritePrivateProfileSection(

LPCTSTR lpAppName, // 指向指定字段的字符串

LPCTSTR lpString, // 指向要写入的键与值字符串

LPCTSTR lpFileName // 指向文件名称字符串，如果不包含完整路径，则在windows目录下创建

);

用法示例：

WritePrivateProfileSection(_T(“windows”),_T(“load=c:\\winnt\\notepad.exe”),_T(“c:\\winnt\\win.ini”));

（2）WritePrivateProfileString（）与上一个函数的不同点在于其将键和值分开了，原型如下：

BOOL WritePrivateProfileString(

LPCTSTR lpAppName, // 指向指定字段的字符串

LPCTSTR lpKeyName, // 指向指定键的字符串

LPCTSTR lpString, // 指向指定值的字符串

LPCTSTR lpFileName // 指向文件名称字符串

);

用法示例：

WritePrivateProfileString(_T(“windows”),_T(load”)_T(“c:\\winnt\\notepad.exe”),_T(“c:\\winnt\\win.ini”));

（3）WritePrivateProfileStruct()与前面两个的不同在于文件尾有校验和，原型如下：

BOOL WritePrivateProfileStruct(

LPCTSTR lpszSection, //指向指定字段的字符串

LPCTSTR lpszKey, //指向指定键的字符串

LPVOID lpStruct, //指向存放要加入的数据的缓冲区，如果为NULL，则删除键

UINT uSizeStruct, //缓冲区大小，以字节为单位

LPCTSTR szFile //以零结尾的文件名称字符串，如果为空，则向win.ini写入

);

用法示例：

WritePrivateProfileStruct(_T(“windows”),_T(“load”),pBuffer,sizeof(pBuffer),_T(“c:\\winnt\\win.ini”));

(4)还有两个函数，是专门用来向win.ini文件写入的，函数原型如下：

BOOL WriteProfileSection(

LPCTSTR lpAppName, //指向指定字段的字符串

LPCTSTR lpString //指向指定值的字符串

);

BOOL WriteProfileString(

LPCTSTR lpAppName, //指向指定字段的字符串

LPCTSTR lpKeyName, //指向指定键的字符串

LPCTSTR lpString //指向指定值的字符串

);

下面来看几个对应的从ini文件获取信息的API函数，上面已经说得很详细了，这里只说其中两个：

DWORD GetPrivateProfileString(

LPCTSTR lpAppName, //指向指定字段的字符串

LPCTSTR lpKeyName, //指向键的字符串

LPCTSTR lpDefault, //如果INI文件中没有前两个参数指定的字段名或键名，则将此值赋给变量

LPTSTR lpReturnedString, //存放INI文件中值的目的缓存区

DWORD nSize, //目的缓冲区的大小，以字节为单位

LPCTSTR lpFileName //指向INI文件名称的字符串

);

UINT GetPrivateProfileInt(

LPCTSTR lpAppName, //指向指定字段的字符串

LPCTSTR lpKeyName, //指向键的字符串

INT nDefault, //如果INI文件中没有前两个参数指定的字段名或键名，则将此值赋给变量

LPCTSTR lpFileName //指向INI文件名称的字符串

);

程序示例1： 我们在这里建立了一个应用程序“App Name”，并且使用了一个INI文件“appname.ini”，在此INI文件中，我们写入如下内容：

[Section1]

FirstKey = It all worked out okay.

SecondKey = By golly, it works.

ThirdKey = Another test.

代码分析如下：

#include <stdio.h>

#include <windows.h>

//主函数

main()

{

//定义局部

CHAR inBuf[80];

HKEY hKey1, hKey2;

DWORD dwDisposition;

LONG lRetCode;

// 试图创建INI文件的键值

lRetCode = RegCreateKeyEx ( HKEY_LOCAL_MACHINE,

"SOFTWARE\\Microsoft\\Windows NT

\\CurrentVersion\\IniFileMapping\\appname.ini",

0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE,

NULL, &hKey1,

&dwDisposition);

//判断是否出错

if (lRetCode != ERROR_SUCCESS){

printf ("Error in creating appname.ini key\n");

return (0) ;

}

//试图设置一个节区的值

lRetCode = RegSetValueEx ( hKey1,

"Section1",

0,

REG_SZ,

"USR:App Name\\Section1",

20);

//判断是否出错

if (lRetCode != ERROR_SUCCESS) {

printf ( "Error in setting Section1 value\n");

return (0) ;

}

//试图创建一个应用名称键值

lRetCode = RegCreateKeyEx ( HKEY_CURRENT_USER,

"App Name",

0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE,

NULL, &hKey2,

&dwDisposition);

//判断是否出错

if (lRetCode != ERROR_SUCCESS) {

printf ("Error in creating App Name key\n");

return (0) ;

}

//强制系统重新读取映射区的内容到共享内存中，以便于将来对应用程序的调用可//以找到它，而不需要重新启动系统

WritePrivateProfileStringW( NULL, NULL, NULL, L"appname.ini" );

//向INI文件中添加一些键值

WritePrivateProfileString ("Section1", "FirstKey",

"It all worked out okay.", "appname.ini");

WritePrivateProfileString ("Section1", "SecondKey",

"By golly, it works.", "appname.ini");

WritePrivateProfileSection ("Section1", "ThirdKey = Another Test.",

"appname.ini");

//测试一下添加的正确性

GetPrivateProfileString ("Section1", "FirstKey",

"Bogus Value: Get didn't work", inBuf, 80,

"appname.ini");

printf ("%s", inBuf);

return(0);

}

程序示例2：通过修改win.ini中的字段[windows]中的键load或run，或者是为system.ini中的字段[boot]中的键shell增加值，可以达到设置程序自动运行的目的。假设我们要自动运行notepad.exe，修改后的win.ini或system.ini文件象这样就可以：

win.ini

[windows]

load=c:\winnt\notepad.exe

run=c:\winnt\notepad.exe

system.ini

[boot]

shell=c:\winnt\explorer.exe c:\winnt\notepad.exe

注意：system.ini文件的修改要特别注意，如果你单纯改成shell=c:\winnt\notepad.exe，则不能首先运行explorer.exe，很明显你将看不到桌面和任务栏，呵呵，笔者在做实验时就曾因为粗心造成了这样的后果，不过不用害怕，只要你用我们下面提供的程序，将它修改过来就可以了，默认时，系统在system.ini中的[boot]下是shell=c:\winnt\explorer.exe。很多非法程序就是通过修改这两个文件来达到自启动的目的的。

下面这个程序可以在附书光盘中找到，名称为“AutoPlay”，使用VC++6.0写成，核心程序源代码如下：

void CAutoRunDlg::OnBrowse()

{

//只浏览exe文件

CfileDialog fileDlg(TRUE,_T("EXE"),_T("*.exe"),OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,(_T("Executable Files (*.exe) |*.exe ||")));//显示打开文件的对话框

//当操作者选择OK时，程序取得选择文件的全路径名（包括文件的路径及文件名称），并将相应的数值传输给相关的控件变量。

if(fileDlg.DoModal()==IDOK)

{

m_strFileName=fileDlg.GetPathName();

//向将变量中的数值传输给控件显示出来。

UpdateData(FALSE);

}

}

void CAutoRunDlg::OnApply()

{

//更新数据

UpdateData(TRUE);

//写入ini文件

LPCTSTR filename;

filename=m_strFileName;

WritePrivateProfileString(_T("windows"),_T("load"),filename,_T("c:\\winnt\\win.ini"));

}

您如果要更改system.ini，可以将WritePrivateProfileString(_T("windows"),_T("load"),filename,_T("c:\\winnt\\win.ini"));

改为WritePrivateProfileString(_T("boot"),_T("shell"),filename,_T("c:\\winnt\\system.ini"));并且在输入文件名时输入c:\winnt\explorer.exe c:\winnt\notepad.exe。

写到这里，本文的意图基本达到，如果您可以把某些代码亲自实现，相信读者会有比较大的收获。

三、删除键值或节

回顾一下WriteProfileString函数的说明

BOOL WriteProfileString(

LPCTSTR lpAppName, // 节的名字，是一个以0结束的字符串

LPCTSTR lpKeyName, // 键的名字，是一个以0结束的字符串。若为NULL，则删除整个节

LPCTSTR lpString // 键的值，是一个以0结束的字符串。若为NULL，则删除对应的键

)

由此可见，要删除某个节，只需要将WriteProfileString第二个参数设为NULL即可。而要删除某个键，则只需要将该函数的第三个参数设为 NULL即可。这是删除系统的win.ini中的节或键，类似的，要删除自己定义的ini文件中的节或键，也可做相同的操作。

如：

::WriteProfileString("Test",NULL,NULL); //删除win.ini中的Test节

::WriteProfileString("Test","id",NULL); //删除win.ini中的id键

::WritePrivateProfileString("Test",NULL,NULL,".\\ex1.ini"); //删除ex1.ini中的Test节

::WritePrivateProfileString("Test","id",NULL,".\\ex1.ini"); //删除ex1.ini中的id键

四、如何判断一个ini文件中有多少个节

要判断一个ini文件中有多少个节，最简单的办法就是将所有的节名都找出来，然后统计节名的个数。而要将所有的节名找出来，使用GetPrivateProfileSectionNames函数就可以了，其原型如下：

DWORD GetPrivateProfileSectionNames(

LPTSTR lpszReturnBuffer, // 指向一个缓冲区，用来保存返回的所有节名

DWORD nSize, // 参数lpszReturnBuffer的大小

LPCTSTR lpFileName // 文件名，若该ini文件与程序在同一个目录下，

//也可使用相对路径,否则需要给出绝度路径

)

下面的是用来统计一个ini文件中共有多少个节的函数，当然，如果需要同时找到每个节中的各个键及其值，根据找到节名就可以很容易的得到了。

int CTestDlg::CalcCount(void)

{

TCHAR chSectionNames[2048]={0}; //所有节名组成的字符数组

char * pSectionName; //保存找到的某个节名字符串的首地址

int i; //i指向数组chSectionNames的某个位置，从0开始，顺序后移

int j=0; //j用来保存下一个节名字符串的首地址相对于当前i的位置偏移量

int count=0; //统计节的个数

//CString name;

//char id[20];

::GetPrivateProfileSectionNames(chSectionNames,2048,".\\ex1.ini");

for(i=0;i<2048;i++,j++)

{

if(chSectionNames[0]=='\0')

break; //如果第一个字符就是0，则说明ini中一个节也没有

if(chSectionNames[i]=='\0')

{

pSectionName=&chSectionNames[i-j]; //找到一个0，则说明从这个字符往前，减掉j个偏移量，

//就是一个节名的首地址

j=-1; //找到一个节名后，j的值要还原，以统计下一个节名地址的偏移量

//赋成-1是因为节名字符串的最后一个字符0是终止符，不能作为节名

//的一部分

//在获取节名的时候可以获取该节中键的值，前提是我们知道该节中有哪些键。

AfxMessageBox(pSectionName); //把找到的显示出来

if(chSectionNames[i+1]==0)

{

break; //当两个相邻的字符都是0时，则所有的节名都已找到，循环终止

}

}

}

return count;

}

  补充四点:

      1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.

      2.文件名的路径中必须为 \\ ,因为在VC++中, \\ 才表示一个 \ .

      3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".\\student.ini".


在Turbo C语言中, 每个变量在使用之前必须定义其数据类型。
Turbo C 有以下几种类型:
整型(int)、浮点型(float)、字符型(char)、指针型(*)、无值型(void)以及结构(struct)和联合(union)。
其中前五种是Turbo C的基本数据类型、后两种数据类型(结构和联合)
1) 整型(int)
一、整型数说明加上不同的修饰符, 整型数有以下几种类型;
signed short int 有符号短整型数说明。简写为short或int, 字长为2字节共16位二进制数, 数的范围是-32768~32767。
signed long int 有符号长整型数说明。简写为long, 字长为4字节共32位二进制数, 数的范围是-2147483648~2147483647。
unsigned short int 无符号短整型数说明。简写为unsigned int, 字长为2字节共16位二进制数, 数的范围是0~65535。
unsigned long int 无符号长整型数说明。简写为unsigned long, 字长为4字节共32位二进制数, 数的范围是0~4294967295。
二、整型变量定义
可以用下列语句定义整型变量 int a, b; /*a、b被定义为有符号短整型变量*/
unsigned long c; /*c被定义为无符号长整型变量*/
三、整型常数表示
按不同的进制区分, 整型常数有三种表示方法:
十进制数:以非0开始的数 如:220, -560, 45900
八进制数: 以0开始的数 如:06; 0106, 05788
十六进制数:以0X或0x开始的数 如:0X0D, 0XFF, 0x4e
另外, 可在整型常数后添加一个"L"或"l"字母表示该数为长整型数, 如22L,0773L, 0Xae4l。 2) 浮点型(float)
一、浮点数说明 Turbo C中有以下两种类型的浮点数: float 单浮点数。
字长为4个字节共32位二进制数,数的范围是3.4x10-38E~3.4x10+38E。
double 双浮点数。字长为8个字节共64位二进制数,数的范围是1.7x10-308E~1.7x10+308E。说明: 浮点数均为有符号浮点数, 没有无符号浮点数。
二、浮点型变量定义
可以用下列语句定义浮点型变量:float a, f;/*a, f被定义为单浮点型变量*/
double b; /*b被定义为双浮点型变量*/
三、浮点常数表示
例如: +29.56, -56.33, -6.8e-18, 6.365
说明:
1. 浮点常数只有一种进制(十进制)。
2. 所有浮点常数都被默认为double。
3. 绝对值小于1的浮点数, 其小数点前面的零可以省略。
如:0.22可写为.22,-0.0015E-3可写为-.0015E-3。
4. Turbo C默认格式输出浮点数时, 最多只保留小数点后六位。
3)字符型(char)
加上不同的修饰符,可以定义有符号和无符号两种类型的字符型变量,
例如: char a: /*a被定义为有符号字符变量*/
unsigned char l; /*l被定义为无符号字符变量*/
字符在计算机中以其ASCII码方式表示, 其长度为1个字节, 有符号字符型数取值范围为-128~127, 无符号字符型数到值范围是0~255。因此在Turbo C语言中,字符型数据在操作时将按整型数处理, 如果某个变量定义成char,则表明该变量是有符号的, 即它将转换成有符号的整型数。
一、Turbo C中规定对ASCII码值大于0x80的字符将被认为是负数。
例如ASCII值为0x8c的字符,定义成char时,被转换成十六进制的整数0xff8c.这是因当ASCII码值大于0x80时,该字节的最高位为1,计算机会认为该数为负数, 对于0x8c表示的数实际上是-74(8c的各位取反再加1),而-74 转换成两字节整型数并在计算机中表示时就是0xff8c( 对0074 各位取反再加1)。因此只有定义为unsigned char 0x8c转换成整型数时才是8c。这一点在处理大于0x80的ASCII码字符时(例如汉字码)要特别注意。一般汉字均定义为unsigned char(在以后的程序中会经常碰到)。
另外, 也可以定义一个字符型数组(关于数组后面再作详细介绍),此时该数组表示一个字符串.
例如: char str[10];
计算机在编译时, 将留出连续10个字符的空间, 即str[0]到str[9]共10个变量, 但只有前9个供用户使用。
第10个str[9]用来存放字符串终止符NULL即"\0",但终止符是编编译程序自动加上的, 这一点应特别注意。
二、字符常数表示
能用符号表示的字符可直接用单引号括起来表示, 如a, 9, Z, 也可用该字符的ASCII码值表示,
例如:
十进制数85表示大写字母U,
十六进制数0x5d表示],
八进制数0102表示大写字母B。
一些不能用符号表示的控制符, 只能用ASCII码值来表示, 如十进制数10 表示换行, 十六进制数0x0d表示回车, 八进制数033表示Esc。
Turbo C2.0中也有另外一种表示表示方法, 如\033表示Esc, 这里\ 0 符号后面的数字表示十六进制的ASCII值当然这种表示方法也适用于可睦接用符号表示的字符。 另外, Turbo C2.0中有些常用的字符用以下特殊规定来表示: 规定符等价于含义\f \X0C换页\r \X0D回车\t \X09制表键 \n \X0A 换行 \\ \X5C \符 \ \X27 符 \" \X22 "符 对于字符串常量, 一般用双引号括起来表示, 如"Hello Turbo C2.0"。
4) 指针型(*)
指针是一种特殊的数据类型, 在其它语言中一般没有。
指针是指向变量的地址,实质上指针就是存贮单元的地址。
根据所指的变量类型不同, 可以是整型指针(int *)、浮点型指针(float *)、字符型指针(char *)、结构指针(struct *)和联合指针(union *)。 5) 无值型(void) 无值型字节长度为0, 主要有两个用途: 一是明确地表示一个函数不返回任何值; 一是产生一个同一类型指针(可根据需要动态分配给其内存)。 例如: void *buffer; /*buffer被定义为无值型指针*/。


熟悉C的程序员都知道union（联合体）的用法，利用union可以用相同的存储空间存储不同型别的数据类型，从而节省内存空间

熟悉C的程序员都知道union（联合体）的用法，利用union可以用相同的存储空间存储不同型别的数据类型，从而节省内存空间。当访问其内成员时可用"."和"->"来直接访问。在C++出现后，它继承了union并保留了其在C中的特性。但是在C++中的union又有了新的扩展，这需要大家了解，要不然你会感到费解和迷惑。下面我讲两点。

    一、在union中存储对象

    在C中union中可以存储任意类型的内置数据类型，那么在C++中union是否可以存储对象呢？还是让我们看一个例子吧，这比任何言语都能说明问题，不是吗？

#pragma warning(disable : 4786)

#include

using namespace std;

class TestUnion

{

  public:

  TestUnion(long l):data_(l)

  {

    };

  int data_;

};

typedef union _tagUtype_

{

  TestUnion obj;

}UT;

int main (void)

{

  return 0;

}

    这样不行，union中不可以存储TestUnion类的对象，但在C中union可以存储struct呀，为什么不能存储类的对象呢？很简单，请问，在C中union可以存储带有构造函数的struct吗？对了，在C中的struct是没有构造函数的。所以如果C++中union可以存储有构造函数的类的对象就不太符合逻辑，那不是说C++和C完全兼容吗？不错，正因为这一点，C++中union不可以存储有构造函数的类的对象，但是可以存储不带构造函数的类的对象，这样就和C保持一致了，不想信你试试。对TestUnion类的声明进行如下修改：

class TestUnion

{

  public:

  int data_;

};

    再进行编译，一切OK！。但是这样却失去了C++的构造初始化特性，这样做是没有任何意义的，我只是在说其在C++中的语义，并不是推荐大家使用（绝对不推荐）。但是我们可以在union中存储对象的指针，从而引用不同的对象类型。不用我再多说了吧，大家还是试试吧！

二、类中union的初始化

    由于union的共享内存特点，我们可以使我们的类存储不同的型别而不浪费内存空间，在类中我们可以声明一个union存储不同型别的指针，示例如下：

#pragma warning(disable : 4786)

#include

using namespace std;

class TestUnion

{

enum StoreType{Long,Const_CharP};

union

{

const char* ch_;

long l_;

} data_;

StoreType stype_;

TestUnion(TestUnion&);

TestUnion& operator=(const TestUnion&);

public:

TestUnion(const char* ch);

TestUnion(long l);

operator const char*() const {return data_.ch_;}

operator long() const {return data_.l_;}

};

TestUnion::TestUnion(const char* ch):data_.ch_(ch),stype_(Const_CharP)

{

}

TestUnion::TestUnion(long l):data_.l_(l),stype_(Long)

{

}

int main (void)

{

TestUnion pszobj("yuankai");

TestUnion lobj(1234);

cout<(pszobj)<

cout<<

return 0;

}

是不幸，编译都通不过，好象没有什么问题呀，为什么呢？data_.ch_(ch)和data_.l_(l)有问题吗？如果你问一个C程序员他会告诉你，绝对没问题。你不会去怀疑编译器有问题吧！不好意思！我一开始就是这么想的，真是惭愧。费解，迷惑。让我们来看看构造TestUnion对象时发生了什么，这样你就会明白了。当创建TestUnion对象时，自然要调用其相应的构造函数，在构造函数中当然要调用其成员的构造函数，所以其要去调用union成员的构造函数，但是其为匿名的，有没有构造函数可调用，所以出错。很明显在C++中union和class一样它可以有构造函数，不能如此直接引用其成员。struct同样有这限制。只要我们给其定义一个构造函数什么问题都解决了。示例如下：

class TestUnion

{

enum StoreType{Long,Const_CharP};

union DataUnion //不能匿名

{

DataUnion(const char*); //声明const char*构造函数

DataUnion(long); //声明long构造函数

const char* ch_;

long l_;

} data_;

StoreType stype_;

TestUnion(TestUnion&);

TestUnion& operator=(const TestUnion&);

public:

TestUnion(const char* ch);

TestUnion(long l);

operator const char*() const {return data_.ch_;}

operator long() const {return data_.l_;}

};

TestUnion::TestUnion(const char* ch):data_(ch),stype_(Const_CharP)

{//注意data_(ch)，这里直接引用data_

}

TestUnion::TestUnion(long l):data_(l),stype_(Long)

{//注意data_(l)，这里直接引用data_

}

TestUnion::DataUnion::DataUnion(const char* ch):ch_(ch)

{

}

TestUnion::DataUnion::DataUnion(long l):l_(l)

{

}

    现在再编译，如果还不行，你怀疑编译器有问题是有理由的。好了就写这么多吧！希望对大家有帮助，我可是花了一个下午的时间呀！如果有什么错误，希望来信指出，很希望和大家共同探讨C++，共同进步（ccplusplus@21cn.com）。

举个例子来说明一吧，为了让大家更明白一点，比如一个铅笔盒中有一支笔，但在没有打开之前你并不知道它是什么笔，可能是铅笔也可能是钢笔，这里有两种可能，那么你就可以定义一个枚举类型来表示它！

enum box{pencil,pen};//这里你就定义了一个枚举类型的变量叫box，这个枚举变量内含有两个元素也称枚举元素在这里是pencil和pen，分别表示铅笔和钢笔。

    这里要说一下，如果你想定义两个具有同样特性枚举类型的变量那么你可以用如下的两种方式进行定义！

enum box{pencil,pen};

enum box box2;//或者简写成box box2;

    再有一种就是在声明的时候同时定义。

enum {pencil,pen}box,box2; //在声明的同时进行定义！

    枚举变量中的枚举元素系统是按照常量来处理的，故叫枚举常量，他们是不能进行普通的算术赋值的，(pencil=1;)这样的写发是错误的，但是你可以在声明的时候进行赋值操作！

enum box{pencil=1,pen=2};

但是这里要特别注意的一点是，如果你不进行元素赋值操作那么元素将会被系统自动从0开始自动递增的进行赋值操作，说到自动赋值，如果你只定义了第一个那么系统将对下一个元素进行前一个元素的值加1操作，例如

enum box{pencil=3,pen};//这里pen就是4系统将自动进行pen=4的定义赋值操作！

    前面说了那么多，下面给出一个完整的例子大家可以通过以下的代码的学习进行更完整的学习！

#include <iostream>

using namespace std;

void main(void)

{

enum egg {a,b,c};

enum egg test; //在这里你可以简写成egg test;

test = c; //对枚举变量test进行赋予元素操作，这里之所以叫赋元素操作不叫赋值操作就是为了让大家明白枚举变量是不能直接赋予算数值的，例如(test=1;)这样的操作都是不被编译器所接受的，正确的方式是先进行强制类型转换例如(test = (enum egg) 0;)！

if (test==c)

{

cout <<"枚举变量判断:test枚举对应的枚举元素是c" << endl;

}

if (test==2)

{

cout <<"枚举变量判断:test枚举元素的值是2" << endl;

}

cout << a << "|" << b << "|" << test <<endl;

test = (enum egg) 0; //强制类型转换

cout << "枚举变量test值改变为:" << test <<endl;

cin.get();

}

    看到这里要最后说一个问题，就是枚举变量中的枚举元素(或者叫枚举常量)在特殊情况下是会被自动提升为算术类型的！

#include <iostream>

using namespace std;

void main(void)

{

enum test {a,b};

int c=1+b; //自动提升为算术类型

cout << c <<endl;

cin.get();

}


使用union必懂

联合(union)在C/C++里面见得并不多，但是在一些对内存要求特别严格的地方，联合又是频繁出现，那么究竟什么是联合、怎么去用、有什么需要注意的地方呢？就这些问题，我试着做一些简单的回答，里面肯定还有不当的地方，欢迎指出！

1、什么是联合？
“联合”是一种特殊的类，也是一种构造类型的数据结构。在一个“联合”内可以定义多种不同的数据类型，一个被说明为该“联合”类型的变量中，允许装入该“联合”所定义的任何一种数据，这些数据共享同一段内存，已达到节省空间的目的（还有一个节省空间的类型：位域）。这是一个非常特殊的地方，也是联合的特征。另外，同struct一样，联合默认访问权限也是公有的，并且，也具有成员函数。

2、联合与结构的区别？
“联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间，一个结构变量的总长度是各成员长度之和（空结构除外，同时不考虑边界调整）。而在“联合”中，各成员共享一段内存空间，一个联合变量的长度等于各成员中最长的长度。应该说明的是，这里所谓的共享不是指把多个成员同时装入一个联合变量内，而是指该联合变量可被赋予任一成员值，但每次只能赋一种值，赋入新值则冲去旧值。

3、如何定义？
例如：
union test
{
test() { }
int office;
char teacher[5];
};
定义了一个名为test的联合类型，它含有两个成员，一个为整型，成员名为office；另一个为字符数组，数组名为teacher。联合定义之后，即可进行联合变量说明，被说明为test类型的变量，可以存放整型量office或存放字符数组teacher。

4、如何说明？
联合变量的说明有三种形式：先定义再说明、定义同时说明和直接说明。
以test类型为例，说明如下：
1) union test
{
int office;
char teacher[5];
};
union test a,b; /*说明a,b为test类型*/
2) union test
{
int office;
char teacher[5];
} a,b;
3) union
{
int office;
char teacher[5];
} a,b;
经说明后的a,b变量均为test类型。
a,b变量的长度应等于test的成员中最长的长度，即等于teacher数组的长度，共5个字节。a,b变量如赋予整型值时，只使用了4个字节，而赋予字符数组时，可用5个字节。

5、如何使用？
对联合变量的赋值，使用都只能是对变量的成员进行。
联合变量的成员表示为：联合变量名.成员名
例如，a被说明为test类型的变量之后，可使用a.class、a.office
不允许只用联合变量名作赋值或其它操作，也不允许对联合变量作初始化赋值，赋值只能在程序中进行。
还要再强调说明的是，一个联合变量，每次只能赋予一个成员值。换句话说，一个联合变量的值就是联合变员的某一个成员值。

6、匿名联合
匿名联合仅仅通知编译器它的成员变量共同享一个地址,而变量本身是直接引用的,不使用通常的点号运算符语法.例如：
#include <iostream>
void main()
{
union{
int test;
char c;
};
test=5;
c='a';
std::cout<<i<<" "<<c;
}
正如所见到的,联合成分象声明的普通局部变量那样被引用,事实上对于程序而言,这也正是使用这些变量的方式.另外,尽管被定义在一个联合声明中,他们与同一个程序快那的任何其他局部变量具有相同的作用域级别.这意味这匿名联合内的成员的名称不能与同一个作用域内的其他一直标志符冲突.
对匿名联合还存在如下限制:
因为匿名联合不使用点运算符，所以包含在匿名联合内的元素必须是数据,不允许有成员函数，也不能包含私有或受保护的成员。还有,全局匿名联合必须是静态(static)的，否则就必须放在匿名名字空间中。

7、几点需要讨论的地方：
1、联合里面那些东西不能存放？
我们知道，联合里面的东西共享内存，所以静态、引用都不能用，因为他们不可能共享内存。
2、类可以放入联合吗？
我们先看一个例子：
class Test
{
public:
Test():data(0) { }
private:
int data;
};

typedef union _test
{
Test test;
}UI;
编译通不过，为什么呢？
因为联合里不允许存放带有构造函数、析够函数、复制拷贝操作符等的类，因为他们共享内存，编译器无法保证这些对象不被破坏，也无法保证离开时调用析够函数。
3、又是匿名惹的祸？？
我们先看下一段代码：
class test
{
public:
test(const char* p);
test(int in);
const operator char*() const {return data.ch;}
operator long() const {return data.l;}
private:
enum type {Int, String };
union
{
const char* ch;
int i;
}datatype;
type stype;
test(test&);
test& operator=(const test&);
};
test::test(const char *p):stype(String),datatype.ch(p) { }
test::test(int in):stype(Int),datatype.l(i) { }
看出什么问题了吗？呵呵，编译通不过。为什么呢？难道datatype.ch(p)和datatype.l(i)有问题吗？
哈哈，问题在哪呢？让我们来看看构造test对象时发生了什么，当创建test对象时，自然要调用其相应的构造函数，在构造函数中当然要调用其成员的构造函数，所以其要去调用datatype成员的构造函数，但是他没有构造函数可调用，所以出错。
注意了，这里可并不是匿名联合！因为它后面紧跟了个data!
4、如何有效的防止访问出错？
使用联合可以节省内存空间，但是也有一定的风险：通过一个不适当的数据成员获取当前对象的值！例如上面的ch、i交错访问。
为了防止这样的错误，我们必须定义一个额外的对象，来跟踪当前被存储在联合中的值得类型，我们称这个额外的对象为：union的判别式。
一个比较好的经验是，在处理作为类成员的union对象时，为所有union数据类型提供一组访问函数。


VC中操作INI文件的api

VC中操作INI文件的函数主要有：

函数名 | 功能 | 备注
GetPrivateProfileInt | 读取INI文件指定块中的键名对应的整数值。 |
GetPrivateProfileSection | 记取INI文件指定块中的所有键名及其对应值。 |
GetPrivateProfileSectionNames | 读取一INI文件中所有的块名。 |
GetPrivateProfileString | 读取INI文件指定块中的键名对应的字符串。 |
GetPrivateProfileStruct | 读取INI文件指定块中的键名对应的数据 |
GetProfileInt | 读取win.ini中指定块中的键名对应的整数值。 |
GetProfileSection | 读取win.ini中指定块中所有的键名及其值。 |
GetProfileString | 读取win.ini中指定块中的键名的对应值。 |
WritePrivateProfileSection | 替换INI文件中指定块中所有键名对应的值。 |
WritePrivateProfileString | 把给定的键名及其值写入到指定INI文件的相应块中。 |
WritePrivateProfileStruct | 把指定的键名及其数据写入到指定INI文件的块中。 |
WriteProfileSection | 替换win.ini中指定块的所有键名对应的值。 |
WriteProfileString | 将给定的键名及值写入win.ini中对应的块中。 |

INI文件可用来保存，共享应用程序中少量的数据，其格式一般为：

[块名1]
键名=值
...
[块名n]
键名=值

一般一个INI文件可有N个块，第块可有n个键名及值对应。每个键名及其值占一行。
一般键的名称可任取，不过建议用有意义的字符及词构成。值一般可为整数和字符串，其它类型要进行转换。

GetProfileInt - 从 Win.ini 文件的某个 Section 取得一个 key 的整数值，它的原形是：

GetProfileInt(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
INT nDefault // 如果 Key 值没有找到，则返回缺省的值是多少
);

如果 Key 值没有找到的话，返回值是 nDefault 指定的缺省值，如果 Key 中的值是负数，则返回 0，如果 Key 指定的是数字和字符串的混合，则返回数字部分的值，比如说 x=1234abcd，则返回 1234

GetProfileString - 从 Win.ini 文件的某个 Section 取得一个 key 的字符串，它的原形是：

GetProfileString(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
LPCTSTR lpDefault, // 如果 Key 值没有找到，则返回缺省的字符串的地址
LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
DWORD nSize // 缓冲区的长度
);

返回的字符串在缓冲区内，返回的 eax 值是返回的字符串的长度（不包括尾部的0）

GetProfileSection - 从 Win.ini 文件中读出整个 Section 的内容，它的原形是：

GetProfileSection(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPTSTR lpReturnedString, // 返回数据的缓冲区地址
DWORD nSize // 返回数据的缓冲区长度
);

WriteProfileSection - 将一个整个 Section 的值 写入 Win.ini 文件的指定 Section 中，它的原形是：

WriteProfileSection(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpString // 要写入的数据的地址
);

如果 Win.ini 没有指定的 Section，API 会新建立一个并写入数据，如果已经存在，则先删除原来 Seciton 中所有的 Key 值然后写入新的。

WriteProfileString - 将一个 Key 值写入 Win.ini 文件的指定 Section 中，它的原形是：

WriteProfileString(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
LPCTSTR lpString // 要写的字符串地址
);

如果 Win.ini 没有指定的 Section，API 会新建 Section，如果没有指定的 Key 则新建一个 Key 并写入数据，如果已经存在，则用字符串代替原来的值。
以上的 Api 是对 Win.ini 操作的，当然对于我们来说，用的更多的是在程序运行的目录中建立自己的 ini 文件，如果需要对自己的 ini 文件操作，就要用到另一组 Api，这一组 api 和上面的很象，只要把上面一组的 Profile 换成 PrivateProfile（私有的）就可以了，参数中也相应的多了一个 ini 文件名的参数。例如 GetPrivateProfileInt、GetPrivateProfileSection、WritePrivateProfileString等等， 下面分别介绍：

GetPrivateProfileInt - 从 ini 文件的某个 Section 取得一个 key 的整数值，它的原形是：

GetPrivateProfileInt(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
INT nDefault // 如果 Key 值没有找到，则返回缺省的值是多少
LPCTSTR lpFileName // ini 文件的文件名
);

中间参数和返回值的定义和 GetProfileInt 是一样的。

GetPrivateProfileString - 从 ini 文件的某个 Section 取得一个 key 的字符串，它的原形是：

GetPrivateProfileString(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
LPCTSTR lpDefault, // 如果 Key 值没有找到，则返回缺省的字符串的地址
LPTSTR lpReturnedString, // 返回字符串的缓冲区地址
DWORD nSize // 缓冲区的长度
LPCTSTR lpFileName // ini 文件的文件名
);

GetPrivateProfileSection - 从 ini 文件中读出整个 Section 的内容，它的原形是：

GetPrivateProfileSection(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPTSTR lpReturnedString, // 返回数据的缓冲区地址
DWORD nSize // 返回数据的缓冲区长度
LPCTSTR lpFileName // ini 文件的文件名
);

这个 api 可以读出整个 section 的内容，当你不知道 section 中有哪些 key 的时候，可以使用这个 api 将整个 section 读出后再处理。

说明：

例ini文件为：

[student]

Name=tian

Age=20

Sex=man

由GetPrivateProfileSection（）函数第二个参数lpReturnedString返回的字符串如下：

Name=tian"0Age=20"0Sex=man"0"0（每条以"0分割，最后以两个"0结束）

而我们是想等到每个key及其对应的值，那怎么分割上面那个字符串呢？在VB有中个叫Split()函数，用起来很方便，但是vc里面没有。

当然，你也可以在VC中去实现一个Split()函数，不过我在这里提供一个简单的方法：

CString strKey;

while(*str!='"0') //str为 lpReturnedString 返回的字符串

{

strKey = str;

MessageBox(strKey); //strKey为每个key及其对应的值

str += strKey.GetLength()+1;

}

GetPrivateProfileSectionNames - 从 ini 文件中获得 Section 的名称，它的原形是：

GetPrivateProfileSectionNames(
LPTSTR lpszReturnBuffer, // 返回数据的缓冲区地址
DWORD nSize // 返回数据的缓冲区长度
LPCTSTR lpFileName // ini 文件的文件名
);

如果 ini 中有两个 Section: [sec1] 和 [sec2]，则返回的是 'sec1',0,'sec2',0,0 ，当你不知道 ini 中有哪些 section 的时候可以用这个 api 来获取名称

WritePrivateProfileSection - 将一个整个 Section 的内容入 ini 文件的指定 Section 中，它的原形是：

WritePrivateProfileSection(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpString // 要写入的数据的地址
LPCTSTR lpFileName // ini 文件的文件名
);

WritePrivateProfileString - 将一个 Key 值写入 ini 文件的指定 Section 中，它的原形是：

WritePrivateProfileString(
LPCTSTR lpAppName, // 指向包含 Section 名称的字符串地址
LPCTSTR lpKeyName, // 指向包含 Key 名称的字符串地址
LPCTSTR lpString // 要写的字符串地址
LPCTSTR lpFileName // ini 文件的文件名
);

如果 ini 中没有指定的 Section，API 会新建 Section，如果没有指定的 Key 则新建一个 Key 并写入数据，如果已经存在，则用字符串代替原来的值。当指定的 ini 也不存在的时候，API 会自动建立一个新的文件，所以使用 ini 的好处是我们不必为了保存少量的数据涉及到文件操作，就连查找文件是否存在的操作都不必要。

使用要点：

在我们实际使用的时候，用的最多的是 GetPrivateProfileString 和 WritePrivateProfileString，但在对自定义 ini 文件操作的时候要注意的是，如果 lpFileName 指定的文件没有路径的话，Api会去 Windows 的安装目录去找而不会在当前目录找，但是每次用到 ini 函数要获取当前路径显然太麻烦了，这里有一个变通的办法，你只要在 ini 文件名前面加上 ." 就可以了，比如说要对本目录下的 user.ini 操作，那么文件名就是 '."user.ini' 这样显然比较方便。另外，当你要把一个 Key 清除的时候，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileString。当你要把一个 section 的全部内容清空的时候，也不必把 key 一个个的清除，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileSection。

--------------------------------

在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入INI文件中,程序初始化时再读入.具体应用如下:

    一.将信息写入.INI文件中.

    1.所用的WINAPI函数原型为:

BOOL WritePrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpString,
LPCTSTR lpFileName
);　　其中各参数的意义:

      LPCTSTR lpAppName 是INI文件中的一个字段名.

      LPCTSTR lpKeyName 是lpAppName下的一个键名,通俗讲就是变量名.

      LPCTSTR lpString 是键值,也就是变量的值,不过必须为LPCTSTR型或CString型的.

      LPCTSTR lpFileName 是完整的INI文件名.

    2.具体使用方法:设现有一名学生,需把他的姓名和年龄写入 c:"stud"student.ini 文件中.

CString strName,strTemp;
int nAge;
strName="张三";
nAge=12;
::WritePrivateProfileString("StudentInfo","Name",strName,
"c:""stud""student.ini");　　此时c:"stud"student.ini文件中的内容如下:

      [StudentInfo]

    3.要将学生的年龄保存下来,只需将整型的值变为字符型即可:

strTemp.Format("%d",nAge);
::WritePrivateProfileString("StudentInfo","Age",strTemp,
"c:""stud""student.ini");二.将信息从INI文件中读入程序中的变量.

    1.所用的WINAPI函数原型为:

DWORD GetPrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpDefault,
LPTSTR lpReturnedString,
DWORD nSize,
LPCTSTR lpFileName
);　　其中各参数的意义:

      前二个参数与 WritePrivateProfileString中的意义一样.

      lpDefault : 如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量.

      lpReturnedString : 接收INI文件中的值的CString对象,即目的缓存器.

      nSize : 目的缓存器的大小.

      lpFileName : 是完整的INI文件名.

    2.具体使用方法:现要将上一步中写入的学生的信息读入程序中.

CString strStudName;
int nStudAge;
GetPrivateProfileString("StudentInfo","Name","默认姓名",
strStudName.GetBuffer(MAX_PATH),MAX_PATH,"c:""stud""student.ini");　　执行后 strStudName 的值为:”张三”,若前两个参数有误,其值为:”默认姓名”.

    3.读入整型值要用另一个WINAPI函数:

UINT GetPrivateProfileInt(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
INT nDefault,
LPCTSTR lpFileName
);　　这里的参数意义与上相同.使用方法如下:

nStudAge=GetPrivateProfileInt("StudentInfo","Age",10,
"c:""stud""student.ini");三.循环写入多个值,设现有一程序,要将最近使用的几个文件名保存下来,具体程序如下:

    1.写入:

CString strTemp,strTempA;
int i;
int nCount=6;
file://共有6个文件名需要保存
for(i=0;i {strTemp.Format("%d",i);
strTempA=文件名;
file://文件名可以从数组,列表框等处取得.
::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,
"c:""usefile""usefile.ini");
}
strTemp.Format("%d",nCount);
::WritePrivateProfileString("FileCount","Count",strTemp,
"c:""usefile""usefile.ini");
file://将文件总数写入,以便读出.　　2.读出:

nCount=::GetPrivateProfileInt("FileCount","Count",0,
"c:""usefile""usefile.ini");
for(i=0;i {strTemp.Format("%d",i);
strTemp="FileName"+strTemp;
::GetPrivateProfileString("CurrentIni",strTemp,
"default.fil", strTempA.GetBuffer(MAX_PATH),MAX_PATH,
"c:""usefile""usefile.ini");
file://使用strTempA中的内容.
}
如果 ini 中没有指定的 Section，API 会新建 Section，如果没有指定的 Key 则新建一个 Key 并写入数据，如果已经存在，则用字符串代替原来的值。当指定的 ini 也不存在的时候，API 会自动建立一个新的文件，所以使用 ini 的好处是我们不必为了保存少量的数据涉及到文件操作，就连查找文件是否存在的操作都不必要。
使用要点：
在我们实际使用的时候，用的最多的是 GetPrivateProfileString 和 WritePrivateProfileString，但在对自定义 ini 文件操作的时候要注意的是，如果 lpFileName 指定的文件没有路径的话，Api会去 Windows 的安装目录去找而不会在当前目录找，但是每次用到 ini 函数要获取当前路径显然太麻烦了，这里有一个变通的办法，你只要在 ini 文件名前面加上 .\ 就可以了，比如说要对本目录下的 user.ini 操作，那么文件名就是 '.\user.ini' 这样显然比较方便。另外，当你要把一个 Key 清除的时候，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileString。当你要把一个 sectio的全部内容清空的时候，也不必把 key 一个个的清除，可以使用把 lpString 指向一个空的字符串然后使用 WritePrivateProfileSection。

1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.

2.文件名的路径中必须为 "" ,因为在VC++中, "" 才表示一个 " .

3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: “.""student.ini”.


在C++中的详细说明

    vector是C++标准模板库中的部分内容，它是一个多功能的，能够操作多种数据结构和算法的模板类和函数库。vector之所以被认为是一个容器，是因为它能够像容器一样存放各种类型的对象，简单地说，vector是一个能够存放任意类型的动态数组，能够增加和压缩数据。

    为了可以使用vector，必须在你的头文件中包含下面的代码：

    #include <vector>

    vector属于std命名域的，因此需要通过命名限定，如下完成你的代码：

    using std::vector;

    vector<int> vInts;

    或者连在一起，使用全名：

    std::vector<int> vInts;

    建议使用全局的命名域方式：using namespace std;

    函数

    表述

    c.assign(beg,end)c.assign(n,elem)

    将[beg; end)区间中的数据赋值给c。将n个elem的拷贝赋值给c。

    c.at(idx)

    传回索引idx所指的数据，如果idx越界，抛出out_of_range。

    c.back()

    传回最后一个数据，不检查这个数据是否存在。

    c.begin()

    传回迭代器中的第一个数据地址。

    c.capacity()

    返回容器中数据个数。

    c.clear()

    移除容器中所有数据。

    c.empty()

    判断容器是否为空。

    c.end()

    指向迭代器中末端元素的下一个，指向一个不存在元素。

    c.erase(pos)

    c.erase(beg,end)

    删除pos位置的数据，传回下一个数据的位置。

    删除[beg,end)区间的数据，传回下一个数据的位置。

    c.front()

    传回第一个数据。

    get_allocator

    使用构造函数返回一个拷贝。

    c.insert(pos,elem)

    c.insert(pos,n,elem)

    c.insert(pos,beg,end)

    在pos位置插入一个elem拷贝，传回新数据位置。在pos位置插入n个elem数据。无返回值。在pos位置插入在[beg,end)区间的数据。无返回值。

    c.max_size()

    返回容器中最大数据的数量。

    c.pop_back()

    删除最后一个数据。

    c.push_back(elem)

    在尾部加入一个数据。

    c.rbegin()

    传回一个逆向队列的第一个数据。

    c.rend()

    传回一个逆向队列的最后一个数据的下一个位置。

    c.resize(num)

    重新指定队列的长度。

    c.reserve()

    保留适当的容量。

    c.size()

    返回容器中实际数据的个数。

    c1.swap(c2)

    swap(c1,c2)

    将c1和c2元素互换。同上操作。

    vector<Elem>

    cvector<Elem> c1(c2)

    vector <Elem> c(n)

    ector <Elem> c(n, elem)

    vector <Elem> c(beg,end)

    c.~ vector <Elem>()

    创建一个空的vector。复制一个vector。创建一个vector，含有n个数据，数据均已缺省构造产生。创建一个含有n个elem拷贝的vector。创建一个以[beg;end)区间的vector。销毁所有数据，释放内存。

    operator[]

    返回容器中指定位置的一个引用。

    创建一个vector

    vector容器提供了多种创建方法，下面介绍几种常用的。

    创建一个Widget类型的空的vector对象：

    vector<Widget> vWidgets;

    创建一个包含500个Widget类型数据的vector：

    vector<Widget> vWidgets(500);

    创建一个包含500个Widget类型数据的vector，并且都初始化为0：

    vector<Widget> vWidgets(500, Widget(0));

    创建一个Widget的拷贝：

    vector<Widget> vWidgetsFromAnother(vWidgets);

    向vector添加一个数据

    vector添加数据的缺省方法是push_back()。push_back()函数表示将数据添加到vector的尾部，并按需要来分配内存。例如：向vector<Widget>中添加10个数据，需要如下编写代码：

    for(int i= 0;i<10; i++) {

    vWidgets.push_back(Widget(i));

    }

    获取vector中制定位置的数据

    vector里面的数据是动态分配的，使用push_back()的一系列分配空间常常决定于文件或一些数据源。如果想知道vector存放了多少数据，可以使用empty()。获取vector的大小，可以使用size()。例如，如果想获取一个vector v的大小，但不知道它是否为空，或者已经包含了数据，如果为空想设置为-1，你可以使用下面的代码实现：

    int nSize = v.empty() ? -1 : static_cast<int>(v.size());

    访问vector中的数据

    使用两种方法来访问vector。

    1、 vector::at()

    2、 vector::operator[]

    operator[]主要是为了与C语言进行兼容。它可以像C语言数组一样操作。但at()是我们的首选，因为at()进行了边界检查，如果访问超过了vector的范围，将抛出一个例外。由于operator[]容易造成一些错误，所有我们很少用它，下面进行验证一下：

    分析下面的代码：

    vector<int> v;

    v.reserve(10);

    for(int i=0; i<7; i++) {

    v.push_back(i);

    }

    try {int iVal1 = v[7];

    // not bounds checked - will not throw

    int iVal2 = v.at(7);

    // bounds checked - will throw if out of range

    } catch(const exception& e) {

    cout << e.what();

    }

    删除vector中的数据

    vector能够非常容易地添加数据，也能很方便地取出数据，同样vector提供了erase()，pop_back()，clear()来删除数据，当删除数据时，应该知道要删除尾部的数据，或者是删除所有数据，还是个别的数据。

    Remove_if()算法如果要使用remove_if()，需要在头文件中包含如下代码：：

    #include <algorithm>

    Remove_if()有三个参数：

    1、 iterator _First：指向第一个数据的迭代指针。

    2、 iterator _Last：指向最后一个数据的迭代指针。

    3、 predicate _Pred：一个可以对迭代操作的条件函数。

    条件函数

    条件函数是一个按照用户定义的条件返回是或否的结果，是最基本的函数指针，或是一个函数对象。这个函数对象需要支持所有的函数调用操作，重载operator()()操作。remove_if()是通过unary_function继承下来的，允许传递数据作为条件。

    例如，假如想从一个vector<CString>中删除匹配的数据，如果字串中包含了一个值，从这个值开始，从这个值结束。首先应该建立一个数据结构来包含这些数据，类似代码如下：

    #include <functional>

    enum findmodes {

    FM_INVALID = 0,

    FM_IS,

    FM_STARTSWITH,

    FM_ENDSWITH,

    FM_CONTAINS

    };

    typedef struct tagFindStr {

    UINT iMode;

    CString szMatchStr;

    } FindStr;

    typedef FindStr* LPFINDSTR;

    然后处理条件判断：

    class FindMatchingString : public std::unary_function<CString, bool> {

    public:

    FindMatchingString(const LPFINDSTR lpFS) :

    m_lpFS(lpFS) {

    }

    bool operator()(CString& szStringToCompare) const {

    bool retVal = false;

    switch (m_lpFS->iMode) {

    case FM_IS: {

    retVal = (szStringToCompare == m_lpFDD->szMatchStr);

    break;

    }

    case FM_STARTSWITH: {

    retVal = (szStringToCompare.Left(m_lpFDD->szMatchStr.GetLength())

    == m_lpFDD->szWindowTitle);

    break;

    }

    case FM_ENDSWITH: {

    retVal = (szStringToCompare.Right(m_lpFDD->szMatchStr.GetLength())

    == m_lpFDD->szMatchStr);

    break;

    }

    case FM_CONTAINS: {

    retVal = (szStringToCompare.Find(m_lpFDD->szMatchStr) != -1);

    break;

    }

    }

    return retVal;

    }

    private:

    LPFINDSTR m_lpFS;

    };

    通过这个操作你可以从vector中有效地删除数据：

    FindStr fs;

    fs.iMode = FM_CONTAINS;

    fs.szMatchStr = szRemove;

    vs.erase(std::remove_if(vs.begin(), vs.end(), FindMatchingString(&fs)), vs.end());

    Remove(),remove_if()等所有的移出操作都是建立在一个迭代范围上的，不能操作容器中的数据。所以在使用remove_if()，实际上操作的时容器里数据的上面的。

    看到remove_if()实际上是根据条件对迭代地址进行了修改，在数据的后面存在一些残余的数据，那些需要删除的数据。剩下的数据的位置可能不是原来的数据，但他们是不知道的。

    调用erase()来删除那些残余的数据。注意上面例子中通过erase()删除remove_if()的结果和vs.enc()范围的数据。


介绍

这篇文章的目的是为了介绍std::vector，如何恰当地使用它们的成员函数等操作。本文中还讨论了条件函数和函数指针在迭代算法中使用，如在remove_if()和for_each()中的使用。通过阅读这篇文章读者应该能够有效地使用vector容器，而且应该不会再去使用C类型的动态数组了。

Vector总览

vector是C++标准模板库中的部分内容，它是一个多功能的，能够操作多种数据结构和算法的模板类和函数库。vector之所以被认为是一个容器，是因为它能够像容器一样存放各种类型的对象，简单地说，vector是一个能够存放任意类型的动态数组，能够增加和压缩数据。

为了可以使用vector，必须在你的头文件中包含下面的代码：

#include <vector>

vector属于std命名域的，因此需要通过命名限定，如下完成你的代码：

using std::vector;

vector<int> vInts;

或者连在一起，使用全名：

std::vector<int> vInts;

建议使用全局的命名域方式：

usingnamespace std;

在后面的操作中全局的命名域方式会造成一些问题。vector容器提供了很多接口，在下面的表中列出vector的成员函数和操作。

Vector成员函数

函数 | 表述
c.assign(beg,end) c.assign(n,elem) | 将[beg; end)区间中的数据赋值给c。 将n个elem的拷贝赋值给c。
c.at(idx) | 传回索引idx所指的数据，如果idx越界，抛出out_of_range。
c.back() | 传回最后一个数据，不检查这个数据是否存在。
c.begin() | 传回迭代器重的可一个数据。
c.capacity() | 返回容器中数据个数。
c.clear() | 移除容器中所有数据。
c.empty() | 判断容器是否为空。
c.end() | 指向迭代器中的最后一个数据地址。
c.erase(pos) c.erase(beg,end) | 删除pos位置的数据，传回下一个数据的位置。 删除[beg,end)区间的数据，传回下一个数据的位置。
c.front() | 传回地一个数据。
get_allocator | 使用构造函数返回一个拷贝。
c.insert(pos,elem) c.insert(pos,n,elem) c.insert(pos,beg,end) | 在pos位置插入一个elem拷贝，传回新数据位置。 在pos位置插入n个elem数据。无返回值。 在pos位置插入在[beg,end)区间的数据。无返回值。
c.max_size() | 返回容器中最大数据的数量。
c.pop_back() | 删除最后一个数据。
c.push_back(elem) | 在尾部加入一个数据。
c.rbegin() | 传回一个逆向队列的第一个数据。
c.rend() | 传回一个逆向队列的最后一个数据的下一个位置。
c.resize(num) | 重新指定队列的长度。
c.reserve() | 保留适当的容量。
c.size() | 返回容器中实际数据的个数。
c1.swap(c2) swap(c1,c2) | 将c1和c2元素互换。 同上操作。
vector<Elem> c vector <Elem> c1(c2) vector <Elem> c(n) vector <Elem> c(n, elem) vector <Elem> c(beg,end) c.~ vector <Elem>() | 创建一个空的vector。 复制一个vector。 创建一个vector，含有n个数据，数据均已缺省构造产生。 创建一个含有n个elem拷贝的vector。 创建一个以[beg;end)区间的vector。 销毁所有数据，释放内存。

Vector操作

函数 | 描述
operator[] | 返回容器中指定位置的一个引用。

创建一个vector

vector容器提供了多种创建方法，下面介绍几种常用的。

创建一个Widget类型的空的vector对象：

vector<Widget> vWidgets;

// ------

// |

// |- Since vector is a container, its member functions

// operate on iterators and the container itself so

// it can hold objects of any type.

创建一个包含500个Widget类型数据的vector：

vector<Widget> vWidgets(500);

创建一个包含500个Widget类型数据的vector，并且都初始化为0：

vector<Widget> vWidgets(500, Widget(0));

创建一个Widget的拷贝：

vector<Widget> vWidgetsFromAnother(vWidgets);

向vector添加一个数据

vector添加数据的缺省方法是push_back()。push_back()函数表示将数据添加到vector的尾部，并按需要来分配内存。例如：向vector<Widget>中添加10个数据，需要如下编写代码：

for(int i= 0;i<10; i++)

vWidgets.push_back(Widget(i));

获取vector中制定位置的数据

很多时候我们不必要知道vector里面有多少数据，vector里面的数据是动态分配的，使用push_back()的一系列分配空间常常决定于文件或一些数据源。如果你想知道vector存放了多少数据，你可以使用empty()。获取vector的大小，可以使用size()。例如，如果你想获取一个vector v的大小，但不知道它是否为空，或者已经包含了数据，如果为空想设置为-1，你可以使用下面的代码实现：

int nSize = v.empty() ? -1 : static_cast<int>(v.size());

访问vector中的数据

使用两种方法来访问vector。

1、 vector::at()

2、 vector::operator[]

operator[]主要是为了与C语言进行兼容。它可以像C语言数组一样操作。但at()是我们的首选，因为at()进行了边界检查，如果访问超过了vector的范围，将抛出一个例外。由于operator[]容易造成一些错误，所有我们很少用它，下面进行验证一下：

分析下面的代码：

vector<int> v;

v.reserve(10);

for(int i=0; i<7; i++)

v.push_back(i);

try

{

int iVal1 = v[7]; // not bounds checked - will not throw

int iVal2 = v.at(7); // bounds checked - will throw if out of range

}

catch(const exception& e)

{

cout << e.what();

}

我们使用reserve()分配了10个int型的空间，但并不没有初始化。如下图所示：

你可以在这个代码中尝试不同条件，观察它的结果，但是无论何时使用at()，都是正确的。

删除vector中的数据

vector能够非常容易地添加数据，也能很方便地取出数据，同样vector提供了erase()，pop_back()，clear()来删除数据，当你删除数据的时候，你应该知道要删除尾部的数据，或者是删除所有数据，还是个别的数据。在考虑删除等操作之前让我们静下来考虑一下在STL中的一些应用。

Remove_if()算法

现在我们考虑操作里面的数据。如果要使用remove_if()，我们需要在头文件中包含如下代码：：

#include <algorithm>

Remove_if()有三个参数：

1、 iterator _First：指向第一个数据的迭代指针。

2、 iterator _Last：指向最后一个数据的迭代指针。

3、 predicate _Pred：一个可以对迭代操作的条件函数。

条件函数

条件函数是一个按照用户定义的条件返回是或否的结果，是最基本的函数指针，或者是一个函数对象。这个函数对象需要支持所有的函数调用操作，重载operator()()操作。remove_if()是通过unary_function继承下来的，允许传递数据作为条件。

例如，假如你想从一个vector<CString>中删除匹配的数据，如果字串中包含了一个值，从这个值开始，从这个值结束。首先你应该建立一个数据结构来包含这些数据，类似代码如下：

#include <functional>

enum findmodes

{

FM_INVALID = 0,

FM_IS,

FM_STARTSWITH,

FM_ENDSWITH,

FM_CONTAINS

};

typedefstruct tagFindStr

{

UINT iMode;

CString szMatchStr;

} FindStr;

typedef FindStr* LPFINDSTR;

然后处理条件判断：

class FindMatchingString

: public std::unary_function<CString, bool>

{

public:

FindMatchingString(const LPFINDSTR lpFS) : m_lpFS(lpFS) {}

bool operator()(CString& szStringToCompare) const

{

bool retVal = false;

switch(m_lpFS->iMode)

{

case FM_IS:

{

retVal = (szStringToCompare == m_lpFDD->szMatchStr);

break;

}

case FM_STARTSWITH:

{

retVal = (szStringToCompare.Left(m_lpFDD->szMatchStr.GetLength())

== m_lpFDD->szWindowTitle);

break;

}

case FM_ENDSWITH:

{

retVal = (szStringToCompare.Right(m_lpFDD->szMatchStr.GetLength())

== m_lpFDD->szMatchStr);

break;

}

case FM_CONTAINS:

{

retVal = (szStringToCompare.Find(m_lpFDD->szMatchStr) != -1);

break;

}

}

return retVal;

}

private:

LPFINDSTR m_lpFS;

};

通过这个操作你可以从vector中有效地删除数据：

// remove all strings containing the value of

// szRemove from vector<CString> vs.

FindStr fs;

fs.iMode = FM_CONTAINS;

fs.szMatchStr = szRemove;

vs.erase(std::remove_if(vs.begin(), vs.end(), FindMatchingString(&fs)), vs.end());

Remove_if()能做什么？

你可能会疑惑，对于上面那个例子在调用remove_if()的时候还要使用erase()呢？这是因为大家并不熟悉STL中的算法。Remove(),remove_if()等所有的移出操作都是建立在一个迭代范围上的，那么不能操作容器中的数据。所以在使用remove_if()，实际上操作的时容器里数据的上面的。思考上面的例子：

1、 szRemove = “o”.

2、 vs见下面图表中的显示。

观察这个结果，我们可以看到remove_if()实际上是根据条件对迭代地址进行了修改，在数据的后面存在一些残余的数据，那些需要删除的数据。剩下的数据的位置可能不是原来的数据，但他们是不知道的。

调用erase()来删除那些残余的数据。注意上面例子中通过erase()删除remove_if()的结果和vs.enc()范围的数据。

压缩一个臃肿的vector

很多时候大量的删除数据，或者通过使用reserve()，结果vector的空间远远大于实际需要的。所有需要压缩vector到它实际的大小。resize()能够增加vector的大小。Clear()仅仅能够改变缓存的大小，所有的这些对于vector释放内存等九非常重要了。如何来解决这些问题呢，让我们来操作一下。

我们可以通过一个vector创建另一个vector。让我们看看这将发生什么。假定我们已经有一个vector v，它的内存大小为1000，当我们调用size()的时候，它的大小仅为7。我们浪费了大量的内存。让我们在它的基础上创建一个vector。

std::vector<CString> vNew(v);

cout << vNew.capacity();

vNew.capacity()返回的是7。这说明新创建的只是根据实际大小来分配的空间。现在我们不想释放v，因为我们要在其它地方用到它，我们可以使用swap()将v和vNew互相交换一下？

vNew.swap(v);

cout << vNew.capacity();

cout << v.capacity();

有趣的是：vNew.capacity()是1000，而v.capacity()是7。

现在是达到我的目的了，但是并不是很好的解决方法，我们可以像下面这么写：

std::vector<CString>(v).swap(v);

你可以看到我们做了什么？我们创建了一个临时变量代替那个命名的，然后使用swap(),这样我们就去掉了不必要的空间，得到实际大小的v。

结论

我希望这个文档可以给那些使用STL vector容器的开发者很有价值的参考。我也希望通过阅读这篇文章你可以放心地使用vector来代替C语言中的数据了。

参考

Plauger, P.J. Standard C++ Library Reference. February, 2003. MSDN.

Schildt, Herbert. C++ from the Ground Up, Second Edition. Berkeley: 1998.

Sutter, Herb. More Exceptional C++. Indianapolis: 2002.


位域

    有些信息在存储时，并不需要占用一个完整的字节，而只需占几个或一个二进制位。例如在存放一个开关量时，只有0和1 两种状态，用一位二进位即可。为了节省存储空间，并使处理简便，C语言又提供了一种数据结构，称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域，并说明每个区域的位数。每个域有一个域名，允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

    一、位域的定义和位域变量的说明位域定义与结构定义相仿，其形式为：

    struct 位域结构名

    { 位域列表 };

    其中位域列表的形式为：类型说明符位域名：位域长度

    例如：

    struct bs

    {

    int a:8;

    int b:2;

    int c:6;

    };

    位域变量的说明与结构变量说明的方式相同。可采用先定义后说明，同时定义说明或者直接说明这三种方式。例如：

    struct bs

    {

    int a:8;

    int b:2;

    int c:6;

    }data;

    说明data为bs变量，共占两个字节。其中位域a占8位，位域b占2位，位域c占6位。对于位域的定义尚有以下几点说明：

    1. 一个位域必须存储在同一个字节中，不能跨两个字节。如一个字节所剩空间不够存放另一位域时，应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如：

    struct bs

    {

    unsigned a:4

    unsigned :0 /*空域*/

    unsigned b:4 /*从下一单元开始存放*/

    unsigned c:4

    }

    在这个位域定义中，a占第一字节的4位，后4位填0表示不使用，b从第二字节开始，占用4位，c占用4位。

    2. 位域的长度不能大于一个字节的长度，也就是说不能超过8位。

    3. 位域可以无位域名，这时它只用来作填充或调整位置。无名的位域是不能使用的。例如：

    struct k

    {

    int a:1

    int :2 /*该2位不能使用*/

    int b:3

    int c:2

    };

    从以上分析可以看出，位域在本质上就是一种结构类型，不过其成员是按二进位分配的。

    二、位域的使用

    位域的使用和结构成员的使用相同，其一般形式为：位域变量名·位域名位域允许用各种格式输出。

    main(){

    struct bs

    {

    unsigned a:1;

    unsigned b:3;

    unsigned c:4;

    } bit,*pbit;

    bit.a=1;

    bit.b=7;

    bit.c=15;

    printf("%d,%d,%d\n",bit.a,bit.b,bit.c);

    pbit=&bit;

    pbit->a=0;

    pbit->b&=3;

    pbit->c|=1;

    printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);

    }

    上例程序中定义了位域结构bs，三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。

    程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值，赋为0。第15行使用了复合的位运算符"&="，该行相当于： pbit->b=pbit->b&3位域b中原有值为7，与3作按位与运算的结果为3(111&011=011,十进制值为 3)。同样，程序第16行中使用了复合位运算"|="，相当于： pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

    为了节省空间，可以把几个数据压缩到少数的几个类型空间上，比如需要表示二个3位二进制的数，一个2位二进制的数，则可以用一个8位的字符表示之。

    struct

    {

    char a : 3;

    char b : 3;

    char c : 2;

    } ;

    这个结构体所占空间为一个字节，8位。节省了空间。

C语言里的位域是一个比较复杂的问题，涉及的方面也比较多，关于位域的基础内容可以参考以下文章：理解C语言位域

    分析代码如下：

    #include "stdio.h"

    #include "memory.h"

    struct BitSeg1{

    int a:4;

    int b:3;

    };

    struct BitSeg2{

    char a:4;

    char b:3;

    };

    int main()

    {

    struct BitSeg1 ba1;

    ba1.a=1;

    ba1.b=2;

    printf("第一次赋值后: a的值为:%d\tb的值为:%d\n",ba1.a,ba1.b);

    ba1.a=100;

    ba1.b=30;

    printf("第二次赋值后: a的值为:%d\tb的值为:%d\n",ba1.a,ba1.b);

    char str[]="0123";

    memcpy(&ba1,str,sizeof(BitSeg1));

    printf("第二次赋值后: a的值为:%d\tb的值为:%d\n",ba1.a,ba1.b);

    printf("BitSeg1的字节数为: %d\n",sizeof(BitSeg1));

    printf("BitSeg2的字节数为: %d\n",sizeof(BitSeg2));

    return 0;

    }

    输出结果为：

    第一次赋值后: a的值为:1 b的值为:2

    第二次赋值后: a的值为:4 b的值为:-2

    第二次赋值后: a的值为:0 b的值为:3

    BitSeg1的字节数为: 4

    BitSeg2的字节数为: 1

    代码中的BigSeg1定义了两个int类型的字段，而且它们分别只占用4位和3位的空间。当BitSeg1中的a，b分别赋值为1和2时，输出的结果也如我们所料。当第二次赋值为100和30时，输出的结果却是4和-2，为什么呢？

    1.赋值问题

    出现上述问题，是由于赋值与位域效果共同形成的，a和b虽然都是int类型，但是在BigSeg1结构里，它们只有4位和3位为实际有效位。也就是BigSeg1中的前4位是a的，接着的3位是b的（这里没有字节的跨越问题）。执行ba1.a=100语句，其中100的二进制代码是：01100100，程序只把这100的二进制数的前面4位（已用红色字体表示）赋值给a，那么ba1中的a只是0100(b)，结果当然是4咯。然后是执行b1.b=30语句，其中30的二进制代码为：00011110，同样的程序只把前3位（注意b定义有效位数是3位）赋值给b，那么ba1中的b就是110(b)，结果是-2，为什么？是这样的，我们定义b为int类型，也就是有符号的整型，如果想定义为无符号整型我们必须这样写unsigned int，而有符号整型的第一位是符号位，用于表示正负的（1表示负数，0表示正数），那么对于b，程序就会把b的第一位（即1）做为符号位，即b应该是负数，而后面的是它的数值（即10(b)），注意计算机里负数是按补码的形式表示的，这种赋值下b的确是110（它是补码，按“即反加一”的法则，即十进制的-2），结果就是-2了。而刚才的a给赋值为0100(b)时，第一位是0，解释为正数。再举一例，若使ba1.b=7，那么ba1.b的值是多少呢？7的二进制是0111，前面3位直接给到b，因为是负数，读出来时按补码形式读，那么就是-1了。

    总之一句话：用位为理解位域。

    接下来是用memcpy对ba1进行内存copy，就更应该用位来考虑位域了。下面我们分析一下：

    首先，sizeof(BitSeg1)的值是4个字节，先记住，后面会对此问题进行详细解释。

    执行memcpy(&ba1,str,sizeof(BitSeg1))，把str的内容中的前面4个字节的内存里的内容复制到ba1中，我们先来看一下str的内存位信息（用16进制表示）：

    0x0012ff74：30 　31 　32　33

    其中0x0012ff74时str数组的地址起始位置，30，31，32，33等16进制值分别表示字符’0’，’1’，’2’，’3’，它们当然是ACII值啦。

    copy之后的ba1的内存位信息如下：

    0x0012ff7c：30 31 32 33

    因为ba1也是占4个字节的空间的，所以不会出现内存溢出。memcpy只是把相应内存复制到了ba1上，位信息与str上的信息一样的。

    现在，我们把30(H)的二进制写出来，是：00110000，ba1的a占前面4位，b占接下来的3位，直观地看，a应该是0011(b)即十进制的3，b是000(b)即十进制的0，但看输出的结果却是a=0，b=3，这又是为什么呢？其实很简单，处理器定义字节的前面4位是指该字节从右往左4位，而不是从左往右的4位，所以a应该是0000(b)，b应该是011(b)。


在计算机程序中，数据的位是可以操作的最小数据单位，理论上可以用“位运算”来完成所有的运算和操作

  在计算机程序中，数据的位是可以操作的最小数据单位，理论上可以用“位运算”来完成所有的运算和操作。

    一般的位操作是用来控制硬件的，或者做数据变换使用，但是，灵活的位操作可以有效地提高程序运行的效率。C语言提供了位运算的功能， 这使得C语言也能像汇编语言一样用来编写系统程序。

    位运算符C语言提供了六种位运算符：

    & 按位与

    | 按位或

    ^ 按位异或

    ~ 取反

    << 左移

    >> 右移

    1. 按位与运算 按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时，结果位才为1 ，否则为0。参与运算的数以补码方式出现。

    例如：9&5可写算式如下： 00001001 (9的二进制补码)&00000101 (5的二进制补码)　00000001 (1的二进制补码)可见9&5=1。

    按位与运算通常用来对某些位清0或保留某些位。例如把a 的高八位清 0 ， 保留低八位， 可作 a&255 运算 ( 255 的二进制数为0000000011111111)。

    应用：

    a. 清零特定位 (mask中特定位置0，其它位为1，s=s&mask)

    b. 取某数中指定位 (mask中特定位置1，其它位为0，s=s&mask)

    2. 按位或运算 按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时，结果位就为1。参与运算的两个数均以补码出现。

     例如：9|5可写算式如下：

    00001001|00000101

    00001101 (十进制为13)可见9|5=13

    应用：

    常用来将源操作数某些位置1，其它位不变。 (mask中特定位置1，其它位为0 s=s|mask)

    3. 按位异或运算 按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。参与运算数仍以补码出现，例如9^5可写成算式如下：

    00001001^00000101 00001100 (十进制为12)

    应用：

    a. 使特定位的值取反 (mask中特定位置1，其它位为0 s=s^mask)

    b. 不引入第三变量，交换两个变量的值 (设 a=a1,b=b1)

    目 标 操 作 操作后状态

    a=a1^b1 a=a^b a=a1^b1,b=b1

    b=a1^b1^b1 b=a^b a=a1^b1,b=a1

    a=b1^a1^a1 a=a^b a=b1,b=a1

    4. 求反运算

    求反运算符～为单目运算符，具有右结合性。 其功能是对参与运算的数的各二进位按位求反。例如～9的运算为： ~(0000000000001001)结果为：1111111111110110

    5. 左移运算

     左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位，由“<<”右边的数指定移动的位数， 高位丢弃，低位补0。 其值相当于乘2。例如： a<<4 指把a的各二进位向左移动4位。如a=00000011(十进制3)，左移4位后为00110000(十进制48)。

    6. 右移运算

    右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位，“>>”右边的数指定移动的位数。其值相当于除2。

    例如：设 a=15，a>>2　表示把000001111右移为00000011(十进制3)。对于左边移出的空位，如果是正数则空位补0，若为负数，可能补0或补1，这取决于所用的计算机系统。移入0的叫逻辑右移，移入1的叫算术右移，Turbo C采用逻辑右移。

main(){

  unsigned a,b;

  printf("input a number: ");

  scanf("%d",&a);

  b=a>>5;

  b=b&15;

  printf("a=%d b=%d ",a,b);

}

    再看一例:

main(){

  char a='a',b='b';

  int p,c,d;

  p=a;

  p=(p<<8)|b;

  d=p&0xff;

  c=(p&0xff00)>>8;

  printf("a=%d b=%d c=%d d=%d ",a,b,c,d);

}

    浮点数的存储格式：

    浮点数的存储格式是符号+阶码(定点整数)+尾数(定点小数)

SEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMM

即1位符号位(0为正，1为负），8位指数位，23位尾数位

浮点数存储前先转化成2的k次方形式，即：

f = A1*2^k + A2*2^(k-1) + ... + Ak +... +An*2^(-m) (Ai = {0, 1}, A1 = 1)

如5.5=2^2 + 2^0 + 2^(-1)

其中的k就是指数，加127后组成8位指数位

5.5的指数位就是2+127 = 129 = 10000001

A2A3.....An就是尾数位，不足23位后补0

所以5.5 = 01000000101000000000000000000000 = 40A00000

所以，对浮点数*2、/2只要对8位符号位+、- 即可，但不是左移、右移

    关于unsigned int 和 int 的在位运算上的不同，下面有个CU上的例子描述的很清楚：

    [问题]：这个函数有什么问题吗？

/**

* 本函数将两个16比特位的值连结成为一个32比特位的值。

* 参数：sHighBits高16位

* sLowBits 低16位

* 返回：32位值

**/

long CatenateBits16(short sHighBits, short sLowBits)

{

long lResult = 0; /* 32位值的临时变量*/

/* 将第一个16位值放入32位值的高16位 */

lResult = sHighBits;

lResult <<= 16;

/* 清除32位值的低16位 */

lResult &= 0xFFFF0000;

/* 将第二个16位值放入32位值的低16位 */

lResult |= (long)sLowBits;

return lResult;

}

/////////////////////////////////////////////////

[问题的发现]：

我们先看如下测试代码：

/////////////////////////////////////////////////

int main()

{

short sHighBits1 = 0x7fff;

short sHighBits2 = 0x8f12;

unsigned short usHighBits3 = 0xff12;

short sLowBits1 = 0x7bcd;

long lResult = 0;

printf("[sHighBits1 + sLowBits1] ";

lResult = CatenateBits16(sHighBits1, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(sHighBits2, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(usHighBits3, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

}

/////////////////////////////////////////////////

运行结果为：

[sHighBits1 + sLowBits1]

lResult = 7fff7bcd

lResult = 8f127bcd

lResult = ff127bcd

嗯，运行很正确嘛……于是我们就放心的在自己的程序中使用起这个函数来了。

可是忽然有一天，我们的一个程序无论如何结果都不对！经过n个小时的检查和调试，最后终于追踪到……CatenateBits16() ！？它的返回值居然是错的！！

“郁闷！”你说，“这个函数怎么会有问题呢！？”

可是，更郁闷的还在后头呢，因为你把程序中的输入量作为参数，在一个简单的main()里面单步调试：

/////////////////////////////////////////////////

int main()

{

short sHighBits1 = 0x7FFF;

short sHighBits2 = 0x8F12;

unsigned short usHighBits3 = 0x8F12;

short sLowBits1 = 0x7BCD; //你实际使用的参数

short sLowBits2 = 0x8BCD; //你实际使用的参数

long lResult = 0;

printf("[sHighBits1 + sLowBits1] ";

lResult = CatenateBits16(sHighBits1, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(sHighBits2, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(usHighBits3, sLowBits1);

printf("lResult = %08x ", lResult, lResult);

printf(" [sHighBits1 + sLowBits2] ";

lResult = CatenateBits16(sHighBits1, sLowBits2);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(sHighBits2, sLowBits2);

printf("lResult = %08x ", lResult, lResult);

lResult = CatenateBits16(usHighBits3, sLowBits2);

printf("lResult = %08x ", lResult, lResult);

return 0;

}

/////////////////////////////////////////////////

发现结果竟然是：

[sHighBits1 + sLowBits1]

lResult = 7fff7bcd

lResult = 8f127bcd

lResult = 8f127bcd

[sHighBits1 + sLowBits2]

lResult = ffff8bcd //oops!

lResult = ffff8bcd //oops!

lResult = ffff8bcd //oops!

前一次还好好的，后一次就ffff了？X档案？

[X档案的真相]：

注意那两个我们用来当作低16位值的sLowBits1和sLowBits2。

已知：

使用 sLowBits1 = 0x7bcd 时，函数返回正确的值；

使用 sLowBits2 = 0x8bcd 时，函数中发生X档案。

那么，sLowBits1与sLowBits2有什么区别？

注意了，sLowBits1和sLowBits2都是short型（而不是unsigned short），所以在这里，sLowBits1代表一个正数值，而sLowBits2却代表了一个负数值（因为8即是二进制1000，sLowBits2最高位是1）。

再看CatenateBits16()函数：

/////////////////////////////////////////////////

long CatenateBits16(short sHighBits, short sLowBits)

{

long lResult = 0; /* 32位值的临时变量*/

/* 将第一个16位值放入32位值的高16位 */

lResult = sHighBits;

lResult <<= 16;

/* 清除32位值的低16位 */

lResult &= 0xFFFF0000;

/* 将第二个16位值放入32位值的低16位 */

lResult |= (long)sLowBits; //注意这一句！！！！

return lResult;

}

/////////////////////////////////////////////////

如果我们在函数中用

printf("sLowBits = %04x ", sLowBits);

打印传入的sLowBits值，会发现

sLowBits = 0x7bcd 时，打印结果为

sLowBits = 7bcd

而sLowBits = 0x8bcd时，打印结果为

sLowBits = ffff8bcd

是的，即使用%04x也打印出8位十六进制。

因此，我们看出来了：

当sLowBits = 0x8bcd时，函数中 "lResult |= (long)sLowBits;" 这一句执行，会先将sLowBits转换为

0xffff8bcd

再与lResult做或运算。由于现在lResult的值为 0xXXXX0000 （其中XXXX是任何值），所以显然，无论sHighBits是什么值，最后结果都会是

0xffff8bcd

而当sLowBits = 0x7bcd时，函数中 "lResult |= (long)sLowBits;" 这一句执行，会先将sLowBits转换为

0x00007bcd

再与lResult做或运算。这样做或运算出来的结果当然就是对的。

也就是说，CatenateBits16()在sLowBits的最高位为0的时候表现正常，而在最高位为1的时候出现偏差。

[教训：在某些情况下作位运算和位处理的时候，考虑使用无符号数值——因为这个时候往往不需要处理符号。即使你需要的有符号的数值，那么也应该考虑自行在调用CatenateBits16()前后做转换——毕竟在位处理中，有符号数值相当诡异！]

下面这个CatenateBits16()版本应该会好一些：

/////////////////////////////////////////////////

unsigned long CatenateBits16(unsigned short sHighBits, unsigned short sLowBits)

{

long lResult = 0;

/* 将第一个16位值放入32位值的高16位 */

lResult = sHighBits;

lResult <<= 16;

/* 清除32位值的低16位 */

lResult &= 0xFFFF0000;

/* 将第二个16位值放入32位值的低16位 */

lResult |= (long)sLowBits & 0x0000FFFF;

return lResult;

}

/////////////////////////////////////////////////

注意其中的 "lResult |= (long)sLowBits & 0x0000FFFF;"。事实上，现在即使我们把CatenateBits16()函数的参数（特别是sLowBits）声明为short，结果也会是对的。

如果有一天你把一只兔子扔给一只老虎，老虎把兔子吃了，第二天把一只老鼠扔给它，它又吃了，那么说明第一天你看错了：它本来就是一只猫。

前言

看到有些人对位运算还存在问题，于是决定写这篇文章作个简要说明。

什么是位(bit)？

很简单，位(bit)就是单个的0或1，位是我们在计算机上所作一切的基础。计算机上的所有数据都是用位来存储的。一个字节(BYTE)由八个位组成，一个字(WORD)是二个字节或十六位，一个双字(DWORD)是二个字(WORDS)或三十二位。如下所示：

0 1 0 0 0 1 1 1 1 0 0 0 0 1 1 1 0 1 1 1 0 1 0 0 0 1 1 1 1 0 0 0

| | | | | | |

| +- bit 31 | | | bit 0 -+ |

| | | | |

+-- BYTE 3 ---- ----+---- BYTE 2 ------------+---- BYTE 1 -------------+--- BYTE 0 -------------+

| | |

+--------------------------- WORD 1 ------------------+-------------------------- WORD 0 -------------------+

| |

+--------------------------------------------------------- DWORD --------------------------------------------------------+

使用位运算的好处是可以将BYTE, WORD 或 DWORD 作为小数组或结构使用。通过位运算可以检查位的值或赋值，也可以对整组的位进行运算。

16 进制数及其与位的关系

用0或1表示的数值就是二进制数，很难理解。因此用到16进制数。

16进制数用4个位表示0 - 15的值，4个位组成一个16进制数。也把4位成为半字节(nibble)。一个BYTE有二个nibble，因此可以用二个16进制数表示一个BYTE。如下所示：

NIBBLE HEX VALUE

====== =========

0000 0

0001 1

0010 2

0011 3

0100 4

0101 5

0110 6

0111 7

1000 8

1001 9

1010 A

1011 B

1100 C

1101 D

1110 E

1111 F

如果用一个字节存放字母"r"(ASCII码114)，结果是：

0111 0010 二进制

7 2 16进制

可以表达为：'0x72'

有6种位运算：

& 与运算

| 或运算

^ 异或运算

~ 非运算(求补)

>> 右移运算

<< 左移运算

与运算(&)

双目运算。二个位都置位(等于1)时，结果等于1，其它的结果都等于0。

1 & 1 == 1

1 & 0 == 0

0 & 1 == 0

0 & 0 == 0

与运算的一个用途是检查指定位是否置位(等于1)。例如一个BYTE里有标识位，要检查第4位是否置位，代码如下：

BYTE b = 50;

if ( b & 0x10 )

cout << "Bit four is set" << endl;

else

cout << "Bit four is clear" << endl;

上述代码可表示为：

00110010 - b

& 00010000 - & 0x10

----------------------------

00010000 - result

可以看到第4位是置位了。

或运算( | )

双目运算。二个位只要有一个位置位，结果就等于1。二个位都为0时，结果为0。

1 | 1 == 1

1 | 0 == 1

0 | 1 == 1

0 | 0 == 0

与运算也可以用来检查置位。例如要检查某个值的第3位是否置位：

BYTE b = 50;

BYTE c = b | 0x04;

cout << "c = " << c << endl;

可表达为：

00110010 - b

| 00000100 - | 0x04

----------

00110110 - result

异或运算(^)

双目运算。二个位不相等时，结果为1，否则为0。

1 ^ 1 == 0

1 ^ 0 == 1

0 ^ 1 == 1

0 ^ 0 == 0

异或运算可用于位值翻转。例如将第3位与第4位的值翻转：

BYTE b = 50;

cout << "b = " << b << endl;

b = b ^ 0x18;

cout << "b = " << b << endl;

b = b ^ 0x18;

cout << "b = " << b << endl;

可表达为：

00110010 - b

^ 00011000 - ^0x18

----------

00101010 - result

00101010 - b

^ 00011000 - ^0x18

----------

00110010 - result

非运算(~)

单目运算。位值取反，置0为1，或置1为0。非运算的用途是将指定位清0，其余位置1。非运算与数值大小无关。例如将第1位和第2位清0，其余位置1：

BYTE b = ~0x03;

cout << "b = " << b << endl;

WORD w = ~0x03;

cout << "w = " << w << endl;

可表达为：

00000011 - 0x03

11111100 - ~0x03 b

0000000000000011 - 0x03

1111111111111100 - ~0x03 w

非运算和与运算结合，可以确保将指定为清0。如将第4位清0：

BYTE b = 50;

cout << "b = " << b << endl;

BYTE c = b & ~0x10;

cout << "c = " << c << endl;

可表达为：

00110010 - b

& 11101111 - ~0x10

----------

00100010 - result

移位运算(>> 与 <<)

将位值向一个方向移动指定的位数。右移 >> 算子从高位向低位移动，左移 << 算子从低位向高位移动。往往用位移来对齐位的排列(如MAKEWPARAM, HIWORD, LOWORD 宏的功能)。

BYTE b = 12;

cout << "b = " << b << endl;

BYTE c = b << 2;

cout << "c = " << c << endl;

c = b >> 2;

cout << "c = " << c << endl;

可表达为：

00001100 - b

00110000 - b << 2

00000011 - b >> 2

译注：以上示例都对，但举例用法未必恰当。请阅文末链接的文章，解释得较为清楚。

位域(Bit Field)

位操作中的一件有意义的事是位域。利用位域可以用BYTE, WORD或DWORD来创建最小化的数据结构。例如要保存日期数据，并尽可能减少内存占用，就可以声明这样的结构：

struct date_struct {

BYTE day : 5, // 1 to 31

month : 4, // 1 to 12

year : 14; // 0 to 9999

}date;

在结构中，日期数据占用最低5位，月份占用4位，年占用14位。这样整个日期数据只需占用23位，即3个字节。忽略第24位。如果用整数来表达各个域，整个结构要占用12个字节。

| 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 |

| | | |

+------------- year --------------+ month+-- day --+

现在分别看看在这个结构声明中发生了什么

首先看一下位域结构使用的数据类型。这里用的是BYTE。1个BYTE有8个位，编译器将分配1个BYTE的内存。如果结构内的数据超过8位，编译器就再分配1个BYTE，直到满足数据要求。如果用WORD或DWORD作结构的数据类型，编译器就分配一个完整的32位内存给结构。

其次看一下域声明。变量(day, month, year)名跟随一个冒号，冒号后是变量占用的位数。位域之间用逗号分隔，用分号结束。

使用了位域结构，就可以方便地象处理普通结构数据那样处理成员数据。尽管我们无法得到位域的地址，却可以使用结构地址。例如：

date.day = 12;

dateptr = &date;

dateptr->year = 1852;

位运算

    前面介绍的各种运算都是以字节作为最基本位进行的。 但在很多系统程序中常要求在位(bit)一级进行运算或处理。C语言提供了位运算的功能， 这使得C语言也能像汇编语言一样用来编写系统程序。

    一、位运算符C语言提供了六种位运算符：

    & 按位与

    | 按位或

    ^ 按位异或

    ~ 取反

    << 左移

    >> 右移

    1. 按位与运算按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时，结果位才为1 ，否则为0。参与运算的数以补码方式出现。

    例如：9&5可写算式如下： 00001001 (9的二进制补码)&00000101 (5的二进制补码)　00000001 (1的二进制补码)可见9&5=1。

    按位与运算通常用来对某些位清0或保留某些位。例如把a 的高八位清 0 ， 保留低八位， 可作 a&255 运算 ( 255 的二进制数为0000000011111111)。

main(){

  int a=9,b=5,c;

  c=a&b;

  printf("a=%d\nb=%d\nc=%d\n",a,b,c);

}

    2. 按位或运算 按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时，结果位就为1。参与运算的两个数均以补码出现。

     例如：9|5可写算式如下：

00001001|00000101

00001101 (十进制为13)可见9|5=13

main(){

  int a=9,b=5,c;

  c=a|b;

  printf("a=%d\nb=%d\nc=%d\n",a,b,c);

}

    3. 按位异或运算 按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。参与运算数仍以补码出现，例如9^5可写成算式如下：

00001001^00000101 00001100 (十进制为12)

main()

{

  int a=9;

  a=a^15;

  printf("a=%d\n",a);

}

    4. 求反运算 求反运算符～为单目运算符，具有右结合性。 其功能是对参与运算的数的各二进位按位求反。例如～9的运算为： ~(0000000000001001)结果为：1111111111110110

    5. 左移运算 左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位，由“<<”右边的数指定移动的位数，

    高位丢弃，低位补0。例如： a<<4 指把a的各二进位向左移动4位。如a=00000011(十进制3)，左移4位后为00110000(十进制48)。6. 右移运算 右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位，“>>”右边的数指定移动的位数。

    例如：设 a=15，a>>2　表示把000001111右移为00000011(十进制3)。 应该说明的是，对于有符号数，在右移时，符号位将随同移动。当为正数时， 最高位补0，而为负数时，符号位为1，最高位是补0或是补1 取决于编译系统的规定。Turbo C和很多系统规定为补1。

main(){

  unsigned a,b;

  printf("input a number: ");

  scanf("%d",&a);

  b=a>>5;

  b=b&15;

  printf("a=%d\tb=%d\n",a,b);

}

    请再看一例!

main(){

  char a='a',b='b';

  int p,c,d;

  p=a;

  p=(p<<8)|b;

  d=p&0xff;

  c=(p&0xff00)>>8;

  printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);

}


关于原码、反码、补码

1、

表示一个数值要先说明是用多少bit，例如：

用8bit表示数值时，(-128)没有相对应的原码和反码, (-128)补码 = (1000 0000)

同理(2B=16bit)表示：(-32768)补码=(1000 0000 0000 0000),后面回给出证明，

因为它是不能简单的用取反加一的方法来求反码的。

2、

证明：用(2B=16bit)表示：(-32768)补码=(1000 0000 0000 0000)

（1）32767（正数补码与原码相同）是0111 1111 1111 1111

（2）-1的补码，其原码取反在加一得 1111 1111 1111 1111

（3）0111 1111 1111 1111+1000 0000 0000 0000=1111 1111 1111 1111

（4）（令上式x==1000 0000 0000 0000）即：32767+x=-1

（5）x=-32768

至于用(2B=16bit)表示，取反加一求(-32768)的补码，还望哪个高手指点简单的证明过程。

希望大家以后在被问为什么(-128)补码 = (1000 0000)， (-32768)补码=(1000 0000 0000 0000)不要在说什么取反加一的话，那样你证明我看看。给出证明，免得人家不明白还以为你在谈什么高深的话题，结果却是被忽悠了。

3、

下面举例说明求负数的补码的补码

-1的补码是0xFFFF. 它是这样求的:

-1的原码: 1000 0000 0000 0001 ，

数值位按位求反: 0xFFFE==1111 1111 1111 1110，

末位加1: 0xFFFF==1111 1111 1111 1111

现在还按这个补码的求法, 作用在0xFFFF上, 0xFFFF: 1111 1111 1111 1111

数值位按位求反: 1000 0000 0000 0000

末位加1: 1000 0000 0000 0001，

这样又得到了-1。

就像求负数的绝对值，彼此导来导去都可以。***************************************************************************

***************************************************************************
补码的计算和引进补码的原因：

数值有正负之分,计算机就用一个数的最高位存放符号(0为正,1为负).这就是机器数的原码了.假设机器能处理的位数为8.即字长为1byte，原码能表示数值的范围为(-127~-0 +0~127)共256个.有了数值的表示方法就可以对数进行算术运算.但是很快就发现用带符号位的原码进行乘除运算时结果正确,而在加减运算的时候就出现了问题,如下: 假设字长为8bits

(1)10 - (1)10= (1)10+ (-1)10 = (1)10

(00000001)原 + (10000001)原 = (10000010)原 = ( -2 ) 显然不正确.

因为在两个整数的加法运算中是没有问题的,于是就发现问题出现在带符号位的负数身上,对除符号位外的其余各位逐位取反就产生了反码.反码的取值空间和原码相同且一一对应.

下面是反码的减法运算:

(1)10- (1)10= (1)10+ (-1)10= (0)10

(00000001) 反+ (11111110)反 = (11111111)反 = ( -0 ) 有问题.

(1)10 - (2)10 = (1)10 + (-2)10 = (-1)10

(00000001) 反+ (11111101)反 = (11111110)反 = ( -1 ) 正确

问题出现在(+0)和(-0)上,在人们的计算概念中零是没有正负之分的.

于是就引入了补码概念. 负数的补码就是对反码加一,而正数不变,正数的原码反码补码是一样的.

在补码中用(-128)代替了(-0),所以补码的表示范围为:(-128~0~127)共256个.已知某数的补码,先求某数的反码,然后在对反码+1,就得到某数的原码.比如:已知某个数的补码是:10100110先对10100110求反,得:11011001再对11011001加1,得: 11011010那么这个数为-86

原码表示的范围为:－（2^(n-1)－1）~＋（2^(n-1)－1），反码表示的范围为与原码一样.

补码表示的范围为：－2^(n-1)~＋（2^(n-1)－1），其中n为机器字长。

注意:

0的补码是唯一的,为0000,0000 [+0]补=[-0]补=0000,0000 -0的反码为1111,1111

8bit表示数值时(-128)没有相对应的原码和反码, (-128) = (10000000)

/*由此可见：负数的最大值没有补码和原码，只有一个二进制表示，该二进制表示与真实值之间没有数学联系，所以知道一个数的补码，利用取反加1的方法求其原码是有限制性的，比如8bit表示数时,(-128)就无法用这种方法来求其原码，因为在补码系统中(-128)的替代了(-0)。*/

/*引申开去：那么计算机中是怎么处理负数的最大值的呢？有待考证*/

补码的加减运算如下:

(1)10- (1)10= (1)10 + (-1)10 = (0)10

(00000001)补 + (11111111)补 = (00000000)补 = ( 0 ) 正确

(1)10 - (2)10= (1)10 + (-2)10 = (-1)10

(00000001) 补+ (11111110) 补= (11111111)补 = ( -1 ) 正确

所以补码的设计目的是:

⑴使符号位能与有效值部分一起参加运算,从而简化计算机的运算规则.

⑵使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计

所有这些转换都是在计算机的最底层进行的，而在我们使用的汇编、C等其他高级语言中使用的都是原码。

看了上面这些大家应该对原码、反码、补码的知识印象更深了吧！！

**************************************************************************

**************************************************************************
下面介绍2的补码记数法

图1显示了两个完整的2的补码系统：一个是基于长度为3的位模式，另一个是基于长度为4的位模式。

这种系统的构造方法是：从相应长度的0的串（例子中用3位和4位）开始，用二进制记数的方式一直记数到位模式是由一个0后面跟随尽可达到的数目1组成（列中是011和0111）为止。这些位模式表示值0，1，3，…。表示负数的位模式是通过从相应长度的1的串开始，然后用二进制倒记数的方式记数，一直到位模式是由一个1和随后尽可能达到的数目0所组成为止。这些位模式表示值-1，-2，-3，…。

如果这种倒记数的方法使用时太困难，也可以从表最底部的位模式开始，向上记数。即从由一个1和随后尽可能达到的个数0组成的位模式开始一直到全由1组成的位模式为止。

图一 贴图01

注意：在2的补码系统中，一个位模式最左边的位指示被表示数值的符号。因此，最左边的位被称为符号位。

负值是用符号位为1的位模式来表示，非负值是由符号位为0的位模式来表示。

在2的补码系统中，表示相同幅度的正负值的模式之间存在着简便的关系。从右至左读位模式，直到包括第一个1时，它们是等同的。继续读位模式，则相互之间就互补了（一个模式的补是由模式中的所有的0变为1、所有的1变为0而得到， 0110和1001是互补的）。例如，图1的4位长模式中表示2和-2的模式都是以10结束，但是表示2的模式是以00开始，而表示-2的模式以11开始。这个观察引出了表示相同幅度的正值和负值的位模式之间进行相互转换的一个算法，即对原位模式从右向左逐位拷贝直到有一个1被拷贝为止，然后对原位模式中余下来的部分求补，把求补结果做拷贝，如图2所示。

图2 贴图1

在熟悉了2的补码系统的这些特性基础上便可引出对2的补码表示形式解码的算法。如果待解码的位模式有为0的符号位，该位模式就好像是二进制表示形式，只需直接读出它的值。例如，0110表示值6，因为0110是6的二进制表示形式。如果待解码的位模式符号位为1，则表示是个负值，并且所有余下的部分表示数值的大小。其做法是：从右向左拷贝原位模式，直到有一个1被拷贝为止，然后对原位模式中余下来的部分求补，最后把所得到的模式当做是二进制表示形式进行解码。

例如，解码模式1010，因为符号位为1，则表示的是个负值。因此，将这个模式转换为0110，此模式代表6，从而得出原来的模式表示-6。

(2) 2的补码记数法中的加法

要加由2的补码记数法表示的数值，除了所有的位模式包括答案在内，都运用了与二进制加法相同的算法。

这就意味着在2的补码系统中进行加法时，最后的进位在答案的左端产生的附加位一定要舍去。于是，加0101和0010得0111；

加0111和1011结果是0010（0111+1011=10010，舍去最左边的1，结果为0010，长度保持4位）。根据这种理解，考察图3中的三个加法问题。在每一种情况中，已经把问题转换成2的补码记数法（使用4位长的位模式），执行前面描述的加法过程，并且将结果解码返回到常用的基数10的记数法。

贴图3 贴图3

采用2的补码记数法的机器需要知道的只是如何去加和求反就够了。例如，减法问题7-5与加法问题7+（-5）是相同的。

因此，如果机器被请求从7（存储为0111）中减5（存储为0101），它首先将5改变为-5（表示为1011），然后执行0111+1011的加法过程取得0010，它表示2


1、原码的定义

①小数原码的定义

[X]原 =X 0≤X ＜1 1－ X －1 ＜ X ≤ 0

例如： X=+0.1011 , [X]原= 01011

X=－0.1011 [X]原= 11011

②整数原码的定义

[X]原 = X 0≤X ＜2n 2n－X － 2n ＜ X ≤ 0

2、补码的定义

①小数补码的定义

[X]补 =X 0≤X ＜1 2＋ X －1 ≤ X ＜ 0

例如： X=+0.1011, [X]补= 01011

X=－0.1011, [X]补= 10101

②整数补码的定义

[X]补 =X 0≤X ＜2n 2n+1＋X － 2n ≤ X ＜ 0

3、反码的定义

①小数反码的定义

[X]反= X 0≤X ＜1 2－2n-1－X －1 ＜ X ≤ 0

例如： X=+0.1011 [X]反= 01011

X=－0.1011 [X]反= 10100

②整数反码的定义

[X]反 = X 0≤X ＜2n 2n+1－1－X － 2n ＜ X ≤ 0[!--empirenews.page--]

4.移码：移码只用于表示浮点数的阶码，所以只用于整数。

①移码的定义：设由1位符号位和n位数值位组成的阶码，则 [X]移=2n + X -2n≤X ≤ 2n

例如： X=＋1011 [X]移=11011 符号位“1”表示正号

X=－1011 [X]移=00101 符号位“0”表示负号

②移码与补码的关系： [X]移与[X]补的关系是符号位互为反码，

例如： X=＋1011 [X]移=11011 [X]补=01011

X=－1011 [X]移=00101 [X]补=10101

③移码运算应注意的问题：

◎对移码运算的结果需要加以修正，修正量为2n ，即对结果的符号位取反后才是移码形式的正确结果。

◎移码表示中，0有唯一的编码——1000…00，当出现000…00时（表示－2n），属于浮点数下溢。

二、补码加、减运算规则

1、运算规则

[X＋Y]补= [X]补＋ [Y]补

[X－Y]补= [X]补＋ [－Y]补

若已知[Y]补，求[－Y]补的方法是：将[Y]补的各位（包括符号位）逐位取反再在最低位加1即可。

例如：[Y]补= 101101 [－Y]补= 010011

2、溢出判断，一般用双符号位进行判断：

符号位00 表示正数 11 表示负数

结果的符号位为01时，称为上溢；为10时，称为下溢

例题：设x=0.1101，y=－0.0111，符号位为双符号位

用补码求x+y，x－y

[x]补+[y]补=00 1101+11 1001=00 0110

[x－y]补=[x]补+[－y]补=00 1101+00 0111=01 0100

结果错误，正溢出

三、原码一位乘的实现：

设X=0.1101，Y=－0. 1011，求X*Y

解：符号位单独处理， x符＋ y符

数值部分用原码进行一位乘，如下图所示：

高位部分积 低位部分积/乘数说明 0 0 0 0 0 0 1 0 1 1 　起始情况＋） 0 0 1 1 0 1 乘数最低位为1，+X

0 0 1 1 0 1 0 0 0 1 1 0 1 1 0 1 1(丢) 右移部分积和乘数＋） 0 0 1 1 0 1 乘数最低位为1，+X

0 1 0 0 1 1 0 0 1 0 0 1 1 1 1 0 1(丢) 右移部分积和乘数＋） 0 0 0 0 0 0 乘数最低位为0，+0

0 0 1 0 0 1 0 0 0 1 0 0 1 1 1 1 0(丢) 右移部分积和乘数＋） 0 0 1 1 0 1 乘数最低位为1，+X

0 1 0 0 0 1 0 0 1 0 0 0 1 1 1 1 1(丢) 右移部分积和乘数

四、原码一位除的实现：一般用不恢复余数法（加减交替法）部分积低位部分积附加位操作说明 0 0 0 0 0 0 1 0 1 1 　起始情况＋） 0 0 0 0 0 0 乘数最低位为1，+X

0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1(丢) 右移部分积和乘数＋） 1 1 0 0 1 1 乘数最低位为1，+X

0 1 0 0 1 1 0 0 1 0 0 1 1 1 1 0 1(丢) 右移部分积和乘数＋） 0 0 0 0 0 0 乘数最低位为0，+0

0 0 1 0 0 1 0 0 0 1 0 0 1 1 1 1 0(丢) 右移部分积和乘数＋） 0 0 1 1 0 1 乘数最低位为1，+X

0 1 0 0 0 1 0 0 1 0 0 0 1 1 1 1 1(丢) 右移部分积和乘数

§2.5 浮点运算与浮点运算器[!--empirenews.page--]

一、浮点数的运算规则

1、浮点加减法的运算步骤

设两个浮点数 X=Mx※2Ex Y=My※2Ey

实现X±Y要用如下5步完成：

①对阶操作：小阶向大阶看齐

②进行尾数加减运算

③规格化处理：尾数进行运算的结果必须变成规格化的浮点数，对于双符号位的补码尾数来说，就必须是

001×××…×× 或110×××…××的形式

若不符合上述形式要进行左规或右规处理。

④舍入操作：在执行对阶或右规操作时常用“0”舍“1”入法将右移出去的尾数数值进行舍入，以确保精度。

⑤判结果的正确性：即检查阶码是否溢出

若阶码下溢（移码表示是00…0），要置结果为机器0；

若阶码上溢（超过了阶码表示的最大值）置溢出标志。

例题：假定X=0 .0110011*211，Y=0.1101101*2-10（此处的数均为二进制） ?? 计算X+Y；

解：[X]浮： 0 1 010 1100110

[Y]浮： 0 0 110 1101101

符号位阶码尾数

第一步：求阶差： │ΔE│=|1010-0110|=0100

第二步：对阶：Y的阶码小， Y的尾数右移4位

[Y]浮变为 0 1 010 0000110 1101暂时保存

第三步：尾数相加，采用双符号位的补码运算

00 1100110

+00 0000110

00 1101100

第四步规格化：满足规格化要求

第五步：舍入处理，采用0舍1入法处理

故最终运算结果的浮点数格式为： 0 1 010 1101101，

即X+Y=+0. 1101101*210

2、浮点乘除法的运算步骤

①阶码运算：阶码求和（乘法）或阶码求差（除法）

即 [Ex+Ey]移= [Ex]移+ [Ey]补

[Ex－Ey]移= [Ex]移+ [－Ey]补

②浮点数的尾数处理：浮点数中尾数乘除法运算结果要进行舍入处理

例题：X=0 .0110011*211，Y=0.1101101*2-10

求X※Y

解：[X]浮： 0 1 010 1100110

[Y]浮： 0 0 110 1101101

第一步：阶码相加

[Ex+Ey]移=[Ex]移+[Ey]补=1 010+1 110=1 000

1 000为移码表示的0

第二步：原码尾数相乘的结果为：

0 10101101101110

第三步：规格化处理：已满足规格化要求，不需左规，尾数不变，阶码不变。

第四步：舍入处理：按舍入规则，加1进行修正

所以 X※Y= 0.1010111※2+000


反码

目录[隐藏]

概述

详细释义

[编辑本段]

概述

    在计算机内，定点数有3种表示法：原码、反码和补码。

[编辑本段]

详细释义

    所谓原码就是前面所介绍的二进制定点表示法，即最高位为符号位，“0”表示正，“1”表示负，其余位表示数值的大小。

    反码表示法规定：正数的反码与其原码相同；负数的反码是对其原码逐位取反，但符号位除外。

    补码表示法规定：正数的补码与其原码相同；负数的补码是在其反码的末位加1。

    1、原码、反码和补码的表示方法

    （1）原码：在数值前直接加一符号位的表示法。

    例如：符号位数值位

    [+7]原= 0 0000111 B

    [-7]原= 1 0000111 B

    注意：a. 数0的原码有两种形式：

    [+0]原=00000000B [-0]原=10000000B

    b. 8位二进制原码的表示范围：-127～+127

    （2）反码：

    正数：正数的反码与原码相同。

    负数：负数的反码，符号位为“1”，数值部分按位取反。

    例如：符号位数值位

    [+7]反= 0 0000111 B

    [-7]反= 1 1111000 B

    注意：a. 数0的反码也有两种形式，即

    [+0]反=00000000B

    [- 0]反=11111111B

    b. 8位二进制反码的表示范围：-127～+127

    （3）补码的表示方法

    1）模的概念：把一个计量单位称之为模或模数。例如，时钟是以12进制进行计数循环的，即以12为模。在时钟上，时针加上（正拨）12的整数位或减去（反拨）12的整数位，时针的位置不变。14点钟在舍去模12后，成为（下午）2点钟（14=14-12=2）。从0点出发逆时针拨10格即减去10小时，也可看成从0点出发顺时针拨2格（加上2小时），即2点（0-10=-10=-10+12=2）。因此，在模12的前提下，-10可映射为+2。由此可见，对于一个模数为12的循环系统来说，加2和减10的效果是一样的；因此，在以12为模的系统中，凡是减10的运算都可以用加2来代替，这就把减法问题转化成加法问题了（注：计算机的硬件结构中只有加法器，所以大部分的运算都必须最终转换为加法）。10和2对模12而言互为补数。

    同理，计算机的运算部件与寄存器都有一定字长的限制（假设字长为8），因此它的运算也是一种模运算。当计数器计满8位也就是256个数后会产生溢出，又从头开始计数。产生溢出的量就是计数器的模，显然，8位二进制数，它的模数为28=256。在计算中，两个互补的数称为“补码”。

    2）补码的表示：

    正数：正数的补码和原码相同。

    负数：负数的补码则是符号位为“1”，数值部分按位取反后再在末位（最低位）加1。也就是“反码+1”。

    例如：符号位数值位

    [+7]补= 0 0000111 B

    [-7]补= 1 1111001 B

    补码在微型机中是一种重要的编码形式，请注意：

    a. 采用补码后，可以方便地将减法运算转化成加法运算，运算过程得到简化。正数的补码即是它所表示的数的真值，而负数的补码的数值部份却不是它所表示的数的真值。采用补码进行运算，所得结果仍为补码。

    b. 与原码、反码不同，数值0的补码只有一个，即 [0]补=00000000B。

    c. 若字长为8位，则补码所表示的范围为-128～+127；进行补码运算时，应注意所得结果不应超过补码所能表示数的范围。

    2．原码、反码和补码之间的转换

    由于正数的原码、补码、反码表示方法均相同，不需转换。

    在此，仅以负数情况分析。

    （1）已知原码，求补码。

    例：已知某数X的原码为10110100B，试求X的补码和反码。

    解：由[X]原=10110100B知，X为负数。求其反码时，符号位不变，数值部分按位求反；求其补码时，再在其反码的末位加1。

    1 0 1 1 0 1 0 0 原码

    1 1 0 0 1 0 1 1 反码，符号位不变，数值位取反

    1 +1

    1 1 0 0 1 1 0 0 补码

    故：[X]补=11001100B，[X]反=11001011B。

    （2）已知补码，求原码。

    分析：按照求负数补码的逆过程，数值部分应是最低位减1，然后取反。但是对二进制数来说，先减1后取反和先取反后加1得到的结果是一样的，故仍可采用取反加1 有方法。

    例：已知某数X的补码11101110B，试求其原码。

    解：由[X]补=11101110B知，X为负数。求其原码表示时，符号位不变，数值部分按位求反，再在末位加1。

    1 1 1 0 1 1 1 0 补码

    1 0 0 1 0 0 0 1 符号位不变，数值位取反

    1 +1

    1 0 0 1 0 0 1 0 原码

    1．3．2 有符号数运算时的溢出问题

    请大家来做两个题目：

    两正数相加怎么变成了负数？？？

    1）（+72）+（+98）=？

    0 1 0 0 1 0 0 0 B +72

    + 0 1 1 0 0 0 1 0 B +98

    1 0 1 0 1 0 1 0 B -42

    两负数相加怎么会得出正数？？？

    2）（-83）+（-80）=？

    1 0 1 0 1 1 0 1 B -83

    + 1 0 1 1 0 0 0 0 B -80

    0 1 0 1 1 1 0 1 B +93

    思考：这两个题目，按照正常的法则来运算，但结果显然不正确，这是怎么回事呢？

    答案：这是因为发生了溢出。

    如果计算机的字长为n位，n位二进制数的最高位为符号位，其余n-1位为数值位，采用补码表示法时，可表示的数X的范围是 -2n-1≤X≤2n-1-1

    当n=8时，可表示的有符号数的范围为-128～+127。两个有符号数进行加法运算时，如果运算结果超出可表示的有符号数的范围时，就会发生溢出，使计算结果出错。很显然，溢出只能出现在两个同符号数相加或两个异符号数相减的情况下。

    对于加法运算，如果次高位（数值部分最高位）形成进位加入最高位，而最高位（符号位）相加（包括次高位的进位）却没有进位输出时，或者反过来，次高位没有进位加入最高位，但最高位却有进位输出时，都将发生溢出。因为这两种情况是：两个正数相加，结果超出了范围，形式上变成了负数；两负数相加，结果超出了范围，形式上变成了正数。

    而对于减法运算，当次高位不需从最高位借位，但最高位却需借位（正数减负数，差超出范围），或者反过来，次高位需从最高位借位，但最高位不需借位（负数减正数，差超出范围），也会出现溢出。

    在计算机中，数据是以补码的形式存储的，所以补码在c语言的教学中有比较重要的地位，而讲解补码必须涉及到原码、反码。本部分演示作何一个整数的原码、反码、补码。过程与结果显示在列表框中，结果比较少，不必自动清除，而过程是相同的，没有必要清除。故需设清除各部分及清除全部的按钮。测试时注意最大、最小正负数。用户使用时注意讲解不会溢出：当有一个数的反码的全部位是1才会溢出，那么它的原码是10000...，它不是负数，故不会溢出。

    在n位的机器数中，最高位为符号位，该位为零表示为正，为一表示为负；其余n-1位为数值位，各位的值可为零或一。当真值为正时，原码、反码、补码数值位完全相同；当真值为负时，原码的数值位保持原样，反码的数值位是原码数值位的各位取反，补码则是反码的最低位加一。注意符号位不变。

    总结：提示信息不要太少，可“某某数的反码是某某”，而不是只显示数值。

    1.原码的求法:(1)对于正数,转化为二进制数,在最前面添加一符号位(这是规定的),用1表示负数,二表示正数.如:0000 0000是一个字节,其中0为符号位,表示是正数,其它七位表示二进制的值.其实,机器不管这些,什么符号位还是值,机器统统看作是值来计算. 正数的原码、反码、补码是同一个数!

    (2)对于负数,转化为二进制数,前面符号位为1.表示是负数.

    计算原码只要在转化的二进制数前面加上相应的符号位就行了.

    2.反码的求法:对于负数,将原码各位取反,符号位不变.

    3.补码的求法:对于负数,将反码加上二进制的1即可,也就是反码在最后一位上加上1就是补码了.


1

1. 怎么高效获得某个字节的高4位或低4位对应的BCD呢？我用移位的方法，对高4位，我逻辑右移4位后处理；对低4位我先逻辑左移4位再逻辑右移动4位。虽然可以实现，但我觉得不太好，有没有别的方法呢？首次使用VB，请各位给予解答！

答： loNibble = (val and &HF);

hiNibble = (val and &HF0) / 16;

2. 直接取出指定字节(Byte)的某一位(Bit)的值,结果只会是0或1,也就是想知道这个位上是0还是1,大家有什么优的算法,不妨帖出来供大家研究和学习!
我这里先帖出我的算法，经测试，第一种方法比第二种方法在速度上要快一半有多，很明显，第一种方法，用的是常量，所以少了计算步骤，而第二种方法还要先计算幂，所以就慢了许多，但从代码量上来讲，第二种方法较简捷。
Option Explicit
Dim byts(0 To 255) As Byte

Private Sub Command1_Click()
Dim ts As Single, td As Single
Dim i As Integer, j As Long, k As Integer, b As Byte
ts = Timer
For j = 0 To 1000
For i = 0 To 255
' Debug.Print byts(i),
For k = 7 To 0 Step -1
' Debug.Print ByteBit(byts(i), k),
' Debug.Print ByteBitEx(byts(i), k),
' b = ByteBit(byts(i), k)
b = ByteBitEx(byts(i), k)
Next
' Debug.Print "================================================"
Next
Next
td = Timer
Text1.Text = ts
Text2.Text = td
Text3.Text = td - ts
Debug.Print Now, td - ts
End Sub

Private Sub Form_Load()
Dim i As Integer
For i = 0 To 255
byts(i) = i
Next
End Sub

'取字节的某一位
Private Function ByteBit(ByVal bytValue As Byte, ByVal intBitIndex As Integer) As Byte
Select Case intBitIndex
Case 0
ByteBit = bytValue And &H1
Case 1
ByteBit = (bytValue And &H2) \ &H2
Case 2
ByteBit = (bytValue And &H4) \ &H4
Case 3
ByteBit = (bytValue And &H8) \ &H8
Case 4
ByteBit = (bytValue And &H10) \ &H10
Case 5
ByteBit = (bytValue And &H20) \ &H20
Case 6
ByteBit = (bytValue And &H40) \ &H40
Case 7
ByteBit = (bytValue And &H80) \ &H80
End Select
End Function

'取字节的某一位
Private Function ByteBitEx(ByVal bytValue As Byte, ByVal intBitIndex As Integer) As Byte
Select Case intBitIndex
Case 0 To 7
ByteBitEx = (bytValue And 2 ^ intBitIndex) \ 2 ^ intBitIndex
End Select
End Function

以下是测试结果:
调用ByteBit
07-04-12 01:32:27 2.015625
07-04-12 01:32:31 2.202637
07-04-12 01:32:34 2
07-04-12 01:32:37 2.016113
07-04-12 01:32:40 2
07-04-12 01:32:43 2.015625
07-04-12 01:32:46 2
07-04-12 01:32:49 2.016113
07-04-12 01:32:55 1.983887
07-04-12 01:32:59 1.984863
07-04-12 01:33:03 2
07-04-12 01:33:06 2.015137
=====================================
调用ByteBitEx
07-04-12 01:34:41 4.219238
07-04-12 01:34:47 4.219238
07-04-12 01:34:52 4.203125
07-04-12 01:34:57 4.203125
07-04-12 01:35:02 4.21875
07-04-12 01:35:07 4.202637
07-04-12 01:35:11 4.219238
07-04-12 01:35:15 4.203125
07-04-12 01:35:19 4.219238
07-04-12 01:35:23 4.21875
07-04-12 01:35:28 4.219238


字节顺序

    字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序，通常有小端、大端两种字节顺序。小端字节序指低字节数据存放在内存低地址处，高字节数据存放在内存高地址处；大端字节序是高字节数据存放在低地址处，低字节数据存放在高地址处。基于X86平台的PC机是小端字节序的，而有的嵌入式平台则是大端字节序的。因而对int、uint16、uint32等多于1字节类型的数据，在这些嵌入式平台上应该变换其存储顺序。通常我们认为，在空中传输的字节的顺序即网络字节序为标准顺序，考虑到与协议的一致以及与同类其它平台产品的互通，在程序中发数据包时，将主机字节序转换为网络字节序，收数据包处将网络字节序转换为主机字节序。

    在本LINUX的书里介绍到INTEL的CPU使用的小端字节序其他比MOTOROLA

    68000系列CPU使用的是大端字节序如果不转换将数据通过网络发出时比如MOTOROLA发一个16位数据：0X1234 传送到INTEL时

    就被INTEL解释为0X3412 也就是4660成了13330 所以有时候需要一些函数来进行大小端字节序的转换

    关于这大小字节序的概念不是很想的明白数据在

    内存里是具体怎么存放的形式？为什么会有CPU解释的不同？数据不是按12345678……这样的顺序一直排列的么？

    如: 一个多字节值 0xFECDBA98,内存从地址100开始存放

    降序: FE | CD | BA | 98---->;对应地址100 | 101 | 102 | 103

    升序: 98 | BA | CD | FE ---->;same above

    注意,我们的书写表示法是从低字节位--->;高字节位

    内存地址生长方向为: 从左到右由低到高(这是不变的)

    数据为: 0x89ABCDEF

    降序(Big-endian)大端字节序存储时由左到右

    升序(Little-endian)小端字节序存储时由右向左

    可以自己编一个小程序验证一下(用C的数组)

    更简单的调用VC里的checkEndian()

    Intel处理器的字节顺序是和DEC VAX处理器的字节顺序一致的。因此它与68000型处理器以及Internet的顺序是不同的，所以用户在使用时要特别小心以保证正确的顺序。

    任何从Windows Sockets函数对IP地址和端口号的引用和传送给Windows Sockets函数的IP地址和端口号均是按照网络顺序组织的，这也包括了sockaddr_in结构这一数据类型中的IP地址域和端口域（但不包括sin_family域）。

    考虑到一个应用程序通常用与“时间”服务对应的端口来和服务器连接，而服务器提供某种机制来通知用户使用另一端口。因此getservbyname()函数返回的端口号已经是网络顺序了，可以直接用来组成一个地址，而不需要进行转换。然而如果用户输入一个数，而且指定使用这一端口号，应用程序则必须在使用它建立地址以前，把它从主机顺序转换成网络顺序（使用htons()函数）。相应地，如果应用程序希望显示包含于某一地址中的端口号（例如从getpeername()函数中返回的），这一端口号就必须在被显示前从网络顺序转换到主机顺序（使用ntohs()函数）。

    由于Intel处理器和Internet的字节顺序是不同的，上述的转换是无法避免的，应用程序的编写者应该使用作为Windows Sockets API一部分的标准的转换函数，而不要使用自己的转换函数代码。因为将来的Windows Sockets实现有可能在主机字节顺序与网络字节顺序相同的机器上运行。因此只有使用标准的转换函数的应用程序是可移植的。

字节序

    BIG-ENDIAN（高字节放低地址）

    LITTLE-ENDIAN（高字节放高地址）

    主机字节序

    网络字节顺序

    JAVA字节序

    1．BIG-ENDIAN、LITTLE-ENDIAN跟多字节类型的数据有关的比如int,short,long型，而对单字节数据byte却没有影响。

    比如 int a = 0x05060708

    在BIG-ENDIAN的情况下存放为：

    字节号 0 1 2 3

    数据 05 06 07 08

    在LITTLE-ENDIAN的情况下存放为：

    字节号 0 1 2 3

    数据 08 07 06 05

    2．BIG-ENDIAN、LITTLE-ENDIAN、跟CPU有关的，每一种CPU不是BIG-ENDIAN就是LITTLE-ENDIAN、。IA架构的CPU中是Little-Endian，而PowerPC 、SPARC和Motorola处理器。这其实就是所谓的主机字节序。而网络字节序是指数据在网络上传输时是大头还是小头的，在Internet的网络字节序是BIG-ENDIAN。所谓的JAVA字节序指的是在JAVA虚拟机中多字节类型数据的存放顺序，JAVA字节序也是BIG-ENDIAN。

    3．所以在用C/C++写通信程序时，在发送数据前务必用htonl和htons去把整型和短整型的数据进行从主机字节序到网络字节序的转换，而接收数据后对于整型和短整型数据则必须调用ntohl和ntohs实现从网络字节序到主机字节序的转换。如果通信的一方是JAVA程序、一方是C/C++程序时，则需要在C/C++一侧使用以上几个方法进行字节序的转换，而JAVA一侧，则不需要做任何处理，因为JAVA字节序与网络字节序都是BIG-ENDIAN，只要C/C++一侧能正确进行转换即可（发送前从主机序到网络序，接收时反变换）。如果通信的双方都是JAVA，则根本不用考虑字节序的问题了。

    4．如果网络上全部是PowerPC,SPARC和Motorola CPU的主机那么不会出现任何问题，但由于实际存在大量的IA架构的CPU，所以经常出现数据传输错误。

    5．文章开头所提出的问题，就是因为程序运行在X86架构的PC SERVER上，发送数据的一端用C实现的，接收一端是用JAVA实现的，而发送端在发送数据前未进行从主机字节序到网络字节序的转换，这样接收端接收到的是LITTLE-ENDIAN的数据，数据解释自然出错。

    具体数据如下，实际发送的数据为23578

    发送端发送数据： 1A 5C

    接收端接收到数据后，按BIG-ENDIAN进行解释具体数据是多少？你们自己去计算并比较吧！

    ===============================================================================================

    Big Endian and Little Endian

    谈到字节序的问题，必然牵涉到两大CPU派系。那就是Motorola的PowerPC系列CPU和Intel的x86系列CPU。PowerPC系列采用big endian方式存储数据，而x86系列则采用little endian方式存储数据。那么究竟什么是big endian，什么又是little endian呢？

    其实big endian是指低地址存放最高有效字节（MSB），而little endian则是低地址存放最低有效字节（LSB），即常说的低位在先，高位在后。

    用文字说明可能比较抽象，下面用图像加以说明。比如数字0x12345678在两种不同字节序CPU中的存储顺序如下所示：

    Big Endian

    低地址高地址

    ----------------------------------------->

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    | 12 | 34 | 56 | 78 |

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    Little Endian

    低地址高地址

    ----------------------------------------->

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    | 78 | 56 | 34 | 12 |

    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    从上面两图可以看出，采用big endian方式存储数据是符合我们人类的思维习惯的。而little endian，!@#$%^&*，见鬼去吧 -_-|||

    为什么要注意字节序的问题呢？你可能这么问。当然，如果你写的程序只在单机环境下面运行，并且不和别人的程序打交道，那么你完全可以忽略字节序的存在。但是，如果你的程序要跟别人的程序产生交互呢？尤其是当你把你在微机上运算的结果运用到计算机群上去的话。在这里我想说说两种语言。C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的，而JAVA编写的程序则唯一采用big endian方式来存储数据。试想，如果你用C/C++语言在x86平台下编写的程序跟别人的JAVA程序互通时会产生什么结果？就拿上面的 0x12345678来说，你的程序传递给别人的一个数据，将指向0x12345678的指针传给了JAVA程序，由于JAVA采取big endian方式存储数据，很自然的它会将你的数据翻译为0x78563412。什么？竟然变成另外一个数字了？是的，就是这种后果。因此，在你的C程序传给JAVA程序之前有必要进行字节序的转换工作。

    无独有偶，所有网络协议也都是采用big endian的方式来传输数据的。所以有时我们也会把big endian方式称之为网络字节序。当两台采用不同字节序的主机通信时，在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。ANSI C中提供了四个转换字节序的宏。

    ========================================================================================================

    /**

    * 通信格式转换

    *

    * Java和一些windows编程语言如c、c++、delphi所写的网络程序进行通讯时，需要进行相应的转换

    * 高、低字节之间的转换

    * windows的字节序为低字节开头

    * linux,unix的字节序为高字节开头

    * java则无论平台变化，都是高字节开头

    */

    public class FormatTransfer {

    /**

    * 将int转为低字节在前，高字节在后的byte数组

    * @param n int

    * @return byte[]

    */

    public static byte[] toLH(int n) {

    byte[] b = new byte[4];

    b[0] = (byte) (n & 0xff);

    b[1] = (byte) (n >> 8 & 0xff);

    b[2] = (byte) (n >> 16 & 0xff);

    b[3] = (byte) (n >> 24 & 0xff);

    return b;

    }

    /**

    * 将int转为高字节在前，低字节在后的byte数组

    * @param n int

    * @return byte[]

    */

    public static byte[] toHH(int n) {

    byte[] b = new byte[4];

    b[3] = (byte) (n & 0xff);

    b[2] = (byte) (n >> 8 & 0xff);

    b[1] = (byte) (n >> 16 & 0xff);

    b[0] = (byte) (n >> 24 & 0xff);

    return b;

    }

    /**

    * 将short转为低字节在前，高字节在后的byte数组

    * @param n short

    * @return byte[]

    */

    public static byte[] toLH(short n) {

    byte[] b = new byte[2];

    b[0] = (byte) (n & 0xff);

    b[1] = (byte) (n >> 8 & 0xff);

    return b;

    }

    /**

    * 将short转为高字节在前，低字节在后的byte数组

    * @param n short

    * @return byte[]

    */

    public static byte[] toHH(short n) {

    byte[] b = new byte[2];

    b[1] = (byte) (n & 0xff);

    b[0] = (byte) (n >> 8 & 0xff);

    return b;

    }

    /**

    * 将将int转为高字节在前，低字节在后的byte数组

    public static byte[] toHH(int number) {

    int temp = number;

    byte[] b = new byte[4];

    for (int i = b.length - 1; i > -1; i--) {

    b = new Integer(temp & 0xff).byteValue();

    temp = temp >> 8;

    }

    return b;

    }

    public static byte[] IntToByteArray(int i) {

    byte[] abyte0 = new byte[4];

    abyte0[3] = (byte) (0xff & i);

    abyte0[2] = (byte) ((0xff00 & i) >> 8);

    abyte0[1] = (byte) ((0xff0000 & i) >> 16);

    abyte0[0] = (byte) ((0xff000000 & i) >> 24);

    return abyte0;

    }

    */

    /**

    * 将float转为低字节在前，高字节在后的byte数组

    */

    public static byte[] toLH(float f) {

    return toLH(Float.floatToRawIntBits(f));

    }

    /**

    * 将float转为高字节在前，低字节在后的byte数组

    */

    public static byte[] toHH(float f) {

    return toHH(Float.floatToRawIntBits(f));

    }

    /**

    * 将String转为byte数组

    */

    public static byte[] stringToBytes(String s, int length) {

    while (s.getBytes().length < length) {

    s += " ";

    }

    return s.getBytes();

    }

    /**

    * 将字节数组转换为String

    * @param b byte[]

    * @return String

    */

    public static String bytesToString(byte[] b) {

    StringBuffer result = new StringBuffer("");

    int length = b.length;

    for (int i=0; i<length; i++) {

    result.append((char)(b & 0xff));

    }

    return result.toString();

    }

    /**

    * 将字符串转换为byte数组

    * @param s String

    * @return byte[]

    */

    public static byte[] stringToBytes(String s) {

    return s.getBytes();

    }

    /**

    * 将高字节数组转换为int

    * @param b byte[]

    * @return int

    */

    public static int hBytesToInt(byte[] b) {

    int s = 0;

    for (int i = 0; i < 3; i++) {

    if (b >= 0) {

    s = s + b;

    } else {

    s = s + 256 + b;

    }

    s = s * 256;

    }

    if (b[3] >= 0) {

    s = s + b[3];

    } else {

    s = s + 256 + b[3];

    }

    return s;

    }

    /**

    * 将低字节数组转换为int

    * @param b byte[]

    * @return int

    */

    public static int lBytesToInt(byte[] b) {

    int s = 0;

    for (int i = 0; i < 3; i++) {

    if (b[3-i] >= 0) {

    s = s + b[3-i];

    } else {

    s = s + 256 + b[3-i];

    }

    s = s * 256;

    }

    if (b[0] >= 0) {

    s = s + b[0];

    } else {

    s = s + 256 + b[0];

    }

    return s;

    }

    /**

    * 高字节数组到short的转换

    * @param b byte[]

    * @return short

    */

    public static short hBytesToShort(byte[] b) {

    int s = 0;

    if (b[0] >= 0) {

    s = s + b[0];

    } else {

    s = s + 256 + b[0];

    }

    s = s * 256;

    if (b[1] >= 0) {

    s = s + b[1];

    } else {

    s = s + 256 + b[1];

    }

    short result = (short)s;

    return result;

    }

    /**

    * 低字节数组到short的转换

    * @param b byte[]

    * @return short

    */

    public static short lBytesToShort(byte[] b) {

    int s = 0;

    if (b[1] >= 0) {

    s = s + b[1];

    } else {

    s = s + 256 + b[1];

    }

    s = s * 256;

    if (b[0] >= 0) {

    s = s + b[0];

    } else {

    s = s + 256 + b[0];

    }

    short result = (short)s;

    return result;

    }

    /**

    * 高字节数组转换为float

    * @param b byte[]

    * @return float

    */

    public static float hBytesToFloat(byte[] b) {

    int i = 0;

    Float F = new Float(0.0);

    i = ((((b[0]&0xff)<<8 | (b[1]&0xff))<<8) | (b[2]&0xff))<<8 | (b[3]&0xff);

    return F.intBitsToFloat(i);

    }

    /**

    * 低字节数组转换为float

    * @param b byte[]

    * @return float

    */

    public static float lBytesToFloat(byte[] b) {

    int i = 0;

    Float F = new Float(0.0);

    i = ((((b[3]&0xff)<<8 | (b[2]&0xff))<<8) | (b[1]&0xff))<<8 | (b[0]&0xff);

    return F.intBitsToFloat(i);

    }

    /**

    * 将byte数组中的元素倒序排列

    */

    public static byte[] bytesReverseOrder(byte[] b) {

    int length = b.length;

    byte[] result = new byte[length];

    for(int i=0; i<length; i++) {

    result[length-i-1] = b;

    }

    return result;

    }

    /**

    * 打印byte数组

    */

    public static void printBytes(byte[] bb) {

    int length = bb.length;

    for (int i=0; i<length; i++) {

    System.out.print(bb + " ");

    }

    System.out.println("");

    }

    public static void logBytes(byte[] bb) {

    int length = bb.length;

    String ut = "";

    for (int i=0; i<length; i++) {

    ut = out + bb + " ";

    }

    }

    /**

    * 将int类型的值转换为字节序颠倒过来对应的int值

    * @param i int

    * @return int

    */

    public static int reverseInt(int i) {

    int result = FormatTransfer.hBytesToInt(FormatTransfer.toLH(i));

    return result;

    }

    /**

    * 将short类型的值转换为字节序颠倒过来对应的short值

    * @param s short

    * @return short

    */

    public static short reverseShort(short s) {

    short result = FormatTransfer.hBytesToShort(FormatTransfer.toLH(s));

    return result;

    }

    /**

    * 将float类型的值转换为字节序颠倒过来对应的float值

    * @param f float

    * @return float

    */

    public static float reverseFloat(float f) {

    float result = FormatTransfer.hBytesToFloat(FormatTransfer.toLH(f));

    return result;

    }

    }[1]


联合数据类型

    结构与联合

    结构类型定义和结构变量说明

    在实际问题中，一组数据往往具有不同的数据类型。例如，在学生登记表中，姓名应为字符型；学号可为整型或字符型；年龄应为整型；性别应为字符型；成绩可为整型或实型。显然不能用一个数组来存放这一组数据。因为数组中各元素的类型和长度都必须一致，以便于编译系统处理。为了解决这个问题，Ｃ语言中给出了另一种构造数据类型——“结构”。它相当于其它高级语言中的记录。

    “结构”是一种构造类型，它是由若干“成员”组成的。每一个成员可以是一个基本数据类型或者又是一个构造类型。结构既是一种“构造”而成的数据类型，那么在说明和使用之前必须先定义它，也就是构造它。如同在说明和调用函数之前要先定义函数一样。

    一、结构的定义

    定义一个结构的一般形式为：

    struct 结构名

    {

    成员表列

    };

    成员表由若干个成员组成，每个成员都是该结构的一个组成部分。对每个成员也必须作类型说明，其形式为：

    类型说明符成员名;

    成员名的命名应符合标识符的书写规定。例如：

    struct stu

    {

    int num;

    char name[20];

    char sex;

    float score;

    };

    在这个结构定义中，结构名为stu，该结构由4个成员组成。第一个成员为num，整型变量；第二个成员为name，字符数组；第三个成员为sex，字符变量；第四个成员为score，实型变量。应注意在括号后的分号是不可少的。结构定义之后，即可进行变量说明。凡说明为结构stu的变量都由上述4个成员组成。由此可见，结构是一种复杂的数据类型，是数目固定，类型不同的若干有序变量的集合。

    二、结构类型变量的说明

    说明结构变量有以下三种方法。以上面定义的stu为例来加以说明。

    1. 先定义结构，再说明结构变量。如：

    struct stu

    {

    int num;

    char name[20];

    char sex;

    float score;

    };

    struct stu boy1,boy2;

    说明了两个变量boy1和boy2为stu结构类型。也可以用宏定义使一个符号常量来表示一个结构类型，例如：

    #define STU struct stu

    STU

    {

    int num;

    char name[20];

    char sex;

    float score;

    };

    STU boy1,boy2;

    2. 在定义结构类型的同时说明结构变量。例如：

    struct stu

    {

    int num;

    char name[20];

    char sex;

    float score;

    }boy1,boy2;

    3. 直接说明结构变量。例如：

    struct

    {

    int num;

    char name[20];

    char sex;

    float score;

    }boy1,boy2;

    第三种方法与第二种方法的区别在于第三种方法中省去了结构名，而直接给出结构变量。三种方法中说明的boy1,boy2变量都具有图7.1所示的结构。说明了boy1,boy2变量为stu类型后，即可向这两个变量中的各个成员赋值。在上述stu结构定义中，所有的成员都是基本数据类型或数组类型。成员也可以又是一个结构，即构成了嵌套的结构。例如，图7.2给出了另一个数据结构。按图7.2可给出以下结构定义：

    struct date{

    int month;

    int day;

    int year;

    }

    struct{

    int num;

    char name[20];

    char sex;

    struct date birthday;

    float score;

    }boy1,boy2;

    首先定义一个结构date，由month(月)、day(日)、year(年) 三个成员组成。在定义并说明变量 boy1 和 boy2 时，其中的成员birthday被说明为data结构类型。成员名可与程序中其它变量同名，互不干扰。结构变量成员的表示方法在程序中使用结构变量时，往往不把它作为一个整体来使用。

    在ANSI C中除了允许具有相同类型的结构变量相互赋值以外，一般对结构变量的使用，包括赋值、输入、输出、运算等都是通过结构变量的成员来实现的。

    表示结构变量成员的一般形式是：结构变量名.成员名例如：boy1.num 即第一个人的学号　boy2.sex 即第二个人的性别如果成员本身又是一个结构则必须逐级找到最低级的成员才能使用。例如：boy1.birthday.month 即第一个人出生的月份成员可以在程序中单独使用，与普通变量完全相同。

    结构变量的赋值

    前面已经介绍，结构变量的赋值就是给各成员赋值。可用输入语句或赋值语句来完成。

    [例7.1]给结构变量赋值并输出其值。

    main(){

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    } boy1,boy2;

    boy1.num=102;

    boy1.name="Zhang ping";

    printf("input sex and score\n");

    scanf("%c %f",&boy1.sex,&boy1.score);

    boy2=boy1;

    printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

    printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

    }

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy1,boy2;

    boy1.num=102;

    boy1.name="Zhang ping";

    printf("input sex and score\n");

    scanf("%c %f",&boy1.sex,&boy1.score);

    boy2=boy1;

    printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

    printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

    本程序中用赋值语句给num和name两个成员赋值，name是一个字符串指针变量。用scanf函数动态地输入sex和score成员值，然后把 boy1的所有成员的值整体赋予boy2。最后分别输出boy2 的各个成员值。本例表示了结构变量的赋值、输入和输出的方法。

    结构变量的初始化

    如果结构变量是全局变量或为静态变量，则可对它作初始化赋值。对局部或自动结构变量不能作初始化赋值。

    [例7.2]外部结构变量初始化。

    struct stu /*定义结构*/

    {

    int num;

    char *name;

    char sex;

    float score;

    } boy2,boy1={102,"Zhang ping",'M',78.5};

    main()

    {

    boy2=boy1;

    printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

    printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

    }

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy2,boy1={102,"Zhang ping",'M',78.5};

    main()

    {

    boy2=boy1;

    ……

    }

    本例中，boy2,boy1均被定义为外部结构变量，并对boy1作了初始化赋值。在main函数中，把boy1的值整体赋予boy2，然后用两个printf语句输出boy2各成员的值。

    [例7.3]静态结构变量初始化。

    main()

    {

    static struct stu /*定义静态结构变量*/

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy2,boy1={102,"Zhang ping",'M',78.5};

    boy2=boy1;

    printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

    printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

    }

    static struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy2,boy1={102,"Zhang ping",'M',78.5};

    本例是把boy1，boy2都定义为静态局部的结构变量，同样可以作初始化赋值。

    结构数组

    数组的元素也可以是结构类型的。因此可以构成结构型数组。结构数组的每一个元素都是具有相同结构类型的下标结构变量。在实际应用中，经常用结构数组来表示具有相同数据结构的一个群体。如一个班的学生档案，一个车间职工的工资表等。

    结构数组的定义方法和结构变量相似，只需说明它为数组类型即可。例如：

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy[5];

    定义了一个结构数组boy1，共有5个元素，boy[0]～boy[4]。每个数组元素都具有struct stu的结构形式。对外部结构数组或静态结构数组可以作初始化赋值，例如：

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy[5]={

    {101,"Li ping","M",45},

    {102,"Zhang ping","M",62.5},

    {103,"He fang","F",92.5},

    {104,"Cheng ling","F",87},

    {105,"Wang ming","M",58};

    }

    当对全部元素作初始化赋值时，也可不给出数组长度。

    [例7.4]计算学生的平均成绩和不及格的人数。

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy[5]={

    {101,"Li ping",'M',45},

    {102,"Zhang ping",'M',62.5},

    {103,"He fang",'F',92.5},

    {104,"Cheng ling",'F',87},

    {105,"Wang ming",'M',58},

    };

    main()

    {

    int i,c=0;

    float ave,s=0;

    for(i=0;i<5;i++)

    {

    s+=boy.score;

    if(boy.score<60) c+=1;

    }

    printf("s=%f\n",s);

    ave=s/5;

    printf("average=%f\ncount=%d\n",ave,c);

    }

    本例程序中定义了一个外部结构数组boy，共5个元素，并作了初始化赋值。在main函数中用for语句逐个累加各元素的score 成员值存于s之中，如score的值小于60(不及格)即计数器C加1，循环完毕后计算平均成绩，并输出全班总分，平均分及不及格人数。

    [例7.5]建立同学通讯录

    #include"stdio.h"

    #define NUM 3

    struct mem

    {

    char name[20];

    char phone[10];

    };

    main()

    {

    struct mem man[NUM];

    int i;

    for(i=0;i<NUM;i++)

    {

    printf("input name:\n");

    gets(man.name);

    printf("input phone:\n");

    gets(man.phone);

    }

    printf("name\t\t\tphone\n\n");

    for(i=0;i<NUM;i++)

    printf("%s\t\t\t%s\n",man.name,man.phone);

    }

    本程序中定义了一个结构mem，它有两个成员name和phone 用来表示姓名和电话号码。在主函数中定义man为具有mem 类型的结构数组。在for语句中，用gets函数分别输入各个元素中两个成员的值。然后又在for语句中用printf语句输出各元素中两个成员值。

    结构指针变量

    结构指针变量的说明和使用一个指针变量当用来指向一个结构变量时，称之为结构指针变量。

    结构指针变量中的值是所指向的结构变量的首地址。通过结构指针即可访问该结构变量，这与数组指针和函数指针的情况是相同的。结构指针变量说明的一般形式为：

    struct 结构名*结构指针变量名

    例如，在前面的例7.1中定义了stu这个结构，如要说明一个指向stu的指针变量pstu，可写为：

    struct stu *pstu;

    当然也可在定义stu结构时同时说明pstu。与前面讨论的各类指针变量相同，结构指针变量也必须要先赋值后才能使用。赋值是把结构变量的首地址赋予该指针变量，不能把结构名赋予该指针变量。如果boy是被说明为stu类型的结构变量，则： pstu=&boy是正确的，而： pstu=&stu是错误的。

    结构名和结构变量是两个不同的概念，不能混淆。结构名只能表示一个结构形式，编译系统并不对它分配内存空间。只有当某变量被说明为这种类型的结构时，才对该变量分配存储空间。因此上面&stu这种写法是错误的，不可能去取一个结构名的首地址。有了结构指针变量，就能更方便地访问结构变量的各个成员。

    其访问的一般形式为： (*结构指针变量).成员名或为：

    结构指针变量->成员名

    例如： (*pstu).num或者： pstu->num

    应该注意(*pstu)两侧的括号不可少，因为成员符“.”的优先级高于“*”。如去掉括号写作*pstu.num则等效于*(pstu.num)，这样，意义就完全不对了。下面通过例子来说明结构指针变量的具体说明和使用方法。

    [例7.6]

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    } boy1={102,"Zhang ping",'M',78.5},*pstu;

    main()

    {

    pstu=&boy1;

    printf("Number=%d\nName=%s\n",boy1.num,boy1.name);

    printf("Sex=%c\nScore=%f\n\n",boy1.sex,boy1.score);

    printf("Number=%d\nName=%s\n",(*pstu).num,(*pstu).name);

    printf("Sex=%c\nScore=%f\n\n",(*pstu).sex,(*pstu).score);

    printf("Number=%d\nName=%s\n",pstu->num,pstu->name);

    printf("Sex=%c\nScore=%f\n\n",pstu->sex,pstu->score);

    }

    本例程序定义了一个结构stu，定义了stu类型结构变量boy1 并作了初始化赋值，还定义了一个指向stu类型结构的指针变量pstu。在main函数中，pstu被赋予boy1的地址，因此pstu指向boy1 。然后在printf语句内用三种形式输出boy1的各个成员值。从运行结果可以看出：

    结构变量.成员名

    (*结构指针变量).成员名

    结构指针变量->成员名

    这三种用于表示结构成员的形式是完全等效的。结构数组指针变量结构指针变量可以指向一个结构数组，这时结构指针变量的值是整个结构数组的首地址。结构指针变量也可指向结构数组的一个元素，这时结构指针变量的值是该结构数组元素的首地址。设ps为指向结构数组的指针变量，则ps也指向该结构数组的0 号元素，ps+1指向1号元素，ps+i则指向i号元素。这与普通数组的情况是一致的。

    [例7.7]用指针变量输出结构数组。

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    }boy[5]={

    {101,"Zhou ping",'M',45},

    {102,"Zhang ping",'M',62.5},

    {103,"Liou fang",'F',92.5},

    {104,"Cheng ling",'F',87},

    {105,"Wang ming",'M',58},

    };

    main()

    {

    struct stu *ps;

    printf("No\tName\t\t\tSex\tScore\t\n");

    for(ps=boy;ps<boy+5;ps++)

    printf("%d\t%s\t\t%c\t%f\t\n",ps->num,ps->name,ps->sex,ps->

    score);

    }

    在程序中，定义了stu结构类型的外部数组boy 并作了初始化赋值。在main函数内定义ps为指向stu类型的指针。在循环语句for的表达式1中，ps被赋予boy的首地址，然后循环5次，输出 boy数组中各成员值。应该注意的是，一个结构指针变量虽然可以用来访问结构变量或结构数组元素的成员，但是，不能使它指向一个成员。也就是说不允许取一个成员的地址来赋予它。因此，下面的赋值是错误的。 ps=&boy[1].sex;而只能是：ps=boy;(赋予数组首地址)

    或者是：

    ps=&boy[0];(赋予0号元素首地址)

    结构指针变量作函数参数

    在ANSI C标准中允许用结构变量作函数参数进行整体传送。但是这种传送要将全部成员逐个传送，特别是成员为数组时将会使传送的时间和空间开销很大，严重地降低了程序的效率。因此最好的办法就是使用指针，即用指针变量作函数参数进行传送。这时由实参传向形参的只是地址，从而减少了时间和空间的开销。

    [例7.8]题目与例7.4相同，计算一组学生的平均成绩和不及格人数。

    用结构指针变量作函数参数编程。

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;}boy[5]={

    {101,"Li ping",'M',45},

    {102,"Zhang ping",'M',62.5},

    {103,"He fang",'F',92.5},

    {104,"Cheng ling",'F',87},

    {105,"Wang ming",'M',58},

    };

    main()

    {

    struct stu *ps;

    void ave(struct stu *ps);

    ps=boy;

    ave(ps);

    }

    void ave(struct stu *ps)

    {

    int c=0,i;

    float ave,s=0;

    for(i=0;i<5;i++,ps++)

    {

    s+=ps->score;

    if(ps->score<60) c+=1;

    }

    printf("s=%f\n",s);

    ave=s/5;

    printf("average=%f\ncount=%d\n",ave,c);

    }

    本程序中定义了函数ave，其形参为结构指针变量ps。boy 被定义为外部结构数组，因此在整个源程序中有效。在main 函数中定义说明了结构指针变量ps，并把boy的首地址赋予它，使ps指向boy 数组。然后以ps作实参调用函数ave。在函数ave 中完成计算平均成绩和统计不及格人数的工作并输出结果。与例7.4程序相比，由于本程序全部采用指针变量作运算和处理，故速度更快，程序效率更高。.

    topoic=动态存储分配

    在数组一章中，曾介绍过数组的长度是预先定义好的，在整个程序中固定不变。Ｃ语言中不允许动态数组类型。例如： int n;scanf("%d",&n);int a[n]; 用变量表示长度，想对数组的大小作动态说明，这是错误的。但是在实际的编程中，往往会发生这种情况，即所需的内存空间取决于实际输入的数据，而无法预先确定。对于这种问题，用数组的办法很难解决。为了解决上述问题，Ｃ语言提供了一些内存管理函数，这些内存管理函数可以按需要动态地分配内存空间，也可把不再使用的空间回收待用，为有效地利用内存资源提供了手段。常用的内存管理函数有以下三个：

    1.分配内存空间函数malloc

    调用形式： (类型说明符*) malloc (size) 功能：在内存的动态存储区中分配一块长度为"size" 字节的连续区域。函数的返回值为该区域的首地址。 “类型说明符”表示把该区域用于何种数据类型。(类型说明符*)表示把返回值强制转换为该类型指针。“size”是一个无符号数。例如： pc=(char *) malloc (100); 表示分配100个字节的内存空间，并强制转换为字符数组类型，函数的返回值为指向该字符数组的指针，把该指针赋予指针变量pc。

    2.分配内存空间函数 calloc

    calloc 也用于分配内存空间。调用形式： (类型说明符*)calloc(n,size) 功能：在内存动态存储区中分配n块长度为“size”字节的连续区域。函数的返回值为该区域的首地址。(类型说明符*)用于强制类型转换。calloc函数与malloc 函数的区别仅在于一次可以分配n块区域。例如： ps=(struet stu*) calloc(2,sizeof (struct stu)); 其中的sizeof(struct stu)是求stu的结构长度。因此该语句的意思是：按stu的长度分配2块连续区域，强制转换为stu类型，并把其首地址赋予指针变量ps。

    3.释放内存空间函数free

    调用形式： free(void*ptr); 功能：释放ptr所指向的一块内存空间，ptr 是一个任意类型的指针变量，它指向被释放区域的首地址。被释放区应是由malloc或calloc函数所分配的区域：[例7.9]分配一块区域，输入一个学生数据。

    main()

    {

    struct stu

    {

    int num;

    char *name;

    char sex;

    float score;

    } *ps;

    ps=(struct stu*)malloc(sizeof(struct stu));

    ps->num=102;

    ps->name="Zhang ping";

    ps->sex='M';

    ps->score=62.5;

    printf("Number=%d\nName=%s\n",ps->num,ps->name);

    printf("Sex=%c\nScore=%f\n",ps->sex,ps->score);

    free(ps);

    }

    本例中，定义了结构stu，定义了stu类型指针变量ps。然后分配一块stu大内存区，并把首地址赋予ps，使ps指向该区域。再以ps为指向结构的指针变量对各成员赋值，并用printf 输出各成员值。最后用free函数释放ps指向的内存空间。整个程序包含了申请内存空间、使用内存空间、释放内存空间三个步骤，实现存储空间的动态分配。链表的概念在例7.9中采用了动态分配的办法为一个结构分配内存空间。每一次分配一块空间可用来存放一个学生的数据，我们可称之为一个结点。有多少个学生就应该申请分配多少块内存空间，也就是说要建立多少个结点。当然用结构数组也可以完成上述工作，但如果预先不能准确把握学生人数，也就无法确定数组大小。而且当学生留级、退学之后也不能把该元素占用的空间从数组中释放出来。用动态存储的方法可以很好地解决这些问题。有一个学生就分配一个结点，无须预先确定学生的准确人数，某学生退学，可删去该结点，并释放该结点占用的存储空间。从而节约了宝贵的内存资源。另一方面，用数组的方法必须占用一块连续的内存区域。而使用动态分配时，每个结点之间可以是不连续的(结点内是连续的)。结点之间的联系可以用指针实现。即在结点结构中定义一个成员项用来存放下一结点的首地址，这个用于存放地址的成员，常把它称为指针域。可在第一个结点的指针域内存入第二个结点的首地址，在第二个结点的指针域内又存放第三个结点的首地址，如此串连下去直到最后一个结点。最后一个结点因无后续结点连接，其指针域可赋为0。这样一种连接方式，在数据结构中称为“链表”。图7.3为链表的示意图。

    在图7.3中，第0个结点称为头结点，它存放有第一个结点的首地址，它没有数据，只是一个指针变量。以下的每个结点都分为两个域，一个是数据域，存放各种实际的数据，如学号num，姓名name，性别sex和成绩score等。另一个域为指针域，存放下一结点的首地址。链表中的每一个结点都是同一种结构类型。例如，一个存放学生学号和成绩的结点应为以下结构：

    struct stu

    { int num;

    int score;

    struct stu *next;

    }

    前两个成员项组成数据域，后一个成员项next构成指针域，它是一个指向stu类型结构的指针变量。链表的基本操作对链表的主要操作有以下几种：

    1.建立链表；

    2.结构的查找与输出；

    3.插入一个结点；

    4.删除一个结点；

    下面通过例题来说明这些操作。

    [例7.10]建立一个三个结点的链表，存放学生数据。为简单起见，我们假定学生数据结构中只有学号和年龄两项。

    可编写一个建立链表的函数creat。程序如下：

    #define NULL 0

    #define TYPE struct stu

    #define LEN sizeof (struct stu)

    struct stu

    {

    int num;

    int age;

    struct stu *next;

    };

    TYPE *creat(int n)

    {

    struct stu *head,*pf,*pb;

    int i;

    for(i=0;i<n;i++)

    {

    pb=(TYPE*) malloc(LEN);

    printf("input Number and Age\n");

    scanf("%d%d",&pb->num,&pb->age);

    if(i==0)

    pf=head=pb;

    else pf->next=pb;

    pb->next=NULL;

    pf=pb;

    }

    return(head);

    }

    在函数外首先用宏定义对三个符号常量作了定义。这里用TYPE表示struct stu，用LEN表示sizeof(struct stu)主要的目的是为了在以下程序内减少书写并使阅读更加方便。结构stu定义为外部类型，程序中的各个函数均可使用该定义。

    creat函数用于建立一个有n个结点的链表，它是一个指针函数，它返回的指针指向stu结构。在creat函数内定义了三个stu结构的指针变量。 head为头指针，pf 为指向两相邻结点的前一结点的指针变量。pb为后一结点的指针变量。在for语句内，用malloc函数建立长度与stu长度相等的空间作为一结点，首地址赋予pb。然后输入结点数据。如果当前结点为第一结点(i==0)，则把pb值 (该结点指针)赋予head和pf。如非第一结点，则把pb值赋予pf 所指结点的指针域成员next。而pb所指结点为当前的最后结点，其指针域赋NULL。再把pb值赋予pf以作下一次循环准备。

    creat函数的形参n，表示所建链表的结点数，作为for语句的循环次数。图7.4表示了creat函数的执行过程。

    [例7.11]写一个函数，在链表中按学号查找该结点。

    TYPE * search (TYPE *head,int n)

    {

    TYPE *p;

    int i;

    p=head;

    while (p->num!=n && p->next!=NULL)

    p=p->next; /* 不是要找的结点后移一步*/

    if (p->num==n) return (p);

    if (p->num!=n&& p->next==NULL)

    printf ("Node %d has not been found!\n",n

    }

    本函数中使用的符号常量TYPE与例7.10的宏定义相同，等于struct　stu。函数有两个形参，head是指向链表的指针变量，n为要查找的学号。进入while语句，逐个检查结点的num成员是否等于n，如果不等于n且指针域不等于NULL(不是最后结点)则后移一个结点，继续循环。如找到该结点则返回结点指针。如循环结束仍未找到该结点则输出“未找到”的提示信息。

    [例7.12]写一个函数，删除链表中的指定结点。删除一个结点有两种情况：

    1. 被删除结点是第一个结点。这种情况只需使head指向第二个结点即可。即head=pb->next。其过程如图7.5所示。

    2. 被删结点不是第一个结点，这种情况使被删结点的前一结点指向被删结点的后一结点即可。即pf->next=pb->next。其过程如图7.6所示。

    函数编程如下：

    TYPE * delete(TYPE * head,int num)

    {

    TYPE *pf,*pb;

    if(head==NULL) /*如为空表，输出提示信息*/

    { printf("\nempty list!\n");

    goto end;}

    pb=head;

    while (pb->num!=num && pb->next!=NULL)

    /*当不是要删除的结点，而且也不是最后一个结点时，继续循环*/

    {pf=pb;pb=pb->next;}/*pf指向当前结点，pb指向下一结点*/

    if(pb->num==num)

    {if(pb==head) head=pb->next;

    /*如找到被删结点，且为第一结点，则使head指向第二个结点，

    否则使pf所指结点的指针指向下一结点*/

    else pf->next=pb->next;

    free(pb);

    printf("The node is deleted\n");}

    else

    printf("The node not been foud!\n");

    end:

    return head;

    }

    函数有两个形参，head为指向链表第一结点的指针变量，num删结点的学号。首先判断链表是否为空，为空则不可能有被删结点。若不为空，则使pb指针指向链表的第一个结点。进入while语句后逐个查找被删结点。找到被删结点之后再看是否为第一结点，若是则使head指向第二结点(即把第一结点从链中删去)，否则使被删结点的前一结点(pf所指)指向被删结点的后一结点(被删结点的指针域所指)。如若循环结束未找到要删的结点，则输出“末找到”的提示信息。最后返回head值。

    [例7.13]写一个函数，在链表中指定位置插入一个结点。在一个链表的指定位置插入结点，要求链表本身必须是已按某种规律排好序的。例如，在学生数据链表中，要求学号顺序插入一个结点。设被插结点的指针为pi。可在三种不同情况下插入。

    1. 原表是空表，只需使head指向被插结点即可。见图7.7(a)

    2. 被插结点值最小，应插入第一结点之前。这种情况下使head指向被插结点，被插结点的指针域指向原来的第一结点则可。即：pi->next=pb;

    head=pi; 见图7.7(b)

    3. 在其它位置插入，见图7.7(c)。这种情况下，使插入位置的前一结点的指针域指向被插结点，使被插结点的指针域指向插入位置的后一结点。即为：pi->next=pb;pf->next=pi；

    4. 在表末插入，见图7.7(d)。这种情况下使原表末结点指针域指向被插结点，被插结点指针域置为NULL。即：

    pb->next=pi;

    pi->next=NULL； TYPE * insert(TYPE * head,TYPE *pi)

    {

    TYPE *pf,*pb;

    pb=head;

    if(head==NULL) /*空表插入*/

    (head=pi;

    pi->next=NULL;}

    else

    {

    while((pi->num>pb->num)&&(pb->next!=NULL))

    {pf=pb;

    pb=pb->next; }/*找插入位置*/

    if(pi->num<=pb->num)

    {if(head==pb)head=pi;/*在第一结点之前插入*/

    else pf->next=pi;/*在其它位置插入*/

    pi->next=pb; }

    else

    {pb->next=pi;

    pi->next=NULL;} /*在表末插入*/

    }

    return head;}

    本函数有两个形参均为指针变量，head指向链表，pi 指向被插结点。函数中首先判断链表是否为空，为空则使head指向被插结点。表若不空，则用while语句循环查找插入位置。找到之后再判断是否在第一结点之前插入，若是则使head 指向被插结点被插结点指针域指向原第一结点，否则在其它位置插入，若插入的结点大于表中所有结点，则在表末插入。本函数返回一个指针，是链表的头指针。当插入的位置在第一个结点之前时，插入的新结点成为链表的第一个结点，因此head的值也有了改变，故需要把这个指针返回主调函数。

    [例7.14]将以上建立链表，删除结点，插入结点的函数组织在一起，再建一个输出全部结点的函数，然后用main函数调用它们。

    #define NULL 0

    #define TYPE struct stu

    #define LEN sizeof(struct stu)

    struct stu

    {

    int num;

    int age;

    struct stu *next;

    };

    TYPE * creat(int n)

    {

    struct stu *head,*pf,*pb;

    int i;

    for(i=0;i<n;i++)

    {

    pb=(TYPE *)malloc(LEN);

    printf("input Number and Age\n");

    scanf("%d%d",&pb->num,&pb->age);

    if(i==0)

    pf=head=pb;

    else pf->next=pb;

    pb->next=NULL;

    pf=pb;

    }

    return(head);

    }

    TYPE * delete(TYPE * head,int num)

    {

    TYPE *pf,*pb;

    if(head==NULL)

    { printf("\nempty list!\n");

    goto end;}

    pb=head;

    while (pb->num!=num && pb->next!=NULL)

    {pf=pb;pb=pb->next;}

    if(pb->num==num)

    { if(pb==head) head=pb->next;

    else pf->next=pb->next;

    printf("The node is deleted\n"); }

    else

    free(pb);

    printf("The node not been found!\n");

    end:

    return head;

    }

    TYPE * insert(TYPE * head,TYPE * pi)

    {

    TYPE *pb ,*pf;

    pb=head;

    if(head==NULL)

    { head=pi;

    pi->next=NULL; }

    else

    {

    while((pi->num>pb->num)&&(pb->next!=NULL))

    { pf=pb;

    pb=pb->next; }

    if(pi->num<=pb->num)

    { if(head==pb) head=pi;

    else pf->next=pi;

    pi->next=pb; }

    else

    { pb->next=pi;

    pi->next=NULL; }

    }

    return head;

    }

    void print(TYPE * head)

    {

    printf("Number\t\tAge\n");

    while(head!=NULL)

    {

    printf("%d\t\t%d\n",head->num,head->age);

    head=head->next;

    }

    }

    main()

    {

    TYPE * head,*pnum;

    int n,num;

    printf("input number of node: ");

    scanf("%d",&n);

    head=creat(n);

    print(head);

    printf("Input the deleted number: ");

    scanf("%d",&num);

    head=delete(head,num);

    print(head);

    printf("Input the inserted number and age: ");

    pnum=(TYPE *)malloc(LEN);

    scanf("%d%d",&pnum->num,&pnum->age);

    head=insert(head,pnum);

    print(head);

    }

    本例中，print函数用于输出链表中各个结点数据域值。函数的形参head的初值指向链表第一个结点。在while语句中，输出结点值后，head值被改变，指向下一结点。若保留头指针head，则应另设一个指针变量，把head值赋予它，再用它来替代head。在main函数中，n为建立结点的数目， num为待删结点的数据域值；head为指向链表的头指针，pnum为指向待插结点的指针。 main函数中各行的意义是：

    第六行输入所建链表的结点数；

    第七行调creat函数建立链表并把头指针返回给head；

    第八行调print函数输出链表；

    第十行输入待删结点的学号；

    第十一行调delete函数删除一个结点；

    第十二行调print函数输出链表；

    第十四行调malloc函数分配一个结点的内存空间，并把其地址赋予pnum;

    第十五行输入待插入结点的数据域值；

    第十六行调insert函数插入pnum所指的结点；

    第十七行再次调print函数输出链表。

    从运行结果看，首先建立起3个结点的链表，并输出其值；再删103号结点，只剩下105，108号结点；又输入106号结点数据，插入后链表中的结点为105，106，108。联合“联合”也是一种构造类型的数据结构。在一个“联合”内可以定义多种不同的数据类型，一个被说明为该“联合”类型的变量中，允许装入该“联合”所定义的任何一种数据。这在前面的各种数据类型中都是办不到的。例如，定义为整型的变量只能装入整型数据，定义为实型的变量只能赋予实型数据。

    在实际问题中有很多这样的例子。例如在学校的教师和学生中填写以下表格：姓名年龄职业单位 “职业”一项可分为“教师”和“学生”两类。对“单位”一项学生应填入班级编号，教师应填入某系某教研室。班级可用整型量表示，教研室只能用字符类型。要求把这两种类型不同的数据都填入“单位”这个变量中，就必须把“单位”定义为包含整型和字符型数组这两种类型的“联合”。

    “ 联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间，一个结构变量的总长度是各成员长度之和。而在“联合”中，各成员共享一段内存空间，一个联合变量的长度等于各成员中最长的长度。应该说明的是，这里所谓的共享不是指把多个成员同时装入一个联合变量内，而是指该联合变量可被赋予任一成员值，但每次只能赋一种值，赋入新值则冲去旧值。如前面介绍的“单位”变量，如定义为一个可装入“班级”或“教研室”的联合后，就允许赋予整型值（班级)或字符串（教研室)。要么赋予整型值，要么赋予字符串，不能把两者同时赋予它。联合类型的定义和联合变量的说明一个联合类型必须经过定义之后，才能把变量说明为该联合类型。

    一、联合的定义

    定义一个联合类型的一般形式为：

    union 联合名

    {

    成员表

    };

    成员表中含有若干成员，成员的一般形式为：类型说明符成员名成员名的命名应符合标识符的规定。

    例如：

    union perdata

    {

    int class;

    char office[10];

    };

    定义了一个名为perdata的联合类型，它含有两个成员，一个为整型，成员名为class；另一个为字符数组，数组名为office。联合定义之后，即可进行联合变量说明，被说明为perdata类型的变量，可以存放整型量class或存放字符数组office。

    二、联合变量的说明

    联合变量的说明和结构变量的说明方式相同，也有三种形式。即先定义，再说明；定义同时说明和直接说明。以perdata类型为例，说明如下：

    union perdata

    {

    int class;

    char officae[10];

    };

    union perdata a,b; /*说明a,b为perdata类型*/

    或者可同时说明为：

    union perdata

    { int class;

    char office[10]; }a,b;或直接说明为： union

    { int class;

    char office[10]; }a,b

    经说明后的a,b变量均为perdata类型。它们的内存分配示意图如图7—8所示。a,b变量的长度应等于 perdata 的成员中最长的长度，即等于

    office数组的长度，共10个字节。从图中可见，a,b变量如赋予整型值时，只使用了2个字节，而赋予字符数组时，可用10个字节。

    联合变量的赋值和使用

    对联合变量的赋值，使用都只能是对变量的成员进行。联合变量的成员表示为：联合变量名.成员名例如，a被说明为perdata类型的变量之后，可使用 a.class　a.office 不允许只用联合变量名作赋值或其它操作。也不允许对联合变量作初始化赋值，赋值只能在程序中进行。还要再强调说明的是，一个联合变量，每次只能赋予一个成员值。换句话说，一个联合变量的值就是联合变员的某一个成员值。

    [例7.15]设有一个教师与学生通用的表格，教师数据有姓名，年龄，职业，教研室四项。学生有姓名，年龄，职业，班级四项。

    编程输入人员数据，再以表格输出。

    main()

    {

    struct

    {

    char name[10];

    int age;

    char job;

    union

    {

    int class;

    char office[10];

    } depa;

    }body[2];

    int n,i;

    for(i=0;i<2;i++)

    {

    printf("input name,age,job and department\n");

    scanf("%s %d %c",body.name,&body.age,&body.job);

    if(body.job=='s')

    scanf("%d",&body.depa.class);

    else

    scanf("%s",body.depa.office);

    }

    printf("name\tage job class/office\n");

    for(i=0;i<2;i++)

    {

    if(body.job=='s')

    printf("%s\t%3d %3c %d\n",body.name,body.age

    ,body.job,body.depa.class);

    else

    printf("%s\t%3d %3c %s\n",body.name,body.age,

    body.job,body.depa.office);

    }

    }

    本例程序用一个结构数组body来存放人员数据，该结构共有四个成员。其中成员项depa是一个联合类型，这个联合又由两个成员组成，一个为整型量class，一个为字符数组office。在程序的第一个for语句中，输入人员的各项数据，先输入结构的前三个成员name,age和job，然后判别job成员项，如为"s"则对联合depa·class输入(对学生赋班级编号)否则对depa·office输入(对教师赋教研组名)。

    在用scanf语句输入时要注意，凡为数组类型的成员，无论是结构成员还是联合成员，在该项前不能再加"&"运算符。如程序第18行中

    body.name是一个数组类型，第22行中的body.depa.office也是数组类型，因此在这两项之间不能加"&"运算符。程序中的第二个for语句用于输出各成员项的值：

    本章小结

    1. 结构和联合是两种构造类型数据，是用户定义新数据类型的重要手段。结构和联合有很多的相似之处，它们都由成员组成。成员可以具有不同的数据类型。成员的表示方法相同。都可用三种方式作变量说明。

    2. 在结构中，各成员都占有自己的内存空间，它们是同时存在的。一个结构变量的总长度等于所有成员长度之和。在联合中，所有成员不能同时占用它的内存空间，它们不能同时存在。联合变量的长度等于最长的成员的长度。

    3. “.”是成员运算符，可用它表示成员项，成员还可用“->”运算符来表示。

    4. 结构变量可以作为函数参数，函数也可返回指向结构的指针变量。而联合变量不能作为函数参数，函数也不能返回指向联合的指针变量。但可以使用指向联合变量的指针，也可使用联合数组。

    5. 结构定义允许嵌套，结构中也可用联合作为成员，形成结构和联合的嵌套。

    6. 链表是一种重要的数据结构，它便于实现动态的存储分配。本章介绍是单向链表，还可组成双向链表，循环链表等。


正则表达式到底是什么东西？

字符是计算机软件处理文字时最基本的单位，可能是字母，数字，标点符号，空格，换行符，汉字等等。字符串是0个或更多个字符的序列。文本也就是文字，字符串。说某个字符串匹配某个正则表达式，通常是指这个字符串里有一部分（或几部分分别）能满足表达式给出的条件。

在编写处理字符串的程序或网页时，经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说，正则表达式就是记录文本规则的代码。

很可能你使用过Windows/Dos下用于文件查找的通配符(wildcard)，也就是*和?。如果你想查找某个目录下的所有的Word文档的话，你会搜索*.doc。在这里，*会被解释成任意的字符串。和通配符类似，正则表达式也是用来进行文本匹配的工具，只不过比起通配符，它能更精确地描述你的需求——当然，代价就是更复杂——比如你可以编写一个正则表达式，用来查找所有以 0开头，后面跟着2-3个数字，然后是一个连字号“-”，最后是7或8位数字的字符串(像 010-12345678 或 0376-7654321)。

入门

学习正则表达式的最好方法是从例子开始，理解例子之后再自己对例子进行修改，实验。下面给出了不少简单的例子，并对它们作了详细的说明。

假设你在一篇英文小说里查找 hi，你可以使用正则表达式hi。

这几乎是最简单的正则表达式了，它可以精确匹配这样的字符串：由两个字符组成，前一个字符是 h,后一个是i。通常，处理正则表达式的工具会提供一个忽略大小写的选项，如果选中了这个选项，它可以匹配 hi,HI,Hi,hI这四种情况中的任意一种。

不幸的是，很多单词里包含 hi 这两个连续的字符，比如 him,history,high等等。用hi来查找的话，这里边的 hi 也会被找出来。如果要精确地查找 hi这个单词的话，我们应该使用\bhi\b。

\b是正则表达式规定的一个特殊代码（好吧，某些人叫它元字符，metacharacter），代表着单词的开头或结尾，也就是单词的分界处。虽然通常英文的单词是由空格，标点符号或者换行来分隔的，但是\b并不匹配这些单词分隔字符中的任何一个，它只匹配一个位置。

如果需要更精确的说法，\b匹配这样的位置：它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w。

假如你要找的是 hi后面不远处跟着一个Lucy，你应该用\bhi\b.*\bLucy\b。

这里，.是另一个元字符，匹配除了换行符以外的任意字符。*同样是元字符，不过它代表的不是字符，也不是位置，而是数量——它指定*前边的内容可以连续重复出现任意次以使整个表达式得到匹配。因此，.*连在一起就意味着任意数量的不包含换行的字符。现在\bhi\b.*\bLucy\b的意思就很明显了：先是一个单词 hi,然后是任意个任意字符(但不能是换行)，最后是Lucy这个单词。

换行符就是'\n',ASCII编码为10(十六进制0x0A)的字符。

如果同时使用其它元字符，我们就能构造出功能更强大的正则表达式。比如下面这个例子：

0\d\d-\d\d\d\d\d\d\d\d匹配这样的字符串：以 0开头，然后是两个数字，然后是一个连字号“-”，最后是8个数字(也就是中国的电话号码。当然，这个例子只能匹配区号为3位的情形)。

这里的\d是个新的元字符，匹配一位数字(0，或1，或2，或……)。-不是元字符，只匹配它本身——连字符或者减号。

为了避免那么多烦人的重复，我们也可以这样写这个表达式：0\d{2}-\d{8}。 这里\d后面的{2}({8})的意思是前面\d 必须连续重复匹配2次(8次)。

测试正则表达式

其它可用的测试工具:

  • RegexBuddy
  • Javascript正则表达式在线测试工具

如果你不觉得正则表达式很难读写的话，要么你是一个天才，要么，你不是地球人。正则表达式的语法很令人头疼，即使对经常使用它的人来说也是如此。由于难于读写，容易出错，所以找一种工具对正则表达式进行测试是很有必要的。

由于在不同的环境下正则表达式的一些细节是不相同的，本教程介绍的是微软 .Net Framework 2.0下正则表达式的行为，所以，我向你介绍一个.Net下的工具Regex Tester。首先你确保已经安装了.Net Framework 2.0，然后下载Regex Tester。这是个绿色软件，下载完后打开压缩包,直接运行RegexTester.exe就可以了。

下面是Regex Tester运行时的截图：

元字符

现在你已经知道几个很有用的元字符了，如\b,.,*，还有\d.正则表达式里还有更多的元字符，比如\s匹配任意的空白符，包括空格，制表符(Tab)，换行符，中文全角空格等。\w匹配字母或数字或下划线或汉字等。

对中文/汉字的特殊处理是由.Net提供的正则表达式引擎支持的，其它环境下的具体情况请查看相关文档。

下面来看看更多的例子：

\ba\w*\b匹配以字母 a开头的单词——先是某个单词开始处(\b)，然后是字母a,然后是任意数量的字母或数字(\w*)，最后是单词结束处(\b)。

好吧，现在我们说说正则表达式里的单词是什么意思吧：就是多于一个的连续的\w。不错，这与学习英文时要背的成千上万个同名的东西的确关系不大 :)

\d+匹配 1个或更多连续的数字。这里的+是和*类似的元字符，不同的是*匹配重复任意次(可能是0次)，而+则匹配重复 1次或更多次。

\b\w{6}\b匹配刚好 6个字母/数字的单词。

表1.常用的元字符
代码 | 说明
. | 匹配除换行符以外的任意字符
\w | 匹配字母或数字或下划线或汉字
\s | 匹配任意的空白符
\d | 匹配数字
\b | 匹配单词的开始或结束
^ | 匹配字符串的开始
$ | 匹配字符串的结束

元字符^（和数字6在同一个键位上的符号）和$都匹配一个位置，这和\b有点类似。^匹配你要用来查找的字符串的开头，$匹配结尾。这两个代码在验证输入的内容时非常有用，比如一个网站如果要求你填写的QQ号必须为5位到12位数字时，可以使用：^\d{5,12}$。

这里的{5,12}和前面介绍过的{2}是类似的，只不过{2}匹配只能不多不少重复 2次，{5,12}则是重复的次数不能少于 5次，不能多于12次，否则都不匹配。

因为使用了^和$，所以输入的整个字符串都要用来和\d{5,12}来匹配，也就是说整个输入必须是 5到12个数字，因此如果输入的QQ号能匹配这个正则表达式的话，那就符合要求了。

和忽略大小写的选项类似，有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项，^和$的意义就变成了匹配行的开始处和结束处。

字符转义

如果你想查找元字符本身的话，比如你查找.,或者*,就出现了问题：你没办法指定它们，因为它们会被解释成别的意思。这时你就得使用\来取消这些字符的特殊意义。因此，你应该使用\.和\*。当然，要查找\本身，你也得用\\.

例如：unibetter\.com匹配 unibetter.com，C:\\Windows匹配 C:\Windows。

重复

你已经看过了前面的*,+,{2},{5,12}这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码，例如*,{5,12}等)：

表2.常用的限定符
代码/语法 | 说明
* | 重复零次或更多次
+ | 重复一次或更多次
? | 重复零次或一次
{n} | 重复n次
{n,} | 重复n次或更多次
{n,m} | 重复n到m次

下面是一些使用重复的例子：

Windows\d+匹配 Windows后面跟1个或更多数字

^\w+匹配一行的第一个单词(或整个字符串的第一个单词，具体匹配哪个意思得看选项设置)

字符类

要想查找数字，字母或数字，空白是很简单的，因为已经有了对应这些字符集合的元字符，但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办？

很简单，你只需要在方括号里列出它们就行了，像[aeiou]就匹配任何一个英文元音字母，[.?!]匹配标点符号(.或?或!)。

我们也可以轻松地指定一个字符范围，像[0-9]代表的含意与\d就是完全一致的：一位数字；同理[a-z0-9A-Z_]也完全等同于\w（如果只考虑英文的话）。

下面是一个更复杂的表达式：\(?0\d{2}[) -]?\d{8}。

“(”和“)”也是元字符，后面的分组节里会提到，所以在这里需要使用转义。

这个表达式可以匹配几种格式的电话号码，像(010)88886666，或 022-22334455，或 02912345678 等。我们对它进行一些分析吧：首先是一个转义字符\(,它能出现0次或1次(?),然后是一个0，后面跟着2个数字(\d{2})，然后是)或-或空格中的一个，它出现1次或不出现(?)，最后是8个数字(\d{8})。

分枝条件

不幸的是，刚才那个表达式也能匹配 010)12345678 或(022-87654321 这样的“不正确”的格式。要解决这个问题，我们需要用到分枝条件。正则表达式里的分枝条件指的是有几种规则，如果满足其中任意一种规则都应该当成匹配，具体方法是用|把不同的规则分隔开。听不明白？没关系，看例子：

0\d{2}-\d{8}|0\d{3}-\d{7}这个表达式能匹配两种以连字号分隔的电话号码：一种是三位区号，8位本地号(如010-12345678)，一种是4位区号，7位本地号(0376-2233445)。

\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}这个表达式匹配 3位区号的电话号码，其中区号可以用小括号括起来，也可以不用，区号与本地号间可以用连字号或空格间隔，也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。

\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字，或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题：使用分枝条件时，要注意各个条件的顺序。如果你把它改成\d{5}|\d{5}-\d{4}的话，那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时，将会从左到右地测试每个条件，如果满足了某个分枝的话，就不会去再管其它的条件了。

分组

我们已经提到了怎么重复单个字符（直接在字符后面加上限定符就行了）；但如果想要重复多个字符又该怎么办？你可以用小括号来指定子表达式(也叫做分组)，然后你就可以指定这个子表达式的重复次数了，你也可以对子表达式进行其它一些操作(后面会有介绍)。

(\d{1,3}\.){3}\d{1,3}是一个简单的 IP地址匹配表达式。要理解这个表达式，请按下列顺序分析它：\d{1,3}匹配 1到3位的数字，(\d{1,3}\.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次，最后再加上一个一到三位的数字(\d{1,3})。

IP地址中每个数字都不能大于255，大家千万不要被《24》第三季的编剧给忽悠了...

不幸的是，它也将匹配 256.300.888.999 这种不可能存在的IP地址。如果能使用算术比较的话，或许能简单地解决这个问题，但是正则表达式中并不提供关于数学的任何功能，所以只能使用冗长的分组，选择，字符类来描述一个正确的IP地址：((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)。

理解这个表达式的关键是理解2[0-4]\d|25[0-5]|[01]?\d\d?，这里我就不细说了，你自己应该能分析得出来它的意义。

反义

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外，其它任意字符都行的情况，这时需要用到反义：

表3.常用的反义代码
代码/语法 | 说明
\W | 匹配任意不是字母，数字，下划线，汉字的字符
\S | 匹配任意不是空白符的字符
\D | 匹配任意非数字的字符
\B | 匹配不是单词开头或结束的位置
[^x] | 匹配除了x以外的任意字符
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符

例子：\S+匹配不包含空白符的字符串。

<a[^>]+>匹配用尖括号括起来的以 a开头的字符串。

后向引用

使用小括号指定一个子表达式后，匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下，每个分组会自动拥有一个组号，规则是：从左向右，以分组的左括号为标志，第一个出现的分组的组号为1，第二个为2，以此类推。

后向引用用于重复搜索前面某个分组匹配的文本。例如，\1代表分组 1匹配的文本。难以理解？请看示例：

\b(\w+)\b\s+\1\b可以用来匹配重复的单词，像 go go, 或者 kitty kitty。这个表达式首先是一个单词，也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b)，这个单词会被捕获到编号为1的分组中，然后是 1个或几个空白符(\s+)，最后是分组 1中捕获的内容（也就是前面匹配的那个单词）(\1)。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名，请使用这样的语法：(?<Word>\w+)(或者把尖括号换成'也行：(?'Word'\w+)),这样就把\w+的组名指定为Word了。要反向引用这个分组捕获的内容，你可以使用\k<Word>,所以上一个例子也可以写成这样：\b(?<Word>\w+)\b\s+\k<Word>\b。

使用小括号的时候，还有很多特定用途的语法。下面列出了最常用的一些：

表4.常用分组语法
分类 | 代码/语法 | 说明
捕获 | (exp) | 匹配exp,并捕获文本到自动命名的组里
 | (?<name>exp) | 匹配exp,并捕获文本到名称为name的组里，也可以写成(?'name'exp)
 | (?:exp) | 匹配exp,不捕获匹配的文本，也不给此分组分配组号
零宽断言 | (?=exp) | 匹配exp前面的位置
 | (?<=exp) | 匹配exp后面的位置
 | (?!exp) | 匹配后面跟的不是exp的位置
 | (?<!exp) | 匹配前面不是exp的位置
注释 | (?#comment) | 这种类型的分组不对正则表达式的处理产生任何影响，用于提供注释让人阅读

我们已经讨论了前两种语法。第三个(?:exp)不会改变正则表达式的处理方式，只是这样的组匹配的内容不会像前两种那样被捕获到某个组里面，也不会拥有组号。

零宽断言

地球人，是不是觉得这些术语名称太复杂，太难记了？我也和你一样。知道有这么一种东西就行了，它叫什么，随它去吧！“无名，万物之始...”

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西，也就是说它们像\b,^,$那样用于指定一个位置，这个位置应该满足一定的条件(即断言)，因此它们也被称为零宽断言。最好还是拿例子来说明吧：

断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。

(?=exp)也叫零宽度正预测先行断言，它断言自身出现的位置的后面能匹配表达式 exp。比如\b\w+(?=ing\b)，匹配以 ing结尾的单词的前面部分(除了ing以外的部分)，如查找 I'm singing while you're dancing.时，它会匹配 sing 和 danc。

(?<=exp)也叫零宽度正回顾后发断言，它断言自身出现的位置的前面能匹配表达式 exp。比如(?<=\bre)\w+\b会匹配以 re开头的单词的后半部分(除了re以外的部分)，例如在查找 reading a book 时，它匹配 ading。

假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了)，你可以这样查找需要在前面和里面添加逗号的部分：((?<=\d)\d{3})*\b，用它对 1234567890 进行查找时结果是 234567890。

下面这个例子同时使用了这两种断言：(?<=\s)\d+(?=\s)匹配以空白符间隔的数字(再次强调，不包括这些空白符)。

负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现，但并不想去匹配它时怎么办？例如，如果我们想查找这样的单词--它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样：

\b\w*q[^u]\w*\b匹配包含后面不是字母 u的字母q 的单词。但是如果多做测试(或者你思维足够敏锐，直接就观察出来了)，你会发现，如果q出现在单词的结尾的话，像 Iraq,Benq，这个表达式就会出错。这是因为[^u]总要匹配一个字符，所以如果q是单词的最后一个字符的话，后面的[^u]将会匹配q后面的单词分隔符(可能是空格，或者是句号或其它的什么)，后面的\w*\b将会匹配下一个单词，于是\b\w*q[^u]\w*\b就能匹配整个 Iraq fighting。负向零宽断言能解决这样的问题，因为它只匹配一个位置，并不消费任何字符。现在，我们可以这样来解决这个问题：\b\w*q(?!u)\w*\b。

零宽度负预测先行断言(?!exp)，断言此位置的后面不能匹配表达式 exp。例如：\d{3}(?!\d)匹配三位数字，而且这三位数字的后面不能是数字；\b((?!abc)\w)+\b匹配不包含连续字符串 abc的单词。

同理，我们可以用(?<!exp),零宽度正回顾后发断言来断言此位置的前面不能匹配表达式 exp：(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。

请详细分析表达式(?<=<(\w+)>).*(?=<\/\1>)，这个表达式最能表现零宽断言的真正用途。

一个更复杂的例子：(?<=<(\w+)>).*(?=<\/\1>)匹配不包含属性的简单 HTML标签内里的内容。(<?(\w+)>)指定了这样的前缀：被尖括号括起来的单词(比如可能是<b>)，然后是.*(任意的字符串),最后是一个后缀(?=<\/\1>)。注意后缀里的\/，它用到了前面提过的字符转义；\1则是一个反向引用，引用的正是捕获的第一组，前面的(\w+)匹配的内容，这样如果前缀实际上是<b>的话，后缀就是</b>了。整个表达式匹配的是<b>和</b>之间的内容(再次提醒，不包括前缀和后缀本身)。

注释

小括号的另一种用途是通过语法(?#comment)来包含注释。例如：2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)。

要包含注释的话，最好是启用“忽略模式里的空白符”选项，这样在编写表达式时能任意的添加空格，Tab，换行，而实际使用时这些都将被忽略。启用这个选项后，在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如，我们可以前面的一个表达式写成这样：

(?<= # 断言要匹配的文本的前缀

<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)

) # 前缀结束

.* # 匹配任意文本

(?= # 断言要匹配的文本的后缀

<\/\1> # 查找尖括号括起来的内容：前面是一个"/"，后面是先前捕获的标签

) # 后缀结束

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时，通常的行为是（在使整个表达式能得到匹配的前提下）匹配尽可能多的字符。考虑这个表达式：a.*b，它将会匹配最长的以 a开始，以b结束的字符串。如果用它来搜索 aabab 的话，它会匹配整个字符串 aabab。这被称为贪婪匹配。

有时，我们更需要懒惰匹配，也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式，只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复，但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧：

a.*?b匹配最短的，以 a开始，以b结束的字符串。如果把它应用于 aabab 的话，它会匹配 aab（第一到第三个字符）和 ab（第四到第五个字符）。

为什么第一个匹配是aab（第一到第三个字符）而不是ab（第二到第三个字符）？简单地说，因为正则表达式有另一条规则，比懒惰／贪婪规则的优先级更高：最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

表5.懒惰限定符
代码/语法 | 说明
*? | 重复任意次，但尽可能少重复
+? | 重复1次或更多次，但尽可能少重复
?? | 重复0次或1次，但尽可能少重复
{n,m}? | 重复n到m次，但尽可能少重复
{n,}? | 重复n次以上，但尽可能少重复

处理选项

在C#中，你可以使用Regex(String, RegexOptions)构造函数来设置正则表达式的处理选项。如：Regex regex = new Regex("\ba\w{6}\b", RegexOptions.IgnoreCase);

上面介绍了几个选项如忽略大小写，处理多行等，这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项：

表6.常用的处理选项
名称 | 说明
IgnoreCase(忽略大小写) | 匹配时不区分大小写。
Multiline(多行模式) | 更改^和$的含义，使它们分别在任意一行的行首和行尾匹配，而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式) | 更改.的含义，使它与每一个字符匹配（包括换行符\n）。
IgnorePatternWhitespace(忽略空白) | 忽略表达式中的非转义空白并启用由#标记的注释。
RightToLeft(从右向左查找) | 匹配从右向左而不是从左向右进行。
ExplicitCapture(显式捕获) | 仅捕获已被显式命名的组。
ECMAScript(JavaScript兼容模式) | 使表达式的行为与它在JavaScript里的行为一致。

一个经常被问到的问题是：是不是只能同时使用多行模式和单行模式中的一种？答案是：不是。这两个选项之间没有任何关系，除了它们的名字比较相似（以至于让人感到疑惑）以外。

平衡组/递归匹配

这里介绍的平衡组语法是由.Net Framework支持的；其它语言／库不一定支持这种功能，或者支持此功能但需要使用不同的语法。

有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构，这时简单地使用\(.+\)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式，懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等，比如( 5 / ( 3 + 2 ) ) )，那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的，配对的括号之间的内容呢？

为了避免(和\(把你的大脑彻底搞糊涂，我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把 xx <aa <bbb> <bbb> aa> yy 这样的字符串里，最长的配对的尖括号内的内容捕获出来？

这里需要用到以下的语法构造：

  • (?'group')把捕获的内容命名为group,并压入堆栈(Stack)
  • (?'-group')从堆栈上弹出最后压入堆栈的名为group的捕获内容，如果堆栈本来为空，则本分组的匹配失败
  • (?(group)yes|no)如果堆栈上存在以名为group的捕获内容的话，继续匹配yes部分的表达式，否则继续匹配no部分
  • (?!)零宽负向先行断言，由于没有后缀表达式，试图匹配总是失败

如果你不是一个程序员（或者你自称程序员但是不知道堆栈是什么东西），你就这样理解上面的三种语法吧：第一个就是在黑板上写一个"group"，第二个就是从黑板上擦掉一个"group"，第三个就是看黑板上写的还有没有"group"，如果有就继续匹配yes部分，否则就匹配no部分。

我们需要做的是每碰到了左括号，就在压入一个"Open",每碰到一个右括号，就弹出一个，到了最后就看看堆栈是否为空－－如果不为空那就证明左括号比右括号多，那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符)，尽量使整个表达式得到匹配。

< #最外层的左括号

[^<>]* #最外层的左括号后面的不是括号的内容

(

(

(?'Open'<) #碰到了左括号，在黑板上写一个"Open"

[^<>]* #匹配左括号后面的不是括号的内容

)+

(

(?'-Open'>) #碰到了右括号，擦掉一个"Open"

[^<>]* #匹配右括号后面不是括号的内容

)+

)*

(?(Open)(?!)) #在遇到最外层的右括号前面，判断黑板上还有没有没擦掉的"Open"；如果还有，则匹配失败

> #最外层的右括号

平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的<div>标签：<div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div>.

还有些什么东西没提到

我已经描述了构造正则表达式的大量元素，还有一些我没有提到的东西。下面是未提到的元素的列表，包含语法和简单的说明。你可以在网上找到更详细的参考资料来学习它们--当你需要用到它们的时候。如果你安装了MSDN Library,你也可以在里面找到关于.net下正则表达式详细的文档。

表7.尚未详细讨论的语法
代码/语法 | 说明
\a | 报警字符(打印它的效果是电脑嘀一声)
\b | 通常是单词分界位置，但如果在字符类里使用代表退格
\t | 制表符，Tab
\r | 回车
\v | 竖向制表符
\f | 换页符
\n | 换行符
\e | Escape
\0nn | ASCII代码中八进制代码为nn的字符
\xnn | ASCII代码中十六进制代码为nn的字符
\unnnn | Unicode代码中十六进制代码为nnnn的字符
\cN | ASCII控制字符。比如\cC代表Ctrl+C
\A | 字符串开头(类似^，但不受处理多行选项的影响)
\Z | 字符串结尾或行尾(不受处理多行选项的影响)
\z | 字符串结尾(类似$，但不受处理多行选项的影响)
\G | 当前搜索的开头
\p{name} | Unicode中命名为name的字符类，例如\p{IsGreek}
(?>exp) | 贪婪子表达式
(?<x>-<y>exp) | 平衡组
(?im-nsx:exp) | 在子表达式exp中改变处理选项
(?im-nsx) | 为表达式后面的部分改变处理选项
(?(exp)yes|no) | 把exp当作零宽正向先行断言，如果在这个位置能匹配，使用yes作为此组的表达式；否则使用no
(?(exp)yes) | 同上，只是使用空表达式作为no
(?(name)yes|no) | 如果命名为name的组捕获到了内容，使用yes作为表达式；否则使用no
(?(name)yes) | 同上，只是使用空表达式作为no


一般情况下，源程序中所有的行都参加编译

一般情况下，源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件下才进行编译，即对一部分内容指定编译条件，这就是“条件编译”。（conditional compile）

    条件编译语句排版时，需考虑以下三种位置：

    （1）条件编译语句块与函数定义体之间不存在相互嵌套（主要在（.h）文件中）

    ◆条件编译关键字语句顶格左对齐；

    ◆所含的#include语句(块) #define语句(块)甚至是被嵌套下级条件编译语句块，按照语句块嵌套的排版方式进行缩进排版。

    （2）条件编译语句块嵌套在函数体之外（主要在（.c）文件中）

    这种情况下，条件编译语句块不影响函数体

    ◆条件编译关键字语句顶格左对齐；

    ◆所含的函数体定义无需缩进，依旧按照单个函数体定义的排版方式进行。

    （3）条件编译语句嵌套在函数体内（主要在（.c）文件中）

    a）当条件编译语句块与被包语句所属的语句块之间没有逻辑路径交叉时，以下两种方式均可

    ◆按照语句块嵌套方式进行缩进排版 (推荐)；

    ◆条件编译语句不影响原先语句块排版，条件编译语句与所包含的关键字语句块左对齐。

    b）当条件编译语句块与被包语句所属的语句块之间存在逻辑路径交叉时

    ◆条件编译语句顶格左对齐，其它语句按照正常顺序排版。

    条件编译的形式如下所示（NNN、MMM等都是在某处已经定义为 1 或者 0 的）：

    #if NNN

    statement1;

    #elif MMM

    statement2;

    #else

    statement3;

#endif

#endif

是预编译处理指令中的条件编译。

    预编译处理是指在编译系统对文件进行编译---词法分析、语法分析、代码生成及优化之前，对一些特殊的编译语句先进行处理，然后将处理结果与源程序一起编译，生成目标文件。

    预编译处理语句都是以#开头，其结尾不带分号，与普通程序语句相区别。

    #endif用于结束条件编译，编译时与前面最近的#if作为一对，编译两者之间的部分程序段。

14.4 条件编译

C语言的预处理器可以识别大量用于支持条件编译的指令。条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片断。

14.4.1 #if指令和#endif指令

假如我们正在调试一个程序。我们想要程序显示出特定变量的值，因此将printf函数调用添加到程序中重要的部分。一旦找到错误，经常需要保留这些printf函数调用，以备以后使用。条件编译允许我们保留这些调用，但是让编译器忽略它们。

下面是我们需要采取的方式。首先定义一个宏，并给它一个非0的值：

#define DEBUG 1

宏的名字并不重要。接下来，我们要在每组printf函数调用的前后加上#if和#endif：

#if DEBUG

printf("Value of i : %d\n", i);

printf("Value of j : %d\n", j);

#endif

在于处理过程中，#if指令会测试DEBUG的值。由于DEBUG的值非0，因此预处理器会将这两个printf函数调用保留在程序中（但#if和#endif行会消失）。如果我们将DEBUG的值改为0并重新编译程序，预处理器则会将这4行代码都消失。编译器将不会看到这些printf函数调用，所以这些调用就不会在目标代码中占用空间，也不会在程序运行时浪费时间。我们可以将#if-#endif保留在最终的程序中，这样如果程序在运行时出错，可以继续产生这些诊断信息（将DEBUG改为1并重新编译）。

一般来说，#if指令的格式如下：

[#if指令]　　#if 常量表达式

#endif指令则更简单：

[#endif指令]　　#endif

当预处理器遇到#if指令时，会计算常量表达式。如果表达式的值为0，那么在#if与#endif之间的行将在预处理过程中从程序中删除。否则，这些在#if和#endif之间的行会被保留在程序中，并继续被编译器处理——这时#if和#endif对程序没有任何影响。

对于没有定义过的标识符，#if指令会把它当作是值为0的宏对待。因此，如果省略DEBUG的定义，测试

#if DEBUG

会失败（但不会产生出错消息），而测试

#if !DEBUG

会成功。

14.4.2 defined运算符

14.3节中介绍过运算符#和##，还有另外一个运算符——defined，它仅用于预处理器。当defined应用于标识符时，如果标识符是一个定义过的宏返回1，否则返回0。#defined运算符通常与#if指令结合使用，允许写成

#if defined(DEBUG)

...

#endif

仅当DEBUG被定义成宏时，#if和#endif之间的代码会被保留在程序中。DEBUG两侧的括号不是必需的，因此可以简单写成

#if defined DEBUG

由于defined运算符仅检测DEBUG是否被定义为宏，所以不需要给DEBUG一个值：

#define DEBUG

14.4.3 #ifdef指令和#ifndef指令

#ifdef指令测试一个标识符是否已经定义为宏：

[#ifdef指令]　　#ifdef 标识符

#ifdef指令的使用与#if指令类似：

#ifdef 标识符

当标识符被定义为宏时需要包含代码

#endif

严格地说， [image] 并不需要#ifdef，因为我们可以组合#if指令和defined运算符来达到相同的效果。换而言之，指令

#ifdef 标识符

等价于

#if defined(标识符)

#ifndef指令与#ifdef指令类似，但是测试的是标识符是否没有被定义为宏：

[#ifndef指令]　　#ifndef 标识符

写成

#ifndef 标识符

等价于写成

#if !defined(标识符)

14.4.4 #elif指令和#else指令

#if指令、#ifdef指令和#ifndef指令可以像普通的if语句那样嵌套使用。当发生嵌套时，最好随着嵌套层次的增加而增加缩进。一些程序员对每一个#endif都加注释，来指明是对应于哪个条件测试的#if指令：

#if DEBUG

...

#endif /* DEBUG */

这种方法可以帮助读者更方便地找到起始的#if指令。

为了提供更多的便利，预处理器提供了#elif和#else指令：

[#elif指令]　　#elif 表达式

[#else指令]　　#else

#elif指令和#else指令可以与#if指令、#ifdef指令和#ifndef指令组合使用，来测试一系列条件：

#if 表达式1

当表达式1非0时需要包含的代码

#if 表达式2

当表达式1为0但表达式2非0时需要包含的代码

#else

其他情况下需要包含的代码

#endif

虽然上面的例子使用了#if指令，但#ifdef指令或#ifndef指令也可以替代使用。在#if指令和#endif指令之间可以有多个#elif指令，但最多只能有一个#else指令。

14.4.5 使用条件编译

条件编译对于调试是非常方便的，但它并不仅限于此。下面是其他一些常见的应用：

l 编写在多台机器或多种操作系统之间可移植的程序。下面的例子中会根据WINDOWS、DOS或OS2是否被定义为宏，而将三组代码之一包含到程序中：

#if defined(WINDOWS)

...

#elif defined(DOS)

...

#elif defined(OS2)

...

#endif

一个程序中可能包含许多这样的#if指令。在程序的开头会定义这些宏之一（而且只有一个），由此选择了一个特定的操作系统。例如，定义OS2宏可以指明程序将运行在OS/2操作系统下。

l 编写可以使用不同的编译器进行编译的程序。不同的编译器经常用于不同的C语言版本，这些版本之间会有一些差异。一些会接受标准C，另外一些则不会。一些版本会包含针对特定机器的扩展，而其他的则或没有，或提供不同的扩展集。条件编译可以使程序适应于不同的编译器。思考编写这样一个程序，当使用标准C的编译器进行编译时，可不可能被编译成功。__STDC__宏允许预处理器检测编译器是否支持标准C，如果不支持，我们可能必须修改程序的某些方面，尤其是有可能必须使用经典C的函数声明替代标准C的函数原型。对于每一处函数的声明，我们都可以使用下面的代码：

#if __STDC__

标准C函数原型

#else

经典C函数声明

#endif

l 为宏提供默认定义。条件编译使我们可以检测一个宏当前是否已经被定义了，如果没有，则提供一个默认的定义。例如，如果它还没有被定义的话，下面的代码会定义宏BUFFER_SIZE：

#ifndef BUFFER_SIZE

#define BUFFER_ZISE 256

#endif

l 临时屏蔽包含注释的代码。由于在标准C中注释不能嵌套，因此不能直接“注释掉”包含注释的代码。然而，可以使用#if指令替代：

#if 0

包含注释的代码行

#endif

将代码以这种方式屏蔽掉经常称为“条件屏蔽”。

15.2节会讨论另外一种条件编译的常用用途：保护头文件以避免重复包含。

14.5 其他指令

在本章的最后，我们将简要地了解一下#error指令、#line指令和#pragma指令。这些指令有一个共同点：C语言的初学者不会经常使用它们。实际上，本书中的所有程序都不会使用它们，因此你可以放心地跳过本节。以后，当你准备成为一个C语言专家时，你会需要熟悉这些指令。

14.5.1 #error指令

#error指令有如下格式：

[#error 指令]　　#error 消息

其中，消息是一个C语言标记序列。如果预处理器遇到一个#error指令，它会显示一个出错消息，这个出错消息一定会包含消息。对于不同的编译器，出错消息的具体形式也可能会不一样。下面是一个典型的示例：

Error directive: 记号序列

碰到#error指令预示着一个严重的程序错误，大多数编译器会立即终止编译而不去找出其他错误。

#error指令通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。例如，假定我们需要确保，一个程序在一台无法使用int类型来存储大于100 000的数的机器上不能编译。最大允许的int值用INT_MAX宏（ [image] 23.2节）表示，所以我们需要做的就是当INT_MAX宏不超过100 000时调用#error指令：

#if INT_MAX < 100000

#error int type is too small

#endif

如果试图在一台整型以16位存储的机器上编译这个程序，将产生一条出错消息：

Error directive: int type is too small

#error指令通常会出现在#if-#elif-#else序列中的#else部分：

#if defined(WINDOWS)

...

#elif defined(DOS)

...

#elif defined(OS2)

...

#else

#error No operating system specified

#endif

14.5.2 #line指令

#line指令是用来改变给程序行编号的方式的。（程序行通常是按1，2，3，…来编号的。）我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取一个程序。

#line指令有两种形式。在一种形式中，指定一个行号：

[#line 指令 (形式1)]　　#line n

n 必须是介于1到32 767之间的整数。这条指令导致程序后面的行被编号为n、n+1、n+2等。

在#line的第二种形式中，需要同时指定行号和文件名：

[#line指令（形式2）]　　#line n "文件名"

指令后面的行会被认为是来自文件，行号由n开始。

#line指令的一种作用是改变__LINE__宏（可能还有__FILE__宏）的值。更重要的是，大多数编译器会使用来自#line指令的信息产生出错消息。例如，假设下列指令出现在文件foo.c的开头：

#line 10 "bar.c"

现在，假设编译器在foo.c的第5行监测到一个错误。出错消息会指向bar.c的第13行，而不是foo.c的第5行。（为什么是第13行呢？因为指令占据了foo.c的第1行，因此对foo.c的重新编号从第2行开始，并将这一行作为bar.c的第10行。）

乍一看，#line指令使人迷惑。为什么要使出错消息指向另一行，甚至是另一个文件呢？这样不是会使程序变得难以调试吗？

实际上，程序员并不经常使用#line指令。然而，它主要用于那些产生C代码作为输出的程序。程序yacc（Yet Another Compiler-Compiler）是其中著名的程序之一。yacc是一个UNIX工具，用于自动生成部分的编译器。在使用yacc之前，程序员需要准备一个包含yacc所需要的信息以及C代码段的文件。通过这个文件，yacc生成了一个C程序y.tab.c，并合并了程序员所提供的代码。程序员接着按照正常方法编译y.tab.c。通过在y.tab.c中插入#line指令，yacc会使编译器认为代码来自原始文件——也就是程序员写的那个文件。于是，任何编译y.tab.c时产生的出错消息会指向原始文件中的行，而不是y.tab.c中的行。其最终结果是：调试变得更容易，因为出错消息都指向程序员编写的文件，而不是（更复杂的）由yacc生成的文件。

14.5.3 #pragma指令

#pragma指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。

#pragma指令有如下形式：

[#pragma指令]　　#pragma 记号

其中，记号是一般C语言的记号。#pragma指令通常只跟着一个记号，这个记号表示了一条编译器需要服从的命令。

一些编译器允许#pragma指令所包含的不仅是简单的命令。特别是有些编译器允许#pragam指令带参数：

#pragma data(heap_size => 1000, stack_size => 2000)

#pragma指令中出现的命令集在不同的编译器上是不一样的。你必须通过查阅你所使用的编译器的文档来了解哪些命令是可以使用的，以及这些命令的功能。顺便提一下，如果#pragma指令包含了无法识别的命令，编译器必须忽略这些#pragma指令，不允许产生出错消息。

问与答

*问：我看到在有些程序中#单独占一行。这样是合法的吗？

*答：是合法的。这就是所谓的空指令，它没有任何作用。一些程序员用空指令作为条件编译模块之间的间隔：

#if INT_MAX < 100000

#

#error int type is too small

#

#endif

当然，空行也可以。不过#可以帮助读者看出模块的范围。

*问：我不清楚程序中哪些常量需要定义成宏。有没有一些可以参照的规则？（p.195）

*答：一条首要的规则是每一个数字常量，如果不是0和1，就需要定义成宏。字符常量和字符串常量有一点复杂，因为使用宏来替换字符或字符串常量并不总是提高程序的可读性。我个人建议在下面的条件下使用宏来替代字符或字符串常量：（1）常量被不止一次地使用；（2）存在常量某天被修改的可能。根据第二条规则，我不会像这样使用宏：

#define NUL '\0'

虽然有些程序员会使用。

*问：如果要被“字符串化”的参数包含 " 或 \ 字符，#运算符会如何处理？（p.197）

*答：它会将 " 转换为 \"，\ 转换为 \\。考虑下面的宏：

#define STRINGIZE(x) #x

预处理器会将STRINGIZE("foo")替换为"\"foo\""。

*问：我无法使下面的宏正常工作：

#define CONCAT(x,y) x##y

*问：如期望那样，CANCAT(a,b)会给出ab，但CONCAT(a,CONCAT(b,c))会给出一个怪异的结果。这是为什么？

*答：感谢那些连Kernighan和Ritchie都承认“怪异”的规则，替换列表中依赖##的宏通常不能嵌套调用。这里的问题在于CONCAT(a,CONCAT(b,c))不会按照“正常”的方式扩展——CONCAT(b,c)首先得出bc，然后CONCAT(a,bc)给出abc。C标准指明，在替换列表中，位于##运算符之前和之后的宏参数在替换时不被扩展，结果，CONCAT(a,CONCAT(b,c))扩展成aCONCAT(b,c)，而不会进一步扩展，因为没有名为aCONCAT的宏。
有一种办法可以解决这个问题，但不太好看。技巧是定义第二个宏，只是简单地调用第一个宏：

#define CONCAT2(x,y) CONCAT(x,y)

写成CONCAT2(a,CONCAT2(b,c))就会得到我们希望的结果。在扩展外面的CONCAT2调用时，预处理器将会同时扩展CONCAT2(b,c)。这里的区别在于CONCAT2的扩展列表不包含##。如果这个也不行，那也不用担心，这种问题并不是经常遇到的。

顺便提一下，#运算符也有同样的问题。如果#x出现在替换列表中，其中x是一个宏参数，其对应的实际参数也不会被扩展。因此，假设N是一个代表10的宏，且STR(x)包含替换列表#x，STR(N)扩展的结果为"N"，而不是"10"。解决的方法与我们在CONCAT例子中类似：定义第二个宏来调用STR。

*问：如果预处理器再重新扫描时又发现了最初的宏名会如何处理呢？例如下面的例子：

#define N (2*M)

#define M (N+!)

i = N; /* infinite loop?*/

*问：预处理器会将N替换为(2*M)，接着将M替换为(N+1)。预处理器还会再次替换N，成为一个无限循环吗？（p.199）

*答：一些早期的预处理器确实会进入无限循环，但符合标准C的预处理器则不会。按照C语言标准，如果在扩展宏的过程中原先的宏名重复出现的话，宏名不会再次被替换。下面是预处理后i的赋值语句的形式：

i = (2*(N+1));

一些大胆的程序员会通过编写其名字与保留字或标准库中的函数名匹配的宏来利用这一行为。以库函数sqrt为例。sqrt函数（ [image] 23.3.5节）计算参数的平方根，如果参数为负数则返回一个由实现定义的值，我们可能希望当参数为负数时返回0。由于sqrt是标准库函数，我们无法很容易地修改它。但是我们可以定义一个宏，使它在参数为负数时返回0：

#define sqrt(x) ((x)>0 ? sqrt(x) : 0)

预处理器会截获sqrt的调用，并将它替换成上面的条件表达式。在扫描宏的过程中条件表达式中的sqrt调用不会被替换，因此会被保留由编译器处理。

*问：我觉得预处理器就是一个编辑器。它如何计算常量表达式呢？（p.202）

*答：预处理器比你想的要复杂。它足够“了解”C语言，所以能够计算常量表达式。虽然它不会完全按照编译器的方式去做。（例如，预处理器认为所有未定义的名字的值为0。其他的差异太深奥，就不再深入了。）在实际使用中，预处理器常量表达式中的操作数通常为常量、表示常量的宏或defined运算符的应用。

*问：为什么C提供#ifdef指令和#ifndef指令，既然我们可以使用#if指令和define运算符达到同样效果？（p.203）

*答：#ifdef指令和#ifndef指令从20世纪70年代就在C语言中存在了，而defined运算符则是在80年代的标准化过程中加到C语言中的。因此，实际的问题是：为什么将defined运算符加到C语言中？答案就是defined增加了灵活性。我们现在可以使用#if和defined运算符来测试任意数量的宏，而不再是只能使用#ifdef和#ifndef对一个宏进行测试。例如，下面的指令检查是否FOO和BAR被定义了而BAZ没有被定义：

#if defined(FOO) && defined(BAR) && !defined(BAZ)

*问：我想编译一个还没有写完的程序，因此我“条件屏蔽”未完成的部分。我在开始的地方加了一条信息来提示自己以后将程序写完：

#if 0

Haven’t finished this part yet.

...

#endif

*问：为什么我在编译时会得到一条出错消息呢？预处理器不是简单地忽略#if指令和#endif指令之间的所有行吗？（p.205）

*答：在#if指令和#endif指令之间的行必须由预处理记号（ [image] 附录A）组成。预处理记号类似于C语言记号（标识符、运算符、数等）。当预处理器试图将第一行分解为记号时，会遇到Haven（一个合法的标识符），接着是’t（一个非法的字符常量）。一些预处理器会跳过#if指令和#endif指令之间所有的行而不会检查预处理记号，但这些预处理器并没有严格遵守C语言标准的规则。

练习

14.3节

1. 编写宏来计算下面的值。

(a) x的立方。

(b) x除以4的余数。

(c) 如果x与y的乘积小于100值为1，否则值为0。

你写的宏始终正常工作吗？如果不是，哪些参数会失败呢？

2. 编写一个宏NELEMS(a)来计算一个一维数组a中元素的个数。提示：使用sizeof运算符。

3. 假定DOUBLE是如下宏：

#define DOUBLE(x) 2*x

(a) DOUBLE(1+2)的值是多少？

(b) 4/DOUBLE(2)的值是多少？

(c) 改正DOUBLE的定义。

4. 针对下面每一个宏，举例说明宏的问题，并提出修改方法。

(a) #define AVG(x,y) (x+y)/2

(b) #define AREA(x,y) (x)*(y)

*5. 下面的宏定义有问题：

#define ABS(a) ((a)<0?-(a):a)

举例说明为什么ABS不能正常工作，并提出修改方法。你可以假定ABS的参数没有副作用。

6. 假定TOUPPER定义成下面的宏：

#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))

假设s是一个字符串，i是一个int型变量。给出下面每个代码段所产生的输出。

(a) strcpy(s, "abcd");

i = 0;

putchar(TOUPPER(s[++i]));

(b) strcpy(s, "0123");

i = 0;

putchar(TOUPPER(s[++i]));

7. (a) 编写宏DISP(f,x)，使其扩展后调用printf函数来显示函数f的参数为x时的值。例如：

DISP(sqrt, 3.0);

应该扩展为

printf("sqrt(%g) = %g\n", 3.0, sqrt(3.0));

(b) 编写宏DISP2(f,x,y)，类似DISP但应用于有两个参数的函数。

*8. 假定GENERIC_MAX是如下宏：

#define GENERIC_MAX(type) \

type type##_max(type x, type y) \

{ \

return x > y ? x : y; \

}

(a) 写出GENERIC_MAX(long)被预处理器扩展后的形式。

(b) 解释为什么GENERIC_MAX不能应用在像unsigned long这样的基本类型上？

(c) 如何使GENERIC_MAX对任何基本类型都可以正常工作？提示：不要改变GENERIC_MAX的定义。

*9. 如果需要一个宏，使它展开后包含当前行号和文件名。换而言之，我们会写

const char *str = LINE_FILE;

扩展后为

const char *str = "Line 10 of file foo.c";

其中foo.c是包含程序的文件，10是调用LINE_FILE行的行号。警告：这个练习仅针对高级程序员。尝试编写前请认真阅读“问与答”小节的内容！

14.4节

10. 假定宏M有如下定义：

#define M 10

下面哪项测试会失败？

(a) #if M

(b) #ifdef M

(c) #ifndef M

(d) #if defined(M)

(e) #if !defined(M)

11. (a) 指出下面的程序预处理后的形式。

#define N 100

void f(void);

main()

{

f();

#ifdef N

#unde f N

#endif

return 0;

}

void f(void)

{

#if defined(N)

printf("N is %d\n", N);

#else

printf("N is undefined\n");

#endif

}

(b) 这个程序的输出是什么？

12. 指出下面的程序预处理后的形式。其中有几行可能会导致编译错误，请找出这些错误。

#define N = 10

#define INC(x) x+l

#define SUB (x,y) x - y

#define SQR(x) ((x)*(x))

#define CUBE(x) (SQR(x)*(x))

#deflne Ml(x,y) x##y

#define M2(x,y) #x #y

main ( )

{

int a[N], i, j, k, m;

#ifdef N

i = j;

#else

j = i;

#endif

i = 10 * INC(j);

i = SUB(j, k);

i = SQR(SQR(j++)) ;

i = CUBE(j);

i = M1(j, k);

puts(M2(i, j)) ;

#undef SQR

i = SQR(j);

#define SQR

i = SQR(j);

return 0 ;

)


  有些程序在调试、兼容性、平台移植等情况下可能想要通过简单地设置一些参数就生成一个不同的软件，这当然可以通过变量设置，把所有可能用到的代码都写进去，在初始化时配置，但在不同的情况下可能只用到一部分代码，就没必

    有些程序在调试、兼容性、平台移植等情况下可能想要通过简单地设置一些参数就生成一个不同的软件，这当然可以通过变量设置，把所有可能用到的代码都写进去，在初始化时配置，但在不同的情况下可能只用到一部分代码，就没必要把所有的代码都写进去，就可以用条件编译，通过预编译指令设置编译条件，在不同的需要时编译不同的代码。

     （一）条件编译方法

     条件编译是通过预编译指令来实现的，主要方法有：

     1、#if， #elif， #else， #endif

  #if　条件　1
  代码段　1
#elif 条件　2
     代码段　2
...
#elif 条件　n
  代码段　n
#else
  代码段 n+1
#endif

     即可以设置不同的条件，在编译时编译不同的代码，预编译指令中的表达式与C语言本身的表达式基本一至如逻辑运算、算术运算、位运算等均可以在预编译指令中使用。之所以能够实现条件编译是因为预编译指令是在编译之前进行处理的，通过预编译进行宏替换、条件选择代码段，然后生成最后的待编译代码，最后进行编译。

     #if的一般含义是，如果#if后面的常量表达式为true，则编译它所控制的代码，如条件1成立时就代码段1，条件1不成立再看条件2是否成立，如果条件2成立则编译代码段2，否则再依次类推判断其它条件，如果条件1－N都不成力则会编译最后的代码段n+1.

     2、#ifdef， #else， #endif或#ifndef， #else， #endif

     条件编译的另一种方法是用#ifdef与#ifndef命令，它们分别表示“如果有定义”及“如果无定义”。有定义是指在编译此段代码时是否有某个宏通过 #define 指令定义的宏，#ifndef指令指找不到通过#define定义的某宏，该宏可以是在当前文件此条指令的关面定义的，也可以是在其它文件中，但在此指令之前包含到该文件中的。
     #ifdef的一般形式是：

  #ifdef macro_name
       代码段　1
#else
       代码段　2
#endif

或
#ifdef的一般形式是：

#ifndef macro_name
       代码段　2
#else
       代码段　1
#endif

     这两段代码的效果是完全一样的。

     3、通过宏函数defined（macro_name）

     参数为宏名（无需加""），如果该macro_name定义过则返回真，否则返回假，用该函数则可以写比较复杂的条件编译指令如

  #if defined(macro1) || (!defined(macro2) && defined(macro3))
...
#else
...
#endif

     （二）条件编译技巧与示例

     （1）#ifdef和#defined（）比较

     首先比较一下这两种方法，第一种方法只能判断一个宏，如果条件比较复杂实现起来比较烦锁，用后者就比较方便。如有两个宏MACRO_1，MACRO_2只有两个宏都定义过才会编译代码段A，分别实现如下：

  #ifdef MACRO_1
#ifdef MACRO_2
       代码段　A
#endif
#endif

或者
#if defined(MACRO_1) && defined(MACRO_2)
#endif

     同样，要实现更复杂的条件用#ifdef更麻烦，所以推荐使用后者，因为即使当前代码用的是简单的条件编译，以后在维护、升级时可能会增加，用后者可维护性较强。旧的编译器可能没有实现#defined（）指令，C99已经加为标准。要兼容老的编译器，还需用#ifdef指令。

     2、#if与 #ifdef或#if　defined（）比较

     比如自己写了一个printf函数，想通过一个宏MY_PRINTF_EN实现条件编译，用#if可实现如下

  #define MY_PRINTF_EN 1

#if MYS_PRINTF_EN == 1
  int printf(char* fmt, char* args, ...)
{
       ...
}
#endif

     如果宏MY_PRINTF_EN定义为1则编译这段代码，如果宏定义不为1或者没有定义该宏，则不编译这段代码。同样也可以通过#ifdef或者#defined（）实现，如

  #define MY_PRINTF_EN 1

#if defined(MY_PRINTF_EN)
  int printf(char* fmt, char* args, ...)
{
       ...
}
#endif

     在这种情况下两种方法具有异曲同工之妙，但试想如果你为了节约代码写了两个printf函数，在不同情况下使用不同的printf函数，一个是精简版一个是全功能标准版，如：

  #define MY_PRINTF_SIMPLE

#ifdef MY_PRINTF_SIMPLE
     void printf(*str) // 向终端简单地输出一个字符串
{...
}
#endif
#ifdef MY_PRINTF_STANDARD
  int printf(char* fmt, char* args, ...)
{...
}
#endif

同样可以用#if defined()实现
#define MY_PRINTF_SIMPLE

#if defined(MY_PRINTF_SIMPLE)
     void printf(*str) // 向终端简单地输出一个字符串
{
       ...
}
#elif defined(MY_PRINTF_STANDARD)
  int printf(char* fmt, char* args, ...)
{
       ...
}
#endif

     两种方法都可以实现，但可见后者更方便。但试想如果你有三个版本，用前者就更麻烦了，但方法相似，用后者就更方便，但仍需三个宏进行控制，你要住三个宏，改进一下就用#if可以用一个宏直接控制N种情况如：

  #define MY_PRINTF_VERSION　　　　 1

#if MY_PRINTF_VERSION == 1
     void printf(*str) // 向终端简单地输出一个字符串
{
       ...
}
#elif MY_PRINTF_VERSION == 2
  int printf(char* fmt, char* args, ...)
{
       ...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
       ...
}
#else
       默认版本
#endif

     这样，你只需修改一下数字就可以完成版本的选择了

     看来好像用#if 比较好了，试想如下情况：你写了一个配置文件叫做config.h用来配置一些宏，通过这些宏来控制代码，如你在config.h的宏

     #define MY_PRINTF_EN 1

     来控制是否需要编译自己的printf函数，而在你的源代码文件printf.c中有如下指令

  ＃i nclude "config.h"
#if MY_PRINTF_EN == 1
  int printf(char* fmt, char* args, ...)
{
       ...
}
#endif

     但这样也会有一个问题，就是如果你忘了在config.h中添加宏MY_PRINTF_EN，那么自己写的printf函数也不会被编译，有些编译器会给出警告：MY_PRINTF_EN未定义。如果你有两个版本的想有一个默认版本，可以在printf.c中这样实现

  #incldue "config.h"
#if !defined(MY_PRINTF_VERSION)
   #define MY_PRINTF_VERSION　　 1
#endif

#if MY_PRINTF_VERSION == 1
     void printf(*str) // 向终端简单地输出一个字符串
{
       ...
}
#elif MY_PRINTF_VERSION == 2
  int printf(char* fmt, char* args, ...)
{
       ...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
       ...
}
#endif

     再试想，如果软件升级了，或者有了大的改动，原来有三个版本，现在只剩下两个版本了，如

  #if MY_PRINTF_VERSION == 2
  int printf(char* fmt, char* args, ...)
{
       ...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
       ...
}
#endif

     因为这些核心代码不想让使用这些代码的人关心，他们只需要修改config.h文件，那就要在printf.c中实现兼容性。如果以前有人在config.h配置宏MY_PRINTF_VERSION为1，即有

     #define MY_PRINTF_VERSION　　 1

     而现在没有1版本了，要想兼容怎么办？那当然可以用更复杂的条件实现如：

  #if MY_PRINTF_VERSION == 2 || MY_PRINTF_VERSION == 1
  int printf(char* fmt, char* args, ...)
{
       ...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
       ...
}
#endif

     不过还有另外一种方法，即使用#undef命令

  #if MY_PRINTF_VERSION == 1
   #undef MY_PRINTF_VERSION
   #define MY_PRINTF_VERSION　 2
#endif
#if MY_PRINTF_VERSION == 2
  int printf(char* fmt, char* args, ...)
{
       ...
}
#elif MY_PRINTF_VERSION == 3
int printf(unsigned char com_number, char* str)
{
       ...
}
#endif

     用#if还有一个好处，如果你把宏名记错了，把MY_PRINTF_EN定义成了MY_PRINT_EN，那么你用#ifdef MY_PRINTF_EN或者#if defined（MY_PRINTF_EN）控制的代码就不能被编译，查起来又不好查，用#if MY_PRINTF_EN ==1控制就很好查，因为你把MY_PRINTF_EN定义成MY_PRINT_EN，则MY_PRINTF_EN实际上没有定义，那么编译器会给出警告#if MY_PRINTF_EN　== 1中的MY_PRINTF_EN没有定义，但错就比较快。


1 浮点数的表示

通常，我们可以用下面的格式来表示浮点数

S

P

M

其中S是符号位，P是阶码，M是尾数

对于IBM-PC而言，单精度浮点数是32位（即4字节）的，双精度浮点数是64位（即8字节）的。两者的S，P，M所占的位数以及表示方法由下表可知

S | P | M | 表示公式 | 偏移量
1 | 8 | 23 | (-1)S*2(P-127)*1.M | 127
1 | 11 | 52 | (-1)S*2(P-1023)*1.M | 1023

以单精度浮点数为例，可以得到其二进制的表示格式如下

S(第31位)

P(30位到23位)

M(22位到0位)

其中S是符号位，只有0和1，分别表示正负；P是阶码，通常使用移码表示（移码和补码只有符号位相反，其余都一样。对于正数而言，原码，反码和补码都一样；对于负数而言，补码就是其绝对值的原码全部取反，然后加1.）

为了简单起见，本文都只讨论单精度浮点数，双精度浮点数也是用一样的方式存储和表示的。

2 浮点数的表示约定

单精度浮点数和双精度浮点数都是用IEEE754标准定义的，其中有一些特殊约定。

（1）当P = 0, M = 0时，表示0。

（2）当P = 255, M = 0时，表示无穷大，用符号位来确定是正无穷大还是负无穷大。

（3）当P = 255, M != 0时，表示NaN（Not a Number，不是一个数）。

当我们使用.Net Framework的时候，我们通常会用到下面三个常量

Console.WriteLine(float.MaxValue); // 3.402823E+38
Console.WriteLine(float.MinValue); //-3.402823E+38
Console.WriteLine(float.Epsilon); // 1.401298E-45
//如果我们把它们转换成双精度类型，它们的值如下
Console.WriteLine(Convert.ToDouble(float.MaxValue)); // 3.40282346638529E+38
Console.WriteLine(Convert.ToDouble(float.MinValue)); //-3.40282346638529E+38
Console.WriteLine(Convert.ToDouble(float.Epsilon)); // 1.40129846432482E-45

那么这些值是如何求出来的呢？

根据上面的约定，我们可以知道阶码P的最大值是11111110（这个值是254，因为255用于特殊的约定，那么对于可以精确表示的数来说，254就是最大的阶码了）。尾数的最大值是11111111111111111111111。

那么这个最大值就是：0 11111110 11111111111111111111111。

也就是 2(254-127) * (1.11111111111111111111111)2 = 2127 * (1+1-2-23) = 3.40282346638529E+38

从上面的双精度表示可以看出，两者是一致的。最小的数自然就是-3.40282346638529E+38。

对于最接近于0的数，根据IEEE754的约定，为了扩大对0值附近数据的表示能力，取阶码P = -126，尾数 M = (0.00000000000000000000001)2 。此时该数的二进制表示为：0 00000000 00000000000000000000001

也就是2-126 * 2-23 = 2-149 = 1.40129846432482E-45。这个数字和上面的Epsilon是一致的。

如果我们要精确表示最接近于0的数字，它应该是 0 00000001 00000000000000000000000

也就是：2-126 * (1+0) = 1.17549435082229E-38。

3 浮点数的精度问题

浮点数以有限的32bit长度来反映无限的实数集合，因此大多数情况下都是一个近似值。同时，对于浮点数的运算还同时伴有误差扩散现象。特定精度下看似相等的两个浮点数可能并不相等，因为它们的最小有效位数不同。

由于浮点数可能无法精确近似于十进制数，如果使用十进制数，则使用浮点数的数学或比较运算可能不会产生相同的结果。

如果涉及浮点数，值可能不往返。值的往返是指，某个运算将原始浮点数转换为另一种格式，而反向运算又将转换后的格式转换回浮点数，且最终浮点数与原始浮点数相等。由于一个或多个最低有效位可能在转换中丢失或更改，往返可能会失败。

4 将浮点数表示为二进制

4.1 无小数的浮点数转换成二进制表示

首先，我们用一个不带小数的浮点数来说明如何将一个浮点数转换成二进制表示。假设要转换的数据是 45678.0f。

在处理这种不带小数的浮点数时，直接将整数部分转化为二进制表示：

1011001001101110.0，这时要加上一位默认的1（这是因为按照浮点数规格化的要求，尾数必须化成 1.M的格式），

那么可以表示成：11011001001101110.0。

然后将小数点向左移，一直移到离最高位只有1位，也就是 1.1011001001101110，一共移动了16位，我们知道，左移位表示乘法，右移位表示除法。所以原数就等于这样：1.1011001001101110 * ( 216 )。现在尾数和指数都出来了。因为最高位的1是根据标准加上去的，只是为了满足规格化的要求，这时候需要把这个1去掉。尾数的二进制就变成了：1011001001101110。

最后在尾数的后面补0，一直到补够23位，就是：10110010011011100000000。

再回来看指数，根据前面的定义，P-127=16，那么P = 143，表示成二进制就是：10001111。

45678.0f这个数是正的，所以符号位是0，那么我们按照前面讲的格式把它拼起来，就是：0 10001111 10110010011011100000000。

这就是 45678.0f 这个数的二进制表示，如果我们要得到16进制的表示，非常简单，我们只需要把这个二进制串4个一组，转换成16进制数就可以了。但是要注意的是x86架构的CPU都是Little Endian的（也就是低位字节在前，高位字节在后），所以在实际内存中该数字是按上面二进制串的倒序存储的。要知道CPU是不是little endian的也很容易。

BitConverter.IsLittleEndian;

4.2 含小数的浮点数表示为二进制

对于含小数的浮点数，会有精度的问题，下面举例说明。假设要转换的小数为 123.456f。

对于这种带小数的就需要把整数部和小数部分开处理。对于整数部分的处理不再赘述，直接化成二进制为：100100011。小数部份的处理比较麻烦一些，我们知道，使用二进制表示只有0和1，那么对于小数就只能用下面的方式来表示：

a1*2-1+a2*2-2+a3*2-3+......+an*2-n

其中a1等数可以是0或者1，从理论上将，使用这种表示方法可以表示一个有限的小数。但是尾数只能有23位，那么就必然会带来精度的问题。

在很多情况下，我们只能近似地表示小数。来看0.456这个十进制纯小数，该如何表示成二进制呢？一般说来，我们可以通过乘以2的方法来表示。

首先，把这个数字乘以2，小于1，所以第一位为0，然后再乘以2，大于1，所以第二位为1，将这个数字减去1，再乘以2，这样循环下去，直到这个数字等于0为止。

在很多情况下，我们得到的二进制数字都大于23位，多于23位的就要舍去。舍入原则是0舍1入。通过这样的办法，我们可以得到二进制表示：1111011.01110100101111001。

现在开始向左移小数点，一共移了6位，这时候尾数为：1.11101101110100101111001，阶码为6加上127得131，二进制表示为：10000101，那么总的二进制表示为：

0 10000101 11101101110100101111001

表示成十六进制是：42 F6 E9 79

由于CPU是Little Endian的，所以在内存中表示为：79 E9 F6 42。

4.3 将纯小数表示成二进制

对于纯小数转化为二进制来说，必须先进行规格化。例如0.0456，我们需要把它规格化，变为1.xxxx * （2n ）的形式，要求得纯小数X对应的n可用下面的公式：
n = int( 1 + log 2X )

0.0456我们可以表示为1.4592乘以以2为底的-5次方的幂，即1.4592 * ( 2-5 )。转化为这样形式后，再按照上面处理小数的方法处理，得到二进制表示

1. 01110101100011100010001

去掉第一个1，得到尾数

01110101100011100010001

阶码为：-5 + 127 = 122，二进制表示为

0 01111010 01110101100011100010001

最后转换成十六进制
11 C7 3A 3D

5 浮点数的数学运算

5.1 浮点数的加减法

设两个浮点数 X=Mx*2Ex ，Y=My*2Ey

实现X±Y要用如下5步完成：

（1）对阶操作：小阶向大阶看齐
（2）进行尾数加减运算
（3）规格化处理：尾数进行运算的结果必须变成规格化的浮点数，对于双符号位（就是使用00表示正数，11表示负数，01表示上溢出，10表示下溢出）的补码尾数来说，就必须是
001×××…×× 或110×××…××的形式
若不符合上述形式要进行左规或右规处理。
（4）舍入操作：在执行对阶或右规操作时常用“0”舍“1”入法将右移出去的尾数数值进行舍入，以确保精度。
（5）判结果的正确性：即检查阶码是否溢出

若阶码下溢（移码表示是00…0），要置结果为机器0；
若阶码上溢（超过了阶码表示的最大值）置溢出标志。

现在用一个具体的例子来说明上面的5个步骤

例题：假定X=0 .0110011*211，Y=0.1101101*2-10（此处的数均为二进制），计算X+Y；

首先，我们要把这两个数变成2进制表示，对于浮点数来说，阶码通常用移码表示，而尾数通常用补码表示。

要注意的是-10的移码是00110
[X]浮： 0 1 010 1100110
[Y]浮： 0 0 110 1101101
符号位阶码 尾数

（1）求阶差：│ΔE│=|1010-0110|=0100

（2）对阶：Y的阶码小，Y的尾数右移4位
[Y]浮变为 0 1 010 0000110 1101暂时保存

（3）尾数相加，采用双符号位的补码运算
00 1100110
+00 0000110
00 1101100

（4）规格化：满足规格化要求

（5）舍入处理，采用0舍1入法处理

故最终运算结果的浮点数格式为： 0 1 010 1101101

即X+Y=+0. 1101101*210

5.2 浮点数的乘除法

（1）阶码运算：阶码求和（乘法）或阶码求差（除法）
即 [Ex+Ey]移= [Ex]移+ [Ey]补
[Ex-Ey]移= [Ex]移+ [-Ey]补
（2）浮点数的尾数处理：浮点数中尾数乘除法运算结果要进行舍入处理

例题：X=0 .0110011*211，Y=0.1101101*2-10 求X*Y

解：[X]浮： 0 1 010 1100110
[Y]浮： 0 0 110 1101101

（1）阶码相加
[Ex+Ey]移=[Ex]移+[Ey]补=1 010+1 110=1 000
1 000为移码表示的0

（2）原码尾数相乘的结果为：
0 10101101101110

（3）规格化处理：已满足规格化要求，不需左规，尾数不变，阶码不变。

（4）舍入处理：按舍入规则，加1进行修正

所以 X※Y= 0.1010111*20

/******************************************************************************************
*【Author】：flyingbread
*【Date】：2007年3月2日
*【Notice】：
*1、本文为原创技术文章，首发博客园个人站点(http://flyingbread.cnblogs.com/)，转载和引用请注明作者及出处。
*2、本文必须全文转载和引用，任何组织和个人未授权不能修改任何内容，并且未授权不可用于商业。
*3、本声明为文章一部分，转载和引用必须包括在原文中。
*4、本文参考了网络上的若干资料，不一一列举，但是一并致谢。


隐式调用

    隐式调用

    基于事件的隐式调用风格的思想是构件不直接调用一个过程，而是触发或广播一个或多个事件。系统中的其它构件中的过程在一个或多个事件中注册，当一个事件被触发，系统自动调用在这个事件中注册的所有过程，这样，一个事件的触发就导致了另一模块中的过程的调用。

    从体系结构上说，这种风格的构件是一些模块，这些模块既可以是一些过程，又可以是一些事件的集合。过程可以用通用的方式调用，也可以在系统事件中注册一些过程，当发生这些事件时，过程被调用。

    基于事件的隐式调用风格的主要特点是事件的触发者并不知道哪些构件会被这些事件影响。这样不能假定构件的处理顺序，甚至不知道哪些过程会被调用，因此，许多隐式调用的系统也包含显式调用作为构件交互的补充形式。

    支持基于事件的隐式调用的应用系统很多。例如，在编程环境中用于集成各种工具，在数据库管理系统中确保数据的一致性约束，在用户界面系统中管理数据，以及在编辑器中支持语法检查。例如在某系统中，编辑器和变量监视器可以登记相应Debugger的断点事件。当Debugger在断点处停下时，它声明该事件，由系统自动调用处理程序，如编辑程序可以卷屏到断点，变量监视器刷新变量数值。而Debugger本身只声明事件，并不关心哪些过程会启动，也不关心这些过程做什么处理。

    隐式调用系统的主要优点有：

    （1）为软件重用提供了强大的支持。当需要将一个构件加入现存系统中时，只需将它注册到系统的事件中。

    （2）为改进系统带来了方便。当用一个构件代替另一个构件时，不会影响到其它构件的接口。

    隐式调用系统的主要缺点有：

    （1）构件放弃了对系统计算的控制。一个构件触发一个事件时，不能确定其它构件是否会响应它。而且即使它知道事件注册了哪些构件的构成，它也不能保证这些过程被调用的顺序。

    （2）数据交换的问题。有时数据可被一个事件传递，但另一些情况下，基于事件的系统必须依靠一个共享的仓库进行交互。在这些情况下，全局性能和资源管理便成了问题。

    （3）既然过程的语义必须依赖于被触发事件的上下文约束，关于正确性的推理存在问题。


●共用体站用的空间为最大的那个成员

●系统为所有共用体成员分配同一地址开始的储存空间,使用覆盖的方式共享储存空间

●共用体变量中,所有的成员首地址相同,

union u_tp

{

struct

{

unsigned char low,high;

}b;

unsigned w;

}r;

r.b.low存取r.w的低字节的值

r.b.high存取r.w高字节的值.

r.w一次存取双字节的值.

为什么low存取低字节high存取高字节,而不是反过来,偶觉得可能是先定义的先分配内存,所以得到低字节吧,

还可以这样,

union u_tp1

{

struct char c[2];

unsigned w;

}r1;

r1.c[0]存取r1.w低字节的值

r1.c[1]存取r1.w高字节的值

#include <stdio.h>

void main()

{

union u_type

{

unsigned char c[2];

unsigned w;

}u;

u.w = 0x6142;

//0x61是字母'a'的ascii码 ,0x42是字母'B'

//c[0]储存0x42 ('B') , c[1]储存 0x61 ('a')

printf("%c,%c,\n",u.c[0],u.c[1]);

}

//输出B,a,

Press any key to continue

c[0]储存0x42 ('B',66) , c[1]储存 0x61 ('a',97)

0x6142 =( 0110 0001 0100 0010 )

//高8位 低8位

//10进制97 10进制66

printf("%c,%c,",66,97);//输出:B,a

偶觉得可能是先定义的先分配内存,所以得到低字节吧//这句不知道正不正确,达人赐教

交换高低字节的Hack技巧

Big Endian和Little Endian的转换过程中需要交换高低字节。

偶们一般是怎么处理的呢？举个简单的例子：

typedef unsigned char BYTE;

typedef unsigned short WORD;

#define BYTE_LENGTH 8

WORD var_test, var_result;

BYTE high, low;

high = (var_test & 0xFF00) >> BYTE_LENGTH;

low = var_test & 0x00FF;

var_result = ( low <<BYTE_LENGTH ) | high;

这是最基本的做法，也是最保险的做法。那么Hack技巧是什么？请看：

var_result = (var_test << BYTE_LENGTH ) | ( var_test >> BYTE_LENGTH );

是不是又简洁，又好记，还不容易写错？^_^

当然，这是建立在C编译器会在移位的空位上自动补0的基础上的。如果C编译器不自动补0，那可就难说了。不过，偶还没有见过不自动补0的C编译器，如果你见到过，请告诉偶呀！:)

-----------------------------------------------------
2005-04-08     关于移位补位的说明

以下摘自Andrew Koenig的《C Traps and Pit falls 》(中文版《C陷阱与缺陷》）：
在向右移位时，空出的位是由0填充，还是由符号位的副本填充？
如果移位的对象是无符号数，那么空出的位将被0填充。如果被移位的对象是有符号数，那么C语言的实现既可以用0填充空出的位，也可以用符号位的副本填充空出的位。编程者如果关注向右移位时空出的位，那么可以将操作数的变量声明为无符号类型，那么空出的位都会被设置为0。

----------- VERSION HISTORY -----------------
2005-03-25      修改了rovershen提出的2处笔误。:)
2005-04-08      明确了所有移位变量为unsigned的类型，添加了Andrew对移位部位的观点。


高位字节优先 低位字节优先

自第一台计算机诞生，其最小存储单元就被永久的定格了——一个由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 {
int a:3;
int b:7;
int c:13;
int d:9;
};

这个结构体的一个对象会占用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，其方案会带来很大的问题。其分配顺序为：

字节物理顺序：从低到高；
字节内bit顺序：从高到底；

也就是说，big endian在bitwise的分配方案上，从字节顺序到bit顺序都反过来了（因为其正向存储顺序为：字节从高到底，bit从低到高（从程序员的观点看））。换句话说：big endian的bit分配顺序为，按照bit的逻辑顺序，从高到底进行分配。

                     |--------|--------|--------|--------|
Logical Byte Order   | byte 3 | byte 2 | byte 1 | byte 0 |
                     |--------|--------|--------|--------|

Bitwise allocation   |-a-|---b---|------c------|----d----|

请注意，并不是硬件平台使用的这种方案，而是C语言编译器。这是一种荒谬的方案，我想可能是C语言编译器的早期开发者希望通过编译器屏蔽掉big endian和little endian在bitwise allocation上的差异，而都与物理存储顺序一致。但由于其采用了bit order的反向分配，反而加剧了这种差异，随后的编译器为了保持兼容，也只好将错误延续了下来。

基于这种原因，在C语言中直接使用bitwise的方式定义结构体是一种危险的方式，因为这些代码是平台依赖的。当进行跨平台移植的时候必须重新定义这些结构体。

有两种方式可以消除这种风险：

1、使用逻辑移位的方式来操作bit；以上面的例子为例，我们可以这么做：

struct foo {
int value;
};

#define SET_A(f,a)  do { (f) |= ((a)&0x7); } while(0)
#define SET_B(f,b)  do { (f) |= (((b)&0x7F)<<3); } while(0)
#define SET_C(f,c)  do { (f) |=
(((c)&0x1FFF)<<10); } while(0)
#define SET_D(f,d)  do { (f) |=
(((d)&0x1FF)<<23); } while(0)

#define
GET_A(f)  ((f)&0x7)
#define GET_B(f)  (((f)>>3)&0x7F)
#define GET_C(f)  (((f)>>10)&0x1FFF)
#define GET_D(f)  (((f)>>23)&0x1FF)

2、对于big endian,我们可以使用相反的顺序来声明bitwise fields。仍然以上例为例：

#if LITTLE_ENDIAN
#define BITWISE(type,a,b,c,d) type a, b, c, d
#else
#define BITWISE(type,a,b,c,d) type d, c, b, a
#endif

struct foo {
BITWISE(int, a:3, b:7, c:13, d:9);
};

对于little endian，逻辑顺序与物理顺序一致，只需要按照原样定义；而对于big endian，由于其整体的bit顺序恰好与逻辑顺序是相反的，所以，我们将顺序反过来，使其bit的分配顺序与逻辑顺序一致即可。