guanjunjian

《C和指针》笔记

2017-01-09
guanjunjian

《C和指针》 笔记

第2章 基本概念

2.1 环境

  • ANSI C存在两种不同的环境
    • 翻译环境:源代码被转换为可执行的机器指令
    • 执行环境:用于实际执行代码
  • 两种环境不必位于同一台机器上,例如交叉编译器

2.1.1 翻译

  • 翻译分为以下几个步骤,图2.1描述了这个过程
    • 组成一个程序的每个(可能有多个)源文件通过编译过程分别转换为目标代码(object code)
    • 各个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。链接器会引入标准C库中的函数和程序员个人的程序库

  • 编译也分为以下几个步骤
    • 预处理器处理,在源代码上执行一些文本操作,例如#define#include指令的执行
    • 源代码解析,这个阶段产生绝大多数错误和警告信息,随后产生目标代码
    • 优化器对目标代码进行进一步处理,提升效率

一、文件名约定

  • C源代码以.c扩展名保存,头文件使用.h扩展名保存
  • 目标文件名:在UNIX中扩展名为.o,在MS-DOS中为.obj

二、编译和链接

  • 在UNIX中,C编译器被称为cc
  • cc有以下几种方法调用:
    • 编译并链接一个完全包含一个源文件的C程序:cc program.c,该命令会产生a.out可执行文件,中间会生成program.o目标文件,但在链接过程完成后会被删除
    • 编译并链接几个C源文件cc main.c sort.c lookup.c,当源文件超过一个时,目标文件便不会被删除
    • 编译一个C源文件,并把它和现存的目标文件链接在一起cc main.o lookip.o sort.c
    • 编译单个C源文件,并产生一个目标文件(本例中为program.o):cc -c program.c
    • 编译几个C源文件,为每个文件产生一个目标文件:cc -c main.c sort.c lookup.c
    • 链接几个目标文件:cc main.o sort.o lookup.o
  • 以上的cc命令还可以加上-o name使链接器把可执行程序保存在name文件中,而不是a.out
  • MS-DOC与UNIX不同:
    • 它的名字是bcc
    • 目标文件的名字是file.obj
    • 当单个源文件被编译并链接时,编译器并不删除目标文件
    • 在缺省情况下,可执行文件以命令行第一个源或目标文件名命名,可以使用-e name把可执行程序命名为name.exe

2.1.2 执行

  • 执行过程分为:
    • 程序载入内存,由操作系统完成,那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值
    • 执行开始,执行小型启动程序,完成系列日常任务,然后调用main函数
    • 开始执行程序代码
    • 程序终止,“正常”环境的终止就是main函数的返回

2.2 词法规则

2.2.1

  • 三字母词:就是几个字符的序列,合并起来表示另一个字符,如下图所示:

  • 转义序列(escape sequence)或字符转义(character escape),由反斜杠\加上一或多个其他字符组成
//转义字符
    \? 在书写连续多个问号时使用,防止它们被解释为三字母词
    \" 用于表示一个字符串常量内部的双引号
    \' 用于表示字符常量`
    \\ 用于表示一个反斜杠,防止它被解释为一个转义字符
    \a 警告字符,它将奏响终端铃声或产生其他一些可听见或可看见的信息
    \b 退格键
    \f 进纸字符,换页
    \n 换行符
    \r 回车符
    \t 水平制表符
    \v 垂直制表符
    \ddd ddd表示1~3个八进制数字,表示的字符就是给定的八进制值所代表的字符
    \xddd 与上类似,表示十六进制

2.2.2 注释

  • 注释不能嵌套于另一个注释中

2.2.3 自由形式的源代码

  • 预处理是以行定位的

2.3 程序风格

  • 在函数定义中,返回类型出现于独立的一行中,而函数名则在下一行的起始处

2.4 总结

  • 一个函数只能完整地出现在另一个源文件中
  • 程序必须载入内存中才能执行,在宿主式环境中,这个任务由操作系统完成;在自由式环境中,程序常常永久存储于ROM中
  • 经过初始化的静态变量在程序执行之前能获得它们的值
  • 注释将被预处理器去除
  • 标识符由字母、数字和下划线组成,但不能以数字开头

2.5 警告的总结

  • 字符串常量中的字符被错误地解释为三字母词

第3章 数据

  • 作用域、链接属性 —> 可视性(可以在什么地方使用)
  • 存储类型 —> 生命期(值将保持多久)

3.1 基本数据类型

  • 4种基本数据类型:整型、浮点型、指针和聚合类型(数组、结构)

3.1.1 整型家族

  • 1.整型家族包括字符、短整型、整型和长整型,又分为有符号和无符号
    • a.字符:char、signed char、unsigned char(char和unsigend char长度不一样的)
    • b.短整型:short int(至少16位)、unsigned short int
    • c.整型:int、unsigned int
    • d.长整型:long int(至少32位)、unsigend long int
  • 2.长度比较:长整型 >= 整型 >= 短整型
  • 3.头文件limits.h说明了各种不同的整型类型的特点:变量范围的限制
  • 4.char类型变量在本质上是小整型值
  • 5.缺省的char要么是sigend char,要么是unsigend char,这取决于编译器
  • 6.char变量的值位于sigend char和unsigend char的交集中,这个程序才是可移植的
  • 7.移植问题:最佳方案是将char限制在sigend char和unsigend char交集内,并且只有char显示声明为sigend或unsigend时才对它执行算术运算

一、整型字面值

  • 字面值是字面值常量的缩写,区分与常量
  • 整型字面值属于哪种类型,取决于字面值是如何书写的,可以通过添加一个后缀来改变缺省的规则
  • 在整数字面值后面添加L或l使这个整数解释为long整型值;U或u解释为unsigned整型值;可两个一起用

  • 十进制整型字面值可能是int、long或unsigend long,缺省是它最短类型但能完整容纳这个值
  • 八进制和十六进制整型字面值可能是int、unsigend int、long或unsigend long,,缺省是它最短类型但能完整容纳这个值
  • 字符常量的类型总是int,不能添加unsigned或long后缀
    • 字符常量就是用一个单引号包围起来的单个字符(或字符转义序列或三个字母)例子在P31
    • 多字节字符常量的前面有一个L,那么它就是宽字符常量

二、枚举类型

  • 枚举类型就是指它的值为符号常量而不是字面值的类型
  • 声明枚举类型:enum Jar_Type { CUP, PINT, QUART }
  • 声明枚举类型的变量:enum Jar_Type mikl_jug;
  • 匿名枚举类型声明
    enum { CUP, PINT, QUART }
        mikl_jug;
  • 枚举类型实际上是整型方式存储,CUP是0,PINK是1
  • 同时也可以给符号名赋值,如果某个符号名赋值了,但下一个没赋值,那么这个没赋值的符号名就比上一个赋值了的符号名的值大1

3.1.2 浮点类型

  • 非整数或数远远超出了计算机整数所能表达的范围,可以用浮点数的形式存储
  • 浮点数通常以一个小数以及一个以某个假设数为基数的指数组成
  • 包括float、double、long double,表示单精度、双精度、扩张精度
  • 长度比较:float >= double >= long double
  • 所有浮点至少能容纳从10E-3710E37之间的任何值
  • float.h定义了浮点数家族的最大值、最小值
  • 浮点字面值总是写成十进制的形式,必须有小数点或一个指数,或两个都有
  • 浮点数字面值在缺省情况下都是double,除非跟了L或l(long double)和F或f(float)

3.1.3 指针

  • 每个内存位置都由地址唯一确定并引用
  • 指针只是地址的另一个名字
  • 指针变量就是一个其值为另外一个(一些)内存地址的变量

一、指针常量

  • 指针常量与非指针常量在本质上是不同的
  • 通过操作符获得一个变量的地址而不是直接把它的地址写成字面值常量的形式
  • 指针常量表达式为字面值的形式几乎没有用处,所以C内部并没有特定第定义这个概念
  • 例外:NULL指针,它可以用零值来表示

二、字符串常量

  • C不存在字符串类型,但C提供了字符串常量
  • 字符串的概念:以NUL字节结尾的0个或多个字符
  • 字符串通常存储在字符数组中
  • 字符串内部不能有NUL字节
  • 字符串常量的书写方式是用一对双引号包围一串字符"Hello"
  • 字符串常量(不像字符常量)可以是空的,但是依然有NUL字节终止
  • K&R C的字符串常量
    • 具有相同值的不同字符串常量在内存中是分开存储的
    • 编译器允许程序修改字符串常量
  • ANSI C的字符串常量
    • 对一个字符串常量进行修改,其效果未定义
    • 它允许编译器把一个字符串常量存储于一个地方
    • 修改字符串常量很危险
    • 许多ANSI编译器不允许修改字符串常量
    • 如果需要修改字符串,请把它存于数组中
  • 字符串常量会生成一个“指向字符的常量指针”
  • 字符串常量出现在表达式中,表达式所使用的值是字符存储的地址
  • 把字符串常量赋给一个“指向字符的指针”
  • 不能把字符串常量赋给一个字符数组,因为字符串常量的直接值是一个指针,不是这些值本身

3.2 基本声明

  • 在声明整型变量时,如果声明中已经至少有了一个其他的说明符,关键字int可以省略unsigend short int a;unsigned short a;是相等的
  • signed一般只用于char,因为其他类型在缺省情况下都是有符号数
  • 相等的整型声明:
    • 表格:
    signed unsigend
    shortsigned shortshort intsigned short int unsigned shortunsigned short int
    intsigned intsigend unsigned intunsigned
    longsigned longlong intsigned long int unsigend longunsigned long int

3.2.1 初始化

3.2.2 声明简单数组

  • 编译器并不检查程序对数组下标的引用是否在数组的合法范围之内

3.2.3 声明指针

  • 声明指针应该int *a;而不应该int* a;
  • 因为在int* b,c,d;会让人误以为b、c、d都是指针,其实这里只有b是指针,应该为int *b,*c,*d;

3.2.4 隐式声明

  • 函数如果不显示地声明并返回值的类型,默认返回整型
  • 旧风格声明函数的形式参数,如果忽略了参数的类型,默认为整型
  • 编译器能得到足够信息,推断出一个语句是一个声明时,如果缺少类型名,会假设为整型
  • 例子 P37

3.3 typedef

  • typedef机制允许你为各种数据类型定义新名字
    typedef char *ptr_to_char;   //ptr_to_char作为指向字符的指针类型的新名字
    ptr_to_char a;   //a是一个指向字符的指针
  • #define不法正确处理指针类型
    # define d_ptr_to_char  char *
    d_ptr_to_char a,b;  //这里只有a是字符指针,而b只是字符
  • 复杂的类型名,如函数指针和指向数组的指针,使用typedef更合适

3.4 常量

  • const关键字声明常量,int const a;const int a;都可以
  • 常量如何拥有一个值
    • 声明时对它进行初始化,int const a = 15;
    • 形参在函数调用时会得到实参的值
  • 关于指针(const往前结合)
    int *pi;  //普通的指向整型的指针
    int const *pci;  //指向整型常量的指针
    int * const cpi;  //指向整型的常量指针
    int const * const cpci;  //指向整型常量的常量指针
  • #define也可以创建名字常量#define MAX 50int const max = 50一样

3.5 作用域

  • 标识符的作用域就是程序中该标识符可以被使用的区域
  • 4种作用域:文件作用域、函数作用域、代码块作用域、原型作用域
  • 标识符声明的位置决定它的作用域

3.5.1 代码块作用域

  • 花括号之间的所有语句称为一个代码块
  • 任何在代码块开始位置声明的标识符都具有代码块作用域
  • 代码块嵌套时,标识符同名,内层标识符隐藏外层标识符
  • k&R C函数形参作用域开始于形参的声明处,位于函数体外,局部变量可以隐藏形参
  • ANSI C形参作用域为函数最外层的那个作用域(整个函数体),局部变量不可能隐藏形参

3.5.2 文件作用域

  • 代码块之外声明的标识符具有文件作用域
  • 文件作用域:表示从标识符声明处起到源文件结尾都是可以访问的
  • 文件中定义的函数名也具有文件作用域
  • #include包含到其他文件中的声明就好像直接写在那些文件中一样,它们的作用域不局限于头文件的文件文件尾

3.5.3 原型作用域

  • 原型作用域只适用于在函数原型中声明的参数名
  • 原型中的参数名不必与函数定义中的参数名匹配

3.5.4 函数作用域

  • 函数作用域只适合于语句标签
  • 语句标签用于goto语句
  • 函数作用域可以简化为一条规则:一个函数中的所有语句标签必须唯一

3.6 链接属性

  • 标识符的链接属性决定如何处理在不同文件中出现的标识符
  • 标识符的作用域和它的链接属性有关
  • 3种:external、internal、none
  • 没有链接属性的标识符(none):总是当作独立的个体
  • internal:同一个源文件内的所有声明都是指向同一个实体
  • external:无论声明多少次,位于几个源文件都是表示同一个实体
  • 函数定义中的函数调用a,a的链接属性是external,它实际链接到其他文件所定义的函数,或某个函数库


  • 关键字externstatic用于声明中修改标识符的链接属性
  • 具有external链接属性的标识符,加上static,变为internal
  • static只对缺省属性为external的声明才会有改变链接属性的效果
  • extern为一个标识符指定external链接属性
  • extern用于标识符的第1次声明时,它指定标识符具有external链接属性;用于标识符的第2次或以后的声明时,不会改变第一次声明所指定的链接属性
    static int i;  //声明1
    int func()
    {
        extern int i; //不修改由声明1所指定的变量i的链接属性
    }

3.7 存储类型

  • 变量的存储类型是指存储变量值的内存类型:普通内存、运行时堆栈、硬件寄存器
  • 存储类型决定变量何时创建、何时销毁和值保持多久
  • 变量的缺省存储类型取决于它的声明位置
    • 代码块之外缺省:静态内存,称为静态(static)变量
    • 代码块内部缺省:堆栈中,称为自动(auto)变量
  • 在代码块内部声明的变量,加上static,自动变为静态(修改变量的存储类型不代表修改变量的作用域)
  • 形式参数不能声明为静态
  • register可以用于自动变量的声明,提示应该存储于硬件寄存器中,称为寄存器变量,但编译器不一定理睬
  • 可以把函数的形式参数声明为寄存器变量
  • 寄存器变量的创建和销毁时间和自动变量相同,但需要做一些额外工作:恢复先前存储的值

初始化

  • 静态变量的初始化可以把初始化的值放在程序执行变量将会使用的位置,不显示地指定其初始值,静态变量将初始化为0
  • 动态变量没有缺省值,如果不显示初始化,那么它们的值总是垃圾

3.8 static关键字

  • static的作用
    • 对于函数定义或代码外之外的变量声明:链接属性external--->internal,存储类型和作用域不受影响
    • 对于代码块内部变量声明:存储类型自动变量--->静态变量,链接属性和作用域不受影响
  • extern的作用
    • 对于代码块内部变量:链接属性none--->external,存储类型自动变量--->静态变量,说明它所引用的是个全局变量而非局部变量(变量声明可能在别的源文件中)

3.9 作用域、存储类型示例

  • 对于函数,存储类型并不是问题,因为代码总是存储在静态内存中

3.10 总结

  • 如果一个变量声明于代码块内部,在它前面添加一个extern将使它引用的是全局变量而非局部变量(有可能是别的源文件中的)
  • 具有external链接属性的实体总是具有静态存储类型
  • 作用域、链接属性和存储类型总结
    • 表:
    变量类型 声明的位置 是否存储于堆栈 作用域 如果声明为static 如果声明为extern
    全局 所有代码块之外 从声明处到文件尾 不允许从其他源文件访问,变为internal
    局部 代码块起始处 整个代码块 变量不存储于堆栈中,它的值在程序整个执行期一直保持 引用的是全局变量而非局部变量
    形式参数 函数头部 整个函数 不允许

3.12 编程提示的总结

  • 除了实体的具体定义位置外,在它的其他声明位置都要使用extern关键字

第4章 语句

  • 本章没有什么需要记录的内容

第5章 操作符和表达式

5.1 操作符

5.1.1 算数操作符

  • 1.+ - * / %
  • 2.%只能用于整数类型

5.1.2 位移操作符

  • 1.<<左移操作,移出界的丢弃
  • 2.>>右移,左边移入新位时有两种方案
    • a. 逻辑移位:左边移入的用0填充
    • b. 算数移位:左边移入的由原先的符号位决定
  • 3.位移操作符的两个操作数都必须是整形类型
  • 4.无符号值都是逻辑位移,有符号值由编译器决定
  • 5.a << -5这个位移的值是不可预测的

5.1.3 位操作符

  • 1.AND、OR、XOR; &、|、^

5.1.4 赋值

  • 1.赋值是表达式的一种,而不是某种类型的语句(没有赋值语句)
  • 2.赋值是表达式,所以它就具有一个值,赋值表达式的值就是左操作数的新值,可以作为其他赋值操作符的右操作数,如a = x = y + 3,即a = ( x = y + 3 )
  • 3.a = x = y + 3认为a和x被赋予相同的值的说法是错误的,因为可能变量类型不同,比如x是字符型变量,那么y+3的值就会被截去一段,所以以下代码是错误的(具体参照P70)
   char ch;
   ...
   while( ( ch = getchar() ) != EOF )...
  • 4.复合赋值符:+=、<<=、&=等等,a += expression等于a += a + ( expression )

5.1.5 单目操作符

!、++、-、&、sizeof、~、--、+、*、(类型)

  • 1.~:按位取反
  • 2.-:负值
  • 3.+:正值,与-相对
  • 4.&:取地址
  • 5.*:间接访问操作符,与指针一起用
  • 6.sizeof:操作数的类型长度,字节为单位;sizeof (int)sizeof x;当操作数为数组名时,返回数组的长度,以字节为单位;判断表达式的长度不需要对表达式求值,所以sizeof( a = b + 1 )并没有向a赋值
  • 7.(类型):强制类型转换
  • 8.++和--:操作数必须是个“左值”;前缀形式:操作数的值被增加,表达式是操作数增加后的值;后缀形式:操作数的值被增加,表达式是增加前的值;增值操作符都是复制一份变量值的拷贝,用于表达式的值正式这份拷贝,前缀后缀只是复制的时间不一样,因此++a = 10是错误的,因为不能向一个拷贝值进行赋值(P73)

5.1.6 关系操作符

> >= < <= != ==

  • 这些操作符产生的结果都是整型值1或0,不是布尔值

5.1.7 逻辑操作符

&& ||

  • 短路求值

5.1.8 条件操作符

expression1 ? expression2 : expression3

5.1.9 逗号操作符

,

  • 逗号操作符将多个表达式分隔开来,这些表达式自左向右逐个进行求值
  • if( b + 1, c / 2, d > 0)这里看的是d > 0

5.1.10 下标引用、函数调用和结构函数

  • C的下标值总是从0开始,并且不会对下标值进行有效性验证
  • 除了优先级不同外,下标引用操作和间接访问表达式是等价的
    • array[ 下标 ]*( array + ( 下标 ) )
  • .->操作符用于访问一个结构的成员,当你拥有一个指向结构体的指针而不是结构体本身时,使用->访问它的成员

5.2 布尔值

  • C不具备显示的布尔类型,使用整数代替
  • 零是假,任何非零值为真
  • 注意这类写法,flag为1以外的其他非零值,这个if语句也是不执行的:
    #define FALSE 0
    #define TRUE 1
    if( flag == TRUE)

5.3 左值和右值

  • 左值就是那些能够出现在复制符号左边的东西,右值同理
  • “表达式不能作为左值”这句话是错的:a[ b + 10 ] = 0中的左值就是表达式,这些操作符包括间接访问操作符和下标引用

5.4 表达式求值

5.4.1 隐式类型转换

  • 整型升级:字符型加法运算时,会提升为普通整型

5.4.2 算术转换

  • 寻常算术转化(P80)

5.4.3 操作符的属性

  • 复杂表达式的求值顺序3个决定因素:操作符的优先顺序、操作符的结合性(L-RR-L)、操作符是否控制执行的顺序(&&||
  • 操作符优先级表 p81

5.4.4 优先级和求值的顺序

  • c + --c根据编译器的不同会产生不同的结果

5.5 总结

  • &&||?:对求值过程施加控制
  • 逗号操作符,整个表达式的值是最右那个子表达式的值
  • 各个不同类型之间的值不能直接进行运算,除非其中一个的操作数转换为另一个操作数的类型(寻常算术转换)
  • 表达式的结果如果依赖于求值的顺序,那么它在本质上就是不可移植的,应该避免使用(P86)
  • 不要混用整型和布尔型值

第6章 指针

6.1 内存和地址

  • 字节:8个位
  • 字:许多机器以字为单位存储整数,每个字一般由2个或4个字节组成
  • 尽管一个字包含了4个字节,它仍然有一个地址,或是最左边那个字节或是最右边那个字节
  • 边界对齐
  • 内存中的每个位置都由一个独一无二的地址标识;内存中的每个位置都包含一个值

6.2 值和类型

  • 不能简单地通过检查一个值的位来判断它的类型,为了判断值的类型,必须观察程序中这个值的使用方式

6.3 指针变量的内容

  • 一个变量的值就是分配给这个变量的内存位置所存储的数值,要区分与指针的内容

6.4 间接访问操作符

  • 通过一个指针访问它所指向的地址的过程称为间接访问或解引指针,使用*

6.5 未初始化和非法的指针

    int *a;
    ...
    *a =12;
  • 是错误的,因为没有对a进行初始化

6.6 NULL指针

  • 要使一个指针变为NULL,你可以给它赋一个零值
  • 为了测试一个指针变量是否为NULL,你可以将它与零值进行比较
  • 对一个NULL指针进行解引用操作是非法的

6.7 指针、间接访问和左值

  • 间接访问操作符所需要的操作数是一个右值,但这个操作符所产生的结果是个左值

6.8 指针、间接访问和变量

  • *&a = 25a = 25从结果上来说是一样的

6.9 指针常量

  • *100 = 25是非法的
  • *(int *)100 = 25是合法的
  • 指针常量通常用来根据已经设备的设备地址来访问设备

6.10 指针的指针

  • 声明:int **c
  • 声明为register的指针变量,不可以再使用&取址(P99)

6.11 指针表达式

  • cp作为字符指针,++cp是不能成为左值的,这个运行结果的返回值是原cp指向地址的下1个地址
  • cp作为字符指针,cp--是不能成为左值的,这个运行结果的返回值是原cp指向的地址
  • 对于*++cp*cp++++*cp参考P103
  • 由于后缀++的优先级高于*,所以*cp++分为三步:
      1. ++操作产生cp的一份拷贝
      1. 然后++操作符增加cp的值
      1. 最后,在cp的拷贝上执行间接访问操作
  • ++*++cp++*cp++参考P104

6.12 实例


6.13 指针运算

  • 指针加上一个整数的结果是另一个指针,如果p是个指向float的指针,那么p+1就指向下一个float

6.13.1 算术运算

  • C的指针算术运算只限于两种形式:1.指针 +/- 整数;2.指针 - 指针
  • 指针 +/- 整数
    • 标准定义这种形式只能用于指向数组中某个元素的指针
    • 这类表达式的结果类型也是指针
  • 指针 - 指针
    • 只有当两个指针都指向同一个数组中的元素时,才允许一个指针减去另一个指针
    • 结果类型是ptrdiff_t,一种有符号整数类型
    • 减法运算的值时两个指针在内存中的距离,以数组元素的长度为单位,不是以字节为单位
    • ptrdiff_t = 实际内存差 / 数组类型长度
    • 存在p1 - p2 = 负数的情况,只要两个指针都指向同一个数组的元素

6.13.2 关系运算

  • < <= > >=
  • 前提是指向同一个数组中的元素
  • 比较表达式将告诉你哪个指针指向数组中更前或更后的元素

6.14 总结

  • 无法通过值的位模式来判断它的类型,类型是通过值的使用方式隐形确定的
  • 声明一个指针变量并不会自动分配任何内存,在指针执行间接访问前,指针必须进行初始化,或使它指向现有的内存,或给它分配动态内存
  • NULL指针执行间接访问操作的后果因编译器而异,常见后果为:返回内存位置零的值或终止程序
  • 指针常量:通过把整型值强行转换为指针类型来创建它
  • 指针加法运算,如果指针指向数组最后一个元素后面的那个内存位置仍是合法的

6.15 警告

  • 错误地对一个未初始化的指针变量进行解引用
  • 错误地对一个NULL指针进行解引用
  • 向函数错误地传递NULL指针
  • 未检测到指针表达式的错误,从而导致不可预料的结果
  • 对一个指针进行减法运算,使它非法地指向了数组第1个元素的前面的内存位置

第7章 函数

7.1 函数定义

  • 函数的定义就是函数体的实现
  • 函数声明出现在函数被调用的地方,函数声明向编译器提供函数的相关信息,用于确保函数被正确地调用
  • 存根(stub):应该就是一个空函数。编写这类存根,或者说为尚未编写的代码“占好位置”,可以保持程序在结构上的完整性,以便于你编译和测试程序的其他部分
    function_name()
    {
    }
  • 过程类型的函数:没有返回值
  • 真函数:从表达式内部调用的,必须返回一个值,用于表达式的求值

7.2 函数声明

7.2.1 原型

  • 第2种向编译器提供函数信息的方法是使用函数原型(第1种应该是函数定义)
  • 使用原型最方便的方法是把原型置于一个单独的文件,使用#include指令包含该文件
  • int func( int i );;区分了函数原型和函数定义的起始部分
  • 原型中的参数名字并不是必需的
  • 函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件
  • 函数原型必须与函数定义匹配
  • int *func()不能表示一个没有参数的函数的原型,因为旧式风格的有参函数是可以这样声明的,一个没有参数的函数原型应该写成int *func(void)

7.2.2 函数的缺省认定

  • 当程序调用一个无法见到原型的函数时,编译器认为该函数返回一个整型值
  • 所有的函数都应该具有原型,尤其是那些返回值不是整型的函数
  • 如果编译器认定函数返回一个整型值,它将产生整型数指令操作这个值(如果返回的不是整型值,那将会出错,例子参考P121)

7.3 函数的参数

  • 1.所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝
  • 2.数组并不会得到一份拷贝,而是得到数组首地址的一份拷贝,这个行为被称为“传值调用”,因为数组名的值实际上是一个指针,传递给函数的就是这个指针的一份拷贝
  • 3.记住两个规则:
    • a.传递给函数的标量参数是传值调用的
    • b.传递给函数的数组参数在行为上就像它们是通过传址调用的那样
  • 4.在函数参数声明中,声明数组参数时不指定它的长度是合法的,因为函数并不为数组元素分配内存

7.4 ADT和黑盒

  • C可以用于设计和实现抽象数据类型(ADT,abstract data type),也被称为黑盒设计
  • 抽象数据类型的基本想法:模块具有功能说明和接口说明
  • 限制对模块的访问是通过static关键字的合理使用实现的,它可以限制那些并非接口的函数和数据的访问

7.5 递归

  • C通过运行时堆栈支持递归函数的实现,递归函数就是直接或间接调用自身的函数。
#include <stdio.h>
void
binary_to_ascii( unsigned int value)
{
	unsigned int quotient;
	
	quotient = value / 10;
	if( quotient != 0 )
		binary_to_ascii( quotient );
	putchar( value % 10 + '0' );
}

7.5.1 追踪递归函数

  • 追踪一个递归函数执行过程的关键是理解函数中所声明的变量是如何存储的,因此可以通过画堆栈图来理解(参考P128的例子)
  • 假设调用函数binary_to_ascii( 4267 );
  • 当函数开始执行时,堆栈的内容如下图所示:

    步骤1:
    

  • 步骤2:

  • 步骤3:

  • 步骤4:

  • 步骤5:

  • 步骤6:

  • 步骤7:

  • 步骤8:

  • 步骤9:

  • 步骤10:

7.5.2 递归与迭代

  • 1.递归函数调用将涉及一些运行时开销
    • a.参数必须压到堆栈中
    • b.为局部变量分配内存空间
    • c.寄存器的值必须保存
  • 2.因此递归函数的开销是十分大的
  • 3.尾部递归:当一个函数在递归调用返回之后不再执行任何任务,这样的递归函数叫尾部递归
  • 4.尾部递归可以很方便地转换成一个简单循环,完成相同任务,但开销更小
  • 5.迭代实现往往比递归实现效率更高,但代码可读性稍差
  • 6.如果一个问题相当复杂,难以用迭代形式实现时,此时递归实现的简便性可以补偿它所带来的运行时开销

7.6 可变参数列表

  • 1.宏是由预处理器实现的
  • 2.可变参数列表是通过宏来实现的,这些宏定义于stdarg.h头文件,它是标准库的一部分
  • 3.分别有一个类型va_list和三个宏va_startva_argva_end
  • 4.参数列表中的省略号提示此处可能传递数量和类型未确定的参数,编写函数原型时,也要使用同样的记法
  • 5.可变参数必须从头到尾按顺序逐个访问,半途终止是可以的,但不能一开始就访问参数列表中的中间的参数
  • 6.由于可变参数部分没有原型,可变参数传递给函数的值都将执行缺省参数类型的提升(//TODO 不明白什么意思)
  • 7.这些宏存在两个基本限制,是由“一个值的类型无法简单地通过检查它的位模式来判断”导致的
    • a.这些宏无法判断实际存在的参数的数量
    • b.这些宏无法判断每个参数的类型
  • 8.要回答7.中的两个问题,就必须使用命名参数
#include <stdarg.h>

float
average ( int n_values, ...)
{
    //用于访问参数列表的未确定部分
    va_list var_agr;
    int count;
    float sum = 0;
    
    //使用va_start来初始化,第1个参数是va_list变量的名字,第2个参数是省略号前最后一个有名字的参数,初始化过程把var_arg变量指向可变参数部分的第1个参数
    va_start( var_agr, n_values );
    
    for( count =0; count < n_values; count +=1 ){
        //访问参数,第1个变量va_list变量,第2个变量,参数列表中下一个参数的类型。va_arg返回这个参数的值,并使var_arg指向下一个可变参数
        sum += va_arg( var_arg, int );
    }
    //访问完毕最后一个可变参数之后,需要调用va_end
    va_end( var_arg );
    
    return sum / n_values;
}

7.7 总结

  • 1.参数列表有两种可以接受的形式:K&R C风格和新风格
  • 2.函数声明也有两种可以接受的形式:
    • a.K&C C每个没有参数列表,只声明了返回值的类型
    • b.新风格又称为函数原型,包含了参数列表的声明
  • 3.对于那些没有原型的函数,传递给函数的实参将进行缺省参数提升
    • a.char和short转换为int
    • b.float转换为double

第8章 数组

8.1 一维数组

8.1.1 数组名

  • 数组名的值是一个指针常量,也就是数组第1个元素的地址,int数组的数组名就是“指向int的常量指针”
  • 数组和指针是不相同的,不同的特征:
    • 1.数组具有确定数量的元素,而指针只是一个变量值
    • 2.数组名只有在表达式中使用,编译器才会产生一个指针常量
  • 两种场合下,数组名并不用指针常量来表示
    • 1.sizeof:返回整个数组的长度
    • 2.&:指向数组的指针,而不是指向某个指针常量的指针,也就是说若array为数组名,那么array == &array,但也存在区别,看数组名a和&a的区别
  • 考虑下面例子:
    int a[10];
    int b[10];
    int *c;
    ...
    c = &a[0];  //和 c = a 是一样的;b = a 是非法的; a = c是非法的,a是常量不能修改

8.1.2 下标引用

  • 除了优先级外,下标引用和间接访问完全相同,如下是相同的:
    array[ subscript ]
    * ( array + ( subscript ) )
    int array[10];
    int *ap = array + 2;
    
    ap[0]; //对等*(ap+(0)),即array[2]
    *ap+6; //array[2]+6
    ap[-1]; //array[1]
  • C的下标检查所涉及的开销比你刚开始想象的要多
  • 2[array]是合法的,等于( 2 + ( array ) ),就是*( array + 2 ),也就是array[2]

8.1.3 指针与下标

  • 下标绝对不会比指针更有效率,但指针有时会比下标更有效率
  • 例子 P145

8.1.4 指针的效率

  • 指针有时比下标更有效率,前提是它们被正确地使用
  • 不要为了效率上的细微差别而牺牲可读性
  • 可以对指针使用寄存器变量,但是指针必须被声明为局部变量

结论

  • 当你根据某个固定数目的增量在一个数组中移动时,使用指针变量比使用下标产生效率更高的代码。当这个增量是1并且机器具有地址自动增量模型时,这点表现更为突出
  • 声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高
  • 如果你可以通过测试一些已经初始化并经过调整的内容来判断是否应该终止循环,那么你就不需要使用一个单独的计数器
  • 那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高

8.1.5 数组和指针

  • 指针和数组并不是相等的
  • 声明一个数组时,为数组保留内存空间,再创建数组名,它的值是一个常量,指向这段空间的起始位置
  • 声明一个指针变量时,只为指针本身保留内存空间,并不为它分配内存空开,它如果是自动变量,它甚至不会被初始化

8.1.6 作为函数参数的数组名

  • 传递给函数的是一份该指针的拷贝
  • 所有的参数都是通过传值方式传递的
  • 无论函数对参数(指针)如何进行修改,都不会修改调整程序的指针实参本身(但可能修改它所指向的内容)

8.1.7 声明数组参数

  • int func( char *string )int func( char string[] )在当前的上下文环境中是相等的,但使用指针声明更为准确
  • 对函数中参数指针使用sizeof string的值是指向字符的指针的长度,而不是数组的长度
  • 数组参数可以与任何长度的数组匹配,这种实现方式使函数无法知道数组的长度

8.1.8 初始化

  • int vector[5] = { 1, 2, 3, 4, 5 };

静态和自动初始化

  • 初始化方式:取决于它们的存储类型
    • 静态内存中的数组只初始化1次,未初始化时,自动设为零
    • 自动变量,缺省情况下未初始化
  • 对于那些非常庞大的数组,它的初始化时间可能非常可观
  • 需要权衡利弊,数组的初始化局部于一个函数(或代码块)时,是不是值得,如果不值得,就把数组声明为static

8.1.9 不完整的初始化

  • 初始化值的数组和数组元素的数目并不匹配
  • 只允许省略最后几个初始值(局部变量也可以,如果最后没有初始化,那么就初始化为0)

8.1.10 自动计算数组长度

  • 如果声明中并未给出数组的长度,编译器就把数组的长度设置为刚好能容纳所有的初始值的长度

8.1.11 字符数组的初始化

  • char m[] = { `H`, `I` };
  • char m[] = "HI";,尽管这个看上去是一个字符串常量,实际上不是
    char m1[] = "HELLO"; //初始化一个字符数组的元素
    char *m2 = "HELLO"; //真正的字符串常量

8.2 多维数组

8.2.1 存储顺序

  • 多维数组的元素存储顺序按照最右边的下标率先变化的原则,称为行主序
  • int matrix[6][10]是6行10列还是10行6列,都对,只要每次都坚持使用一种方法,这两种解释都是可行的,但并不会改变数组的存储顺序

8.2.2 数组名

8.2.3 下标

  • matrix[x][y]等于*( *( matrix + x) + y )
  • &matrix[0][0]等于matrix
  • &matrix[x]等于matrix+x
  • matrix[x]等于*(matrix+x)
  • &matrix[x][y]等于*(matrix +x) + y
  • matrix[4,3]等于matrix[3],因为逗号表达式是最后一个子表达式的值

8.2.4 指向数组的指针

    int v[10], *vp = v;  //合法
    int m[3][10], *mp = m; //非法,mp是指向整型的指针,而m是一个指向整型数组的指针
    int (*p1)[10] = m;  //合法
    int (*p2)[] = m;  //应该避免这种类型的声明,不能执行指针运算

8.2.5 作为函数参数的多维数组

    int matrix[3][10];
    ...
    func2( matrix );
    
    //那么func2的声明可以是:
    void func2( int (*mat)[10] );
    void func2( int mat[][10] );
    //但以下方式是错误的
    void func2( int **mat ); //指向整型指针的指针和指向整型数组的指针并不是一回事

8.2.6 初始化

  • 初始化多维数组时,数组的存储顺序非常重要
  • 初始化例子:int m[2][3] = { 1, 2, 3, 4, 5, 6};
  • 初始化也可以是:
    int m[2][5] = {
        { 1, 2, 3, 4, 5},
        { 6, 7, 8, 9, 10}
    };
  • 如果使用了这些花括号,每个子初始化列表都可以省略尾部的几个初始值,每一维的初始列表都各自都是一个初始化列表

8.2.7 数组长度自动计算

  • 数组长度只有第1维才能根据初始化列表缺省地提供,其余的几个维必须显示写出
  • 如果别的维也想缺省,编译器是允许这样做的,但是每个列表中的子初始值列表至少有一个要以完整的形式出现(不得省略末尾的初始值)
  • 如果我们要求除第1维之外的其他维的大小都显示提供,所有的初始值列表都无需完整

8.3 指针数组

  • 声明指针数组int *api[10],api是数组名,数组元素是整型指针;区别与二维数组int (*api)[10],api是指针名,指向的是一个长度为10的整型数组
  • 区别:
    • 字符串以矩阵存储还是以指针常量方式存储(需要两种方式占用内存空间方面的区别,图在P164)
    //指针常量
    char const *keyword1[] = {
        "do",
        "for"
    }
    //矩阵,每行必须与最长字符串的长度一样,不需要指针
    char const keyword2[][5] = {
        "do",
        "for"
    }
  • 哪种更好?
    • 字符串长度差不多:矩阵,因为无需使用指针
    • 字符串长度千差万别:指针数组

8.4 总结

  • sizeof返回整个数组占用的字节而不是指针的字节;&返回一个指向数组的指针,而不是一个指向数组第1个元素的指针的指针(形参是指针,但是传入的数组的情况除外)
  • 数组形参既可以声明为数组,也可以声明为指针,这两种声明形参只有当它们作为函数的形参时才相等
  • 如果初始化列表包含的值的个数少于数组元素的个数,数组最后几个元素就用缺省值进行初始化

8.5 警告的总结

  • 当访问多维数组时,误用逗号分隔下标,a[3,5]其实是a[5]
  • 在一个指向未指定长度的数组的指针上执行指针计算int (*p)[] = matrix

8.6 编程提示的总结

  • 源代码的可读性几乎总是比程序的运行时效更为重要
  • 只要有可能,函数的指针形参都应该声明为const
  • 对维数组初始化使用多层花括号能提高可读性

第9章 字符串、字符和字节

  • C语言没有显示的字符串数据类型
  • 字符串以字符串常量的形式出现或者存储于字符数组中,字符串常量适合用于不会对它们进行修改的字符串

9.1 字符串基础

  • 字符串就是一串零个或多个字符,并且以一位模式全为0的NUL字节结尾,但它本身不是字符串的一部分,所以字符串的长度并不包括NUL字节
  • string.h包含了字符串函数所需要的原型和声明,但是并非必须

9.2 字符串长度

  • 字符串的长度就是它所包含的字符个数,不包括NUL
  • size_t strlen( char const *string )
  • 返回类型为size_t,定义在头文件stddef.h,是一个无符号整数类型
  • if( strlen( x ) - strlen( y ) >= 0)这条语句永远是true,因为strlen返回的是无符号数,而无符号数是绝对不可能是负的
  • if( strlen( x ) >= 10 )if( strlen( x ) -10 >= 0 )不相等,原因与上同,可以将返回值强制转换为int就可以解决这个问题
  • 标准库函数有时是用汇编语言实现的,目的就是充分利用某些机器所提供的字符串操作指令,从而追求最大的速度

9.3 不受限制的字符串函数

9.3.1 复制字符串

  • char *strcpy( char *dst, char const *src);
  • 由于dst参数是需要修改的,所以不能使用字符串常量
  • 必须保证目标字符数组的空间足以容纳需要复制的字符串。如果超长,多余的字符仍然被复制,会覆盖原先存储于数组后面的内存空间的值

9.3.2 连接字符串

  • char *strcat ( char *dst, char const *src);
  • 如果src和dst的位置发生重叠,其结果是未定义的

9.3.3 函数的返回值

  • strcpy和strcat返回第1个参数的一份拷贝,就是一个指向目标字符数组的指针
  • 所以这些函数都可以嵌套地调用这些函数

9.3.4 字符串比较

  • int strcmp( char const *s1, char const *s2 );
  • 两个字符串对应的字符逐个进行比较,直到发现不匹配为止
    • 最先不匹配的字符较“小”的那个字符所在的那个字符串被认为“小于”另外一个字符串
    • 其中一个字符串是另一个字符串前面一部分,那么它也被认为“小于”另外一个字符串
  • s1小于s2,返回一个小于零的值(不一定是-1);s1大于s2,返回一个大于零的值(不一定是1);两个字符串相等,则函数返回零

9.4 长度受限的字符串函数

  • 这些函数接受一个现实的长度参数
  • 如果源参数和目标参数发生重叠,strcpy和strncat的结果就是未定义的
char *strncpy( char *dst, char const *src, size_t len );
char *strncat( char *dst, char const *src, size_t len );
int *strncmp( char const *s1, char const *s2, size_t len );
  • 如果strlen(src)的值小于len,dst数组就用额外的NUL字节填充到len长度
  • 如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中,注意!它的结果不会以NUL字节结尾
  • 在使用不受限的函数之前,你首先必须确定字符串实际上是以NUL字节结尾的,但长度受限函数不需要
  • strncat总是在结果字符串后面添加一个NUL字节,并且不会像strcpy用NUL字节进行填充

9.5 字符串查找基础

9.5.1 查找一个字符

/*
在字符串str中查找字符ch第一次出现的位置,找到后返回一个指向该位置的指针
如果不存在,则返回NULL
*/
char *strchr ( char const *str, int ch);
//与strchr功能相似,但是是最后一次出现的位置
char *strrchr ( char const *str, int ch);

9.5.2 查找任何几个字符

/*
查找任何一组字符第1次在字符串中出现的位置
返回一个指向str中第1个匹配group中任何一个字符的字符位置
未匹配,返回NULL
*/
char *strpbrk( char const *str, char const *group );

9.5.3 查找一个子串

/*
如果s2并没有完整地出现在s1的任何地方,函数返回NULL
如果s2是一个空字符串,返回s1
*/
char *strstr( char const *s1, char const *s2 );
  • 标准库中并不存在strrstr或strrpbrk

9.6 高级字符串查找

9.6.1 查找一个字符串前缀

  • strspn和strcspan用于在字符串的起始位置对字符计数
  • strspn()从参数str字符串的开头计算连续的字符,而这些字符都完全是group 所指字符串中的字符。简单的说,若strspn()返回的数值为n,则代表字符串s开头连续有n 个字符都是属于字符串group内的字符
//返回str起始部分匹配cgroup中任意字符的字符数
size_t strspn( char const *str, char const *group );
//对str字符串起始部分中不与group中任何字符匹配的字符数
size_t strcspn( char const *str, char const *group );

int len1,len2;
char buffer[] = "25,142,330,Smith,J,239-4123";
len1 = strspn (buffer, "0123456789" );  //结果为2,因为只有"25"
len1 = strspn (buffer, ",0123456789" ); //结果为11,因为"25,142,330,"

//计算一个指向字符串中第1个非空白字符的指针
ptr = buffer + strspn( buffer, "\n\r\f\t\v"); 

9.6.2 查找标记

  • strtok:从字符串中隔离各个单独的称为标记(token)的部分,并丢弃分隔符
char *strtok( char *str, char const *sep)

void print_tokens( char *line )
{
	static char whitespace[] = " \t\f\r\v\n";
	char *token;

	for( token = strtok( line, whitespace ); token !=NULL; token = strtok( NULL, whitespace ) )
	{
		printf("Next token is %s\n", token);
	}
}
  • sep参数是个字符串,定义了用作分隔的字符集合
  • strtok找到str的下一个标记,并将其用NUL结尾,然后返回一个指向这个标记的指针
  • 它将会修改它所处理的字符串
  • 如果strtok函数的第1个参数不是NULL,函数将找到字符串的第1个标记,strtok同时保存它在字符串中的位置
  • 如果strtok函数的第1个参数是NULL,函数就在同一个字符串中从这个被保存的位置开始像前面一样查找下一个标记
  • 如果字符串内不存在更多的标记,strtok返回一个NULL指针


  • 你可以在每次调用strtok时使用不同的分隔符集合
  • 由于strtok函数保存它所处理的函数的局部状态信息,所以你不能用它同时解析两个字符串,因此,如果for循环的循环体内调用了一个在内部调用strtok函数的函数,程序会失败

9.7 错误信息

  • 当调用一些函数,请求操作系统执行一些功能,如果出现错误,操作系统是 通过设置一个外部的整型变量errno进行错误代码报告的
  • strerror把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针
  • char *strerror ( int error_number );

9.8 字符操作

  • 标准库包含了两组函数用于操作单独的字符:对字符分类和转换字符
  • ctype.h

9.8.1 字符分类

  • 每个分类函数接受一个包含字符值的整型参数
  • 函数测试这个字符并返回一个整型值
  • isspaceisupper等函数
  • 字符分类函数 表 P184

9.8.2 字符转换

  • 转换函数把大写字母转换为小写字母,或反过来
int tolower( int ch );
int toupper( int ch );
  • 如果参数并不是一个处于适当大小写状态的字符(不是大写或小写字母),函数将不修改参数直接返回
  • 直接测试或操纵字符将会降低程序的可移植性

9.9 内存操纵

  • 它们的操作与字符串函数类型,但这些函数能够处理任意的字节序列(可以包括NUL)
//length是以字节为单位的
void *memcpy( void *dst, void const *src, size_t length );
void *memmove( void *dst, void const *src, size_t length );
void *memcmp( void const *a, void const *b, size_t length );
void *memchr( void const *a, int ch, size_t length );
void *memset( void *a, int ch, size_t length );
  • 它们在遇到NUL字节时并不会停止操作
  • memcpy中如果src和dst以任何形式出现了重叠,它的结果是未定义的
  • 任何类型的指针都可以转换为void*型指针
  • memmove的行为和memcpy差不多,但它的源和目标操作数可以重叠,它可能比memcpy慢一些,但源和目标参数真的可能存在重叠,就应该使用memmove。原理:把源操作数复制到一个临时位置,这个临时位置不会与源或目标操作数重叠,然后再把它从这个临时位置复制到目标操作数
  • memcmp按照无符号字符逐字节进行比较,函数的返回类型和strcmp一样。如果比较不是单字节的数据如整型或浮点数时就可能出现不可预料的结果
  • memset把从a开始的length个字节都设置为字符值ch

9.10 总结

  • 字符串的长度就是它所包含的字符的数目,不包括NUL
  • strncpy中,如果源字符串比指定长度更长,结果字符串将不会以NUL字节结尾
  • strncat它的结果始终以一个NUL字节结尾

9.11 警告的总结

  • 应该使用有符号数的表达式中使用strlen函数,返回值类型size_t是无符号整型
  • 把strcmp函数的返回值当做布尔值进行测试,是错误的
  • 把strcmp函数的返回值与1或-1进行比较,是错误的
  • 使用strcpy函数产生不以NUL字节结尾的字符串
  • 忘了strtok函数将会修改它所处理的字符串
  • strtok函数是不可再入的,即连续几次调用中,即使它们的参数相同,其结果也可能不同

9.12 编程提示的总结

  • 使用字符分类和转换函数可以提高函数的移植性

第10章 结构和联合

10.1 结构基础知识

  • 聚合数据类型能够同时存储超过一个的单独数据,如数组和结构
  • 结构的值称为它的成员,各个成员可能具有不同的类型,可以通过名字来访问
  • 和数组名不同,当一个结构变量在表达式中使用时,它并不能替换成一个指针
  • 结构变量属于标量类型,你可以声明指向结构的指针,取一个结构变量的地址

10.1.1 结构声明

  • struct tag { member-list } variable-list;
  • 两个成员列表完全相同的结构体,也是不同的类型
  • 声明结构体时可以用typedef创建一种新的类型
    typedef struct {
        int a;
        char b;
    } Simple;
    
    Simple x;

10.1.2 结构成员

  • 一个结构的成员的名字可以和其他结构的成员的名字相同

10.1.3 结构成员的直接访问

  • 结构变量的成员是通过点操作符(.)访问的

10.1.4 结构成员的间接访问

  • 拥有一个指向结构的指针
    • struct COMPLEX *cp
    • 可以(*cp).f
    • 也可以cp->f
    • 后者称为->操作符,左操作数必须是一个指向结构的指针

10.1.5 结构的自引用

//非法
struct SELF_REF1 {
    int a;
    struct SELF_REF1 b;
    int c;
}

//合法
struct SELF_REF2 {
    int a;
    struct SELF_REF2 *b;  //注意这里是指针
    int c;
}

//非法,类型名直到生命的末尾才定义,所以在结构体生命的内部它尚未定义
struct {
    int a;
    struct SELF_REF3 *b;  
    int c;
} SELF_REF3

//合法
struct SELF_REF3_TAG {
    int a;
    struct SELF_REF3_TAG *b;  
    int c;
} SELF_REF3

10.1.6 不完整的声明

  • 相互之间存在依赖的结构,需要使用不完整声明
struct B;

struct A {
    struct B *partner;
}

struct B {
    struct A *partner;
}

10.1.7 结构的初始化

  • 这些值根据结构成员列表的顺序写出,如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化
struct INIT_EX {
    int a;
    short b[10];
    simple c;
} x = {
    10,
    { 1, 2, 3 };
    { 25, `x`, 1.9 }
};

10.2 结构、指针和成员

typedef struct {
    int a;
    short b[2];
} Ex2;
typedef struct EX {
    int a;
    char b[3];
    Ex2 c;
    struct EX *d;
} Ex;

Ex x = { 10, "Hi", { 5, { -1,25 } }, 0 };
Ex *px = &x;

10.2.1 访问指针

  • px + 1表达式并不是一个合法的左值
  • 右值:如果px指向一个结构数组的元素,这个表达式将指向该数组的下一个结构,但仍然是非法的,因为我们没法分辨内存下一个位置所存储的是这个结构元素之一还是其他东西

10.2.2 访问结构体

  • 访问结构体:*px
  • *px + 1是非法的
  • *(px + 1)由于x是标量,所以这个表达式实际上是非法的

10.2.3 访问结构成员

  • px->ax.a相等
  • 比较*pxpx->a
    • a的地址和结构的地址是一样的
    • 尽管两个地址是相等的,但它们的类型不同
    • int *pi; pi = px;是非法的,因为它们的类型不匹配
    • pi = &px->a是合法的,这个操作之后pi和px具有相同的值,但是类型不同,*px结果是整个结构,*pi结果是一个单一的整型值

10.2.4 访问嵌套的结构

10.2.5 访问指针成员


10.3 结构的存储分配

  • 编译器安装成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正确的便捷对齐要求时,成员之间才可能出现填充的额外内存空间
struct ALIGN {
    char a;
    int b;
    char c;
}
  • 如果某个机器的整型值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这个结构体在内存中的存储将如下所示
  • [a][][][][b, , , ][c][][][]
  • 系统禁止编译器在一个结构体的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置,因此a必须存储于一个能够被4整除的地址
  • sizeof操作符能够得到一个结构的整体长度,包括因边界对齐而跳过的那些字节
  • 确定结构体某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)
  • offsetof( type, member );
  • type就是结构的类型,member就是你需要的那个成员名,表达式的结果是一个size_t,表示这个指定成员开始存储的位置距离结构体开始存储的位置偏移几个字节
  • offsetof( struct ALIGN, b)的结果是4

10.4 作为函数参数的结构

  • 一般传指针,可以传值,但是效率低
  • 向函数传递指针的缺陷在于函数现在可以对调用程序的结构体进行修改,可以使用const来防止
  • void print_receipt ( register Transaction const * trans)
  • 参数声明为寄存器变量,可以进一步提高指针方案的效率

10.5 位段

  • 位段(bit field)
  • 位段的声明和结构类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际上存储于一个或多个整型变量中
  • 位段成员必须声明为int、signed int或unsigned int
  • 在成员名的后面是一个冒号和一个整数,这个整数指定该位所占用的位的数目
  • 如果位段声明为int,编译器决定是有符号还是无符号
  • 注重可移植性的程序应该避免使用位段
  • 位段中的成员在内存中是从左向右分配还是相反,在不同机器上是不同的
  • 位段声明的例子:
    struct CHAR {
        unsigned ch     : 7;
        unsigned font   : 6;
        unsigned size   : 19;
    };
    
    struct CHAR ch1;
  • 使用位段的理由:
    • 它能够把长度为奇数的数据包装在一起,节省存储空间
    • 可以很方便地访问一个整型值的部分内容

10.6 联合

  • 联合的所有成员引用的是内存中相同位置
    union {
        float f;
        int i;
    }
  • 如果f被使用,这个数就作为浮点值访问,如果i被使用,就作为整型值访问
  • 如果联合的各个成员具有不同的长度,联合的长度就是它最长成员的长度

10.6.1 变体记录

  • 变体记录:内存中某个特定的区域将不同的时刻存储不同类型的值,它们的每一个都是完整的结构
  • 如果这些成员的长度相差悬殊,当存储短成员时,浪费的空间相当可观
  • 为了节省空间,更好的办法是在联合中存储指向不同成员的指针而不是直接存储成员本身,因为所有指针的长度都是相同的

10.6.2 联合的初始化

  • 联合变量可以被初始化,但这个初始化必须是联合第1个成员的类型,而且必须在一对花括号里
    union {
        int a;
        float b;
        char c[4];
    } x = { 5 };
  • 如果给出的初始值时任何其他类型,它就会转换(如果可能的话)成一个整数并复制给x.a

10.7 总结

  • 结构标签是一个名字,它与一个成员列表相关联
  • 结构不能包含类型也是这个结构的成员,但它的成员可以是一个指向这个结构的指针,常常用于链式数据结构中
  • 编译器为一个结构变量的成员分配内存时要满足它们的边界对齐要求
  • sizeof返回的值包含了结构中浪费的内存空间
  • 位段是结构的一种,但它的成员长度以位为单位指定。位段声明在本质上是不可移植的

10.8 警告

  • 具有相同成员列表的结构声明产生不同类型的变量
  • 使用typeof为一个自引用的结构定义名字时应该小心

10.9 编程提示的总结

  • 把位段成员显示地声明为signed int或unsigned int类型

第11章 动态内存分配

  • 数组被声明时,它所需要的内存在编译时就分配
  • 也可以使用动态内存分配在运行时为它分配内存

11.1 为什么使用动态内存分配


11.2 malloc和free

  • malloc和free分别用于执行动态内存分配和释放,这些函数维护一个可用的内存池
  • malloc从内存池中提取一块合适的内存,返回这块内存起始位置的指针
  • 如果这块内存需要初始化,要么手动进行初始化,要么使用calloc
  • free函数把malloc等函数分配的内存还给内存池
  • 这些函数原型在stdlib.h中
  • void *malloc ( size_t size ); //以字节为单位
  • void free( void *pointer );
  • malloc实际分配的内存有可能比你请求的稍微多一点,由编译器定义
  • 如果内存池是空的,或者它的可用内存无法满足请求,那么:
    • malloc向系统请求要求得到更多内存,并在这块新内存上执行分配任务
    • 系统无法向malloc分配更多内存,malloc就返回一个NULL
  • free的参数
    • NULL,函数不会产生任何效果
    • malloc、calloc、realloc返回的值
  • malloc不知道内存的数据类型,返回void *类型的指针,可以转换为其他任何类型的指针
  • malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求

11.3 calloc和realloc

void *calloc( size_t num_element, size_t element_size);
void realloc( void *ptr, size_t new_size);
  • malloc和calloc区别:
    • 后者在返回指向内存的指针之前把它初始化为0
    • 它们请求内存数量的方式不一样,calloc的参数包括所需元素的数量和每个元素的字节数
  • realloc用于修改一个原先已经分配的内存的大小,扩大或缩小
    • 如果原先的内存无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的快上
    • 如果第一个参数是NULL,那么它的行为就和malloc一模一样

11.4 使用动态分配的内存

  • NULL定义于stdio.h,实际上是字面值常量0
  • malloc等函数分配出来的内存,不仅可以使用指针,也可以使用下标

1.5 常见的动态内存错误

  • 常见错误
    • 对NULL指针进行解引用操作
    • 对分配的内存进行操作时越过边界
    • 释放并非动态分配的内存
    • 试图释放一块动态分配的内存的一部分
    • 一块动态内存被分配之后被继续使用
  • 传递给free的指针必须是malloc、calloc或realloc返回的指针,让它释放一块并非动态分配的内存可能导致程序终止或在晚些时候终止
  • free释放一块内存的一部分是不允许的,动态分配的内存必须整体一块释放
  • realloc可以缩小动态分配的内存,有效地释放它尾部的部分内存

内存泄漏

  • 分配内存但在使用完毕后不释放将引起内存泄漏(memory leak)

11.6 分配内存实例

  • free联合的任一成员都可以,因为free不会理会指向内容的类型

11.7 总结

  • malloc函数返回时内存并未以任何方式进行初始化
  • realloc增加内存块大小时可能采取的方式是把原来内存块上的所有数据复制到一个新的、更大的内存块上
  • 内存泄漏是指内存被动态分配以后,当它不再使用时未被释放,内存泄漏会增加程序的体积,有可能导致程序或系统的崩溃

第12章 使用结构和指针

  • 本章代码较多,涉及大多是链接插入操作的优化方法,复习本章更好的方法是看书

12.1 链表

  • 链表就是一些包含数据的独立结构(通常称为节点)的集合
  • 通过链或指针连接在一起
  • 通常节点是动态分配的,但也有由节点数组构建的链表
  • 通过指针来遍历链表

12.2 单链表

  • 在单链表中,每个节点包含一个指向链表下一个节点的指针
  • 链表最后一个节点的指针字段的值为NULL
  • 为了记住链表的起始位置,可以使用一个根指针
  • 根指针指向链表的第1个节点
  • 链表中的节点可能分布于内存的各个地方
  • 单链表无法从相反的方向进行遍历

12.3 双链表

  • 在一个双链表中,每个节点都包含两个指针,一个指向前一个节点的指针和一个指向后一个节点的指针
  • 链表第1个节点的bwd字段和最后一个节点的rwd字段都为NULL

12.4 总结

  • 语句提炼是一种简化程序的技巧,其方法是消除程序中的冗余语句

12.5 警告的总结

  • 从if语句中提炼语句可能会改变测试结果

12.6 编程提示的总结

  • 不要仅仅根据代码的大小评估它的质量

第13章 高级指针话题

13.1 进一步探讨指向指针的指针


13.2 高级声明

//f是一个函数,这个函数的返回值是一个指向整型的指针
int *f();
//f是一个函数指针,它所指向的函数返回一个整型值
int (*f)();
//f是一个函数指针,它所指向的函数返回一个整型指针
int *(*f)();
//f是一个数组,数组的元素是整型指针
int *f[];
//f是一个函数,函数的返回值是一个整型数组,但是这个是非法的,因为函数只能返回标量,不能返回数组
int f()[];
//f是一个数组,数组的元素时返回值为整型的函数,但这个是非法的,因为数组元素必须具有相同的长度,但不同的函数显然可能具有不同的长度
int f[]();
//f是个数组,数组元素是函数指针,指针所指向的类型是返回值为整型的函数,其中int ()为指针指向的类型
int (*f[])();
//f是个数组,数组元素是函数指针,指针所指向的类型是返回值为整型指针的函数,其中int * ()为指针指向的类型
int *(*f[])();

13.3 函数指针

  • 函数指针的用途:转换表和作为参数传递给另一个函数
int f( int );
//初始化,在函数指针的初始化之前具有f的原型是很重要的,否则编译器无法检查f的类型是否与pf所指向的类型一致
int (*pf)( int ) = &f;  
  • 初始化表达式中&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针
  • 三种方式调用函数
int ans;
ans = f( 25 );
//把指针函数转焕为一个函数名,这个转焕并不是真正需要的,因为编译器在执行函数调用操作符之前会把它转换为函数指针
ans = (*pf)( 25 );
ans = pf( 25 );

13.3.1 回调函数

  • 回调函数:把一个函数指针作为参数传递给其他函数,被传递的这个函数称为回调函数

13.3.2 转移表

  • 转换表就是一个函数指针数组
  • 转换表需要两步操作
    • 声明并初始化函数指针数组
    • 用下面这条语句替换前面整条switch语句result = oper_func[ oper ]( op1, op2 );

13.4 命令行参数

13.4.1 传递命令行参数

  • C程序的main函数具有两个参数:
    • argc:表示命令行参数的数目
    • agrv:指向一组参数
    • int main( int argc, char **argv )
  • 在有些系统中,参数字符串是挨个存储的,这样当你把指向第1个参数的指针向后移动,越过第1个参数的尾部时,就达到了第2个参数的起始位置,即++*argv。但这个是由编译器决定的,不能依赖
  • 为了徐州一个参数的起始位置,你应该使用数组中适合的指针,即*++agr,指向下一个数组元素,然后使用间接访问操作获得字符串指针

13.5 字符串常量

//它的结果是个指针,指向字符串中第2个字符:y
"xyz"+1;
//它的结果一个字符,即x
*"xyz";
//以下结果为字符z
"xyz"[2]
//偏移量4超出了这个字符串的范围,是一个不可预测的字符
*("xyz"+4)

13.6 总结

  • 字符串常量的值时一个常量指针,它指向字符串的第1个字符,和数组名一样,你既可以用指针表达式也可以用下标来使用字符串常量

第14章 预处理器

  • 编译一个C程序的第1步称为预处理阶段
  • C预处理器在编译代码之前对其进行一些文本性质的操作
  • 主要任务包括:
    • 删除注释
    • 插入被#include指令包含的文件的内容
    • 定义和替换由#define指令定义的符号
    • 确定代码的部分内容是否应该根据一些条件编译指令进行编译

14.1 预定义符号

  • 预处理符号
    • _FILE_:进行编译的源文件名
    • _LINE_:文件当前行的符号
    • _DATE_:文件被编译的日期
    • _TIME_:文件被编译的时间
    • _STDC_:如果编译遵循ANSI C,其值就为1,否则未定义

14.2 #define

  • #define name stuff
  • 使用#define指定,可以把任何文本替换到程序中
  • 如果定义的stuff很长,可以分行,每行的末尾使用\

14.2.1 宏

  • #define机制允许把参数替换到文本中,称为宏或者定义宏
  • #define name(parameter-list) stuff 左括号必须与name紧邻,不能有空格
  • 一定要用宏替换产生的文本,来检查正确性
  • 所有用于对数值表达式进行求值的宏定义都应该使用括号
  • 宏,其作用域和变量不一样,宏是从定义的地方开始到代码块结束都是有效的。没有什么局部之分

14.2.2 #define替换

  • 宏参数和#define定义可以包含其他#define定义的符号,但宏不可以出现递归
  • 预处理器搜索#define定义的符号时,字符串常量的内容并不进行检查,如果想把宏参数插入到字符串常量中,可以
    • 方法1:利用邻近字符串自动连接的特性,把一个字符串分为几段,每一段实际上都是一个宏参数
    #define PRINT(FORMAT,VALUE) printf( "The value is " FORMART "\n", VALUE)
    int x = 22;
    PRINT("%d", x+3 );
    
    //输出为:The value is 25
-   方法2:`#argument`这种结构被预处理器翻译为`argument`
     #define PRINT(FORMAT,VALUE) printf( "The value of " #VALUE "is" FORMART "\n", VALUE)
    int x = 22;
    PRINT("%d", x+3 );
    
    //输出为:The vaule of x+3 is 25


  • ##结构把位于两边的符号连接成一个符号
  • 这种连接必须产生一个合法的标识符
#define ADD_TO_SUM( sum_number, value ) \
        sum ## sum_number += value
...
//把25加到变量sum5中
ADD_TO_SUM( 5, 25 );

14.2.3 宏与函数

  • 宏比函数的优势:
    • 规模和速度
    • 函数参数必须指定类型,而宏与类型无关
  • 宏比函数的劣势:每次使用宏都要代码拷贝

14.2.4 带副作用的宏参数

  • 当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么使用这个宏就可能出现危险
  • 副作用是在表达式求值时出现永久性效果

14.2.5 命名约定

  • 一个常见的约定就是把宏名字全部大写
  • 宏和函数的不同之处(参考P285 表14.2):
    • 代码长度
    • 执行速度
    • 操作符优先级
    • 参数求值
    • 参数类型

14.2.6 #undef

  • 移除一个宏定义#undef name

14.2.7 命令行定义

  • int array[ARRAY_SIZE];
  • 可以使用两种方式来编译前定义
    • -Dname
    • -Dname=stuff
  • cc -DARRAY_SIZE=100 prog.c

14.3 条件编译

  • 使用条件编译可以选择代码的一部分被正常编译还是完全忽略
/*
常量表达式由预处理器进行求值,非零为真,为真则编译,否则忽略,常量表达式可以是字面值常量或#define定义的符号
*/
#if 常量表达式  
    语句
#endif

/*
#elif出现次数不限
#else语句只有前面所有的常量表达式的值都是假才会被编译
*/
#if 常量表达式
    语句
#elif 常量表达式
    其他语句
#else 
    其他语句
#endif

14.3.1 是否被定义

  • 测试一个符号是否已经被定义
if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol
  • 每对定义的两条语句等价,但#if形式更强

14.3.2 嵌套指令

  • 为每个#endif加上一个注释标签是很有帮助的

14.4 文件包含

  • #include指令替换执行的方式:预处理器删除这条指令,并包含文件的内容取而代之,一个头文件如果被包含到10个源文件中,它实际上被编译了10次
  • 这样会涉及一些开销,但是这个开销只是在程序被编译时才存在,对运行时效率并无影响

14.4.1 函数库文件包含

  • 两种不同类型的#include文件包含:函数库文件和本地文件
  • 函数库头文件:#include <filename>,标准库文件以一个.h后缀结尾,运行于UNIX系统上的C编译器在/usr/include目录查找函数库头文件

14.4.2 本地文件包含

  • include "filename"
  • 处理本地头文件的一种常见策略是在源文件所在的当前目录进行查找,未找到就按照查找函数库头文件一样在标准位置查找本地头文件
  • 可以在所有的#include语句中使用双引号而不是尖括号,但对于函数库头文件,查找效率变低
  • 也可以使用绝对路径#include absolute_path
  • 如说使用绝对路径,那么正常的目录查找就被跳过

14.4.3 嵌套头文件包含

  • 标准要求必须支持失少8层头文件嵌套,但它并没有限制嵌套深度的最大值
  • 嵌套的不利之处
    • 它使得很难判断源文件之间的真正依赖关系
    • 一个头文件可能会被多次包含
  • 解决多重包含,可以使用条件编译
    • 例如,以下为某个头文件:
    #ifndef _HEADERNAME_H
    #define _HEADERNAME_H 1   // <-----这里也可以直接 #define _HEADERNAME_H
    #include <stdio.h>
	//  头文件具体内容
    #endif

14.5 其他指令

  • #error允许你生产错误信息#error text of error message
  • #line number "string":该语句通知预处理器number是下一行输入的行号,"string"预处理器把它当做当前文件名。该语句会修改_LINE__FILE_
  • #progma用于支持因编译器而异的特性,它的语法也是因编译器而异,有些编译器使用#progma指令
    • 在编译过程中打开或关闭清单显示
    • 把汇编代码插入到C程序中
  • #progma是不可移植的
  • 无效指令就是一个#符号开头,但后面不跟任何内容的一行
    • 例如:
    #
    #include <stdio.h>
    #

14.6 总结

  • 为了防止可能出现表达式中的于宏有关的错误,在宏完整定义的两边应该加上括号,在宏定义中每个参数的两边也要加上括号

14.7 警告的总结

  • 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句

第15章 输入\输出函数

  • 本章讨论ANSI C的输入和输出函数

15.1 错误报告

  • 标准库函数在一个外部整型变量errno(定义在errno.h)中保存错误代码之后把这个消息传递给用户程序,提示操作失败的准确原因
  • void perror( char const *message );定义于stdio.h
  • perror打印message字符串,后面跟一个分好和一个空格,然后打印一条用于解释error当前错误代码的信息
  • perror( "data3" );的结果是data3: No such file or directory
  • 只有当被调用的函数提示有错误发生时检查errno的值才有意义

15.2 终止执行

  • void exit( int status )定义于stdlib.h
  • status用于提示程序是否正常完成,这个值和main函数返回的整型状态值相同,EXIT_SUCCESSEXIT_FAILURE分别表示成功和失败
  • 调用perror之后再调用exit终止程序

15.3 标准I/0函数库

  • 在设计ANSI函数库时,可移植性和完整性是两个关键的考虑内容

15.4 ANSI I/0概念

  • stdio.h包含了与ANSI函数库的I/O部分有关的声明

15.4.1 流

  • 字节流被称为流
  • 绝大多数流是完全缓冲的,意味着“读取”和“写入”是从一块被称为缓冲区的内存区域来回复制数据
  • 用于输出流的缓冲区只有写满时才会被刷新(flush,物理写入)到设备或文件中
  • 输入缓冲区当它为空时通过设备或文件读取下一块较大的输入,重新填充缓冲区
  • 请求输入时同时刷新输出缓冲区,这样在用户必须进行输入之前,提示用户进行输入的信息和之前写入到缓冲区中的内容将出现在屏幕上
  • 每个用于调试的printf函数之后立即调用fflush
  • fflush迫使缓冲区的数据立即写入,不管它是否已满

一、文本流

  • 流分为两种:文本流和二进制流
  • 文本流特性不同系统不同
    • 文本的最大长度:标准规定至少254个字符
    • 文本行的结束方式:UNIX使用换行符结尾

二、二进制流

  • 二进制流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且根据它们从文件或设备读取的形式读入到程序中,并未作任何改变
  • 这种类型的流适合非文本数据

15.4.2 文件

  • FILE是一个数据结构,用于访问一个流
  • 每个流都有一个相关的FILE与它关联
  • 运行时系统提供至少三个流:标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们都是一个指向FILE结构的指针
  • 标准输入是缺省的输入的来源,标准输出是缺省的输出设置
  • 通常标准输入为键盘设备,标准输出为终端或屏幕
  • 程序运行时修改缺省的输入输出设备:program < data > answer,data作为标准输入,answer作为标准输出
  • 标准错误就是错误信息写入的地方,标准输出和标准错误在缺省情况下是相同的

15.4.3 标准I/0常量

  • EOF提示到了文件尾,EOF所选择的实际值比一个字符要多几位,为了避免二进制值被错误地解释为EOF
  • 一个程序最多能打开多少个文件和编译器有关,同时打开至少FOPEN_MAX个文件,它的值至少是8
  • FILENAME_MAX:一个字符数组应该多大以便容纳编译器所支持的最长合法文件名

15.5 流I/O总览

  • 文件I/O
    • 每个文件声明一个指针变量,其类型为FILE *
    • 流通过调用fopen函数打开,必须指定访问方式
    • 调用fclose关闭流
  • 标准I/0,并不需要打开或关闭
  • I/O函数三种基本形式:单个字符、文本行、二进制数据
  • P301 表15.1列出了每种I/O形式的函数家族
    • 字符:getcharputchar:读取(写入)单个字符
    • 问本行:gets、、puts:文本行未格式的输入(输出);scanfprintf:文本行格式化的输入(输出)
    • 二进制数据:freadfwrite:读取(写入)二进制数据
  • 每个族函数里都包含了各种函数变种用于执行下面的任务,这些函数的区别在于输入的来源或写入的地方不同
    • 只用于stdin或stdout
    • 随作为参数的流使用
    • 使用内存中的字符串而不是流
  • P301 表15.2 输入/输出函数家族列出了每个函数族中的变种函数

15.6 打开流

  • FILE *fopen( char const *name, char const *mode)打开一个特定的文件,并把一个流和这个文件相关联
  • mode提示流是只读、只写、既读又写;是文本流还是二进制流
    • 如下:

    • 读取 写入 添加  
      文本 “r” “w” “a”
      二进制 “rb” “wb” “ab”
  • 如果一个文件打开是用于写入的,如果它原先存在,那么原来的内容会被删除,如果原先不存在,那就创建一个新文件
  • 无论哪种情况,数据只能在文件的尾部写入
  • mode为“a+”表示文件打开用于更新,既允许读也可以写
  • 如果你已经从该文件读了一些数据,在你开始写入之前,必须调用其中一个文件定位函数(fseek、fsetpos、rewind)
  • 在你向一个文件写一些数据后,如果又想从该文件读,必选先调用fflush或文件定位函数之一
  • fopen执行失败,则返回NULL,errno会提示问题的性质
  • FILE *freopen( char const *filename, char const *mode, FILE *stream );用于打开(重新打开)一个特定的文件流,stream参数可以是fopen打开的流,也可以是标准流。该函数首先关闭这个流,然后用指定的文件和模式重新打开这个流,如果打开成功,函数就返回它的第3个参数值

15.7 关闭流

  • int fclose( FILE *f );关闭流
  • 在文件关闭之前刷新缓冲区,如果成功,返回零值,否则返回EOF

15.8 字符I/O

  • 当一个流被打开后,它可以用于输入和输出
  • 字符输入是由getchar家族执行的
int fgetc( FILE *stream );
int getc( FILE *stream );
//只用于标准输入
int getchar( void );
  • 以上函数,如果流中不存在更多的字符,返回EOF
  • 它们返回一个int型值而不是char,是为了允许函数报告文件的末尾(EOF)

  • 把单个字符写入流中,使用putchar函数家族
int fputc( int character, FILE *stream );
int putc( int character, FILE *stream );
//只用于标准输
int putchar( int character ); 
  • 以上函数把这个整型参数裁剪未一个无符号字符整型值
  • putchar( `abc` )只打印一个字符(至于是哪一个由编译器决定)
  • 函数失败,返回EOF

15.8.1 字符I/O宏

  • fgetc和fputc是真正的函数,程序长度更胜一筹
  • getc、putc、getchar、putchar都是宏,执行效率更高

15.8.2 撤销字符I/O

  • int ungetc( int character, FILE *stream);把一个先前读入的字符返回流中,这样它可以在以后被重新读入
  • 每个流都允许至少一个字符被退回
  • 如果一个流允许退回多个字符,那么这些字符再次被读取的顺序就以退回时的反序进行(类似栈,先进后出,先退后出)

15.9 未格式化的行I/O

  • 行I/O可以用两种方式执行:未格式化的和格式化的
  • 未格式化的I/O简单读取或写入字符串
  • 格式化的I/O执行数字和其他变量的内部和外部表示形式之间的转换
/*
fgets从指定的stream读取字符并复制到buffer中
当它读取第一个换行符并存储到缓冲区之后就不再读取
如果缓冲区存储的字符数达到buffer_size-1也停止读取
在任何一种情况下,一个NUL将添加到缓冲区数据的末尾,使之成为一个字符串
如果在任何字符读取之前就达到了文件尾部,则fgets返回NUL;否则返回第1个参数
*/
char *fgets( char *buffer, int buffer_size, FILE *stream );
//用于标准输入
char *gets( char *buffer ); 
/*
如果fputs的字符串包含好几个换行符,所有的换行符都会被写入
fputs既可以一次写入一行的一部分,也可以一次写入一行,也可以一次写入好几行
出错时返回EOF,否则返回一个非负值
*/
char *fputs( char *buffer, FILE *stream );
//用于标准输出
char *puts( char *buffer );
  • 以上函数用于操作字符串。
  • 如果函数需要计算被复制的行的数目,太小的缓冲区将产生一个不正确的计数,因为一个长行可能会被分成数段进去读取
  • fgets无法把字符串去读到一个长度小于2个字符的缓冲区,因为其中一个字符需要为NUL保留
  • gets和puts为的是允许向后兼容,它们与其他两个函数的功能性区别是:
    • 当gets读取一行时,并不在缓冲中存储结尾的换行符;当puts写入一个字符串时,它在字符串写入之后再添加一个换行符
    • gets没有缓冲长度参数,如果一个长输入行读到一个短的缓冲区,多出来的字符将被写入到缓冲区后面的存储位置

15.10 格式化的行I/O

  • scanf和printf并不仅限于单行,也可以在行的部分或多行上执行I/O操作

15.10.1 scanf家族

  • 一下每个原型中的...表示一个可变长度的指针列表。从传入的转换而来的值逐个存储到这些指针参数所指向的内存位置
//输入源为stream
int fscanf( FILE *stream, char const *format, ...);
//输入源为标准输入
int scanf( char const *format, ...);
//输入源为string字符串
int sscanf( char const *string, char const *format, ...);
  • 这些函数都是从输入源读取字符并根据format字符串给出的格式码对它们进行转换
  • 当格式化字符串到达末尾或读取的输入不再匹配格式字符串所指定的类型时,输入停止
  • 被转换的输入值的数目作为函数的返回值;转换之前文件就已经到达尾部,则返回EOF
  • scanf函数的参数前面为什么要加一个&是因为要转为指针列表

15.10.2 scanf格式代码

  • format字符串可能包含下列内容
    • 空白字符:与输入中的零个或多个空白字符匹配,处理过程中被忽略
    • 格式代码:指定函数如何解释接下来的字符
    • 其他字符:当任何其他字符休闲在格式字符串中时,下一个输入字符必须与它匹配。如果匹配,该输入字符随后被丢弃。如果不匹配,函数就不再读取直接返回
  • scanf的格式代码都以一个百分号开头,后面可以是
    • 一个可选星号,将转换后的值丢弃,用于跳过不需要的输入字符
    • 一个可选宽度,限制被读取用于转换的输入字符的个数,未给宽度,就连续读入直到遇到下一个空白字符
    • 一个可选限定符,用于修改有些格式代码的含义,为了指定参数的长度,P309 表15.3 scanf限定符
    • 格式代码,就是一个单字符,用于指定输入字符如何被解释,P310 表15.4 scanf格式码
  • 例子:
nfields = fscanf( input, "%d4 %d4 %d4", &a, &b, &c);
//若输入为12345 67890
//a为1234,b为5,c为6789,nfields为3,最后的0保持在未输入状态

15.10.3 printf家族

//输出源为stream
int fprintf( FILE *stream, char const *format, ...);
//输出源为标准输入
int printf( char const *format, ...);
//输出源为string字符串
int sprintf( char const *string, char const *format, ...);
  • 返回值是实际打印或存储的字符数
  • sprintf缓冲区大小并不是它的一个参数,如果输出结果很长溢出缓冲区时,就可能改写缓冲区后面内存位置中的数据
  • prinf函数家族的格式代码和scanf函数家族的格式代码用法不同
  • 参数类型与对应的格式代码不匹配,这个错误将导致输出结果是垃圾,也有可能导致程序失败

15.10.4 printf格式代码

  • 格式代码由一个百分号开头,后面跟
    • 零个或多个标志符号,用于修改有些转换的执行方式。决定填充是空白还是零以及它出现在值的左边还是右边
    • 一个可选的最小字段宽度,是一个十进制整数,用于指定将出现在结果中的最小字符数,值的字符数少于字段宽度,就对它进行填充以增加长度
    • 一个可选的精度,以一个句点.开头,后面跟一个可选的十进制整数,缺省值为零
    • 一个可选的修改符,P314 表15.7 printf格式代码修改符,用于指定整数和浮点数参数的准确长度,当打印长整数值时,最好坚持使用l修改符
    • 转换类型
  • P313 表15.5:printf格式代码表;表15.6:prinf格式标志表
  • 对于d、i、u、o、x、X类型的转换,精度指定将出现在结果中的最小的数字个数并覆盖零标志
  • e、E、f类型的转换,精度决定出现在小数点知乎的位数
  • g、G类型的转换,精度决定出现在结果中的最大有效位数
  • s类型的转换,精度指定将被转换的最多字符数
  • 字符或短整数值作为printf函数的参数时,它们在传递给函数之前先转换为整数
  • P314 表15.8 printf转换的其他形式

15.11 二进制I/O

  • 二进制形式写入效率最高,二进制输出避免了再数值转换为字符串过程中所涉及的开销和精度损失
  • 二进制I/O只有当数据将被另一个程序按顺序读取时才能使用
/*
@buffer:保存数据的内存位置的指针
@size:缓冲区中每个元素的字节数
@count:读取或写入的元素数
@返回值:实际读取或写入的元素(并非字节)数目,如果输入过程中遇到文件尾货输出过程中出错,这个数字可能比请求的元素数目要小
*/
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t fwrite( void *buffer, size_t size, size_t count, FILE *stream );

15.12 刷新和定位函数

  • int fflush( FILE *stream )迫使一个输出流的缓冲区内的数据进行物理写入,不管缓冲区是否已经写满
  • 调用fflush保证调试信息实际打印出来,而不是保存在缓冲区中直到以后才打印
  • C支持随机访问I/O,以任意顺序访问文件的不同位置,通过读取或写入先前定位到文件中需要的位置来实现
long ftell( FILE *stream );
int fseek ( FILE *stream, long offset, int from);
  • ftell返回流的当前位置,即下一个读取或要写入将要开始的位置距离文件起始位置的偏移量
    • 在二进制流中,这个值就是当前位置距离文件起始位置的字节数
    • 在文本流中,值表示一个位置,但并不一定准确地表示当前位置和文件起始位置之间的字符数
  • ftell返回值总是可以用于fseek函数中
  • fseek允许你在一个流中定位
    • 定位到一个文件起始位置之前是一个错误
    • 定位到文件尾之后并进行写入将拓展这个文件
    • 定位到文件尾之后并进行读取将导致返回一条“达到文件尾”的信息
    • fseek参数:SEEK_SETSEEK_CURSEEK_END
  • fseek带来三个副总用
    • 行末指示字符被清除
    • 如果在fseek之前使用ungetc把一个字符返回流中,那个这个被退回的字符将会被丢弃
    • 定位允许你从写入模式切换到读取模式,或回到打开的流以便更新
//将读/写指针设置回指定流的起始位置,同时清除流的错误提示标志
void rewind( FILE *stream );
//ftell的替代方案
int fgetpos( FILE *stream, fpos_t *position );
//fseek的替换方案
int fsetpos( FILE *stream, fpos_t const *position );
  • fgetpos、fsetpos与ftell、fseek主要区别
    • 这对函数接受一个指向fpos_t的指针作为参数
    • fgetpos在这个存储位置的当前位置
    • fsetpos把文件位置设置为存储在这个位置的值
  • fpos_t表示一个文件位置的方式并不是由标准定义的,可能是字节偏移量,也可能不是
  • 使用fgetpos返回的fpos_t的值的唯一安全用法是把它作为参数传递给后续的fsetpos

15.13 改变缓冲方式

void setbuf( FILE *stream, char *buf );
int setvbuf( FILE *stream, char *buf, int mode, size_t size );
  • 以上函数可以用于对缓冲方式进行修改,只有当指定的流被打开但还没有在它上面执行任何其他操作前才能被调用
  • setbuf设置了另一个数据,用于对流进行缓冲
    • 这个数组的字符长度必须为BUFSI(在stdio.h定义)
    • 为一个流自行指定缓冲区可以防止I/O函数库为它动态分配缓冲区
    • 如果用NULL参数调用函数,setbuf将关闭流的所有缓冲方式,字符准确地将程序所规定的方式进行读取和写入
  • setvbuf更为通用
    • mode用于指定缓冲流的类型。_IOFBF完全缓冲的流。_IONBF不缓冲的流。_IOLBF行缓冲流,即每当一个换行符写入到缓冲区时,缓冲区便进行刷新
    • buf和size用于指定需要使用的缓冲区

15.14 流错误函数

//流当处于文件尾时,feof返回真,这个状态可以通过fseek、rewind或fsetpos来清除
int feof( FILE *stream );
//报告流的错误状态,如果出现任何读/写错误函数就返回真
int ferror( FILE *stream );
//对指定流的错误标准进行重置
void clearerr( FILE *stream );

15.15 临时文件

  • FILE *tmpfile( void );
  • 这个函数创建一个文件,当文件被关闭或程序终止时这个文件便自动删除
  • 该文件以wb+模式打开,可用于二进制和文本数据
  • 临时文件的名字可以用char *tmpnam( char *name );创建

15.16 文件操作函数

//删除一个指定的文件
int remove( char const *filename );
改变一个文件的名字
int rename( char const *oldname, char const *newname );
  • 如果执行成功,返回零;失败,返回非零值

15.17 总结

  • 所有的I/O操作都是一种在程序中移动或移除字节的事物
  • 通常一个函数家族的各个变型包括接受一个流参数的函数,一个只用于标准流之一的函数以及一个使用内存中的缓冲区而不是流的函数
  • ungetc用于把一个字符退回流中,这个被退回的字符是下一个输入操作所返回的第1个字符,改变流的位置将导致这个被退回的字符被丢弃
  • fgets函数更为安全,它把缓冲区长度作为参数之一,因此可以保证一个长输入行不会溢出缓冲区,而且数据不会丢失。长输入行的超出缓冲区的那部分将被fgets下一次调用读取
  • gets去除它所读取的行的换行符,puts在写入到缓冲区的文本后加一个换行符
  • 二进制I/P直接读写的各个位,而不必把值转换为字符,但二进制输出的结果非人眼所能阅读
  • fsetpos函数的参数只有当它是先前一个作用于用一个流的fgetpos的返回值才是合法的
  • tmpnam为临时文件创建合适的文件名,这个名字不会与现存的文件名冲突

15.18 警告的总结

  • 忘了在一条调试用的printf后面跟一个fflush调用
  • 在任何scanf系列函数的每个非数组、非指针参数前忘了加上&符号
  • 注意在使用scanf系列函数转换double、long double、short、long整型时,在格式代码中加上合适的限定符
  • 混淆printf和scanf格式代码
  • 在有些长整数长于普通整数的机器上打印长整数值时,忘了在格式代码中指定l修改符

15.19 编程提示的总结

  • 当你打印长整数时,坚持使用l修改符可以提高可移植性

第16章 标准函数库

16.1 整型函数

  • 分为三类:算数、随机数、字符串转换

16.1.1 算数

//绝对值
int abs( int value );
//绝对值,作用对象是长整数
long int labs( long int value );
//即计算numberator/denominator,用一个div_V结构体返回,与/的区别是对运算结果精确定义:如果不能整除,商将是所有小于代数商的整数中最靠近它的那个整数
div_t div ( int numberator, in denominator );
//与div相同,作用对象是长整数
ldiv_t ldiv( long int number, long int denom );

16.1.2 随机数

  • 以下两个函数何在一起使用能够产生伪随机数,“伪”是因为它们通过计算产生随机数,因此可能重复出现,并不是正真的随机数
//返回一个范围在0和RAND_MAX(至少为32767)之间的伪随机数
//为了得到一个更小范围的伪随机数,首先把这个函数的返回值根据所需要的范围的大小进行取模,再通过加上或减去一个偏移量进行调整获得
int rand( void );
//该函数使用seed参数对随机数发生器进行初始化
void srand( unsigned int seed );
//一般使用每天的时间作为随机数产生器的种子
srand( ( unsigned int )time( 0 ) );

16.1.3 字符串转换

//把字符转换为整数
int atoi( char const *string );
//把字符转换为长整数
long int atol( char const *string );
/*
把字符转换为长整数
@unused:保存一个指向转换值后面第1个字符的指针unused,这个指针允许字符串的剩余部分进行处理而无需推测转换在字符串的哪个位置终止
@base:转换所执行的基数(即什么进制,例如十进制、二进制),若基数为0,任何在程序中用于书写整数字面值的形式都被接收,包括指定数字基数的形式,如0x2af4和0377;否则基数值应该在2到36的范围内
*/
long int strtol( char const *string, char **unused, int base );
//与strtol相同,但作用对象是无符号长整数
unsigned long int strtoul( char const *string, char **unused, int base );
//x为9947(以12进制来计算),指向字母e的指针保存在*next中
x = strtol( "     590bear", next, 12 );
  • 若第1个参数包含前导空白字符,将被跳过;存在任何非法尾缀字符,也将被忽略
  • 如果这些函数的string不包含一个合法的数值,函数就返回0
  • 被转换的值无法表示,则在errno中存储ERANGE这个值,并返回特定值,这些值在[P329,表16.1,strtol和strtoul返回的错误值]中

16.2 浮点型函数

  • 定义域错误:如果一个函数的参数不在该函数的定义域之内
    • sqrt( -6.0 );
    • 当出现定义域错误时,函数返回一个由编译器定义的错误值,并在errno中存储EDOM
  • 范围错误:如果一个函数的结果值过大或过小,无法用double类型表示
    • exp( DBL_MAX )
    • 值过大,函数将返回HUGE_VAL
    • 值过小,无法用double表示,返回0,但errno会不会设置为ERANGE取决于编译器

16.2.1 三角函数

16.2.2 双曲函数

16.2.3 对数和指数函数

//e值的x次幂
double exp( double x );
//x以e为底的对数,loge x
double log( double x );
//x以10为底的对数,log10 x
double log10( double x );

16.2.4 浮点表示形式

double frexp( double value, int *exponent );
double ldexp( double fraction, int exponent );
//把浮点值分成整数和小数两个部分,每个部分具有和原值一样的符号。整数部分以double类型存储于ipart指向的内存位置,小数部分作为函数的返回值返回
double modf( double value, double *ipart );

16.2.5 幂

//x的y次幂
double pow( double x, double y );
//返回x的平方根
double sqrt( double x );

16.2.6 底数、顶数、绝对值和余数

//返回不大于其参数的最大整数值
double floor( double x );
//返回不小于其参数的最小整数值
double ceil( double x );
//返回参数的绝对值
double fabs( double x );
//返回x除以y所产生的余数,这个除法的商被限制为一个整数值
double fnid( double x, double y );

16.2.7 字符串转换

double atof( char const *string);
//将string转换为一个double值,保存一个指向字符串中被转换的值后面的第1个字符的指针
double strtod(char const *string, char **unuserd );
  • string中前导空白符和缀尾非法字符被忽略
  • 如果值太大或太小,无法用double表示,errno存储ERANGE
  • 如果值太大,返回HUGE_VAL;值太小,返回零

16.3 日期和时间函数

16.3.1 处理器时间

clock_t clock( void )
  • 返回值为近似值
  • 如果机器无法提供处理器时间或时间值太大,无法用clock_t表示,返回-1
  • clock的返回值为处理器时钟滴答的次数,若要转换为秒,要除以常量CLOCKS_PER_SEC

16.3.2 当天时间

time_t time( time_t *returned_value );
  • 如果参数非NULL,时间值也会存储到参数中
  • 无法提供,或值太大,time_t无法表示,返回-1

日期和时间的转换

  • 以下的函数用于操作time_h值
/
*
返回值为一个指向字符串的指针,字符串的格式为 Sun Jul 4 04:02:48 1976\n\0
该字符串存储在一个静态数组中,下次调用ctime后,旧值会被覆盖
*/
char *ctime( time_t const *time_value );
//计算time1-time2的差,并转为秒
double difftime( time_t time1, time_t time2 );
  • 以下两个函数把一个time_t结构转换为tm结构
  • 月份从0开始计算,即0表示1月,11表示12月
  • tm结构的字段位于[P334,表16.2,tm结构的字段]
  • tm_year是从1900年后的年数,为了计算实际年份,需要加上1900
//转为世界协调世界(UTC),即格林尼治标准时间
struct tm *gmtime( time_t const *time_value );
//转为当地世界
struct tm *localtime( time_t const *time_value );
  • 当拥有一个tm结构之后,可以使用以下函数
//返回一个类似Sun Jul 4 04:02:48 1976\n\0的字符串,与ctime的一样,ctime在内部应该就是调用了asctime实现自己的功能的
char *asctime( struct tm const *tm_ptr );
/*
把tm转换为一个根据某个格式字符串而定的字符串
如果转换结果字符串的长度小于maxsize,则结果字符串复制到string中,返回值为字符串的长度;否则返回-1
*/
size_t strftime( char *string, size_t maxsize, char const *format, struct tm const *tm_ptr );
  • strftime的格式代码包括一个%字符,位于[P335,表16.3,strftime格式代码]
//tm转换为time_t
time_t mktime( struct tm *tm_ptr );

16.4 非本地跳转

  • setjmp和longjmp提供了类似goto语句的机制,但并不局限于一个函数的作用域之内,这些函数常用于深层嵌套的函数连用链
int setjmp ( jmp_buf state );
void longjmp( jump_buf state, int value );
  • 声明一个jmp_buf,并调用setjmp对它进行初始化,setjmp返回零,setjmp把程序的状态信息保存到跳转缓冲区中,你调用setjmp所处的函数称为你的“顶层”函数
  • 调用longjmp将导致jmp_buf这个保存的状态重新恢复,longjmp的效果就是使执行流通过再次从setjmp函数返回,从而跳回到顶层函数中
  • setjmp第1次被调用时,返回0;当setjmp作为longjmp的执行结果再次返回时,它的返回值是longjmp的第2个参数,它必须是一个非零值

16.4.1 实例

jmp_buf restart;

int main()
{
    int value;
    //确定一个我们希望在longjmp的调用之后恢复执行的地点
    value = setjmp( restart );
    
    switch( setjmp( restart ) ){
    default:
        //longjmp被调用--致命错误
        break;
    case 1:
        //longjmp被调用---小错误
        break;
    case 0:
        //最初从setjmp返回的地点:执行正常的处理,若出错,调用类似longjmp( restart, 1 );会跳回顶层函数,并调用case 1
    }
}

16.4.2 何时使用非本地跳转

  • 当顶层函数(调用setjmp的那个)返回时,保存在跳转缓冲区的状态信息便不再有效,在此之后再调用longjmp可能失败

16.5 信号

  • 信号表示一个时间,它可能异步地发生,也就是并不与程序执行过程的任何事件同步
  • 如果程序未安排怎样处理一个特定的信号,会做出一个缺省反应,一般缺省反应为终止程序
  • 程序可以设置一个信号处理函数,当信号发生时程序就调用这个函数,而不选择缺省反应

16.5.1 信号名

  • 同步表示信号在程序内部发生
  • 异步表示它们在程序的外部产生,通常是程序的用户触发,表示用户试图向程序传递一些信息
  • 以下为[P338,表16.4]
同步或异步 信号 含义 产生原因
同步 SIGABRT 程序请求异常终止 由abort函数引发
同步 SIGFPE 发生一个算术错误 算术上溢或下溢或除零
同步 SIGILL 检测到非法指令 CPU试图执行一条非法的指令
同步 SIGSEGV 检测到对内存的非法访问 程序试图非法访问内存
异步 SIGINT 收到一个交互性注意信号 用户试图中断程序
异步 SIGTERM 收到一个终止程序的请求 用户另一种请求终止程序的信息
  • SIGINT和SIGTERM的区别
    • SIGINT定义一个信号处理函数,目的是执行一些日常维护工作并在程序退出前保存数据
    • SIGTERM不配备信号处理函数,这样当程序终止时便不必执行这些日常维护工作

16.5.2 处理信号

//用于显示地引发一个信号,将引发参数所指定的信号
int raise( int sig );
  • 当一个信号发生时,程序可以使用三种方式作出反应
    • 缺省:由编译器定义,通常是终止程序
    • 可以被忽略
    • 可以设置一个信号处理函数
//用于指定程序希望采取的反应
void ( *signal ( int sig, void ( *handle )( int ) ) )( int );
  • 下面拆开分析signal函数
/*
@sig:为表16.4的信号之一
@void ( *handle )( int ):为sig信号设置的信号处理函数,是一个函数指针。当信号发生时,信号的代码作为参数传递给信号处理函数
*/
signal ( int sig, void ( *handle )( int ) ) 

/*
下面将原型的参数去掉
signal函数返回一个指向该信号以前的处理函数的指针,通过保存这个返回值,可以为信号设置一个处理函数并在将来恢复为先前的处理函数
*/
void ( *signal() )( int );
  • signal.h还定义了宏SIG_DFLSIG_IGN,可以作为siganl函数的第2个参数
    • SIG_DFL:恢复对该信号的缺省反应
    • SIG_IGN:该信号被忽略

16.5.3 信号处理函数

  • 当一个已经设置了信号处理函数的信号发生时
    • 首先,恢复对该信号的缺省行为
    • 然后,信号处理函数被调用,信号代码作为参数传递给函数
  • 信号处理函数可能执行的工作类型是很有限的
    • 异步信号,不应该调用除siganl之外的任何库函数
    • 信号处理函数除了向一个类型为volatile sig_atomatic_t静态变量赋值外,可能无法访问任何其他静态变量
    • 信号处理函数能做的就是对这些变量之一进行设置然后返回
    • 类型sig_atomatic_t定义了一种CPU可以以原子方式访问的数据类型

一、volatile数据

  • volatile关键字告诉编译器,变量的值不能确保在两条相邻的程序语句中具有相同的值,防止编译器以一种可能修改程序含义的方式“优化”程序

二、从信号处理函数返回

  • 从一个信号处理函数返回导致程序的执行流从信号发生的地点恢复,这个规则的例外情况是SIGFPE

16.6 打印可变参数列表

int vprintf( char const *format, va_list arg );
int vfprintf( FILE *stream, char const *format, va_list arg );
int vsprintf( char *buffer, char const *format, va_list arg );
  • arg参数必须使用va_start进行初始化
  • 不需要调用va_end

16.7 执行环境

16.7.1 终止执行

  • 以下三个函数与正常或不正常的程序终止有关
//用于不正常地终止一个正在执行的程序,引发SIGABRT信号,可以设置信号处理函数,在程序终止之前采取任何你想要的动作
void abort( void );
//该函数可以把一些函数注册为退出函数,当程序将要正常终止时,退出函数被调用
void atexit( void (func)( void ) );
/*
用于正常终止程序
如果程序以main函数返回一个值结束,那么其效果相当于用这个值作为参数调用exit函数
*/
void exit( int status );
  • 当exit函数被调用(函数终止过程)
    • 所有被atexit函数注册为退出函数的函数将按照它们注册的顺序的反序依次调用
    • 所有流的缓冲区被刷新
    • 所有打开文件被关闭
    • 用tmpfile创建的文件被删除
    • 退出状态返回宿主环境,程序停止执行

16.7.2 断言

  • 断言就是声明某种东西应该为真
  • ANSI C实现了一个assert宏
  • void assert( int expression );
  • 当它被执行时,这个宏对表达式参数进行测试。
  • 如果为假(零),就向标准错误打印一条诊断信息并终止程序;如果为真(非零),就不打印任何东西,程序继续执行
  • assert( value != NULL );,如果value为NULL,则会打印Assertion failed: value != NULL, file.c line 280
  • assert只适合用于验证必须为真的表达式
  • 可以在编译时通过定义NDEBUG消除所有的断言,以下任一操作,预处理器将丢弃所有的断言
    • 使用-D NDEBUG编译器命令行选项
    • 在源文件中头文件assert.h被包含之前增加#define NDEBUG

16.7.3 环境

  • 环境就是 由一个编译器定义的名字/值对的列表
  • 该列表由操作系统进行维护
  • getenv函数在这个列表中查找一个指定的名字,如果找到,返回一个指向其值对应的指针;如果未找到,返回NULL
  • char *getenv( char const *name );

16.7.4 执行系统命令

  • system函数把它的字符串参数传递给宿主操作系统,这样它就可以作为一条命令,由操作系统的命令处理器指执行
  • void system( char const *command );
  • system可以用一个NULL参数调用,用于询问命令处理器是否实际存在

16.7.5 排序和查找

  • qsort函数在一个数组中以升序方式对数据进行排序,和数组中的数据类型无关
/*
@base:指向需要排序的数组
@n_elements:指定数组中元素的数目
@el_size:每个元素的长度(以字节为单位)
@compare:函数指针,用于带需要排序的元素进行比较。比较函数返回一个整数,大于零、等于零和小于零分别表示第1个参数大于、等于和小于第2个参数
*/
void qsort( void *base, size_t n_elements, size_t el_size, int (*compare)( void const *, void const * ) );
  • bsearch函数在一个已经排好序的数组中用二分法查找一个特定的元素
  • 如果数组未排序,其结果是未定义的
/*
@key:指向需要查找的值
@base:指向查找所在的数组
@n_elements:指定数组中元素的数目
@el_size:每个元素的长度(以字节为单位)
@compare:函数指针,用于带需要排序的元素进行比较。比较函数返回一个整数,大于零、等于零和小于零分别表示第1个参数大于、等于和小于第2个参数
@返回值:一个指向查找到的数组元素的指针,若不存在,则返回NULL
*/
void *bsearch( void const *key, void const *base, size_t n_elements, size_t el_size, int (*compare)( void const *, void const * ) );

16.8 locale

  • 为了使C语言在全世界的范围内更为通用,定义了locale标准,是一组特定的参数,每个国家可能各不相同
  • 缺省情况下是“C” locale
/*
用于修改整个或部分locale
@category:指定locale哪个部分需要修改,可以的值包括LC_ALL:整个locale;LC_COLLATE:对照序列,将影响strcoll和strxfrm函数,更多参照[P346,表16.5,setlocale类型]
@locale:若不是NULL,它指定需要使用的新locale
@返回值:返回一个指向给定类型的当前locale的名字的指针
*/
char *setlocale( int category, char const *locale );

16.8.1 数值和货币格式

  • struct lconv *localeconv( void );用于获取根据当前的locale对非货币值和货币值进行核实的格式化所需要的信息,它只提供一些如何进行格式化的信息

16.8.2 字符串和locale

  • 一台机器的字符集的对照序列是固定的,但locale提供了一种方法指定不同的序列
//对两个根据当前locale的LC_CIKKATE类型参数指定的字符串进行比较
int strcoll( char const *s1, char const *s2 );
/*
把根据当前的locale解释的s2转换为另一个不依赖于locale的字符串(把一个当前对照序列的字符串转换为一个位于缺省对照序列的字符串)
尽管转换后的字符串的内容是未确定的,但使用strcmp函数对这种字符串进行比较和使用strcoll对原先的字符串比较的结果是相同的
*/
size_t strxfrm( char *s1, char const *s2, size_t size );

16.8.3 改变locale的效果

  • 可能使得字符集增加字符
  • 打印的方向可能会改变
  • printf和scanf函数家族使用当前定义的小数点符号
  • isalpha、islower、isspace、isupper函数可能比之前包括更多的字符
  • 字符集的对照序列可能会改变
  • strftime所产生的日期和时间格式的许多方面都是特定于locale的

16.9 总结

  • div和ldiv用于执行整数除法。和/操作符不同,当其中一个参数为负时,商的值是精确定义的
  • frexp用于计算一个给定值的表示形式,ldexp用于解释一个表示形式,恢复它的原先值,modf把浮点值分隔成整数和小树部分
  • tm结构包含了日期和时间的所有组成部分
  • 一个信号处理函数中修改的变量应该声明为volatile
  • locale包括了
    • 定义数值如何进行格式化的参数,他们描述的值包括非货币值、本地货币值和国际货币值
    • 可以指定一个和机器的缺省序列不同的对照序列

16.10 警告的总结

  • longjmp不能返回一个已经不再处于活动状态的函数
  • 从异步信号的处理函数中调用exit或abort是不安全的(处理函数不要再调用除siganl之外的任何库函数)
  • 当每次信号发生时,你必须重新设置信号处理函数
  • 避免exit函数的多重调用

第17章 经典抽象数据类型

  • 经典抽象数据类型有链表、堆栈、队列和树等,链表在第12章已经介绍过,本章会讨论剩余的ADT
  • 由于本章是介绍堆栈、队列和树的实现,代码较多,如果复习本章,推荐重看一遍

17.1 内存分配

  • ADT存储方式:
    • 静态数组:长度固定,长度在编译时确定,最简单最不易出错
    • 动态分配数组:运行时才决定长度,可动态改变数组长度
    • 动态分配链式结构:最大灵活性,需要时才单独分配,但链式结构的链接字段需要消耗一定的内存,访问特定元素的效率不如数组

17.2 堆栈

  • 堆栈特点:后进先出(Last-In First-Out,LIFO)

17.2.1 堆栈接口

  • 传统接口
    • push:把一个新值压入到堆栈的顶部
    • pop:把堆栈顶部的值移出堆栈并返回这个值
  • 另一类堆栈接口
    • push:把一个新值压入到堆栈的顶部
    • pop:把堆栈顶部的值移出堆栈但不返回这个值
    • top:返回顶部的值,但不把顶部元素移除
  • 还需要两个额外的函数:堆栈是否为空、堆栈是否已满

一、数组堆栈

  • 所有不属于外部接口的内容都声明为static,可以防止用于使用预定义接口之外的任何方式访问堆栈中的值
  • 数组实现的堆栈使用下标记录栈的顶部,这个值的初始化为static int top_element = -1

二、动态数组堆栈

  • 动态数组堆栈还需要
    • 创建堆栈函数void create_stack( size_t size );
    • 销毁堆栈函数void destroy_stack( void );

三、链式堆栈

  • 不再需要create_stack函数,但可以实现destroy_stack函数用于清除堆栈
  • 由于链式堆栈不会填满,所以is_full函数始终返回假

17.3 队列

  • 队列是 一种先进先出(First-In First-Out,FIFO)的结构

17.3.1 队列接口

  • 插入和删除函数并没有被普遍接受的名字
  • 对于插入应该在队列的头部还是在尾部也没有完全一致的意见,在队列的尾部插入以及在头部删除更容易记忆
  • 传统接口:delete函数从队列的头部删除一个元素并将其返回
  • 另一种接口:delete函数从队列的头部删除一个元素,但并不返回它;first函数返回第1个元素但并不将它从队列删除

17.3.2 实现队列

  • 让队列的尾部“环绕”到数组的头部,新元素可以存储到以前删除元素所留出来的空间,这个方法称为循环数组
  • 有两种方法实现循环:
//下面定义的QUEUE_SIZE都表示数组的长度
//方法1:
rear += 1;
if( rear >= QUEUE_SIZE )
    rear = 0;

//方法2:
rear = ( rear + 1 ) % QUEUE_SIZE;
  • 判断队列是否为空、是否已满也有两种方法
    • 方法1:引入新的变量,用于记录队列中的元素数量
    • 方法2:重新定义“满”的含义,使数组中的一个元素始终保持不用
//方法2:
//队列为空
( rear + 1 ) % QUEUE_SIZE == front
//队列已“满”
( rear + 2 ) % QUEUE_SIZE == front

17.4 树

  • 二叉搜索树(binarg search tree,BST)
  • 树是一种数据结构,它要么为空,要么具有一个值并具有零个或多个孩子,每个孩子本身也是树
  • 二叉树是树的一种特殊形式,它的每个节点至多具有两个孩子,分别称为左孩子和右孩子
  • 二叉搜索树具有额外的属性:每个节点的值比它的左子树的所有节点的值都要大,但比它的右子树的所有节点的值都要小
  • 没有孩子的节点称为叶节点或叶子

17.4.1 在二叉树搜索树中插入

  • 基本算法如下:
如果树为空:
    把新值作为根节点插入
否则:
    如果新值小于当前节点的值:
        把新值插入到当前节点的左子树
    否则:
        把新值插入到当前节点的右子树

17.4.2 从二叉搜索树删除节点

  • 处理三种情况:
    • 删除没有孩子的节点:删除一个叶节点不会导致任何子树断开,所以不存在重新连接的问题
    • 删除只有一个孩子的节点:把这个节点的双亲节点和它的孩子连接起来
    • 删除有两个孩子的节点:不删除这个节点,删除它的左子树中值最大的那个节点,并用这个值替代原先应被删除的那个节点的值

17.4.3 在二叉搜索树中查找

  • 基本算法如下:
如果树为空:
    这个值不存在于树中
否则:
    如果这个值和根节点的值相等:
        成功找到这个值
    否则:
        如果这个值小于根节点的值:
            查找左子树
        否则:
            查找右子树

17.4.4 树的遍历

  • 遍历方法包括:前序、中序、后序和层次遍历,可以从树的根节点或你希望开始遍历的子树的根节点开始
    • 前序(pre-order):根->左->右
    • 中序(in-order):左->根->右
    • 后序(post-order):左->右->根
    • 层次遍历(breadth-first):逐层检查树的节点,一层从左到右扫过

17.4.6 实现二叉搜索树

一、数组形式的二叉搜索树

  • 用数组表示数的关键是使用下标来寻找某个特定值的双亲和孩子
//从数组下标1开始计算的树
节点N的双亲节点N/2(整除操作符将截取小数部分)
节点N的左孩子是节点2N
节点N的右孩子节点2N+1
//从数组下标0开始计算的树
节点N的双亲节点(N+1)/2-1(整除操作符将截取小数部分)
节点N的左孩子是节点2N+1
节点N的右孩子节点2N+2
  • 数组形式的树问题在于数组空间常常利用得不够肠粉,空间被浪费是由于新值必须插入到树中特定的位置,无法随便防止到数组中的空位置
  • 不平衡的树空间浪费严重

二、链式二叉搜索树

  • 链式实现消除了数组空间利用不充分的问题

三、树接口的变型

  • find函数只用于验证值是否存在于树中
  • 树中的元素实际上是一个结构,它包括一个关键值和一些数据
  • find函数必须设法比较每个节点元素的关键值部分,解决办法是编写一个函数执行这个比较
  • TreeNode结构和指向树根节点的指针都必须声明为公用,以便用户遍历该树
  • 通过函数向用户提供根指针,可以防止用户自行修改根指针,从而导致丢失整棵树

17.5 实现的改进

17.5.4 标准函数库的ADT

  • 泛型是一种编写一组函数,但数据类型暂时可以不确定的能力,这组函数随后用用户需要的不同类型进行实例化或创建,但C语言未提供泛型,可以用#define定义近似地模拟这种机制
  • 泛型是面向对象编程语言处理得比较完美的问题之一

17.6 总结

  • 数组可以用于实现BST,但如果树不平衡,会浪费很多内存空间,链式BST可以避免这种浪费

第18章 运行时环境

18.1 判断运行时环境

  • 第1步:从你的编译器获得一个汇编语言列表
  • 第2步:阅读你的机器上的汇编语言代码

18.1.1 测试程序

  • C代码
//静态初始化
int static_variable = 5;

void f()
{
    register int i1, i2, i3, i4, i5, i6, i7, i8, i9, i10;
    register char *c1, *c2, *c3, *c4, *c5, *c6, *c7, *c8, *c9, *c10;
    extern inta_very_long_name_to_see_how_long_they_can_be;
    double dbl;
    intfunc_ret_int();
    double func_ret_double();
    char *func_ret_char_ptr();
    
    //寄存器变量的最大数量
    i1 = 1; i2 = 2; i3 = 3; i4 = 4; i5 = 5;
    i6 = 6; 71 = 7; i8 = 8; i9 = 9; i10 = 10;
    c1 = (char *)110; c2 = (char*)120;
    c3 = (char *)130; c4 = (char*)140;
    c5 = (char *)150; c6 = (char*)160;
    c7 = (char *)170; c8 = (char*)180;
    c9 = (char *)190; c10 = (char*)200;
    
    //外部名字
    int a_very_long_name_to_see_how_long_they_can_be = 1;
    
    //函数调用/返回协议,堆栈帧(过程活动记录)
    i2 = func_ret_int( 10, i1, i10 );
    db1 = func_ret_double();
    c1 = func_ret_char_ptr( c1 );
}

int
func_ret_int( int a, int b, register int c )
{
    int d;
    
    d = b - 6;
    return a + b + c;
}

double 
func_ret_double()
{
    return 3.14;
}

char *
func_ret_char_ptr( char *cp )
{
    return cp + 1;
}
  • 汇编代码
    .data
    .even
    .global _static_variable
_static_variable:
    .long 5
    .text
    
    .globl _f
_f: 
    link   a6, #-88
    moveml #0x3cfc,sp@
    moveq  #1,d7
    moveq  #2,d6
    moveq  #3,d5
    moveq  #4,d4
    moveq  #5,d3
    moveq  #6,d2
    movl   #7,a6@(-4)
    movl   #8,a6@(-8)
    movl   #9,a6@(-12)
    movl   #10,a6@(-16)
    movl   #110,a5
    movl   #120,a4
    movl   #130,a3
    movl   #140,a2
    movl   #150,a6@(-20)
    movl   #160,a6@(-24)
    movl   #170,a6@(-28)
    movl   #180,a6@(-32)
    movl   #190,a6@(-36)
    movl   #200,a6@(-40)
    movl   #1,_a_very_long_name_to_see_how_long_they_can_be
    movl   a6@(-16),sp@-
    movl   d7,sp@-
    pea    10
    jpsr   _func_ret_int
    lea    sp@(12),sp
    movl   d0,d6
    jpsr   _func_ret_double
    movl   d0,a6@(-48)
    movl   d1,a6@(-44)
    pea    a5@
    jpsr   _func_ret_char_ptr
    addqw  #4,sp
    movl   d0,a5
    moveml a6@(-88),#0x3cfc
    unlk   a6
    rts
    
    .globl _func_ret_int
_func_ret_int:
    link   a6, #-8
    moveml #0x80,sp@
    movl   a6@(16),d7
    movl   a6@(12),d0
    subql  #6,d0
    movl   d0,a6@(-4)
    movl   a6@(8),d0
    addl   a6@(12),d0
    addl   d7,d0
    moveml a6@(-8),#0x80
    unlk   a6
    rts
    
    .globl _func_ret_double
_func_ret_double:
    link   a6, #0
    moveml #0,sp@
    movl   L2000000,d0
    movl   L2000000+4,d1
    unlk   a6
    rts
L2000000:.long  0x40091eb8,0x51eb851f

    .globl _func_ret_char_ptr
_func_ret_char_ptr:
    link   a6, #0
    moveml #0,sp@
    movl   a6@(8),d0
    addql  #1,d0
    unlk   a6
    rts

18.1.2 静态变量和初始化

//静态初始化
int static_variable = 5;
    .data   //进入程序的数据区
    .even   //确保变量开始于内存的偶数地址
    .global _static_variable    //变量被声明为全局类型,变量名以下划线开始
_static_variable:   //编译器为变量创建空间
    .long 5     //进行初始化

18.1.3 堆栈帧

  • 一个函数分为三个部分:
    • 函数序:用于执行函数启动需要的一些工作,如为局部变量保留堆栈中的内存
    • 函数体:用于执行有用工作的地方
    • 函数跋:用于在函数即将返回之前清理堆栈
void f()
{
    register int i1, i2, i3, i4, i5, i6, i7, i8, i9, i10;
    register char *c1, *c2, *c3, *c4, *c5, *c6, *c7, *c8, *c9, *c10;
    extern inta_very_long_name_to_see_how_long_they_can_be;
    double dbl;
    intfunc_ret_int();
    double func_ret_double();
    char *func_ret_char_ptr();
    .text   //表示进入程序的代码(文本)段
    
    .globl _f   //函数名的全局声明,也有下划线
_f: 
    link   a6, #-88     //第1条可执行指令,为函数创建堆栈帧,堆栈帧是堆栈中的一个区域,函数在那里存储变量和其他值(寄存器),link在堆栈帧中保留了88个字节的空间,用于存储局部变量和其他值
    /*
    把指定寄存器中的旧值复制到堆栈中,68000处理器有8个用于操作数据的寄存器,d0至d7;8个用于操作地址的寄存器,a0至a7
    0x3cfc即二进制0011110011111100,分别表示[a7,a6,a5,a4,a3,a2,a1,a0,d7,d6,d5,d4,d3,d2,d1,d0]的使用情况
    可以看到d2至d7、a2至a5需要被存储,被存储的即时“其他值”
    */
    moveml #0x3cfc,sp@  
  • 局部变量声明和函数原型不会产生任何汇编代码,所以下面这些C代码不会产生汇编代码。
register int i1, i2, i3, i4, i5, i6, i7, i8, i9, i10;
    register char *c1, *c2, *c3, *c4, *c5, *c6, *c7, *c8, *c9, *c10;
    extern inta_very_long_name_to_see_how_long_they_can_be;
    double dbl;
    intfunc_ret_int();
    double func_ret_double();
    char *func_ret_char_ptr();
  • 如果任何局部变量在声明时进行了初始化,那么这里也会出现指令用于执行赋值操作

18.1.4 寄存器变量

    //寄存器变量的最大数量
    i1 = 1; i2 = 2; i3 = 3; i4 = 4; i5 = 5;
    i6 = 6; 71 = 7; i8 = 8; i9 = 9; i10 = 10;
    c1 = (char *)110; c2 = (char*)120;
    c3 = (char *)130; c4 = (char*)140;
    c5 = (char *)150; c6 = (char*)160;
    c7 = (char *)170; c8 = (char*)180;
    c9 = (char *)190; c10 = (char*)200;
    moveq  #1,d7    //值1至6倍存放在数据寄存器,最多只有6个整型值可以被存放在数据寄存器
    moveq  #2,d6
    moveq  #3,d5
    moveq  #4,d4
    moveq  #5,d3
    moveq  #6,d2
    /*
    7至10倍存放在其他地方
    a6称为帧指针,它指向堆栈帧内部的一个“引用”位置,堆栈中的所有值都是通过这个引用位置加上一个偏移量进行访问的
    a6@(-4)表示偏移地址-4
    使用偏移地址,可以建立一张映射表,准地地显示堆栈中的每个值相对于帧指针a6的位置
    */
    movl   #7,a6@(-4)   
    movl   #8,a6@(-8)
    movl   #9,a6@(-12)
    movl   #10,a6@(-16)
    movl   #110,a5      //前4个值被存放在地址寄存器,最多运行4个指针变量放在寄存器,指针的长度是固定的,任何类型的指针都可以放在寄存器中
    movl   #120,a4
    movl   #130,a3
    movl   #140,a2
    movl   #150,a6@(-20)    //被存放在其他地方
    movl   #160,a6@(-24)
    movl   #170,a6@(-28)
    movl   #180,a6@(-32)
    movl   #190,a6@(-36)
    movl   #200,a6@(-40)
  • 前面提到的moveml #0x3cfc,sp@即是将寄存器的旧值保存到堆栈中,函数必须对任何将用于存储寄存器变量的寄存器进行保存,这样它们原先的值可以在函数返回到调用函数前恢复,即moveml a6@(-88),#0x3cfc语句,这样就能保留调用函数的寄存器变量
  • d0-d1、a0-a1以及a6-a7并未用于存储寄存器
    • a6用作帧指针,即平时所说的ebp
    • a7是堆栈指针(别名SP),即平时所说的esp
    • d0、d1用于从函数返回值
    • a0、a1用于其他某种目的

18.1.5 外部标识符的长度

	//外部名字
    int a_very_long_name_to_see_how_long_they_can_be = 1;
    movl   #1,_a_very_long_name_to_see_how_long_they_can_be
  • 外部名字的最终限制是链接器施加的,它很可能接收任何长度的名字但忽略除前几个字符以外的其他字符

18.1.6 判断帧布局

  • 运行时堆栈保存了每个函数运行时所需要的数据,包括它的自动变量和返回值
  • 下面将分析两个部分
    • 堆栈帧的组织形式
    • 调用和从函数返回的协议

一、传递函数参数

//函数调用/返回协议,堆栈帧(过程活动记录)
    i2 = func_ret_int( 10, i1, i10 );
    movl   a6@(-16),sp@-    //i10存在a6@(-16),所以是把参数i10压入堆栈
    movl   d7,sp@-  //i1存在d7,所以是把i1压入堆栈
    pea    10   //pea指令简单地把它的操作数压入堆栈,所以这里将参数10压入堆栈
    /*
    跳转子程序,把返回值压入到堆栈中(即main函数中_func_ret_int函数返回后下一条指令的地址),并跳转到_func_ret_int的起始位置
    当被调用函数结束任务后需要返回到它的调用位置,需要用到压入到堆栈中的返回值
    堆栈的情况如图18.2所示
    */
    jpsr   _func_ret_int    
  • 图18.1显示了到目前为止所创建的内容
    • 低内存地址位于顶部而高内存地址位于底部
    • 当值压入堆栈时,堆栈向低地址方向生长(向上)
    • 在原先的堆栈指针以下的内容是未知的

二、函数序

  • 接下来,执行流来到被调用函数的函数序:
int
func_ret_int( int a, int b, register int c )
{
    int d;
    .globl _func_ret_int
_func_ret_int:
    /*
    link指令分成三步
    1.a6的内容被压入到堆栈中(旧的a6值被压入)
    2.堆栈指针的当前值被复制到a6(a6指向SP当前位置,当前位置为a6@(0),也就是将当前栈顶变为新的栈底),图18.3显示了目前为止的堆栈帧状态
    3.link指令从堆栈指针中减去8(SP指向a6@(-8)),这将创建空间用于保存局部变量和被保存的寄存器的旧值,图18.4显示了目前为止的堆栈帧状态
    */
    link   a6, #-8
    /*
    0x80二进制为10000000,表示[d7,d6,d5,d4,d3,d2,d1,d0]的使用情况,所以这里是把d7寄存器的旧址存储在堆栈的顶部,它提示堆栈帧的顶部就是寄存器值保存的位置
    */
    moveml #0x80,sp@    
    /*
    从堆栈把第三个参数复制到d7,因为第三个参数声明为寄存器变量
    从图18.4.1可以看到参数的偏移值
    */
    movl   a6@(16),d7   

  • 图18.4.1如下

三、堆栈中的参数次序

  • 被调用函数使用帧指针(a6)加一个偏移量来访问参数
  • 当参数以反序压入到堆栈时,参数列表的第1个参数便位于堆栈中这堆参数的顶部,它距离帧指针的偏移量是一个常数。任何一个参数距离帧指针的偏移量都是一个常数,这和堆栈中压入多少个参数并无关系
  • 如果参数以相反的顺序(正序)压入到堆栈中,第1个参数距离帧指针的偏移量就和压入到堆栈的参数数量有关

四、最终的堆栈布局

    d = b - 6;
    return a + b + c;
}
    movl   a6@(12),d0   //把第2个参数复制到d0,即参数b
    subql  #6,d0    //将d0中的值减去6,即 b - 6
    movl   d0,a6@(-4)   //将结果存储到布局变量d,即 d = b - 6
    movl   a6@(8),d0    //将a的值放到d0
    addl   a6@(12),d0   //将a6@(12)的值与d0相加,即 a + b
    addl   d7,d0    //将d7中的值与d0相加,即 a + b + c,这个运行结果存放在d0中,之后return的时候会用到
    ------------------------------------从下面开始为函数跋
    moveml a6@(-8),#0x80
    unlk   a6
    rts
  • 从上面可以看出,d0的作用,其实d0主要有两个作用,这两个作用也是它不能用于存放寄存器变量的原因之一:
    • 计算过程中的“中间结果暂存器”或临时位置,即上面汇编代码中看到的
    • 存返回值,后文将看到

五、函数跋

    /*
    用于恢复以前被保存的寄存器值
    如图18.4.1所示,a6@(-8)指向寄存器旧值
    将这个旧值恢复到d7中
    */
    moveml a6@(-8),#0x80    
    unlk   a6   //把a6的值复制给堆栈指针SP,并把从堆栈中弹出的a6旧值装入a6中,这时SP指向原a6@(0)位置
    rts     //通过把返回地址从堆栈中弹出到程序计数器,从而从该函数返回,这时SP指向原a6@(4)位置
  • 下面执行流将从调用程序的地点继续。注意此时堆栈尚未被完全清空(参数还没清空)
    i2 = func_ret_int( 10, i1, i10 );
    /*
    把12加到堆栈指针,在此之前SP指向原a6@(4),在此之后SP指向原a6@(16)
    三个参数值从堆栈中弹出
    在此之前,被调用函数并没有从堆栈中完全清除它的整个堆栈帧,参数还留在那里等待调用函数清除
    */
    lea    sp@(12),sp
    movl   d0,d6    //把d0的值复制到d6中,即把d0的值复制到变量i2

六、返回值

  • 函数跋并没有使用d0,因此它依然保存着函数的返回值
  • 函数返回一个值时把它放在d0,这是d0不能用于存放寄存器变量的另一个原因


  • 下一个被调用的函数返回一个double值
    db1 = func_ret_double();
    c1 = func_ret_char_ptr( c1 );
    /*
    这个函数没有任何参数,所以没有什么东西被压入到堆栈中
    */
    jpsr   _func_ret_double
    /*
    double长度是8个字节,无法放入一个寄存器中
    因此要返回这种类型的值,必须同时使用d0和d1寄存器
    */
    movl   d0,a6@(-48)  
    movl   d1,a6@(-44)
    
    //函数_func_ret_char_ptr说明了指针变量是如何返回的
    pea    a5@  //将参数c1压入堆栈
    jpsr   _func_ret_char_ptr   //跳转子程序
    addqw  #4,sp    //将参数c1弹出堆栈
    movl   d0,a5    //将返回值复制到a5,即变量c1

18.1.7 表达式的副作用

//尽管这个函数存在一个巨大错误,但仍然能在某些机器上正确地运行
int
erroneous( int a, int b )
{
    int x;
    
    x = a + b;
    //这里返回语句没有指定返回哪个变量的值
    return;
}
  • 这个函数实际上可以返回计算结果的值
  • d0被用于计算x,并且由于这个表达式是最后进行求值的,所以当函数结束时d0仍然保存了这个结果值
  • 这个函数很意外地调用函数返回了正确的值
  • 若在return语句之前加入a + 3;,那么d0倍修改,就会返回错误的值

18.2 C和汇编语言的接口

  • 为了编写能够调用C程序或被C程序调用的汇编语言,必须遵守的规则
    • 汇编程序中的名字必须遵循外部标识符的规则,例如以一个下划线开始
    • 汇编程序必须遵循正确的函数调用/返回协议
  • 为了编写一个由C程序调用的汇编程序
    • 保存任何你希望修改的寄存器(除d0、d1、a0和a1之外)
    • 参数值从堆栈中获得,因为调用它的C函数把参数压入到堆栈中
    • 如果函数应该返回一个值,它的值应该保存在d0中(在这种情况下,d0不能进行保存和恢复)
    • 在返回之前,函数必须清除任何它压入到堆栈中的内容
  • 在一个由C程序调用的汇编程序里,你必须访问C函数放置在那里的参数
  • 以下为C程序调用汇编程序的例子
//C代码
sum = sum_three_values( 25, 14, -6 );
//汇编代码
    .text
    
    .globl  _sum_three_values
_sum_three_values:
    movl    sp@(4),d0   //第1个参数复制到d0中
    addl    sp@(8),d0   //d0与第2个参数相加
    addl    sp@(12),d0  //d0与第2个参数相加
    rts                 //函数返回

18.3 运行时效率

  • 虚拟内存是由操作系统实现的,它需要把程序的活动部分放入内存并把不活动的部分复制到磁盘中,这样就允许系统允许大型的程序
  • 由于虚拟内存,随着程序的增大,它的执行效率逐渐降低

18.5 警告的总结

  • 是链接器而不是编译器决定外部标识符的最大长度
  • 你无法链接由不同编译器产生的程序

更详细的C函数栈帧操作流程,可以看《C函数栈帧》



Content