《C Primer Plus》学习笔记—第15章

阅读: 评论:0

《C Primer Plus》学习笔记—第15章

《C Primer Plus》学习笔记—第15章

目录

  • 《C Primer Plus》学习笔记
    • 第15章 位操作
      • 1.二进制数、位和字节
        • 1.二进制整数
        • 2.有符号整数
        • 3.二进制浮点数
          • 1.二进制小数
          • 2.浮点数表示法
      • 2.其他进制数
        • 1.八进制
        • 2.十六进制
      • 3.C按位运算符
        • 1.按位逻辑运算符
          • 1.二进制反码或按位取反: ~
          • 2.按位与: &
          • 3.按位或: |
          • 4.按位异或: ^
        • 2.用法:掩码
        • 3.用法:打开位(设置位)
        • 4.用法:关闭位(清空位)
        • 5.用法:切换位
        • 6.用法:检查位的值
        • 7.移位运算符
          • 1.左移:<<
          • 2.右移:>>
          • 3.用法:移位运算符
        • 8.编程示例
        • 9.另一个例子
          • 1.程序invert4.c
      • 4.位字段
        • 1.位字段示例
          • 1.程序fields.c
        • 2.位字段和按位运算符
          • 1.程序dualview.c
      • 5.对齐特性(C11)
        • 1.程序align.c
      • 6.关键概念
      • 7.本章小结
      • 8.编程练习

《C Primer Plus》学习笔记

第15章 位操作

在C语言中,可以单独操控变量中的位。有时必须单独操控位,例如,通常向硬件设备发送一两个字节来控制这些设备,其中每个位(bit)都有特定的含义。另外,与文件相关的操作系统信息经常被储存,通过使用特定位表明特定项。许多压缩和加密操作都是直接处理单独的位。C在提供高级语言便利的同时,还能在为汇编语言所保留的级别上工作,这使其成为编写设备驱动程序和嵌入式代码的首选语言。

1.二进制数、位和字节

通常都是基于数字10来书写数字。例如2157的千位是2,百位是1,十位是5,个位是7,可以写成:
2x1000 + 1x100 + 5x10 + 7x1
注意,1000是10的立方(即3次幂),100是10的平方(即2次幂),10是10的1次幂,而且10(以及任意正数)的0次幂是1。因此,2157也可以写成:
2x10³ + 1x10² + 5x10¹ + 7x10º
因为这种书写数字的方法是基于10的幂,所以称以10为基底书写2157。
从某种意义上看,计算机的位只有2根手指,因为它只能被设置为0或1,关闭或打开。因此,计算机适用基底为2的数制系统。它用2的幂而不是10的幂。以2为基底表示的数字被称为二进制数(binary mumber)。二进制中的2和十进制中的10作用相同。例如,二进制数1101可表示为:
1x2³ + 1x2² + 0x2¹ + 1x2°
以十进制数表示为:
1x8 +1x4+0x2+1x1= 13
用二进制系统可以把任意整数(如果有足够的位)表示为0和1的组合。由于数字计算机通过关闭和打开状态的组合来表示信息,这两种状态分别用0和1来表示。

1.二进制整数

通常,1字节包含8位。C语言用字节(byte)表示储存系统字符集所需的大小,所以C字节可能是8位、9位、16位或其他值。不过,描述存储器芯片和数据传输率中所用的字节指的是8位字节。假设1字节是8位(计算机界通常用八位组(octet) 这个术语特指8位字节)。可以从左往右给这8位分别编号为7~0。在1字节中,编号是7的位被称为高阶位(high-order bit),编号是0的位被称为低阶位(low-order bit)。每1位的编号对应2的相应指数。因此,可以根据图15.1所示的例子理解字节。

这里,128是2的7次幂,以此类推。该字节能表示的最大数字是把所有位都设置为1: 11111111这个二进制数的值是:
128+64+32+16+8+4+2+1=255
而该字节最小的二进制数是00000000,其值为0。因此, 1字节可储存0~255范围内的数字,总共256个值。或者,通过不同的方式解释位组合(bit pattern),程序可以用1字节储存-128~+127范围内的整数,总共还是256个值。例如,通常unsigned char用1字节表示的范围是0~255,而signed char用1字节表示的范围是-128~+127。

2.有符号整数

如何表示有符号整数取决于硬件,而不是C语言。也许表示有符号数最简单的方式是用1位(如,高阶位)储存符号,只剩下7位表示数字本身(假设储存在1字节中)。用这种符号量(sign-magniude) 表示法,00000001表示-1, 10000001表示1。因此,其表示范围是-127~+127。
这种方法的缺点是有两个0: +0和-0。这很容易混淆,而且用两个位组合来表示一个值也有些浪费。

二进制补码(two s-complement)方法避免了这个问题,是当今最常用的系统。以1字节为例,讨论这种方法。二进制补码用1字节中的后7位表示0~127,高阶位设置为0。目前,这种方法和符号量的方法相同。另外,如果高阶位是1,表示的值为负。这两种方法的区别在于如何确定负值。从一个9位组合100000000(256 的二进制形式)减去一个负数的位组合,结果是该负值的量。例如,假设一个负值的位组合是10000000,作为一个无符号字节,该组合为表示128; 作为一个有符号值,该组合表示负值(编码是7的位为1),而且值为100000000-10000000,即10000000(128)。因此,该数是-128(在符号量表示法中,该位组合表示-0)。类似地,10000001是-127,11111111是-1。该方法可以表示-128~+127范围内的数。
要得到一个二进制补码数的相反数,最简单的方法是反转每一位(即0变为1,1变为0),然后加1。因为1是00000001,那么-1则是11111110+1,或11111111。这与上面的介绍一致。

二进制反码(one s-complement)方法通过反转位组合中的每一位形成一个负数。例如,00000001是1,那么11111110是-1。这种方法也有一个-0: 11111111该方法能表示-127~+127之间的数。

3.二进制浮点数

浮点数分两部分储存:二进制小数和二进制指数。

1.二进制小数

一个普通的浮点数0.527,表示如下:
5/10 + 2/100 + 7/1000
从左往右,各分母都是10的递增次幂。在二进制小数中,使用2的幂作为分母,所以二进制小数.101表示为:
1/2+0/4+1/8
用十进制表示法为:
0.50 + 0.00 + 0.125
即是0.625。
许多分数(如,1/3) 不能用十进制表示法精确地表示。与此类似,许多分数也不能用二进制表示法准确地表示。实际上,二进制表示法只能精确地表示多个1/2的幂的和。因此,3/4和7/8可以精确地表示为二进制小数,但是1/3和2/5却不能。

2.浮点数表示法

为了在计算机中表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位储存指数。
一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。例如,一个浮点数乘以4,那么二进制小数不变,其指数乘以2,二进制分数不变。如果一份浮点数乘以一个不是2的幂的数,会改变二进制小数部分,如有必要,也会改变指数部分。

2.其他进制数

计算机界通常使用八进制记数系统和十六进制记数系统。因为8和16都是2的幂,这些系统比十进制系统更接近计算机的二进制系统。

1.八进制

八进制(octal)是指八进制记数系统。该系统基于8的幂,用07表示数字(正如十进制用09表示数字一样)。例如,八进制数451(在C中写作0451)表示为:
4x8²+5x8¹+1x8°=297(十进制).
了解八进制的一个简单的方法是,每个八进制位对应3个二进制位。表15.1列出了这种对应关系。这种关系使得八进制与二进制之间的转换很容易。例如,八进制数0377的二进制形式是11111111。即,用111代替0377中的最后一个7,再用111代替倒数第2个7,最后用011代替3,并舍去第1位的0。这表明比0377大的八进制要用多个字节表示。这是八进制唯一不方便的地方:一个3位的八进制数可能要用9位二进制数来表示。注意,将八进制数转换为二进制形式时,不能去掉中间的0。例如,八进制数0173的二进制形式是01111011,不是0111111。

2.十六进制

十六进制(hexadecimal或hex)是指十六进制记数系统。该系统基于16的幂,用015表示数字。但是,由于没有单独的数(digit,即09这样单独一位的数)表示1015,所以用字母AF来表示。例如,十六进制数A3F(在C中写作0xA3F)表示为:
10x162 +3x16’+ 15x16° = 2623(十进制)
由于A表示10, F表示15。在C语言中,A~F既可用小写也可用大写。因此,2623也可写作0xa3f。
每个十六进制位都对应一个4位的二进制数(即4个二进制位),那么两个十六进制位恰好对应一个8位字节。第1个十六进制表示前4位,第2个十六进制位表示后4位。因此,十六进制很适合表示字节值。
表15.2列出了各进制之间的对应关系。例如,十六进制值0xC2可转换为11000010。相反,二进制值11010101可以看作是1101 0101,可转换为0xD5。

C有两个操控位的工具。第1个工具是一套(6个)作用于位的按位运算符第2个工具是字段(field)数据形式,用于访问int中的位。

3.C按位运算符

C提供按位逻辑运算符和移位运算符。在下面的例子中,为了方便读者了解位的操作,我们用二进制记数法写出值。但是在实际的程序中不必这样,用一般形式的整型变量或常量即可。例如,在程序中用25或031或0x19, 而不是00011001。另外,下面的例子均使用8位二进制数,从左往右每位的编号为7~0。

1.按位逻辑运算符

4个按位逻辑运算符都用于整型数据,包括char之所以叫作按位(binwvise)运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。不要把这些运算符与常规的逻辑运算符(&&、||和!)混淆,常规的逻辑运算符操作的是整个值。

1.二进制反码或按位取反: ~

一元运算符~1变为0,把0变为1。如下例子所示:

~(10011010) // 表达式
(01100101)//结果值

假设val的类型是unsigned char,已被赋值为2。在二进制中,00000010表示2。那么,~val的值是1111101,即253。注意,该运算符不会改变val的值,就像3 * val不会改变val的值一样,val仍然是2。但是,该运算符确实创建了一个可以使用或赋值的新值:

newval = ~val;
printf("%d", ~val);

如果要把val的值改为~val,使用下面这条语句:

val = ~val;
2.按位与: &

二元运算符&通过逐位比较两个运算对象,生成一个新值。 对于每个位,只有两个运算对象中相应的位都为1时,结果才为1(从真/假方面看,只有当两个位都为真时,结果才为真)。因此,对下面的表达式
求值:

(10010011) & (00111101)//表达式

由于两个运算对象中编号为4和0的位都为1,得:

(00010001) //结果值

C有一个按位与和赋值结合的运算符:&=。下面两条语句产生的最终结果相同:

val &= 0377;
val = val & 0377;
3.按位或: |

二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1(从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真)。因此,对下面的表达式求值:

(10010011) | (00111101) // 表达式

除了编号为6的位,这两个运算对象的其他位至少有一个位为1,得:

(10111111) // 结果值

C有一个按位或和赋值结合的运算符:|=。下面两条语产生的最终作用相同:

val |= 0377;
val = val| 0377;
4.按位异或: ^

二元运算符^逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1(但不是两个为1),结果为1(从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个为同为1,那么结果为真)。因此,对下面表达式求值:

(10010011) ^ (00111101) // 表达式

编号为0的位都是1,所以结果为0,得:

(10101110) // 结果值

C有一个按位异或和赋值结合的运算符:^=。下面两条语句产生的最终作用相同:

val ^= 0377;
val = val ^ 0377;
2.用法:掩码

按位与运算符常用于掩码(mask)。所谓掩码指的是一些设置为开(1)或关(0)的位组合。假设定义符号常量MASK为2(即,二进制形式为00000010),只有1号位是1,其他位都是0。下面的语句:

flags = flags & MASK;

把flags中除1号位以外的所有位都设置为0,因为使用按位与运算符(&)任何位与0组合都得0。1号位的值不变(如果1号位是1,那么1&1得1;如果1号位是0,那么0&1也得0)。这个过程叫作“使用掩码”,因为掩码中的0隐藏了flags中相应的位。
可以这样类比:把掩码中的0看作不透明,1看作透明。表达式flags & MASK相当于用掩码覆盖在flags的位组合上,只有MASK为1的位才可见(见图15.2)。

用&=运算符可以简化前面的代码,如下所示:

flags &= MASK;

下面这条语句是按位与的一种常见用法:

ch &= 0xff;//或者ch&=0377;

前面介绍过oxff的二进制形式是11111111,八进制形式是0377。这个掩码保持ch中最后8位不变,其他位都设置为0。无论ch原来是8位、16 位或是其他更多位,最终的值都被修改为1个8位字节。在该例中,掩码的宽度为8位。.

3.用法:打开位(设置位)

有时,需要打开一个值中的特定位,同时保持其他位不变。例如,一台IBM PC通过向端口发送值来控制硬件。例如,为了打开内置扬声器,必须打开1号位,同时保持其他位不变。这种情况可以使用按位或运算符(|)
以上一节的flags和MASK (只有1号位为1)为例。下面的语句:

flags = flags | MASK;

把flags的1号位设置为1,且其他位不变。因为使用|运算符,任何位与0组合,结果都为本身;任何位与1组合,结果都为1。例如,假设flags是00001111,MASK是10110110。下面的表达式:

flags| MASK

即是:

(00001111) | (10110110)//表达式

其结果为:

(10111111)//结果值

MASK中为1的位,flags 与其对应的位也为1。MASK 中为0的位,flags与其对应的位不变。
用|=运算符可以简化上面的代码,如下所示:

flags |= MASK;

同样,这种方法根据MASK中为1的位,把flags中对应的位设置为1,其他位不变。

4.用法:关闭位(清空位)

和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。假设要关闭变量flags中的1号位。同样,MASK只有1号位为1 (即,打开)。可以这样做:

flags = flags & ~MASK;

由于MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0以外,其他位全为1。使用&,任何位与1组合都得本身,所以这条语句保持1号位不变,改变其他各位。另外,使用&,任何位与0组合都的0。所以无论1号位的初始值是什么,都将其设置为0。
例如,假设flags是00001111,MASK是10110110。下面的表达式:

flags & ~MASK

即是:

(00001111) & ~(10110110) //表达式

其结果为:

(00001001)//结果值

MASK中为1的位在结果中都被设置(清空)为0。flags中与MASK为0的位相应的位在结果中都未改变。
可以使用下面的简化形式:

flags &= ~MASK; 
5.用法:切换位

切换位指的是打开已关闭的位,或关闭已打开的位。可以使用**按位异或运算符()切换位**。也就是说,假设b是一个位(1或0),如果为1,则1b为0;如果b为0,则1b为1。另外,无论b为1还是0,0b均为b。因此,如果使用^组合一个值和一个掩码,将切换该值与MASK为1的位相对应的位,该值与MASK为0的位相对应的位不变。要切换flags中的1号位,可以使用下面两种方法:

flags = flags ^ MASK;
flags ^= MASK;

例如,假设flags是00001111,MASK是10110110,表达式:

flags ^ MASK

即是:

(00001111) ^ (10110110)//表达式

其结果为:

(10111001)//结果值

flags中与MASK为1的位相对应的位都被切换了,MASK为0的位相对应的位不变。

6.用法:检查位的值

有时,需要检查某位的值。例如,flags中1号位是否被设置为1?不能这样直接比较flags和MASK:

if (flags == MASK)
puts("wow!"); //不能正常工作

这样做即使flags的1号位为1,其他位的值会导致比较结果为假。因此,必须覆盖flags中的其他位,只用1号位和MASK比较:

if ((flags & MASK) == MASK)
puts("Wow!");

由于按位运算符的优先级比==低,所以必须在flags & MASK周围加上圆括号。为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。

7.移位运算符

下面介绍C的移位运算符。移位运算符向左或向右移动位。在示例中仍然使用二进制数,有助于理解其工作原理。

1.左移:<<

左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。下面的例子中,每一位都向左移动两个位置:

(10001010) << 2 //表达式
(00101000)//结果值

该操作产生了一个新的位值,但是不改变其运算对象。例如,假设stonk为1,那么stonk << 2为4,但是stonk本身不变,仍为1。可以使用左移赋值运算符(<<=)来更改变量的值。该运算符将变量中的位向左移动其右侧运算对象给定值的位数。如下例:

int stonk = 1;
int onkoo;
onkoo = stonk << 2;//把4赋给onkoo
stonk <<= 2;//把stonk的值改为4
2.右移:>>

右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用0填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用0填充,或者用符号位(即,最左端的位)的副本填充:

(10001010) >> 2//表达式,有符号值
(00100010)//在某些系统中的结果值
(10001010) >> 2//表达式,有符号值
(11100010)//在另一些系统上的结果值

下面是无符号值的例子:

(10001010) >> 2//表达式,无符号值
(00100010)//所有系统都得到该结果值

每个位向右移动两个位置,空出的位用0填充。
**右移赋值运算符(>>=)**将其左侧的变量向右移动指定数量的位数。如下所示:

int sweet = 16;
int ooosw;
ooosw = sweet >> 3;//ooosw = 2,sweet的值仍然为16
sweet >>= 3;// sweet的值为2
3.用法:移位运算符

移位运算符针对2的幂提供快速有效的乘法和除法:
number << n number乘以2的n次幂
number >> n 如果number为非负,则用number除以2的n次幂
这些移位运算符类似于在十进制中移动小数点来乘以或除以10。

移位运算符还可用于从较大单元中提取一些位。 例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节储存红色的强度,下一个字节储存绿色的强度,第3个字节储存蓝色的强度。随后你希望把每种颜色的强度分别储存在3个不同的unsigned char类型的变量中。那么,可以使用下面的语句:

#define BYTE MASK 0xff
unsigned long color = 0x002a162f;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;

以上代码中,使用右移运算符将8位颜色值移动至低阶字节,然后使用掩码技术把低阶字节赋给指定的变量。

8.编程示例

在第9章中,用递归的方法编写了一个程序,把数字转换为二进制形式(程序binary.c)。现在,要用移位运算符来解决相同的问题。程序binbit.c中的程序,读取用户从键盘输入的整数,将该整数和一个字符串地址传递给itobs()函數(itobs表示interger to binary string,即整数转换成:二进制字符串)。然后,该函数使用移位运算符计算出正确的1和0的组合,并将其放入字符串中。

/* binbit.c -- 使用位操作显示二进制 */
#include <stdio.h>
#include <limits.h>  // 提供CHAR_BIT的定义,CHAR_BIT表示每字节的位数 
char * itobs(int, char *);
void show_bstr(const char *);int main(void)
{char bin_str[CHAR_BIT * sizeof(int) + 1];// 8 * 4 + 1int number;puts("Enter integers and see them in binary.");puts("Non-numeric input terminates program.");while (scanf("%d", &number) == 1){itobs(number,bin_str);//假设输入3 printf("%d is ", number);show_bstr(bin_str);putchar('n');}puts("Bye!");return 0;
}char * itobs(int n, char * ps)// 3的二进制:00000000 00000000 00000000 00000011
{int i; const static int size = CHAR_BIT * sizeof(int);// size = 32 for (i = size - 1; i >= 0; i--, n >>= 1)//i = 31开始{ps[i] = (01 & n) + '0';//01在这是掩码,八进制形式,表示十进制1,二进制是00000000 00000000 00000000 00000001//'0'在ascll中表示48,(01 & n)的值为1,那么1+48=49,是字符'1',存入字符数组中//char grade='A';与char grade=65;等效(前提是系统能使用ASCLL码) }        ps[size] = '';return ps;
}/* 4位一组显示二进制字符串 */
void show_bstr(const char * str)
{int i = 0;while (str[i])  /* 不是一个空字符 */{putchar(str[i]);if(++i % 4 == 0 && str[i])putchar(' ');}
}

输出示例:

Enter integers and see them in binary.
Non-numeric input terminates program.
3
3 is 0000 0000 0000 0000 0000 0000 0000 0011
2013
2013 is 0000 0000 0000 0000 0000 0111 1101 1101
-1
-1 is 1111 1111 1111 1111 1111 1111 1111 1111
q
Bye!

程序binbit.c使用limits.h中的CHAR_ BIT宏,该宏表示char中的位数。sizeof运算符返回char的大小,所以表达式CHAE_BIT * sizeof(int)表示int类型的位数。bin_str数组的元素个数是CHAE_BIT * sizeof(int) + 1, 留出一个位置给末尾的空字符。

itobs()函数返回的地址与传入的地址相同,可以把该函数作为printf()的参数。在该函数中,首次执行for循环时,对01 & n求值。01是一个八进制形式的掩码,该掩码除0号位是1之外,其他所有位都为0。因此,01 & n就是n最后一位的值。该值为0或1。但是对数组而言,需要的是字符’0’或字符’1’。该值加上’0’即可完成这种转换(假设按顺序编码的数字,如ASCII)。 其结果存放在数组中倒数第2个元素中(最后一个元素用来存放空字符)。

顺带一提,用1 & n或01 & n都可以。用八进制1而不是十进制1,只是为了更接近计算机的表达方式。

然后,循环执行i–和n >>= 1。i–移动到数组的前一个元素,n >>= 1使n中的所有位向右移动一个位置。进入下一轮迭代时,循环中处理的是n中新的最右端的值。然后,把该值储存在倒数第3个元素中,以此类推。itobs ()函数用这种方式从右往左填充数组。
可以使用printf()或puts()函数显示最终的字符串,但是程序binbit.c中定义了show_bstr()函数,以4位一组打印字符串,方便阅读。

9.另一个例子

这次要编写的函数用于切换一个值中的后n位,待处理值和n都是函数的参数。
~运算符切换一个字节的所有位,而不是选定的少数位。但是,**运算符(按位异或)可用于切换单个位**。假设创建了一个掩码,把后n位设置为1,其余位设置为0。然后使用组合掩码和待切换的值便可切换该值的最后n位,而且其他位不变。方法如下:

cint invert end(int num,int bits)
{int mask = 0;int bitval = 1;while (bits-- > 0){mask |= bitval; bitval <<= 1;}return num ^ mask;
}

while循环用于创建所需的掩码。最初,mask的所有位都为0。第1轮循环将mask的0号位设置为1。然后第2轮循环将mask的1号位设置为1,以此类推。循环bits次,mask的后bits位就都被设置为1。最后,num ^ mask运算即得所需的结果。
把这个函数放入前面的程序中,测试该函数。如程序invert4.c所示。

1.程序invert4.c
/* invert4.c -- 使用位操作显示二进制 */
#include <stdio.h>
#include <limits.h>
char * itobs(int, char *);
void show_bstr(const char *);
int invert_end(int num, int bits);int main(void)
{char bin_str[CHAR_BIT * sizeof(int) + 1];int number;puts("Enter integers and see them in binary.");puts("Non-numeric input terminates program.");while (scanf("%d", &number) == 1){itobs(number,bin_str);printf("%d isn", number);show_bstr(bin_str);putchar('n');number = invert_end(number, 2);printf("Inverting the last 2 bits givesn");show_bstr(itobs(number,bin_str));putchar('n');

本文发布于:2024-01-30 20:38:47,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170661833022682.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:学习笔记   Primer
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23