< C 和指针 >
一、快速上手
1、要从逻辑上删除一段C代码,最好的办法是使用#if指令,即:
#if 0
statements
#endif
在#if和#endif之间的程序段就可以有效的从程序中删除,即使这段代码之间原先存在注释也无妨。
2、如果有一些声明用于不同的源文件,可以再单独的文件中编写这些声明,然后使用#include指令把这个文件包含到需要使用这些声明的源文件中。
3、函数原型声明:告诉编译器以后将在源文件中定义的函数的特征;每个原型声明以一个类型名开头,表示函数返回值的类型,跟在返回值类型名后面的是函数的名字,在后面是函数期望接收的参数。
无返回值的函数称为过程。
4、假如程序的源代码由几个源文件所组成,那么使用该函数的源文件都必须写明该函数的原型。把原型放在头文件中并使用#include指令包含他们,可以避免由于同一个声明的多份拷贝而导致的维护性问题。
5、参数传递分为传址调用和传值调用
被调用函数无法修改调用函数以传值形式传递给它的参数;
被调用可以修改调用函数以传址形式传递给它的参数;例如指针变量传递和数组名传递
6、gets()在输入字符串时会丢弃换行符,并在该行的末尾存储一个NUL字节(一个NUL字节是指字节模式全0的字节),返回值为输入字符串的首地址。
7、C编译器一般不进行对数组下标的检查,如果读取的数据多于数组的最大标号,那么多出来的值就会存放在紧随数组之后的内存位置,这样就会破坏原先存储在这个位置的数据。
8、puts()是gets()函数的输出版本,它把指定的字符串写到标准输出并在末尾天上一个换行符。
9、EOF是一个整型值,将存放字符型数据的变量ch声明为int型,可以防止从输入读取的字符意外的被解释成EOF(?),但同时,也就意味着接收字符的ch必须足够大,足以容纳EOF,这就是ch使用整数值的原因。
10、编程提示的总结:
(1)在#include文件中放置函数原型
(2)使用#include指令避免重复声明
(3)使用#define指令给常量值取名
(4)使用下标前先检查它们的值
(5)始终要进行检查,确保数组不越界
二、基本概念
1、ANSI C的任何一种实现中,存在两种不同的环境:
第一种是翻译环境:在这个环境里,源代码被转换为可执行的机器指令
第二种是执行环境:用于实际执行代码
两种环境不必在同一台机器上
交叉编译器:就是在一台机器上运行,但它所生成的可执行代码运行于不同类型的机器上。
2、翻译
翻译有几个步骤组成:
首先,组成一个程序的若干个源文件通过编译过程分别转换为目标代码;
然后,各个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序;
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它也可以搜索程序个人的程序库,将其中需要使用的函数也链接到程序中。
3、编译
首先由预处理器处理,这个阶段,预处理器在源代码上执行一些文本操作。
然后,源代码经过解析,判断它的语句的意思。第2个阶段是产生绝大多数错误和警告的信息的地方。随后,产生目标代码。
目标代码是机器指令的初步形式,用于实现程序的语句。如果我们在编译程序的命令行中加入进行优化的选项,优化器就会对目标代码进一步进行处理,使他的效率更高
4、编译并链接一个完全包含于一个源文件的C程序,中间会产生*.o的目标文件,但它在链接过程完成后会被删除。
5、编译并链接几个C文件,当源文件超过一个时,目标文件便不会删除,这就允许你对程序进行修改后,只对那些进行改动的源文件进行重新编译。
在UNIX系统中,C编译器被称为cc,
cc program.c这个命令会产生一个称为a.out的可执行程序,中间会产生program.o的目标程序,但是链接完成后就被删除
cc main.c sort.c lookup.c----->cc -o name main.o sort.o lookup.o 意思是可以是链接器把可执行程序保存在name文件中,而不是a.out
6、执行
首先、程序必须载入到内存中,在宿主环境中,这个任务由操作系统完成,那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到 初始值。在独立环境中,程序的载入必须由手工安排,也可能是通过把可执行代码置入只读内存中来完成。
然后,程序执行便开始。通常一个小型的启动程序与程序链接在一起,它负责处理一系列日常事务,如收集命令行参数以便使程序能够访问它们。接着,便调用main函数
然后,便开始执行程序代码;程序将使用一个运行时堆栈,它用于存储函数的局部变量和返回地址;程序同时也可以使用静态内存,存储静态内存中的变量在程序的整个执行过程中将一直保留他们的值。
最后一个阶段:程序的终止;正常终止就是main函数返回;或者是用户终止执行;或者是执行过程中出现错误而自行中断。
7、标识符的长度没有限制,但是标准允许编译器忽略第31个字符以后的字符。
虽然一个源文件可以包含超过一个的函数,但是每个函数都必须完整的出现在同一个源文件中。
绝大部分注释都是成块出现的,这样他们从视觉上在代码中很突出,读者就可以很容易找到和跳过他们。
总结:
一个C程序的源代码保存在一个或多个源文件中,但是一个函数只能完整的出现在同一个源文件中。把相关的函数放在同一个文件内是一种好策略。每个源文件都分别编译,产生对应的目标文件。然后,目标文件被链接到一起,形成可执行程序。编译和最终运行 程序的机器有可能相同,有可能不同。
程序必须载入到内存中才能执行。在宿主式环境中,这个任务由操作系统完成。在自由式环境中,程序常常永久存储在ROM中;经过初始化的静态变量在程序执行前能获得他们的值。你的程序执行的起点是main函数。绝大多数环境使用堆栈来存储局部变量和其他数据。
三、数据
1、长整型至少应该和整型一样长,而整型至少应该和短整型一样长。
2、short int 至少16位,long int至少32位,至于缺省的int型是16位还是32位,或者是其他值,由编译器设计者决定。通常这个选择的缺省值是这种机器最为自然(高效)的位数;例如,如果某种机器的环境的字长是32位,而且没有什么指令能够更高效的处理更短的整型值,他可能就把这3个整型值都设定为32位。
3、头文件limits.h说明了各种不同的整型类型的特点。
4、枚举类型:符号名被当做整型常量处理、声明为枚举类型的变量实际上是整型类型。
5、变量的值存储在计算机的内存中,每个变量都占据一个特定的位置,每个内存位置都由地址唯一确定并引用,指针只是地址的另外一个名字;
指针变量就是存放另外一个内存地址的变量。
6、字符串都是以NUL字节结尾的零个或多个字符,即使空字符串,依然存在作为终止符的NUL字节。
在程序中使用字符串常量会生成一个指向字符的常量指针;当一个字符串常量出现在一个表达式中,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。
因此你可以将一个字符串常量赋值给一个指向字符的指针变量,后者指向存放这些字符的地址,但是你不能将字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
7、signed关键字一般只用于char类型
8、C编译器并不检查对数组下标的引用是否符合数组的合法范围内;一个良好的经验法则是:
如果下标值时从那些已知是正确的值计算而来的,那么无需检查它的值,如果一个用作下标的值时根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保他们位于有效的范围之内。
9、函数如果不显式的声明返回值的类型,它会默认返回整型。
10、标识符的作用域就是程序中该标志符可以被使用的区域。
编译器可以确认4中不同类型的作用域-----文件作用域、代码块作用域、函数作用域、原型作用域
(在C primer Plus中,认为有3种作用域,除了没有函数作用域之外)
(1)代码块作用域:位于一对花括号之间的所有语句称为一个代码块,任何在代码块的开始位置声明的标识符都具有代码块作用域。
如果内层代码块有一个标识符名字和外层代码块的一个标识符相同,内层的标识符就会隐藏外层的标识符----在内层代码块中无法通过标识符名字访问外层代码块的标识符。
注意:应该尽量避免内层代码和外层代码出现相同的标识符。
(2)文件作用域
在所有代码块之外声明的变量具有文件作用域,它表示这些变量从声明处开始至源文件结束处都是可以访问的。
若是在头文件中声明变量,并通过#include指令包含到其他文件中,这就好像这个变量直接写在文件中一样;
(3)原型作用域
只适应于在函数原型中声明的参数名;
函数原型声明中可以不必出现变量名,如果出现变量名,你可以给取任意名字,他们不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。
(4)函数作用域
是适用于语句标签,语句标签用于goto语句,基本上函数作用域可以指:一个函数中的所有语句标签必须唯一。
*****************************************************************************************************************************************************************************************
2013年2月4日更新
*****************************************************************************************************************************************************************************************
11、链接属性
标识符的链接属性决定如何处理在不同文件中出现的标识符,标识符的作用域与它的链接属性有关,但是并不相同。
链接属性一共有3种------external即外部链接、internal即内部链接、none即无链接
无链接属性的标识符总是被当做单独的个体,也就是说该标志符的多个声明被当做独立不同的实体;
属于internal链接属性的标识符在同一个源文件内的所有声明中都指同一个实体,但位于不同源文件中的多个声明则分属不同的实体;
最后,属于external链接属性的标识符不论声明多少次、位于几个源文件中都表示同一实体。
(1)当extern关键字用于源文件中的一个标识符的第一次声明时,它指定该标识符具有external链接属性,但是,如果它用于该标识符的第2次或以后的声明时,它并不会更改由第一次声明所指定的链接属性。例如:
static int i;<-------------------1
int func()
{
extern int i;<-----------------2
}
1指定变量i的链接属性是静态内部链接,声明2不会改变1所指定的变量i的链接属性。
12、存储类型
变量的存储类型是指存储变量值的内存类型;变量的存储类型决定变量何时创建、何时销毁、以及它的值能保持多久。
有3个地方可以存储变量:普通内存、运行时堆栈、硬件寄存器
变量的缺省 存储类型取决于它的声明位置。
(1)凡是在任何代码块之外声明的变量总是存储于静态内存中,这类变量称为静态变量,静态变量在程序运行之前创建,在程序的整个执行期间始终存在
(2)在代码块内部声明的变量的缺省存储类型是自动的,也就是说它存储于堆栈中,称为自动变量;在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。如果你反复调用这些代码块,这些变量就会反复创建和销毁,他们存储的位置可能与上一次存储的位置相同,可能不同。
在代码块内部声明的变量,如果给它加上关键字static,可以使它的存储类型从自动变为静态,但是修改变量的存储类型并不表示修改该变量的作用域。
(3)关键字register可以用于自动变量的声明,提示他们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常寄存器变量效率要比存储于内存中的变量的访问效率高,但是编译器不一定将所有的register声明的变量存储于寄存器,可能只是选取前几个,其余按自动变量处理。
一旦一个变量称为寄存器变量,机器并不像你提供寄存器变量的地址。
初始化:
静态变量如果不显式的指定其初始值,静态变量将初始化为0;
自动变量在链接时,链接器无法判断自动变量的存储位置,因为函数的局部变量在函数的每调用都可能占据不同的位置,基于这个理由,自动变量没有缺省值;
因此上面原因有四个影响:
首先,是自动变量的初始化相比赋值语句效率并无提高;
其次,每次调用都会重新初始化;
再次,你可以使用任何表达式作为初始化值;
第四,如果自动变量不初始化,它的值时不确定的
13、static关键字
(1)当static用于函数定义时,或者用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不变;
(2)当static用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量改为静态变量,但变量的链接属性和作用域不受影响;
对于函数来说,存储类型是静态存储,因为代码总是存储在静态内存中。
四、语句
1、break:跳出当前循环层,即终止本层循环,执行完break语句后,执行的下一条语句就是循环正常结束后应该执行的那条语句。
2、continue:终止当前循环层的本次循环,开始下一次循环。
3、当你需要循环体至少执行一次时,选择do
4、switch语句在执行中遇到break语句,执行流就会立即跳到语句列表的末尾;break语句的实际效果是把语句列表分为不同的部分;这样,switch就能按照更为传统的方式工作。在最后一条语句的后面加上一条break语句,在实际运行时没有什么效果,但是这样可以为了以后维护方便,以防有人在switch中再添加一个case,可以避免出现忘记添加break语句。
如果表达式的值与所有的case标签都不匹配,你又不想忽略不匹配所有case标签的表达式值时,可以再语句列表中添加一条default子句,这样做是一个好习惯,因为可以检测到任何非法值;
5、goto语句慎用,可以再跳出多层嵌套循环时使用。
6、编程提示
(1)在一个没有循环体的循环中,用一个分号表示空语句,并让它独占一行;
(2)for循环的可读性比while循环强,因为它把用于控制循环的表达式收集起来放在一个地方
(3)每个switch语句中都是用default子句;
五、操作符和表达式
1、右移位操作存在一个左移位不曾面临的问题:从左边移入新位时,有两种选择:一是逻辑移位,左边移入的位用0填充;一是算术移位,左边移入的位用原先该值的符号位决定。
无符号值执行的移位都是逻辑移位;对于有符号值,到底是采用逻辑移位还是算术移位取决于编译器;
2、!操作符实际产生一个整形结果:0或者1
3、sizeof操作符判断它的操作数的类型的长度
4、> >= < <= !===这些关系操作符产生的结果都是一个整型值,而不是布尔值。如果两端的操作数符合操作符指定的关系,表达式的结果是1,如果不符合,表达式的结果是0;
5、&&操作符的做操作数总是首先进行求值,如果他的值为真,然后紧接着对右操作数进行求值,如果左操作数的值为假,那么右操作数便不再进行求值,因为整个表达式的值肯定是假的;
||操作符也有相同的特点,首先对左操作数进行求值,如果它的值时真,右操作数便不再求值,因为整个表达式的值此时已经确定
以上这种行为通常称为短路求值
6、0是假,任何非0值都是真
7、C的整形算术运算总是至少以缺省整形类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整形,这种转换被称为整形提升。
8、寻常算术转换:
long double
double
float
unsigned long long int
long long int
unsigned long int
long int
unsigned int
int
unsigned char
char
某个操作数在上面表中排名较低时,先转化成表达式中排名最高的类型,然后一起运算
9、左值是一个表达式,它可以出现在赋值符的左边,它表示计算机内存中的一个位置;右值表示一个值,所以只能出现在赋值符的右边。
11、位操作符:
&:与
|:或
^:异或
六、指针
1、名字和内存位置之间的关联并不是硬件提供的,它是由编译器为我们实现的,所有这些变量给了我们一种更方便的方法记住地址---硬件仍然通过地址访问内存位置
2、NULL指针:
(1)标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西,要使一个指针变量为NULL,可以给它赋值为0;之所以选0是因为这是源代码的一种约定,就机器内部而言,NULL指针的实际值可能与此不同;在这种情况下,编译器将负责零值和内部值之间的翻译转换。
(2)让函数返回两个独立的值:首先是状态值,用于提示程序是否执行成功;其次是个指针,当提示执行成功时,就指向想要的结果。
(3)对一个指针解引用可以获得它所指向的值,但对一个NULL指针解引用是非法的;因此在对指针变量解引用之前,首先保证它并非NULL指针。
(4)如果你已经知道指针变量将被初始化为哪一个地址,就把它初始化为该地址,否则就把它初始化为NULL;
3、为什么诸如&a的表达式不能当左值使用,因为当表达式&a进行求值时,它的结果应该存储在计算机的什么地方呢?它肯定会位于某个地方,但你无法知道它位于何处,这个表达式并未标识任何机器内存的特定位置,所以它不是一个合法的左值。
4、指针变量运算
指针变量加上一个整数的结果是另一个指针变量;那它指向哪呢?当一个指针变量和一个整数量执行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整,这个合适的大小就是指针变量指向数据类型的大小,“调整”就是把整数值和“合适的大小”进行相乘。
同时指针变量加减一个整数后,所指向的数据类型不变。
(1)指针变量的算术运算只限于两种形式:
指针变量 + 整数
指针变量 - 整数
并且这类表达式的结果类型也是指针变量,这种形式适用于指向数组元素的指针变量或者是指向malloc函数动态分配获得的内存;
如果对指针执行加法或者减法运算之后,结果指针变量所指的位置位于数组第一个元素的前面或者是数组最后一个元素的后面,那么其值时未定义的,若是此时对这个指针变量进行解引用则可能失败。
(2)指针变量 - 指针变量
只有当两个指针变量都指向同一个数组中的元素时,才允许从一个指针变量减去另一个指针变量;
减法运算的结果是两个指针变量在内存中的距离,以数组元素类型的长度为单位,而不是以字节为单位,因为减法运算的结果将除以数组元素类型的长度;
例如
int arr[n];
int* p1 = arr[4] ;
int* p2 = arr[7];
则p2 - p1 = 7-4 = 3;p1 - p2 = -3;
如果两个指针变量所指向的不是同一个块内存中的元素,则想减是无意义的;
注意:越界指针和指向未知值的指针变量时两个常见的错误根源;
(3)一个例子
for( vp = &values[N_VALUES - 1] ; vp >= &values[0] ; vp-- )
当这个循环执行到 vp = &values[0]时,vp还要减1,这样vp就会指向数组values[]第一个元素前面的位置上去了,所以要尽量避免这种。
5、总结
声明一个指针变量并不会自动分配任何内存,在对指针执行间接访问之前,指针变量必须进行初始化:或者使它指向现有的内存,或者给它分配动态内存。对未初始化的指针变量执行间接访问操作时非法的
NULL指针就是不指向任何东西的指针,它可以赋值给一个指针,用于表示那个指针并不指向任何值,对NULL指针执行间接访问操作的后果因编译器而异,两个常见的后果分别是返回内存位置0的值以及终止程序。
指针运算只有作用于数组中时,其结果才是可以预测的,对任何非指向数组元素的指针变量执行算术运算都是非法的,因为常常很难被检测到。
如果一个指针减去一个整数后,运算结果产生的指针变量所指向的位置在数组第一个元素之前,那么它也是非法的。
如果指针变量在进行加上一个整数后,结果指针变量指向数组最后一个元素后面的那个内存位置仍是合法的,但是不能对这个指针变量进行间接访问操作,不过再往后就不合法了。
七、函数
1、return语句:当执行流到函数定义的末尾时,函数就将返回,也就是说执行流返回到函数被调用的地方;return语句允许你从函数体的任何位置返回,并不一定从函数体的末尾。
没有返回值的函数称为过程,这种函数在声明时应该把函数的类型声明为void。
若是有返回值,则return语句必须返回一个值,这类函数的return语句必须包含一个表达式,通常表达式的类型就是函数声明的返回值类型。
2、调用函数时向编译器提供函数信息的方法有两种:
一种是在调用函数前面就出现该函数的定义
另一种是在调用之前提供被调函数的函数原型声明
使用函数原型最方便且最安全的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就是用#include指令包含该文件,这个技巧避免了键入错误函数原型的可能性。
头文件包含函数声明的好处有:
(1)头文件中的函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易的多。
(2)现在的函数原型只书写一次,避免多次拷贝出现不匹配的现象;
(3)如果函数的定义进行了修改,我们只需要修改该头文件中的原型即可,并且重新编译所有包含了该原型的源文件即可;
3、当程序调用一个无法见到函数原型的函数时,编译器变认为该函数返回一个整数值。对于那些并不返回整数值的函数来说,这种认定可能会引起错误;
4、参数传递的两个规则:
(1)传递给函数的标量参数是传值调用的
(2)传递给函数的数组参数在行为上就像它们是通过传址调用的那样。
5、递归函数就是直接或间接调用自身的函数。
递归调用的变量时创建于运行时堆栈上的,以前调用的函数的变量仍保留在堆栈上,但它们被新函数的变量所掩盖,因此是不能访问的。
递归所需要的两个特性:
(1)存在限制条件,当符合这个限制条件时递归便不再继续
(2)每次递归调用之后越来越接近这个限制条件
*****************************************************************************************************************************************************************************************
2013年2月5日更新
*****************************************************************************************************************************************************************************************
八、数组
1、数组名的值是一个指针常量,也就是数组第一个元素的地址,它的类型取决于数组元素的类型,如果他们是int型,则数组名就是指向int型数据的常量指针,如果他们是其他类型,那么数组名的类型就是指向指向其他数据类型的常量指针。
注:数组和指针具有一些完全不同的特征:例如:数组具有确定数量的元素,而指针只是一个标量值;编译器用数组名记住这些属性,只有当数组名在表达式中使用时,编译器才会为他产生一个指针 常量。
(1)数组名的值是一个指针常量,而不是指针变量;你不能修改指针常量的值:指针常量所指向的是内存中数组的起始位置,如果修改这个指针常量,唯一可行的操作就是把整个数组移动到内存的其他位置。但是,在程序完成连接后,内存中数组的位置时固定的,所以当程序运行时再想移动数组就为时已晚了,因此数组名是一个指针常量。
(2)只有在两种场合下,数组名并不用指针常量来表示:一是数组名作为sizeof操作符,sizeof返回整个数组的长度,而不是只想数组的指针的长度;二是数组名做单目操作符&的操作数时,取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。
不能使用赋值符把一个数组的所有元素复制到另一个数组,必须使用一个循环,每次复制一个元素;
不能对数组进行集体赋值,只有在数组进行初始化赋值才可以进行集体赋值。
2、下表引用操作符是一对方括号,下标引用操作符接受两个操作数:一个数组名和一个索引值。事实上下标引用并不仅限与数组名。C的下标值总是从0开始,并且不会对下标值进行有效性检查。除了优先级不同之外,下标引用操作和间接访问表达式是等价的,这里是它们的映像关系:
array[ 下标 ]
*(array + ( 下标 ))
在使用下标引用的地方,你可以使用对等指针表达式来代替;在使用上面这种形式的指针表达式的地方,你也可以使用下标表达式来代替。
&array[ 下标 ]
array + 下标
上面两个表达式等价
例子:
int array[10];
int *ap = array + 2;
以下各标识符的意思:
ap :即array+2或者&array[2]
*ap :即*(array+2)或者array[2]
ap[0] :即*(ap+0)或者*ap或者array[2]
ap+6 :即&ap[6]或者array+8或者&array[8]
*(ap+6) :ap[6]或者array[8]或者*(array+2+6)
*ap+6 :即array[2]+6
&ap :是合法的,但是相对于array的位置。
ap[-1]:合法,即*(array+2+(-1))=*(array+1)=array[1]
ap[9]:看似合法,但是由于超出了数组的右边界,所以是非法的。
注:编译器不检查数组下标是否正确;
2[array] = *(2 + array) = *(array + 2) = array[2]:太不可思议了,这样竟是合法的。
*****************************************************************************************************************************************************************************************
2013年2月6日更新
*****************************************************************************************************************************************************************************************
3、指针的效率
(1)下标更具可读性,但是指针的效率更高,因为指针变量进行自增或自减时,当中对地址运算涉及到的乘法运算只在编译时执行一次,而实际运行时并不执行乘法运算,而下标要根据数组元素的数据类型以及下标计算当前的地址,所以效率较低;
当然前提是指针能够正确的使用,不要为了效率上的细微差别而影响程序的可读性
(2)当你根据某个固定数目的增量在一个数组中移动式,使用指针变量将比使用下标产生效率更高的代码。当这个增量是1并且机器具有地址自动增量模型时,这点表现更为突出。
(3)声明为寄存器变量的指针变量通常比位于静态内存中和堆栈中的指针效率更高
(4)如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的计数器。
(5)那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
4、数组和指针的比较
数组和指针并不是相等的。
例如:
int a[5];
int *b;
他们都具有指针值,尽管它们都可以进行间接访问和下标引用操作,但是还是存在很大区别的:
(1)声明一个数组时,编译器将根据声明所指定的元素个数为数组保留内存空间,然后再创建数组名,它的值是一个 常量,指向这段空间的起始位置;
声明一个指针变量时,编译器只为指针变量本身保留内存空间,它并不为任何整数值分配内存空间;而且指针变量并未初始化为指向任何现有内存空间,如果它是一个自动变量,它根本不会被初始化,把这两个声明用图的方法表示,你可以发现它们之间的不同:
a
? |
因此,上述声明后,使用表达式*a是合法的,使用表达式*b是非法的。*b将访问内存中某个不确定的位置,
(2)表达式b++是正确的,但是a++是错误的,因为a是指针常量
5、作为函数参数的数组名
所有的函数参数都是通过传值方式传递的;当然,如果你传递了一个指向某个变量的指针变量,而该函数对该指针变量进行间接访问操作,那么就可以修改该指针变量,那么指针变量参数的传值调用又体现在哪里呢,实际上,传递给函数的是指针变量参数一份拷贝,所以函数可以自由的操作它的指针变量形参,而不必担心修改对应的指针变量实参,实际上指针变量形参修改的是实参指针变量的指向的内容,而不是修改指针变量存放的地址。
注:
如果传递一个数组名,那么形式参数应该声明为一个指针变量还是一个数组呢?
使用指针变量更为准确,因为实际参数实际上是一个指针变量,而不是数组。
6、初始化
(1)静态变量在程序编译时初始化一次,每次使用都是上次使用后的数值。若静态变量没有初始化则自动初始化为0;自动变量则是每次调用时重新初始化。
(2)不完整的数组初始化,则没有初始化的元素被初始化为0;
(3)如果数组初始值列表经常改变,可以在初始化时忽略数组的长度,不写;
(4)初始化字符串数组可以使用如下形式:
char memssage[] = "hello";
当用于初始化一个字符串数组时,它就是一个初始化列表;在其他任何地方,它都表示一个字符串常量。
char message1[] = "hello";
char *message2 = "hello";
这两个初始化看上去很像,但是他们具有不同的含义;前者初始化为一个字符数组的元素,而后者则是一个真正的字符串 常量。这个指针变量被初始化为指向这个字符串常量的存储位置。
(5)多维数组的元素存储顺序按照最右边的下标优先变化的原则,则称为行主序。
由于内存中数组元素的实际存储方式是不变的,所以对于array[m][n]来说,在某些上下文环境中,按照m行n列 或者n行m列来说都是对的。
(6)一维数组名的值是一个指针常量,它的类型是指向元素类型的指针,它指向数组的第一个元素的首地址。
(7)下标引用实际上只是间接访问表达式的一种伪装形式;
int array[3][10];
array的类型是“指向包含10个整形元素的数组的指针”;它的值是指向包含10个整形元素的第一个子数组。
array+1也是一个“指向包含10个整形元素的数组的指针”,指向array的令一行;原因是1这个值根据包含10个整形元素数组的长度进行调整,所以它指向array的下一行。
所以*(array + 1)事实上标识了一个包含10个整型元素的子数组,表示指向array数组第二行第一个元素的首地址。数组名的值是个常量指针,它指向数组的第一个元素,在这个表达式中也是如此。类型是指向整型的指针。
*(array + 1)+ 5则表示指向第二行第五个元素的首地址;
*(*(array + 1)+5)则是表示指向第二行第五个元素。
注:array[4,3]:逗号操作符首先对第一个表达式求值,但随即丢弃这个值,最后的结果是第2个表达式,因此前面这个表达式和下面这个表达式是相等的:
array[3];
下标引用高于间接访问。
int (*p)[10]:p是一个指向拥有10个整型元素的数组的指针变量;当你把p与一个整数相加时,该整数首先根据10个整型值的长度进行调整,然后再执行加法,所以我们可以使用这个指针一行一行的在array中移动。
(8)作为函数参数的多维数组名的传递方式和一维数组名相同-------实际传递的是指向数组第一个元素的指针变量,但是两者的区别在于,多维数组的每个元素本身是另一个数组,编译器需要知道它的维数,以便为函数形参的下标表达式进行求值。
例如:一维数组的传递可以是:
void func1(int vector[]);
或者
void func1(int* vector);
二维数组可以是:
void func2(int arr[][10]);
或者
void func2(int (*arr)[10]):
这里的关键在于编译器必须知道第2个及以后各维的长度才能对各下标进行求值,因此在原型中必须声明这些维的长度,第一维的长度不需要,因为在计算下标值时用不到它。
注:
在编写一维数组形参的函数原型时,你既可以把它写成数组的形式,也可以把它写成指针的 形式。
但是对于多维数组,只有第1维可以进行数组或者指针形式的选择,尤其是,把func2写成下面这样的原型是不正确的:
void func2(int **arr);这是错误的
这里是把arr声明为一个指向整型指针变量的指针,它和指向整型数组的指针并不是一回事。
(9)多维数组的初始化:
多维数组初始化加括号的好处:
一是利于显示数组的结构;
二是每个子初始化列表都可以省略尾部的几个初始值,同时,每一维的初始化列表各自都是一个初始化列表。
注:
多维数组中,只有第1维才能根据初始化列表缺省的提供。剩余的几个维必须显式的写出,这样编译器就能推断出每个子数组维数的长度。
(10)指针数组
下表引用的优先级高于间接访问;
int *arr[10];
arr是个数组,每一个数组元素都是指针变量
下面有三个声明:
第1种声明:
char const keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
}
这种声明创建了一个指针数组,每个指针元素都初始化为指向各个不同的字符串常量;
第2种声明:
char const keyword[][9] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
}
这种声明是创建一个矩阵,每一个行的长度都刚好可以容纳最长的关键字,包括字符串结束符,如下所示:
比较:
矩阵看上去效率低一些,因为它的每一行的长度都被固定为刚好能容纳最长的关键字,但是,它不需要任何指针;
指针数组本身也要占用空间,但是每个字符串常量占据的空间只是它本身的长度。
哪个方案更好一些,取决于你希望存储的具体字符串,如果他们长度都差不多,那么矩阵形式更紧凑一些,因为无需使用指针,但是如果各个字符串 的长度千差万别,那么指针数组更紧凑一些,他取决于指针所占用的空间是否小于每个字符串都存储于固定长度的行所浪费的空间。‘
第3种声明
char const *keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
NULL
}
这里在表的末尾增加一个NULL指针,这个NULL指针使函数在搜索这个表时能够检测到表的结束,而无需预先知道表的长度。
总结:
(1)绝大多数表达式中,数组名的值是指向数组第一个元素的指针,这个规则只有两个例外,sizeof返回整个数组所占用的字节而不是一个指针所占用的字节;单目操作符&返回一个指向数组的指针,而不是一个指向数组第一个元素的指针的指针。
(2)除了优先级不同之外,下标表达式arr[value]和间接访问表达式*(arr + (value) )是一样的。因此,下标不仅可以用于数组名,也可以用于指针表达式中,不过这样一来,编译器很难检查下标的有效性。指针表达式可能比下标表达式效率更高,但是下标表达式绝不可能比指针表达式效率更高。
(3)指针和数组并不相等;数组的属性和指针的属性大相径庭,当我们声明一个数组时,他同时也分配了一些内存空间,用于容纳数组元素。但是,当我们声明一个指针时,它只分配了容纳指针本身的空间。
(4)用数组名作为函数参数传递时,实际传递给函数的是一个指向数组第1个元素的指针;函数多接受到的参数实际上是原参数的一份拷贝,所以函数可以对其进行操作而不会影响实际的参数,但是,对指针参数执行间接访问操作允许函数修改原先的数组元素;数组参数既可以声明为数组,也可以声明为指针;这两种声明形式只有当它们作为函数的形参时才是相等的。
(5)数组也可以用初始值列表进行初始化,初始值列表就是由一对花括号包围的一组值;静态变量(包括数组)在程序载入到内存时得到初始值。自动变量(包括数组)
每次当执行流进入它们声明所在的代码块时都要使用隐式的赋值语句重新进行初始化。如果初始值列表包含的值的个数少于数组元素的个数时,数组最后几个元素就用缺省值进行初始化;如果数组的长度在初始化时未给出,编译器将是这个数组的长度设置为刚好能容纳初始值列表中所有值的长度。
(6)多维数组实际上是一维数组的一种特型,就是它的每个元素本身也是一个数组。多维数组数组中的元素根据行主序进行存储,也就是最右边的下标率先变化;
多维数组名的值是一个指向它第一个元素的指针,也就是一个指向数组的指针。对该指针进行运算将根据它所指向数组的长度对操作数进行调整。
多维数组的下表引用也是指针表达式,当一个多维数组名作为参数传递给一个函数时,它所对应的函数形参的声明必须显式指明第2维的长度;
在多维数组的初始值列表中,只有第1维的长度会被自动计算出来。
源代码的可读性几乎总是比程序的运行时效率更为重要;
多维数组的初始值列表中使用完整的多层花括号能提高可读性;
只要有可能,函数的指针形参都应该声明为const;
*****************************************************************************************************************************************************************************************
2013年2月26日更新
*****************************************************************************************************************************************************************************************
九、字符串、字符和字节
1、字符串就是一串零个或多个字符,并且以一个位模式全为0的NUL字节结尾。(位模式:计算机所有二进制的0、1代码所组成的数字串)
NUL字节是字符串的终止符,但它本身不是字符串的一部分,所以字符串的长度并不包括NUL字节,但是在为字符串分配存储空间时,要为NUL字节预留出一个存储位置。
2、字符串长度
size_t strlen(char const *string);
返回值:类型为size_t的值,是一个无符号整数类型;
注:
(1)在表达式中使用无符号数可能导致不可预料的结果;例如:
if(strlen(x) >= strlen(y))
if(strlen(x) - strlen(y) >= 0)
事实上,上述两式并不相等,第一句是将按照你预想的工作;第二句的结果将永远是真,因为strlen的结果是个无符号数,所以
strlen(x)-strlen(y)也将是个无符号数,而无符号数绝不可能是负数的。
但是如果你将strlen(x)和strlen(y)的返回值强制转化为int型,则可以消除这个问题。
(2)标准库有时使用汇编语言实现的,目的就是为了充分利用某些机器所提供的特殊的字符串操纵命令
寻找一种更好的算法比改良一种差劲的算法更有效率,复用已经存在的软件比重新开发一个效率更高。
2、不受限制的字符串函数
最常用的字符串函数都是不受限制的,就是说它们只是通过寻找字符串参数结尾的NUL字节来判断它的长度;这些函数一般都指定一块内存用于存放结果字符串,所以在使用这些字符串时,程序员必须保证结果字符串不会溢出这块内存。
(1)char* strcpy( char* dst, char const * src);
函数作用:把参数src字符串复制到dst参数;如果src和dst在内存中出现 重叠,结果是未定义的。
注意:由于将对dst参数进行修改,所以它必须是个字符数组或者是一个指向动态分配内存的数组的指针,不能使用字符串常量。
目标参数dst以前内容将被覆盖并丢失,即使新的字符串比 dst 原先内容更短,由于新字符串是以NUL字节结尾,所以老字符串最后剩余的几个字符串也会被有效删除。
例如:
第一个NUL字节后面的几个字符再也无法被字符串函数访问,因此从任何现实的角度看,它们都已经是丢失的了。
警告:
程序员必须保证目标字符数组的空间足以容纳需要复制的字符串,如果字符串比数组长,多余的字符仍被复制,他们将覆盖原先存储于数组后面的内存空间的值,改写原来存储在哪里的变量。
(2)连接字符串
char* strcat( char* dst ,char const * src);
strcat函数要求dst参数原先已经包含了一个字符串,可以是空字符串;他找到这个字符串的末尾,并把src字符串的一份拷贝添加到这个位置。如果src和dst的位置发生重叠,其结果是未定义的。
注意:程序员必须保证目标字符串数组剩余的空间足以保存整个源字符串。
(3)strcpy和strcat的返回值
strcpy和strcat都返回他们第一个参数的一份拷贝,即返回一个指向目标字符数组的指针;由于他们返回这种类型的值,所以可以嵌套使用这些函数。
(4)字符串比较
int strcmp(char const *str1, char const *str2);
如果str1大于str2,则返回一个大于0的值;如果str1小于str2,则返回小于0的值;如果相等,则返回0;
注意:标准并没有规定用于提示不相等的具体值,他只是说返回大于0和小于0的值,并不一定是1和-1;
3、长度受限的字符串函数
这些函数接受一个显式的长度参数,用于限定进行复制或比较的字符数。
函数原型如下所示:
char* strncpy(char * dst,char const *src,size_t len):
char* strncat(char* dst,char const * src,size_t len);
char* strncmp(char const *s1,char const *s2,size_t len);
和他们不受限制的版本一样,如果源参数和目标参数发生重叠,strncpy和strncat的结果是未定义的。
(1)strncpy
将源字符串的len个字符复制到目标数组中;
如果strlen(src) < len,则dst数组就用额外的NUL字节填充到len长度;
如果strlen(src) > len,则将src的前len个字符复制到dst中,但是复制的结果不会以NUL字节结尾,所以这种情况下你需要在dst的结尾加上NUL字节。
如果min(strlen(src),len) > strlen(dst),则将src中前strlen(dst)个字符复制到dst中,这样dst中全部为字符,就没有结尾的NUL字节,所以访问这样的dst,有可能造成系统崩溃,最好在dst[strlen(dst) - 1] = NUL,防止越界访问,造成系统崩溃。
strncpy调用的结果可能不是一个字符串,因此字符串必须以NUL字节结尾,如果在一个需要字符串的地方(例如strlen函数的参数)使用了一个不是以NUL字节结尾的字符序列,会发生什么呢?由于strlen函数无法知道参数代表的字符序列是没有NUL字节的,所以它将在到达字符序列结尾后继续进行查找,一个字符接着一个字符,知道她发现一个NUL字节为止,或许它找了几百个字符才找到,而strlen函数这个返回值从本质上说是一个随机数,或者说,如果strlen函数试图访问系统分配给这个程序以外的内存范围,程序就会崩溃。
考虑下列代码:
char src[len1];
char dst[len2];
............................
strncpy(dst,src,len1);
dst[ len2 - 1 ] = '\0';
如果src的内容可以容纳于dst中,最后那个赋值语句没有任何效果,但是,如果src太长,这条赋值语句可以保证src中字符串是以NUL字节结尾的;
(2)strncat
对于strncat来说,它从src中最多复制len个字符到目标数组dst的后面,但是strncat总是在结果字符串后面添加一个NUL字节,而且他不会向strncpy那样对目标数组用NUL字节进行填充,注意目标数组中原先的字符串并没有算在strncat的长度中,strncat最多向目标数组中复制len个字符,再加一个结尾的NUL字节,它才不管目标参数除去原先存在的字符串之后的空间够不够;
4、字符串查找基础
(1)查找一个字符
char* strchr(char const *str,int ch);
查找ch在字符串str中第一次出现的位置,找到后函数返回一个指向该位置的指针;若不存在,则返回NULL指针;
char* strrchr(char const *str,int ch);
返回的是指向该字符串中该字符最后一次出现的位置。
(2)查找任何几个字符
char* strpbrk(char const * str,char const *group);
这个函数返回一个指向str中第一个匹配group中任何一个字符的字符位置。如果未找到匹配,函数返回一个NULL指针。
(3)查找一个子串
char* strstr(char const *s1,char const *s2);
这个函数在s1中查找整个s2第一次出现的起始位置,并返回一个指向该位置的指针。
如果s2并没有完整的出现在s1中的任何地方,函数将返回NULL指针;
如果s2是一个空字符串,函数就返回s1。
5、高级字符串查找
(1)查找一个字符串前缀
strspn和strcspn函数用于在字符串的起始位置对字符计数。
size_t strspn( char const *str, char const *group );
size_t strcspn( char const *str,char const * group);
group指定一个或多个字符;strspn返回str自第一个字符开始连续匹配group中任意字符的字符数,即str中自第一个字符开始连续包含于group中字符数;
举例:
int len1,len2;
char buff[] = "25,142,330,smith,j,239"
len1 = strspn(buffer,"0123456789");
len2 = strspn(buffer,",0123456789");
len1 = 2:
buffer之中,自第一个字符'2'开始,只有第一个字符’2‘和第二个字符’5‘是字符串"0123456789"中的字符,而buffer中的第三个字符’,'不是"0123456789"中的字符,所以buffer中自起始位置开始,连续匹配"0123456789"中任意字符的字符数就为2,返回值为2
len2 = 11:
buffer之中,自第一个字符'2'开始,‘2’,‘5’,‘,',‘1’,‘4’,‘2’,‘,’,‘3’,‘3’,‘0’,‘,’共连续十一个字符为"0123456789"中的字符,第十二字符'S'不再是"0123456789"中的字符,所以返回值为11
而strcspn函数正好与strspn正好相反,它对str字符串起始部分中不与group中任何字符匹配的字符进行计数,返回连续不与group中字符匹配的字符的个数。
(2)查找标记
一个字符串常常包含几个单独的部分,它们彼此被分隔开来,每次为了处理这些部分,你首先必须把它们从字符串中抽取出来。
这个任务是由strtok函数实现的功能,它从字符串中隔离各个单独的称为标记的部分,并丢弃分隔符。
函数原型:
char* strtok( char* str,char const * sep);
sep参数是个字符串,定义了用作分隔符的字符集合,第一个参数str指定了一个字符串,它包含了0个或多个由sep字符串中一个或多个分隔符分隔的标记。strtok找到了str的下一个分隔符,并将该分隔符替换为NUL字节,然后返回一个指向这个被分割片段的指针。
在第一次调用时,strtok()必须给予参数s字符串,往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针。
(3)错误 信息
当你调用一些函数,请求操作系统执行一些功能如打开文件时,如果出现错误,操作系统是通过设置一个外部的整形变量errno进行错误代码报告。
strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。
char* strerror(int error_number);
事实上,返回值应该被声明为const,因为你不应该修改它。
6、字符操作
(1)字符分类函数:
如果它的参数符合下列条件就返回真
(2)字符转换
转换函数把大写字母转换为小写字母或者小写字母转换为大写字母
int tolower(int ch);
int toupper(int ch);
7、内存操作
根据定义,字符处啊由一个NUL字节结尾,所以字符串内部不能包含任何NUL字符,但是,非字符串数据内部包含零值的情况并不罕见,所以你无法使用字符串函数来处理这种类型的数据,因为当它们遇到第一个NUL字节时将停止工作。
但是可以使用下列函数,它们能够处理任意的字节序列。
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);
每个原型都包含一个显式的参数说明需要处理的字节数,但和strn带头的函数不同,它们在遇到NUL字节时并不会停止操作。
(1)memcpy从src起始位置复制length个字节到dst的内存的起始位置。如果src和dst以任何形式出现了重叠,结果是未定义的。
任何类型的指针变量都可以转换为void*型指针变量。
(2)memmove的函数行为,在src和dst重叠的情况下依然可以;但是memmove通常无法使用某些机器所提供的特殊的字节-字符串处理指令来实现,所以他可能比memcpy慢一些,但是如果src和dst真的可能存在重叠,就应该使用memmove;
(3)memcmp:由于memcmp比较的是一串unsigned char类型的字符,所以当比较的对象是其他类型的数据如整数或者浮点型时,就可能出现不可预料的结果。
(4)memchr:从a的起始位置开始查找字符ch第一次出现的位置,并返回一个指向该位置的指针,它共查找length给字节,如果没有找到,则返回NULL指针变量。
(5)memset:把从a开始的length个字节都设置为字符值ch;一般将一段存储空间清零是使用memset;
本章总结:
(1)应该在包含有符号数的表达式中使用strlen函数,无符号数的表达式仍然是无符号数
(2)使用strcpy函数把一个长字符串复制到一个较短的数组中,导致溢出
(3)使用strcat函数把一个长字符串添加到较短一个数组中,导致数组溢出。
(4)strcmp函数的返回值当做布尔值进行测试是错误的,因为strcmp在比较对象不相等时返回非零值,并不一定是1或-1;
(5)使用strncpy函数容易产生不以NUL字节结尾的字符串,要保证dst的最后一个字节为NUL字节,
(6)strtok在找到分隔符之后,会将该分隔符替换为NULL
(7)不要试图自己编写功能相同的函数来替代库函数
(8)使用字符分类和转换函数可以提高函数的移植性。
*****************************************************************************************************************************************************************************************
2013年3月19日更新
*****************************************************************************************************************************************************************************************
十、结构和联合
1、结构基础知识
(1)数组时相同类型的元素的集合,它的每个元素是通过下标引用或指针间接访问来选择的,数组元素可以通过下标访问,这只是因为数组元素的长度相同;
结构不能够通过下标来访问成员,因为一个结构的成员可能长度不同,然而每个结构成员都有自己的名字,它们是通过名字访问的。
这个区别非常重要,结构并不是一个他自身成员的数组,和数组名不同,当一个结构变量在表达式中使用时,它并不被替换成一个指针,结构变量也无法使用下标来选择特定的成员。
结构变量属于标量类型,所以你可以像对待其他标量类型那样执行相同类型的操作。
(2)结构声明:
声明方式:
struct tag {member-list} variable-list;
例子:
struct {
int a;
char b;
float c;
} x;
这个声明创建了一个名字为x的变量;
struct {
int a;
char b;
float c;
} y[20],*z;
这个声明创建了y和z;
警告:这两个声明被编译器当做两种截然不同的类型,即使它们的成员列表完全相同,因此y和z的类型 与 x的类型不同,即下列语句 z = &x;
但是,这是不是意味着某种特定类型的所有结构都必须使用一个单独的声明来创建呢?
有两种方式解决:(1)使用标签字段:
struct SIMPLE {
int a;
char b;
float c;
} ;
struct SIMPLE x;
struct SIMPLE y[20],*z;
这样使用标签SIMPLE之后,x,y,z都是同类型的结构变量,则 z = &x;是正确的
(2)使用typdef定义新的数据类型
typedef struct{
int a;
char b;
float c;
} Simple;
这里就使用typedef创建了一个新的类型Simple;
Simple x;
Simple y[20],*z;
则: z = &x;
2、结构成员的访问:
(1)结构成员是通过点操作符(.)访问的,下标引用和点操作符具有相同的优先级,它们的结合性都是从左向右;
(2)如果你拥有一个指向结构的指针,你该如何访问这个结构的成员呢?
首先就是对指针执行间接访问操作,这就使你获得这个结构;
然后你使用点操作符来访问它的成员。
注意,点操作符的优先级高于间接反问操作符,所以你必须在表达式中使用括号,确保间接访问首先执行。
例子:
Simple *cp;
定义了一个指向cp类型结构的指针变量cp,如何访问Simple的成员a呢?
首先对指针变量cp进行间接访问操作:即*cp,获得这个结构
然后使用点操作符访问它的成员:即(*cp).a
还有一种方法就是使用箭头操作符(->),例如cp->a
->这个操作符对于访问结构指针的指向结构的成员比较普遍,也比较方便
(3)结构的自引用
对于左边的定义来说,这种类型的自引用是非法的,因为b会包含b,这样会永远重复下去,所以这个结构的长度不确定,即占用的空间不明确,编译器无法为它分配空间,因此是非法的;
对于右边的定义来说,这种类型的自引用是合法的,尽管b会指向b,但是b本身是一个指针变量,我们知道无论何种类型的指针变量都是占用4个字节,这样一来,右边的结构长度就确定了,因此是合法的。
警告:
对于左边的定义来说,由于SELF_REF3这个类型名,知道定义的末尾才生效,提前引用是错误的;
解决方案是:定义一个结构标签来声明b,如右图
(4)不完整的声明
如果一个结构包含了另一个结构的一个或者多个成员,和自引用结构一样,至少有一个结构必须在另一个结构内部以指针的形式存在。问题在于声明部分:如果没给结构都引用了其他结构的标签,哪个结构应该首先声明呢?
这个问题的解决方案是使用不完整声明,它声明一个作为结构标签的标识符;然后我们可以把这个标签用在不需要知道这个结构的长度的生命中,如指向这个结构的指针。
例如:
在A的成员列表中需要标签B的不完整声明,一旦A被声明后,B的成员列表也可以被声明;
3、结构、指针和成员
我们以下面例子为例:
左值表示存在计算机存储空间中的对象,或者说左值代表一个内存地址值,并且通过这个内存地址,可以对内存进行读写操作;
右值表示存放在某个内存地址里的具体的数据值;
(1)对于表达式:
EX *px = &x;
px是个指针变量,这个表达式的左值就是px所代表的存储空间,这个表达式的右值就是&x,即结构x的存储地址,*px的右值是px所指向的整个结构体x
px+1:这个表达式并不合法,因为它的值并不存储于任何可标识的内存位置;
你可以把表达式*px赋值给另一个类型相同的结构,你也可以把它作为点操作符的左操作数来访问一个指定的成员;
表达式*px+1是非法的,因为*px的结果是一个结构;C语言并没有定义结构和整形值之间的加法运算。但表达式*(px+1)又如何呢,如果x是一个数组元素,这个表达式表示它后面的那个结构,但是x是一个标量,所以这个表达式实际上是非法的。
(2) 比较一下表达式*px和px->a:
在这两个表达式中,px保存的地址都用于寻找这个结构,但是结构的第一个成员是a,所以a的地址和结构的地址是一样的,这样px看上去是指向整个结构,同时指向结构的第一个成员,毕竟它们具有相同的地址。但是,这个分析只有一半是正确的,尽管两个地址的值是相等的,但它们的类型不同,变量px被声明为一个指向结构的指针,所以表达式*px的结果是整个结构,而不是它的第一个成员;
这样理解比较好,即看定义:
EX *px;int a
*px的类型是EX类型的,而a的类型是int,即使px所保存的地址是整个结构的起始地址,同时也是结构第一个成员的起始地址,但是*px的结果是整个结构;
&px->a是a的起始地址,但是px = &px->a是错误的,因为px和&px->a的数据类型不同,px是EX*型指针变量,而&px->a是int*指针变量。
箭头操作符->的优先级高于取地址符&的优先级。
(3)表达式px->c.a说明:
为了访问本身也是结构的成员c,我们可以使用表达式px->c,它的左值是整个结构。
使用点操作符访问c的特定成员。
这个表达式既包含了点操作符,也包含了箭头操作符,之所以使用箭头操作符,是因为px并不是结构,而是一个指向结构的指针,接下来之所以要使用点操作符是因为px->c的结果并不是一个指针,而是一个结构。
4、结构的存储分配
当存储成员时需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间;
例子:
struct ALIGN{
char a;
int b;
char c;
}
如果某个机器的整形值长度为4个字节,并且它的起始存储位置必须能够被4整除,那么这个结构在内存中的存储将如下所示:
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对其要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置;因此,成员a必须存储于一个能够被4整除的地址,结构的下一个成员是一个整型值,所以他必须跳过3个字节到达合适的边界才能存储,在整型之后是最后一个字符型
如果你声明了相同类型的第2个变量,它的起始存储位置也必须满足4这个边界,所以第1个结构的后面还要再跳过3个字节才能存储第2个结构;因此每个结构将占据12个字节的内存空间,但是实际只使用其中的6个;
你可以在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现,这样做法可以最大限度的减少因边界对齐而带来的空间损失。
即 struct ALIGN2{
int b;
char a;
char c;
}
这样这个结构只占用了8个字节。
如果你需要确定某个成员的实际位置,应该考虑边界对齐因素,可以使用offsetof宏(定义与stddef.h)
offsetof(type,member)
type就是结构的类型,member就是你需要的那个成员名,表达式的结果是一个size_t值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节。
例如对前面的声明:
offsetof( struct ALIGN,b)
的返回值是4;
5、作为函数参数的结构
结构变量是一个标量,它可以用于其他标量可以使用的任何场合。因此,把结构作为参数传递给一个函数时合法的,但是这种做法往往并不适宜。
传递指向结构的指针变量是一个更好的选择;指针比整个结构要小的多,所以把它压到堆栈上效率能提高很多,传递指针另外需要付出的代价是我们必须在函数中使用间接访问来访问结构的成员,即使用箭头标识符->;同时,结构越大,把指向它的指针传递给函数的效率就越高。
像函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改,如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。
6、位段
位段的声明和任何普通的结构成员声明相同,但是有两个例外:
首先,位段成员必须声明为int、signed int或unsigned int类型;
其次,在成员名后面是一个冒号和一个整数,这个整数指定该位段所使用的位的数目;
注:用signed或unsigned整数显式的声明位段是个好主意,如果把位段声明为int类型,它究竟被解释为有符号数还是无符号数是由编译器决定的。
位段在不同的系统中可能有不同的结果,所以位段是不可移植的:
(1)int位段被当做有符号数还是无符号数
(2)位段中位的最大数目;许多编译器把位段成员的长度限制在一个整型值的长度之内,所以一个能够运行于32为整数的机器上的位段声明可能在16位整数的机器上无法运行。
(3)位段中的成员在内存中是从左向右分配的还是从右向左分配的
(4)当一个声明指定了两个位段,第2个位段比较大,无法容纳于第一个位段剩余的位时,编译器有可能把第二个位段放在内存中的下一个字,也可能放在第一个位段后面,从而在两个内存位置的边界上形成重叠。
7、联合
(1)联合的所有成员引用的是内存中的相同位置。当你想在不同的时刻把不同的东西存储于同一个位置时,就可以使用联合;
某一时刻,联合的这几个字段只有一个被使用;
如果联合的各个成员具有不同的长度,联合的长度就是他最长成员的长度。
(2) 在一个成员长度不同的联合里,分配给联合的内存数量取决于它的最长成员的长度;这样联合的长度总是足以容纳最长的成员;
如果这些成员的长度相差悬殊,当存储长度较短的成员时,浪费的空间是相当可观的;在这种情况下,更好的办法是在联合中存储指向不同成员的指针而不是直接 存储成员本身,所有指针的长度都是相同的,这样就解决了内存浪费的问题。当他决定需要使用哪个成员时,就分配正确数量的内存来存储它。
(3)联合变量可以被初始化,但是这个初始值必须是联合的第一个成员的类型,而且它必须位于一对花括号里面。
union {
int a;
float b;
char c[4];
} x= {5};
这里初始化的结果为:x.a = 5;
我们不能把这个类量初始化为一个浮点值或者字符值;如果给出的初始值是任何其他类型,他就会转换为一个整数并赋值给x.a。
8、总结
(1)结构变量是一个标量,可以出现在普通变量变量可以出现的任何场合;
(2)结构的声明列出了结构包含的成员列表;不同的结构声明即使它们的成员列表相同也被认为是不同的类型,结构标签是一个名字,它与一个成员列表相关联,你可以使用结构标签在不同的声明中创建相同类型的结构变量,这样就不用每次在声明中重复成员列表,typedef也可以实现这一目标
(3)为了声明两个结构,每个结构都包含一个指向对方的指针的成员,我们需要使用不完整的声明来定义一个结构标签名,结构变量可以用一个花括号包围的值列表进行初始化;这些值的类型必须适合它所初始化的哪些成员;
(4)编译器为一个结构分配内存是要满足它们边界对齐要求,根据边界对齐要求降序排列结构成员可以最大限度的减少结构存储中浪费的内存空间,sizeof返回的值包含了结构中浪费的内存空间。
(5)向函数传递一个指向结构的指针效率很高,在结构指针参数的声明中加上const关键字可以防止函数修改指针所指向的结构。
(6)位段允许你把长度为奇数的值包装在一起以节省存储空间,源代码如果需要访问一个值内部任意的一些位,使用位段比较简便。
9、警告
(1)具有相同成员列表的结构声明产生不同类型的变量
(2)向函数传递结构参数是低效的;
(3)把结构标签声明和结构typedef声明放在头文件中,当源文件使用这些声明时可以通过#include命令将他们包含进来。
(4)结构成员的最佳排列形式并不一定就是考虑边界对齐而浪费内存空间最少的哪种排列形式
(5)位段成员应该显式的声明为signed int或unsigned int类型。
(6)位段是不可移植的
*****************************************************************************************************************************************************************************************
2013年3月21日更新
*****************************************************************************************************************************************************************************************
十一章、动态内存分配
1、数组的内存分配:
数组元素存储于内存中连续的位置上,当一个数组被声明时,它需要的内存在编译时就被分配。
2、为什么使用动态内存分配:
数组声明时,无法指定合适的长度:
(1)首先使用的元素数量超过了数组长度,则无法处理
(2)数组长度远大于元素个数,则巨大的内存空间被浪费
(3)如果输入的数据超过了数组的容纳范围时,程序必须以一种合理的方式作出响应,它不应该由于一个异常而失败退出;
3、malloc和free
(1)malloc用于执行动态内存分配,malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针;当这块内存不再使用时,调用free函数把它归还给内存池;
函数原型:
void *malloc(size_tsize);
void free(void *pointer);
malloc所分配的是一块连续的内存;同时,malloc实际分配的内存有可能比你请求的稍微多一点,但是,这个行为是由编译器定义的,所以你不能指望它肯定分配比你请求更多的内存。
分配失败,malloc返回NULL指针;
free的参数要么是NULL,要么是一个先前从malloc、calloc或者realloc返回的值,向free传递一个NULL参数不会产生任何效果。
标准表示一个void*类型的指针可以转换为其他任何类型的指针;
对于要求边界对齐的机器,malloc所返回的内存起始位置将始终能够满足对边界对齐要求最严格的类型的要求。
4、calloc和realloc
函数原型:
void*calloc(size_t num_elements,size_t element_size);
void *realloc(void* ptr,size_t new_size);
calloc也用于分配内存,
malloc和calloc之间的主要区别是calloc在返回指向内存的指针之前把内存初始化为0;
malloc和calloc之间的另一个区别是他们请求内存数量的方式不同,calloc的参数包含所需元素的数量和每个元素的字节数。根据这些值,calloc能够计算出总共需要分配的内存。
realloc用于修改一个原先已经分配好的内存块的大小,使用这个函数,你可以使一块内存扩大或缩小,如果扩大内存,新增内存添加原先内存后面,且没有进行初始化,如果缩小内存,原先内存后面的一部分被删除,剩余部分的原先内容依然保留;
如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上;
因此在使用realloc之后,应该改用realloc所返回的指针。
5、常见动态内存错误
包括:
对NULL指针进行解引用操作;
对分配的内存进行操作时越过边界;
释放非动态分配的内存;
试图释放一块动态分配的内存的一部分;
在一块动态内存被释放之后继续使用。
(1)动态内存分布最常见的错误是忘记检查所请求的内存是否成功分配;
当分配失败时,被访问的内存可能保存了其他变量的值,对该内存修改将会破坏这个变量,这类错误难以发现,所以一旦动态分配内存,就要检查是否分配成功
(2)动态内存分布的第二大错误来源是操作内存时超出了分配内存的边界;
malloc和free的有些实现中,他们以链表形式维护内存,对分配之外的区域进行访问可能破坏这个链表;
(3)当使用free时,传递给free的指针必须是一个从malloc、calloc或realloc函数返回的指针,free非动态分配内存可能会发生错误,free一块动态分布内存的一部分也有可能引起错误,因为释放一块内存的一部分是不允许的,动态分配的内存必须整块一起释放;但是realloc函数可以i缩小一块动态分配的内存,有效的释放它尾部的部分内存。
(4)不要访问已经被free函数释放了的内存;
假定你对一个指向动态内存的指针进行了赋值,而且这个指针的几份拷贝散布在程序各处,你无法保证当你使用其中一个指针时对它所指向的内存是不是已被另一个指针释放,另一方面,你必须确保程序中所有使用这块内存的地方在这块内存被释放之前停止使用。
6、内存泄露
当动态分配的内存不需要使用时,它应该被释放,这样它以后还可以被重新分配使用,分配内存但在使用完毕后不释放将引起内存泄露memory leak;
在那些所有执行程序共享一个通用内存池的操作系统中,内存泄露将一点点的消耗可用内存,最终使其一无所有,要摆脱这个困境,只有重启系统。
还有一个问题就是程序在释放动态分配内存之前,持续申请动态分布内存,这样最终将耗尽可用内存;
动态分配内存一个常见的用图就是为那些长度在运行时才知的数组分配内存空间;
7、总结:
(1)动态分配内存有助于消除程序内部存在的限制;
(2)使用sizeof计算数据类型的长度,提高程序的可移植性;
十二、 使用结构和指针
1、链表:包含数据的独立数据结构(通常称为节点)的集合;链表中的每个节点通过链或指针连接在一起,程序通过指针访问链表中的节点;通常节点是通过动态分配的,但有时也是由节点数组构建的链表,即使这种情况下,也是通过指针来遍历链表的;
节点结构的定义:
typedef struct NODE {
struct NODE *link;
int value;
} Node;
节点相邻仅是逻辑顺序,事实上,链表中的节点有可能分布在内存中各个地方。
2、单链表
单链表:每个节点包含一个指向链表下一个节点的指针,链表的最后一个节点的指针字段指向NULL;
单链表可以从链表的开始位置遍历到结束位置,但是无法从相反的方向进行遍历;
单链表插入程序:
3、双链表
双链表中,每个节点都包含两个指针---指向前一个节点的指针和指向后一个节点的指针,这就使我们可以从两个方向遍历双链表;
(1)节点的声明:
typedef struct NODE{
struct NODE *fwd;
struct NODE *bwd;
int value;
} Node;
根节点的fwd字段指向链表的第一个节点,根节点的bwd字段指向链表的最后一个节点;如果链表为空,这两个字段都为NULL;
链表的第一个节点的bwd字段和最后一个节点的rwd字段都为NULL;
4、总结:
语句提炼是一种简化程序的技巧,其方法是消除程序中冗余的语句,如果一条if语句的then和else子句以相同序列的语句结尾,它们可以被一份单独的出现在if语句之后的拷贝代替;相同序列的语句也可以从if语句的起始位置提炼出来,但是这种提炼不能改变if的测试结果;如果不同的语句事实上执行相同的功能,你可以把它们写成相同的样子,然后再使用语句提炼简化程序。
警告:
从if语句中提炼语句可能会改变测试结果;
十三、高级指针话题
1、为什么还要使用更为复杂的涉及间接访问的方法呢?
这是因为简单赋值并不总是可行,例如链表插入,在那些函数中,我们无法使用简单赋值,因为变量名在函数的作用域内部是未知的;函数所拥有的只是一个指向需要修改的内存位置的指针,所以要对该指针进行间接访问操作以访问需要修改的变量。
2、高级声明:
(1)int *f;
它把表达式*f声明为一个整数;
C声明的这种解释方法可以通过下面的声明得到验证:
int *f,g;
声明的结果是只有f是整型指针变量,g是整型变量;
(2)int *f();
首先执行的是函数调用操作符(),因为它的优先级高于间接访问操作符,因此,f是一个函数,它的返回值类型是一个指向整型的指针。
(3)int (*f)();
第一个括号只起到聚组作用,第二个括号是函数调用操作符;
因此,此表达式的结果是,将f声明为一个函数指针,它所指向的函数返回一个整型值;
程序中的每个函数都位于内存中的某个位置,所以存在指向那个位置的指针是完全有可能的;
(4)int *(*f)();
它和前一个声明类似,f也是一个函数指针,只是所指向的函数的返回值是一个整型指针,必须对其进行间接访问操作才能得到一个整型值。
(5)int *f[];
f是一个数组,它的元素类型是指向整型的指针;
(6)int f()[];
这个声明是非法的,是错误的;
(7)int f[]()
这个声明也是非法的;
(8)int (*f[])()
f是一个数组,每一个数组元素都指针,而这些指针指向的函数的返回值是整型变量
(9)int *(*f[])()
f是一个数组,每一个数组元素是指针,而这些指针指向的函数的返回值是整型指针
3、函数指针
函数指针最常见的两个用途是转换表和作为参数传递给另一个函数;
(1)简单声明为一个函数指针并不意味着它马上就可以使用,和其他指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数;
例如:
int f(int);
int (*pf)(int) = &f;
第二个声明创建了函数指针pf,并把它初始化为指向函数f,函数指针的初始化也可以通过一条赋值语句来完成,在函数指针的初始化之前具有f的原型是很重要的,否则编译器就无法检查f的类型是否与pf所指向的类型一致。
初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针。&操作符只是显式的说明编译器将隐式执行的任务。
函数指针被声明并且初始化之后,我们可以使用三种方式调用函数:
int ans;
ans = f( 25 );
ans = (*pf)(25);
ans = pf(25);
第一条语句简单的使用名字调用函数f,但它的执行过程可能和你想像的不太一样,函数名f首先被转换为一个函数指针,该指针指定函数在内存中的位置。然后,函数调用操作符调用该函数,执行开始于这个地址的代码。
第二条语句对pf执行间接访问操作,它把函数指针转换为一个函数名,这个转换并不是真正需要的,因为编译器在执行函数调用操作符之前又会把它转换回去,不过,这条语句和第一条语句是完全相同的。
第三条语句和前两条语句效果一样,间接访问操作并非必须,因为编译器需要的是一个函数指针,这个例子显示了函数指针通常是如何使用的。
总结:
(1)回调函数:你可以使用函数指针来实现回调函数,一个指向回调函数的指针作为参数传递给另一个函数,后者使用这个指针调用回调函数。
使用这种技巧,你可以创建通用型函数,用于执行普通的操作如在一个链表中查找。
(2)转移表也是用函数指针;转移表像swith语句一样执行选择,转移表由一个函数指针数组组成(这些函数必须具有相同的原型);函数通过下标选择某个指针,再通过指针调用对应的函数。你必须始终保证下标值处于适当的范围之内,因为在转移表中调试错误是非常困难的;
初始化列表中的各个函数名的正确顺序取决于程序中用于表示每个操作符的整形代码,这个例子假定ADD = 0, SUB = 1, MUL = 2,依次类推:
result = oper_func[oper](op1,op2)
(3)字符串常量:出现在表达式中的字符串常量的值是一个常量指针,它指向字符串的第一个字符,和数组名一样,你既可以用指针表达式也可以用下标来使用字符串常量。
*****************************************************************************************************************************************************************************************
2013年3月22日更新
*****************************************************************************************************************************************************************************************
十四、预处理器
1、预处理符号
(1)_FILE_ :进行编译的原文件名,例子:“name.c”,
(2)_LINE_:文件当前行的行号;例子:12
_FILE_和_LINE_在确认调试输出的来源方面很有用处;
(3)_DATE_:文件被编译的日期:例子:“Jan 31 1997”
(4)_TIME_:文件被便衣的时间:例子:“18:04:30”
_DATE_和_TIME_常用语在被编译的程序中加入版本信息
(5)_STDC_:如果编译器遵循ANSI C,其值就为1,否则未定义;例子:1
用于哪些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译
2、#define
格式:
#define name stuff
有了这条指令之后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff;
如果定义的stuff很长,它可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠:
注:相邻的字符串常量被自动连接成为一个字符串。
3、宏:#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏;
注:所有用于数值表达式进行求值的宏定义都应该在参数左右加上括号,避免在使用宏时,由于参数中的操作符或临近的操作符之间不可预料的相互作用;而且整个宏定义左右也应该加上括号,避免整个宏定义与临近操作符之间产生不可预料的相互作用。
4、宏定义技巧:
(1)临近字符串自动连接的特性使我们很容易把一个字符串分成几段,每段实际上都是一个宏参数。
(2)使用预处理器把一个宏参数转换为一个字符串,#argument这种结构被预处理器翻译为 ”argument“
(3)##把位于它两边的符号连接成一个符号,即将它两边的文本粘贴为同一个标识符;
4、宏和函数
(1)优势:
用于调用函数和从函数返回的代码工作量大,宏在程序的规模和速度方面更好;
宏与数据类型无关;
还有一些任务,函数无法完成:
例子: #define MALLOC(n,type)((type* )malloc((n)* sizeof(type)))
pi = MALLOC( 25,int);
pi = ((int* )malloc((25)* sizeof(int)))
这个例子中,宏的第二个参数就是一种类型,他无法作为函数参数进行传递;
(2)劣势:
除非宏比较短,否则使用宏可能会大幅度增加程序的长度;
5、#undef
#undef name
如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除;
6、命令行定义
允许你在命令行中定义符号,用于启动编译过程;
同一个源文件编一个一个程序的不同版本时,这个特性是很有用的,例如,假定某个程序声明了一个某种 长度的数组,如果某个机器的内存很有限,这个数组必须很小,但是在另一个内存充裕的机器上,你可能希望数组能够大一些;
int arr[ARRAY_SIZE];
那么,在编译程序时,ARRAY_SIZE可以在命令行中指定;
在UNIX编译器中, -D选项可以完成这项任务,我们可以用两种方式使用这个选项;
-Dname
-Dname=stuff
第一种形式定义了符号name,它的值为1;第二种形式把该符号的值定义为等号后面的stuff。
7、条件编译
8、测试是否被定义
测试是否已经定义:
#if defined(symbol)
#ifdef symbol
测试是否没有定义:
#if !defined(symbol)
#ifndef symbol
*****************************************************************************************************************************************************************************************
2013年3月23日更新
*****************************************************************************************************************************************************************************************
十五、输入输出函数
1、错误报告
perror函数一种简单、统一的方式报告错误:
perror函数的原型:
void perror( char const *message);
如果message不是NULL并且指向一个非空的字符串,perror函数就打印出这个字符串,后面跟一个分号和一个空格,然后打印出一条用于解释errno当前错误代码的信息;
2、终止执行
exit函数原型:
void exit(int status);
status参数返回操作系统,用于提示程序是否正常完成。这个值和main函数返回的整型状态值相同;
当程序发现错误情况使他无法继续执行下去时,这个函数尤其有用;你经常会调用perrno之后再调用exit终止程序,尽管终止程序并非处理所有错误的正确方法,但是总比继续错误的程序更好。
3、标准I/O函数库
ANSI C版函数库和以往旧式I/O函数库相比,最大的区别旧式那些在可移植性和功能性方面的改进;
ANSI C的一个主要优点旧式这些修改将通过增加不同函数的方式实现,而不是通过对现存函数进行修改实现;因此,程序的可移植性不会受到影响;
4、ANSI I/O概念
(1)绝大多数流是完全缓冲的,这意味着读取和写入实际上是从一块被称为缓冲区的内存区域来回复制数据;从内存中来回复制数据是非常快速的,用于输出流的缓冲区只有当它写满时才会被刷新到设备或文件中,一次性写满的缓冲区写入和逐片把程序产生的输出分别写入相比,前者效率更高;
但是完全缓冲在调试时会遇到问题,因为如果程序失败,缓冲输出可能不会被实际写入,这样就可能使程序员得到关于错误出现位置的不正确结论,这个问题的解决办法就是在每个用于调试的printf函数之后立即调用fflush,如下所示:
printf(”something is”);
fflush(stdout);
注:fflush迫使缓冲区的数据立即写入,不管它是否已满;
(2)FILE结构
FILE结构:不要把它和存储于磁盘上的数据文件相混淆,FILE是一个数据结构,用于访问一个流;如果你同时激活几个流,每个流都有一个相应的FILE与它相关联;
(3)标准I/O常量
EOF:所选择的实际值比一个字符要多几位,这是为了避免二进制被 错误的解释为EOF;
FOPEN_MAX:一个程序同时最多能够打开多少个文件呢,他和编译器有关,可以保证你能够同时打开最少FOPEN_MAX个文件,这个常量包括了三个标准流,同时它的值最少是8;
FILENAME_MAX:用于提示一个字符数组应该多大以便容纳编译器所支持的最长合法文件名。
5、字符I/O
(1)输入
int fgetc(FILE* stream);
int getc(FILE* stream);
int getchar(void);
需要操作的流作为参数传递给getc和fgetc,但getchar始终从标准输入读取;每个函数从流中读取下一个字符,并把它作为函数的返回值返回;
这些函数都用于读取字符,但返回int型值而不是char值的原因是:为了允许函数报告文件的末尾(EOF),如果返回值是char型,那么在256个字符中必须有一个被指定用于表示EOF,如果这个字符出现在文件内部,那么这个字符以后的内容将不会被读取,因为它被解释为EOF标识;
而让函数返回一个int型值,EOF被定义为一个整形,它的值在任何可能出现的字符范围之外,这种解决方法允许我们使用这些函数来读取二进制文件。
(2)写入:
int fputc(int character,FILE*stream);
int putc(int character,FILE*stream);
int putchar(int character):
第一个参数就是要被打印的字符;
注:fgetc和fputc都是真正的函数,但getc、putc、getchar和putchar都是通过#define指令定义的宏,宏在执行时间上效率稍高,而函数在程序的长度方面更胜一筹,之所以提供两种类型的方法,是为了允许你根据程序的 长度和执行的速度那个更重要进行选择适当的方法;
(3)撤销字符
int ungetc(int character ,FILE* stream);
ungetc把一个先前读取的字符返回六种,这样它可以在以后被重新读入;
(4)fgets和fputs函数
char* fgets(char* buffer,intbuffer_size,FILE* stream);
int fputs(charconst *buffer,FILE* stream);
fgets从指定的stream中读物字符并把它们复制到buffer中,当它读取一个换行符并存储到缓冲区之后,不在读取;或者缓冲区内 的字符达到buffer_size-1时也停止读取;
NUL字节将被添加到缓冲区所存储数据的末尾,使她成为一个字符串。
传递给fputs的缓冲区必须包含一个字符串,它的字符被写入到流中,这个字符串预期以NUL字节结尾,所以这个函数没有一个缓冲区长度参数,这个字符串时逐字写入的:如果它不包含换行符,就不会写入换行符,如果包含好几个换行符,所有的换行符都会被写入;写入错误,则返回常量值EOF
(5)格式化的行I/O:
int fscanf( FILE* stream, char const *format,.......);
int scanf( char const *format,.....);
int sscanf( char cosnt *string,charconst*format,......);
fscanf的输入源就是作为参数给出的流,scanf从标准输入读取,而sscanf则从第一个参数所给出的字符串中读取字符;
从输入转换而来的值逐个存储到这些指针参数所指向的内存位置;由于C的传值参数传递机制,把一个内存位置作为参数传递给函数的唯一方法就是传递一个指向该位置的指针。
如果指针参数的类型不正确,那么结果值就会是垃圾,而临近的变量有可能在处理过程中被改写;
(5)printf家族
int fprintf(FILE* stream,charconst*format,.....);
int printf(char const*format,....);
int sprintf(char *buffer,charconst*format,...);
fprintf可以使用任何输出流,printf将结果输出送到标准输出,sprintf把它的结果作为一个NUL结尾的字符串存储到指定的buffer缓冲区而不是写入到流中,这三个函数的返回值是实际打印或存储的字符数;
sprintf是一个潜在的错误根源,缓冲区的大小并不是sprintf函数的一个参数,所以输出结果很长就会溢出缓冲区,此时有可能改写缓冲区后面内存位置中的数据;
6、二进制I/O
将数据写到文件效率最高的方法就是用二进制形式写入。二进制输出避免了在数值转换为字符串过程中所涉及的开销和精度损失;但是二进制数据并非人眼所能阅读,所以这个技巧只有当数据将被另一个程序按顺序读取时才能使用:
(1)fread()和fwrite()函数
size_t fread( void* buffer,size_t size,size_tcount,FILE*stream);
size_t fwrite( void* buffer,size_t size,size_tcount,FILE*stream);
buffer是一个指向用于保存数据缓存的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数,当然stream是数据读取或写入的流;
(2)fflush函数
int fflush( FILE* stream );
fflush迫使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满;当我们需要立即把输出缓冲区的数据进行物理写入时,应该使用这个函数;
例如,调用fflush函数保证调试信息实际打印出来,而不是保存在缓存区直到以后才打印;
(3)ftell()函数和fseek()函数
long ftell( FILE* stream ):
int fseek( FILE* stream,long offset,int from );
ftell函数返回流的当前位置,也就是说,下一个读取或写入将要开始的位置距离文件起始位置的偏移量。这个函数允许你保存一个文件的当前位置,这样你可能在将来会返回到这个位置;
在二进制流中,这个值就是当前位置距离文件起始位置之间的字节数;
在文本流中,这个值表示一个位置,但是他不一定能正确反映当前位置与文件起始位置之间的字符数,因为有些系统将行末字符进行翻译转换;
但是ftell函数返回的值总是可以用于fseek函数,作为一个距离文件起始位置的偏移量;
fseek函数允许你在一个流中定位;
试图定位到文件起始位置之前是错误的;
定位到文件末尾之后将扩展这个文件;
之所以存在这些限制,部分原因是文本流所执行的行末字符映射;由于这种字符映射的存在,文本文件的字节数可能和程序写入的字节数不同;因此,一个可移植的程序不能根据实际写入字符数的计算结果定位到文本流的一个位置;
用fseek改变一个流的位置会带来三个副作用:
首先是,行末指示字符被清除;
其次是,如果在fseek之前使用ungetc把一个字符返回到流中,那么这个被退回的字符会被丢弃,因为在定位操作以后,他不在是“下一个字符”;
最后,定位允许你从写入模式切换到读取模式,或者回到打开的流以便更新;
(4)rewind()和fgetpos()和fsetpos()
void rewind( FILE* stream );
int fgetpos( FILE*stream,fpos_t*position);
int fsetpos( FILE*stream,fpos_tconst*position);
rewind函数将读写指针设置回到指定流的起始位置;他同时清除流的错误提示标志;
fgetpos和fsetpos函数分别是ftell和fseek函数的替代方案;
它们的主要区别在于这对函数接受一个指向fpos_t的指针作为参数;
fgetpos在这个位置存储文件的当前位置,fsetpos把文件位置设置为存储在这个位置的值;
5、改变缓冲方式:
在流上执行的缓冲方式有时并不合适,下面两个函数可以用于对缓冲方式进行修改:
void setbuf( FILE* stream,char* buf);
int setvbuf(FIEL* stream,char*buf,intmode,size_tsize);
setbuf设置了另一个数组,进行对流的缓冲;这个数组的长度必须为BUFSIZE;这是为一个流自行指定缓冲区可以防止I/O函数库为它动态分布一个缓冲区;如果用一个NULL参数调用这个函数,setbuf函数将关闭流的所有缓冲方式
setvbuf函数更为通用,mode参数指定缓冲类型,_IOFBF指定一个完全缓冲的流,_IONBF指定一个不缓冲的流,_IOLBF指定一个行缓冲流;
所谓行缓冲,就是每当一个换行符写入到缓冲区时,缓冲区立即进行刷新;
所谓完全缓冲,就是当缓冲区写满之后,缓冲区才进行刷新;
所谓不缓冲,就是每当写一个字符,就立即刷新;
buf和size参数用于指定需要使用的缓冲区,如果buf为NULL,那么size的值必须为0;
一般而言,最好用一个长度为BUFSIZ的字符数组作为缓冲区,尽管使用一个非常大的缓冲区可能课可以稍稍提高程序的效率,但是使用不当,会降低程序的效率;
如果你需要一个很大的缓冲区,它的长度应该是BUFSIZ的整数倍;
警告:
这个函数只有当指定的流被打开但还没有在它上面执行任何其他操作前才能被调用;
为流缓冲区使用一个自动数组是很危险的,如果在关闭流之前,程序的执行流离开了数组声明所在的代码块,流就会继续使用这个内存块,但是此时他可能已经分配给其他函数另作他用了。
6、流 错误函数
int feof( FILE* stream);
int ferror( FILE* stream);
void clearerr( FILE*stream);
如果流处于当前文件末尾,feof函数就返回真;这个状态可以通过对流执行fseek,rewind或fsetpos函数来清除;
ferror函数报告流的错误状态,如果出现任何读/写错误函数就返回真;
clearerr函数对指定流的错误标识进行重置;
7、临时文件
有时我们会使用一个文件来临时保存数据,当程序结束时,这个文件便被删除,因为它所包含的数据不再有用;
tmpfile函数就是用于这个目的的;
FILE* tmpfile( void );
这个函数创建了一个文件,当文件被关闭或程序终止时这个文件便自动删除;该文件以wb+模式打开,这时它可用于二进制和文本数据。
当临时文件必须以其他模式打开或者由一个程序打开但由另一个程序读取,就不适合用tmpfile函数创建,在这些情况下,我们必须使用fopen函数,而且当结果文件不再需要时使用remove函数显式删除。
char* tmpnam(char* name);
如果传递函数的参数为NULL,那么函数返回一个指向静态数组的指针,该数组包含了被创建的文件名;
无论哪种情况,这个被创建的文件名保证不会与已经存在的文件名同名,只要调用的次数不超过TMP_MAX,tmpnam函数每次调用时都能产生一个新的不同名字。
8、文件操纵函数
int remove( char const *filename);
int rename(charconst*oldname,charconst*newname);
remove函数删除一个指定的文件,如果单remove被调用时文件处于打开状态,其结果则取决于编译器;
rename函数用于改变一个文件的名字;如果名为newname的文件已经存在,其结果取决于编译器;如果这个函数失败,则仍可以用原来的名字访问该文件;
警告:
(1)记得在调试用的printf语句后面跟一个fflush调用;
(2)在使用fgets时指定合适的缓冲区
(3)注意gets的输入有可能溢出缓冲区且未被检测到;
*****************************************************************************************************************************************************************************************
2013年3月24日更新
*****************************************************************************************************************************************************************************************
十六、标准函数库
1、整型函数
(1)算术<stdlib.h>
int abs(int value);abs函数返回它的参数的绝对值;
long intlabs( longint value);labs的作用对象是长整型,返回参数的绝对值
div_t div(int numerator,int denominator);将第二个参数除以第一个参数,产生商和余数;用一个div_t结构返回,
ldiv_t ldiv(long intnumer, longint denom);参数和返回值都是长整型,用一个ldiv_t结构返回;
div_t结构包含以下两个成员:
int quot;
int rem;
(2)随机数<stdlib.h>
int rand(void);
void srand( unsigned int seed );
rand返回一个范围在0和RAND_MAX之间的伪随机数;当它重复调用时,函数返回这个范围内的其他数;为了得到一个更小范围的伪随机数,可以把这个函数的返回值根据所需范围的大小进行取模,然后通过加上或减去一个偏移量对它进行调整;
为了避免程序每次运行都获得相同的随机数序列,可以调用srand函数;一个常用的技巧就是使用每天的时间作为随机数产生器的种子(seed),如:
srand((unsigned int )time(0));
(3)字符串转换<stdlib.h>
字符串转换函数把字符串转换为数值;
int atoi(char const*string );
long intatol( charconst *string );
long intstrtol( charconst *string,char **unused,int base );
unsigned long int strtoul(char cosnt *string,char **unused,int base );
如果上面函数的第一个参数包含了前导空白字符,它们将被跳过;
atoi和atol执行十进制的转换;
strtol和strtoul中,第二个参数保存一个指向转换值后面第1个字符的指针;base是转换时的基数,即采用几进制转换;
base范围是0或者是2~36,0和10时代表采用十进制
2、浮点型函数
注意在使用时不要忘了包含头文件math.h
(1)三角函数<math.h>
sin、cos、tan函数的参数是一个用弧度表示的角度,这些函数分别返回这个角度的正弦、余弦和正切值;
asin、acos的参数不再1和-1之间,就出现一个定义域错误;
asin和atan的返回值是范围在-π/2和π/2之间的一个弧度;
acos返回值是一个范围在0和π之间的一个弧度;
atan2函数返回表达式y/x的反正切值,但它使用这两个参数的符号来决定结果值位于哪个象限;返回值是-π和π之间的弧度;
(2)双曲函数<math.h>
(3)对数和指数函数<math.h>
exp函数返回e值的x次幂,即e^x;
log函数返回x以e为底的对数;
log10函数返回x以10为底的对数;
注:x以任意b为底的对数可以通过以下计算:
对应到此处可以用
log(x)/log(b)
(4)浮点型表示形式<math.h>
(5)幂<math.h>
double pow( double x,double y);
double sqrt(double x);
(6)底数、顶数、绝对值和余数<math.h>
(7)字符串转换<stdlib.h>
3、日期和时间函数
(1)处理器时间<time.h>
clock函数返回从程序开始执行起处理器消耗的时间;
clock_t clock( void );
这个值可能是个近似值,如果你需要更精确的值,你可以在main函数刚开始执行时调用clock,然后把以后调用clock时所返回的值减去前面这个值,如果机器无法提供处理器时间,或者如果时间太大,无法用clock_t变量表示,函数就返回-1;
clock函数返回一个数字,它是由编译器定义的;通常它是处理器时钟滴答的次数;为了把这个值转换为秒,你应该把这个值除以常量CLOCKS_PER_SEC;
警告:
在有些编译器中,这个函数可能只返回程序所使用的处理器时间的近似值;
如果宿主操作系统不能追踪处理器时间,函数可以返回已经流逝的实际时间数量;
(2)当天时间<time.h>
time函数返回当前的日期和函数。
time_t time(time_t*returned_value);
功能:获取当前的系统时间,返回的结果是一个time_t类型,其实就是一个大整数,其值表示从CUT(Coordinated Universal Time)时间1970年1月1日00:00:00(称为UNIX系统的Epoch时间)到当前时刻的秒数;然后调用localtime将time_t所表示的CUT时间转换为本地时间(我们是+8区,比CUT多8个小时)并转成struct tm类型,该类型的各数据成员分别表示年月日时分秒。
返回的时间存放在returned_value指针变量中;如果时间值太大,无法用time_t变量表示,函数就返回-1;
警告:
调用time函数两次并把两个值想减,由此判断期间所流逝的时间,这种做法很危险,因为标准并未要求函数的结果值用秒来表示;
difftime函数可用于这个目的;
日期和时间的转换<time.h>
下面的函数用于操纵time_t值
char *ctime(time_tconst *time_value);
double difftime( time_t time1,time_t time2);
ctime函数的参数是一个指向time_t的指针,并返回一个指向字符串的指针;
注意:ctime实际可能一下面这种方式实现:
asctime(localtime(time_value));
difftime函数计算time1-time2的差,并把结果值转换为秒。注意它返回的是一个double类型的值。
解析来是一个把time_t值转换为tm结构,后者允许我们方便的访问日期和时间的各个组成部分。
struct tm *gmtime(time_t const *time_value );
struct tm *localtime( time_t const *time_value );
gmtime函数把时间转换为世界协调时间UTC,UTC以前被称为格林尼治标准时间GreenwichMean Time,这也是gmtime名字的由来,
localtime讲一个时间转换为当地时间,标准包含了这两个函数,但并没有描述UTC和当地时间的实现之间的关系。
注意:tm_year这个值只是1900年之后的年数,为了计算实际的年份,这个值必须与1900相加;
当你拥有一个tm结构之后,你可以直接使用它的值,也可以作为下面函数的参数:
char* asctime( struct tmconst *tm_ptr );
size_t strftime( char* string,size_t maxsize,char const *format,struct tm const *tm_ptr);
asctime转换后的时间的字符串和ctime函数的格式一样,ctime可能在内部调用了asctime来实现自己的功能;
strftime函数把一个tm结构转换为一个根据某个格式字符串而定的字符串;如果转换结果字符串的长度小于maxsize参数,那么该字符串就被复制到第一个参数所指向的数组中,strftime函数返回的字符串的长度,否则,函数返回-1且数组的内容是未定义的;
struct tm *block;
block=localtime(&t1);
char tembuf[128];
strftime(tembuf,125,"%M %P %S, %U ",block);
printf("%s",tembuf);
最后,mktime函数用于把一个tm结构转换为一个time_t值;
time_t mktime(struct tm *tm_ptr);
tm结构中,tm_wday和tm_yday的值被忽略,其他字段的值也无需限制在它们的通常范围内。
转换之后,该tm结构会进行规格化,因此tm_wday和tm_yday的值将是正确的,其余字段的值也都将位于它们通常的范围之内。
4、非本地跳转<setjmp.h>
setjmp和longjmp函数提供了一种类似goto语句的机制,但它并不局限于一个函数的作用域之内,这些函数常用于深层嵌套的函数调用链;
如果在某个低层的函数中检测到一个错误,你可以立即返回到顶层函数,不必调用链中每个中间层函数返回一个错误标志;
int setjmp(jmp_buf state);
void longjmp(jmp_buf state,int value);
5、信号
信号表示一种事件,它可能异步发生,也就是并不与程序执行过程的任何事件同步;
(1)信号名<signal.h>
SIGABRT:是一个由abort函数所引发的的信号,用于终止程序;
SIGFPE:至于哪些错误将引发SIGFPE信号,这取决于编译器:常见的有算术上溢或下溢以及除零错误;有些编译器可能对信号进行了扩展,但是有可能影响程序的可移植性;
SIGILL:提示CPU试图执行一条非法指令;这个错误可能由于不正确的编译器设置所导致;
SIGSEGV:提示程序试图访问非法内存;有两个常见原因:一是试图访问未安装于机器上的内存或者是系统未分配给程序的内存;二是程序违反了内存访问的边界要求;后者可能在那些要求数据边界对齐的机器上发生;
前面的几个信号是同步的,因为都是在程序内部发生的;
SIGINT和SIGTERM是异步的:他们是在程序外部发生,通常是由程序的用户所触发,表示用户试图向程序传达一些信息;
SIGINT:绝大多数机器中都是当用户试图中断程序时所发生的。
SIGTERM:则是另一种用于请求终止程序的信号;
在实现这两个信号的 系统中:
一种常用的策略是为SIGINT定义一个信号处理函数,目的是执行一些日常维护工作并在程序退出前保存数据;
但是SIGTERM则不配备信号处理函数,这样当程序终止时便不必执行这些日常维护工作;
(2)处理信号<signal.h>
raise函数用于显式的引发一个信号;
int raise( int sig );
调用这个函数将引发它的参数所指定的信号;程序对这类信号的反应和那些自主发生的信号是相同的;你可以调用这个函数对信号处理函数进行测试;但是如果误用,会实现一种非局部的goto效果,因此要避免以这样的方式使用它。
当一个信号发生时,程序可以使用三种方式对他做出反应。缺省的反应是由编译器定义的,通常是终止程序。程序也可以指定其他行为对信号作出反应:信号可以被忽略,或者程序可以设置一个信号处理函数,当信号发生时调用这个函数;
signal函数用于指定程序希望采取的反应:
void (*signal(int sig,void(*handler)( int)))(int);
首先,我们将省略返回类型,这样我们可以先对参数进行研究:
signal(int sig,void(*handler)(int));
第一个参数sig是指SIGABRT、SIGFPE等6个信号;第二个参数时你希望为这个信号设置的信号处理函数,这个函数是一个函数指针,它所指向的函数接受一个整型参数且没有返回值。当信号发生时,信号的代码将作为参数传递给信号 处理函数,这个参数允许一个处理函数处理几种不同的信号;
现在我们将原型中去掉参数,这样函数的返回类型看上去就比较清楚:
void(*signal())(int);
signal是个函数,它返回一个函数指针,后者所指向函数接受一个整型参数且没有返回值。
事实上,signal函数返回一个指向该信号以前的处理函数的指针。通过保存这个值,你可以为信号设置一个处理函数并在恢复为先前的处理函数。如果调用signal失败,例如由于非法的信号代码所致,函数将返回SIG_ERR值,这个值是个宏,他在signal.h中定义。
signal.h中还定义了两个宏,SIG_DFL和SIG_IGN,它们可以作为signal函数的第二个参数,SIG_DFL恢复对该信号的缺省反应,SIG_IGN使该信号被忽略;
(3)信号处理函数
当一个已经设置信号处理函数的信号发生时,系统首先恢复对该信号的缺省行为,防止信号处理函数内部也发生这个信号导致无限循环;
然后,信号处理函数被调用,信号代码作为参数传递给函数;
信号处理函数可能执行的工作类型是很有限的:
如果信号是异步的,也就是说不是由于调用abort或raise函数引起的,信号处理函数便不应调用除signal之外的任何库函数,因为在这种情况下结果是未定义的;
信号处理函数除了能向一个类型为volatilesig_atomic_t的静态变量赋一个值以外,可能无法访问其他任何静态数据;为了保证真正的安全,信号处理函数所能做的就是对这些变量之一进行设置然后返回;程序剩余部分必须定期检查变量的值,看看是否有信号发生;
这些严格的限制是由信号处理的本质产生的,信号通常用于提示发生了错误,在这些情况下,CPU的行为时精确定义的,但在程序中,错误所处的上下文环境可能很不相同,因此他们并不一定能够良好定义;
访问限制定义了在信号处理函数中保证能够运行的最小功能;访问非原子数据的中间步骤时如果产生一个信号可能导致不一致的结果,在信号处理函数中把数据访问限制为原子单位可以消除这种可能性;
标准表示信号处理函数可以通过调用exit终止程序,用于处理除了SIGABRT之外所有信号的处理函数也可以通过调用abort终止程序。但是这两个都是库函数,所以当它们被异步信号处理函数调用时可能无法正常运行。
volatile数据:
信号可能在任何时候发生,所以由信号处理函数修改的变量的值可能会在任何时候发生改变,因此,你不能指望这些变量在两条相邻的程序与剧中肯定具有相同的值。volatile关键字告诉编译器:防止它以一种可能修改程序含义的方式“优化”程序;
从信号处理函数返回:
从一个信号处理函数返回导致程序的执行流从信号发生的地点恢复执行,这个规则的例外情况是SIGFPE,由于计算无法完成,从这个信号返回的效果是未定义的;
可变长参数:
7、执行环境
(1)终止执行<stdlib.h>
void abort(void)
void atexit(void (func)(void));
void exit( int status)
abort函数用于不正常的终止一个正在执行的程序;由于这个函数将引发SIGABRT信号,你可以在程序中为这个信号设置一个信号处理函数,在程序终止之前采取任何你想采取的动作,甚至可以不终止程序;
atexit函数可以把一些函数注册为退出函数;当程序将要正常终止时(或者由于调用exit,或者由于main函数返回),退出函数将被调用,退出函数不能接受任何参数;
exit函数,用于正常终止程序;
当exit函数被调用时,所有被atexit函数注册为退出函数的函数将按照它们所注册的顺序被反序依次调用;然后,所有用于流的缓冲区被刷新,所有打开的文件被关闭,用tmpfile函数创建的文件被删除,然后退出状态返回给宿主环境,程序停止执行;
由于程序停止执行,exit函数绝不会返回到它的调用处;但是任何一个用atexit注册为退出函数的函数再次调用了exit,其效果是未定义的;这个错误可能导致一个无线循环,很可能只有堆栈内存耗尽后才会终止;
(2)断言<assert.h>
断言就是声明某种东西应该为真;ANSI C实现了一个assert宏,它在调试程序时很有用。
void assert( int expression );
当它被执行时,这个宏对表达式参数进行测试,如果它的值为假,它就向标准错误打印一条诊断信息并终止程序;这条信息的格式是由编译器定义的,但它将包含这个表示式和源文件的名字以及断言所在的行号;如果表达式为真,它不打印任何东西,程序继续执行;
这个宏提供了一种简便的方法,对应该是真的东西进行检查,例如,如果一个函数必须用一个不能为NULL的指针参数进行调用,那么函数可以用断言验证这个值:
assert( value != NULL );
如果函数错误的接受恶劣一个NULL参数,程序就会打印一条类似下面形式的信息:
Assertion failed:value != NULL,file.c line 274
注:用这种方法使用断言让调试变的更容易,因为一旦出现错误,程序就会终止,而且,这条信息准确的提示了症状出现的地点;如果没有断言,程序可能继续运行,并在以后失败,这就很难进行调试。
注意,assert只适用于验证必须为真的表达式;由于它会终止程序,所以你无法用它检查那些你试图进行处理的情况;
当程序被完整的测试完毕后,你可以在编译时通过定义NDEBUG消除所有的断言,你可以使用-DNDEBUG编译器命令行选项或者在源文件中头文件assert.h被包含之前增加下面这个定义:
#define NDEBUG
当NDEBUG被定义之后,预处理将丢弃所有的断言,这样就消除了这方面的开销,而不必从源文件中把所有的断言实际删除;
(3)环境<stdlib.h>
10、总结
(1)clock函数可能只产生处理器时间的近似值
(2)time函数的返回值并不一定是以秒为单位的
(3)tm结构中月份的范围并不是从1到12的,而是从0到11
(4)tm结构中的年是从1900年开始计数的年数
(5)longjmp不能返回到一个已经不再处于活动状态的函数
(6)从异步信号的处理函数中调用exit或abort函数时不安全的
(7)每次信号发生时,你必须重新设置信号处理函数
(8)避免exit的多重调用
(1)滥用setjmp和longjmp可能导致晦涩难懂的代码
(2)对信号进行处理将导致程序的可移植性变差
(3)使用断言可以简化程序的调试;
***************************************************************************************************************************************************************************************
2013年3月25日更新
*****************************************************************************************************************************************************************************************
十八、运行时环境
1、
*****************************************************************************************************************************************************************************************
2013年3月26日更新
*****************************************************************************************************************************************************************************************