太子爷小说网 > 社科电子书 > [免费下载 c语言深度解剖[1] >

第14节

[免费下载 c语言深度解剖[1]-第14节

小说: [免费下载 c语言深度解剖[1] 字数: 每页4000字

按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!



从而造成这个结构在你的发行的 
DLL中使用某种对齐方式,而在第三方开发者哪里却使用
另外一种对齐方式。这将会导致重大问题。

比如,TestStruct1结构,我们的 
DLL使用默认对齐选项,对齐为 
c100000000;s00000002;c200000004;i 
00000008,同时 
sizeof(TestStruct1)的值为 
12。
而第三方将对齐选项关闭,导致 
c100000000;s00000001;c200000003;i 
00000004,同时 
sizeof(TestStruct1)的值为 
8。

除此之外我们还可以利用#pragma 
pack()来改变编译器的默认对齐方式(当然一般编译器


也提供了一些改变对齐方式的选项,这里不讨论)。

使用指令#pragma 
pack 
(n),编译器将按照 
n个字节对齐。

使用指令#pragma 
pack 
(),编译器将取消自定义字节对齐方式。

在#pragma 
pack 
(n)和#pragma 
pack 
()之间的代码按 
n个字节对齐。

但是,成员对齐有一个重要的条件 
;即每个成员按自己的方式对齐。也就是说虽然指定了
按 
n字节对齐;但并不是所有的成员都是以 
n字节对齐。其对齐的规则是 
;每个成员按其类型
的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是 
n字节)中较小的一个对齐,即: 
min( 
n;sizeof( 
item 
))。并且结构的长度必须为所用过的所有对齐参数的整数倍;不够就补空
字节。看如下例子: 


#pragma 
pack(8) 


structTestStruct4 


{ 
char 
a; 
long 
b; 


}; 


structTestStruct5 


{ 
char 
c; 
TestStruct4 
d; 
longlonge; 


}; 


#pragma 
pack()

问题: 


A);sizeof(TestStruct5)=? 


B);TestStruct5的 
c后面空了几个字节接着是 
d? 
TestStruct4中;成员 
a是 
1字节默认按 
1字节对齐;指定对齐参数为 
8;这两个值中取 
1;a
按 
1字节对齐;成员 
b是 
4个字节;默认是按 
4字节对齐;这时就按 
4字节对齐;所以 
sizeof(TestStruct4)应该为 
8; 


TestStruct5中;c和 
TestStruct4中的 
a一样;按 
1字节对齐;而 
d是个结构;它是 
8个字节;它
按什么对齐呢?对于结构来说;它的默认对齐方式就是它的所有成员使用的对齐参数中最大
的一个;TestStruct4的就是 
4。所以;成员 
d就是按 
4字节对齐。成员 
e是 
8个字节;它是默认按 
8
字节对齐;和指定的一样;所以它对到 
8字节的边界上;这时;已经使用了 
12个字节了;所以又添
加了 
4个字节的空;从第 
16个字节开始放置成员 
e。这时;长度为 
24;已经可以被 
8(成员 
e按 
8
字节对齐)整除。这样;一共使用了 
24个字节。内存布局如下(*表示空闲内存,1表示使用内存。

单位为 
1byete): 
a 
b 
TestStruct4的内存布局:1***;1111; 
c 
TestStruct4。a 
TestStruct4。b 
d 
TestStruct5的内存布局: 
1***; 
1***; 
1111; 
****,11111111 



这里有三点很重要:
首先,每个成员分别按自己的方式对齐;并能最小化长度。
其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式;这样在成员是复杂
类型时;可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍;这样在处理数组时可以保
证每一项都边界对齐。

补充一下;对于数组;比如:char 
a'3';它的对齐方式和分别写 
3个 
char是一样的。也就是说
它还是按 
1个字节对齐。如果写: 
typedef 
charArray3'3';Array3这种类型的对齐方式还是按 
1
个字节对齐;而不是按它的长度。

但是不论类型是什么;对齐的边界一定是 
1;2;4;8;16;32;64。。。。中的一个。

另外,注意别的#pragma 
pack的其他用法: 


#pragma 
pack(push) 
//保存当前对其方式到 
packingstack 


#pragma 
pack(push;n)等效于 


#pragma 
pack(push) 


#pragma 
pack(n) 
//n=1;2;4;8;16保存当前对齐方式,设置按 
n字节对齐 


#pragmapack(pop) 
//packingstack出栈,并将对其方式设置为出栈的对齐方

3。7;#运算符 
#也是预处理?是的,你可以这么认为。那怎么用它呢?别急,先看下面例子: 


#defineSQR(x) 
printf(〃Thesquareof 
x 
is%d。n〃;((x)*(x)));

如果这样使用宏: 


SQR(8);

则输出为: 


Thesquareof 
x 
is64。
注意到没有,引号中的字符 
x被当作普通文本来处理,而不是被当作一个可以被替换的语言
符号。

假如你确实希望在字符串中包含宏参数,那我们就可以使用 
“#”,它可以把语言符号转
化为字符串。上面的例子改一改: 


#defineSQR(x) 
printf(〃Thesquareof 
〃#x〃 
is%d。n〃;((x)*(x)));

再使用: 


SQR(8);

则输出的是: 


Thesquareof 
8 
is64。
很简单吧?相信你现在已经明白#号的使用方法了。


3。8,##预算符
和#运算符一样,##运算符可以用于宏函数的替换部分。这个运算符把两个语言符号组


合成单个语言符号。看例子: 


#define 
XNAME(n) 
x##n

如果这样使用宏: 


XNAME(8)

则会被展开成这样: 


x8

看明白了没?##就是个粘合剂,将前后两部分粘合起来。


第四章指针和数组

几乎每次讲课讲到指针和数组时,我总会反复不停的问学生:到底什么是指针?什么
是数组?他们之间到底是什么样的关系。从几乎没人能回答明白到几乎都能回答明白,需
要经历一段“惨绝人寰”的痛。指针是 
C/C++的精华,如果未能很好地掌握指针,那 
C/C++
也基本等于没学。可惜,对于刚毕业的计算机系的学生,几乎没有人真正完全掌握了指针
和数组、以及内存管理,甚至有的学生告诉我说:他们老师认为指针与数组太难,工作又
少用,所以没有讲解。对于这样的学校与老师,我是彻底的无语。我没有资格去谴责或是
鄙视谁,只是窃以为,这个老师肯怕自己都未掌握指针。大学里很多老师并未真正写过多
少代码,不掌握指针的老师肯定存在,这样的老师教出来的学生如何能找到工作?而目前
市面上的书对指针和数组的区别也是几乎避而不谈,这就更加加深了学生掌握的难度。我
平时上课总是非常细致而又小心的向学生讲解这些知识,生怕一不小心就讲错或是误导了
学生。还好,至少到目前为止,我教过的学生几乎都能掌握指针和数组及内存管理的要点,
当然要到能运用自如的程度还远远不够,这需要大量的写代码才能达到。另外需要说明的
是,讲课时为了让学生深刻的掌握这些知识,我举了很多各式各样的例子来帮助学生理解。
所以,我也希望读者朋友能好好体味这些例子。

三个问题: 


A),什么是指针? 
B),什么是数组? 
C),数组和指针之间有什么样的关系?
4。1,指针
4。1。1,指针的内存布局
先看下面的例子: 


int*p;

大家都知道这里定义了一个指针 
p。但是 
p到底是什么东西呢?还记得第一章里说过,
“任何一种数据类型我们都可以把它当一个模子”吗?p,毫无疑问,是某个模子咔出来的。
我们也讨论过,任何模子都必须有其特定的大小,这样才能用来“咔咔咔”。那咔出 
p的这
个模子到底是什么样子呢?它占多大的空间呢?现在用 
sizeof测试一下( 
32位系统):sizeof

(p)的值为 
4。嗯,这说明咔出 
p的这个模子大小为 
4个 
byte。显然,这个模子不是“ 
int”,
虽然它大小也为 
4。既然不是“ 
int”那就一定是“ 
int*”了。好,那现在我们可以这么理解
这个定义:
一个“ 
int*”类型的模子在内存上咔出了 
4个字节的空间,然后把这个 
4个字节大小的


空间命名为 
p,同时限定这 
4个字节的空间里面只能存储某个内存地址,即使你存入别的任
何数据,都将被当作地址处理,而且这个内存地址开始的连续 
4个字节上只能存储某个 
int
类型的数据。

这是一段咬文嚼字的说明,我们还是用图来解析一下: 


4bytepp4p0x0000FF00100x0000FF00int0x0000FF00p4bytep
如上图所示,我们把 
p称为指针变量 
;p里存储的内存地址处的内存称为 
p所指向的内存。
指针变量 
p里存储的任何数据都将被当作地址来处理。

我们可以简单的这么理解:一个基本的数据类型(包括结构体等自定义类型)加上 
“*”
号就构成了一个指针类型的模子。这个模子的大小是一定的,与“ 
*”号前面的数据类型无
关。“*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在 
32位
系统下,不管什么样的指针类型,其大小都为 
4byte。可以测试一下 
sizeof(void 
*)。

4。1。2,“*”与防盗门的钥匙
这里这个“ 
*”号怎么理解呢?举个例子:当你回到家门口时,你想进屋第一件事就是
拿出钥匙来开锁。那你想想防盗门的锁芯是不是很像这个 
“*”号?你要进屋必须要用钥匙,
那你去读写一块内存是不是也要一把钥匙呢?这个“ 
*”号就是不是就是我们最好的钥匙?
使用指针的时候,没有它,你是不可能读写某块内存的。

4。1。3,int 
*p 
=NULL和*p 
=NULL有什么区别?
很多初学者都无法分清这两者之间的区别。我们先看下面的代码: 


int*p 
= 
NULL;
这时候我们可以通过编译器查看 
p的值为 
0x00000000。这句代码的意思是:定义一个指针
变量 
p,其指向的内存里面保存的是 
int类型的数据;在定义变量 
p的同时把 
p的值设置为 
0x00000000,而不是把*p的值设置为 
0x00000000。这个过程叫做初始化,是在编译的时候
进行的。


明白了什么是初始化之后,再看下面的代码: 


int*p; 


*p 
= 
NULL;
同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量 
p,其指向
的内存里面保存的是 
int类型的数据;但是这时候变量 
p本身的值是多少不得而知,也就是
说现在变量 
p保存的有可能是一个非法的地址。第二行代码,给 
*p赋值为 
NULL,即给 
p
指向的内存赋值为 
NULL;但是由于 
p指向的内存可能是非法的,所以调试的时候编译器可
能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使 
p指向一块合
法的内存: 


inti 
=10; 


int*p 
= 
&i; 


*p 
= 
NULL;
在编译器上调试一下,我们发现 
p指向的内存由原来的 
10变为 
0了;而 
p本身的值,即内
存地址并没有改变。

经过上面的分析,相信你已经明白它们之间的区别了。不过这里还有一个问题需要注
意,也就是这个 
NULL。初学者往往在这里犯错误。

注意 
NULL就是 
NULL,它被宏定义为 
0: 


#defineNULL0
很多系统下除了有 
NULL外,还有 
NUL(VisualC++6。0上提示说不认识 
NUL)。NUL是 
ASCII
码表的第一个字符,表示的是空字符,其 
ASCII码值为 
0。其值虽然都为 
0,但表示的意思
完全不一样。同样,NULL和 
0表示的意思也完全不一样。一定不要混淆。

另外还有初学者在使用 
NULL的时候误写成 
null或 
Null等。这些都是不正确的,C语
言对大小写十分敏感啊。当然,也确实有系统也定义了 
null,其意思也与 
NULL没有区别,
但是你千万不用使用 
null,这会影响你代码的移植性。

4。1。4,如何将数值存储到指定的内存地址
假设现在需要往内存 
0x12ff7c地址上存入一个整型数 
0x100。我们怎么才能做到呢?我
们知道可以通过一个指针向其指向的内存地址写入数据,那么这里的内存地址 
0x12ff7c其
本质不就是一个指针嘛。所以我们可以用下面的方法: 


int*p 
= 
(int*)0x12ff7c; 


*p 
= 
0x100;

需要注意的是将地址 
0x12ff7c赋值给指针变量 
p的时候必须强制转换。至于这里为什
么选择内存地址 
0x12f

返回目录 上一页 下一页 回到顶部 0 1

你可能喜欢的