《C专家编程》小记

书上底层的东西比较多,还有些东西不是很理解,等学习完更多的东西后得回头看看
知识点比较杂,这里只是简单的把自己觉得有用的东西罗列出来

  • C语言的基本数据类型与底层硬件相对应
    • 使用应适量,不宜过度使用
    • 宏名建议要大写
    • 不宜使用宏来改变C语言的基础结构
  • 不宜省略形参名
  • 合法的指针赋值形式
    1. 两个操作数都是指向有限定符或无限定符的相容类型指针
    2. 左边指针所指向的类型必须具有右边指针所指向类型的全部限定符
  • const并不能把一个变量变为一个常量
    只是限定它不可修改
    const常用于函数的形参
  • 算术转换
    当执行算术运算时,操作数的类型如果不同,就会发生转换。
    数据类型一般朝着浮点精度更高,长度更长的方向转换,
    整型数如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned
  • sizeof()不是函数,而是运算符,其结果为无符号整型
    • sizeof的操作数如果是类型,必须加括号,如sizeof(int)
    • sizeof的操作数如果是变量,有无括号都可以,如sizeof(a)sizeof a
  • 程序中应避免无符号数,以免增加不必要的复杂性
  • 编程语言的缺陷一般分为三类:
    不该做的做了,该做的没做,该做但做的不合适
  • NUL 和 NULL
    NUL用于结束一个字符串(即\0
    NULL表示一个空指针
  • 临时变量可声明于花括号的局部空间内(如if、while等语句的花括号内)
  • 两个相邻的常量字符串会自动合并
    注意:两个常量字符串作为函数的两个参数时,如果不小心漏掉了逗号,会出现错误
  • C语言中许多符号是被“重载”的,即在不同的上下文环境里有不同的意义
  • C语言中部分符号优先级是不符合常规逻辑的,如:
    1. ==和!=高于位操作符
      注意val & mask != 0
    2. ==和!=高于赋值符
      注意c = getchar() != EOF
    3. 算数运算符高于移位运算符
      注意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不易发现
  • 函数返回数组的方法
    1. 若为字符串常量,可以直接返回指针
    2. 使用全局声明的数组
    3. 使用静态数组
    4. 显示分配一些堆内存
    5. 最好的方法:由调用者分配堆内存,将内存的指针传入函数
  • lint程序已经从编译器中被分离的出来,但是源代码应尽可能通过lint和cstyle程序的检查
  • C语言声明形式的原则:与使用形式相似
    如:int *p;
    但是,这种原则并不直观,如指针声明形式改为int &p;更为合理(C++已采纳这种声明形式)
  • const和指针
    int const * grape;:指向的整型只读
    int * const grape;:指针只读
    记忆:把const作为类型后缀,接在*之后针对指针,接在int之后针对整型
  • 声明的结构
    1. 至少一个类型说明符(包含类型说明符、存储类型、类型限定符)
    2. 至少一个声明器(包括指针、直接声明器、初始化内容),多个声明器由逗号分隔开
    3. 有且只有一个分号
  • 声明中,与标识符最贴近的(按照优先级、结合性)的符号即为标识符的本质
    如:int (*foo[])();,最贴近的是[],即foo是一个数组
  • 结构的定义及其变量的声明最好分开
    “一行代码只做一件事”的C语言编程原则
  • 如果需要频繁的对整个数组进行赋值操作,可以把该数组放入结构中
    (结构变量可以整体赋值)
  • 联合的作用:
    1. 节省空间(但在大内存的今天,该作用不大)
    2. 将同一个数据解释成两种不同的东西
      (下面这个例子可以整体取得整个四字节变量,也可以分别取得各个字节)
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[])() )
    1. A x;NEW(x);等价,均可行
    2. A x, y;NEW(x, y);不等价,只有前者可行
  • 不能给typedef加入多个声明器
    即不能企图通过typedef定义一个类型名,使之一次性声明多种类型的变量
  • 不能把“typedef”嵌入到声明当中
  • typedef和struct的标签名可以和变量名相同,但应尽量避免
  • typedef的用法:
    1. 数组、结构、指针、函数等的组合类型
    2. 可移植类型
  • typedef定义的标签名也可以用于强制类型转换
  • 赋值符左侧的操作数称为左值
    左值分为可修改左值和不可修改左值(如数组名)
  • 编译过程:C预处理器、前端(语法和语义分析)、后端(代码生成器)、优化器、汇编程序、链接-载入器
  • 静态链接(*.a):编译过程中,所用到的函数会被装入到可执行文件中(即每个程序对函数代码有一份单独的拷贝)
    动态链接(*.so):编译过程中,只把用到的库文件名或路径名装入可执行文件,需要时再实时调用(即每个程序对函数代码共用一份拷贝)
  • 动态编译运行速度稍慢,但可执行文件体积小,函数库版本升级更容易,而且部分函数库只能通过动态链接的形式使用
  • 函数库包含函数的定义,头文件包含函数原型的声明
    头文件的名字通常不和它所对应的函数库名相似
  • Interpositioning:
    自己编写的函数名如果与函数库函数同名,自定义的函数会取代函数库的函数而不会引发编译器的报错

  • 在UNIX中,段是一个二进制文件相关的内容块
    在Intel x86内存模型中,地址空间并非一个整体,而是分成若干64K大小的区域,这些区域称为段
  • a.out(assembler output,汇编程序输出,一种目标文件格式)依次包含以下内容:
    1. a.out的标志数字0407(即Kirk McKusick的生日,也是PDP-11无条件转移指令的二进制编码)
    2. a.out的其他内容
    3. BSS段所需的大小
      BSS即Block Started by Symbol(由符号开始的块),保存了没有值的变量
      这里只保存了变量的大小,但并不占据目标文件的任何空间
      如:C语言中未初始化的全局变量
      注意:局部变量并不进入a.out,而是在运行时创建
    4. 数据段
      包括初始化后的全局变量和静态变量
    5. 文本段
      包含可执行文件的指令
  • 进程的地址空间分布(由低到高)
    1. 未被映射的虚拟地址空间
      任何对它的引用都是非法的,一般包括几K字节,用于捕捉空指针和小整型值的指针引用内存的情况
    2. 文本段
    3. 数据段
      通常是进程中最大的段
    4. BSS段
      数据段和BSS段合称数据区
    5. 堆栈段
      • 主要有三个用途:
        1. 为函数内部声明的局部变量提供存储空间
        2. 进行函数调用时,保存相关的维护性信息(过程活动记录等)
        3. 作为暂时的存储区
      • 函数可以通过参数或全局指针访问它所调用的函数的局部变量,运行时系统维护一个用于提示堆栈当前顶部位置的指针(常位于寄存器中,称为sp)
  • C不支持函数嵌套的原因
    支持函数嵌套的语言一般采取上层引用的访问方式,需要活动记录包含一个指向它的外层函数的活动记录的指针(被称为静态链接),它允许内层过程访问外层过程的活动记录,因此也可以访问外层过程的局部数据;
    而C语言所用函数在词法层次中都是最顶层的、分别独立的
  • volatile类型修饰符
    • volatile本意为易变的
    • 用来告知编译器,程序每次使用该变量都要从内存中读取(出于优化目的,编译器有可能会直接在暂存的寄存器中读取)
    • 常用于多线程编程等情况
  • 线程控制
    • 头文件:<setjmp.h>
    • setjmp( jmp_buf j ):保存当前的过程活动记录到j,并返回0
    • longjmp( jmp_buf j, int i ):跳转到变量j所在的过程活动记录位置,并返回i
    • setjmp / longjmp{标签} / goto
      前者只能跳转回曾经到过的地方,后者可以任意设置标签的位置
      后者不能跳出函数,前者甚至可以跳转到其他文件的函数
    • 为了保证跳转过程中局部变量的可靠性,应使用volatile类型修饰符
    • 常用于错误修复(发现不可修复的错误即把控制转入主输入循环,重新开始)、从深层嵌套的函数中跳出等
    • 跟goto一样,这样控制线程会使程序难以理解和调试,应尽量避免使用
  • UNIX和MS-DOS中的堆栈段
    • UNIX的堆栈段是自动生长的
      当试图访问系统分配给堆栈的空间之外时,将会产生一个硬件中断,称为页错误
      一般,内核会通过向进程发送合适的信号(可能是段错误)来处理无效地址的引用
    • DOS的堆栈段是不可增长的,建立可执行文件时必须确定
  • 常用的C语言工具
    • 源代码检查:
      • cb:C程序美化器,使源文件有标准的布局和缩进格式
      • lint:C程序检查器
    • 可执行文件检查:
      • dis:(/usr/ccs/bin)目标代码反汇编工具
      • nm:(/usr/ccs/bin)打印目标文件的符号表
    • 调试:
      • debugger:交互式调试器
    • 性能优化:
      • time:(/usr/bin/time)显示程序所使用的实际使用和CPU时间
  • 内存媒介(速度、成本依次增加)
    磁带、硬盘、内存、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数据都要写入内存中去
  • 拷贝整个数组
    用库函数memcpy( 目的数组, 源数组, 元素个数 )进行快速拷贝
    比循环拷贝快的多
  • malloc请求申请的内存大小为方便起见通常会被圆整为2的乘方
  • 生存时间长的程序需要管理动态内存的分配和回收,堆经常会出现两种问题:
    • 内存损坏:释放或改写仍在使用的内存
    • 内存泄漏:未释放不再使用的内存
  • alloca分配的动态内存,在离开函数时会被自动释放
    可以避免内存损坏和内存泄漏,但是不适用于创建比其所在函数生命周期更长的结构
  • 体积大的进程更有可能被系统换出,换进换出的时间也更长
  • 总线错误(bus error)
    • 主要是未对齐的读写引起的
      对齐:数据项只能存储在地址是数据项大小的整倍数的内存位置上,通过迫使每个内存访问局限在一个Cache行或一个单独的页面内,可以极大的简化并加速如Cache控制器和MMU等硬件
      出现未对齐的内存访问请求时,被堵塞的组件就是地址总线
    • 也有可能是引用物理上不存在的内存引起
  • 段错误(segmentation fault)
    • MMU异常导致的错误,通常由解除引用一个未初始化或非法值的指针引起
      但是,如果未初始化的指针恰好具有未对齐的值,它会引起总线错误但不引起段错误
    • 导致段错误的常见直接原因:
      1. 解除引用一个包含非法值的指针
      2. 解除引用一个空指针
      3. 在未得到正确的权限时进行访问(如往只读的文本段存储值)
      4. 堆栈或堆空间用完
    • 导致段错误的常见编程错误:
      1. 坏指针值错误
        对未初始化的指针解除引用,或向库函数传递一个坏指针,或解除引用已经被释放的指针
        建议养成指针释放后立即将其设置为NULL的习惯
      2. 改写错误
        越过数组边界写入数据,或在动态分配的内存之外写入数据,或改写一些堆管理数据结构
      3. 指针释放引起的错误
        多次释放同一块内存,或释放一块未分配的内存,或释放一个无效指针
  • 类型提升
    在所有表达式中,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/*会分别被解释为 除法解除引用
  • else始终与 同一对括号内 最近未匹配 的if结合