[C和指针]第四部分

 

第十一章 高级指针话题
第十二章 预处理器
第十三章 输入/输出函数

第十一章            高级指针话题

指向指针的指针

    int i;

    int *pi;

    int **ppi;

image108

变量i 是一个整数,pi是一个指向整型指针,ppi是一个指向pi的指针,所以它是一个指向整型的指针的指针。

 

    ppi = π这条语句把ppi初始化为指向变量pi

    *ppi = &i;这条语句把pi(通过ppi间接访问)初始化为指向变量i。经过上面两条语句之后:

image110

现在,下面各语句具有相同的效果(都是将变量i的值赋值为10):

    i = 10;

    *pi = 10;

    **ppi = 10;

经过上面赋值语句后,变量的内存关系结构:

image112

 

为什么需要间接的访问呢?这是因为简单的赋值有进并不可行,比如在一个函数里想修改调用它的函数里的变量,则只能使用指针的形式来传递,对该指针进行间接访问操作可以访问需要修改的变量。上面的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是一个指向字符串指针的指针:

image114

 

/*

 * 打印参数,路过程序名

 */

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环境中的不同文本编辑器中显示是不一样的,有时会显示可以成换行效果(如notepadeclipse中),有时显示成?(如在UltraEdit中,不过在打开时它会询问你是否转换成DOS格式,如果选择了是的话,也会显示成换行效果);但在Unix环境下时,按一个回车键后,只会产生一个字符  (注,在使用vi编辑一个文件时,当你在某行输入了一些字符后即使你没有按回车键,vi编辑器在保存内容时也会在行末加上   ,这有点奇怪,规则是vi编辑的文本每行后面都会固定有个   字符,即使从Windows上传一个只有一行的文本文件到且没有回车换行符时,在服务上那怕没有按回车,通过vi编辑保存后就会加上   字符,但如果是通过bin模式传送时不会相互转换)

 

通过编辑vi /etc/vsftpd.conf配置文件,如果将设置为:

ascii_upload_enable=YES

ascii_download_enable=YES

后,则windowslinux之间通过ftpasc模式传递时会相互转换,windowslinux时,会将回车换行转换为一个换行符,从linuxwindows时,会将换行符转换为回车换行两个字符。

 

Windows环境中:

freopen("d:/test1","w",stdout);

printf("%s","abcde ");//不会在 后面加上

//或者 printf("abcde ");

printf("%s","fghij ");//会在 前面加上

//或者 printf("fghij ");

最后输出的文件内容为13个字节(从程序实际输入的字符来看只有12个),Ultra显示如下:

image116

(注:回车是ASCII码为13,换行为10

Linux环境中上面程序输出结果如下,且文本所存储的内容只有12个字节,存储的内容即程序中显示录入的:

image118

 

Windows环境中,在输入时,文本中的连续的回车/换行符中的回车符会被丢弃:

    freopen("d:/test1","r",stdin);

    int c ;

    while((c= getchar() )!= EOF){

       /*

        * 注,虽然printf见到 会将它转换为  (windows环境下)

        * ,但如果c以整型类型 %d 输出时,不会 字符前加上 ,但如

        * 果以字符类型 %c 输出时,则会将   转换为 

        */

       printf("%d ",c);

    }

image120

 

 

二进流中的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式计入到程序中,它们并未作任何改变,这种类型的流适用于非文本数据,当然如果你不希望I/O函数修改文本文件的末字符,也可以把它用于文本文件的读取。

 

启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序, 它们都是一个指向FILE结构的指针,在程序中我们可以直接使用它们(printf("%c",getc(stdin));)。这3个文件分别是:标准输入、标准输出、标准错误,相应的FILE文件指针分别是stdinstdoutstderr,它们在<stdio.h>中定义的(#define),一般stdin指向键盘、而stdoutstderr指向显示器。标准错误就是错误信息写入的地方,perror函数把它的输出也写到这个地方。

 

标准错误流使用一个单独的流,这样即使标准输出的缺省值重定向为其他位置,错误信息仍能够显示在它的缺省位置(显示器)。

 

FOPEN_MAX 是你能够同时打开的最多文件数,具体数目因编译器而异,但不能小于8

FILENAME_MAX 是用于存储文件名的字符数组的最大限制长度。

输入输出重定向

在许多系统中,标准输出和标准错误在缺省情况下是相同的,但是,为错误信息准备一个不同的流意味着,即使标准输出重写向到其他地方,错误信息仍将出现在屏幕或其他缺省的输出设备上。

 

在许多环境中,可以使用符号“<”来实现输入重定向,它将把键盘输入替换为文件输入:如果程序prog中使用了函数 int getchar (void)默认从标准输入中一次读取一个字符,遇到文件尾时,则返回EOF),则命令行: prog <infile,将使得程序prog从输入文件infile(而不是从键盘)中读取字符。字符串“<infile”并不包含在argv的命令行参数中。也可通过管道提供输入源:otherprog | prog 将运行两个程序otherprogprog,并将程序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

/写二进制数据

下表是对上表家族函数进一步说明:

家族名

目的

可用于所有的流

固定用于stdinstdout

用于内存中的字符串

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将返回NULLerrno存储了问题的原因。

 

mode(模式)

 

添加

文本

r

w

a

二进制

rb

wb

ab

 

mode需以rwa开头,如果是读取,则必须是已存在的文件。如果是写入,文本存在,则会删除原来内容,如果文件原先不存在,则新建文件。如果是追加,原文件不存在时也会先创建,如果存在则在文件末添加内容,并不会删除原来的内容。

 

mode中添加“a+”表示文件打开用于更新,流即可读可写。但是,如果你已经从该文件中读取了一些数据,那么在你开始向它写入数据之前,你必须调用其中一个文件定位函数(fseekfsetposrewind),在你向文件写入一些数据后,想从文件读取,你先必须调用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(通常是stdinstdoutstderr)流先关闭,然后重新打开这个流,且这个重新打开的流重定向到了指定文件上,返回的是重新打开的流。简单的说该函数就是实现重定向功能

//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)

fgetcgetc从指定的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,并返回写入的字符

 

fgetcfputc都是真正的函数,但getcputcgetcharputchar都是通过#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的影响。“退回”字符和流的当前位置有关,所以如果用fseekfsetposrewind函数改变了流的当前位置,所有退回的字符都将被抛弃

未格式化的行I/O

I/O可以用两种方式执行——未格式化或格式化的,这两种都用于操作字符串。

 

getsputs函数家族是用于操作字符串而不是单个字符

 

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字节结尾,所以这个函数没有一个缓冲区长度参数,这个字符串是逐字符写入的:如果它不包含一个换行符,就不会写入换行符,如果包含了好几个换行符,所有换行符都会被写入。fputsfgets不同,它即可一次写入一行的一部分, 也可以一次写入一整行,甚至可以一次写入好几行,如果出错EOF,否则返回一个非负值。

 

getsputs函数几乎和fgetsfputs相同,之所以存在它们是为了允许向后兼容。它们之间的一个主要的功能性区别在于当gets读取一个输入时,它会丢弃掉行标示符(与Java中的BufferedReaderreadLine方法有点像);当写入一个字符时串时,它在字符串写入之后向输出再添加一个行结束标示符。另一个区别仅限于gets函数,它并没有缓冲区长度大小参数,所以在读取时很有可能会溢出,所以我们尽量不用这个函数。

 

下面是标准库中fgetsfputs函数的代码:

//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字节的文本文件:

image122

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

xX

unsigned *

有符号整数被转换,但它按照无符号数存储。如果使用u,值被解释为十进制数;如果使用o,值被解释为八进制数;如果使用x,值被解释为十六进制数

f

eE

gG

float *

转换一个浮点值

s

char *

读取一串非空白字符。参数须指向一个足够大的字符数组。当发现空白时输入就停止,字符串后面会自动加上NUL终止符

[xxx]

char *

根据给定组合的字符从输入中读取一串字符。参数必须指向一个足够大的字符数组。当遇到第1个不在给定组合中出现的字符时,就停止输入。字符串后面会自动加上NUL终止符。代码%[abc] 表示字符组合包括abc。如果列表中以一个 ^ 字符开头,表示字符组合是所列出字符的补集。右方括号也可以出现在字符列表中,但它必须是列表的第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的值将是1b的值将是2c的值将没有改变,nfields的值将是2,但下面的输入:

12345 67890

a的值将是1234b的值为5c的值是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

xX

unsigned *

参数作为一个无符号值打印,u使用十进制,o使用八进制,xX使用十六进制,两者的区别是x使用abcdef,而X使用ABCDEF

eE

double

参数根据指数形式打印。例如,6.023000e23是使用代码e6.023000E23是使用代码E,小数点后面的位数由精度字段决定,缺省为6

f

double

参数按照常规的浮点格式打印。精度字段决定小数点后面的位数,缺省为6

gG

double

参数以%f%e(如果为%G则为%E)的格式打印,如果指数大于等于-4但小于精度字段就使用%f格式,否则使用指数格式。

s

char *

顺序打印字符串中的字符,直到遇到’’打印由精度指定的字符数为止

p

void *

指针值被转换为一串因编译器而民的可打印字符。这个代码主要是和scanf中的%p代码组合使用

n

int *

这个代码是独特的,因为它并不产生任何输出。相反,到目前为止函数所产生的输出字符数目将被保存到对应的参数中

%

 

%匹配

 

printf格式标志

标志

含义

-

输出时左对齐,缺省情况下右对齐

0

当数组为右对齐时,缺省情况下是使用空格填充值左边未使用的位置。这个标志表示用0来填充,它可用于d i u o x e f g 代码,使用d i u o x 代码时,如果给出精度字段,零标示就被忽略。如果格式代码中出现了负号标示,零标示也没有效果

+

当用于一个格式化某个有符号值的代码时,如果值为非负,输出值前面就会有个正号;如果为负值,则会在输出值前加上一个负号。缺省情况下,正号并不会显示

空格

只用于转换有符号值代码。当值为非负时,这个标志把一个空格添加到它的开始位置。注意这个标志和正号标志是互斥的,如果同时出现,空格标志会被忽略

#

选择某些代码的另一种转换形式,请见下表:

用于...

#标志...

o

保证产生的值以一个零开头

x X

在非零值前面加0x前缀(%X则为0X

e E f

确保结果始终包含一个小数点,即使它后面没有数字

g G

和上面的e Ef代码相同,缀尾的0并不从小数中去除

 

 

最小字段宽度:一个十进制数,用于指定出现在结果中的最小字符数,如果值的字符数小于字段宽度,就对它进行填充以增加长度。标志决定填充是用空白还是使用零以及它出现在值的左边还是右边。

 

精度:对于d i u o x 类型的转换,精度字段指定将出在结果中的最小的数字个数并覆盖零标志。如果转换后的值的位数小于宽度,就在它的前面插入零。如果值为零且精度也为零,则转换结果就不会产生数字。对于e f类型的转换,精度决定将出现在小数点之后的数字位数。对于g类型的转换,它指定将出现在结果中的最大有效位数。当使用s类型转换时,精度指定将被转换的最多字符数。精度以一个句点开头,后面跟一个可选的十进制整数。如果未给出整数,精度缺省为零

如果用于表示字段宽度和/或精度的十进制数由一个星号代替,那么printf的下一个参数(必须是个整数)就提供宽度和(或)精度。因此,这些值可以通过计算获得而不必预行指定。

当字符或短整型值作为printf函数参数时,它们在传递给函数之前先转换为整数,有时候转换可能影响函数的输出。同样,在一个长整数的长度大于普通整数的环境里,当一个长整数作为参数传递给函数时,prinft必须知道这个参数是个长整数。下表所示的修改符用于指定整数和浮点数参数的准确长度,从而解决了这个问题:

printf格式代码修改符

修改符

用于...

表示参数是...

h

d i u o x

一个(可能是无符号)short型整数

h

n

一个指向short型整数的指针

l

d i u o x

一个(可能是无符号)long型整数

l

n

一个指向long型整数的指针

L

e f g

一个long double型值

 

 

image124

 

image126

 

image128

 

image130

 

 

%p 是专门打印地址的转换说明符,它通常以十六进制数的格式输出地址。

二进制I/O

使用二进制流写入二进制数据(如整数和浮点数)比使用字符I/O效率更高。二进制I/O直接读写值的各个位,而不必把值转换为字符。

 

fread函数用于读取二进制数据,fwrite函数用于写入二进制数据。原型:

size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);

buffer是一个指向用于保存数据的内存位置的指针,会被解释为一个或多个值的数组。count参数指定数组中有多少个值,所以读取或写入一个标量时,count的值为1

size是缓冲区中每个元素的字节数,count是读取或写入的元素个数。函数返回是实际读取或写入的元素个数

 

 

2286166545(十进制)=10001000 01000100 00100010 00010001(二进制)=88442211(十六进制)

int main(int argc, char **argv) {

    FILE *input = fopen("d:/a.txt", "w");

    int a[] = { 2286166545 };

    fwrite(a, sizeof(int), 1, input);

    fclose(input);

}

输出结果为4个字节,即将内存中的原二进制位输出到了文件中,以下是Ultr二进制视图(另外,从二进制显示可以看出内存中是以低字节序存放的,即低位在前,高位在后):

image132

以下是Ultr文本视图:

image134

 

 

'a'=41(十六进制)

int main(int argc, char **argv) {

    FILE *input = fopen("d:/a.txt", "w");

    char a[] = { 'a' };

    fwrite(a, sizeof(char), 1, input);

    fclose(input);

}

输出结果为1个字节,Ultr二进制视图:

image136

Ultr文本视图:

image138

下面程序与上面结果是一样的:

    freopen("d:/a.txt","w",stdout);

    putchar('a');

 

从上面示例可以看出,C中的二进制I/O函数与Java中的字节流I/O概念是一样的(这些函数有点像Java里的DataInputStreamDataOutputStream两个类的相关方法),另外都可以使用二进制来读写文本文件。

 

示例:将一个结构类型的变量写入到文件,然后再读取出来:

int main(int argc, char **argv) {

    struct VALUE {//会占用 4 * 3 = 12字节的空间

       char c;

       int i;

       char chr[2];

    };

    struct VALUE values =  { 'a', 255, { 'a', 'b' } };

    struct VALUE v, *buffer = &v;

    freopen("d:/a.txt", "w", stdout);

    int writeCounts = fwrite(&values, sizeof(struct VALUE), 1, stdout);

    fflush(stdout);//写完后一定要刷新,否则下面读取不到

 

    fprintf(stderr, "writeCounts = %d ", writeCounts);

    freopen("d:/a.txt", "r", stdin);

    int readCounts = fread(buffer, sizeof(struct VALUE), 1, stdin);

    fprintf(stderr, "readCounts = %d ", readCounts);

    fprintf(stderr, "v->c = %c ", buffer->c);

    fprintf(stderr, "v->i = %d ", v.i);

    fprintf(stderr, "(v->chr)[1] = %c ", (buffer->chr)[1]);

}

image140

image142

刷新与定位函数

int fflush(FILE *stream);

fflush迫使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满。调用fflush函数保证调试信息立即打印出来,stdout使用到了缓冲,而stderr没有使用缓冲,所以在进行标准输出进行打印调试信息时,fflush很有用处。

 

long ftell(FILE *stream);

Returns the current offset in the file, or -1L on error.并设置errno标志为某个正值,具体值由编译器决定。

int fseek(FILE *stream, long offset, int from);

fseek函数允许你在一个流中定位,offset argument is the position that you want to seek to, and from is what that offset is relative toFor fseek(), on success zero is returned; 非零is returned on failureerrno的值不会改变)。

long pos;
 
// 将当前的位置存储在变量 "pos" :
pos = ftell(fp);
 
// 向前移动 10 bytes:
fseek(fp, 10, SEEK_CUR);
 
// 移动后向流写入数据
do_mysterious_writes_to_file(fp);
 
// 返回到"pos"存储的开始位置:
fseek(fp, pos, SEEK_SET);

 

form参数如下:

如果from ...

你将定位到...

SEEK_SET

从流的起始位置起offset个字节,offset必须是一个非负值

SEEK_CUR

从流的当前位置起offset个字节,offset可正可负

SEEK_END

从流的尾部位置起offset个字节,offset可正可负。如果它是正值,它将定位到文件尾的后面

 

由于文本流所执行的行标识转换,由于这种转换的存在,文本文件的字节数可能和程序写入的字节数不同,所以一个可移植的程序不应该根据写入的字符计算结果到文本流的某个位置。

 

fseek(fp, 100, SEEK_SET); // seek to the 100th byte of the file
fseek(fp, -30, SEEK_CUR); // seek backward 30 bytes from the current pos
fseek(fp, -10, SEEK_END); // seek to the 10th byte before the end of file
 
fseek(fp, 0, SEEK_SET);   // seek to the beginning of the file
rewind(fp);               // seek to the beginning of the file

 

将读写位置移动到文件尾:fseek(FILE *stream,0,SEEK_END)

 

fseek副作用:

1、  行标示符被清除

2、  fseek之前使用ungetc后,被退回的字符会被丢弃,因为在定位操作以后,它不再是“下一个字符”

3、  定位允许你从写入模式切换到读取模式,或者回到打开流以便更新

 

 

另外三个函数,用一些限制更严的方式执行相同的任务:

void rewind(FILE *stream);

rewind设置回指定流的起始位置,同时清除流的错误提示标志errno

int fgetpos(FILE *stream, fpos_t *position);

int fsetpos(FILE *stream, fpos_t const *position);

fgetposfsetpos函数是标准C新增的,增加它们的目的是为了处理那些因为过于庞大而无法由long int类型的整数来定位的文件(ftellfseek使用long int类型来定位)。成功返回0,错误返回一个非零,并在errno中存储一个因编译器而异的正值。

fseek(fp, 0, SEEK_SET); // same as rewind()
rewind(fp);             // same as fseek(fp, 0, SEEK_SET)

 

char s[100];
fpos_t pos;
 
fgets(s, sizeof(s), fp); // read a line from the file
 
fgetpos(fp, &pos);   // save the position
 
fgets(s, sizeof(s), fp); // read another line from the file
 
fsetpos(fp, &pos);   // now restore the position to where we saved

 

int main(int argc, char *argv[]) {

    FILE * stream;

    long offset;

    fpos_t pos;

    stream = fopen("d:/a.txt", "r");

    fseek(stream, 5, SEEK_SET);

    printf("offset=%d ", ftell(stream));//5

    rewind(stream);

    fgetpos(stream, &pos);

    printf("offset=%d ", pos);//0

    pos = 10;

    fsetpos(stream, &pos);

    printf("offset=%d ", ftell(stream));//10

    fclose(stream);

}

改变缓冲方式

为一个流自行指定缓冲区可以防止I/O函数库为它动态分配一个缓冲区。

 

void setbuf(FILE *stream, char *buf);

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

这些函数允许程序在默认的缓冲区无法满足要求的罕见情况下控制流的缓冲区策略。这些函数必须在流被打开之后并且在任何数据被读取或写入之前被调用。

 

setbuf以另一个数组为缓冲区,它的长度必须为BUFSIZstdio.h中定义),原因是setbufsetvbuf的简化版本(请参看后面)。如果bufNULL,则setbuf将关闭流的所有缓冲方式。

 

setvbuf函数更为通用。mode参数用于指定缓冲的类型。_IOFBF指定一个完全缓冲的流,_IONBF指定一个不缓冲的流,_IOLBF指定一个行缓冲流(行缓冲,就是每当一个换行符写入到缓冲区时,缓冲区便进行刷新)。如果bufNULL,那么size值必须为0。最好使用BUFSIZ的作为缓冲数组长度,或者是BUFSIZ的倍数,这样有助于提示效率。调用成功返回0,否则返回非0

 

setbufsetvbuf的简化版本,相当于下面这个表达式:

((buf==NULL)?(void)setvbuf(stream,NULL,_IONBF,0):(void)setvbuf(stream,buf,_IOFBF,BUFSIZ))

流错误函数

int feof(FILE *stream);

int ferror(FILE *stream);

void clearerr(FILE *stream);

如果流当前处于文件尾,feof函数返回真。如果对流执行了fseekrewindfsetpos函数,则流的是否达到末尾状态会被清除。

ferror函数报告流的错误状态,如果出现任何读/写错误就返回真。

clearerr函数对指定流的错误标示进行重置。

临时文件

FILE * tmpfile(void);

这个函数会创建一个文件,当文件被关闭或程序终止时,这个文件会自动删除。该文件以 wb+ 模式(创建二进制文件用于更新,并删除已存在的内容)打开,这可以用于二进制和文本数据。调用这个函数的目的是创建一个只在程序执行期间使用的新文件。在数据写入到这个文件之后,可以使用rewind函数把文件位置定位到文件的起始处,以便进行读取。

 

如果临时文件必须以其他模式打开或由一个程序打开但由另一个程序读取,就不适合用tmpfile函数,此时需使用fopen函数创建并在不使用后使用remove函数删除。

 

char *tmpnam(char *buf);

tmpnam函数在每次被调用时均生成不同的名字,为临时文件创建一个合适的文件名。在程序的执行过程中,最多只能确保(在多线程)TMP_MAX个不同的名字。注意tmpnam函数只是用于创建一个名字,而不是创建一个文件。

如果bufNULLtmpnam返回一个指向新文件名字符串的静态数组的指针,如果不为NULLbuf必须是一个指向一个不小于L_tmpnam(在stdio.h中定义)个字符的数组,tmpnam将把这个新文件名字符串复制到这个数组,并返回buf,如果失败返回NULL

文件操纵函数

int remove(char const *filename);

int rename(char const *oldname, char const *newname);

成功返回零,否则返回非零。

原文地址:https://www.cnblogs.com/jiangzhengjun/p/4257272.html