C/C++ IO细节

1 C和C++的IO函数

C主要使用scanf/printf及其相关函数( fread, fwrite等 )
C++主要使用std::cin/std::cout流对象

2 IO缓冲区

缓冲区分几个层次:

  • 用户级别缓冲,例如你可以开一个很大的char数组,然后多次调用snprintf,最后输出这个char数组;
  • 库级别缓冲,这是当调用库封装的函数(注意,不是调用操作系统提供的api函数)时,库为了提高性能所设定缓冲区,缓冲区类型有三种(不缓冲,全缓冲(块缓冲),行缓冲),可以通过setvbuf来更改,一个需要注意的地方是库的默认行为。libc一般是这样的:默认情况下就会打开stdin,stdout,stderr,其中stderr是不缓冲的,如果stdin和stdout映射到交互式设备(例如终端),那么是行缓冲的,否则是全缓冲的。[6][7]
    下面举两个例子来说明:
int main() {
    fprintf( stdout, "hello, " );
    fprintf( stderr, "world!\n" );
}

实际输出是:

world!
hello,

这是因为默认情况下stderr不缓冲,而stdout无论是否映射到交互设备,都有缓冲,所以会后输出。

int main() {
    fprintf( stdout, "hello\n" );
    // fflush( stdout );
    write( STDOUT_FILENO, "world\n", 6 );
}

这段代码如果在终端屏显,那么输出

hello
world

如果将结果重定向到一个文件,那么输出

world
hello

这是因为重定向到文件,stdout就是全缓冲了(并不会因为读到’\n’刷新缓冲区),这时”hello\n”还在库缓冲区里,而write是系统调用,直接就把”world\n”写给操作系统了,然后进程退出时,触发刷新库缓冲区,”hello\n”才被提交给操作系统。
上面这个情况需要注意,因为它会因为重定向产生不一样的结果,@aikilis同学就在这个上面吃了大亏。

另外标准库提供了一个fflush函数,它会将库缓存提交给操作系统;如果将上面那个fflush的注释去掉,那么输出结果就一致了。

  • 操作系统级别缓存,磁盘操作是个慢操作,操作系统为了提高效率,可能会提供一块内核的内存作为缓冲区,所以即使调用fflush,也并不意味着数据已经到了磁盘,一般来说操作系统可靠性相当高了,但是如果系统掉电,内存数据就over了,对于某些数据库应用,这个损失也无法忍受,所以系统api会提供刷新缓存操作fsync,FlushFileBuffers。[5]
  • 磁盘cache,它的作用类似于cpu cache,也是提供一个高速缓存,使得效率提高,而这一层缓存,对于我们来说已经无法控制了。

C++的std::endl

它不仅仅是输出一个换行符,还执行了flush操作(也就是将库缓存数据提交给操作系统),这样做的优点是不会因为程序意外退出少打印数据,缺点是无法利用buffer,性能急剧下降。所以对于不那么重要的操作,尤其是OJ的一些输出换行,建议用’\n’而不要用std::endl。

3 性能对比

scanf/printf

a> 从编译器角度,C编译器生成的代码不会比C++编译器差,甚至可能略占优势(之前是这么讲,不知道现在情况如何)
b> 需要解析format字符串,先判断类型,然后才能做相应的处理,这会占用一定的时间
c> mingw版本下的cstdio可能会慢很多,msvcrt的IO不符合ANSI标准,所以mingw自己加入了一段wrapper,并且引入了__USE_MINGW_ANSI_STDIO宏作为开关,默认是关闭的,但是对于C++来讲还需要libstdc++库,在编译libstdc++的时候__USE_MINGW_ANSI_STDIO却是打开的。[1][2]

cin/cout

a> 默认情况下,C++为了在交杂使用cin/cout和scanf/printf不至于出错时,将C和C++输出输入流绑在了一起,导致每次同步都要额外消耗时间,这个可以通过std::ios::sync_with_stdio(false);关闭
b> 默认情况下,C++的cin上会绑定对应的输出流cout,每次对cin进行任何IO操作都会flush cout,可以通过std::cin.tie( 0 );解除这种绑定
c> 因为数据类型编译期间可以确定,所以节省了parse时间,较新版本的编译器已经可以体现出这个优势。

在gcc 4.9.1下测试如下代码(预先生成一个data文件,里面存储1000000个整数):

#ifdef _USE_STDIO
    #include <stdio.h>
#endif

#ifdef _USE_CSTDIO
    #include <cstdio>
#endif

#ifdef _USE_CIN
    #include <stdio.h>
    #include <iostream>
#endif

const int NUM = 1000000;

int main()
{
    freopen( "data", "r", stdin );
    int n;
    for(int i = 0 ; i < NUM ; i++) {

#ifdef _USE_STDIO
        scanf( "%d", &n );
#endif
#ifdef _USE_CSTDIO
        scanf( "%d", &n );
#endif

#ifdef _USE_CIN
#ifdef _NO_SYNC
        std::ios::sync_with_stdio( false );
#endif
#ifdef _NO_TIE
        std::cin.tie( 0 );
#endif
        std::cin >> n;
#endif
    }  
    return 0;
}

使用如下命令行编译和运行

g++ test_cin.cc -D_USE_STDIO -O2 -o test_stdio
g++ test_cin.cc -D_USE_CSTDIO -O2 -o test_cstdio
g++ test_cin.cc -D_USE_CIN -O2 -o test_cin
g++ test_cin.cc -D_USE_CIN -D_NO_SYNC -O2 -o test_cin_nosync
g++ test_cin.cc -D_USE_CIN -D_NO_SYNC -D_NO_TIE -O2 -o test_cin_nosync_notie
time ./test_stdio
time ./test_cstdio
time ./test_cin
time ./test_cin_nosync
time ./test_cin_nosync_notie

测试结果(大于号表示速度快)是:
test_cin_nosync_notie > test_cin_nosync > test_stdio ≈ test_cstdio > test_cin
因为我的测试环境是linux,没有使用mingw,所以stdio.h和cstdio没区别。

更快速度

当然速度更快的还是自己申请比较大的缓冲区,调用fread(间接调用系统api read)直接读一大块数据,然后自己手动解析,这样做可以减少系统调用次数,从而缩短时间,缺点是额外使用空间。
如果确定不会有并发问题,可以调用fread_unlocked,从而去掉同步过程,速度更快,这只有在fread被频繁调用时才能展现优势。

4 格式符format相关

a> 在%后面的接*号
对于scanf来说,是按照相应格式读入,然后忽略;对于printf来说,会先把一个参数替换到*的位置,然后该处格式对应下一个参数。

scanf( "%*d%d", &a ); // 输入1 2,a == 2,因为1被忽略了
printf( "%0*.*f\n", 10, 4, 1.2 );  // 输出 00001.2000

b> 在%或*后面接m$,其中m是一个正整数
这表示使用第几个参数,参数列表索引从1开始,相当于用%m$对应参数代替%,用*m$对应的参数代替*

printf( "%2$*1$d", a, b );  // 等价于printf( "%*d", a, b ); a是第一个参数,b是第二个参数,%2$表示用第二个参数作为%,*1$表示用第一个参数做宽度控制

c> %n的使用
对于printf来说,可以将输出字符个数统计到参数中

printf( "%nhello%n\n", &a, &b );  // a == 0 b == 5

d> %[的使用
对于scanf来说,可以使用部分正则字符串功能,可以用’[’和’]’中间写一个字符串集合,遇到任何一个字符都匹配,用’^’表示不包括

scanf( "%*[ \n]%c", &c );  // 略过输入开头的空格和换行符,将第一个字符读入c中

5 安全性

因为sprintf没有对缓冲区大小做检查,很容易引起缓冲区溢出攻击,所以建议替换成snprintf
printf也因为使用格式符,一旦其可能引入用户输入,就会产生问题,例如你如果从某个地方得到用户输入字符串str,直接将其作为format串输出,printf( str );就会造成安全隐患,应该用printf( “%s”, str );这样的方法调用,更详细的分析可见[4]

参考
[1] http://www.zhihu.com/question/21016898
[2] http://stackoverflow.com/questions/17236352/mingw-w64-slow-sprintf-in-cstdio
[3] http://www.hankcs.com/program/cpp/cin-tie-with-sync_with_stdio-acceleration-input-and-output.html
[4] http://drops.wooyun.org/binary/6259
[5] http://blog.csdn.net/tianwailaibin/article/details/6709490
[6] http://www.douban.com/note/162435828/
[7] man stdout

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值