做网站要有什么功能,ui设计培训多长时间,什么值得买网站模板,网站建设与管理的网页文章目录 第20章 底层程序设计20.1 位运算符20.1.1 移位运算符20.1.2 按位取反运算符、按位与运算符、按位异或运算符和按位或运算符20.1.3 用位运算符访问位20.1.4 用位运算符访问位域20.1.5 程序——XOR加密 20.2 结构中的位域20.2.1 位域是如何存储的 20.3 其他底层技术20.3… 文章目录 第20章 底层程序设计20.1 位运算符20.1.1 移位运算符20.1.2 按位取反运算符、按位与运算符、按位异或运算符和按位或运算符20.1.3 用位运算符访问位20.1.4 用位运算符访问位域20.1.5 程序——XOR加密 20.2 结构中的位域20.2.1 位域是如何存储的 20.3 其他底层技术20.3.1 定义依赖机器的类型20.3.2 用联合来提供数据的多个视角20.3.3 将指针作为地址使用20.3.4 volatile类型限定符 20.4 对象的对齐(C1X)20.4.1 对齐运算符_Alignof(C1X)20.4.2 对齐指定符_Alignas和头stdalign.h(C1X) 问与答写在最后 第20章 底层程序设计
——如果程序要关心不该关心的事那这门语言就是低级的。 前面几章中讨论了C语言中高级的、与机器无关的特性。虽然这些特性对不少程序都够用了但仍有一些程序需要进行位级别的操作。位操作和其他一些底层运算在编写系统程序包括编译器和操作系统、加密程序、图形程序以及其他一些需要高执行速度或高效地利用空间的程序时非常有用。 20.1节介绍C语言的位运算符。位运算符提供了对单个位或位域的方便访问。20.2节介绍如何声明包含位域的结构。最后20.3节描述如何使用一些普通的C语言特性类型定义、联合和指针来帮助编写底层程序。 本章中描述的一些技术需要用到数据在内存中如何存储的知识可能会因不同的机器和编译器而不同。依赖于这些技术很可能会使程序丧失可移植性因此除非必要否则最好尽量避免使用它们。如果确实需要尽量将使用限制在程序的特定模块中不要将其分散在各处。同时最重要的是确保在文档中记录所做的事 20.1 位运算符
C语言提供了6个位运算符。这些运算符可以用于对整数数据进行位运算。这里先讨论2个移位运算符然后再讨论其他4个位运算符按位取反、按位与、按位异或以及按位或。 20.1.1 移位运算符 移位运算符可以通过将位向左或向右移动来变换整数的二进制表示。C语言提供了2个移位运算符见表20-1。 表20-1 移位运算符
符号含义左移位右移位
运算符和运算符的操作数可以是任意整数类型包括char型。这两个运算符对两个操作数都会进行整数提升返回值的类型是左操作数提升后的类型。
i j的值是将i中的位左移j位后的结果。每次从i的最左端溢出一位在i的最右端补一个0位。i j的值是将i中的位右移j位后的结果。如果i是无符号数或非负值则需要在i的左端补0。如果i是负值其结果是由实现定义的一些实现会在左端补0其他一些实现会保留符号位而补1。 可移植性技巧为了可移植性最好仅对无符号数进行移位运算。 下面的例子展示了对数13应用移位运算符的效果简单起见这些例子以及本节中的其他例子使用短整型一般是16位
unsigned short i, j;
i 13; /* i is now 13 (binary 0000000000001101) */
j i 2; /* j is now 52 (binary 0000000000110100) */
j i 2; /* j is now 3 (binary 0000000000000011) */ 如上面的例子所示这两个运算符都不会改变它的操作数。如果要通过移位改变变量需要使用复合赋值运算符和
i 13; /* i is now 13 (binary 0000000000001101) */
i 2; /* i is now 52 (binary 0000000000110100) */
i 2; /* i is now 3 (binary 0000000000000011) */请注意!!移位运算符的优先级比算术运算符的优先级低因此可能产生意料之外的结果。例如i 2 1等同于i (2 1)而不是(i 2) 1。 20.1.2 按位取反运算符、按位与运算符、按位异或运算符和按位或运算符 表20-2列出了余下的位运算符。 表20-2 其他位运算符
符号含义~按位取反按位与^按位异或|按位或
运算符~是一元运算符对其操作数会进行整数提升。其他运算符都是二元运算符对其操作数进行常用的算术转换。
运算符~、、^和|对操作数的每一位执行布尔运算。~运算符会产生对操作数求反的结果即将每一个0替换为1将每一个1替换为0。运算符对两个操作数相应的位执行逻辑与运算。运算符^和|相似都是对两个操作数执行逻辑或运算不同的是当两个操作数的位都是1时^产生0而|产生1。 请注意!!不要将位运算符和|与逻辑运算符和||相混淆。有时候位运算会得到与逻辑运算相同的结果但它们绝不等同。 下面的例子演示了运算符~、、^、|的作用
unsigned short i, j, k;
i 21; /* i is now 21 (binary 0000000000010101) */
j 56; /* j is now 56 (binary 0000000000111000) */
k ~i; /* k is now 65514 (binary 1111111111101010) */
k i j; /* k is now 16 (binary 0000000000010000) */
k i ^ j; /* k is now 45 (binary 0000000000101101) */
k i | j; /* k is now 61 (binary 0000000000111101) */其中对~i所显示的值是基于unsigned short类型的值占有16位的假设。 对运算符~需要特别注意因为它可以帮助我们使底层程序的可移植性更好。假设我们需要一个整数它的所有位都为1。最好的方法是使用~0因为它不会依赖于整数所包含的位的个数。类似地如果我们需要一个整数除了最后5位其他的位全都为1我们可以写成~0x1f。 运算符~、、^和|有不同的优先级(从左往右由最高~到最低|)。
因此可以在表达式中组合使用这些运算符而不必加括号。例如可以写i ~j|k而不需要写成(i (~j))|k同样可以写i ^ j ~k而不需要写成i ^ (j (~k))。当然仍然可以使用括号来避免混淆。 请注意!!运算符、^和|的优先级比关系运算符和判等运算符低。因此下面的语句不会得到期望的结果 if (status 0x4000 ! 0) ... 这条语句会先计算0x4000 ! 0结果是1接着判断status 1是否非0而不是判断status 0x4000是否非0。 复合赋值运算符、^和|分别对应于位运算符、^和|
i 21; /* i is now 21 (binary 0000000000010101) */
j 56; /* j is now 56 (binary 0000000000111000) */
i j; /* i is now 16 (binary 0000000000010000) */
//i j 等价于 i i ji ^ j; /* i is now 40 (binary 0000000000101000) */
//i ^ j 等价于 i i ^ ji | j; /* i is now 56 (binary 0000000000111000) */
//i | j 等价于 i i | j按位取反可以通过使用位取反操作符~和普通赋值操作符来实现而不需要单独的复合赋值运算符且按位取反是一元操作单目运算因此C语言没有提供按位取反赋值运算符。 20.1.3 用位运算符访问位 在进行底层编程时经常会需要将信息存储为单个位或一组位。例如在编写图形程序时可能会需要将两个或更多个像素挤在一个字节中。使用位运算符就可以提取或修改存储在少数几个位中的数据。 假设i是一个16位的unsigned short变量来看看如何对i进行最常用的单位运算。 位的设置。假设我们需要设置i的第4位。假定最高有效位为第15位最低有效位为第0位。设置第4位的最简单方法是将i的值与常量0x0010一个在第4位上为1的“掩码”进行或运算 i 0x0000; /* i is now 0000000000000000 */
i | 0x0010; /* i is now 0000000000010000 */ 更通用的做法是如果需要设置的位的位置存储在变量j中可以使用移位运算符来构造掩码 [惯用法] i | 1 j; /* sets bit j */ 例如如果j的值为3则1 j是0x0008。 位的清除。要清除i的第4位可以使用第4位为0、其他位为1的掩码 i 0x00ff; /* i is now 0000000011111111 */
i ~0x0010; /* i is now 0000000011101111 */ 按照类似的思路我们可以很容易地编写语句来清除一个特定的位这个位的位置存储在一个变量中 [惯用法] i ~(1 j); /* clears bit j */ 位的测试。下面的if语句测试i的第4位是否被设置 if (i 0x0010) ... /* tests bit 4 */ 如果要测试第j位是否被设置可以使用下面的语句 [惯用法] if (i 1 j)... /* tests bit j */ 为了使针对位的操作更容易经常会给位命名。例如如果想要使一个数的第0、1和2位分别对应蓝色BLUE、绿色GREEN和红色RED。首先定义分别代表这三个位的位置的名字 #define BLUE 0
#define GREEN 1
#define RED 2 设置、清除或测试BLUE位可以如下进行
i | BLUE; /* sets BLUE bit */
i ~BLUE; /* clears BLUE bit */
if (i BLUE) ... /* tests BLUE bit */ 同时设置、清除或测试几个位也一样简单
i | BLUE | GREEN; /* sets BLUE and GREEN bits */
i ~(BLUE | GREEN); /* clears BLUE and GREEN bits */
if (i (BLUE | GREEN)) ... /* tests BLUE and GREEN bits */ 其中if语句测试BLUE位或GREEN位是否被设置了。 20.1.4 用位运算符访问位域 处理一组连续的位位域比处理单个位要复杂一点。下面是2种最常见的位域操作的例子。 修改位域。修改位域需要使用按位与用来清除位域接着使用按位或用来将新的位存入位域。下面的语句显示了如何将二进制值101存入变量i的第4~6位 i i ~0x0070 | 0x0050; /* stores 101 in bits 4-6 */运算符清除了i的第4位至第6位接着运算符|设置了第6位和第4位。注意使用i | 0x0050并不总是可行这只会设置第6位和第4位但不会改变第5位。为了使上面的例子更通用我们假设变量j包含了需要存储到i的第4~6位的值。需要在执行按位或操作之前将j移至相应的位置 i (i ~0x0070) | (j 4); /* stores j in bits 4-6 */运算符|的优先级比运算符和的优先级低因此可以去掉圆括号 i i ~0x0070 | j 4; 获取位域。当位域处在数的右端最低有效位时获得它的值非常方便。例如下面的语句获取了变量i的第0~2位 j i 0x0007; /* retrieves bits 0-2 */ 如果位域不在i的右端那首先需要将位域移位至右端再使用运算符提取位域。例如要获取i的第4~6位可以使用下面的语句 j (i 4) 0x0007; /* retrieves bits 4-6 */ 20.1.5 程序——XOR加密 对数据加密的一种最简单的方法就是将每一个字符与一个密钥进行异或XOR运算。假设密钥是一个字符。如果将它与字符z异或会得到字符\假定使用ASCII字符集。具体计算如下 00100110 的ASCII码
XOR 01111010 z的ASCII码01011100 \的ASCII码要将消息解密只需采用相同的算法。换言之只需将加密后的消息再次加密即可得到原始的消息。例如如果将字符与\字符异或就可以得到原来的字符 z 00100110 的ASCII码
XOR 01011100 \的ASCII码01111010 z的ASCII码下面的程序xor.c通过将每个字符与字符进行异或来加密消息。原始消息可以由用户输入或者使用输入重定向22.1节从文件读入。加密后的消息可以在屏幕上显示也可以通过输出重定向22.1节存入文件中。例如假设文件msg包含下面的内容
Trust not him with your secrets, who, when left
alone in your room, turns over your papers. --Johann Kaspar Lavater (1741-1801)为了对文件msg加密并将加密后的消息存储在文件newmsg中需要使用下面的命令
xor msg newmsg # windows系统使用cmd不要用powershell文件newmsg将包含下面的内容
rTSUR HIR NOK QORN _IST UCETCRU, QNI, QNCH JCR
GJIHC OH _IST TIIK, RSTHU IPCT _IST VGVCTU. --lINGHH mGUVGT jGPGRCT (1741-1801) 要恢复原始消息需要使用命令
xor newmsg # windows系统使用cmd不要用powershell将原始消息显示在屏幕上。
正如在例子中看到的程序不会改变某些字符包括数字。将这些字符与异或会产生不可见的控制字符这在一些操作系统中会引发问题。在第22章中我们会看到在读和写包含控制字符的文件时如何避免问题的发生。而这里为了安全我们将使用isprint函数23.5节来确保原始字符和新字符加密后的字符都是可打印字符即不是控制字符。如果不满足条件就让程序写原始字符而不用新字符。下面是完成的程序这个程序相当短小
/*
xor.c
-- Performs XOR encryption
*/
#include ctype.h
#include stdio.h
#define KEY
int main(void)
{ int orig_char, new_char; while ((orig_char getchar()) ! EOF) { new_char orig_char ^ KEY; if (isprint(orig_char) isprint(new_char)) putchar(new_char); else putchar(orig_char); } return 0;
} 20.2 结构中的位域 虽然20.1节的方法可以操作位域但这些方法不易使用而且可能会引起一些混淆。幸运的是C语言提供了另一种选择——声明其成员表示位域的结构。 例如来看看MS-DOS操作系统通常简称为DOS是如何存储文件的创建和最后修改日期的。由于日、月和年都是很小的数将它们按整数存储会很浪费空间。DOS只为日期分配了16位其中5位用于日day4位用于月month7位用于年year。
yearmonthday15-98-54-0
利用位域可以定义相同形式的C结构
struct file_date { unsigned int day: 5; unsigned int month: 4; unsigned int year: 7;
}; 每个成员后面的数指定了它所占用位的长度。由于所有的成员的类型都一样如果需要可以简化声明
struct file_date {unsigned int day: 5, month: 4, year: 7;
}; 位域的类型必须是int、unsigned int或signed int。使用int会引起二义性因为有些编译器将位域的最高位作为符号位另一些编译器则不会。 可移植性技巧 将所有的位域声明为unsigned int或signed int。 从C99开始位域也可以具有类型_Bool以及其他额外的位域类型。可以将位域像结构的其他成员一样使用如下面的例子所示
struct file_date fd;
fd.day 28;
fd.month 12;
fd.year 8; /* represents 1988 */注意year成员是根据其相距1980年根据微软的描述这是DOS出现的时间的时间而存储的。在这些赋值语句之后变量fd的形式如下所示
00010001100111001514131211109876543210
使用位运算符可以达到同样的效果甚至可能使程序更快些。然而使程序更易读通常比节省几微秒更重要。 使用位域有一个限制这个限制对结构的其他成员不适用。因为通常意义上讲位域没有地址所以C语言不允许将运算符用于位域。由于这条规则像scanf这样的函数无法直接向位域中存储数据: scanf(%d, fd.day); /*** WRONG ***/ 当然可以用scanf函数将输入读入到一个普通的变量中然后再赋值给fd.day。 20.2.1 位域是如何存储的 我们来仔细看一下编译器如何处理包含位域成员的结构的声明。C标准在如何存储位域方面给编译器保留了相当的自由度。 编译器处理位域的相关规则与“存储单元”的概念有关。存储单元的大小是由实现定义的通常为8位、16位或32位。当编译器处理结构的声明时会将位域逐个放入存储单元位域之间没有间隙直到剩下的空间不够存放下一个位域。这时一些编译器会跳到下一个存储单元的开始而另一些则会将位域拆开跨存储单元存放。具体哪种情况会发生是由实现定义的。位域存放的顺序从左至右还是从右至左也是由实现定义的。
前面的file_date例子假设存储单元是16位的8位的存储单元也可以编译器只要将month字段拆开跨两个存储单元存放即可。也可以假设位域是从右至左存储的第一个位域会占据低序号的位。 C语言允许省略位域的名字。未命名的位域经常用作字段间的“填充”以保证其他位域存储在适当的位置。考虑与DOS文件关联的时间存储方式如下 struct file_time { unsigned int seconds: 5; unsigned int minutes: 6; unsigned int hours: 5;
}; 你可能会奇怪怎么可能将秒——0~59范围内的数——存储在一个5位的字段中呢实际上DOS将秒数除以2因此seconds成员实际存储的是0~29范围内的数。如果并不关心seconds字段则可以不给它命名
struct file_time { unsigned int : 5; /* not used */ unsigned int minutes: 6; unsigned int hours: 5;
}; 其他的位域仍会正常对齐如同seconds字段存在时一样。
另一个用来控制位域存储的技巧是指定未命名的字段长度为0
struct s { unsigned int a: 4; unsigned int : 0; /* 0-length bit-field */ unsigned int b: 8;
}; 长度为0的位域是给编译器的一个信号告诉编译器将下一个位域在一个存储单元的起始位置对齐。假设存储单元是8位编译器会给成员a分配4位接着跳过余下的4位直到下一个存储单元然后给成员b分配8位。如果存储单元是16位编译器会给成员a分配4位接着跳过12位然后给成员b分配8位。 20.3 其他底层技术 前面几章中讲过的一些C语言特性也经常用于编写底层程序。作为本章的结尾我们来看几个重要的例子定义代表存储单元的类型使用联合来回避通常的类型检查以及将指针作为地址使用。本节中还将介绍18.3节中没有讨论的volatile类型限定符。 20.3.1 定义依赖机器的类型 根据定义char类型占据1字节因此我们有时将字符当作字节并用它们来存储一些并不一定是字符形式的数据。但这样做时最好定义一个BYTE类型 typedef unsigned char BYTE; 对于不同的机器我们还可能需要定义其他类型。x86体系结构大量使用了16位的字因此下面的定义会比较有用
typedef unsigned short WORD;稍后的例子中会用到BYTE和WORD类型。 20.3.2 用联合来提供数据的多个视角 虽然16.4节的例子中已经介绍了有关联合的便捷的使用方式但在C语言中联合经常被用于一个完全不同的目的从两个或更多个角度看待内存块。 这里根据20.2节中描述的file_date结构给出一个简单的例子。由于一个file_date结构正好放入两个字节中可以将任何两个字节的数据当作一个file_date结构。特别是可以将一个unsigned short值当作一个file_date结构假设短整数是16位。下面定义的联合可以使我们方便地将一个短整数与文件日期相互转换
union int_date { unsigned short i; struct file_date fd;
}; 通过这个联合可以以两个字节的形式获取磁盘中文件的日期然后提取出其中的 month、day和year字段的值。反之也可以以file_date结构构造一个日期然后作为两个字节写入磁盘中。 下面的函数举例说明了如何使用int_date联合。当传入unsigned short参数时这个函数将其以文件日期的形式显示出来 void print_date(unsigned short n)
{ union int_date u; u.i n; printf(%d/%d/%d\n, u.fd.month, u.fd.day, u.fd.year 1980);
} 在使用寄存器时这种用联合来提供数据的多个视角的方法会非常有用因为寄存器通常划分为较小的单元。以x86处理器为例它包含16位寄存器——AX、BX、CX和DX。每一个寄存器都可以看作两个8位的寄存器。例如AX可以被划分为AH和AL这两个寄存器。
当针对基于x86的计算机编写底层程序时可能会用到表示寄存器AX、BX、CX和DX中的值的变量。我们需要访问16位寄存器和8位寄存器同时要考虑它们之间的关系改变AX的值会影响AH和AL改变AH或AL也会同时改变AX。为了解决这一问题可以构造两个结构一个包含对应于16位寄存器的成员另一个包含对应于8位寄存器的成员。然后构造一个包含这两个结构的联合
union { struct { WORD ax, bx, cx, dx; } word; struct { BYTE al, ah, bl, bh, cl, ch, dl, dh; } byte;
} regs;word结构的成员会和byte结构的成员相互重叠。例如ax会使用与al和ah同样的内存空间。当然这恰恰就是我们所需要的。下面是一个使用regs联合的例子
regs.byte.ah 0x12;
regs.byte.al 0x34;
printf(AX: %hx\n, regs.word.ax); 对ah和al的改变也会影响ax所以输出是
AX: 1234 注意尽管AL寄存器是AX的“低位”部分AH寄存器则是“高位”部分但在byte结构中al在ah之前。究其原因当数据项多于一个字节时在内存中有两种存储方式“自然”序先存储最左边的字节或者相反的顺序最后存储最左边的字节。 第一种方式叫作大端big-endian第二种方式叫作小端little-endian。C对存储的顺序没有要求因为这取决于程序执行时所使用的CPU。一些CPU使用大端方法另一些使用小端方法。这与byte结构有什么关系呢原来x86处理器假设数据按小端方式存储所以regs.word.ax的第一个字节是低位字节。
通常我们不用担心字节存储的顺序。但是在底层对内存进行操作的程序必须注意字节的存储顺序regs的例子就是如此。处理含有非字符数据的文件时也需要当心字节的存储顺序。 请注意!!用联合来提供数据的多个视角时要特别小心。把原始格式下有效的数据看作其他格式时就不一定有效了因此有可能会引发意想不到的问题。 20.3.3 将指针作为地址使用 在11.1节中我们已经看到指针实际上就是一种内存地址。虽然通常不需要知道其细节但在编写底层程序时这些细节就很重要了。 地址所包含的位数与整数或长整数一致。构造一个指针来表示某个特定的地址是十分方便的只需要将整数强制转换成指针就行。例如下面的例子将地址1000十六进制存入一个指针变量
BYTE *p;
p (BYTE *) 0x1000; /* p contains address 0x1000 */ 程序——查看内存单元 下一个程序允许用户查看计算机内存段这主要得益于C允许把整数用作指针。大多数CPU执行程序时处于“保护模式”这就意味着程序只能访问那些分配给它的内存。这种方式还可以阻止对其他应用程序和操作系统本身所占用内存的访问。因此我们只能看到程序本身分配到的内存如果要对其他内存地址进行访问则将导致程序崩溃。
程序viewmemory.c先显示了该程序主函数的地址和主函数中一个变量的地址。这可以给用户一个线索去了解哪个内存区可以被探测。程序接下来提示用户输入地址以十六进制整数格式和需要查看的字节数然后从该指定地址开始显示指定字节数的内存块内容。
字节按10个一组的方式显示最后一组例外有可能少于10个。每组字节的地址显示在一行的开头后面是该组中的字节按十六进制数形式再后面是该组字节的字符显示以防字节恰好是表示字符的有时候会出现这种情况。只有打印字符使用isprint函数判断才会被显示其他字符显示为点号。
假设int类型的值使用32位存储且地址也是32位。地址按惯例用十六进制显示。
/*
viewmemory.c
--Allows the user to view regions of computer memory
*/
#include ctype.h
#include stdio.h
typedef unsigned char BYTE;
int main(void)
{ unsigned int addr; int i, n; BYTE *ptr; printf(Address of main function: %x\n, (unsigned int) main); printf(Address of addr variable: %x\n, (unsigned int) addr); printf(\nEnter a (hex) address: ); scanf(%x, addr); printf(Enter number of bytes to view: ); scanf(%d, n); printf(\n); printf( Address Bytes Characters\n); printf( ------- ----------------------------- ----------\n); ptr (BYTE *) addr; for (; n 0; n - 10) { printf(%8X , (unsigned int) ptr); for (i 0; i 10 i n; i) printf(%.2X , *(ptr i)); for (; i 10; i) printf( ); printf( ); for (i 0; i 10 i n; i){ BYTE ch *(ptr i); if (!isprint(ch)) ch .; printf(%c, ch); } printf(\n); ptr 10; } return 0;
} 这个程序看起来有些复杂这是因为n的值有可能不是10的整数倍所以最后一组可能不到10字节。有两条for语句由条件i 10 i n控制这个条件让循环执行10次或n次10和n中的较小值。还有一条for语句处理最后一组中缺失的字节为每个缺失的字节显示三个空格。这样跟在最后一组字节后面的字符就可以与前面的各行对齐了。
转换说明符%X在这个程序中与%x是类似的这在7.1节中讨论过。不同的是%X按大写显示十六进制数位A、B、C、D、E和F而%x按小写显示这些字母。 下面是用GCC编译这个程序并在运行Linux的x86系统下测试的结果 Address of main function: 804847c
Address of addr variable: bff41154 Enter a (hex) address: 8048000
Enter number of bytes to view: 40Address Bytes Characters
-------- ----------------------------- ----------8048000 7F 45 4C 46 01 01 01 00 00 00 .ELF...... 804800A 00 00 00 00 00 00 02 00 03 00 .......... 8048014 01 00 00 00 C0 83 04 08 34 00 ........4. 804801E 00 00 C0 0A 00 00 00 00 00 00 .......... 让程序从地址8048000开始显示40个字节这是main函数之前的地址。注意7F字节以及其后所跟的表示字母E、L和F的字节。这4个字节标识了可执行文件存储的格式即ELF。可执行和链接格式Executable and Linking Format, ELF广泛应用于包括Linux在内的UNIX系统。8048000是x86平台下ELF可执行文件的默认装载地址。
再次运行该程序这次显示从addr变量的地址开始的一些字节
Address of main function: 804847c
Address of addr variable: bfec5484 Enter a (hex) address: bfec5484
Enter number of bytes to view: 64Address Bytes Characters
-------- ----------------------------- ----------
BFEC5484 84 54 EC BF B0 54 EC BF F4 6F .T...T...O
BFEC548E 68 00 34 55 EC BF C0 54 EC BF h.4U...T..
BFEC5498 08 55 EC BF E3 3D 57 00 00 00 .U...W...
BFEC54A2 00 00 A0 BC 55 00 08 55 EC BF ....U..U..
BFEC54AC E3 3D 57 00 01 00 00 00 34 55 .W.....4U
BFEC54B6 EC BF 3C 55 EC BF 56 11 55 00 ..U..V.U.
BFEC54C0 F4 6F 68 00 .oh. 存储在这个内存区域的数据都不是字符格式所以有点难以理解。但我们知道一点addr变量占了这个区域的前4个字节。如果对这4个字节进行反转就得到了BFEC5484这就是用户输入的地址。为什么要反转呢这是因为x86处理器按小端方式存储数据如本节前面所述。 20.3.4 volatile类型限定符 在一些计算机中一部分内存空间是“易变”的保存在这种内存空间的值可能会在程序运行期间发生改变即使程序自身并未试图存放新值。例如一些内存空间可能被用于保存直接来自输入设备的数据。 volatile类型限定符使我们可以通知编译器程序中的某些数据是“易变”的。volatile限定符通常用于指向易变内存空间的指针的声明中:
volatile BYTE *p; /* p will point to a volatile byte */ 为了解为什么要使用volatile假设指针p指向的内存空间用于存放用户通过键盘输入的最近一个字符。这个内存空间是易变的用户每输入一个新字符这里的值都会发生改变。我们可能使用下面的循环获取键盘输入的字符并将其存入一个缓冲区数组中
while (缓冲区未满) { 等待输入; buffer[i] *p; if (buffer[i] \n) break;
} 比较好的编译器可能会注意到这个循环既没有改变p也没有改变*p因此编译器可能会对程序进行优化使*p只被取一次:
在寄存器中存储*p;
while (缓冲区未满) { 等待输入; buffer[i] 存储在寄存器中的值; if (buffer[i] \n) break;
} 优化后的程序会不断复制同一个字符来填满缓冲区这并不是我们想要的程序。将p声明为指向易变的数据的指针可以避免这一问题的发生因为volatile限定符会通知编译器*p每一次都必须从内存中重新取值。 20.4 对象的对齐(C1X) 受硬件布线的限制或者为了提高存储器访问效率要求特定类型的对象在存储器里的位置只能开始于某些特定的字节地址(内存地址都是按字节来顺序编排的从第一个字节开始每个字节都有一个地址这些地址都叫字节地址)而这些字节地址都是某个数值N的特定倍数以不超过实际的存储空间为限这称为对齐alignment。更进一步我们称那个对象是对齐于N的。 举一个实际的例子。假设在某台计算机上int类型的对象可以位于0x00000004、0x00000008、0x0000000C等字节地址上都是4的倍数但不能超过物理内存芯片可以提供的实际地址范围long long int类型的对象只能位于0x00000008、0x00000010、0x00000018、0x00000020等字节地址上都是8的倍数但不能超过物理内存芯片可以提供的实际地址范围。
再比如char类型的对象可以位于任何字节地址上如0x00000001、0x00000002、0x00000003等都是1的倍数但不能超过内存的实际地址范围。 注意!!这里没有提到字节0x00000000。在C中这是一个特殊的字节地址任何对象都不能起始于这个位置。 对于完整的对象类型来说“对齐”限制了它在存储器中可以被分配到的地址。实际上这是一个由C实现定义的整数值。
未对齐的存储器访问对不同的计算机来说会有不同的效果。在有些硬件架构上比如Intel x86系列未对齐的访问不会引发实质性的问题也不会影响结果的正确性但会使处理器对存储器的访问变得笨拙在另一些硬件架构上未对齐的访问将导致总线错误。 20.4.1 对齐运算符_Alignof(C1X) 从C11开始可以用运算符_Alignof得到指定类型的对齐值。_Alignof运算符的操作数要求是用括号括起来的类型名 [_Alignof 表达式] _Alignof(类型名)_Alignof运算符的结果类型是size_t。注意这个运算符不能应用于函数类型和不完整的对象类型。如果应用于数组则返回元素类型的对齐需求。下面是一个应用_Alignof运算符的例子
# include stdio.h
void f (void)
{ printf(%zu, %zu, %zu, %zu\n, _Alignof (char), _Alignof (int), _Alignof (int [33]), _Alignof (struct {char c; float f;}) );
}
//z常用于指定整数类型的长度为size_t20.4.2 对齐指定符_Alignas和头stdalign.h(C1X) 从C11开始在变量的声明里新增了对齐指定符。为此还新增了关键字_Alignas。对齐指定符的语法格式为 [对齐指定符] _Alignas(类型名) _Alignas(常量表达式)以上的第一种形式等价于_Alignas (_Alignof (类型名))。对齐指定符只能在声明里使用或者在复合字面量中使用强制被声明的变量按指定的要求对齐。例如
int _Alignas(8) foo;
struct s {int a; int _Alignas (8) bar;}; 以上代码将使int类型的对象foo和结构类型的成员bar按8字节对齐。
C11新增了一个头stdalign.h它很简单只是定义了4个宏。宏alignas被定义为关键字_Alignas宏alignof被定义为关键字_Alignof宏__alignas_is_defined和__alignof_is_defined分别被定义为整型常量1并分别表示alignas和alignof已经定义。 问与答 问1为什么说和|运算符产生的结果有时会跟和||一样但又不总是如此呢 答我们来比较一下i j与i j对|与||是类似的。只要i和j的值是0或1任何组合都可以两个表达式的值就是一样的。然而一旦i和j是其他的值两个表达式的值就不会始终一致。例如如果i的值是1而j的值是2那么i j的值是0i和j之间没有哪一位同为1而i j的值是1。如果i的值是3而j的值是2那么i j的值是2而i j的值是1。另一个区别是副作用。计算i j始终会使j自增而计算i j有时会使j自增。 问2谁还会在意DOS存储文件日期的方式呢DOS不是已经被淘汰了吗 答大部分情况下是这样的。但是目前仍然有大量的文件是多年前创建的其日期是按DOS格式存储的。不管怎样DOS文件日期是一个很好的示例它可以告诉我们如何使用位域。 问3“大端”和“小端”这两个术语是从哪里来的 答在Jonathan Swift的小说《格列佛游记》中两个虚拟的小人国 Lilliput和Blefuscu为煮熟的鸡蛋应该从大的一端敲开还是从小的一端敲开而争执不休。选择当然是任意的就像数据项中字节的顺序一样。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much!