第十一章 高级指针话题
指向指针的指针
int i;
int *pi;
int **ppi;
变量i 是一个整数,pi是一个指向整型指针,ppi是一个指向pi的指针,所以它是一个指向整型的指针的指针。
ppi = π这条语句把ppi初始化为指向变量pi。
*ppi = &i;这条语句把pi(通过ppi间接访问)初始化为指向变量i。经过上面两条语句之后:
现在,下面各语句具有相同的效果(都是将变量i的值赋值为10):
i = 10;
*pi = 10;
**ppi = 10;
经过上面赋值语句后,变量的内存关系结构:
为什么需要间接的访问呢?这是因为简单的赋值有进并不可行,比如在一个函数里想修改调用它的函数里的变量,则只能使用指针的形式来传递,对该指针进行间接访问操作可以访问需要修改的变量。上面的ppi可以改变i(通过**ppi修改)与pi(通过*ppi修改)两个变量的值。
高级声明
int* f,g; 它并没有声明两个指针,尽管它们之间存在空白,但星号是作用于f的,只有f才是一个指针,g只是一个普通的整型变量。
int (*f)(); f是一个函数指针:
int fun() {
return 1;
}
int main(int argc, char **argv) {
int (*f)();
f=fun;
printf("%d", (*f)());//1
}
如果作为函数的参数类型,则可能省略名称f,如:
void bubble(int *, const int, int(*)(int, int));
int (*f)()与int (*f())()区别
int (*f)(int); 是一种类型,定义了一个函数指针类型,指针变量名为f
int (*f())(int); 是一个函数原型,现在的f是一个函数名,这个函数的返回值是一个函数指针
函数指针
指向函数的指针包含了该函数在内存中的地址。函数名实际上是完成函数任务的代码在内存中的起始地址,就像数组名一样,是第一个元素在内存中的地址。
取数组的地址时,前面不需要加上&运算符,数组名就是地址,函数也一样,函数名就是函数地址,也不需要在函数名前加上&运算符。
指针的通用类型为void *,任何类型的指针都可以转换为void *,并且在将它转换回原来的类型时不会丢失信息。
int (*comp) (void *, void *)参数类型表明comp是一个指向函数的指针,该函数具有两个void*类型的参数,其返回值类型为int。不能去掉 comp 外层的括号,如果去掉了,则表示comp是一个函数,该函数返回一个指向int类型的指针。
int f(int);
int (*pf)(int) = &f;
初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针。&操作符只是显式地说明了编译器将隐式执行的任务(就像数组名一样,当数组名与&一起使用时,就不会将数组看作是一个常量指针,而是看作一个普通的变量)。我们可以使用三种方式来调用:
f(25);
(*pf)(25);
pf(25);
函数指针数组:指向函数的指针数组:void (*f[3])(int) = { 函数1, 函数2, 函数3 };,函数的调用方式如下:(*f[choice])(choice)
int *(*f[])(int, float);声明了一个指针数组,每个指针元素所指向的类型是返回值为整型指针的函数,而且这个函数还带两个参数类型。
函数指针最大的用处就是回调。函数指针像Java中的接口一样,可以动态的改变其运行时性为。下面一个冒泡排序,它会根据用户的选择来决定调用升序还是降序函数:
#include <stdio.h>
#define SIZE 10
void bubble(int *, const int, int(*)(int, int));
int ascending(const int, const int);
int descending(const int, const int);
//主程序
int main(int argc, char **argv) {
int a[SIZE] = { 2, 6, 4, 8, 10, 12, 89, 68, 45, 37 };
int counter, order;
printf("输入1使用升序排序,2使用降序:");
scanf("%d", &order);
for (counter = 0; counter < SIZE - 1; ++counter) {
printf("%4d", a[counter]);
}
if (order == 1) {
bubble(a, SIZE, ascending);
printf(" 使用升序排序结果:");
} else {
bubble(a, SIZE, descending);
printf(" 使用降序排序结果:");
}
for (counter = 0; counter < SIZE - 1; ++counter) {
printf("%4d", a[counter]);
}
return 0;
}
//冒泡排序算法
void bubble(int * work, const int size, int(*compare)(int, int)) {
int pass, count;
void swap(int *, int *);
for (pass = 1; pass <= size - 1; pass++) {
for (count = 0; count < size - 2; ++count) {
//动态的调用函数,也可像平时调用函数一样直接调用,
//如:compare(work[count], work[count + 1]),
//但最好像下面这样调用,这样可以很清楚的知道compare
//是一个函数指针,而不是一个普通的函数
if ((*compare)(work[count], work[count + 1])) {
swap(&work[count], &work[count + 1]);
}
}
};
}
//交换
void swap(int *element1Ptr, int * element2Ptr) {
int temp;
temp = *element1Ptr;
//将element2Ptr指向的变量的值赋给element1Ptr指向的变量
*element1Ptr = *element2Ptr;
*element2Ptr = temp;
}
//升序
int ascending(const int a, const int b) {
return b < a;
}
//降序
int descending(const int a, const int b) {
return b > a;
}
main命令行参数
调用main函数时,会有两个参数,第一个参数(argc)的值表示运行程序时命令行中参数的数目;第二个参数(argv)是一个指向字符串数组的指针,其中每个字符串对应一个参数。按照C语言的约定,argv[0]的值是启动该程序的程序名,因此argc的值至少为1。如果argc的值为1,则说明程序名后面没有命令行参数。另外,ANSI标准要求argv[argc]的值必须为空指针(即地址值为0)。
int main(int argc, char * argv[]) {...}
或者是:
int main(int argc, char ** argv) {...}
argv是一个指向字符串指针的指针:
/*
* 打印参数,路过程序名
*/
int main(int argc, char **argv) {
while (*++argv != NULL)
printf("%s ", *argv);
return EXIT_SUCCESS;
}
字符串常量
当一个字符串常量出现于表达式上时,它的值是个指针常量。编译器把这些指定字符的一份拷贝存储在内存的某个位置,并存储一个指向第1个字符的指针。另外,当数组名用于表达式中时,它的值也是指针常量。
"xyz" + 1,这个表达式的结果是一个指针,指向字符串中的第2个字符y
*"xyz",这个表达式的结果是一个字符:x
"xyz"[2],结果为一个字符:z
将某个数以16进制输出:
void to_hex(unsigned int value) {
unsigned int quotient = value / 16;
if (quotient != 0) {
to_hex(quotient);
}
unsigned int remainder = value % 16;
if (remainder < 10) {
putchar(remainder + '0');
} else {
putchar(remainder - 10 + 'A');
}
}
妙用:现在使用最简单的方法:
void to_hex(unsigned int value) {
unsigned int quotient = value / 16;
if (quotient != 0) {
to_hex(quotient);
}
putchar("0123456789ABCDEF"[value % 16]);
}
int main(int argc, char **argv) {
char * a[] = { "ab", "cd" };
char **p = a;
printf("%s ", *a);//ab
printf("%s ", *(p+1));//cd
printf("%s ", *(a+1));//cd
}
第十二章 预处理器
预定义符号
有一些是标准库已经定义好的符号,如:
__FILE__:进行编译的源文件名;
__LINE__:文件当前行的行号;
__FUNCTION__:函数名;
__DATE__:文件被编译的日期;
__TIME__:文件被编译的时间;
__STDC__:如果编译器遵循ANSI C,其值就是1,否则未定义;
宏的命名约定:宏的定义使全使用大写
#define
#define name stuff
#define指令把一个符号名与一个任意的字符序列联系在一起,这些字符可能是一个字面值常量、表达式或者是程序语句。
替换文本并不仅限于数值字面常量,可以把任何文本替换到程序中,如:
#define reg register
#define do_forever for(;;);
#define define CASE break;case
如果替换文本很长,可以分成几行来写,除了最后一行外,每行的末尾都要加一个反斜杠,如:
#define DEBUG_PRINT printf("File %s line %d:"
" x=%d, y=%d, z=%d",
__FILE__,__LINE__,
x, y, z)
int x = 1, y = x + 1, z = y + 1;
DEBUG_PRINT;//File ..srcinsert3.c line 84: x=1, y=2, z=3
#define甚至还可以定义一段代码
#define定时最后面最好不要加上分号,语法规定是没有分号的,如果加在了最后,则会将分号作为文本一起插入到替换位置,这样有时会出现问题,如替换在未使用{}的if语句中时就可能会出问题。
宏
被替换的文本中可以含有参数
#define name(parameter-list) stuff
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
#define SQUARE(x) ((x)*(x))
在定义时要注意,文本中的每个参数都要使用括号括起来,最后整体也需要使用括号括起来,否则如果参数传递的不是单个常量,还是一个表达式时就会出问题。
#define替换
宏参数和替换文本stuff可以包含其他#define定义的符号,但要注意,宏是不可以递归的。#define与宏的处理过程如下:
1、 在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号,如果是,它们首先被替换。
2、 替换文本随后被插入到程序中原来文本的位置。如果文本中含有参数,则它们将被传进的参数值所替换。
3、 最后,再次对结果文本进行扫描,看它是否包含了任何由#define定义的符号,如果是,就重复上面的过程。
C语言中相邻字符串会自动连接起来:printf("%s","ab" "cd");//abcd
当预处理器在#define的替换文本中搜索#define定义的符号时,替换文本中的字符串常量的内容并不进行检查(当然程序中的字符串常量更不会搜索了),如果想把宏参数插入到字符串常量中,可以使用以下两种技巧:
第一种:根据相邻字符串自动连接的特性,我们把一个字符串分成几段,如
#define PRINT(format,value)
printf("The value is " format " ",value)
int x = 1;
PRINT("%d",x+3);//The value is 4
上面的 PRINT("%d",x+3) 会解释成如下:printf("The value is " "%d" " ",x+3)
所以此种情况下如果你传递的不是一个字符串常量时。
注:这种技巧只有当format宏参数传入的确实是一个字符串时才能使用,因为此种情况下会严格将传递进来的参数原样替换,如果原来是数字型而非常量字符串时,拼接就会有问题,因为数字未使用引号引起来,所以不能进行字符串拼接。
第二种:使用预处理器把一个宏参数转换为一个字符串,可以使用#argument这种写法就会将宏参数argument转换为字符串,如
#define PRINT(format,value)
printf("The value of " #value
" is " format " ",value)
int x = 1;
PRINT("%d",x+3);//The value of x+3 is 4
上面的会解释成:
printf("The value of " "x+3"
" is " "%d" " ",x+3)
注:#value的作用是将参数表达式使用双引号引起来,强制转换成字符串。
如果此时实参中含有双引号和 时,都会被自动转义,如:
#define dprint(expr) printf(#expr " = %s ",expr)
dprint("str"\");//"str"\" = str"
上面会解释为:
printf(""str\"\\"" " = %s ","str"\")
这可进一步看出 #argument 会将传递进来的内容使用引号引起来,即不管传递进来的内容是字符串常量、算术表达式、还是变量,都会转换成字符串,而且如果传递进来的是字符串常量,则原字符串常量两端的引号也会原样显示出来。
另外,使用 ## 可以把位于它两边的符号(非字符串常量,所以传递进来的只能是数字型的)连接成一个符号(前后的空白符将被删除),它的用途是允许宏定义从分离的文本片段创建标识符:
#define ADD_TO_SUM(sum_number,value)
_ ## sum_number ## sum += value
int _5sum = 1;
printf("%d", ADD_TO_SUM(5,25));//26
printf("%d", _5sum);//26
其作用是把值25加到变量 sum5 上,注意,这种连接必须要产生一个合法的标识符。
#与##区别
#会将传递进来的参数使用双引号引起来,而##则不会,它会原样将传递进来的内容(数字常量、字符串常量、符号、以及表达式)进行替换:
#define paster(n) printf("token" #n " = %d",token##n)
int token9 = 9;
paster(9);//token9 = 9
上面的 paster(9) 会解释成:
printf("token" "9" " = %d",token9)
#define paster(n) printf("toke" #n " = %d",toke##n)
int token = 9;
paster(n);//token = 9
上面的 paster(n) 会解释成:
printf("token" "n" "%d",token)
上面传递进去的 n 会在#前缀下会加上双引号,而在##前缀下都不会加上双引号
宏与函数
宏非常频繁地用来执行简单的计算,如:
#define MAX(a,b) ((a)>(b)?(a):(b))
这里不使用函数而使用宏的好处:
1、 函数调用需要花时间(函数的调用与返回都需要时间),而宏则是在编译时就已替换,运行速度会提高
2、 更重要的是,函数的参数必须声明为一种特定的类型,所以在调用时只适合特定的类型。但是宏的参数是没有类型的,它可以用于任何一种类型,比如这里可以对整型、浮点型都可以进行比较。
3、 宏的不好之处是,每次在使用宏时,宏的定义代码都会拷贝插入到程序中,除非宏非常短,否则使用宏可能会大幅度增加程序的长度。
还有一些根本不能使用函数来实现,比如你要将一个类型标识作为参数进行传递时,只能使用宏参数来实现,如:
#define MALLOC(n, type) ((type *)malloc((n) * sizeof(type)))
int *p = MALLOC(2,int);
上面的MALLOC会解释为:((int *)malloc((2) * sizeof(int)))
宏参数的副作用
#define MAX(a,b) ((a)>(b)?(a):(b))
int main(int argc, char **argv) {
int x = 5, y = 8, z = MAX(x++,y++);
printf("x=%d y=%d z=%d", x, y, z);//x=6 y=10 z=9
}
上面的MAX会解释为:((x++)>(y++)?(x++):(y++))
#undef
移除一个宏的定义:
#undef name
如果一个现在的名字需要被重新定义,那么首先必须用#undef进行移除
条件编译
#if constant-expression
statements
#elif constant-expression
other statements ...
#else
other statements ...
#endif
constant-expression是一个常量表达式,它由预处理器进行求值。所谓常量表达式,就是说它组成该表达式的是一些字面值常量,或者是由一些#define定义的符号组成,或者两者都有。
测试一个符号是否已被定义:
#if defined(symbol) 或 #ifdef symbol
#if !defined(symbol) 或 #ifndef symbol
上面每对定义的两条语句是等价的,但#if 形式功能更强,因为有可能还包含其他条件:
#if x > 0 || defined(ABC) || defined(BCD)
#ifdef 与 #ifndef 也都是由 #endif 匹配来结尾
条件编译也可以嵌套
文件包含
#include指令处理方式:预处理器在编译时会删除这条指令,并用包含文件的内容取而代之,然后与源文件一起进行编译,所以头文件(以“.h”为后缀名的源文件)本身并不会单独进行编译,它是被包含到“.c”后缀的源文件中后再与“.c”后缀源文件一起进行编译,但“.c”文件本身会进行编译,所以如果将一个“.c”后缀的源文件使用#include(按理来说.c 后缀源文件是不用作头文件的,头文件的的作用是定义一些符号常量与一起函数原型的声明,并不进行全局变量的定义,而全局变量的定义一般都是在“.c”后缀源文件中定义的,在运行时会自动读取得到)被包含到其他源文件中,如果这个被包含的.c源文件中定义了全局变量,则在编译时就会出错,报全局变量重复定义了,原因就是该被包含的.c源文件被编译了两次,一次就是被原样拷贝到其他源文件中后与源文件一起进行编译,另外由于是.c后缀源文件,所以本身还要进行一次编译,所以编译就通不过。
库函数头文件使用下面的语法:
#include <filename>
本地库文件使用下面的语法:
#include "filename"
如果本地查找失败,编译器会去搜索库函数头文本。处理本地头文件的常见策略就是在源文件所在的当前目录进行查找,如果未找到,编译器就像查找函数库头文件一样在标准位置查找本地头文件。
你可以在所有的#include语句中使用双引号而不是尖括号,但是,使用这种方式,有些编译器会浪费少许时间,而更不好的是,没有将库函数文件与本地文件给区别开来。
有些编译器可以使用绝对文件路径:
#include <C:Documents and Settingsjzj374workspaceHellosrcinsert2.c>
或
#include "C:Documents and Settingsjzj374workspaceHellosrcinsert2.c>"
一旦使用了绝对路径,不管它是使用在哪种形式的#include,它们都不会去在正常目录(标准库目录或本地头文件目录)下去查找,因为这个路径已经指定了查找的位置。
标准约定:头文件以 .h 后缀命名
标准要求编译器必须支持至少8层的并没有文件嵌套,但没有限制最深的值。
如果一个头文件 a.h 中 #include 了 b.h,则我们只需要在源文件中 #include 一下 a.h就可以将 b.h 也包含进来了。
有的头文件是不能重复包含的,如某个头文件中声明了一个全局变量并定义时初始化了,则在多次#include时会出现重复定义编译问题,如果某个符号被#defined多次,虽然编译时不报错,但最后的会覆盖以前的定义。
为了防止重复宏的定义与变量的声明,我们一般头文件这样写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
//这里写需要定义或声明的内容
//...
#endif
HEADERNAME为头文件的文件名,这种约定可以避免由于其他并没有文件使用相同的符号而引起的冲突,大部分的标准函数库的头文件都是这样定义的,另外,“#define_HEADERNAME_H 1”可以写成“#define _HEADERNAME_H”,尽管现在它的值是一个空字符串而不是“1”,但这个符号仍然是被定义过了的。
即使头文件的所有内容将被忽略,预处理器仍将读入整个头文件内容,由于这种处理将拖慢编译速度,所以如果可能,应避免出现多重包含。
第十三章 输入/输出函数
使用标准库函数有助于程序的可移植性。一种编译器可以在它的函数库中提供额外的函数,但不应修改标准要求提供的函数。
错误报告
ANSI C函数库的许多函数调用操作系统来完成某些任务,I/O函数尤其如此。当操作系统执行任务时可能会失败,标准库函数在一个外部整型变量 errno(在 errno.h 中定义)中保存错误代码之后把这个信息传递给用户程序,提示操作失败的原因。perror函数简化向用户报告这些特定错误的过程,它的原型在stdio.h中定义:
void perror (const char* message);
如果message不是NULL并且指向一个非空字符串,perror函数就打印出这个字符串,后面跟一个分号和一个空格,然后打印出一条用于解释errno当前存储的错误代码的信息。
注:只有当一个函数失败时,errno才被设置,成功时不会被修改。所以我们不能根据判断errno的值来判断是否有错误发生,因此只有调用的函数发生错误时检查errno才有意义。
ANSI I/O概念
头文件stdio.h包含了与ANSI函数库的I/O部分有关的声明,尽管不包含这个头文件也可以使用某些I/O函数,但绝大多数I/O函数在使用前都需要包含这个头文件。
printf函数会使用到缓冲,所以在调试程序时,在调用printf后立即调用fflush函数。
流分为两种:文本流与二进制流。二进制流的字节不经修改地从二进制流读取或向二进制流写入。文本流能够允许的最大文本行因编译器而异,但至少允许254个字符,如果宿主操作系统使用不同的约定结束文本行标示符,I/O函数必须在这种形式和文本行的内部形式之间进行翻译转换。
总规律:在Windows环境中,写入时,是否在 前加上 符,要看写入模式是否附加了二进制模式 b,如果加上了,则在写入时不会在 前加上 符,puts时也只会在行尾只是加上 ,而不是 ;读取时,是否去掉 前的 符,要看读取模式是否附加了二进制模式 b,如果加上了,则在写入时不会丢弃 前加上 符,那怕使用gets函数来读取时, 也只会丢弃 字符,而不会将前面的 丢弃掉。
不管是在Windows环境中还是Linux环境中,行I/O函数只要读取到 就算一行读取完。
文本流会因操作系统不同特性有所不同,其中之一就是文本行的最大长度,标准规定至少允许254个字符,另一个可能不同的特性是文本行的结束方式,如在MS-DOS系统中,文本文件约定以一个回车符和一个换行符结尾,但UNIX系统只使用一个换行符。标准把文本行定义为零个或多个字符,后面跟一个表示结束的换行符,而不带回车符,对于那些文本行的外在表现形式与这个定义不同的系统上,库函数会负责外部表现形式和内部存储形式之间的转换(请看下面的测试,下面是中写模式为),如在MS-DOS系统中,输出时,文本中的换行符被替换成一对回车/换行符(注:只在在写模式为w的情况下才会这样,wb情况下则不会在前面加上回车符),在输入时,文本中的连续的回车/换行符中的回车符会被丢弃(注:只在在读取模式为r的情况下才会这样,rb的情况则不会丢弃),这种不必考虑文本的外部形式而操作文本的能力简化了可移植程序的创建。
freopen("d:/test1", "w", stdout);
printf(" ");//只会输出一个字符
printf(" ");//会输出两个字符
printf(" ");//会输出三个字符
printf(" ");//会输出三个字符
freopen("d:/test1", "wb", stdout);
printf(" ");//只会输出一个字符
printf(" ");//只会输出一个字符
printf(" ");//只会输出两个字符
printf(" ");//只会输出两个字符
freopen("d:/test1", "w", stdout);
putchar(' ');//只输出一个字符
putchar(' ');//会输出
freopen("d:/test1", "wb", stdout);
putchar(' ');//只输出一个字符
putchar(' ');//也只输出一个字符
从上面的测试可以看出,在Windows系统上,如果使用C语言中的I/O字符输出函数(注,不管是格式化输出家族函数printf、行字符输出家族函数puts、还是字符输出家族函数putchar都满足这个规律:putchar(' ')也会输出两个字符 ,而不是一个字符 )将一个换行字符输出,则会在换行字符前加上一个回车字符,但如果输出的是回车字符时,则不会在前面加上换行符。
在Windows环境下,录入的文本时按一个回车键时相当于输入了两个字符: ,单个的 字符在Windows环境中的不同文本编辑器中显示是不一样的,有时会显示可以成换行效果(如notepad、eclipse中),有时显示成?(如在UltraEdit中,不过在打开时它会询问你是否转换成DOS格式,如果选择了是的话,也会显示成换行效果);但在Unix环境下时,按一个回车键后,只会产生一个字符 (注,在使用vi编辑一个文件时,当你在某行输入了一些字符后即使你没有按回车键,vi编辑器在保存内容时也会在行末加上 ,这有点奇怪,规则是vi编辑的文本每行后面都会固定有个 字符,即使从Windows上传一个只有一行的文本文件到且没有回车换行符时,在服务上那怕没有按回车,通过vi编辑保存后就会加上 字符,但如果是通过bin模式传送时不会相互转换)
通过编辑vi /etc/vsftpd.conf配置文件,如果将设置为:
ascii_upload_enable=YES
ascii_download_enable=YES
后,则windows与linux之间通过ftp的asc模式传递时会相互转换,windows到linux时,会将回车换行转换为一个换行符,从linux到windows时,会将换行符转换为回车换行两个字符。
Windows环境中:
freopen("d:/test1","w",stdout);
printf("%s","abcde ");//不会在 后面加上
//或者 printf("abcde ");
printf("%s","fghij ");//会在 前面加上
//或者 printf("fghij ");
最后输出的文件内容为13个字节(从程序实际输入的字符来看只有12个),Ultra显示如下:
(注:回车是ASCII码为13,换行为10)
在Linux环境中上面程序输出结果如下,且文本所存储的内容只有12个字节,存储的内容即程序中显示录入的:
在Windows环境中,在输入时,文本中的连续的回车/换行符中的回车符会被丢弃:
freopen("d:/test1","r",stdin);
int c ;
while((c= getchar() )!= EOF){
/*
* 注,虽然printf见到 会将它转换为 (在windows环境下)
* ,但如果c以整型类型 %d 输出时,不会 字符前加上 ,但如
* 果以字符类型 %c 输出时,则会将 转换为
*/
printf("%d ",c);
}
二进流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式计入到程序中,它们并未作任何改变,这种类型的流适用于非文本数据,当然如果你不希望I/O函数修改文本文件的末字符,也可以把它用于文本文件的读取。
启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序, 它们都是一个指向FILE结构的指针,在程序中我们可以直接使用它们(printf("%c",getc(stdin));)。这3个文件分别是:标准输入、标准输出、标准错误,相应的FILE文件指针分别是stdin、stdout和stderr,它们在<stdio.h>中定义的(#define),一般stdin指向键盘、而stdout和stderr指向显示器。标准错误就是错误信息写入的地方,perror函数把它的输出也写到这个地方。
标准错误流使用一个单独的流,这样即使标准输出的缺省值重定向为其他位置,错误信息仍能够显示在它的缺省位置(显示器)。
FOPEN_MAX 是你能够同时打开的最多文件数,具体数目因编译器而异,但不能小于8。
FILENAME_MAX 是用于存储文件名的字符数组的最大限制长度。
输入输出重定向
在许多系统中,标准输出和标准错误在缺省情况下是相同的,但是,为错误信息准备一个不同的流意味着,即使标准输出重写向到其他地方,错误信息仍将出现在屏幕或其他缺省的输出设备上。
在许多环境中,可以使用符号“<”来实现输入重定向,它将把键盘输入替换为文件输入:如果程序prog中使用了函数 int getchar (void)(默认从标准输入中一次读取一个字符,遇到文件尾时,则返回EOF),则命令行: prog <infile,将使得程序prog从输入文件infile(而不是从键盘)中读取字符。字符串“<infile”并不包含在argv的命令行参数中。也可通过管道提供输入源:otherprog | prog 将运行两个程序otherprog和prog,并将程序otherprog标准输出通过管道重定向到程序prog的标准输入上。
如果程序prog调用了函数 int putchar (int)(将字符送至标准输出上,在默认情况下,标准输出为屏幕显示,并返回输出的字符,发生错误时返回EOF),那么命令行 prog>outfile(可以使用符号“>”来实现输出重定向),将把程序prog的输出从标准输出设备重定向到文件。如果系统支持管道,那么命令行 prog | anotherprog 将把程序prog的输出从标准输出通过管道重定向到程序anotherprog的标准输入中。
I/O函数总览
I/O函数以三种基本的形式处理数据:单个字符、文本行和二进制数据,对每种形式,都会有特定的函数来进行处理。下表列出了用于每种I/O形式的函数或家族函数,家族函数在表中以斜体表示,它代表着一组功能相当的函数,这些函数只是输出来源或输出地方不同:
数据类型 |
输入 |
输出 |
描述 |
字符 |
getchar |
putchar |
读/写单个字符 |
文本行 |
gets scanf |
puts printf |
文本行未格式化的输入/出 格式化的输入输出 |
二进制数据 |
fread |
fwrite |
读/写二进制数据 |
下表是对上表家族函数进一步说明:
家族名 |
目的 |
可用于所有的流 |
固定用于stdin和stdout |
用于内存中的字符串 |
getchar |
字符输入 |
int fgetc(FILE *stream) int getc(FILE *stream) |
int getchar(void) |
不需要特殊IO函数,可使用字符指针操作 |
putchar |
字符输出 |
int fputc(int chr, FILE * stream); int putc(int chr, FILE * stream); |
int putchar(int chr); |
不需要特殊IO函数,可使用字符指针操作 |
gets |
文本行输入 |
char * fgets(char * buffer, int buffer_size, FILE * stream); 注:不会丢弃行结束符 |
char * gets(char * buffer); 注:会丢弃行结束符(在Windows上连续的 或 单个的 都会被丢弃,但单个的 不会被丢弃。另外,如果模式为rb,则连续的 中的 还是不会被丢弃,只会将后面的 丢弃掉) |
不需要特殊IO函数,可使用strcpy函数操作 |
puts |
文本行输出 |
int fputs(char const* buffer, FILE * stream); 注:不会在字符串后加上行结束符 |
int puts(char const * buffer); 注:会在字符串后加上行结束符,Windows上为 |
不需要特殊IO函数,可使用strcpy函数操作 |
scanf |
格式化输入 |
int fscanf(FILE *stream, char const *format, ...); |
int scanf(char const*format, ...); |
int sscanf(char const *string, char const *format, ...); |
printf |
格式化输出 |
int fprintf(FILE *stream, char const *format, ...); |
int printf(char const *format, ...); |
int sprintf(char *buffer, char const*format, ...); |
打开文件
fopen把一个流和文件相关联。
FILE是一个结构体类型,用于管理缓冲区和存储流的I/O状态。它被定义于 stdio.h 头文件中,声明原型如下:
typedef struct _iobuf{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
} FILE;
FILE *fp = fopen(char *name, char *mode);
如果打开时发生错误,则fopen将返回NULL,errno存储了问题的原因。
mode(模式):
|
读 |
写 |
添加 |
文本 |
r |
w |
a |
二进制 |
rb |
wb |
ab |
mode需以r、w或a开头,如果是读取,则必须是已存在的文件。如果是写入,文本存在,则会删除原来内容,如果文件原先不存在,则新建文件。如果是追加,原文件不存在时也会先创建,如果存在则在文件末添加内容,并不会删除原来的内容。
在mode中添加“a+”表示文件打开用于更新,流即可读可写。但是,如果你已经从该文件中读取了一些数据,那么在你开始向它写入数据之前,你必须调用其中一个文件定位函数(fseek、fsetpos、rewind),在你向文件写入一些数据后,想从文件读取,你先必须调用fflush函数或者文件定位函数。
"r" 打开文本文件用于读
"w" 创建文本文件用于写,并删除已存在的内容(如果有的话)
"a" 添加;打开或创建文本文件用于在文件末尾写
"rb" 打开二进制文件用于读
"wb" 创建二进制文件用于写,并删除已存在的内容(如果有的话)
"ab" 添加;打开或创建二进制文件用于在文件末尾写
"r+" 打开文本文件用于更新(即读和写)
"w+" 创建文本文件用于更新,并删除已存在的内容(如果有的话)
"a+" 添加;打开或创建文本文件用于更新和在文件末尾写
"rb+"或"r+b" 打开二进制文件用于更新(即读和写)
"wb+"或"w+b" 创建二进制文件用于更新,并删除已存在的内容(如果有的话)
"ab+"或"a+b" 添加;打开或创建二进制文件用于更新和在文件末尾写
后六种方式允许对同一文件进行读和写,要注意的是,在写操作和读操作的交替过程中,必须调用fflush()或文件定位函数如fseek()、fsetpos()、rewind()等。
FILE * input;
input = fopen("data3", "r");
if (input == NULL) {
perror("data3");//显示错误信息,如果文件不存在,则显示类似于:data3: No such file or directory
exit(EXIT_FAILURE);
}
exit为每个已打开的输出文件调用fclose函数,以将缓冲区中的所有输出写到相应的文件中。
在主程序main中,语句return expr 等价于 exit(expr) ,其他函数的退出会直接导致整个程序的退出。
void exit(int status)
status参数值会返回给操作系统,任何调用该程序的进程都可以获得exit参数的值,因此,调用该程序的进程可以通该参数值来测试该程序是否执行成功。
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
FILE * freopen(char * filename, char *mode, FILE *stream);
将stream(通常是stdin、stdout、stderr)流先关闭,然后重新打开这个流,且这个重新打开的流重定向到了指定文件上,返回的是重新打开的流。简单的说该函数就是实现重定向功能。
//从GetData.txt文件中读取数据
freopen("GetData.txt","r",stdin);
//把输出结果重定向到OutData.txt文件中
freopen("OutData.txt","w",stdout);
模拟Unix上的cat回显命令:
#include <stdio.h>
#include <stdlib.h>
/*模仿cat命令,可以显示多个文件内容*/
int main(int argc, char *argv[]) {
FILE *fp;
void filecopy(FILE *, FILE *);
char *prog = argv[0];//记下程序名称,供错误处理使用
printf("%d", argc);
if (argc == 1) {//如果不带参时,直接将键盘输入的显示在屏幕上
filecopy(stdin, stdout);
} else {
while (--argc > 0) {
if ((fp = fopen(*++argv, "r")) == NULL) {
fprintf(stderr, "%s: can't open %s ", prog, *argv);
exit(1);
} else {
filecopy(fp, stdout);
if (fclose(fp) != 0) {
perror("fclose");
exit(EXIT_FAILURE);
};
}
}
}
if (ferror(stdout)) {//判断是否写成功
fprintf(stderr, "%s: error writing stdout %s ", prog);
exit(2);
}
return EXIT_SUCCESS;
}
void filecopy(FILE * ifp, FILE *ofp) {
int c;
while ((c = getc(ifp)) != EOF) {
putc(c, ofp);
fflush(ofp);
}
}
如果在读或写的过程中出现错误,则函数ferror返回一个非0值。
int ferror(FILE *fp)
int feof(FILE *fp):如果指定的文件到达文件结尾,将返回一个非0值。
关闭文件
int fclose(FILE *fp):关闭文件连接,并刷新缓冲区。当程序正常终止时,程序会自动为每个打开的文件调用fclose,成功时返回0,失败时返回EOF。
字符I/O函数
getchar函数家族
int getc(FILE *stream)
int fgetc(FILE *stream)
int getchar(void)
fgetc、getc从指定的stream参数流中读取,而getchar固定从标准输入stdin中读取,达到文件尾或发生错误时返回EOF(-1)。注:读出来的永远是一个字节的内容,并且将这一个字节的内容永远看作是正数,然后将一个字节内容转换成int类型,且在位扩充时是补零,所以返回的永远是正数 0~255,表示了256个字符,这个与Java中的字节流是一样的道理。这些函数返回的都是一个int类型值而不是char型值,尽管表示一个字符的内存代码(二进制码)以一个小整型就可以存储了,但返回int型值的真正原因是为了允许函数返回EOF(-1),如果返回的是char那么256个字符中必须有一个被指定用于表示EOF(这里即-1会被占用)如果这个字符出现在了流中,那么这个字符后面的内容将不会被读取,因为它被解释为EOF标志了。
让函数返回一个int型值就能解决这个问题。EOF被定义为一个整型(约定为-1,其实可为任何负数),它的值在任何可能出现的字符范围(0~255)之外,这种解决方法允许我们使用这些函数来读取二进制文件也是可以的。
putchar函数家族
int fputc(int character, FILE * stream);
int putc(int character, FILE * stream);
int putchar(int character);
在输出之前,函数把character参数裁剪为一个无符号字符型值,失败时返回EOF,并返回写入的字符。
fgetc和fputc都是真正的函数,但getc、putc、getchar和putchar都是通过#define指令定义的宏。宏在执行时间上效率稍高,而函数在程序长度方面更胜一筹,之所以提供两种类型的方法,是为了允许你根据程序的长度和执行速度哪个更重要来选择正确的方法。
int ungetc(int character, FILE * stream);
把一个先前读入的字符character返回到流中,这样它可以在以后被重新读入,成功时返回“退回”的字符,失败时返回EOF。下面从标准输入中读取整数,直到非数字字符止:
int read_int() {
int value;
int ch;
value = 0;
/*
** Convert digits from the standard input; stop when we get a
** character that is not a digit.
*/
while ((ch = getchar()) != EOF && isdigit(ch)) {
value *= 10;
value += ch - '0';
}
/*
** Push back the nondigit so we don't lose it.
*/
if (ch != EOF) {
ungetc(ch, stdin);
}
printf("ch = %d, next char in buffer = %c ", ch, getchar());
return value;
}
输入“123s”:
123s
ch = 115, next char in buffer = s
123
-end-
每个流都允许至少一个字符被退回。如果一个流允许退回多个字符,那么这些字符再次被读取的顺序就以退回时的反序进行。注,退回并不等同于写入,原流的存储区域中的内容并不受ungetc的影响。“退回”字符和流的当前位置有关,所以如果用fseek、fsetpos或rewind函数改变了流的当前位置,所有退回的字符都将被抛弃。
未格式化的行I/O
行I/O可以用两种方式执行——未格式化或格式化的,这两种都用于操作字符串。
gets和puts函数家族是用于操作字符串而不是单个字符。
char *fgets(char * buffer, int buffer_size, FILE * stream);
char *gets(char * buffer);
int fputs(char const * buffer, FILE * stream);
int puts(char const * buffer);
fgets函数从stream中读取字符并把它们复制到buffer中,在读取时可能以一行为单位来读取,也可能不以一行为单位(少于一行),这要看buffer_size参数的大小,如果buffer_size大于或等于一行字符总数(包括回车换行字符)加1(每次读取后放在buffer中时都会加上一个NUL字节来构成字符串)时,就会读取一整行,但要注意的是如果 buffer_size -1 大于一行字符总数,此时也只会读取一行的字符,而不会因为buffer空间多余而读取一行多的字符;如果buffer_size -1小于了一行字符总数时,就只读取 buffer_size -1 个字符,而不是一行;总的来说,读取的规则是:如果已经读取完行结束符或者缓冲区已经存入了buffer_size -1个字符,则都会停止读取,剩余的字符等到下一次进行读取。在任何一种情况下,都会在读取的字符串后加上NUL字节,使它成为一个字符串,所以一次最多只能读取buffer_size -1个字符。如果在任何字符还没读取前就到达了文件尾,缓冲区就不会被修改,此时fgets函数返回一个NULL指针,否则返回它的第一个参数,所以可以通过这个返回值来判断是否到达文件尾部。
注:fgets无法把字符串计入到一个长度为一的缓冲区,因为其中一个字符需要为NUL字节保留。
传递给fputs函数的缓冲区中存储的字符必须以NUL字节结尾,所以这个函数没有一个缓冲区长度参数,这个字符串是逐字符写入的:如果它不包含一个换行符,就不会写入换行符,如果包含了好几个换行符,所有换行符都会被写入。fputs与fgets不同,它即可一次写入一行的一部分, 也可以一次写入一整行,甚至可以一次写入好几行,如果出错EOF,否则返回一个非负值。
gets和puts函数几乎和fgets与fputs相同,之所以存在它们是为了允许向后兼容。它们之间的一个主要的功能性区别在于当gets读取一个输入时,它会丢弃掉行标示符(与Java中的BufferedReader的readLine方法有点像);当写入一个字符时串时,它在字符串写入之后向输出再添加一个行结束标示符。另一个区别仅限于gets函数,它并没有缓冲区长度大小参数,所以在读取时很有可能会溢出,所以我们尽量不用这个函数。
下面是标准库中fgets和fputs函数的代码:
//fgets行函数基于getc函数实现:从iop文件中最多读取n-1个字符,再加上一个NULL
char * fgets(char *s, int n, FILE *iop) {
register int c;
register char *cs;
cs = s;
while (--n > 0 && (c = getc(iop)) != EOF) {
//先将读取出的字符存储在cs中后指针再下移,然后再判断
//一行是否读取完
if ((*cs++ = c) == ' ') {
break;
}
}
//最后将在读取出的字符后面加上字符串结束标示符
*cs = ' ';
//如果为空文件或读取出错,则返回NULL
return (c == EOF && cs == s) ? NULL : s;
}
//fputs行函数基于putc函数实现
int fputs(char *s ,FILE *iop){
int c;
while (c=*s++){
putc(c,iop);
}
return ferror(iop)?EOF:非负值;
}
示例:测试fgets是以 为行结束标示,且当读取模式为“rb”时,连续的 中的 不会被丢弃,但当读取模式为“r”时,会丢弃 字符:
先在Liunx环境上使用以下程序创建这样一个文件:
#include <stdio.h>
main(){
freopen("/root/a/a.txt","w",stdout);
printf("ab ");
printf("cd ");
printf("ef ");
}
运行的结果为会产生一个10字节的文本文件:
在Windows环境中全以下程序来读取上面Linux产生的文件:
FILE * fp = fopen("d:/a.txt", "r");
char *s, buffer[9];
int size = 9;
while ((s = fgets(buffer, size, fp)) != NULL) {
printf("%d- -", strlen(buffer));
printf("%s- -", buffer);
}
输出结果:
6-
-ab
cd
-
-3-
-ef
-
-
如果将读取模式改成“rb”,则输出结果为:
7-
-ab
cd
-
-3-
-ef
-
-
格式化I/O
scanf家族函数:
int fscanf(FILE *stream, char const *format, ...);
int scanf(char const *format, ...);
int sscanf(char const *string, char const *format, ...);
这三个函数的输入源的来源不同。当到达格式化字符串的末尾或读取的输入不再匹配格式字符串所指定的类型时,输入就停止。它们都返回读取的字符个数。如果达到文件的尾,则返回EOF。
格式化串的空白字符(空白字符包括空格、横向与纵向制表符、换行符、回车符、换页符)一般情况下会被忽略掉,它将不会用来与输出中的空白字符进行匹配(但%c不会忽略空白字符)。
%[*][宽度][限定符] 格式代码
格式码 |
scanf限定符 |
||
h |
l |
L |
|
d i n |
short |
long |
|
o u x |
unsigned short |
unsigned long |
|
e f g |
|
double |
long double |
数据类型 |
printf/scanf函数转换格式 |
long double |
%Lf |
double |
%lf |
float |
%f |
unsigned long |
%lu |
long |
%ld |
unsigned |
%u |
int |
%d |
unsigned short |
%hu |
short |
%hd |
char |
%c |
scanf格式代码 |
||
代码 |
参数类型 |
含义 |
c |
char * |
读取和存储单个字符。前导的空白字符并不跳过。如果给出宽度,就读取和存储这个数目的字符。字符后面不会添加一个NUL字节。参数必须是一个指向足够大的字符数组 |
i d |
int * |
有符号整数被转换。d把输入解释为十进制;i根据它的第一个字符决定值的基数,就像整型字面值常量的表示形式一样 |
u o x(X) |
unsigned * |
有符号整数被转换,但它按照无符号数存储。如果使用u,值被解释为十进制数;如果使用o,值被解释为八进制数;如果使用x,值被解释为十六进制数 |
f e(E) g(G) |
float * |
转换一个浮点值 |
s |
char * |
读取一串非空白字符。参数须指向一个足够大的字符数组。当发现空白时输入就停止,字符串后面会自动加上NUL终止符 |
[xxx] |
char * |
根据给定组合的字符从输入中读取一串字符。参数必须指向一个足够大的字符数组。当遇到第1个不在给定组合中出现的字符时,就停止输入。字符串后面会自动加上NUL终止符。代码%[abc] 表示字符组合包括a、b和c。如果列表中以一个 ^ 字符开头,表示字符组合是所列出字符的补集。右方括号也可以出现在字符列表中,但它必须是列表的第1个字符。至于横杠是否用于指定某个范围的字符(如 %[a-z]),则因编译器而异 |
p |
void * |
输入预期为一串字符,诸如那些由printf函数的%p格式代码所产生的输出。它的转换方式因编译器而异,但转换结果将和按照上面描述的进行打印所产生的字符的值是相同的 |
n |
int * |
到目前为止通过这个scanf函数的调用从输入读取的字符数被返回。%n转换的字符并不计算在scanf函数的返回值之内。它本身并不消耗任何输入 |
% |
|
与%匹配 |
数字在格式化时会采用四舍五入的方式来截断。
nfields = fscanf(input, "%4d %4d %4d", &a, &b, &b);
这个宽度参数把整数值的宽度限制为4个数字或者更少。使用下面的输入:
1 2
a的值将是1,b的值将是2,c的值将没有改变,nfields的值将是2,但下面的输入:
12345 67890
a的值将是1234,b的值为5,c的值是6789,而nfields的值是3,输入中最后一个0将保持在未输入状态。
注:格式化输入函数会跳过空白字符,包括换行符。
示例:假设我们要读取包含下列日期格式的输入行:
25 Dec 1988
相应的scanf语句可以这样编写:
int day ,year;
char monthname[20];
scanf(“%d %s %d”,&day,monthname,&year);
scanf不会跳过换行,如果想以行为单位来读取,则可以采用下面的处理方法:
#include <stdio.h>
#define BUFFER_SIZE 100 /* Longest line we'll handle */
void function(FILE *input) {
int a, b, c, d;
char buffer[BUFFER_SIZE];
while (fgets(buffer, BUFFER_SIZE, input) != NULL) {
if (sscanf(buffer, "%d %d %d %d", &a, &b, &c, &d) != 4) {
fprintf(stderr, "Bad input skipped: %s", buffer);
fflush(stderr);
continue;
}
/*
** Process this set of input.
*/
printf("%d %d %d %d", a, b, c, d);
fflush(stdout);
}
}
printf家族函数
int fprintf(FILE *stream, char const *format, ...);
int printf(char const *format, ...);
int sprintf(char *buffer, char const *format, ...);
sprintf会在输出结果末加上NUL字符。
printf家族函数的格式代码和scanf函数家族的格式代码用法是完全相同的。
%[零个或多个标志符][最小字段宽度][精度][修改符] 格式化代码
printf格式代码 |
||
代码 |
参数类型 |
含义 |
c |
int |
参数被截断为unsigned char 类型并作为字符进行打印 |
i d |
int |
参数作为一个十进制整数打印,如果给出了精度而且值的少于精度,前面就用0填充 |
u o x(X) |
unsigned * |
参数作为一个无符号值打印,u使用十进制,o使用八进制,x或X使用十六进制,两者的区别是x使用abcdef,而X使用ABCDEF |
e(E) |
double |
参数根据指数形式打印。例如,6.023000e23是使用代码e,6.023000E23是使用代码E,小数点后面的位数由精度字段决定,缺省为6 |
f |
double |
参数按照常规的浮点格式打印。精度字段决定小数点后面的位数,缺省为6 |
g(G) |
double |
参数以%f或%e(如果为%G则为%E)的格式打印,如果指数大于等于-4但小于精度字段就使用%f格式,否则使用指数格式。 |
s |
char * |
顺序打印字符串中的字符,直到遇到’ |