《c语言深度剖析》笔记2

1.

y = x/*p,这是表示x 除以p 指向的内存里的值,把结果赋值为y?我们可以在编译器
上测试一下,编译器提示出错。
实际上,编译器把/*当作是一段注释的开始,把/*后面的内容都当作注释内容,直到出
现*/为止。这个表达式其实只是表示把x 的值赋给y,/*后面的内容都当作注释。但是,由
于没有找到*/,所以提示出错。
我们可以把上面的表达式修改一下:
y = x/ *p
或者
y = x/(*p)
这样的话,表达式的意思就是x 除以p 指向的内存里的值,把结果赋值为y 了。
也就是说只要斜杠(/)和星号(*)之间没有空格,都会被当作注释的开始。这一点一
定要注意。

出色注释的基本要求

【规则2-1】注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
【规则2-2】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。
不再有用的注释要及时删除。
【规则2-3】注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太
多了会让人眼花缭乱。
【规则2-4】一目了然的语句不加注释。
例如:i++; /* i 加1 */
多余的注释
【规则2-5】对于全局数据(全局变量、常量定义等)必须要加注释。
【规则2-6】注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写。
因为不一定所有的编译器都能显示中文,别人打开你的代码,你的注释也许是一团乱
码。还有,你的代码不一定是懂中文的人阅读。
【规则2-7】注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但
不可放在下方。同一结构中不同域的注释要对齐。
【规则2-8】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于
阅读。
【规则2-9】注释的缩进要与代码的缩进一致。
【规则2-10】注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”。
说明怎么做的注释一般停留在编程语言的层次,而不是为了说明问题。尽力阐述“怎么做”
的注释一般没有告诉我们操作的意图,而指明“怎么做”的注释通常是冗余的。
【规则2-11】数值的单位一定要注释。
注释应该说明某数值的单位到底是什么意思。比如:关于长度的必须说明单位是毫米,
米,还是千米等;关于时间的必须说明单位是时,分,秒,还是毫秒等。
【规则2-12】对变量的范围给出注释。
【规则2-13】对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚
编号)。
【规则2-13】对于函数的入口出口数据给出注释。
关于函数的注释在函数那章有更详细的讨论。



2.

我们知道双引号引起来的都是字符串常量,单引号引起来的都是字符常量但初学者
还是容易弄错这两点。比如:‘a’和“a”完全不一样,在内存里前者占1 个byte,后者占2
个byte。""内部自己添加了一个'\0',所以长度加1 
关于字符串常量在指针与数组那章将有更多的讨论。
这两个列子还好理解,再看看这三个:
1,‘1‘,“1”。
第一个是整形常数,32 位系统下占4 个byte;
第二个是字符常量,占1 个byte;
第三个是字符串常量,占2 个byte。
三者表示的意义完全不一样,所占的内存大小也不一样,初学者往往弄错。
字符在内存里是以ASCAII 码存储的,所以字符常量可以与整形常量或变量进行运算。
如:'A' + 1



3.

C 语言中位运算包括下面几种:注意要与逻辑运算符区别开!!
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移

前4 种操作很简单,一般不会出错。但要注意按位运算符|和&与逻辑运算符||和&&完全
是两码事,别混淆了。


左移和右移
下面讨论一下左移和右移:
左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干
位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。
右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若
干位,“>>”右边的数指定移动的位数。但注意:对于有符号数,在右移时,符号位将随同
移动。当为正数时, 最高位补0;而为负数时,符号位为1,最高位是补0 或是补1 取决
于编译系统的规定。Turbo C 和很多系统规定为补1。


0x01<<2+3 的值为多少?
再看看下面的例子:
0x01<<2+3;
结果为7 吗?测试一下。结果为32?别惊讶,32 才是正确答案。因为“+”号的优先
级比移位运算符的优先级高
(关于运算符的优先级,我并不想在这里做过多的讨论,你几
乎可以在任何一本C 语言书上找到)。好,在32 位系统下,再把这个例子改写一下:
0x01<<2+30;或0x01<<2-3;
这样行吗?不行。一个整型数长度为32 位,左移32 位发生了什么事情?溢出!左移-1
位呢?反过来移?所以,左移和右移的位数是有讲究的。左移和右移的位数不能大于数据
的长度,不能小于0。






4.

++、--操作符
这绝对是一对让人头疼的兄弟。先来点简单的:
int i = 3;
(++i)+(++i)+(++i);
表达式的值为多少?15 吗?16 吗?18 吗?其实对于这种情况,C语言标准并没有作出
规定。有点编译器计算出来为18,因为i 经过3 次自加后变为6,然后3 个6 相加得18;
而有的编译器计算出来为16(比如Visual C++6.0),先计算前两个i 的和,这时候i 自加两
次,2 个i 的和为10,然后再加上第三次自加的i 得16。其实这些没有必要辩论,用到哪个
编译器写句代码测试就行了。但不会计算出15 的结果来的。
++、--作为前缀,我们知道是先自加或自减,然后再做别的运算;但是作为后缀时,到
底什么时候自加、自减?这是很多初学者迷糊的地方。假设i=0,看例子:
A),j =(i++,i++,i++);
B),for(i=0;i<10;i++)
{
//code
}
C),k = (i++)+ (i++)+ (i++);
你可以试着计算他们的结果。
A) 例子为逗号表达式,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加。
关于逗号表达式与“++”或“--”的连用,还有一个比较好的例子:
int x;
int i = 3;
x = (++i, i++, i+10);
问x 的值为多少?i 的值为多少?
按照上面的讲解,可以很清楚的知道,逗号表达式中,i 在遇到每个逗号后,认为本计算
单位已经结束,i 这时候自加。所以,本例子计算完后,i的值为5,x的值为15。
B) 例子i 与10 进行比较之后,认为本计算单位已经结束,i 这时候自加。
C) 例子i 遇到分号才认为本计算单位已经结束,i 这时候自加。
也就是说后缀运算是在本计算单位计算结束之后再自加或自减。C 语言里的计算单位大体分
为以上3 类。
留一个问题:
for(i=0,printf(“First=%d”,i);
i<10,printf(“Second=%d”,i);
i++,printf(“Third=%d”,i))
{
printf(“Fourth=%d”,i);
}
打印出什么结果?


上面的例子很简单,那我们把括号去掉看看:
int i = 3;
++i+++i+++i;
天啦!这到底是什么东西?好,我们先看看这个:a+++b 和下面哪个表达式想当:
A),a++ +b;
B),a+ ++b;

贪心法
C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程
序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,
那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组
成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串
已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法”
。需要注意到是,除
了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等)。比如:==是
单个符号,而= =是两个等号。
按照这个规则可能很轻松的判断a+++b 表达式与a++ +b 一致。那++i+++i+++i;会被解
析成什么样子呢?希望读者好好研究研究。另外还可以考虑一下这个表达式的意思:
a+++++b;


5.

2/(-2)的值是多少?
除法运算在小学就掌握了的,这里还要讨论什么呢?别急,先计算下面这个例子:
2/(-2)的值为多少?2%(-2)的值呢?
如果与你想象的结果不一致,不要惊讶。我们先看看下面这些规则:
假定我们让a 除以b,商为q,余数为r:
q = a/b;
r = a%b;
这里不妨先假定b 大于0。
我们希望a、b、q、r 之间维持什么样的关系呢?
1,最重要的一点,我们希望q*b + r == a,因为这是定义余数的关系。
2,如果我们改变a 的正负号,我们希望q 的符号也随之改变,但q 的绝对值不会变。
3,当b>0 时,我们希望保证r>=0 且r<b。
这三条性质是我们认为整数除法和余数操作所应该具备的。但是,很不幸,它们不可
能同时成立。
先考虑一个简单的例子:3/2,商为1,余数也为1。此时,第一条性质得到了满足。
好,把例子稍微改写一下:(-3)/2 的值应该是多少呢?如果要满足第二条性质,答案应
该是-1。但是,如果是这样,余数就必定是-1,这样第三条性质就无法满足了。如果我们首
先满足第三条性质,即余数是1,这种情况下根据第一条性质,商应该为-2,那么第二条性
质又无法满足了。
上面的矛盾似乎无法解决。因此,C 语言或者其他语言在实现整数除法截断运算时,必
须放弃上述三条性质中的至少一条。大多数编程语言选择了放弃第三条,而改为要求余数与
被除数的正负号相同,商按实际情况来。这样性质1 和性质2 就可以得到满足。大多数C 语言编译器也都是
如此。

但是,C 语言的定义只保证了性质1,以及当a>=0 且b>0 时,保证|r|<|b|以及r>=0。后
面部分的保证与性质2 或性质3 比较起来,限制性要弱得多。
通过上面的解释,你是否能准确算出2/(-2)和2%(-2)的值呢?



7.



8.

第一章我们详细讨论了const 这个关键字,我们知道const 修饰的数据是有类型的,而
define 宏定义的数据没有类型。为了安全,我建议你以后在定义一些宏常数的时候用const
代替,编译器会给const 修饰的只读变量做类型校验,减少错误的可能。但一定要注意const
修饰的不是常量而是readonly 的变量,const 修饰的只读变量不能用来作为定义数组的维数,
也不能放在case 关键字后面。


除了定义宏常数之外,经常还用来定义字符串,尤其是路径:
#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3

但是请注意:有的系统里规定路径的要用双反斜杠“\\”,比如:
#define ENG_PATH_4 E:\\English\\listen_to_this\\listen_to_this_3

用define 宏定义注释符号?
上面对define 的使用都很简单,再看看下面的例子:
#define BSC //
#define BMC /*
#define EMC */
D),BSC my single-line comment
E),BMC my multi-line comment EMC
D)和E)都错误,为什么呢?因为注释先于预处理指令被处理,当这两行被展开成//…或
/*…*/时,注释已处理完毕,此时再出现//…或/*…*/自然错误.因此,试图用宏开始或结束一段
注释是不行的。


9.

条件编译
条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目
标代码文件。这对于程序的移植和调试是很有用的。条件编译有三种形式,下面分别介绍:
第一种形式:
#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是,如果标识符已被#define 命令定义过则对程序段1 进行编译;否则对程序段2
进行编译。如果没有程序段2(它为空),本格式中的#else 可以没有,即可以写为:
#ifdef 标识符
程序段
#endif
第二种形式:
#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define 命令定
义过则对程序段1 进行编译,否则对程序段2 进行编译。这与第一种形式的功能正相反。
第三种形式:
#if 常量表达式
程序段1
#else
程序段2
#endif
它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2 进行
编译。因此可以使程序在不同条件下,完成不同的功能。
至于#elif 命令意义与else if 相同,它形成一个if else-if 阶梯状语句,可进行多种编译选择。


#error 预处理

#error 预处理指令的作用是,编译程序时,只要遇到#error 就会生成一个编译错误提
示消息,并停止编译。其语法格式为:
#error error-message
注意,宏串error-message 不用双引号包围。遇到#error 指令时,错误信息被显示,可能同时
还显示编译程序作者预先定义的其他内容。关于系统所支持的error-message 信息,请查找
相关资料,这里不浪费篇幅来做讨论


10.

这里重点讨论内存对齐的问题和#pragma pack()的使用方法。
什么是内存对齐?
先看下面的结构:
struct TestStruct1
{
char c1;
short s;
char c2;
int i;
};
假设这个结构的成员在内存中是紧凑排列的,假设c1 的地址是0,那么s 的地址就应该
是1,c2 的地址就是3,i 的地址就是4。也就是c1 地址为00000000, s 地址为00000001, c2
地址为00000003, i 地址为00000004。
可是,我们在Visual C++6.0 中写一个简单的程序:
struct TestStruct1 a;
printf("c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
运行,输出:
c1 00000000, s 00000002, c2 00000004, i 00000008。

为什么会这样?这就是内存对齐而导致的问题。

字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自
然边界分别是偶数地址,可以被4 整除的地址,和可以被8 整除的地址。
)无论如何,为了
提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为
了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访
问。
一个字或双字操作数跨越了4 字节边界,或者一个四字操作数跨越了8 字节边界,被
认为是未对齐的,从而需要两次总线周期来访问内存。
一个字起始地址是奇数但却没有跨
越字边界被认为是对齐的,能够在一个总线周期中被访问。某些操作双四字的指令需要内
存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常。
双四字的自然边界是能够被16 整除的地址。其他的操作双四字的指令允许未对齐的访问
(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
缺省情况下,编译器默认将结构、栈中的成员数据进行内存对齐。因此,上面的程序输
出就变成了:c1 00000000, s 00000002, c2 00000004, i 00000008。编译器将未对齐的成员向后
移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲
一点空间(成员之间有部分内存空闲),但提高了性能。也正是这个原因,我们不可以断言
sizeof(TestStruct1)的结果为8。在这个例子中,sizeof(TestStruct1)的结果为12。

那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。
比如我们可以将上面的结构改成:
struct TestStruct2
{
char c1;
char c2;
short s;
int i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例
子中,sizeof(TestStruct2)的值为8。这个技巧有一个重要的作用,尤其是这个结构作为API
的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,
从而造成这个结构在你的发行的DLL 中使用某种对齐方式,而在第三方开发者哪里却使用
另外一种对齐方式。这将会导致重大问题。
比如,TestStruct1 结构,我们的DLL 使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(TestStruct1)的值为12。
而第三方将对齐选项关闭,导致
c1 00000000, s 00000001, c2 00000003, 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)
struct TestStruct4
{
char a;
long b;
};
struct TestStruct5
{
char c;
TestStruct4 d;
long long e;
};
#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 char Array3[3];Array3 这种类型的对齐方式还是按1
个字节对齐,而不是按它的长度。
但是不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。
另外,注意别的#pragma pack 的其他用法:
#pragma pack(push) //保存当前对其方式到packing stack
#pragma pack(push,n) 等效于
#pragma pack(push)
#pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按n 字节对齐
#pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐方


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值