C语言三剑客-C和指针、C专家编程、C陷阱和缺陷

阅读这三本书,记录一下平时没注意的细节点,持续更新…

C和指针

数据

1.四种基本类型是 整型、浮点型、指针、聚合类型(数组和结构等)
2.整型字面值缺省情况下是能完整容纳这个值的最短类型
3.字面值后加L/l为long整型,U/u为unsigned,数值前面以0开头为8进制,0x开头为16进制
4.字符常量类型总为int,单引号包围,’M’
5.宽字符常量:多字节字符常量前面加一个L,L’X’,需要运行环境支持才能使用
6.浮点类型存储非整数或远超过计算机整数所能表达的范围(unsigned int 型最大:4294967295 即10的10次方量级)
7.浮点类型缺省情况下都是double型,所有的浮点类型至少能容纳10-37~1037之间的任何值
8.字符串常量以NUL字节终止符结尾 “”空字符串依然有NUL
9.不允许修改字符串常量,如果需要修改字符串,请将其存储于数组中
10.程序中使用字符串常量会生成一个“指向字符串的常量指针”,使用时是指针
11.C函数库包含一组操纵字符串的函数
12.除char以外其他整型类型缺省情况下都是符号数,char因编译器而异
13.浮点类型除了long double之外,其余几个说明符(short signed unsigned)都是不可用的
14.编译器不检查数组下标是否在合法范围内,如果下标值是根据用户输入的数据产生的,那么必须进行检测
15.注意指针类型声明写法 int *a,*b,c 这条语句表达a产生的结果类型是int,*操作符执行的是间接访问操作,由此推定a是一个指向int的指针
16.函数如果不显式地声明返回值的类型,它就默认返回整型
17.作用域分为四类:文件作用域、函数作用域(只适用于语句标签,用于goto,一个函数中语句标签唯一)、代码块作用域、原型作用域(只适用于在函数原型中声明的参数名)
18.链接属性有3种:external(外部):不论声明多少次,位于几个源文件都表示同一实体、internal(内部):同一源文件内的所有声明指向同一实体,不同源文件的多个声明则分属不同的实体、none(无):该标识符多个声明都是独立不同的实体
19.关键字extern指定链接属性external(仅在第一次声明时有效),static指定链接属性internal
20.存储类型:普通内存(全局变量、静态变量,程序运行之前创建,程序执行期间始终存在)、运行时堆栈(代码内部声明的变量缺省类型是自动的automatic,执行到时创建,执行流离开该代码块时自动销毁)、硬件寄存器(关键字register用于自动变量的声明,存储于硬件寄存器,通常效率更高,实际使用情况根据编译器的寄存器优化方案决定)
21.静态变量初始化中,可以把想要的值放到程序执行时变量会用到的位置,可执行程序载入内存时,会将值赋给那个变量(即内存中位置不变)
22.Staic的理解:用于函数定义或代码块之外的变量声明时,用于修改标识符的链接属性,但标识符的存储类型和作用域不受影响(本来就是普通内存,本文件),只能在声明它们的源文件中访问;用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响(static只对external的声明才有改变链接属性的效果)。

语句

23.嵌套循环中,内层循环的break(结束)或continue(本次)只对内层循环起作用,无法影响外层
24.goto 语句标签;不建议使用,技巧:可用于立即从深层嵌套的循环中退出,但通过把深层嵌套的循环整体封装成一个函数,在深层嵌套处return效果一样

操作符和表达式

25.操作符 右移位分为逻辑移位和算数移位,无符号值都为逻辑移位,有符号数取决于编译器
26.位操作 置1:value=value|1< 27.Sizeof是一个单目操作符,判断它的操作数的类型长度,以字节为单位
28.a++与++a都复制一份变量值的拷贝,是一个右值,无法对它赋值,如++a=10不对
29.a++为复制后再增加变量a的值,拷贝值为a的值;++a为增加变量值后再复制,拷贝值为(a+1)的值
30.&&与||的短路求值特性:&&若左值为0,则不计算右值;||若左值为1,则不计算右值
31.逗号操作符(了解即可,基本没用):将两个或多个表达式分隔开来,这些表达式自左向右逐个进行求值,整个逗号表达式的值就是最后那个表达式的值
32.左值与右值:左值,显式的标识了一个可以存储结果值的地点,是一个位置;右值,指定了一个值;所以使用右值的地方可以使用左值,使用左值的地方不能使用右值
33.地址每一个值代表存的一个字节,是以字节为单位进行地址编码的
34.以字为单位存储的机器,每个字一般有2个或4个字节组成,有些机器要求边界对齐,即整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数

指针

35.指针未初始化,使用时可能发生错误,若指针指向非法地址,则会引发段违例(segmentation violation)或内存错误(memory fault);对于必须存储于特定边界的机器而言,如果处于内存中存储地址错误的边界上,对这个地址访问会产生总线错误(bus error)
36.对NULL指针解引用是违法的,在对指针解引用操作之前,必须确保它并非NULL指针
37.为了测试一个指针变量是否为NULL,可以将它与零值进行比较
38.&a是一个右值,因为无法得知这个地址值本身存储的位置,左值必须标识机器内存的特定位置,相同的道理,指针加减、自加自减是一个右值,但加上间接访问后就是一个左值
39.指针运算只有作用于数组中其结果才是可以预测的,指针运算使指针指向了数组第一个元素前面的内存位置或数组最后一个元素后面的内存位置都是非法的
40.加减整数是以指针类型为单位,如int*型加1即为加4字节

函数

41.函数原型即函数声明,编译器会记录函数参数数量,每个参数类型以及返回值的类型,之后就可以检查它的所有调用
42.函数原型具有文件作用域,所以单独写到.h文件中,在调用该函数的c文件中include,函数原型要与函数定义匹配
43.当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值
44.C的所有参数都是传值调用的,会复制一份被传递参数的拷贝;传数组名或者指针其实也是拷贝了一份数组名或指针,通过这个拷贝的指针去操作原来的数组,给人一种传址调用的感觉
45.抽象数据类型ADT也叫黑盒设计,即用static关键字,使模块的用户除了定义好的接口之外,用户不能以任何方式访问模块
46.可变参数:需要包含stdarg.h头文件,头文件中声明了一个类型va_list和三个宏–va_start、va_arg和va_end;函数声明了一个名叫var_arg,用于访问参数列表的未确定部分;用va_start来初始化,参数为va_list变量名(var_arg)和省略号前最后一个有名字的参数;宏va_arg有两个参数,va_list变量和参数列表中下一个参数的类型;访问完毕最后一个可变参数之后,我们需要调用va_end

数组

47.数组名的值是一个指针常量
48.sizeof数组名返回的是数组的长度
49.复制数组必须用循环,每个复制
50.对指针变量使用寄存器变量register,就不必复制指针值,可以使汇编代码更紧凑高效,但必须声明为局部变量;因为执行间接访问之前,需要把指针变量复制到地址寄存器中,使用register声明指针变量,其开始就在寄存器中每减少了复制的指令
51.声明为寄存器的指针通常比位于静态内存和堆栈中的指针效率更高
52.如果可以通过测试一些已经初始化并经过调整的内容来判断循环是否中止,那么你就不需要使用一个单独的计数器
53.数组与指针的区别:声明一个数组a时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明指针变量b时,编译器只为指针本身保留内存空间,并未被初始化指向任何现有的内存空间;*a可以,*b不行,b++可以,a++不行
54.数组作为参数传递时,是指针接收,无法知道数组的长度,需要显示传递一个数组长度的参数,注意这里说的是形参指针而不是数组名,sizeof数组名得到的是数组占用字节
55.二维数组a[4][3],不可写为a[4,3],操作符,表示首先对第一个表达式求值但随即丢弃这个值,a[4,3]相当于a[3]

字符串、字符和字节

56.字符串以一个位模式为全0的NUL字节结尾
57.Strlen返回的是无符号数,不可用来相减比较,因为结果不可能小于0
58.不受限制的字符串函数:strcpy会覆盖、strcat拼接,两者返回指向目标字符数组的指针;strcmp比较
59.长度受限的字符串函数:strncpy、strncat、strncmp
60.查找字符串strchr,返回一个指向该位置的指针;查找任何几个字符strpbrk,返回str中第1个匹配group中任何一个字符的字符位置;strstr查找子串
61.高级字符串查找:查找一个字符串前缀strspn;查找标记strtok,会修改它所处理的字符串,不可再入
62.错误信息strerror把操作系统的外部整形变量errno作为参数并返回一个指向用于描述错误的字符串的指针
63.字符操作:ctype.h,字符判断,符合则返回真:islower,isuper,isalpha,ispunct,isalnum等;字符转换:tolower,toupper;使用这些有助于提高函数的可移植性
64.内存操作:memcpy指针为void*类型,对于长度大于一个字节的数据要确保把数量和数据类型的长度相乘;memmove如果源和目标参数可能重叠,应使用memmove,因为它会先复制到一个临时位置;memchr查找给定长度数据中chr的位置;memcmp对两段内存比较,根据一串无符号字节进行比较的,如果用于比较不是但字节的数据如整数或浮点数时就可能给出不可预料的结果;memset从a开始length字节都设置成字符值ch

结构和联合

65.结构体声明时的名字为结构标签,不能用来单独常见变量,要结合struct关键字,可以用typedef创建一个新的类型,之后可以用这个类型名来创建变量
66.结构的自引用,自引用的结构体声明内部要是该结构体类型的指针,这样不会造成递归错误,链表和树都是通过这种技巧实现的
67.结构的存储分配:要满足边界对齐,让对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现,确定结构某个成员的实际位置,可以使用offsetof(type,member)宏,返回指定成员开始存储的位置距离结构开始存储的位置便宜几个字节
68.联合的各个成员具有不同的长度,联合的长度就是它最长成员的长度,如果成员长度相差悬殊,当存储长度较短的成员时,浪费很多空间;优化方法:在联合中存储指向不同成员的指针而不是直接存储成员本身,指针长度都是相同的,解决了内存浪费问题
69.联合变量可以被初始化,但初始值必须时联合第1个成员的类型,而且它必须位于一堆花括号里面
70.位段是结构的一种,它的成员长度以位为单位指定(即自定义类型都是长度),位段允许你把长度为奇数的值包装在一起节省存储空间,访问一个值内部的任意的一些位,使用位段比较简便;但本质上不可移植

动态内存分配

71.动态内存分配malloc失败会返回NULL,对于要求边界对齐的机器,malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求
72.malloc和calloc之间的主要区别calloc返回指向内存的指针之前把它初始化为0,但如果程序只是想把一些值存储到数组中,那么这个初始化过程纯属浪费时间;realloc用于修改一个原先已经分配的内存块的大小,如果原先的内存块无法改变大小,就新分配另一块内存,把原内存的内容复制到新的快上,因此要使用新的指针
73.动态内存分配常见错误:对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态内存的内存、试图释放一块动态分配内存的一部分以及一块动态内存被释放之后被继续使用
74.分配不释放导致内存泄露

链表

75.链表要始终保存一个指向链表当前节点之前的那个节点的指针
76.Root变量是指向链表第1个节点的指针,函数不能访问
77.单链表插入:首先新节点的link字段必须设置为指向它的后续目标节点,其次前一个节点的link字段要指向新节点。第二步可以通过保存前一个节点的link字段实现,但如果插入的链表的起始位置就需要单独处理(没有前一个节点的link)。改进方法是保存一个指向必须修改的link字段的指针(插入起始位置时指向root即可),而不是保存前一个节点的指针。
78.双链表插入一个新节点需要修改四个指针:新节点的前向后向,前一个节点的后向和后一个节点的前向

高级指针

79.变量赋值时,由于变量名在函数的作用域内部是未知的,无法简单赋值,所以可以通过指针进行间接访问操作以访问需要修改的变量
80.函数名被使用时总是由编译器把它转换为函数指针 int (*pf)(int)=&fint (*pf)(int)=f相同;同理在使用时,ans=(*pf)(25);与ans=pf(25)是相同的,因为编译器需要的是一个函数指针;
81.函数指针的应用:回调函数、转移表
82.转换表就是一个函数指针数组,创建需要两个步骤:1、声明并初始化一个函数指针数组,注意确保这些函数的原型出现在这个数组的声明之前;2、用下标选择正确的指针,函数调用操作符将执行这个函数;转换表可以理解为switch case语句,要注意检查下标在合法范围
83.命令行参数int main(int argc, char **argv);argc表示命令行参数的数目,argv指向一组参数值(本质上是一个数组),每个参数都是指向一个参数文本的指针(即指针数组),数组的结尾是一个NULL指针,其中第一个参数就是程序的名字。
84.字符串常量的加深理解:字符串常量的值是个指针常量,编译器把这些指定字符的一份拷贝存储在内存的摸个位置。并存储一个指向第一个字符的指针。数组名也是指针常量。“xyz”+1表示指向y的指针;*“xyz”表示字符x;”xyz”[2]表示字符z
85.将十进制转为十六进制输出的小技巧putchar(“0123456789ABCDEF”[value%16])

预编译

86.预处理阶段是在源代码编译之前对其进行一些文本性质的操作:删除注释、插入被#include指令包含的文件的内容、定义、替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译
87.预定义符号:__FILE__进行编译的源文件名,__LINE__文件当前行的行号,__DATE__文件被编译的日期(月日年),__TIME__文件被编译的时间(时分秒),__STDC__如果编译器遵循ANSI C,其值为1,否则未定义
88.如果#define的语句非常长,可以分几行,除最后一行外,每行的末尾都要加一个反斜杠
89.宏参数和#define定义可以包含其他#define定义的符号,但宏不可以出现递归
90.#define替换:当处理器搜索#define定义的符号时,字符串常量的内容并不进行检查, 如果想把宏参数插入到字符串常量中,可以使用两种技巧:
第一个技巧:根据临近字符串自动连接的特性将一种将字符串分成几段,每段实际上都 是一个宏参数

#define PRINT(FORMAT,VALUE) \
   		printf(“The value is” FORMAT “\n”,VALUE)
PRINT(%d”,x+3);

这种技巧只有当字符串常量作为宏参数给出时才能使用
第二个技巧:使用预处理器把一个宏参数转换为一个字符串。#argument这种结构被预 处理器翻译为“argument”

#define PRINT(FORMAT,VALUE)   \
   		printf(“The value of ” #VALUE \ 
“is” FORMAT “\n”,VALUE)
PRINT(%d”,x+3);

结果为:The value of x+3 is 25
91.##结构把位于它两边的符号连接成一个符号,允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(sum_number,value)
sum ## sum_number +=value
ADD_TO_SUM(5,25)
结果为sum5+=25
92.宏与函数:宏非常频繁地用于执行简单的计算,如
#define MAX(a,b) ( (a) > (b) ? (a) : (b) ) 与使用函数相比
优势:用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大, 所以宏比函数速度方面更胜一筹;更重要的是函数的参数必须声明为一种特定的类型, 而上面的宏可以用于整型、浮点等任何可以>操作符比较值大小的类型,即宏与类型无 关;还有一些任务无法用函数实现,如

#define MALLOC(n,type)\
   		((type*)malloc((n)*sizeof(type)))

第二个参数无法作为函数参数进行传递
劣势:宏每次使用时,一份宏定义代码的拷贝都将插入程序中,除非宏非常短,否则使 用宏可能会大幅度增加程序的长度
93.注意宏定义并没有用一个分号结尾,分号出现在调用这个宏的语句中
94.注意宏参数在宏定义中出现的次数超过一次时,展开后可能会导致不符合预想的副作用
95.#undef 用于移除一个宏定义
96.命令行定义,在编译时,可在命令行中用-D的方式进行宏定义或对程序中的宏定义进行复制,实现条件编译或设置程序中数组的大小;用-U选项可以在命令行去掉符号的定义
97.文件包含,#include,预编译器删除这条指令,并用包含文件的内容取而代之,声明放入头文件便于维护
98.函数库文件包含#include,编译器根据编译器的一系列标准位置查找函数库文件,如UNIX系统上的C编译器在/user/include目录查找函数库头文件,编译器由一个命令行选项,允许把其他目录添加到这个列表中;本地文件包含#include”filename”,常见策略是在源文件所在当前目录进行查找,找不到就像查函数库头文件一样在标准位置查找本地头文件
99.标准要求编译器必须支持至少8层的头文件嵌套
100.头文件卫士是对一个源文件反复包含同一头文件的保护,只包含一次,不是对多个源文件之间的,由可知#include是将指令替换成包含的内容,所以一个C文件包含一份然后编译成.o,再链接成可执行程序
101.其他指令 #error 生成错误信息 #line number “string” 它通知预处理器number是下一行输入的行号,string为可选部分,作为当前文件的名字,注意这条指令将修改__LINE__和__FILE__符号的值,常用于把其他语言转换成C程序,C编译器产生错误信息可以引用源文件而不是翻译程序产生的C中间源文件的文件名和行号;#progma不同编译器功能不同,允许编译器提供不标准的处理过程,本质上不可移植;无效指令#,会被预处理器简单删除,仅用于空出一行,凸显代码格式

输入/输出函数

102.ANSI C和早期C相比最大优点之一就是它在规范里所包含的函数库,每个ANSI编译器必须支持一组规定的函数,并具备规范所要求的接口,按照规定的行为工作
103.标准库函数在一个外部整型变量errno中保存错误代码之后把这个信息传递给用户程序,提示操作失败的准确原因,只有当一个库函数失败时,errno才会被设置,成功运行时,errno不会被修改,所以不能通过测试errno的值来判断是否有错误发生
104.错误报告 void perror(char const *message);打印message字符串,后面跟一个分号和一个空格,然后打印一条用于解释errno当前错误代码的信息
105.终止执行 void exit(int status);status参数返回给操作系统用于提示程序是否正常完成。预定义符号有EXIT_SUCCESS和EXIT_FAILURE,函数没有返回值,结束时程序已经消失无处可反
106.106.绝大多数流是完全缓冲的,用于输出流的缓冲区只有写满才会被刷新到设备或文件中,同理,输入缓冲区为空时通过从设备或文件读取下一大块较大的输入,重新填充缓冲区
107.在printf之后立即调用fflush可以迫使缓冲区的数据立即写入,不管它是否已满
108.就C程序而言,所有的I/O操作只是简单地从程序移进和移除字节的事情,这种字节流便被称为流,流分为两种类型,文本流和二进制流
109.文本流有些特性不同系统可能不同,如文本行的最大长度,标准规定至少允许254个字符,UNIX系统使用一个换行符结尾
110.二进制流是不经过任何改变的,适用于非文本数据,若不希望I/O函数修改文本文件的行末字符,也可以把它用于文本文件
111.每个ANSI C程序,运行时至少有三个流–标准输入stdin(缺省情况下输入的来源,如键盘)、标准输出stdout(缺省的输出设置,如屏幕)、标准错误stderr,它们都是一个指向FILE结构的指针
112.MS-DOS与UNIX支持用下面的方法重定向,data为来源,answer为输出设备
$progtam answer
113.EOF常量是许多函数的返回值,提示到达了文件尾,EOF所选择的实际值比一个字符要多几位,只是避免二进制值被错误的解释为EOF
114.一个程序做多打开文件数与编译器有关,但至少能打开FOPEN_MAX个文件(包括三个流),值至少是8
115.文件相关流I/O,程序为每个活动状态的文件声明一个FILE*,fopen打开流,按模式初始化FILE结构;fclose关闭流,刷新缓冲区的数据到文件中,释放FILE结构
116.标准I/O更简单,因为不需要打开或关闭,I/O三种基本形式处理数据:单个字符、文本行、二进制数据,这几个函数只用于stdin或stdout,随作为参数的流使用,使用内存中的字符串而不是流
117.输入输出函数家族
118.字符I/O fgetc和fputc是真正的函数,getc、putc、getchar、putchar都是通过#define指令定义的宏
119.撤销字符I/O ungetc,把一个先前读入的字符返回到流中,注意退回到流中和写入到流中并不相同,与一个流相关联的外部存储并不受ungetc的影响,退回字符与流当前位置有关,如果用fseek、fsetpos、rewind函数改变了流的位置,所有退回的字符都将被丢弃
120.行I/O分为未格式化的、格式化的,都用于操作字符串;未格式化的I/O简单读取或写入字符串,格式化的I/O则执行数字和其他的内部和外部表示形式之间的转换
121.未格式化I/O fgets、fputs和gets、puts,两者区别在于gets读取一行输入时,它并不在缓冲区中存储结尾的换行符,puts写入一个字符串时,在字符串写入之后向输出再添加一个换行符;另外gets没有缓冲区长度参数,可能会出现缓冲区越界
122.格式化I/O scanf家族、printf家族
123.二进制I/O,把数据写到文件效率最高的方法是用二进制写入,避免了在数值转换为字符串过程中所涉及的开销和精度损失,fread和fwrite
124.刷新函数 fflush,定位函数ftell返回流的当前位置,fseek根据参数在流中的定位
125.改变缓冲方式setbuf与setvbuf可以设置一个数组用于对流进行缓冲,防止I/O库函数为它动态分配一个缓冲区,传入NULL将关闭流的所有缓冲方式

void setbuf(FILE *stream, char *buf);
Int setvbuf(FILE *stream, char *buf, int mode, size_t size); 

mode指定缓冲的类型:_IOFBF完全缓冲的流、_IONBF不缓冲的流、_IOLBF行缓冲流 即每当一个换行符写入缓冲区时,缓冲区便进行刷新
指定缓冲区的大小最好用一个长度为BUFSIZ的字符数组作为缓冲区(stdio.h中定义), 若指定的缓冲区不是操作系统内部使用的缓冲区的整数倍,就可能需要一些额外的磁盘 操作,反而降低效率
126.流错误函数:feof判断是否在流末尾,ferror报告流的错误状态,clearerr函数对指定流的错误标准进行重置
127.临时文件FILE *tmpfile(void);创建一个文件,当文件被关闭或程序终止时这个文件便自动删除,该文件以wb+模式打开,注意如果临时文件需要以其他模式打开或者由一个程序打开但由另一个读取就不适合用tmpfile了,必须用fopen打开并用remove显式删除;char *tmpnam(char *name);用于创建临时文件的名字,传入NULL,则返回一个指向包含文件名的静态数组的指针,否则传入一个指向长度至少L_tmpnam的字符数组指针,文件名在这个数组里,返回值就是这个参数。只要调用次数不超过TMP_MAX次,tmpnam每次调用时都能产生一个新的不同的名字,不会出现同名的问题
128.文件操纵函数remove删除一个指定文件,要处于关闭状态,若打开状态,其结果取决于编译器,rename用于改变一个文件的名字

标准函数库

129.非本地跳转:setjmp和longjmp
int setjmp(jmp_buf state);
void longjmp(jmp_buf state, int value);
声明一个jmp_buf变量,调用setjmp初始化,setjmp把程序的状态信息保存到跳转缓冲区,调用setjmp时所处的函数便为跳转到的顶层函数,在顶层函数及其他任何它所调用的函数内任何地方调用longjmp,将导致这个被保存的状态重新恢复,适用于嵌套特别深层的函数中出现一个错误,可以简化中间函数对错误代码的判断;
注意:当调用setjmp的函数返回时,保存在跳转缓冲区的信息不在有效,longjmp很可能失败,所以setjmp要在顶层函数中调用
130.信号
同步信号:程序内部发生的,可由abort和raise引起
SIGABRT 程序请求异常终止:abort函数引发
SIGFPE 发生一个算术错误:由编译器决定,常见有算术上溢以及除零错误
SIGILL 检测到非法指令:不正确的编译器设置导致或程序执行流错误,未初始化的函数指针调用一个函数,导致CPU试图执行实际上是数据的东西(把数据段当成的代码段)
SIGSEGV 检测到对内存的非法访问:程序访问未安装、未分配的内存或违反内存访问的边界要求(未初始化的指针易引起)
异步信号:程序外部产生,用户触发
SIGINT 收到一个交互性注意信号:默认终止程序,常用策略是定义一个SIGINT的信号处理函数,执行一些日常维护工作并在程序退出前保存数据
SIGYERM 收到一个终止程序的请求:一般不配备信号处理函数,程序终止是不执行日常维护工作
131.程序显式引发信号:int raise(int sig); 可用于对信号处理函数进行测试
132.设置信号处理函数 void(*signal(int sig, void (*handler(int)))) (int); 返回值为一个指向该信号以前处理函数的指针,第二个参数若可以不传入函数,使用SIG_DFL恢复缺省设置和SIG_IGN忽略该信号
133.信号发生时,系统会首先恢复对该信号的缺省行为,然后调用信号处理函数,目的是方式信号处理函数内部也发生这个信号导致无线循环
134.异步信号处理函数不能调用除signal之外的任何库函数,无法访问除volatile sig_atomic_t类型之外的静态变量,为了保证安全,信号处理函数所做的是对这些变量之一进行设置然后返回,程序设于部分定期检查变量的值看是否有信号发生
135.sig_atomic_t类型是CPU以原子方式访问的数据类型,不可分割的访问单位。如16位机器可以以原子方式访问一个16位整数,但访问32位可能要两个操作。信号处理函数中要把数据访问限制为原子单位,防止访问数据中间产生一个信号到导致不一致的结果
136.volatile告诉编译器,变量的值可能在任何时候发生改变,防止编译器以修改程序的方式优化程序
137.如果希望捕捉同种类型的信号,在信号处理函数返回之前注意要调用signal函数重新设置信号处理函数,否则只有第一个信号才会被捕捉,接下来将使用缺省反应进行处理
138.打印可变参数列表 vsprintf/vfprintf/vsprintf,arg参数必须使用va_start进行初始化
139.终止执行
void abort(void) 引发SIGABRT信号
void atexit(void (func)(void))可以把一些函数注册为退出函数,当程序正常终止时,退出函数将被调用,退出函数不接受任何参数
void exit(int status) 用于正常终止程序,exit函数被调用时,所有被atexit函数注册为退出函数的函数将按照它们所注册的顺序被反序依次调用,所有用于流的缓冲区被刷新,所有打开的文件被关闭,用tmpfile函数创建的文件被删除,然后退出状态返回给宿主环境,程序停止执行
140.编译时可以通过定义NDEBUG消除所有的断言,也可以在assert.h中加#define NDEBUG
141.环境是一个由编译器定义的名字/值对的列表,getenv函数在列表中查找一个特定的名字,返回指向其对应值的指针,程序不能修改char *getenv(cahr const *name)
142.执行系统命令:system函数把它的字符串参数传递给宿主操作系统,这样它就可以作为一条指令,由系统的命令处理器执行

经典抽象数据类型

143.ADT-数据抽象类型:链表、堆栈、队列和树等
144.获取内存来存储值有三种方案:静态数组、动态数组、动态链式结构
静态数组长度固定,编译时确定
动态数组运行时决定长度,可以通过分配一个更大的数组,把原来的数组复制过来并删除,实现改变数组长度的目的
链式结构灵活性最大,但链接字段消耗内存,且访问特定元素的效率不如数组
145.堆栈:push、pop、top(访问栈顶元素但不弹出);队列:insert、delete;
146.二叉树:每个节点的值比它的左子树的所有节点值都要大,但比右子树的所有节点的值都要小
147.二叉树操作:
插入:按照定义,可递归,但属于尾部递归,换用迭代效率更高
删除:删除叶节点不需重新连接;删除有一个孩子的节点,直接把双亲和孩子连接;删除有两个孩子的节点,删除它左子树中值最大的那个节点,并用这个值代替应被删除的那个节点的值
查找:按照定义,可递归,但属于尾部递归,换用迭代效率更高
遍历:前序(根左右)、中序(左根右)、后序(左右根)、层次遍历
148.二叉树可用链式结构或者数组结构实现(结合动态数组也可以不限制树中的元素数量)
149.数组形式的二叉树规则:节点N的双亲是节点N/2、左孩子是节点2N、右孩子是2N+1;由于数组下标第一个为0,若略过会浪费空间,所以节点N的双亲是节点((N+1)/2)-1、左孩子是节点2N+1、右孩子是2N+2
150.数组二叉树的问题:空间利用不充分,因为新值必须插入到数中的指定位置,不能随便放置到数组中的空位置,如全是右子树,左子树部分空间就是全部浪费的
151.链式二叉树不存在不使用的内存,每个节点用一个结构容纳值和两个指针
152.ADT使用技巧
拥有超过一个的堆栈:
若把保存结构的内存和用于操纵的函数封装在一起,则一个程序只能有一个堆栈,从堆栈的实现模块中去除数组和top_element的声明,把他们放入用户代码中,然后通过参数被堆栈函数访问,这些函数便不再固定于某个数组,用户可以创建任意数量的数组,并调用堆栈函数将它们作为堆栈使用
拥有超过一种类型:
三种方法:1.编写堆栈函数的拷贝,处理不同的数据类型,缺点是维护困难;2.把堆栈模块实现为一个#define宏,把目标类型作为参数,便可以用于创建每种目标类型的堆栈函数,注意必须要为不同类型的堆栈函数产生独一无二的函数名,每种类型只能创建一组函数;3.堆栈与类型无关:堆栈只存储void类型的指针,转成void后push,top函数返回值转换回原先的类型,注意这种方法绕过了类型检查,要保证是预期的正确类型
153.标准函数库的ADT,由于名字冲突、类型缺乏安全性、用户操纵数据危险,使用泛型来实现,编写一组函数,随后用户不同类型进行实例化或创建,C语言未提供这种能力(C语言远早于泛型这个概念提出),但可以用#define近似的模拟这种机制,分成三个独立的宏:声明接口、创建操作数据的函数、创建数据

运行时环境

154.提高运行时效率,统计各函数耗费时间及调用次数,1.库函数耗费时间多的,重新设计程序尽量不用;2.调用次数多,每次耗费时间短的可以加上register声明提高效率;3.调用次数少,每次耗费时间多的,寻找更高效的算法

C陷阱与缺陷

这个博主总结的特别好
C陷阱与缺陷

你可能感兴趣的:(c语言,数据结构,算法)