Linux中C/C++输入输出流浅析

版权声明:此文章转载自极infocool

原文链接:http://www.infocool.net/kb/CPlus/201607/165227.html

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com

概要 

本文详细介绍Linux中C和C++程序的输入输出流,从操作系统层面向上至C++的相关类。

目录

Linux I/O

I/O重定向

C的输入输出

C++的输入输出

Linux I/O

Linux中,每个打开的文件都是通过文件描述符(File Descriptor)来标识的,内核为每个进程维护了一个文件描述符表(FDT),这个表以FD为索引,再进一步指向文件的详细信息。在进程创建时,内核为进程默认创建了0、1、2三个特殊的FD,这就是STDIN、STDOUT和STDERR:

这里写图片描述

首先看一个测试程序:

#include<stdio.h>
int main()
{
        extern FILE *stdin;
        extern FILE *stdout;
        extern FILE *stderr;
        printf("stdin's fd is : %d\n", stdin->_fileno);
        printf("stdout's fd is : %d\n", stdout->_fileno);
        printf("stderr's fd is : %d\n", stderr->_fileno);
        return 0;
}

其中直接获得结构体成员_fileno即是fd,也可以调用函数fileno获得fd,fileno其实就是一个宏定义,也是访问结构体成员_fileno达到目的。输出结果:

stdin's fd is : 0
stdout's fd is : 1
stderr's fd is : 2

我们可以在相关头文件看到声明,在stdio.h中:

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. */
typedef struct _IO_FILE FILE;

可以看出stdin,stdout,stderr就是我们熟知的FILE结构体指针。在glibc的代码中,以stdin为例:

_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS)

FILE结构体中成员_fileno即fd,故stdin指向的FILE结构体fd为0。 

再看一个测试程序:

#include <stdio.h>
 int main()
 {
        printf("%s\n", "hello world!");
        fprintf(stdout, "%s\n", "hello world!");
        return 0;
 }

输出结果:

hello world!
hello world!

可以看出,两个语句效果相同。其实,我们在glibc的代码中找到printf的实现:

 __nldbl_vfprintf (stdout, fmt, arg);

和fprintf的实现:

__nldbl_vfprintf (stream, fmt, arg);

可以看出printf就是将stdout作为默认参数的fprintf。

小结

Linux中程序的输入输出都是对文件操作的。每个程序开始运行时都会初始化3个FILE指针stdin,stdout,stderr,他们的文件描述符为0,1,2。printf函数的实质就是一个默认参数为stdout的fprintf。 

另外,值得一提的是,stdin和stdout都是行缓冲的,而stderr是无缓冲的,磁盘文件I/O是全缓冲的,参考这里。

I/O重定向

我们可以在shell重定向I/O,也可以在程序的代码中重定向I/O。在I/O重定向的过程中,不变的是FD 0/1/2代表STDIN/STDOUT/STDERR,变化的是文件描述符表中FD 0/1/2对应的具体文件。 

shell中有一下几个重定向符:

< 或 0<

open a file for reading and associate with STDIN

用法:command < filename

<< 或 0<<

use the current input stream as STDIN for the program until token is seen. i.e. "here document".

用法:command << endflag

     input for command

     endflag

> 或 1>

open a file for writing and truncate it and associate it with STDOUT.

用法:command > filename

>> 或 1>>

open a file for writing and seek to the end and associate it with STDOUT. This is how you append to a file using a redirect.

用法:command >> filename

2>

用法同1>,输出的是错误信息

用法:command 2> filename

2>>

用法同1>>,输出的是错误信息

用法:command 2>> filename

n>&m

redirect FD n to the same places as FD m. Eg, 2>&1 means send STDERR to the same place that STDOUT is going to.

n>&-

关闭文件描述符n

用法:n如果为0,1都是可以省略的,m前不能有空格。

也可以使用exec命令进行重定向,用法举例:

#!/bin/bash 
exec 3<&0 0<input.txt
read line1
read line2
exec 0<&3
echo $line1
echo $line2

其中exec 3<&0 0<input.txt把标准输入重定向到文件描述符3(0表示标准输入),然后把文件input.txt内容重定向到文件描述符0,实际上就是把文件name.txt中的内容重定向到文件描述符3。然后通过exec打开文件描述符3;然后,通过read命令读取name.txt的第一行内容line1,第二行内容line2,通过Exec 0<&3关闭文件描述符3;最后,用echo命令输出line1和line2。

在程序中也可以重定向,C语言中有freopen函数:

#include<stdio.h>
int main()
{
        freopen("input","r",stdin);
        freopen("output","w",stdout);
        char str[10];
        scanf("%s", str);
        printf("%s\n", str);
        freopen("/dev/pts/1","r",stdin);
        freopen("/dev/pts/1","w",stdout);
        //将stdin和stdout恢复
        scanf("%s", str);
        printf("%s\n", str);
        return 0;
}

scanf会从文件input读入字符串,而第一个printf会将读入的字符串输出到文件output;第二个freopen则将stdout再次重定向到输出设备,这里的输出设备是从xshell连接的伪终端,可以在shell中用w命令或ps命令查看都有哪些登录的用户以及他们的终端类型。这里简单说下tty和pts的区别,tty是硬链接的终端,如在物理机中ctrl+alt+F1进入的界面即是tty1,GUI中点开的terminal,或是xshell的连接,都是pts伪终端,这些信息都可以在w命令中看到,pts设备文件的路径为/dev/pts/num,tty设备文件路径为/dev/ttynum。

C的输入输出

scanf

定义:int scanf ( const char * format, … ); 

Read formatted data from stdin and stores them according to the parameter format into the locations pointed by the additional arguments.The additional arguments should point to already allocated objects of the type specified by their corresponding format specifier within the format string.

参数const char * format 

常量字符串format控制着流中的字符如何被处理,关于format: 

1. scanf会读入并丢弃format开头的所有空白符(空白符包括空格,换行,制表)直到遇到非空白符,而format中的一个空白符可以匹配流中任意个空白符,包括0个; 

2. scanf对于format中空白符和格式区分符(以字符%开头)以外的字符,将与流中的字符严格匹配,如果匹配成功,将忽略这个字符,继续看format下一个字符。如果匹配失败,scanf将fail并返回,流中仍有未读的字符。(经测试流中匹配失败的那个字符仍在缓冲区); 

3. 格式区分符:以%开头,用来说明流中将被读取数据的类型和格式。接下来重点介绍格式区分符。

格式区分符 

%[*][width][length]specifier 

需要说明的是,参数列表中的变量个数应该不少于格式区分符的个数,多出的变量参数会被忽略。specifier有以下几种,粗体为对于specifier能匹配的字符:

specifier Description Characters extracted

i Integer Any number of digits, optionally preceded by a sign (+ or -).Decimal digits assumed by default (0-9), but a 0 prefix introduces octal digits (0-7), and 0x hexadecimal digits (0-f).Signed argument.

d or u Decimal integer Any number of decimal digits (0-9), optionally preceded by a sign (+ or -).d is for a signed argument, and u for an unsigned.

o Octal integer Any number of octal digits (0-7), optionally preceded by a sign (+ or -).Unsigned argument.

x Hexadecimal integer Any number of hexadecimal digits (0-9, a-f, A-F), optionally preceded by 0x or 0X, and all optionally preceded by a sign (+ or -).Unsigned argument.

f, e, g, a Floating point number A series of decimal digits, optionally containing a decimal point, optionally preceeded by a sign (+ or -) and optionally followed by the e or E character and a decimal integer (or some of the other sequences supported by strtod).Implementations complying with C99 also support hexadecimal floating-point format when preceded by 0x or 0X.

c Character The next character. If a width other than 1 is specified, the function reads exactly width characters and stores them in the successive locations of the array passed as argument. No null character is appended at the end.(注:%c会读入空白符)

s String of characters Any number of non-whitespace characters, stopping at the first whitespace character found. A terminating null character is automatically added at the end of the stored sequence.

p Pointer address A sequence of characters representing a pointer. The particular format used depends on the system and library implementation, but it is the same as the one used to format %p in fprintf.

[characters] Scanset Any number of the characters specified between the brackets.A dash (-) that is not the first character may produce non-portable behavior in some library implementations.(注:与%[length]c不同的是,这个会在接收变量最后加\0)

[^characters] Negated scanset Any number of characters none of them specified as characters between the brackets.(注:同上,会在接收变量最后加\0)

n Count No input is consumed.The number of characters read so far from stdin is stored in the pointed location.

% % A % followed by another % matches a single %.

The format specifier can also contain sub-specifiers: asterisk (*), width and length (in that order), which are optional:

sub-specifier description

* An optional starting asterisk indicates that the data is to be read from the stream but ignored (i.e. it is not stored in the location pointed by an argument).

width Specifies the maximum number of characters to be read in the current reading operation (optional).

length One of hh, h, l, ll, j, z, t, L (optional).This alters the expected type of the storage pointed by the corresponding argument.

scanf返回值

成功时返回参数列表中得到值的变量个数(可以是0。匹配失败且此时还未读入变量的情况算成功),这个个数可能由于匹配失败,读错误,到达EOF而小于总的变量个数。 

如果在未成功读入任何数据前发生读错误,或是到达EOF,则返回EOF(-1)。

scanf总结

scanf对于数字类型,字符串类型的格式区分符匹配,会先把流中开始的空白符(whitespace,换行空格制表)读入并丢弃,直到遇见非空白符;匹配失败时,失败的那个字符仍在输入缓冲区里。

接下来看几个应用场景的测试例:

1, 在已知输入格式时读入一行

input.txt(每行开头,间隔,还是末尾的空格数量随意):

     xjy     18      USTC 
   cc     19      MIT   
     nami             20 DKU

读入并输出

#include<stdio.h>
int main()
{
        freopen("input.txt", "r", stdin);
        char name[10], school[10];
        int age;
        while (scanf("%s%d%s", name, &age, school) != EOF) {
                printf("name: %s, age: %d, school: %s\n", name, age, school);
        }   
        return 0;
}

再次强调,scanf在匹配数字类型%i, %d, %u, %o, %x, %f以及字符串类型%s时,会在成功匹配前把流中的空白符都读入并忽略。如在读入第一行最后一个字符串USTC后,为了匹配format中下一个格式区分符%s,则把USTC之后的数个空格和换行以及第二行开头的数个空格都读入并丢弃,然后把cc读入name。

2, 未知输入格式时读入一行到一个字符数组,input.txt同上。

#include<stdio.h>
int main()
{
        char s[50], *fmt = "%[^\n]%*c";
        freopen("input.txt", "r", stdin);
        while (scanf(fmt, s) != EOF) {
                printf("%s\n", s); 
        }   
        return 0;
}

这个测试例中,%[^\n]即匹配任意不是\n的字符,而每行末尾的换行却不能被处理掉,所以用%*c来吸收掉换行,从而完成读取一行含空格的字符串的目的。

3, %[characters]和%[^characters]的使用

#include<stdio.h>
int main()
{
        char s1[10] = {'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y'};
        char s2[10] = {'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y'};
        char s3[10] = {'y', 'y', 'y', 'y', 'y', 'y', 'y', 'y'};
        scanf("%5c", s1);
        //读入5个字符给s1,并不补\0
        printf("s1:%s\n", s1);
        scanf("%[^x]", s2);
        //遇到字符'x'前都读入到s2,如果上来就是x,则scanf返回EOF
        printf("s2:%s\n", s2);
        scanf("%[a-x]", s3);
        //范围在a-x内的字符读入到s3
        printf("s3:%s\n", s3);
        return 0;
}

测试:

输入:

abcdexxx

输出:

s1:abcdeyyy

s2:yyyyyyyy

s3:xxx

分析: 

第一个scanf读入5个字符并写入s1,且不补\0,所以s1并未截断;第二个scanf要求读入不是x的字符,然后此时输入缓冲区中还有xxx和\n,故匹配失败,scanf直接返回0;第三个scanf则要求读入a-x的字符,则把xxx读入,且自动补\0。

4, 挨个读取字符,可以用getchar。相关函数getch、getche、fgetc、getc、getchar、fgets、gets可参考这里。

#include<stdio.h>
int main()
{
        freopen("input.txt", "r", stdin);
        char c;
        while ((c = getchar()) != EOF)
                printf("%d\n", c); 
        return 0;
}

值的一提的是,vim会自动在文本最后加入换行(\n,值是10)。

printf

这个貌似没什么梗~=.=~参考手册http://www.cplusplus.com/reference/cstdio/printf/

C++的输入输出

Google C++ 编码规范中提到,建议不要使用流,除非是日志接口需要,使用printf 之类的代替。

cin的确比scanf效率低,虽然可以使用ios_base::sync_with_stdio(false);关闭C++与C输入输出流的同步来提升性能,不过也有相关问题。所以建议C++也使用printf和scanf。

不过这里还是总结下C++输入输出的相关类。

C++_stream_class

啊。C++的相关梗太多。。坑之后再填。。。

想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

郝淼emily

重新开始,从心开始

我要评论

评论请先登录,或注册