书上底层的东西比较多,还有些东西不是很理解,等学习完更多的东西后得回头看看
知识点比较杂,这里只是简单的把自己觉得有用的东西罗列出来
- C语言的基本数据类型与底层硬件相对应
- 宏
- 使用应适量,不宜过度使用
- 宏名建议要大写
- 不宜使用宏来改变C语言的基础结构
- 不宜省略形参名
- 合法的指针赋值形式
- 两个操作数都是指向有限定符或无限定符的相容类型指针
- 左边指针所指向的类型必须具有右边指针所指向类型的全部限定符
- const并不能把一个变量变为一个常量
只是限定它不可修改
const常用于函数的形参 - 算术转换
当执行算术运算时,操作数的类型如果不同,就会发生转换。
数据类型一般朝着浮点精度更高,长度更长的方向转换,
整型数如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned - sizeof()不是函数,而是运算符,其结果为无符号整型
- sizeof的操作数如果是类型,必须加括号,如
sizeof(int)
- sizeof的操作数如果是变量,有无括号都可以,如
sizeof(a)
和sizeof a
- sizeof的操作数如果是类型,必须加括号,如
- 程序中应避免无符号数,以免增加不必要的复杂性
- 编程语言的缺陷一般分为三类:
不该做的做了,该做的没做,该做但做的不合适 - NUL 和 NULL
NUL用于结束一个字符串(即\0
)
NULL表示一个空指针 - 临时变量可声明于花括号的局部空间内(如if、while等语句的花括号内)
- 两个相邻的常量字符串会自动合并
注意:两个常量字符串作为函数的两个参数时,如果不小心漏掉了逗号,会出现错误 - C语言中许多符号是被“重载”的,即在不同的上下文环境里有不同的意义
- C语言中部分符号优先级是不符合常规逻辑的,如:
- ==和!=高于位操作符
注意val & mask != 0
- ==和!=高于赋值符
注意c = getchar() != EOF
- 算数运算符高于移位运算符
注意msk << 4 + lsb
- ==和!=高于位操作符
- 逗号表达式的值为最右边操作数的值
注意i = 1, 2
- 一些比较老的程序存在诸如
if( a == b & c == d )
的语句
由于==优先级高于&,所以与if( a == b && c == d)
的执行效果是相同的
历史原因:逻辑运算符是后来从&中分离出来的,但为了兼容旧的代码,只好使==优先级高于& - 一个表达式中各意群的执行顺序是未定义的
如:x = f() + g() * h();
,其中f()、g()、h()谁先调用是未定义的(注意f()不一定是在g()和h()之后) - 有些编程专家建议:只记住乘除法优先于加减法即可,其他运算都用括号显式地指定优先级
- 结合性用于相同优先级的操作符之间消除歧义;
- 优先级相同的操作符,其结合性也相同
- 如果计算表达式的值时需要考虑结合性,最好将其一分为二或者使用括号
- 函数调用中,各个参数的计算顺序是不确定的
但是传参顺序是一定的(从右向左) - “在调用函数时,参数按照从右到左的次序压到堆栈里”的说法是错误的
参数在传递时首先尽可能地放到寄存器中,以提高速度
注意:int变量i与结构变量s的int成员的传参方式是不同的
前者一般会传递到寄存器中,后者一般会传递到堆栈中 - gets()函数存在严重漏洞,官方手册强烈建议用fgets()函数取代
即把gets(line)
改为
if( fgets(line, sizeof(line), stdin) == NULL ) exit(1);
- C语言中读取argv的选项,其开头必须先判断首字符是否为“-”(有可能是文件名而不是参数)
- 尽量避免用
\
转义回车来将代码分解为多行
万一在\
之后不小心加入了空格,将带来难以发现的bug - maximal munch strategy(最大一口策略)
如果下一个标记有超过一种的解释方案,编译器将选取能组成最长字符序列的方案
如:z=y+++x
将被解释为z = (y++) + x
(但是最好还是用括号显示表示出来) - 不应试图返回一个指向局部自动变量或数组的指针
若为单一变量,会引起报错
但如果为数组,有可能会因为某些间接形式而不会报错,该bug不易发现 - 函数返回数组的方法
- 若为字符串常量,可以直接返回指针
- 使用全局声明的数组
- 使用静态数组
- 显示分配一些堆内存
- 最好的方法:由调用者分配堆内存,将内存的指针传入函数
- lint程序已经从编译器中被分离的出来,但是源代码应尽可能通过lint和cstyle程序的检查
- C语言声明形式的原则:与使用形式相似
如:int *p;
但是,这种原则并不直观,如指针声明形式改为int &p;
更为合理(C++已采纳这种声明形式) - const和指针
int const * grape;
:指向的整型只读
int * const grape;
:指针只读
记忆:把const作为类型后缀,接在*
之后针对指针,接在int
之后针对整型 - 声明的结构
- 至少一个类型说明符(包含类型说明符、存储类型、类型限定符)
- 至少一个声明器(包括指针、直接声明器、初始化内容),多个声明器由逗号分隔开
- 有且只有一个分号
- 声明中,与标识符最贴近的(按照优先级、结合性)的符号即为标识符的本质
如:int (*foo[])();
,最贴近的是[]
,即foo是一个数组 - 结构的定义及其变量的声明最好分开
“一行代码只做一件事”的C语言编程原则 - 如果需要频繁的对整个数组进行赋值操作,可以把该数组放入结构中
(结构变量可以整体赋值) - 联合的作用:
- 节省空间(但在大内存的今天,该作用不大)
- 将同一个数据解释成两种不同的东西
(下面这个例子可以整体取得整个四字节变量,也可以分别取得各个字节)
union bits32_tag{
int whole;
struct { char c0, c1, c2, c3; } byte;
} value;
- 可以巧用枚举类型来方便调试
宏定义的名字在编译时会被丢弃,但枚举名字在调试器中会一直存在 - C语言声明解析方法(见书P64-66)
typedef
和#define
前者较后者,“封装”更为彻底
后者在声明时能加入其他类型说明符进行扩展,前者声明时不行
如将诸如int (*foo[])();
的定义“封装”起来
typedef int (*A[])();
和#define NEW(B) ( (*B[])() )
A x;
和NEW(x);
等价,均可行A x, y;
和NEW(x, y);
不等价,只有前者可行
- 不能给typedef加入多个声明器
即不能企图通过typedef定义一个类型名,使之一次性声明多种类型的变量 - 不能把“typedef”嵌入到声明当中
- typedef和struct的标签名可以和变量名相同,但应尽量避免
- typedef的用法:
- 数组、结构、指针、函数等的组合类型
- 可移植类型
- typedef定义的标签名也可以用于强制类型转换
- 赋值符左侧的操作数称为左值
左值分为可修改左值和不可修改左值(如数组名) - 编译过程:C预处理器、前端(语法和语义分析)、后端(代码生成器)、优化器、汇编程序、链接-载入器
- 静态链接(
*.a
):编译过程中,所用到的函数会被装入到可执行文件中(即每个程序对函数代码有一份单独的拷贝)
动态链接(*.so
):编译过程中,只把用到的库文件名或路径名装入可执行文件,需要时再实时调用(即每个程序对函数代码共用一份拷贝) - 动态编译运行速度稍慢,但可执行文件体积小,函数库版本升级更容易,而且部分函数库只能通过动态链接的形式使用
- 函数库包含函数的定义,头文件包含函数原型的声明
头文件的名字通常不和它所对应的函数库名相似 - Interpositioning:
自己编写的函数名如果与函数库函数同名,自定义的函数会取代函数库的函数而不会引发编译器的报错 - 段
在UNIX中,段是一个二进制文件相关的内容块
在Intel x86内存模型中,地址空间并非一个整体,而是分成若干64K大小的区域,这些区域称为段 - a.out(assembler output,汇编程序输出,一种目标文件格式)依次包含以下内容:
- a.out的标志数字0407(即Kirk McKusick的生日,也是PDP-11无条件转移指令的二进制编码)
- a.out的其他内容
- BSS段所需的大小
BSS即Block Started by Symbol(由符号开始的块),保存了没有值的变量
这里只保存了变量的大小,但并不占据目标文件的任何空间
如:C语言中未初始化的全局变量
注意:局部变量并不进入a.out,而是在运行时创建 - 数据段
包括初始化后的全局变量和静态变量 - 文本段
包含可执行文件的指令
- 进程的地址空间分布(由低到高)
- 未被映射的虚拟地址空间
任何对它的引用都是非法的,一般包括几K字节,用于捕捉空指针和小整型值的指针引用内存的情况 - 文本段
- 数据段
通常是进程中最大的段 - BSS段
数据段和BSS段合称数据区 - 堆栈段
- 主要有三个用途:
- 为函数内部声明的局部变量提供存储空间
- 进行函数调用时,保存相关的维护性信息(过程活动记录等)
- 作为暂时的存储区
- 函数可以通过参数或全局指针访问它所调用的函数的局部变量,运行时系统维护一个用于提示堆栈当前顶部位置的指针(常位于寄存器中,称为sp)
- 主要有三个用途:
- 未被映射的虚拟地址空间
- C不支持函数嵌套的原因
支持函数嵌套的语言一般采取上层引用的访问方式,需要活动记录包含一个指向它的外层函数的活动记录的指针(被称为静态链接),它允许内层过程访问外层过程的活动记录,因此也可以访问外层过程的局部数据;
而C语言所用函数在词法层次中都是最顶层的、分别独立的 - volatile类型修饰符
- volatile本意为易变的
- 用来告知编译器,程序每次使用该变量都要从内存中读取(出于优化目的,编译器有可能会直接在暂存的寄存器中读取)
- 常用于多线程编程等情况
- 线程控制
- 头文件:
<setjmp.h>
setjmp( jmp_buf j )
:保存当前的过程活动记录到j,并返回0longjmp( jmp_buf j, int i )
:跳转到变量j所在的过程活动记录位置,并返回isetjmp / longjmp
和{标签} / goto
:
前者只能跳转回曾经到过的地方,后者可以任意设置标签的位置
后者不能跳出函数,前者甚至可以跳转到其他文件的函数- 为了保证跳转过程中局部变量的可靠性,应使用volatile类型修饰符
- 常用于错误修复(发现不可修复的错误即把控制转入主输入循环,重新开始)、从深层嵌套的函数中跳出等
- 跟goto一样,这样控制线程会使程序难以理解和调试,应尽量避免使用
- 头文件:
- UNIX和MS-DOS中的堆栈段
- UNIX的堆栈段是自动生长的
当试图访问系统分配给堆栈的空间之外时,将会产生一个硬件中断,称为页错误
一般,内核会通过向进程发送合适的信号(可能是段错误)来处理无效地址的引用 - DOS的堆栈段是不可增长的,建立可执行文件时必须确定
- UNIX的堆栈段是自动生长的
- 常用的C语言工具
- 源代码检查:
- cb:C程序美化器,使源文件有标准的布局和缩进格式
- lint:C程序检查器
- 可执行文件检查:
- dis:(
/usr/ccs/bin
)目标代码反汇编工具 - nm:(
/usr/ccs/bin
)打印目标文件的符号表
- dis:(
- 调试:
- debugger:交互式调试器
- 性能优化:
- time:(
/usr/bin/time
)显示程序所使用的实际使用和CPU时间
- time:(
- 源代码检查:
- 内存媒介(速度、成本依次增加)
磁带、硬盘、内存、cache存储器、CPU寄存器 - 虚拟内存
- 以“页”为单位(一般为几K)在磁盘和内存之间来回移动(page in为移入内存,page out为移到硬盘),也以“页”为单位对内存进行保护
- 只有用户进程的内存在会被换进还出,系统内核进程常驻于物理内存当中
- 进程只能操作位于物理内存中的页面
当进程引用一个不存在于物理内存中的页面时,MMU(内存管理单元,负责支持虚拟内存的硬件)会产生“页错误”。内核将对事件作出响应,
若判断有效,内核将从硬盘中取回对应的页,换入物理内存当中
若判断无效,内核将向进程发出一个“segmentation violation(段违规)”信号
- Cache存储器
- 从MMU的角度看,不同机器Cache存储器的所属不同
- 有些机器的Cache属于CPU一侧,Cache采用的是虚拟地址,每次切换进程时,它的内容必须进行刷新
- 有些机器的Cache属于物理内存一侧,Cache采用的是物理地址,容易使多个CPU共享同一个Cache
- 数据以“行”(一般为16字节或32字节)为单位从内存中读入Cache
- 所有对内存的读写都要经过Cache
当处理器需要从某个特定的地址提取数据时,这个请求首先递交给Cache
如果数据已存在于Cache,它可以被直接提取;
否则,Cache向内存传递这个请求,进行较缓慢的访问内存操作,并将数据读取到Cache - 常见的Cache类型
- 全写法Cache:每次写入Cache时同时写入内存,保持两者内容一致
- 写回法Cache:第一次写入时,只写入Cache;再次写入时,此时会把之前的数据写入到内存中保存起来;内核切换进程时,Cache数据都要写入内存中去
- 从MMU的角度看,不同机器Cache存储器的所属不同
- 拷贝整个数组
用库函数memcpy( 目的数组, 源数组, 元素个数 )
进行快速拷贝
比循环拷贝快的多 - malloc请求申请的内存大小为方便起见通常会被圆整为2的乘方
- 生存时间长的程序需要管理动态内存的分配和回收,堆经常会出现两种问题:
- 内存损坏:释放或改写仍在使用的内存
- 内存泄漏:未释放不再使用的内存
- alloca分配的动态内存,在离开函数时会被自动释放
可以避免内存损坏和内存泄漏,但是不适用于创建比其所在函数生命周期更长的结构 - 体积大的进程更有可能被系统换出,换进换出的时间也更长
- 总线错误(bus error)
- 主要是未对齐的读写引起的
对齐:数据项只能存储在地址是数据项大小的整倍数的内存位置上,通过迫使每个内存访问局限在一个Cache行或一个单独的页面内,可以极大的简化并加速如Cache控制器和MMU等硬件
出现未对齐的内存访问请求时,被堵塞的组件就是地址总线 - 也有可能是引用物理上不存在的内存引起
- 主要是未对齐的读写引起的
- 段错误(segmentation fault)
- MMU异常导致的错误,通常由解除引用一个未初始化或非法值的指针引起
但是,如果未初始化的指针恰好具有未对齐的值,它会引起总线错误但不引起段错误 - 导致段错误的常见直接原因:
- 解除引用一个包含非法值的指针
- 解除引用一个空指针
- 在未得到正确的权限时进行访问(如往只读的文本段存储值)
- 堆栈或堆空间用完
- 导致段错误的常见编程错误:
- 坏指针值错误
对未初始化的指针解除引用,或向库函数传递一个坏指针,或解除引用已经被释放的指针
建议养成指针释放后立即将其设置为NULL的习惯 - 改写错误
越过数组边界写入数据,或在动态分配的内存之外写入数据,或改写一些堆管理数据结构 - 指针释放引起的错误
多次释放同一块内存,或释放一块未分配的内存,或释放一个无效指针
- 坏指针值错误
- MMU异常导致的错误,通常由解除引用一个未初始化或非法值的指针引起
- 类型提升
在所有表达式中,char都会被提升为int,float都会被提升为double
在老式编译器中,传参的时候也会被提升类型,然后再被适当裁剪
意义:简化编译器,压到堆栈中的参数都是同一长度的,运行时只需要知道参数数量,而不需要知道他们的长度
但是,在新风格的函数定义下,已经声明过原型的函数,其参数不会进行类型提升,即char会直接以char类型进行传参 - 如果库函数调用或系统调用出错,程序会设置全局变量
errno
的值来提示问题原因
注意:只有确实出现问题时,errno的值才是有效的
用法:在函数调用或系统调用前先设置全局变量errno = 0;
,函数调用后再检查errno的值
具体数值与其对应的错误类型参见errno_百度百科 - C语言与有限状态机(Finite State Machine)
- FSM用于基于输入的在几个不同的可选动作中进行循环的程序
- 基本思路:用一张表保存所有可能的状态,并列出进入每个状态时可能执行的所有动作,其中最后一个动作就是计算(通常是在当前状态和下一次输入的基础上,再经过一次查询)下一个应该进入的状态
- C语言实现:绝大多数基于函数指针数组,状态函数返回一个通用的函数指针(void *),再转换为适当的类型
- 分析环(最简单的FSM)
- 分析环用于绝大多数状态转换都是按连续的顺序进行的,与输入无关的程序
- 不需要建立一个转换表用于匹配状态/输入以获得下次状态
- 直接用switch语句和循环也可以实现简单的FSM
- 如果函数的参数涉及多个不同的参数,可以考虑使用类似
int argc, char *argv[]
的参数计数器和一个字符串指针数组 - 可调试性编码:
把系统分成几个部分,先让程序总体结构运行,再逐步细化调试 - 程序的提示信息应具有启发性,而非煽动性,要避免使用带有亵渎性、口语化、幽默(平时自己的小程序还是该幽默点的好QAQ)或者夸张的非专业用语
- 使用realloc()函数时,不应直接把返回值直接赋给原指针
否则,一旦realloc失败,返回NULL,那么将无法重新访问、或是释放原来的内存块 - 指针&数组
- 复合符号中间不允许嵌有空白
以a---b
为例,- 根据C语言词法分析器的“贪心法”处理策略,
该表达式应被视为( a-- ) - b
- 在变量与符号之间添加空白,如
a --- b
是没有问题的, - 但如果在符号之间添加空白,会使符号失去原意,产生准二义性问题,
如a - -- b
将被解释为a - ( --b )
- 除了
++
和--
之外,/*
等也可能产生准二义性问题
如y=x/*p
中/*
会被解释为注释起始
而y = x / * p
中/
和*
会分别被解释为 除法 和 解除引用
- 根据C语言词法分析器的“贪心法”处理策略,
- else始终与 同一对括号内 最近 的 未匹配 的if结合