UNIX再学习 -- 标准I/O

这部分之前有所总结:

参看:C语言再学习 -- 文件

参看:C语言再学习 -- 输入/输出

参看:UNIX再学习 -- 文件描述符

对比:UNIX再学习 -- 文件I/O

一、流

文件I/O中所有的 I/O 函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的 I/O 操作。
而对于标准 I/O 库,它们的操作是围绕流(stream)进行的。当用标准 I/O 库打开或创建一个文件时,我们已使一个流与一个文件相关联。
对于ASCII字符集,一个字符用一个字节表示,对于国际字符集,一个字符可用多个字节表示。 标准 I/O 文件流可用于单字节或多字节字符集。 流的定向决定了所读、写的字符时单字节还是多字节 。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节 I/O 函数,则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节 I/O 函数,则将该流的定向设为字节定向的。

只有两个函数可以改变流的定向:freopen 函数清除一个流的定向;fwide 函数可用于设置流的定向。
下面介绍下 fwide 函数:
#include <wchar.h>
int fwide(FILE *stream, int mode);

1、参数解析

第一个参数:文件流
第二个参数:mode 模式
根据 mode 参数的不同值,fwide 函数执行不同的工作。
如若 mode 参数值为负,fwide 将试图使指定的流是字节定向的。
如若 mode 参数值为正,fwide 将试图使指定的流是宽定向的。
如若 mode 参数值为 0,fwide 将不试图设置流的定向,但返回标识该流定向的值。

2、函数功能

fwide 函数用于设置流的定向。
注意,fwide 并不改变已定向流的定向。还应注意的是,fwide 无出错返回。如若流是无效的,我么唯一可依靠的是,在调用 fwide 前先清除 errno,从 fwide 返回时检查 errno 的值。  

3、返回值

若流是宽定向则返回正值,若是字节定向则返回负值,若是未定向的则返回 0。

4、示例说明

#include<stdio.h>
#include <wchar.h>
int main()
{
	int res = fwide (stdin, 6);
	printf("%d\n",res);
	return 0;
}
输出结果:
1

二、FILE 对象

当打开一个流时,标准 I/O 函数 fopen 返回一个指向 FILE 对象的指针该对象通常是一个结构,它包含了标准 I/O 库为管理该流需要的所有信息,包括用于实际 I/O 的文件描述符,指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
通过 /usr/include/libio.h 查看 C 语言中 _IO_FILE 结构体的定义: 
struct _IO_FILE {  
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */  
#define _IO_file_flags _flags  
  
  /* The following pointers correspond to the C++ streambuf protocol. */  
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */  
  char* _IO_read_ptr;   /* Current read pointer */  
  char* _IO_read_end;   /* End of get area. */  
  char* _IO_read_base;  /* Start of putback+get area. */  
  char* _IO_write_base; /* Start of put area. */  
  char* _IO_write_ptr;  /* Current put pointer. */  
  char* _IO_write_end;  /* End of put area. */  
  char* _IO_buf_base;   /* Start of reserve area. */  
  char* _IO_buf_end;    /* End of reserve area. */  
  /* The following fields are used to support backing up and undo. */  
  char *_IO_save_base; /* Pointer to start of non-current get area. */  
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */  
  char *_IO_save_end; /* Pointer to end of non-current get area. */  
  
  struct _IO_marker *_markers;  
  
  struct _IO_FILE *_chain;  
  
  int _fileno;  
#if 0  
  int _blksize;  
#else  
  int _flags2;  
#endif  
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */  
  
#define __HAVE_COLUMN /* temporary */  
  /* 1+column number of pbase(); 0 is unknown. */  
  unsigned short _cur_column;  
  signed char _vtable_offset;  
  char _shortbuf[1];  
  
  /*  char* _save_gptr;  char* _save_egptr; */  
  
  _IO_lock_t *_lock;  
#ifdef _IO_USE_OLD_IO_FILE  
};  
 /usr/include/stdio.h 也可以看到对标准输入、标准输出、标准错误的定义
/* Standard streams.  */
extern struct _IO_FILE *stdin;      /* Standard input stream.  */
extern struct _IO_FILE *stdout;     /* Standard output stream.  */
extern struct _IO_FILE *stderr;     /* Standard error output stream.  */

/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr

三、缓冲

标准 I/O 库提供缓冲的目的是尽可能减少使用 read 和 write 调用的次数。它也对每个 I/O 流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。

1、标准 I/O 提供了以下 3 种类型的缓冲。

(1)全缓冲

在这种情况下,在填满标准 I/O 缓冲区后才进行实际 I/O 操作。对于驻留在磁盘上的文件通常是有标准 I/O 库实施全缓冲的。
在一个流上执行第一次 I/O 操作时,相关标准的 I/O 函数通常调用 malloc 获取需使用的缓冲区。
这部分可参看:C语言再学习 -- 输入/输出    了解缓冲相关函数

(2)行缓冲

在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。这允许我们一次输出一个字符,但只有写了一行之后才进行实际 I/O 操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

对于行缓冲有两个限制:
第一,因为标准 I/O 库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行 I/O 操作
第二,任何时候只要通过标准 I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流得到输入数据,那么就会冲洗所有行缓冲输出流

(3)不带缓冲

标准 I/O 库不对字符进行缓冲存储。例如,标准 I/O 函数 fputs、标准错误流 stderr 通常是不带缓冲的

2、ISO要求下列缓冲特征

(1)当且仅当标准输入和标准输出并不指向交互设备时,它们才是全缓冲的。
(2)标准错误绝不会是全缓冲的
(3)标准错误是不带缓冲的
(4)若是指向终端设备的流,则是行缓冲;否则是全缓冲的。

3、函数 setbuf 和 setvbuf

参看:C语言再学习 -- 输入/输出  讲的很详细了,就不重复了。

四、打开流

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);

1、参数解析

第一个参数:文件路径/文件描述符
第二个参数:mode 方式

mode:C 字符串,包含了文件访问模式,模式如下:

模式字符串

 

“r”

以只读方式打开文件,该文件必须存在

“r+”

以只读写方式打开文件,该文件必须存在

“w”

打开只写文件,若文件存在则文件长度清零,即该文件内容会消失。

若文件不存在则建立该文件

“w+”

打开可读写文件,若文件存在则文件长度清零,即该文件内容会消失。

若文件不存在则建立该文件。

“a”

以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,

写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)

“a+”

以附加方式打开可读写的文件。若文件不存在,则会建立文件,如果文件存在,

写入的数据会被加到文件尾后,即文件原先的内容会被保留。(原来的EOF符不保留)

“rb”, “wb”, “ab”, “ab+”, “a+b”,

 “wb+”, “w+b”, “ab+”, “a+b”

与前面的模式相似,只是使用二进制模式而非文本模式打开文件


2、函数功能

打开一个标准 I/O 流。

这 3 个函数的区别如下:
(1)fopen 函数打开路径为 path 的一个指定的文件
(2)freopen 函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出、标准错误。
(3)fdopen 函数取一个已有的文件描述符(我们可能从 open,dup,dup2,fcntl,pipe,socket,socketpair或accept函数得到此文件描述符) ,并使一个标准的 I/O 流与该描述符相结合。此函数常用于创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准 I/O 函数 fopen 打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用 fdopen 使一个标准 I/O 流与该描述符相结合。

3、返回值

成功返回文件指针,失败返回 NULL

4、示例说明

//示例一 fopen函数
#include<stdio.h>  
#include <stdlib.h>

int main (void)  
{  
    FILE*fp = NULL;  
    fp = fopen("abc.txt", "r");  
    if(NULL == fp)  
    perror("fail to fopen"), exit (1);  

    fclose (fp);  
    fp = NULL;  
     return 0;  
}  
//示例二 freopen 函数
#include<stdio.h>  
#include <stdlib.h>

int main (void)  
{  
	FILE*fp = NULL;  
	fp = freopen("abc.txt", "w", stdout);  
	if(NULL == fp)  
		perror("fail to freopen"), exit (1);  

	printf ("hello world!\n");
	
	fclose (stdout);
	fclose (fp);  
	fp = NULL;  
     return 0;  
}  
查看 abc.txt
# cat abc.txt 
hello world!
//示例三 fdopen 函数
#include <stdio.h>
#include <unistd.h>

int main (void)
{
	FILE * fp = fdopen (STDOUT_FILENO, "w+");
	fprintf (fp, "%s\n", "hello!");
	fclose(fp);

	return 0;
}
输出结果:
hello!

5、示例总结

示例一:fopen 函数很好理解,没什么可讲的。
示例二:freopen 函数,一般用于将指定的文件打开为一个预定义的流:标准输入、标准输出、标准错误。
示例三:fdopen 函数,打开文件描述符。对于 fdopen 来说,mode 参数的意义稍有区别。因为该描述符已被打开,所以 fdopen 为写而打开并不截断该文件 例如,若该描述符原来是由 open 函数创建的,而且该文件已经存在,则其 O_TRUNC 标志将决定是否截断该文件。
另外,标准 I/O 追加写方式也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。

6、扩展部分

除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可使用 setbuf 和 setvbuf 改变缓冲的类型
在该文件被关闭之前,冲洗(fflush )缓冲中的输出数据,缓冲区中的任何输入数据被丢弃。如果标准 I/O 库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时(直接调用 exit 函数,或从main 函数返回),则所有带未写缓冲数据的标准 I/O 流都被冲洗,所有打开的标准 I/O 流都被关闭
这部分在讲 return 和 exit 区别是有讲过,参看:C语言再学习 -- 关键字return和exit ()函数

五、读和写流 (文件输入/输出)

一旦打开了流,则可在 3 种不同类型的非格式化 I/O 中进行选择,对其进行读、写操作。
(1)每次一个字符的 I/O。
一次读或写一个字符,如果流是带缓冲的,则标准 I/O 函数处理所有缓冲。
(2)每次一行的 I/O。
如果想要一次读或写一行,则使用 fgets 和 fputs。每行都以一个换行符终止。当调用 fgets 时,应说明能处理的最大行长。
(3)直接 I/O。
fread 和 fwrite 函数支持这种类型的 I/O。每次 I/O 操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。

1、输入函数

以下 3 个函数可用于一次读一个字符。
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

(1)返回值

 成功返回下一个字符,若已到达文件尾端或出错,则返回 EOF

(2)函数比较

函数 getchar 等同于 getc (stdin)。前两个函数的区别是,getc 可被实现为宏,而 fgetc 不能实现为宏。这意味着以下几点。
1) getc 的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
2) 因为 fgetc 一定是个函数,所以可以得到其地址。这就允许将 fgetc 的地址作为一个参数传送给另一个函数。
3) 调用 fgetc 所需时间很可能比调用 getc 要长,因此调用函数所需的时间通常长于调用宏。
扩展:有副作用的表达式,指的是表达式执行后,会改变表达式中某些变量的值 
最简单的如++i,这个表达式执行后,i 的值会改变,这样的表达式是不应该在宏调用里出现的。

(3)示例说明

//示例一 fgetc 函数
#include <stdio.h>

int main (void)
{
	FILE *fp;
	int c;
	fp = fopen ("abc.txt", "r");
	while ((c = fgetc (fp)) != EOF)
	{
		if (c == 'b')
			fputc (c,stdout);
	}
	printf ("\n");
	fclose (fp);
	return 0;
}
输出结果:
b
//示例二 getc 函数 
#include<stdio.h>  
int main()  
{  
   char c;  
  
   printf("请输入字符:");  
   c = getc(stdin);  //等同函数 getchar
   printf("输入的字符:");  
   putc(c, stdout);  
     
   return(0);  
}  
输出结果:
请输入字符:f
输入的字符:f
//示例三 getchar 函数
#include <stdio.h>  
  
int main (void)  
{  
    int c;  
    while((c=getchar())!=EOF)  
        putchar(c);  
    return 0;  
}  

(4)示例总结

这 3 个函数在返回下一个字符时,将其 unsigned char 类型转换为 int 类型。说明为无符号的理由是,如果最高位为 1 也不会使返回值为负。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。在 <stdio.h> 中常量 EOF 被要求是一个负值,其值经常是 -1。这就意味着不能将这 3 个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量 EOF 比较。
注意,不管是出错还是到达文件尾端,这 3 个函数都返回同样的值。为了区分这两种不同的情况,必须调用 ferror 或 feof。
从流中读取数据以后,可以调用 ungetc 将字符再压送回流中。

2、输出函数

对应于上面所述的每个输入函数都有一个输出函数。
#include <stdio.h>
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

(1)函数比较

putchar函数,输出到显示器

putc函数,将字符输入到文件

把stdout作为putc()函数的第二个参数。stdout是在stdout中定义的与标准输出相关的文件指针,

所以putc (ch, stdout) 和 putchar ( )的作用是一样的。

putc 可被实现为宏,而 fputc 不能实现为宏。

(2)示例说明 同上

六、每次一行 I/O (字符串输入/输出)

1、下面两个函数提供了每次输入一行的功能。

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);

(1)函数比较

这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets 从标准输入读,而 fgets 则从指定的流读
对于 fgets 必须指定缓冲的长度 n。此函数一直读到下一个换行符为止,但是不超过 n-1 个字符,读入的字符被送入缓冲区。该缓冲区以 null 字节结尾。如若该行包括最后一个换行符的字符数超过 n-1,则 fgets 只返回一个不完整的行,但是,缓冲区总是以 null 字节结尾。对 fgets 的下一次调用会继续读该行。
gets 是一个不推荐使用的函数。其问题是调用者在使用 gets 时不能指定缓冲区的长度。这样就可能造成缓冲区溢出,写到缓冲区之后的存储空间中,从而产生不可预料的后果。

(2)示例说明

//示例一 fgets函数
#include <stdio.h>  
int main (void)  
{  
    char name [20];  
    char *ptr;  
    ptr = fgets (name, 20, stdin);  
    printf ("%s?, hi %s!\n", name, ptr);  
    return 0;  
}  
输出结果:  
JOY  
JOY  
?, hi JOY  
!  
//示例二 gets 函数
#include <stdio.h>  
int main (void)  
{  
    char name[50];  
    char *ptr;  
    while ((ptr = gets (name)) != NULL)  
    {  
        printf ("name is %s\n", name);  
        printf ("ptr is %s\n", ptr);  
        break;  
    }  
    return 0;  
}  
输出结果:  
HELLO  
name is HELLO  
ptr is HELLO  

2、fputs 和 puts 提供每次输出一行的功能

#include <stdio.h>
int fputs(const char *s, FILE *stream);
int puts(const char *s);

(1)函数比较

函数 fputs 将一个以 null 字节终止的字符串写到指定的流,尾端的终止符 null 不写出。注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非 null 字节。通常,在 null 字节之前是一个换行符,但并不要求总是如此。
puts 将一个 null 字节终止的字符串写到标准输出,终止符不写出。但是,puts 随后又将一个换行符写到标准输出。
puts 并不像它所对应的 gets 那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。如果总是使用 fgets 和 fputs ,那么就会熟知在每行终止处我们必须自己处理换行符。

(2)示例说明

//示例一 fputs函数
#include <stdio.h>  
int main (void)  
{  
    char name [20];  
    fgets (name, 20, stdin);  
    fputs (name, stdout);
    return 0;  
}  
输出结果:
hello
hello
//示例二 puts函数
#include <stdio.h>  
int main (void)  
{  
    char name[50];  
    char *ptr;  
    while ((ptr = gets (name)) != NULL)  
    {  
        puts (name);
    }  
    return 0;  
}  

七、二进制 I/O

#include <stdio.h>
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);

1、函数用法

这些函数有以下两种常见的用法。

(1)读或写一个二进制数组。

例如:为了将一个字符数组的第 2~5 个元素写到一文件上,可编写如下程序:
//fwrite 函数
#include <stdio.h>  
#include <stdlib.h>

int main (void)  
{
  	FILE *fp = fopen ("abc.dat", "wb");	
	char data[10] = "123456789";
	if (fwrite (&data[2], sizeof (char), 4, fp) != 4)
		perror ("fail to  fwrite"), exit (1);
    return 0;  
}  
查看 abc.data
# cat abc.dat 
3456
//fread 函数
#include <stdio.h>
int main()
{
	int arr[5]={},size=0,num=0;	//不要老是打错了好吗,仔细仔细再仔细。
	FILE *p_file=fopen("a.bin","r");
	if(p_file)
	{


		size=fread(arr,sizeof(int),5,p_file); //就是个数
		printf("size是%d\n",size);
		for(num=0;num<=4;num++)
		{
			printf("%d ",arr[num]);
		}
		printf("\n");
		fclose(p_file);
		p_file=NULL;
	}
	return 0;
}

(2)读或写一个结构。例如,可以编写如下程序:

//fwrite 函数
#include <stdio.h>    
#include <string.h>    
#include <stdlib.h>
    
typedef struct     
{    
	int n;    
	float m;    
	char name[20];    
}Ptr;    
    
int main (void)    
{    
	Ptr p;
	strcpy (p.name, "hello");  //注意字符串不能直接赋值    
	p.n = 11;    
	p.m = 12.9;    
	
	FILE *fp = fopen ("abc.dat", "wb");
  
	if (fwrite (&p, sizeof (Ptr), 1, fp) != 1)
		perror ("fail to  fwrite"), exit (1);

	return 0;    
}    
//fread 函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
	int id;
	float salary;
	char name[20];
}person;
int main()
{
	person p={};
	FILE *p_file=fopen("person.bin","rb");
	if(p_file)
	{
		while(1)
		{
			if(!fread(&p,sizeof(person),1,p_file))
			{
				break;
			}
			fread(&p,sizeof(person),1,p_file);//二进制写操作
			printf("id是%d,工资是%g,姓名是%s\n",p.id,p.salary,p.name);
		}
		fclose(p_file);
		p_file=NULL;
	}
	return 0;
}

2、使用二进制 I/O 的基本问题

它只能用于读在同一系统上已写的数据。常常有这种情况,在一个系统上写的数据,要在另一个系统上进行处理。在这种情况下,这两个函数可能就不能正常工作,其原因是:
(1)在一个结构中,同一成员的偏移量可能因编译器和系统而异。
(2)用来存储多字节整数和浮点值的二进制格式在不同的机器体系结构间也可能不同。

八、定位流

有三种方法定位标准 I/O。

1、ftell 和 fseek 函数。

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
它们都假定文件的位置可以存放在一个长整型中。 参看:C语言再学习 -- 文件  在此不做重复。

2、ftello 和 fseeko 函数。

#include <stdio.h>
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);
可使文件偏移量不必一定使用长整型。它们使用 off_t 数据类型代替了长整型。
除了类型区别外,ftello 函数 与 ftell 相同,fseeko 函数和 fseek 函数相同。
off_t 类型我们前面有讲到,linux中的 off_t 类型默认是 32 位的 long int。

3、fgetpos 和 fsetpos 函数

#include <stdio.h>

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, fpos_t *pos);
返回值:成功返回0,失败返回非0
它们使用一个抽象数据类型 fpos_t 记录文件的位置。这两个函数是由ISO C引入的。这种数据类型可以定义为记录一个文件位置所需的长度。
fgetpos 将文件位置指示器的当前值存入由 pos 指向的对象中在以后调用 fsetpos 时,可以使用此值将流重新定位至该位置。

(1)示例说明

#include <stdio.h>
#include <string.h>

int main (void)
{
	FILE *fp;
	fpos_t filepos;
	fp = fopen ("abc.txt", "w+");

	fgetpos (fp, &filepos);
	fputs ("hello world!", fp);

	fsetpos (fp, &filepos);
	fputs ("这将覆盖之前的内容", fp);

	fclose (fp);
	return 0;
}
查看 abc.ttx
# cat abc.txt 
这将覆盖之前的内容

(2)示例总结

首先我们使用 fgetpos() 函数获取文件的初始位置,接着我们向文件写入 Hello, World!,然后我们使用 fsetpos() 函数来重置写指针到文件的开头,重写文件。

九、格式化 I/O

1、格式化输出

格式化输出是由 5 个printf 函数来处理的。
#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

(1)函数比较

printf 将格式化数据写到标准输出;
fprintf 写至指定的流;
dprintf 写至指定的文件描述符;
sprintf 将格式化的字符送入数组 buf 中。sprintf 在该数组的尾端自动加一个 null 字节,但该字符不包括在返回值中
这部分之前也讲过:

(2)示例说明

//示例一  fprintf 函数
#include <stdio.h>  
int main()  
{  
    FILE *p_file = fopen("b.txt","w");  
    if(p_file)  
    {   //fprintf函数可以把数据按照格式记录到文本文件中  
        fprintf(p_file,"%c,%g,%d\n",'c',3.14,46);
        fclose(p_file);  
        p_file=NULL;  
    }  
    return 0;  
}  
查看 b.txt
# cat b.txt 
c,3.14,46
//示例二 dprintf 函数
#include <stdio.h>  
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>

int main (void)  
{  
	int fd = open ("a.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if (-1 == fd)
		perror ("fail to fopen"), exit (1);

	dprintf (fd, "%c, %g, %d\n", 'A', 5.12, 68);

	close (fd);
    return 0;  
}  
输出结果:
# cat a.txt 
A, 5.12, 68
//示例三 sprintf 函数
#include <stdio.h>  
  
#define SIZE 30  
int main (void)  
{  
    char str[SIZE];  
    sprintf (str, "%s %s %d\n", "I","love",512 );  
    puts (str);  
    return 0;  
}  
输出结果:  
I love 512  

(3)示例总结

printf 函数 太简单,这里就不写例子了。

示例一:fprintf () 的工作方式和 printf() 相似,区别在于前者需要第一个参数来指定合适的文件流
示例二: dprintf 一样和 printf 相似,区别在于 前者需要第一个参数来指定合适的文件描述符
虽然 dprintf 不处理文件指针,但我们仍然把它包括在处理格式化输出的函数中。注意,使用 dprintf 不需要调用 fdopen 将文件描述符转换为文件指针(fprintf 需要)。

以上 3 个函数,它们的返回值,成功 返回 输出的字符个数,失败返回负值。

例:rv = printf ("hello");  结果为rv = 5;

再有,需要掌握的 printf( ) 格式转换说明符,这里就不讲了。


示例三:注意,sprintf 函数可能会造成由 buf 指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。

溢出错误如下:

# ./a.out 
str = 12345678901234567890
len = 20
*** stack smashing detected ***: ./a.out terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xb7677dd5]
/lib/i386-linux-gnu/libc.so.6(+0xffd8a)[0xb7677d8a]
./a.out[0x80484d5]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb75914d3]
./a.out[0x80483a1]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 2102158    /home/tarena/project/c_test/a.out
08049000-0804a000 r--p 00000000 08:01 2102158    /home/tarena/project/c_test/a.out
0804a000-0804b000 rw-p 00001000 08:01 2102158    /home/tarena/project/c_test/a.out
094f1000-09512000 rw-p 00000000 00:00 0          [heap]
b7547000-b7563000 r-xp 00000000 08:01 1704884    /lib/i386-linux-gnu/libgcc_s.so.1
b7563000-b7564000 r--p 0001b000 08:01 1704884    /lib/i386-linux-gnu/libgcc_s.so.1
b7564000-b7565000 rw-p 0001c000 08:01 1704884    /lib/i386-linux-gnu/libgcc_s.so.1
b7577000-b7578000 rw-p 00000000 00:00 0 
b7578000-b7717000 r-xp 00000000 08:01 1704863    /lib/i386-linux-gnu/libc-2.15.so
b7717000-b7719000 r--p 0019f000 08:01 1704863    /lib/i386-linux-gnu/libc-2.15.so
b7719000-b771a000 rw-p 001a1000 08:01 1704863    /lib/i386-linux-gnu/libc-2.15.so
b771a000-b771d000 rw-p 00000000 00:00 0 
b772d000-b7731000 rw-p 00000000 00:00 0 
b7731000-b7732000 r-xp 00000000 00:00 0          [vdso]
b7732000-b7752000 r-xp 00000000 08:01 1704843    /lib/i386-linux-gnu/ld-2.15.so
b7752000-b7753000 r--p 0001f000 08:01 1704843    /lib/i386-linux-gnu/ld-2.15.so
b7753000-b7754000 rw-p 00020000 08:01 1704843    /lib/i386-linux-gnu/ld-2.15.so
bf960000-bf981000 rw-p 00000000 00:00 0          [stack]
已放弃 (核心已转储)

因为缓冲区溢出会造成程序不稳定甚至安全隐患,为了解决这种缓冲区溢出问题,引入了 snprintf 函数例如:

#include <stdio.h>  
#include <stdlib.h>

int main (void)  
{  
	char str[10] = {0};
	int len = snprintf (str, sizeof (str), "12345678901234567890");
	printf ("str = %s\n", str);
	printf ("len = %d\n", len);
    return 0;  
}  
输出结果:
str = 123456789
len = 20
在该函数中,缓冲区长度是一个显示参数,超过缓冲区尾端的所有字符都被丢弃。如果缓冲区足够大,snprintf 函数就会返回写入缓冲区的字符数。与 sprintf 相同该返回值不包括结尾的 null 字节。

以上这 2 个函数的返回值,成功返回将要存入数组的字符数,若编码出错,返回负值。

2、格式化输入

执行格式化输入处理的是 3 个 scanf 函数。
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
3个函数返回值:赋值的输入项数;若输入出错或在任一转换前已到达文件尾端,返回 EOF

(1)函数比较

scanf 族用于分析输入字符串,并将字符序列转换成指定类型的变量。在格式之后的各参数包含了变量的地址,用转换结果对这些变量赋值。

(2)示例说明

//示例一 fscanf 函数
#include <stdio.h>  
int main()  
{  
    char ch=0;  
    float fnum=0.0;  
    int num=0;  
    FILE *p_file=fopen("b.txt","r");  
    if(p_file)  
    {  
    	//fscanf函数可以从文件中按照格式把数据拷贝到内存的存储区里  
        fscanf(p_file,"%c %g %d",&ch,&fnum,&num);
        //拷贝到存储区我们就可以打印出来  
        printf("%c %g %d\n",ch, fnum, num);  
        fclose(p_file);  
        p_file=NULL;  
    }  
    return 0;  
}  
输出结果:
c 3.14 46
//示例二 sscanf 函数
#include <stdio.h>  
  
#define SIZE 30  
int main (void)  
{  
    char str[SIZE];  
	sscanf ("12345", "%s", str);
    puts (str);  
    return 0;  
}  
输出结果:
12345

(3)示例总结

示例一:fscanf 函数工作方式和 scanf 相似,区别在于前者需要第一个参数来指定合适的文件流
示例二:参看:C语言函数sscanf()的用法
sscanf 与 scanf 类似,都是用于输入的,只是后者以屏幕 (stdin) 为输入源,前者以固定字符串为输入源

需要掌握的 scanf() 格式转换修饰符,这里就不再重复了。
最后,需要注意下这 3 个函数的返回值,成功输入的项目个数
巧用scanf()返回值:
status = scanf ("%ld", &num);
while (status == 1)
{
  status = scanf ("%ld", &num);
}  
/*当输入整数则执行while循环,例如输入Q,则scanf返回值为0,循环终止*/
也可用下列形式代替:
while(scanf ("%ld", &num) == 1) {}

十、函数 fileno

#include <stdio.h>
int fileno(FILE *stream);
返回值:与该流相关联的文件描述符

1、函数功能

每个标准 I/O 流都有一个与其相关联的文件描述符。可以对一个流调用 fileno 函数以获得其描述符。
在讲文件描述符时讲过了,参看:UNIX再学习 -- 文件描述符

2、示例说明

//示例一
#include <stdio.h>  
int main(void)  
{  
    FILE *fp;  
    int fd;  
    fp = fopen("/etc/passwd", "r");  
    fd = fileno(fp);  
    printf("fd = %d\n", fd);  
    fclose(fp);  
    return 0;  
}  
输出结果:  
fd = 3  
//示例二
#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
int main (void)  
{  
		int fd = fileno (stdout);
    int newfd = dup (fd);  
    if (-1 == newfd)  
        perror ("Fail to dup"), exit (-1);  
  
    printf ("newfd = %d\n", newfd);  
    write (newfd, "hello world\n", 12);  
    return 0;  
}  
输出结果:
newfd = 3
hello world

3、示例总结

示例一: fileno 可以用来取得参数 stream 指定的文件流所使用的文件描述符。
示例二:如果要调用 dup 或 fcntl 等函数,则需要此函数。

十一、未讲部分

标准 I/O 的效率
格式化 I/O printf/scanf 族的变体
临时文件
内存流
标准 I/O 的替代软件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值