1)switch语句,switch语句不会在每个case执行完毕后自动终止(称为fall through),这也是每个case语句最后加上break的原因;break语句跳出的是最近的循环语句或switch语句;
2)相邻字符串自动连接,尤其在字符指针数组的初始化中,少了逗号会使两个字符串拼接在一起;
3)缺省全局范围,当我们自己定义一个与库函数同名的函数时,会取而代之,不管在自己调用还是系统调用时,具体见“三、连接与链接”;
1)C 语言的简洁性 ,C关键字在多种上下文中使用,即符号重载,如static、&、()、void等
2)运算符的优先级,优先级存在的问题:
a. .的优先级大于*,->操作符用于消除这个问题;
b. []的优先级高于*;
c. 函数()高于*;
d. ==和!=高于位操作符和赋值符;
e. 算术运算高于移位运算;
f. 逗号运算符在所有运算符中优先级最低;
3)gets()函数接收任意长的参数会改写函数栈,超过的长度会污染别的栈帧(函数调用时的内存分配过程,以后详细看CSAPP);
1)命令行参数解析,选项开关"-"与文件名中的“-”混淆;
2)空格引发的血案
a. 多了空格,行末最后一个字符“\”后跟上换行符标识下一行为本行的后续,警惕“\”和换行符之间的空格;
b. 少了空格,造成歧义,例如z=y+++x,将被解析为 z= y++ + x;
c. ratio = *x/*y,错误地编写了一个注释符号,语法器(?)报错;
3)函数返回指向局部变量的指针,(C语言中自动变量在堆栈中分配内存,生命周期结束时回收);
a. 返回一个指向字符串常量的指针;
b. 使用全局声明的数组;
c. 使用静态数组;(静态变量在静态数据区分配内存,深入看内存空间模型);
d. 显示分配一些内存,保存返回的值;(感觉最常用到,每次调用都创建一个缓冲区,但需要记得释放,否则会导致内存泄漏);
e. lint程序(?);
1)声明器:零个或多个(指针)+ 有且只有一个(直接声明器) + 零个或一个(初始化内容);
2)声明:至少一个(类型说明符) + 有且只有一个(声明器)+ 零个或多个(,+更多的声明器)+ 一个(分号);
3)例如:int (*func())();
4)声明的优先级规则;
图 1 声明的优先级规则
5) 声明的状态机;
6)cdecl 程序(实践)
1)typedef unsigned long size_t; size_t long_number;
2)typedef int x[10] 和 define x int[10]的区别: define为宏定义,编译时整体x替换为int[10];
3)编写解析C语言声明的程序,类似前、中、后缀表达式的互换,利用优先级与栈;
预处理 ====> 语法和语义分析 ===> 代码生成器 ===> 优化器 ===> 汇编程序 ===> 链接-载入器
(此处应该深入查看中间过程文件的打印)
程序经过编译但不链接接后,产生目标文件 *.o ; 此时程序需要将所有目标文件和库文件集中在一起,产生可执行文件(可以编写makefile控制编译、链接过程理解)。此时链接器确认main函数为程序开始执行的地方(此处可深入main函数的调用机制),把符号引用绑定到内存中固定的地址。
1)静态链接
函数库文件的一份拷贝是可执行文件的物理组成部分;
缺陷:已有的函数库可能与运行的系统不兼容;
2)动态链接
可执行文件只包含文件名,程序运行时到指定目录寻找函数库;
优点:
a. 减小可执行文件体积,但运行速度稍慢,(strip 去掉调试信息也能缩小可执行文件的体积);
b. 共享函数库的一个单独拷贝(映射到内存中的函数库被所有可使用它们的进程共享);
位置无关的代码(?),如果不使用与位置无关的代码,每个全局引用就不得不在运行时通过修改页面安排到固定的位置,这就使得页面无法共享(每个使用共享库的进程一般会把它映射到不同的虚拟地址;
1)GNU编译器会用-L pathname来指定动态链接库的位置,以gcc为例,
gcc test.c -o test -lmyBaic -L ./code/libs
该语句链接./code/libs下的libmyBasic.so,生成可执行文件test.
另外也可以指定环境变量,用来指定编译时查找动态库的位置,为LD_LIBRARY_PATH和LD_RUNPATH.
2)库约定
预处理命令#include<头文件>,有些约定了头文件的查找路径(自动链接),而有些会需要动态链接.
以Solaris 2.x为例:
自动链接:
动态链接:
3)静态库与动态库的动作(链接语义)不同
a.编译器命令行的符号解析是从左到右(C函数参数的内存分配是从右到左),静态库出现的顺序不同,结果就可能不同;同样,在代码之前引入静态库(gcc -lm main.c)会导致符号不会被提取(符号解析与提取过程),导致出错;
b.所以,编译时应始终将-l指令参数放在命令行的最右边;
4)interpositioning
interposion和缺省全局域:使用interpositoning后,系统函数被自己的取代,不管是在自己还是系统调用中
图 2 interpositioning
准则:不要让程序中的任何符号成为全局的,除非有意把它们作为程序的接口之一
在ld程序中使用-m选项,让连接器产生一个报告,里面包含了被interpose的符号说明(链接器报告文件).
当程序在scanf()和printf()中使用浮点数格式,但并不调用任何其他浮点数函数(浮点库)时,就有可能猜测错误(?).
1)声明:declaration; 定义: definition
引用:声明相当于普通的声明,它所说明的并非自身,而是描述其他地方的创建的对象;定义相当于特殊的声明,它为对象分配内存;
2)数组和指针的访问
a. 数组的下标引用(图)
图 3 数组的下标引用
b. 指针的引用(图)
图 4 对指针的引用
图 5 数组与指针的区别
1)“表达式中的数组名”就是指针;
2)C语言把数组下标作为指针的偏移量;
3)“作为函数参数的数组名”等同于指针;
4)其他所有情况中,定义和声明必须匹配;
1)C语言中的多维数组
2)使用指针创建和使用动态数组
#include
#include
int size;
char *dynamic;
char *input[10];
scanf("%d", size);
dynamic = (char *)malloc(size);
dynamic[0] = 'a';
1.a,out:assembler output(汇编程序输出)缩写,汇编程序和链接编辑输出格式(老式BSD文档),链接器输出.
2.运行时数据结构:堆栈,活动记录,数据,堆
1)堆栈:为临时、局部变量分配空间;函数调用时保护现场;用作暂时存储区;
2)过程活动记录:函数调用时用来保护现场的数据结构,存放于寄存器中时效率更高,相关函数setjmp()和longjmp(),类似catch/throw;
3)关于多线程,为每个线程分配不同的堆栈,提高处理性能
图 6 利器 -- 储备
* 寄存器窗口 -- 过程活动记录的内容放在寄存器中
* 保护模式 -- 段寄存器并不与偏移地址相加,而是为一个存放实际段地址的表提供索引
* Intel 80x86内存模型 -- 各处理器的地址空间并不一致,因为要保持兼容性
* 不同的段地址和偏移地址形成的指针可能指向同一个内存地址
* PC的内存模型:
* huge large far near
* small :
* large :
* medium :
* compact :
* 非标准关键字
* __near
* __far
* __huge
* 这些关键字修改的时它们右邻的项目,const和volatile类型修饰符所修改的是它们左边紧邻的项目
* 内存扩展方案expander和内存扩充方案extender
* 虚拟内存
* 页是操作系统在磁盘和内存之间移来移去或进行保护的单位,一般为几K字节
* 在磁盘中有一个特殊的“交换区”,用于保存从内存中换出的进程
* 两种类型的cache
* 全写法cache
* 写回法cache
* 行是对cache进行访问的单位
* 一个cache行内的数据称作块,保存来回移动于cache行和内存之间的字节数据;
* 进程的内部布局
* 堆不能按名字直接访问,只能通过指针访问
* 管理内存的调用是:
* malloc 和 free : 从堆中获得内存以及把内存返回给堆;
* brk 和 sbrk : 调整数据段的大小至一个绝对值(通过某个增量)
* 想获得以后能够返回给系统调用的内存时,使用mmap系统调用 映射/dev/zero文件
* 需要返回这种内存时,可以使用munmap系统调用
* 堆经常会出现的问题:
* 释放或改写仍在使用的内存(称为“内存损坏”)
* 未释放不再使用的内存(称为“内存泄漏”)
* bus error(core dumped) 总线错误 和 segmentation fault(core dumped) 段错误(信息已转储)
* 总线错误几乎都是由于未对齐的读或写引起的,被阻塞的组件就是地址总线, 对齐--数据项只能存储在地址是数据项大小的整数倍的内存位置上, 也可能由于引用一块物理上不存在的内存引起
* 段错误是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常通常是由于解除引用一个未初始化或非法值的指针引起的
* 通常导致段错误的几个直接原因:
* 解除引用一个包含非法值的指针;
* 解除引用一个空指针(常常由于从系统程序中返回空指针,并未检查就使用)
* 在未得到正确的权限时进行访问,例如,试图往一个只读的文本段存储值就会引起段错误
* 用完了堆栈和堆空间(虚拟内存虽然巨大但绝非无限)
* 可能导致段错误的常见编程错误:
* 坏指针值错误,在指针赋值之前就用它来引用内存;想库函数传送一个坏指针;对指针释放后再访问它的内容
* 改写错误:越过数组边界写入数据;改写一些堆管理数据结构
* 指针释放引起的错误
* 简单的说法:bus error意味着cpu对进程引用的一些做法不满,而segmentation error则是MMU对进程引用内存的一些情况发出抱怨
1)隐式类型转换
* C隐式类型转换注意:
* 隐式类型转换是语言的一种临机手段,起源于统一编译器处理数据的想法;
* 即便不理睬缺省的类型转换,也可以;
* 隐式类型转换在涉及原型的上下文显得非常重要;
隐性类型转换在涉及原型的上下文中非常重要
2)强制类型转换,涉及位扩展和位截除
一种新的函数声明机制,原因在于C函数声明可以不指定参数(不定参数?),而在原型中指定参数有利于形参和实参的参数检查;