第一章:《C穿越时空的迷雾》
B 优先级从高到低依次是:
B1 声明中被括号括起来的那部分
B2 后缀操作符:
括号()表示这是一个函数,而方括号[]表示这是一个数组。
B3 前缀操作符:星号*表示 “指向……的指针”
C 如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等),那么它作用于子类型说明符,在其他情况下,const和(或)volatile关键字关键字作用于它左边紧邻的指针星号。
例如:char * const *(*next)();这个声明表示“next十一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char的常量指针”。
1、C语言的基本数据类型直接与底层硬件相对应,C语言中不允许嵌套函数。
2、register关键字:当某个变量需要经常被使用时,可以给变量加上register关键字把他们存放到寄存器中。
3、C语言中,绝大多数库函数或辅助程序都需要显示调用。例如,程序员必须管理动态内存的使用,创建各种大小的数组,测试数组边界,并自己进行范围检测。
4、宏定义只应该适量使用,因为宏定义只是单纯的字符替换,不提供类型安全检查。在宏的扩展中,空格会对扩展的结果造成很大的影响。
试比较: #define a(y) a_expanded(y)
#define a (y) a_expanded (y)
5、要使两个指针赋值合法,必须要两个操作数都是指向有限定符或无限定符的相容类型的指针,并且左边指针所指向的类型必须具有右边指针指向类型的全部限定符。
6、const不能把一个变量变成常量,它只能表示一个符号是只读的,const最常用来限定函数的参数。
注意: const int *p / int const *p 这两个都代表指针指向的内容是只读的
int* const p 这个代表指针本身是只读的
const int * const p 这个代表指针本身只读并且其指向的内容也是只读的
7、尽量不要在代码中使用无符号类型,尽量使用像int那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如-1被翻译为非常大的正数)。只有在使用位段和二进制掩码时,才可以用无符号数,即使这样,也应该在表达式中使用强制类型转换来明确统一操作数是否有符号,避免由编译器来选择结果类型。
第二章:《这不是bug,而是语言特性》
1、注意一下在给字符串动态分配内存空间时要使用malloc(strlen(str)+1),字符串是以'\0'结尾的,所以要多加个1。
2、switch...case的条件中只能是整型、字符型、布尔型和枚举型,此外还要注意一下在每个case执行完后用break退出,否则会顺序执行以下的语句。
3、
相邻的字符串常量将被自动合并成一个字符串,换行无须’/‘符号。
4、缺省情况下,函数的名字是全局可见的,因为其缺省了extern关键字,如想要某个函数只对该文件可见,可以在函数前加上static关键字。
5、在表达式中如果有布尔操作、算术运算、位操作等混合计算,应该在适当的地方加上括号使之清楚明了。牢记以下运算符优先级表:
优先级
|
运算符
|
名称或含义
|
使用形式
|
结合方向
|
说明
|
1
|
[]
|
数组下标
|
数组名[常量表达式]
|
左到右
| |
()
|
圆括号
|
(表达式)/函数名(形参表)
| |||
.
|
成员选择(对象)
|
对象.成员名
| |||
->
|
成员选择(指针)
|
对象指针->成员名
| |||
2
|
-
|
负号运算符
|
-表达式
|
右到左
|
单目运算符
|
(类型)
|
强制类型转换
|
(数据类型)表达式
| |||
++
|
自增运算符
|
++变量名/变量名++
|
单目运算符
| ||
--
|
自减运算符
|
--变量名/变量名--
|
单目运算符
| ||
*
|
取值运算符
|
*指针变量
|
单目运算符
| ||
&
|
取地址运算符
|
&变量名
|
单目运算符
| ||
!
|
逻辑非运算符
|
!表达式
|
单目运算符
| ||
~
|
按位取反运算符
|
~表达式
|
单目运算符
| ||
sizeof
|
长度运算符
|
sizeof(表达式)
| |||
3
|
/
|
除
|
表达式/表达式
|
左到右
|
双目运算符
|
*
|
乘
|
表达式*表达式
|
双目运算符
| ||
%
|
余数(取模)
|
整型表达式/整型表达式
|
双目运算符
| ||
4
|
+
|
加
|
表达式+表达式
|
左到右
|
双目运算符
|
-
|
减
|
表达式-表达式
|
双目运算符
| ||
5
|
<<
|
左移
|
变量<<表达式
|
左到右
|
双目运算符
|
>>
|
右移
|
变量>>表达式
|
双目运算符
| ||
6
|
>
|
大于
|
表达式>表达式
|
左到右
|
双目运算符
|
>=
|
大于等于
|
表达式>=表达式
|
双目运算符
| ||
<
|
小于
|
表达式<表达式
|
双目运算符
| ||
<=
|
小于等于
|
表达式<=表达式
|
双目运算符
| ||
7
|
==
|
等于
|
表达式==表达式
|
左到右
|
双目运算符
|
!=
|
不等于
|
表达式!= 表达式
|
双目运算符
| ||
8
|
&
|
按位与
|
表达式&表达式
|
左到右
|
双目运算符
|
9
|
^
|
按位异或
|
表达式^表达式
|
左到右
|
双目运算符
|
10
|
|
|
按位或
|
表达式|表达式
|
左到右
|
双目运算符
|
11
|
&&
|
逻辑与
|
表达式&&表达式
|
左到右
|
双目运算符
|
12
|
||
|
逻辑或
|
表达式||表达式
|
左到右
|
双目运算符
|
13
|
?:
|
条件运算符
|
表达式1? 表达式2: 表达式3
|
右到左
|
三目运算符
|
14
|
=
|
赋值运算符
|
变量=表达式
|
右到左
| |
/=
|
除后赋值
|
变量/=表达式
| |||
*=
|
乘后赋值
|
变量*=表达式
| |||
%=
|
取模后赋值
|
变量%=表达式
| |||
+=
|
加后赋值
|
变量+=表达式
| |||
-=
|
减后赋值
|
变量-=表达式
| |||
<<=
|
左移后赋值
|
变量<<=表达式
| |||
>>=
|
右移后赋值
|
变量>>=表达式
| |||
&=
|
按位与后赋值
|
变量&=表达式
| |||
^=
|
按位异或后赋值
|
变量^=表达式
| |||
|=
|
按位或后赋值
|
变量|=表达式
| |||
15
|
,
|
逗号运算符
|
表达式,表达式,…
|
左到右
|
从左向右顺序运算
|
【
运算符优先级表】
6、强烈建议用fgets完全取代gets的功能。
第三章:《分析C语言的声明》
1、理解C语言声明的优先级规则
A 声明从它的名字开始读取,然后按照优先级顺序依次读取;B 优先级从高到低依次是:
B1 声明中被括号括起来的那部分
B2 后缀操作符:
括号()表示这是一个函数,而方括号[]表示这是一个数组。
B3 前缀操作符:星号*表示 “指向……的指针”
C 如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等),那么它作用于子类型说明符,在其他情况下,const和(或)volatile关键字关键字作用于它左边紧邻的指针星号。
例如:char * const *(*next)();这个声明表示“next十一个指针,它指向一个函数,该函数返回另一个指针,该指针指向一个类型为char的常量指针”。
2、struct和union的区别:struct中每个成员是依次存储的,整个struct占用多少内存空间取决于每个成员的大小。而在union中,所有的成员都从偏移地址零开始存储,整个union占用多少内存空间取决于union中占用内存最大的那个成员。所以union通常用来节省内存空间。
3、枚举的作用:枚举是将一串名字和一串整型值联系到一起,其作用类似于#define,但是增强了代码的可读性。如:enum sizes{small = 7, medium, large = 10, humungous};
缺省情况下,整型值从零开始,但如果像上面small被赋予了某个值,那么紧接其后的那个medium就比所赋的值
大1。#define在编译时被丢弃,而枚举名字可以在调试器中可见。
4、typedef为一种类型引用新的名字,而不是为变量分配空间。
5、
typedef 与#define之间存在一个关键性的区别。正确思考这个问题的方法就是把typedef看成是一种彻底的“封装”类型——在声明它之后不能再往里面增加别的东西。首先,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名却不能这样做。其次,在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。
第四章:《令人震惊的事实:数组和指针并不相同》
1、声明和定义的区别,声明相当于普通的声明:它所说明的并非自身,而是描述其他地方的创建的对象。定义相当于特殊的声明:它为对象分配内存。
2、extern char a[]与extern char a[100]等价,这俩个声明都提示a是个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共有多长,因为它只产生偏离起始地址的偏移地址。相反,如果声明extern char *p;它将告诉编译器p是一个指针,它指向的对象是一个字符。
2、extern char a[]与extern char a[100]等价,这俩个声明都提示a是个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共有多长,因为它只产生偏离起始地址的偏移地址。相反,如果声明extern char *p;它将告诉编译器p是一个指针,它指向的对象是一个字符。
3、数组和指针的区别:
指针 | 数组 |
保存数据的地址 | 保存数据 |
间接访问数据,首先取得指针的内容,把它作为地址,然后 从这个地址提取数据 | 直接访问数据 |
用于动态数据结构 | 用于存储固定数目且数据类型 相同的元素 |
相关操作malloc(),free() | 隐式分配和删除 |
通常指向匿名数据 | 自身即为数据名 |
4、定义指针时不为指针所指向的对象分配空间,只为指针本身分配空间,除非在定义的同时赋给指针一个字符串常量进行初始化,初始化指针所创建的字符串常量是只读的
(char *p = "breakfast")
,但是由字符串常量初始化的数组却是可以修改的。
(char p[] = "breakfast")。
第五章:《对链接的思考》
1、如果函数库的一份拷贝是可执行文件的物理组成部分,就称之为静态链接;如果可执行文件只包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,称之为动态链接。
2、动态链接库提高性能的方式:
①相比于静态链接,动态链接可执行文件体积更小,节省磁盘空间和虚拟内存,函数库在被需要的时候才映射到进程中。
②所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。这就提供了更好的I/O和交换空间利用率,节省了物理内存。
3、收集模块准备执行的三个阶段的规范名称是链接-编辑(link-editing),载入(loading)和运行时链接(runtime linking)。动态链接的模块被链接编辑后载入,并在运行时进行链接以便运行。程序执行时,在main()函数被调用前,运行时载入器把共享的数据对象载入到进程的地址空间。外部函数被真正调用之前,运行时载入器并不解析它们。所以即使链接了函数库,如果没有实际调用,也不会带来额外开销。即使是在静态链接中,整个libc.a文件也没有全部装入到可执行文件中,所装入的只是所需要的函数。
第六章:《运动的诗章:运行时数据结构》
1、BSS段只保存没有值的变量,所以事实上它并不需要保存这些变量的映像。运行时所需要的BBS段的大小记录在目标文件中,但BBS段(不像其他段)并不占据目标文件的任何空间。
2、文本段包含程序的指令。链接器把指令直接从文件拷贝到内存中,以后便再也不用管它。
3、数据段包含经过初始化的全局和静态变量以及它们的值。
4、堆栈段(stack segment)用于保存局部变量,临时数据,传送到函数的参数等。堆(heap)空间,用于动态分配的内存。只要调用malloc()函数,就可以根据需要在堆上分配内存。
5、堆栈段包含一种单一的数据结构--堆栈。堆栈是一块动态内存区域,后进先出。编译器设计者采用了一种稍微灵活一些的方法,我们从顶部增加或拿掉盘子,但我们也可以修改位于堆栈中部的盘子的值。函数可以通过参数或全局指针访问它所调用的函数的局部变量。运行时系统维护一个指针(常位于寄存器中),通常称为sp。用于提示堆栈当前的顶部位置。
6、堆栈段的三个主要用途。第一,堆栈为函数内部声明的局部变量提供存储空间,进行函数调用时,堆栈存储与此有关的一些维护信息,这些信息被称为堆栈结构(stack frame)。第二,作为过程活动记录(precedure activation recored),它包括函数调用地址(即当所调用的函数结束后跳回的地方),任何不适合装入寄存器的参数以及一些寄存器值的保存。第三,堆栈也可以被用作暂时存储区,alloca()函数分配的内存就是位于堆栈中。如果想让内存在函数调用结束之后仍然有效,就不要使用alloca()来分配(它将被下一个函数调用所覆盖)。
7、可以用setjmp(jmp_buf j)和longjmp(jmp_buf j, int i)来进行错误恢复,setjmp必须首先被调用来记录现在的位置,然后调用longjmp跳回到曾经的地方。
8、堆栈段大小的限制:在UNIX中,当进程需要更多空间时,堆栈会自动生长。程序员可以想象堆栈是无限大的。在UNIX的实现中一般使用某种形式的虚拟内存。当试图访问当前系统分配给堆栈的空间之外时,它将产生一个硬件中断,称为页错误(page fault)。在MS-DOS中,在建立可执行文件时,堆栈的大小必须同时确定,而且不能进行运行时增长。