学习书籍《C Primer Plus》
一、作用域:变量起作用的范围;
分为:代码块作用域、函数原型作用域、文件作用域
代码块作用域:代码块是指包含在开始花括号和对应花括号之内的一段代码或者是一个函数内的任一复合语句;在代码块中定义的变量具有代码块作用域,从该变量定义开始至包含该定义的代码块结束,该变量均起作用。可以什么时候使用什么时候定义。
函数原型作用域:适用于函数原型声明中使用的变量名;作用域从变量定义处一直到原型声明的末尾。
文件作用域:在所有函数之外定义的变量具有文件作用域;具有文件作用域的变量从定义开始到包含该定义的文件结束都是可见的。
变量作用域的判断方式是:
查看该变量定义的位置。
例如:下面一段小程序:
int var;
char ch;
void fun(int arr[n])
{
int i;
for(i = 0 ; i < n; i++)
arr[i] = i;
}
那么var的作用域就是文件作用域,从该变量开始定义到该文件结束,还包括其他文件的引用;而i的作用域就是函数作用域,在fun()函数内部,从i定义开始到arr[i] = i;结束。
二、链接:
C语言中的变量分为外部链接、内部链接、空连接三种:
(1)外部链接:外部链接的变量可以在多个文件中使用;
(2)内部链接:内部链接的变量只能在一个文件中使用;
(3)空链接:由定义变量的代码块作用域所私有;
文件作用域变量前面加关键字static就是内部链接,表明该变量为定义该变量的文件私有。
变量链接的判断方式是:
查看变量定义的位置以及前面是否有static关键字。
三、存储时期:分为静态存储时期和自动存储时期
静态存储时期:程序执行期间一直存在;具有文件作用域的变量
自动存储时期:具有代码块作用域的变量;进入定义该变量的代码块时才为该变量分配内存,退出该代码块时,收回内存。
变量存储时期的判断方式是:
看变量作用域以及是否加static关键字。
集合上面三个概念,可以将变量分为5类:
存储类 |
存储时期 |
作用域 |
链接 |
声明方式 |
自动(局部变量) |
自动存储,定义该变量的代码块运行后即释放 |
代码块作用域 |
空链接 |
代码块内或者函数原型声明 |
寄存器(寄存器变量) |
自动存储,定义该变量的代码块运行后即释放 |
代码块作用域 |
空链接 |
代码块内或者函数原型声明,使用关键字register |
具有外部链接的静态(外部变量) |
静态存储,即整个程序运行期间 |
文件作用域 |
外部链接 |
所有函数外 |
具有内部链接的静态(静态外部变量) |
静态存储,即整个程序运行期间 |
文件作用域 |
内部链接 |
所有函数外,使用关键字static |
空链接的静态(静态局部变量) |
静态存储,即整个程序运行期间 |
代码块作用域 |
空链接 |
代码块内,使用关键字static |
静态变量:静态外部变量和静态局部变量;特点是只在编译时进行一次初始化,静态变量能够保存上次调用后的值,以供本次调用;
*****************************************************************************************************************************************************************************************
2012年12月26日更新
*****************************************************************************************************************************************************************************************
四、数据类型的数据表示范围,在计算机中的存放方式(原码、反码、补码),如果最大正数+1会怎样,最小负数-1会怎样?
unsigned 数据类型只表示0和正数,正数的最大值一般是对应signed 数据类型可表示的正数最大值的2倍;
同一计算机上,int 型不长于long型,不短于short型,也就是说int型可能与long或者short型数据的位数一致,具体情况要看计算机本身了;
C保证:int型至少16位;short型至少16位;long型至少32位;long long型至少64位;long型长于short型,int型可以在long型长度至short型长度之间任选;
最大数+1时,会变成最小值:即当一个数是signed int型的最大正数值,+1后变成最小的负值;当一个数是unsigned int型的最大数时,+1后变成0;
举例:
以32位int型为例: int max = 2147483647; max +1之后,printf打印出来就是-2147483648
unsigned int max =4294967295;max+1之后,printf打印出来就是0
最小数-1时,会变成最大值:即当一个数是signed int型的最小负数值,-1后变成最大的正数值;当一个数是unsigned int型的最小数是,-1后变成最大正数值;
以32位int型为例: int min = -2147483648; max -1之后,printf打印出来就是2147483647
unsigned int min =0;min-1之后,printf打印出来就是4294967295
五、printf()和scanf的一些注意点:
(1)printf()也有返回值,就是打印的字符的数目;挺好,以前没注意过
当使用printf()实现有符号的数和无符号的数之间的转换时,要注意二者之间的范围;因为无符号数的范围大于有符号数的正数范围,所以如果无符号数大于有符号数的最大值时,这时的转化会出错,结果会将正数转为负数输出;如果是有符号的负数转为无符号时,结果负数会转换为正数输出。
当使用printf()将长字节数据类型转为短字节数据类型输出时,会截取对应的低位字节输出;
例如int a = 324;转成char 型输出时,会输出什么呢:
因为a的二进制表示为:1 0100 0100;然后转成char型时,截取低八位,即0100 0100,这样就输出为D了
当使用printf()将短字节数据类型转为长字节数据类型输出时,会额外读取短字节数据相邻的数据用来补足字节数;
(2)scanf()也有返回值,就是成功读入的项目的个数
空白字符:空格键、制表符 、换行符
除了在%c模式下,会读入空白字符
六、几个术语:
数据对象:data object:泛指数据存储区,数据存储区用于保存值。
左值:用于标识一个特定的数据对象的名字或者表达式;
数据对象就是对存储空间的抽象,而左值就是对某些存储空间的具体表示,数据对象对你是不可见的,而左值你却可以看到,注意,并不是所有的左值都可以修改,有些左值是只读的,比如变量名前面加上const的话,那这个左值就不可修改。一般赋值表达式等号的左边是可修改的左值,不可修改的左值不能用于赋值表达式等号的左边。
右值:能够赋给可以修改的左值的量,不一定是数字,可以是数字、表达式或者字符等。
操作数:这里先说项目,item,在原来读程序的时候经常看到item,总是会迷惑这个到底在这里指什么,原来item即项目就是所谓的操作数,操作数就是运算符操作的对象。
操作数可以是变量、表达式、数据类型、函数、宏定义等
side effect:是对数据对象或者文件的修改。
sequence point:在这个point之前,所有的side effect都要计算出来,以供下面的语句使用;任何一个表达式的结束就是一个这样的point,即使这个表达式只是该语句中的一部分,但是只要执行完了这个表达式,所有的side effect就会完成。
例如:
int i;
i = 0;
while(i++ < 5)
printf("%d\t",i);
或许你想打印的是0 1234;但是实际上打印出的是:12345;
七、类型转换的规则:
(1)char和short型在表达式中会自动转换为int型,若还不够,则转化成unsigned int
(2)两种数据类型运算时,将级别较低的转化为级别较高的类型,然后在进行运算;级别从低到高为:int-----unsigned int-------long--------unsigned long------long long-----
unsigned long long----------float-----------double--------long double;总之是
不同类型的数据级别是: 字符型 < 短整型 < 整型 < 长整型 < 浮点型 < 双精度型 < 长双精度
相同类型数据是: signed < unsigned
注意:在实际编程时,最好不要依赖于自动转换,即使需要转换,最好用强制转换,这样程序不容易犯错,而且更具可读性。
八、逗号运算符
逗号运算符可以将简单的表达式联合起来,构成一个表达式,可以成为逗号表达式;
逗号表达式在for循环中应用最广,例如可以同时以若干个变量的初值为初值表达式、可以同时使用多个条件、可以同时循环多个变量;
逗号表达式有两个属性:
一是逗号表达式按照逗号分割,从左至右运算,逗号左边表达式产生的side-effect在逗号右边表达式执行之前生效;
二是逗号表达式的值是逗号右边表达式的值;
结合上边两个属性,可以讲逗号左边的表达式看成是逗号右边表达式的条件了,逗号左边产生的所有结果都被逗号右边接受并应用,最后的结果就是逗号右边表达式的值。
九、一些运算符
(1)优先级: 算术运算符 高于 关系运算符 高于 赋值运算符
(2)算术运算符:* /高于+-
(3)关系运算符:< <=>>=高于 ==!=
(4)赋值运算符:=
注意:+ - ++ -- 以及 =都是从右向左结合的,除此之外都是从左向右结合的。
十、continue
continue:跳出当前循环层当次循环,执行当前循环层的下一次循环。
例如:有五层for循环,continue语句位于最内层时,执行continue时,就会从该处跳出当次循环(当次循环中continue之后的语句不再执行),开始执行最内层for循环的下一次循环;假设是第三层for,则跳出该层的本次循环,开始第三层的下一侧循环。
在
while(表达式)
;
这种形式中,添加continue可使程序更具可读性,意思不变。
while(表达式)
continue;
十一、break
break:跳出当前循环层。break用于switch语句中,可以跳出switch语句。
continue和break的区别就是:
continue跳出当前循环层的当次循环,开始当前循环层的下一次循环,而break是跳出当前循环层;
break用于循环体和switch中,continue只用于循环体;
十二、goto
goto:使程序跳到标签指定的位置执行;
尽量不要使用goto语句,因为这会让程序可读性很差,只有一种情况下可以使用:在多层循环中,单单使用continue和break无法使程序完全跳出所有循环时,可以考虑使用goto语句,在某些条件下,使程序转到goto标签处执行,这样就跳出了多层循环。
*****************************************************************************************************************************************************************************************
2012年12月28日更新
*****************************************************************************************************************************************************************************************
十三、一些命令:
(1)重定向
在dos下命令行格式下,输入命令行后加上 > filename.格式 就可以将输出重定向到filename文件中
(2)在dos下输入内容,然后保存在某个文件中:
copy con filename.格式
当打入上述命令时,就可以在dos下输入内容,按ctrl+Z可以退出输出模式,那么输入的内容就保存在文件filename中了
(3)查看某文件,即在dos下显示某文件内容
想查看的话,输入 type filename.格式
(4)文件、流、键盘输入、屏幕输出、重定向
文件:是指存储信息的存储区域;文件通常保存在某些永久存储器上。
流:C程序处理的实际是一个数据流而不是文件;而流是一个理想化的数据流,是指实际输入或者输出映射到这个数据流;打开文件的过程就是将流与文件相关联,并通过流进行读写文件的过程。
低级I/O函数的标准库:即操作系统处理文件的基本文件工具;但是操作系统多种多样,C不可能包含所有操作系统的I/O函数,这样太过繁琐,况且C即使提供所有操作系统的I/O函数,你在使用不同操作系统时,也会感到很麻烦,因为太多不同的IO函数了,这让人怎么记;所以这里就引出了标准I/O函数库,即能不能使C提供一种标准的I/O函数库,我们输入输出时只与这个标准I/O函数库打交道,而与操作系统I/O打交道的事交给C函数库,这样就减少了麻烦;
标准I/O函数库:是指你在进行输入输出时,只需要与C标准库提供的I/O函数打交道即可,不必在意操作系统的不同,因为与操作系统的IO打交道的是C函数库,C提供一个统一的I/O函数,你只需要和这几个函数打交道即可;C提供的标准IO函数包括:scanf(),printf(),getchar(),putchar()
C对待输入和输出设备与其对待存储设备上的普通文件相同,特别的是:键盘和屏幕作为每个C程序自动打开的文件来对待。
键盘输入由一个称为stdin的流来表示,屏幕(打字机、或其他输出设备)输出由一个称为stdout的流来表示。
C程序就是通过scanf()、printf()、getchar()、putchar()函数和stdin流、stdout流打交道,通过这个两个流将数据从文件中读出中或写到文件:
用图表示上述说明就是:
键盘(输入文件,键盘作为文件对待,自动打开)——映射——>stdin流——读出——>scanf()或getchar()函数——>C程序其他处理模块——>printf()或者putchar()函数——写入——>stdout流——映射——>屏幕(输出文件,屏幕作为文件对待,自动打开)
重定向:
输入重定向就是不再把键盘输入作为映射到stdin流的文件,而是自己定义一个输入文件,即以后的输入行为都从这个输入文件映射到stdin流,即以后由C的标准输入函数读取;
输出重定向就是不再把屏幕输入作为映射到stdiout流的文件,自己顶一个输出文件,即以后的输出行为都从stdout映射到这个文件,即以后由C的标注输出函数写入;
用图表示:
输入文件(不再是键盘,即重定向之后的文件)——映射——>stdin流——读出——>scanf()或getchar()函数——>C程序其他处理模块——>printf()或者putchar()函数——写入——>stdout流——映射——>输出文件(不再是屏幕,即重定向之后的文件)
(1)映射:即stdin和stdout流中数据都是二进制形式,那么你输入或输出的数据与二进制数据之间的对应关系就是所谓的映射,从键盘映射到stdin流就是根据字符与二进制的映射关系将字符表示成对应的二进制形式,从stdout流映射到屏幕,就是根据二进制数据与字符之间的映射关系,将stdout流的中二进制数据显示到屏幕上。
(2)文本文件:即可视化文件,文件中的所有数据均是按照ASCII码的形式存放的,输出的时候 当然也是按照ASCII码输出的;
(1)二进制文件:二进制文件中的数据不完全是按照ASCII码形式存放的,即某些数据就不是ASCII码,你强行显示到屏幕的话,就是一堆乱码,为什么呢,ASCII都是unsigned char型数据,而且数字范围是从0~127,超出这个范围的数据就不是ASCII码了,所以二进制文件中很多数据都不在这个范围内,显示出来当然是乱码。
重定向的符号:
> 输出重定向符号,即将可执行文件的输出重定向到某个文件,而不再是屏幕;例子:
x264 -b 0 -q 26 -o test.264 test_1280x720.yuv > x264_test.txt
在命令行的末尾加上 > x264_text.txt之后,就将这个命令行产生的输出都重定向到x264_text.txt,即将输出都输出到x264_test.txt,不再出现在屏幕上。
< 输入重定向符号,即将某个文件作为可执行文件的输入,而不是键盘,格式
可执行文件名 < 重定向文件
结果就是将重定向文件作为可执行文件的输入。
重定向需要遵循的规则:
(1)重定向运算符是将一个可执行程序与一个数据文件连接起来。可执行程序包括标准的操作系统的命令,例如:ping www.hao123.com > ping.txt那么就将ping命令的结果都输出到ping.txt中去了,屏幕上不再显示ping命令的结果。
重定向运算符不能讲一个数据文件与另一个数据文件连接起来,也不能将一个可执行文件与另一个可执行文件连接起来。
(2)使用这些运算符时,不能重定向输入多个文件,也不能重定向输出多个文件。
(3)可以使用组合重定向:即将输入重定向和输出重定向结合起来。
*****************************************************************************************************************************************************************************************
2013年1月4日更新
*****************************************************************************************************************************************************************************************
十四、数组和指针
数组实际上是变相使用指针的一种形式;
例如:int arr[SIZE];
那么arr[n]就是指*(arr+n),即首先寻址到内存中arr,然后再移动n个单位,再取出数值。
所以会有数组名其实就是数组首元素的地址,因为数组名其实是个指针,即数组首元素的地址就是指向数组首元素的指针,就是数组名。
在函数参数传递时,如果实际参数是数组名,那么形式参量必须是一个指针变量,因为数组名是一个地址,只有指针变量才能匹配。
下面四种函数原型声明是等价的:
int sum(int *arr,int n);
int sum(int *,int );
int sum(int arr[],int n);
int sum(int [],int );
十五、const
(1)只有非常量数据的地址才可以赋给普通的指针变量,即把const型指针赋给非const型指针是错误的;
例如const int arr[10] = {1,2,3,4,5,6,7,8,9,0}
int *ptr;
ptr = arr;
这就是错误的,因为arr[]是常量数组,否则你就可以通过指针修改数组的内容了。
(2)在const常量在做实际参数时,函数的形式参数必须也得声明为const型,否则是错误。
*****************************************************************************************************************************************************************************************
2013年1月10日更新
*****************************************************************************************************************************************************************************************
十六、数组和指针
(1)数组只有在初始化时可以整体赋值,初始化过后,如果想改变数组元素的值,只能循环的对数组元素逐个赋值。
(2)当数组没有初始化时,其中存储的数据是无效的;如果只进行了部分初始化,则未被初始化的元素被初始化为0。
(3)初始化列表中元素的个数不能大于数组的大小,但是可以省略数组中括号里的数字,让编译器自动匹配数组大小和初始化列表中的元素。
(4)C99中规定:可以对某些元素进行初始化,未经初始化的值则被设置成0。例如:int arr[10] = {2、5、6、[7]=8}:结果为:arr[0]=2,arr[1]=5,arr[2]=6,arr[7]=8,其他值为0。
(5)在调用数组元素时,编译器不会检查数组索引是否越界:比如int arr[6];你使用arr[7],编译器检查不出来。
(6)数组标记其实是一种变相使用指针的形式。
(7)数组名就是该数组首元素的地址。
(8)指针变量的数值就是它所指向对象的地址,对于包含多个字节的数据类型,对象的地址就是其首字节的地址。
注意区分指针和指针变量:
指针就是指地址
指针变量是指能够存放指针的变量,即能够存放地址的变量。
所以在说指针和指针变量时,最好区分指针和指针变量。
(9)在指针变量前面加运算符*就可以得到该指针变量所指向的对象的数值。
(10)对指针变量+1表示对指针变量的值加上它指向的对象占据的字节数;即int *ar;ar+n也是指针变量;
(11)因为数组名就是数组首元素的地址,如果实际参数是一个数组名,那么形式参数必须是与之相匹配的指针。在这种情况下,而且仅在这种情况下,C对int ar[]和int *ar作出同样的解释,即ar是指向int的指针,由于原型声明允许省略名称,因此下面四种原型声明是等价的:
int sum(int *ar,int n);
int sum(int *,int );
int sum(int ar[],int n);
int sum(int [],int );
但是在定义函数时,名称是不可省略的,因此,在定义时,下面是等价的:
int sum(int *ar,int n)
{
}
int sum(int ar[],int n)
{
}
(12)在对指针变量使用*、++、--时,最好加上括号,这样容易理解。
注意:只有在变量ar是指针变量时,才能使用ar++和ar--。
(13)打印指针变量使用%p,若不支持,则使用%u或者是%lu
(14)指针变量加减指针变量是一个整数,指针变量加减整数还是一个指针变量
(15)C保证指向数组元素的指针变量和指向数组后面的第一个地址的指针变量都是有效的,但是如果指针变量在进行增量或减量运算后超出了这个范围,后果是未知的。
可以对指向一个数组元素的指针变量进行取值运算,但是不能对指向数组后面的地址进行取值运算(如果这个地址是通过指针变量加减整数或者自增自减得到的)。,尽管指针变量是合法的。
(16)在对指针变量使用const限制时,要注意是限制的指针变量存放的地址还是指针变量指向的数据
注:将const型或者非const型指针变量赋给const型指针变量是合法的;但是将const型指针赋给非const型指针变量是非法的。
(17)对一个指针变量使用*运算符和带有索引的[]运算符,得到的是该指针变量所指向的对象的数值
例如:
int var = 10;
int *p = &var;
printf("%p,%p,%d,%d",p,&p[0],p[0],*p);
打印出来就是p和&p[0]是一样的,p[0]和*p是一样的。
(18)对一个数组名使用*运算符,得到的是该数组的数值。
例如:
int arr[12] = {12,13};
printf("%d,%d",*arr,*(arr+1));
打印出来就是12和13
(19)二维数组:
int arr[3][4] ;
可以看成是数组的数组;arr[m][n]中,arr[m]可以看成是行的数组,arr[m][n]才是单个元素
则arr指向的对象数组arr[3][4]的第一行元素,即指向的对象大小是一行的大小:4个int;而arr[0]指向的对象时数组arr[3][4]的第一行的第一个元素,即指向的对象大小是一个元素的大小:1个int;
所以arr和arr[0]所指向的数据对象的大小不一样:一个是一行数据,一个是单个数据。
有一点要注意,每一行的首地址和每一行第一个元素的首地址是相同的,因为开始于同一地址。
看一个数组和指针变量时,要先清楚这个数组和指针是几重的,然后再根据*运算符、带索引的[]、&运算符来判定表示的到底是地址还是数据。
在vs上看到:
int var = 10;
那么&var的数据类型是:int*,即整型指针;
int *p = &var;
那么&p的数据类型是:int**,即指针的指针;
(20)指针的赋值:
int arr[3][4];
int *p;
p = arr[0];这句话是错误的,因为p是指向的对象的数据类型包含一个int值,而arr[0]指向的对象的数据类型包含4个int值。
即p的类型是:int*;arr[0]的类型是:int*[4];所以不匹配。
所以在进行指针的赋值时,要先判断等号两边是否指针,如果都是指针那类型是否相同。
如果要进行这种指针的指向,就要用到数组指针,即指向数组的指针:
int (*p)[3];
int arr[4][3];
p = arr;正确
因为p指向包含3个int值构成的数组;arr指向有3个int值构成的数组;所以p和arr类型是一致的,可以赋值;
注:
指针是一个地址,指针变量是一个变量,是一个存放指针的变量,再根据具体的数据类型可以称为:
例如:
int var = 120;
int *p = &var;
p是指针变量,p是整型指针变量,再具体点是整型数据var的指针变量,再具体点是整型数据var地址变量,再具体点是存放整型数据var地址的变量,哎,这才是指针变量p的全称,这样才好理解;
十七、字符串常量属于静态存储类,静态存储是指如果一个函数中使用字符串常量,即使多次调用这个函数,该字符串在程序的整个运行过程中只存储一份,整个引号中的内容作为指向该字符串存储位置的指针。
例如:char *str[] = {"ni hao ","zai na ","chi fan le me ","xiu xi hao le me "}
str是个一维数组,数组中存放的每个字符串的首地址,例如str[0]存放的是第一个字符串中第一个字符n的地址,str[1]中存放的是第二个字符串中第一个字符z的地址、、、、
则str数组其实不存放实际的字符串,只是存放字符串的地址,即str[0]是第一个字符串的首地址,str[1]是第二个字符串的首地址,str[2]是第三个字符串的首地址;则:
可以使用str[0][0]表示第一个字符串的第一个字符,尽管str并没有定义成二维数组。
为字符串数组指定大小时,一定要确保数组元素比字符串长度至少多1,多出来的一个元素用于容纳空字符。
未被使用的元素均被自动初始化为\0
注:在使用++和--时,要注意只能变量使用,常量不能使用;
十八、字符串输入函数:
gets():从系统的标准输入设备获得一个字符串,输入参数是一个字符型指针变量,完毕后返回一个指向字符串的指针;gets()在读取换行符时,会丢弃。
注意,gets()函数返回的指针域传递给它的是同一个指针,输入字符串只是一个备份,它放在座位函数参数传递过来的地址中。
不足:gets()的一个不足是它不检查预留存储区是否能够容纳实际输入的数据,多出来的字符简单的溢出到相邻的内存区。
fgets():此函数能够改进gets()函数不检查预留存储区大小的缺点,让用户指定最大的读入字符数;但是,本函数是为文件I/O而设计的
fgets()和gets()函数有三个方面的不同:
(1)它由用户指定最大读入字符数,作为输入的参数之一;
(2)如果fgets()读取换行符,就会把它存到字符串里,而不是像gets()那样丢弃它。
(3)它还需要指定读取哪一个文件;若是从键盘输入,则可以stdin(即标准输入)作为该参数,这个标识符在stdio.h中定义。
scanf()函数:scanf()函数和gets()函数的主要差别在于它们如何决定字符何时结束。scanf()函数更倾向于获取单词,而不是获取字符串;而gets()函数,会读取所有的字符,知道遇到第一个换行符为止;scanf()使用两种方法决定输入结束:字符串从遇到的第一个非空白字符开始,如果使用%s,则读到(但不包括)第一个空白字符(换行符、制表符、空格)或者是达到字符串指定的宽度为止,二者任何一个满足都结束输入。
scanf()函数返回成功读入字符的个数;或者遇到文件结束时返回一个EOF;
综上:
gets()函数从键盘读取文本可能更好,因为更容易使用、速度更快
scanf()函数主要用于以某种标准形式输入的混合类型数据的读取和转换。
十九、字符串输出:
puts():只需要给出输出字符串的地址;puts()显示字符串时自动在其后添加一个换行符。
fputs():和puts()函数的区别是:
(1)fputs()需要第二个参数来说明要写的文件,可以使用stdout来作为参数进行屏幕输出。
(2)与puts()不同,fputs()并不为输出自动添加换行符。
注:如果把fgets()和puts()输出结合使用,每个字符串后就会显示两个换行符;关键就在于gets()是为了和puts()一起设计使用的,而fgets()是和fputs一起设计使用的。
*****************************************************************************************************************************************************************************************
2013年1月14日更新
*****************************************************************************************************************************************************************************************
二十、如果在内层代码块和外层代码块的变量同名,则内层代码块定义的名字是内层代码块所使用的变量。我们称之为内层定义覆盖(hide)外部定义,但当运行离开内层代码块时,外部变量重新恢复作用。
但是这种写法不鼓励,尽量少写。
二十一、寄存器变量和自动变量都具有代码块作用域、自动存储时期、空链接。
由于寄存器变量是存放在一个寄存器中而非内存中,所以无法获得寄存器的地址。
寄存器变量的声明仅是一条请求,而非命令,编译器必须在您的请求与可用寄存器的个数或可用高速内存的数量之间做权衡,所以请求可能会被拒绝,结果就是寄存器变量当成普通的自动变量;然而,你依然不能对该变量使用地址符。
二二、静态变量:前面加关键字static的变量即为静态变量。
静态变量的静态是指该变量的位置不变,而非变量的值不变;
当创建具有代码块作用域、同时具有静态存储时期的静态变量时,当包含这些静态变量的函数执行完毕后,这些静态变量并不释放,而是一直存在,等待下一次执行该函数时,每一次调用该变量时该变量的值都是上一次调用后的变量值。
具有代码块作用域、空链接、静态存储时期的静态变量只在编译包含该变量的函数时被初始化一次,如果不显式的初始化,他们都被初始化为0;在调试时你会发现,前面加关键字static的代码块作用域的变量不会执行,并不是不执行,而是静态变量和外部变量在程序调入内存时就已经就位了。
例如:
int summ(int arr[],int n)
{
int i;
static int sum;
、、、、、
}
变量i在每次执行该程序时都会重新初始化,而static int sum 在程序调入内存中时就已经就位,只在编译时初始化一次,在调试时并没有执行,将sum放在这里仅仅是告诉编译器只有summ()函数才能调用sum变量。
还有就是对函数参数不能使用static关键字
int sum(static int n);不合法
静态变量与静态存储时期的区别:
(1)静态变量是指前面加关键字static的关键字,静态存储时期是指整个程序运行期间都存在的变量;
(2)静态变量肯定具有静态存储时期,具有静态存储时期的变量不一定是静态变量:例如外部变量具有静态存储时期,但是不是静态变量
(3)静态变量具有独特的性质就是:只在编译时初始化一次,并且保存上一次调用时留下的值,并应用于本次调用;这一点和同具有静态存储时期的外部变量不同;无论是静态局部变量还是静态全局变量均能够保存上一次调用结束后的值,下一次调用的就是上次调用后的值。
二三、外部变量如果不显式的初始化,则自动被赋初值为0;对应指针变量的话,就是指向NULL了;如果是数组,则所有数组元素被初始化为0;但是要注意的是不能使用变量表达式初始化,只能使用常量表达式初始化。
外部变量的作用域是从声明处起始到文件末尾。
二四、普通的外部变量可以被其他文件引用,但是具有内部链接的静态变量只能被与它在同一个文件中的函数使用。、
当然,只有在使用多文件的程序时,外部链接和内部链接才能由区别。
多文件程序在使用外部变量时,除了一个定义外,其他声明必须要使用extern关键字,而且只有在定义中才能初始化外部变量。
二五、存储类关键字说明符:
(1)auto:表明一个变量具有自动存储时期,该说明符只能用于具有代码块作用域的变量声明中,而这样的变量已经默认是自动存储时期,因此它主要用于明确指出意图,是程序更具可读性,实际中很少用。
(2)register:只能用于具有代码块作用域的变量,它将一个变量归入寄存器存储类,这相当于请求将该变量存储在一个寄存器中,但是寄存器变量的地址无法获取;而且register只是请求而非定义,也就是说编译器不一定答应这个请求,要经过权衡。
(3)static:分为两种:一种是在具有代码块作用域的变量之前使用,会使该变量具有静态存储时期,从而得以在程序运行期间存在并保留其值,以供以后调用时能够使用上次调用后的值,变量具有代码块作用域和空连接;一种是在具有文件作用域的变量之前使用,表明该变量具有内部链接。
(4)extern:表明是在声明一个已经定义的变量。如果包含extern的声明具有文件作用域,所指向的变量必然具有外部链接;如果包含extern的声明具有代码块作用域,则所指向的变量可能具有外部链接也可能具有内部链接,这取决于该变量的定义声明,即如果该变量时本文件中定义的,则是内部链接;如果该变量时在别的文件中定义的,则是外部链接。
*****************************************************************************************************************************************************************************************
2013年1月15日更新
*****************************************************************************************************************************************************************************************
二六、malloc():参数:所需的字节数;返回值:分配内存的首地址;具体应用为:分配要求的内存空间,存储空间指向返回值以确保为任何类型的对象存储正确对齐;将返回的首地址赋值给一个指针变量,并用指针变量来访问分配的内存,若失败则返回NULL。
(void*)malloc(size_t * sizeof(数据类型))
void* 是将malloc()函数返回的地址强制转化成void型指针,当然在具体应用时可以强制转化成你想用的数据类型。
size_t*sizeof(数据类型):就是分配的字节数
注意:
在使用malloc分配的内存块之前,最好判断一下是否分配成功,因为有可能分配不足;
需要的头文件:stdlib.h 和 malloc.h
二七、free():参数:内存块的首地址;作用:释放由malloc()函数分配的内存空间,防止内存泄露。
二八、calloc():
(void*)calloc(size_t num,size_t size);
具体应用:
int * pt;
pt = (int *)calloc(100,sizeof(int));
这个函数接受两个参数,都是无符号的整数;第一个参数100表示所需内存单元的数量,第二个参数sizeof(int)是每个单元以字节计的大小。
在此例子中,int型数据每个占4个字节,则共分配400个字节
注:calloc()函数会将分配的内存中的所有单元都置为0。某些硬件系统中,浮点值0不是用全部为0来表示的。
函数free()也可以释放calloc()函数分配的内存。
动态分配内存在调用malloc()或者相关函数时产生,在调用free()时释放,由程序员而不是一系列固定的规则控制内存持续时间,因此内存块可能在一个函数中创建,而在另外一个函数中释放,由于这一点,动态分配的内存可能变成碎片状,也就是在活动的内存块之间散布着未使用的字节片,不管怎样,使用动态分配内存往往导致进程比使用堆栈内存慢;
二九、ANSIC的类型限定词:
(1)const:不变性
const:如果变量定义中带有关键字const,则不能通过赋值、增量或者减量运算来修改该变量的值。
其中对指针变量使用const要清楚是对指针变量本身使用const还是对指针变量指向的对象使用const:对指针变量使用const就是指针变量存放的地址不可变,但是这个地址中的内容是可变的;对指针变量指向的对象使用const就是指针变量指向数据对象是不可变的,但是指针变量存放的地址不可变,即若是地址发生变化,则此地址中存放的值和原来一样。
例如:const int*p;则*p不可变,即指针变量p指向的数据对象*p不可变;这个等同于int const * p;
int * const p:则指针变量p不可变,但是p指向数据对象*p可以发生变化
const int* const p:指针变量p和指针变量p指向的数据对象*p都不可变
对全局变量使用const:因为全局变量可以被所有的文件调用,所以容易被改动,但是如果加上const关键字,则可以避免这种风险。
(2)volatile:易变性
volatile:告诉编译器,除了可被程序改变之外,还可以被其他代理改变;典型的应用为:它被应用于硬件地址和并行运行的程序共享的数据;例如:一个地址中可能保存当前的时钟时间;不管程序做些什么,该地址内的时间数据都会随着时间改变;另外一种情况就是一个地址被专门用来接收其他计算机的信息。
一个变量可以同时使用const 和volatile关键字,const关键字是告诉本地程序不能修改,volatile是告诉可以由外部代理修改。
(3)restrict:
只可用于指针,表明该指针是访问一个数据对象的唯一且初始的方式;
例如:
int arr[100];
int * restrict ptr = (int *)malloc(100 * sizeof (int));
int *pt = arr;
ptr是指向由malloc()分配的内存块的唯一指针;
而arr[10]数组可以由多个指针指向。
例如:
for(i = 0; i < 10; i++)
{
pt[n] + = 5;
ptr[n] + = 5;
arr[n] + = 2;
` ptr[n] + =3;
pt[n] + = 3;
}
在上例中,由于编译器知道了指针变量pt是指向数据块的唯一指针,编译器可以用具有相同效果的一条语句来代替包含pt的两条语句:
pt[n] + = 5;
pt[n] + =3;
------>pt[n] + = 8;
然而两条包含ptr的语句却不能精简为一条语句,因为两条ptr访问同一数据块之间,使用arr改变了该数据块的值;
pt[n] + = 5;
arr[n] + = 2;使用这个语句改变了该数据块
pt[n] + = 3;
三十、文件相关:
(1)文件:文件通常是磁盘上的一段命名的存储区。
C将文件看成是连续的字节序列,其中的每一个字节都可以单独的读取。
ANSI C提供了两种文件视图:文本视图、二进制视图;
二进制视图中,每个字节都可以被程序访问;文本视图中,程序看到的内容和文件内容可能有所不同。
(2)标准文件:C程序自动打开3个文件:标准输入、标准输出、标准错误输出
默认的标准输入是系统的一般输入设备,通常为键盘;
默认的标准输出和标准错误输出是系统的一般输出设备,通常为显示器;
标准输入:getchar()、gets()、scanf()读取的文件
标准输出:putchar()、puts()、printf()
标准错误输出提供一个可供发送错误信息的位置。
(3)I/O级别
分为两个级别:
低级I/O:使用操作系统提供的基本I/O服务
标准高级I/O:使用一个标准的C库函数包和stdio.h头文件中的定义。
由于无法保证所有的操作系统都支持相同的低级I/O,所以ANSI C只支持标准I/O
(4)标准I/O
标准I/O相对于低级I/O有两点优势:
一是标准I/O包含很多专用的函数,可以方便处理不同的问题:如上面的标准输入、标准输出函数
二是对输入和输出进行缓冲:大块的转移信息(通常每次不少于512个字节),而不是每次一个字节进行转移;例如,当程序读入一个文件时,会把一大块数据复制到缓冲区即一块中介存储区中。这样的缓冲大大提高了数据传输率。随后程序就可以分析缓冲区中的个别字节。缓冲过程是在后台处理的,所以你会有逐字节读取的错觉。
(5)exit()函数:
函数原型:void exit(int status)
exit()函数:关闭所有打开的文件并终止程序。通常约定正常终止程序时返回0,非正常终止的程序传递非0值。
exit()函数返回的参数会被传递给一些操作系统,包括UNIX、Linux和MS DOS,但并非所有的操作系统都识别相同范围的可能返回值。
ANSI C标准规定使用一个相当有限的最小范围:具体的说就是要求使用值0和EXIT_SUCCESS来指示程序成功终止;使用EXIT_FAILURE来指示程序非成功终止。
exit()和return 的不同是:
一是如果main函数在一个递归调用程序中,exit()仍然会终止程序,而return将控制权交给递归的上一级,知道最初的一级,return才会终止。
二是如果在main函数之外的函数调用exit()函数,它也将终止程序。
(6)fopen()函数
函数原型:
FILE *fopen( const char *filename, const char *mode );
fopen函数包括两个参数:
filename:包含将要打开的文件名的字符串地址
mode:用于指定文件打开模式的一个字符串
模式种类分为:
打开文本文件,读取文件;当然文件不存在的话,就会返回错误
打开一个空文本文件,写入文件;如果文件存在,则内容会被清0;如果文件不存在,则先创建文件并命名为传递的文件名
Opens for writing at the end of the file (appending) without removing the EOF marker before writing new data to the file; creates the file first if it doesn't exist.
打开文本文件,并向已有的文件末尾追加内容,在写入新数据之前,文件末尾的EOF不会删除;如果该文件不存在则先创建文件
打开文件,可以进行读和写,但是文件必须存在才能进行此操作;
打开文件,进行读和写,文件内容在读和写之前会被清除,但是文件必须存在才能进行此操作
打开文件,进行读和写,向已有的文件末尾追加内容,如果该文件不存在则先创建,可以读取整个文件,但是写入时只能在文件末尾追加内容
注:如果在各种模式后面加上b,即”rb“、”wb“等,就是使用二进制模式而非文本模式,但是对于linux和unix这种只有一种文件类型的系统来说,都是一样的。
使用任何一种”w“模式打开文件,文件原有内容将被删除,以便程序以一个空文件开始操作。
fopen函数的返回值:
返回指向打开文件的文件指针变量;
如果不能打开文件,fopen()函数返回空指针NULL;可能存在的问题一般包括:磁盘已满,文件名非法,存储权限不够,或者是硬件问题
(7)文件指针变量
文件指针变量是一种指向FILE的指针变量;FILE是stdio.h中定义的一种派生类型。文件指针变量并不指向实际的文件,而是指向一个关于文件的信息的数据包,其中包括文件I/O使用的缓冲区信息。
因为标准库中的I/O函数使用缓冲区,所以他们需要知道缓冲区的位置,还需要知道缓冲区的当前缓冲能力以及所使用的文件。这样这些函数在必要的时候就可以再次填充或者清空缓冲区;而文件指针变量指向的数据包包含这些全部信息;其实这里所谓的数据包其实一个数据结构,上面的信息都以成员变量的形式存在这个数据包中。
*****************************************************************************************************************************************************************************************
2013年1月16日更新
*****************************************************************************************************************************************************************************************
(8)getc() 函数和putc()函数
getc()
函数原型:char getc(FILE *stream)
函数的功能是从指定的文件输入一个字符;
函数参数是:文件指针
函数返回值:返回从文件中读到的字符;若是达到文件结尾,则返回EOF
putc()
函数原型:char putc(char ch ,FILE *stream)
函数功能:将字符ch写到文件stream中
函数参数:字符、文件指针
函数返回值:
这两个函数与getchar()和putchar()函数不同的就是需要指定文件,而getchar()默认从键盘输入,putchar()默认从屏幕输出
(9)fclose()函数
函数原型:int fclose(FILE * stream)
函数参数:将要关闭的文件指针变量
函数返回值:成功关闭则返回0,失败则返回EOF。
发生错误的一般原因:磁盘损坏、磁盘已满、磁盘被移动或者I/O出现错误等
(10)标准文件指针
stdio.h文件将三个文件指针和3个C程序自动打开的标准文件进行了关联
标准文件 |
文件指针 |
一般使用的设备 |
标准输入 |
stdin |
键盘 |
标准输出 |
stdout |
屏幕 |
标准错误输出 |
stderr |
屏幕 |
stdin、stdout和stderr都是文件指针变量
每个文件的打开和关闭都是相互独立的,所以要为每个打开的文件定义一个文件指针变量;文件可以同时打开,但是同时打开的文件数目是有限的,通常是10到20之间;
可以使同一个文件指针变量指向不同的文件,但是这些文件不能同时打开。
(11)fprintf()和fscanf()函数
文件I/O函数fprintf()和fscanf()函数与前面的printf()和scanf()函数类似,区别在于前两者需要第一个参数来指定合适的文件,即使用文件指针变量来指定要操作的文件。
fprintf()和printf()函数:
fprintf()是将结果按照指定的格式打印到文件指针指定的文件中去,而printf()是将结果按照指定格式打印到默认的输出文件即屏幕。
fscanf()和fprintf()函数:
fscanf()是从文件指针变量指定的文件夹读取指定格式的数据,并存放在指定的地址,而scanf()是将从键盘读取指定格式的数据,并存放在指定的地址
(12)fgets()和fputs()
fgets函数:
函数原型:char* fgets(char *str,int n,FILE *stream)
函数功能:从FILE型文件指针变量stream指定的文件中,获取n个字符,存放在str中。
函数参数:str:数据存储的位置;n:读取的最大字符数;stream:FILE型结构指针,即指定的文件。
fgets()函数读取到他遇到的第一个换行字符的后面,或者读取n-1个字符,或者读到文件尾;然后fgets()函数在存放在str中的字符串结尾放一个空字符”\0“以构成完整的字符串;
fgets()函数与gets()函数的不同:
如果fgets()函数在达到n-1之前读完了一整行,会添加一个换行符以标识一行结束;这是fgets()函数和gets()函数不同,gets()函数会将读取的换行符丢弃。
相同点:fgets()函数遇到EOF的时候就会返回NULL值,可以据此检查文件结尾;否则,它返回传递给他的地址值。
fputs()函数
函数原型:int fputs(const char* str,FILE* stream)
函数功能:将字符串str写入FILE文件指针变量指向的文件
函数参数:str:将要写的字符串;stream:指定写入的文件
fputs()和puts()
fputs()在输出时不会添加换行符;而puts()函数会在一行结束时添加一个换行符;
注:
一是fgets()和fputs()是一对,fgets()会保留输入时的换行符,而fputs()在输出时不会添加换行符
二是gets()和puts()是一对,gets()会在输入字符串时丢弃换行符,而puts()会在输出时添加换行符。
(13)fseek()和ftell()
fseek()函数
函数原型:int fseek(
FILE *stream,
long offset,
int origin
)
函数功能:将文件指针变量指向特定的位置
函数参数:stream:指向特定文件的文件指针变量
offset:偏移量,表示从起点origin开始移动的距离,单位是字节;这个值必须为long型,为正则是向前移,为后则是向后移;
origin:起点,共有三种模式:
SEEK_SET:文件开始
SEEK_CUR:当前位置
SEEK_END:文件末尾
返回值:如果成功,则返回0;失败则返回-1;
fseek( stream, 23L, SEEK_SET);从文件起始位置向后移动23个字节
fseek( stream, -23L, SEEK_END);从文件末尾向前移动23个字节
fseek( stream, 23L, SEEK_CUR);从当前位置向后移动23个字节
ftell()函数:
函数原型:long ftell(FILE* stream)
函数功能:返回文件的当前位置
函数参数:stream:指向当前文件的文件指针变量
返回值:成功则返回当前的偏移量;错误则返回-1
(14)UNIX和MS-DOS的区别
第一文件模式:
UNIX只有一种文件格式;
MS-DOS编辑器使用字符Ctrl+Z作为标识文本文件的结尾。如果以文本模式打开,C可以识别出标识文件结尾的字符;可是以二进制模式打开,那C只会把Ctrl+Z作为文件中的一个字符,真正的结尾也许在后面,也许紧跟着Ctrl+Z,也许用空字符填补文件以使它的大小为256的倍数,在DOS下不打印空字符,程序中包含了防止程序打印Ctrl+Z的代码
第二是文件结尾标识
MS-DOS使用\r\n组合表示文本文件的换行符,以文本文件模式打开文件的C程序将\r\n看做\n;但是如果使用二进制模式打开文件,程序将把这两个字符就看做\r和\n;所以要在程序中包含防止打印\r的代码。
因为UNIX文本文件通常不包括Ctrl+Z和\r,所以不影响。
注:ftell()函数在文本模式和二进制模式下工作方式有所不同:
(15)fgetpos()和fsetpos()
这两个函数的出现是因为fseek和ftell函数限制文件的大小只能是long型,即20亿个字节,但是日益增长的文件大小,long型不能满足要求。
fgetpos和fsetpos使用fpos_t的新类型代表位置,但是这个类型不是基本类型,而是通过其他类型定义的。
fgetpos()
函数原型:
int fgetpos(FILE* restrict stream,fpos_t* restrict pos)
被调用时,该函数在pos所指位置放置一个fpos_t类型的值;这个值描述了文件中的一个位置,如果成功返回0,失败返回非零值
fsetpos()
函数原型:
int fsetpos(FILE* restrict stream,const fpos_t* pos)
被调用时,该函数使用pos指向的位置上的那个fpos_t型数值设置文件指针指向该值所指示的位置。成功返回0,失败返回非零值。
(16)标准I/O相关
通常使用标准I/O的第一步就是调用fopen()函数打开一个文件(注:stdin、stdout、stderr文件是自动打开的)
在fopen()打开文件的过程中,创建了一个缓冲区(注:在读写模式下将建立两个缓冲区),并且创建一个包含文件和缓冲区相关数据的数据结构以及返回一个指向该数据结构的指针变量;
fopen返回的指针变量就是FILE型指针变量;
FILE型指针变量指向的数据结构都含有哪些成员变量:
struct _iobuf {
char *_ptr;指向当前缓冲区内容的指针
int _cnt;如果是输入缓冲区,那他就是显示现在缓冲区里还有多少有效数据
char *_base;缓冲区的基地址
int _flag;标志位:就是读和写的标志
int _file;文件句柄
int _charbuf;
int _bufsiz;缓冲区的大小,一般都是0x1000,也就是4k,也就是一个分页
char *_tmpfname;
};
typedef struct _iobuf FILE;
(17)缓冲区相关:
设置缓冲区的原因自然是为了提高效率,CPU总不能一直等待用户输入完毕或者是输入一个字符处理一个字符,这样效率就太低了,不如设置一个缓冲区,先将数据存放到缓冲区内,等到缓冲区满了或者用户输入完毕,再由CPU处理;
操作系统会自动的为标准输入和标注输出设置一个缓冲区,这个缓冲区的大小通常为4KB,这和计算机的分页机制有关,因为进程在计算机中分配内存使用的就是分页与分段相结合的机制,并且每一个页存储区的大小都是4KB,因此通常缓冲区的大小通常设置为4KB,这样不仅划分方便而且处理方便;
这个缓冲区的类型是一个全缓冲区,所谓的全缓冲区就是:当缓冲区里的数据写满后,缓冲区中的数据才会”写“到标准输入文件中,这里说的”写“不是将缓冲区中的数据移动到标准输入文件中去,而是复制到标准输入文件中,即在标准输入文件中的数据是缓冲区文件的备份;
缓冲区的类型不缓冲和行缓冲:不缓冲不常用;行缓冲就是当键盘输入回车键时,缓冲区中的数据就会复制到标准输入文件中
(18)输入函数对于缓冲区的操作:
指向数据结构的文件指针变量和缓冲区初始化之后,输入函数将从缓冲区读取请求的数据,同时文件位置指示器被设置为当前正在读取的字符的位置即最后一个被读取字符的位置,因为stdio.h中所有输入函数都是使用同一个缓冲区,所以任何一个被调用函数都将在上一个函数停止调用的位置继续开始操作。
当输入函数检测到已经读取了缓冲区中的全部字符时,他会请求系统将下一块缓冲区大小的数据复制到缓冲区;通过这种方式,输入函数可以读入文件中的全部内容,知道文件结尾;函数在读入最后一个缓冲区数据中的最后一个字符后,就会将文件结尾指示器设置为真,如实下一个被调用的输入函数会返回EOF。
源文件---------fopen打开文件----建立缓冲区、建立文件指针-----系统将源文件中缓冲区大小的数据块复制到缓冲区-----将缓冲区数据读到指定位置----读到文件结尾,返回EOF
(19)输出函数对缓冲区的操作:
输出函数将数据写入缓冲区,等到写满之后,将缓冲区数据复制到指定的文件中
目的文件---fopen打开文件---建立缓冲区、建立文件指针变量---将指定数据读到缓冲区----缓冲区已满,复制到目的文件----源文件已读完,返回EOF
(20)int ungetc(int c,FILE*fp)函数
函数功能:将c指定的字符放回输入流中;
(21)int fflush()函数
函数原型:
int fflush( FILE*fp)
调用fflush函数可以将缓冲区中任何未写的数据发送到一个由fp指定的输出文件中区。这个过程称为刷新缓冲区;如果fp是一个空指针,则刷新掉所有的输出缓冲,对于一个输入流使用fflush()没有效果。
(22)int setvbuf()函数
函数原型:
int setvbuf(FILE* restrict fp,char* restrict buf,int mode ,size_t size )
函数功能:建立一个供标准I/O函数使用的替换缓冲区。打开文件以后,在没有对流进行任何操作之前,可以调用这个函数。
fp 指定数据流流,
buf 指向即将使用的存储区。如果buf不指向NULL,就必须创建这个缓冲区。
size 变量即为创建的缓冲区的大小;
mode 为缓冲类型:_IOFBF:完全缓冲;_IOLBF:行缓冲;_IONBF:无缓冲;
函数返回值:成功则返回0,失败则返回非零值
*****************************************************************************************************************************************************************************************
2013年1月17日更新
*****************************************************************************************************************************************************************************************
(23)二进制模式下的I/O函数:fread()和fwrite()函数
fwrite()函数:
size_t fwrite(
const void *buffer,
size_t size,
size_t count,
FILE *stream
);
buffer:指向将要被写数据的缓冲区首地址
size:表示要写入的数据块的大小
count:表示要写入的数据块的个数
stream:表示要写入的文件指针变量;
返回值:成功写入的数据块的个数,即count;
fread()函数:
size_t fread(
void *buffer,
size_t size,
size_t count,
FILE *stream
);
buffer:表示存放读取后数据的缓存
size:表示读取数据块的大小
count:表示读取数据块的个数
stream:表示将要读的文件
返回值:成功读取的数据块个数,即count
当标准输入函数返回EOF时,通常表示已经到达了文件结尾,可是,这也有可能表示发生了读取错误。
使用feof和ferror函数可以区分这两种可能性:
如果最近一次输入调用检测到文件结尾,feof()函数返回一个非0值,否则返回零值;
如果数据流中没有错误,则ferror()函数返回一个0值;否则返回一个非零值
int feof(
FILE *stream
);
int ferror(
FILE* stream
);
(25)总结:
C程序将输入看做字节流,流的来源可以是文件、输入设备(如键盘),甚至是另一个程序的输出等;同样,C程序也将输出看做字节流;流的目的地可以是文件、视屏显示等。
C如何解释输入字节流和输出字节流依赖于所使用的输入输出函数。程序可以不加改动的读取或者存储字节,也可以将字节解释为字符,这样就可以将这些字符解释成普通的文本或者数字的文本表示。与之类似,对于输出,所使用的函数决定了是将二进制值不加改动的转移,还是将其转换成文本或者数字的文本表示。如果需要在不损失精度的前提下保存或者回复数字数据,请使用二进制模式,并利用fread()和fwrite()函数;如果是保存文本信息或者是要创建可以用普通文本编辑器查看的文件,请使用文本模式和诸如个getc()、fprintf()之类的函数。
要存取文件,需要创建一个文件指针变量,并将其和一个具体的文件名关联起来,后续代码就可以使用这个指针变量而不是文件名来处理这个文件。
C处理文件结尾:通常一个读取文件的程序使用循环读取输入、直到遇到文件结尾。C输入函数直到尝试读取超出文件结尾的时候才会检测到文件结尾。这意味着应该在一次尝试读取之后立即进行文件结尾判断。
三一:结构体相关
1、初始化具有静态存储时期的变量,只能使用常量值;对于结构体来说,如果结构体具有静态存储周期,则初始化项目列表中的值必须是常量表达式。
2、结构体成员运算符(.)的优先级比&要高。
3、例如一个结构体:
struct book{
char name[60];
char author[60];
int value;
}
可以进行部分初始化:形式为:
struct book sun={.value = 20};
可以按照任意的顺序指定初始化项目:
struct book lu = { .value = 30, .name = "nihao",.author = "hao"}
另外对于多次声明的特定成员的最后一次赋值是它实际获得的值;
struct book lu = { .value = 30, .name = "nihao",author = "hao",25}
则value的值为25,因为author成员后面紧跟的value成员,
4、在使用指针变量访问结构体成员时,不能使用结构体成员运算符(.),只能使用->,同样,结构体变量在访问结构体成员时,不能使用->,只能使用结构体成员运算符(.)
5、允许 同类型的结构体之间的相互赋值。
6、通常若是为了追求效率而使用结构指针作为函数参数;当需要保护数据、防止意外改变数据时对指针使用const限定词,传递结构值是处理小型结构最常用的方法。
*****************************************************************************************************************************************************************************************
2013年1月31日更新
*****************************************************************************************************************************************************************************************
7、union:共用体,或者叫联合;就是一个能在同一个存储空间里存储不同类型数据的数据类型,但是同一时间只能存放一种数据类型,即使有足够的空间。
点运算符表示正在使用哪种数据类型。如同指向结构体的指针变量一样,指向共用体的指针变量可以使用->运算符。
8、struct {
int name;
float years;
} man, *people;
people = &man;
以下三种表达方式是等价的:
people->name; man.name;(*people).name
9、枚举:使用枚举类型声明代表整数常量
通过使用关键字enum,可以创建一个新“类型”并指定它可以具有的值,实际上enum常量是int型,因此在使用int类型的任何地方都可以使用它。枚举类型的目的是提高程序的可读性。
C的某些枚举属性不能延至C++中,C允许对枚举变量使用运算符++,而C++不允许;
(1)默认时,枚举列表中的常量被指定为整数值0、1、2等,因此:
enum days{mon,tue,wend,tir,mar,sta,sun}中,默认值为:mon = 0;tue = 1;wend =2;tir =3; mar = 4; sta =5; sun =6
(2)指定值,对某个常量指定值时,后面的常量就会一次增加1;例如:
enum days{mon,tue = 8,wed,tir = 20,mar}中,mon = 0;wed =9;mar =21;
10、typedef和define的不同之处:
(1)与#define不同的是:typedef给出的符号名称仅限于对类型,而不是对数值。
(2)typedef的解释由编译器,而不是预处理器执行。
(3)虽然它的范围有限,但在其受限范围内,typedef比#define更灵活。
例如:typedef unsigned char BYTE;
那么你就可以使用BYTE 来定义类型了:BYTE x,*y,z[2];
typedef char* STRING;
STRING name,year;
即为:char* name,*year;
#define STRING char*;
STRING name,year;
即为:char*name,year;
11、一个指针指向一个int型变量,它保存的是这个int型变量在内存中存储空间的首地址,同样,函数也有地址,这是因为函数的机器语言实现是载入到内存的代码组成,指向函数的指针保存着函数代码起始处的地址。
当声明一个数据指针时,必须声明它指向的数据类型;同样的,当声明一个函数指针时,必须声明它指向的函数类型,要指定函数类型,就要指出函数的返回值类型以及函数的参量类型。
当使用函数指针时,应该吧同样类型的函数地址赋给它,此时,函数名就可以用来表示函数的地址。
12、要想得到结构的地址,可以使用运算符&,与数组不同的是,结构体名字不是结构体的地址。
三二、位操作
1、正数的原码、补码、反码都等于它本身;
负数的反码是除了最高位的符号位不变外,其余各位求反
负数的补码是反码+1;
2、位逻辑运算符:
(1)按位取反:~
~(01101001) = 10010110
(2)与运算符:&
(3)或运算符:|
(4)位异或:^
3、移位运算符
(1)左移:<<
左移运算符<<将其左侧的操作数的每位向左移动,移动的位数由右侧操作数指定。空出的位用0填充,并且丢弃移出左侧操作数的位。
(2)右移:>>
右移运算符>>将其右侧的操作数的每位向右移动,移动的位数由其右侧操作数指定。并且丢弃移出右侧操作数右端的位。
注意:右移时,左侧填充的数字可能不同,对于unsigned类型来说,使用0填充左端空出的位,对于有符号类型,结果依赖于机器,可能用0填充,或者使用符号位填充。
例子:
(10001010)>>2:
(00100010)
或者:
(10001010)>>2
(11100010)
4、对位操作的方法还有位字段(bit field),位字段是一个signed int 或者unsigned int中一组相邻的位。位字段有一个结构声明建立,该结构声明为每个字段提供标签,并决定字段的宽度。
例如:
struct{
unsigned int autfd:1;
unsigned int bldfc:1;
unsigned int undln:1;
unsigned int itals:1;
}prnt;
该定义是prnt包含了4个1位字段。现在,可以使用普通的结构体成员运算符将值赋给单独的字段,由于都是一位字段,所以只有0和1可以赋值:
prnt.bldfc = 0;
prnt.itals = 1;
或者是多位段声明:
struct {
unsigned int code1:2;
unsigned int code2:2;
unsigned int code3:8;
}pp;
赋值为:
pp.code1 = 0;
pp.code2 = 3;
pp.code3 = 105;
如果声明的总位数超过一个unsigned int大小,那么就会使用下一个unsigned int存储位置,不允许一个字段跨越两个unsigned int之间的边界;编译器会自动移位跨越边界的字段,是字段边界对齐,这时,会在第一个unsigned int中留下一个未命名的洞。
三三、C预处理和C库
1、翻译程序的过程:
(1)编译器首先把源代码中出现的字符映射到源字符集。
(2)编译器查找反斜线后紧跟换行符的实例并删除这些实例。
(3)编译器将文本划分成预处理的语言符号序列和空白字符及注释序列。
2、预处理器指令从#开始,到期后第一个换行符为止,也就是说,指令的长度限于一行代码。在预处理之前,系统会删除反斜线和换行符的组合,因此可以把指令扩展到几个物理行,由这些物理行组成单个逻辑行。
3、每个#define行由三部分组成:第一部分是#define自身,第二部分是宏,宏名字中间不允许有空格,必须遵循C变量命名的规则。第三部分是替换列表
4、系统把宏的主体当做语言符号类型字符串,而不是字符型字符串。C预处理器中的语言符号是宏定义主体中的单独的词,用空白字符把这些词分开。
在处理主体中的多个空格时,字符型字符串和语言符号类型字符串采用不同方法:
#define EIGHT 4 * 8
把主体解释为字符型字符串时,预处理器用4 * 8;来代替EIGHT,也就是说,额外的空格也当做替换文本的一部分。但是,当把主体解释为语言符号类型时,预处理器用单个空格分隔的三个语言符号,即4 * 8来替换EIGHT。换句话说,用字符型字符串的观点来看,空格也是主体的一部分;而用语言符号字符串的观点来看,空格只是分隔主体中语言符号的符号。
5、在#define中使用参数:
(1)通过使用参数,可以创建外形和作用都与函数相似的类函数宏(function-like macro)。宏的参数也用括号扣起来,因此,带参数的宏外形与函数非常相似。
注意:
由于预处理器只做字符串的替换,不进行计算,所以所有的参数都用括号括起来,以免发生错误。
避免在宏定义中使用自增和自减运算符:++ --
(2)在类函数宏的替换部分中,#符号用作一个预处理运算符,它可以把语言符号转化为字符串。例如,如果x是一个宏参量,那么#x可以把参数名x转换为相应的字符串“x”,
该过程称为字符串化。
例如:#define PR(x) printf("The square of "#x"is %d.\n",((x)*(x)))
#x被替换位“x”,即printf("The square of " “x”"is %d.\n",((x)*(x)))
接着,字符串连接功能将这三个字符串连接成一个字符串。
(3)预处理器的粘合剂:##运算符
这个运算符把两个语言符号组合成单个语言符号。
例如:
#define XNAME(n) x##n
调用XNAME(4)时,就会展开为:x4
(4)可变宏:...和__VA_ARGS__
实现思想就是宏定义中参数列表的最后一个参数为省略号,预定义红__VA_ARGS__就可以被用在替换部分中,以表明省略号代表什么:
#define PR(...)printf(__VA_ARGS__);
PR("How are you");
展开后为:
printf(“How are you”);
注意:省略号只能代替最后的宏参数,下面的定义是错误的:
#define WRONG(X,...,Y)#X#__VA_ARGS__#Y
宏和函数之间的选择实际上是时间和空间的权衡,宏产生内联代码,占用空间多,但是时间消耗少;
函数节省空间,但是会增大时间开销;
注意:
(1)宏名中不能有空格,但是在替代字符串中可以使用空格;
(2)用圆括号括住每个参数,并括住宏的整体定义。
(3)用大写字母表示宏函数名。
6、#include命令
(1)#include命令有两种:
#include
#include "my.h"
在UNIX系统中,尖括号告诉预处理器在一个或多个标准系统目录中寻找文件,双引号告诉预处理器现在当前目录(或者是文件名指定的其他目录)中寻找文件,然后再标准位置寻找文件。
对于系统头文件,集成开发环境(IDE)具有标准搜索路径,许多集成开发环境提供菜单选项用于指定使用尖括号时搜索的其他路径。对于UNIX,使用双引号意味着首先搜索本地目录,但是具体搜索那个目录依赖编译器。有些编译器搜索源代码文件所在目录,有些则搜索当前工作目录,还有些搜索工程所在文件目录。
很多情况下,头文件中的内容是编译器产生最终代码所需的信息,而不是加到最终代码里的具体语句。
头文件内容的最常见内容形式包括:
明显常量:例如stdio.h文件定义的EOF、NULL和BUFFSIE(标准I/O缓冲区的大小)
宏函数:
函数声明:
结构体定义:
类型定义
7、其他命令
预处理器提供一些指令来帮助程序员编写出这样的代码:改变一些#define宏的值后,这些代码就可以从一个系统移植到另一个系统。
#undef指令取消前面#define定义。#if、#ifdef、#ifndef、#else、#elif、和#endif用于选择在什么情况下编译哪些代码。#line用于重置行和文件信息,#error用于给出错误信息,#pragma指令用于向编译器发出指示。
(1)#undef取消一个给定的#define,例如:
#define LIMIT 400
#undef LIMIT则会取消前面的定义,后面就可以重新定义LIMIT。
(2)在现代编译器中,可用命令行参数或者IDE菜单修改编译器的某些设置,也可以用#pragma将编译器指令置于源代码中。
(3)内联函数
通常函数调用需要一定的时间开销。这意味着执行调用时花费了时间用于建立调用、传递参数、跳转到函数代码段并返回。使用类函数宏的一个原因就是可减少执行时间。
C99标准定义内联函数:是函数变为内联函数可能会简化函数的调用机制,但也可能不起作用。
创建内联函数的方法是在函数声明中使用函数说明符inline。通常首次使用内联函数前在文件汇总对函数进行定义。因此,该定义也作为函数原型。
注:因为没有给内联函数预留单独的代码块,所以无法获得内联函数的地址,实际上可以获得地址,但这样会是编译器产生内联函数,另外,内联函数不会再调试器中显示。
内联函数应该比较短小。对于很长的函数,调用函数的时间少于执行函数主体的时间,此时,使用内联函数不会节省多少时间。
编译器在优化内联函数时,必须知道函数定义的内容。这意味着内联函数的定义和对该函数的调用必须在同一文件中。正因为这样,内联函数通常具有内部链接。因此,在多文件程序中,没给盗用内联函数的文件都要对该函数进行定义。达到这个目标的最简单的方法为:在头文件中定义内联函数,并在使用该函数的文件中包含该头文件。一般不在头文件中放置可执行代码,但内联函数是个例外。因为内联函数具有内部链接,所以在多个文件中定义同一内联函数不会产生什么问题。
C允许混合使用内联函数定义和外部函数定义。具体调用中,编译器可以随意使用该函数的内部定义或者外部定义,甚至两次调用所使用的定义可以不一致。
8、通用工具库:
通用工具库包含各种函数:其中包含随机数产生函数、搜索和排序函数、转换函数和内存管理函数。
(1)aexit()函数
该函数使用函数指针;要使用atexit()函数,只需把退出时调用的函数地址传递给atexit()。因为函数名在作为函数参数时,代表地址,于是atexit()把作为其参数的函数在调用exit()时执行的函数列表中进行注册。ANSI保证这个列表中至少可放置32个函数。通过使用一个单独的atexit()调用把每个函数添加到列表中。最后,调用exit()函数时,按先进后出的顺序执行这些函数。
由atexit()注册的函数类型应为不接受任何参数的void函数。通常他们执行内部处理任务。
注意:main()终止时会隐式的调用exit()。
(2)exit()函数
exit()执行了atexit()指定的函数后,将做一些自身的清理工作。它会刷新所有输出流、关闭所有打开的流,并关闭通过调用标准I/O函数tmpfile()创建的临时文件。然后,exit()把控制返回主机环境(如果可能,还向主机环境报告终止状态)。习惯上,UNIX程序用0表示成功终止,用非零值表示失败。
UNIX返回的代码并不适用与所有系统;在非递归的main()函数中使用exit()函数等价于使用关键字return。
9、诊断库
有头文件assert.h支持的诊断库是设计用于辅助调试程序的小型库。它由宏assert()构成。该宏接受整数表达式作为参数。如果表达式值为假(非0),宏assert()向标准错误流(stderr)写一条错误消息并调用abort()函数以终止程序(在头文件stdlib.h中定义了abort()函数的原型)。
assert()宏的作用为:标示出程序中某个条件应为真的关键位置,并在条件为假时用assert()语句终止该程序。
通常,assert()的参数为关系或者逻辑表达式。如果assert()终止程序,那么首先它会显示失败的判断、包含该判断的文件名和行号。
assert()好处在于:能自动识别文件,并自动识别发生问题的行号。另外,还有一种无须改动代码就能开启或者禁用assert()宏的机制;如果你认为已经派出了程序的漏洞,那么可把宏定义
#define NDEBUG
放在assert.h包含语句所在位置前,并重新编译该程序。编译器将禁用文件中所有的assert()语句。如果程序又出现问题,可以去除这个#define指令并重新编译,这样就重新启用了assert()语句。
10、memcpy和memmove
memcpy和memmove都可以针对任何数据类型进行复制
二者的区别是:
(1)memcpy在两个内存区域之间没有重叠时可以正确复制,但是若是有重叠就可能会发生错误。
memmove在两个内存区域之间有重叠时也可以正确复制
(2)memcpy成功进行复制后,两块内存区域的内容一致,
memmove成功复制后不能保证源数据区域保持原样,可能会发生改变
三四、高级数据表示
1、在使用malloc()分配存储空间时,要检查一下是否分配成功,否则有可能出错。
2、数据类型由两类信息组成:一个属性集、一个操作集;
定义一种新的数据类型,首先提供存储数据的方式;二是提供操作数据的方式;
3、定义新类型的方法:
(1)为类型的属性和可对类型执行的操作提供一个抽象的描述;该数据类型称为抽象数据类型(ADT)
(2)开发一个实现ADT的编程接口:及说明如何存储数据并描述用于执行所需操作的函数集合。
(3)编码实现。
*****************************************************************************************************************************************************************************************
2013年2月1日更新
*****************************************************************************************************************************************************************************************
4、列表类型总结:
类型属性: 可保存一个项目序列
类型操作: 可把列表初始化为空列表
确定列表是否为空
确定列表是否已满
确定列表中项目的个数
向列表末尾添加项目
遍历列表,处理列表中的每个项目
清空列表
(1)简单列表的借口分为两部分:
第一部分是描述数据如何表示
第二部分描述实现ADT操作的函数。
借口的设计应和ADT的描述尽可能保持一致,因此,应该用某种通用的Item类型来进行表达,这样做的方法就是使用C的typedef工具将Item定义为所需类型:
*****************************************************************************************************************************************************************************************
2013年2月2日更新
*****************************************************************************************************************************************************************************************
第二遍看
一、编程的七个步骤
1、定义程序目标
在设计程序之初,应该对程序要做什么有一个清晰的想法;在这一段,不妨用人类语言来描述问题,而不是使用计算机语言。
2、设计程序
第一步将问题描述出来之后,下一步就是通过思考怎么才能解决目标,这一步也是使用人类语言进行描述,而不是使用计算机语言
3、编写代码
这一步是将想法用计算机语言实现的过程。
4、编译
这一步就是将编程语言转换成机器语言,即变成纯粹的二进制
5、运行程序
6、测试和调试程序
一个好的程序员必定是一个调试高手,除非你是一次成功。
7、维护和修改程序
对程序作出清楚的文字注释。
二、目标代码文件、可执行文件和库
C编程的基本策略就是使用程序将源代码文件转换为可执行文件,此文件包含可以运行的机器语言代码。
C分两步完成这一工作,编译和链接:
编译器将源代码转换为中间代码;链接器将此中间代码和其他代码相结合生成可执行文件。
C划分成两步,易于是程序便于模块化,你可以分别编译各个模块,然后使用链接器将编译过的模块联合起来,这样如果需要改变一个模块,就不用重新编译所有其他模块,同时,链接器将使你的程序与预编译的库代码结合起来。
中间文件的形式一般选择是:将源代码转换为机器语言代码,放置在目标代码文件中,虽然目标文件中包含机器语言代码,但是该文件还不能运行,因为它还不是一个完整的程序。
目标代码文件缺少的第一个元素叫做启动代码,此代码相当于你的程序与操作系统之间的接口。
目标代码文件缺少的第二个元素叫做库例程的代码,几乎所有的C程序都利用标准C库中所包含的例程。
链接器的做用就是将这3个元素:目标代码、系统标准启动代码和库代码结合在一起,并将他们放在单个文件,即可执行文件中。
目标文件和可执行文件都是由机器语言指令组成的,但目标文件只包含您所编写的代码转换成的机器语言。而可执行文件还包括启动代码的机器代码和目标程序所使用的库例程的机器代码。
*****************************************************************************************************************************************************************************************
2013年2月3日更新
*****************************************************************************************************************************************************************************************