都说细节决定成败,我觉得的编程来说,特别是面试的时候细节最能决定的是关键时候你能装的程度,所以我想有个系列记录我遇到的各种我遇到的细节问题,以备不时之需啊。
cout<<"Hello,world"<<endl;作为我真正学习写程序的起点我一直对其怀有感激之心,想到大一学C++的时候看到这个的时候觉得,这就是写程序吗?这就是我以前梦想的能够让电脑听我指挥,什么软件,游戏,病毒的开发吗?不像啊,这个也没什么作用啊,就看着个黑屏幕显示一句话,什么也做不了,那时也不懂得什么东西的学习都是漫长的过程,但是好在后面也没想太多,相比当时大多数兴趣被这黑屏浇灭而后再也没有研究过编程的同学,我还算幸运的莫名的坚持了下来。这几天,无意中看到一个C++教材第一页的hello world,脑子里面竟然想到当时我初学的时候老师跟我们说想把东西输出到屏幕上还可以用cout,cerr和clog,我还记得那个时候我还都试了试,发现都可以,但是也没有太在意其中的学问,只是疑问一样的功能为什么要搞三个名字,老师自然也是搬ppt的东西,也没有解释。后来我学会了查msdn,查到了这三个的区别在于重定向和缓冲,直到前几天,我居然在项目中遇到了有关这三个的问题,虽然不难解决,于是我觉得我还是有必要研究研究这个细节。
一切都先从msdn开始比较靠谱,对于这三个的差异,msdn是这么解释的:
The ostream class, through the derived class basic_ostream, supports the predefined stream objects:
-
cout standard output
-
cerr standard error with limited buffering
-
clog similar to cerr but with full buffering
这里面关键的一个词是buffering(什么?重定向,这里暂时没有看到,生命有限,一次不能研究的太多),buffer存在于计算机软硬件的各个方面,缓冲区的意义很好理解,最大的好处无非是可以提高效率,每天跑到银行存1000块钱和你十天凑齐10000块钱一次性向银行存入,所耗费的时间果断不能是一样的,所以无论在哪个方面缓冲都是人类的好朋友啊。这三个的差别从字面上看,第一个根本没有提buffering什么事,第二个有limited buffering,第三个是full buffering,有什么区别呢,先从第一个开始吧,继续在msdn中查找cout,看看这家伙怎么说的。让人是失望的是这里面基本没什么有用的信息,但是都说超链接改变世界,这里面的example有个cerr的超连接,正好它也排在第二个,顺手看看cerr好了,里面是这样描述的:
The object controls unbuffered insertions to the standard error output as a byte stream. Once the object is constructed, the expression cerr.flags & unitbuf is nonzero.
对比cout和前面的内容,unbuffered这个关键词再一次引起我们的关注,另外后面还多了一句话,提到了flags和unitbuf,在看了clog发现和cout没什么差别之后,每一个发育自然并且想研究一下的人都会查看一下unitbuf或者flags。查看flags发现这是一个和fmtflags相关的函数,而且后面还有一个例子:
#include <iostream>
#include <fstream>
int main ( )
{
using namespace std;
cout << cout.flags( ) << endl;
cout.flags( ios::dec | ios::boolalpha );
cout << cout.flags( );
}
运行一下会查看到cout的flag值,会看到输出是两个很奇怪的整数531和16896,这个时候就要去看看fmtflags到底是什么,msdn上是这么写的:
Constants to specify the appearance of output.
static const fmtflags boolalpha(15), dec(10), fixed(14), hex(12), internal(9), left(7), oct(11), right(8), scientific(13), showbase(4), showpoint(5), showpos(6), skipws(1), unitbuf(2), uppercase(3), adjustfield, basefield, floatfield; |
第一反应是这是什么玩意儿,还好msdn是高大全的参考,在Remark部分可以看到如下语句:
Supports the manipulators in ios.
The type is a bitmask type that describes an object that can store format flags.
两层含义,第一个支持ios里面的操作符,第二个是这是一个bitmask类型,也就是说上面的18个fmtflags类型定义的玩意儿都是这个类型的,ios里面的操作符很好理解,就是那些可以定义cout(当然同样作为输出流的cerr,clog也是可以被定义的)格式的东西,比如最简单的cout<<hex<<12;可以输出十六进制的12,也就是说fmtflags这个的作用就是使用另一种方式设定格式,设定的方法前面那个例子中也显示了,虽然是定义了18个fmtflags变量,但是在这个词条的底部也说明了最后三个是前面15个某些的组合,也就是说可以大胆的猜想flags有15位,具体这15个具体有什么作用,对此有兴趣的可以继续探究,上面每个变量旁边的括号中的数字是我亲测得每个对应的flags的位,假设最低位是从1开始的。
另外,上面的程序531转换成二进制就是1000000001,也就是默认的cout设定的是dec和skipws两个位,skipws全名叫做skipwhitespace,这也很容易解释了cout的时候输出的十进制的数并且能够忽略前导空格,这算一个细节吧。
在所有的flag里面,凭借一股风骚的意识你应该能发现有一个很眼熟啊--unitbuf,这个在前面cerr的说明里出现过,滚动一下鼠标可以发现cerr的说明中说这个位在cerr里面是非0的,而其余两个没有特别说明,那么我们就看看初始情况下clog和cerr的flag是什么,可以发现clog和cout是一样的,cerr却是1000000011,是不是有种看破的感觉,unitbuf既然有个buf,再加上cerr关于buffer的特别说明,可以猜想这个位就是控制buf的,再看看它的说明:
unitbuf, to flush output after each insertion.
翻译成中文就是每次输出的时候刷新输出,这句话在你什么也不知道的时候确实很疑惑,什么叫flush?前面说了这里面的符号位和ios的操作都是对应的,那么看看ios里面有什么相关的内容好了,找到ios members可以看到其中有个函数也是较unitbuf,这个时候就得赶紧点进去看看,Remark部分是这么写的:
Note that endl also flushes the buffer.
如果你从没有接触过这方面的内容,那么恭喜你,除了endl能换行之外,你至少知道了endl还可以刷新缓冲区,现在面临一个新问题,什么叫flushes the buffer,msdn不好找了,这就要回到缓冲区这个概念上来了。
所谓缓冲区前面已经大概的解释了,你每天能赚1000(貌似有点多),然后你去存银行,这就是没有缓冲,但是你嫌这样太麻烦,你决定先攒几天钱再存银行,这样的话就是有缓存。但是总会有个条件让你达到要去存这个行为,这个条件或者是到了多少天,或者是到了多少钱,或者是你老婆迫使你赶紧去存,因为身上现金多了容易遭抢啊或者现金多了容易起干坏事,毕竟男人的银行卡一般都不在自己身上。但是事务都有两面性,虽然你每天存很耗时间,但是至少保证比好几天一存的安全性大(假设银行是信得过的)。
输出缓冲区和上面这个过程也很像,如果不是设置的强制刷新的这个位,那么缓冲区只能在两种情况下刷新,第一个是缓冲区满了(时间到了或者钱总量到了),或者是强制要其刷新,比如使用了endl,flush等等(你老婆叫你去存钱),或者设置了unitbuf位。回到这三个输出流上面来,cerr既然设置了unitbuf项的位,它每次输出都会刷新输出缓冲区,这样的好处就是在程序出了一些特殊错误的情况下仍然能够有输出(费时但是安全),比如递归堆栈崩溃,而cout不会,那么刷新缓冲区是往哪儿刷新呢?在这里是往输出设备(不一定是屏幕),通俗的说就是如果刷新没有进行,那么就不会显示在控制台的窗口上,好了,费了这么多话,最关键的要眼见为实,怎么样才能看到这一个现象呢?按照上面费的这么多话,cerr和cout在表现上应该是不同的,所以我们用这两个试试看。
想看到这个过程,我决定用一个Sleep,用来延缓看到这个过程,除了加上windows头文件,在main里面写如下代码:
cout<< "A";
Sleep(1000);
cout<<flush;
如果上面说的是真的,那么应该是过了1秒左右之后再显示'A',可是你却发现它直接出来了,没有那关键的一秒延迟,再换乘cerr,发现现象是一样的,cerr应该是这样的,我觉得输出cout的flags看看,发现unitbuf没有改变阿,是0啊,没错啊,我被这个问题困扰了很久,最后我终于找到了原因,上面费的那么多话都是C++标准,而VS里面使用std里面的cout不一定是符合这个标准的,无论你用什么都会自动加一个flush,通俗的说就是无论你用哪一个都会立即输出,原因应该是无论发生错误都能输出东西,以方便调试等等,好了,问题找到了,得找个解决方案了,最终我找到了一个c语言中的函数,在这些代码之前加上:
char* pbuffer = new char[512];
setbuf(stdout, pbuffer);
这个函数就是设置缓冲区(或者你用更新的setvbuf),用完这个之后,你会发现A是一秒之后再出现的,你可以试试删除掉flush,你会发现'A'根本不会出现,换成cerr不用cerr同样就完全不会出现这个问题,'A'是立即出现的。这说明了缓冲和立即刷新之间的区别,而且还可以把第二个cout换成cout<<flush<<'B',你会发现B根本不会出现,因为B之后没有再刷新了,你还可以保证第一个cout不变,第二个改成cerr,虽然刷新了缓冲区,但是仍然不能输出,说明这三个输出流用的不是一个缓冲区,这个是我下一篇的主题。
上面还说了,缓冲区满了也会强制输出,那么我们用以下代码看看结果:
char* pbuffer = new char[1024];
setbuf(stdout, pbuffer);
for(int i=0;i<1024;i++) cout<<'A';
cout<<'B';
Sleep(1000);
可以看到1024个A输出了,可是B没有输出,因为1024个A占满了缓冲区,强制刷新了缓存,后面的B自然输出不能。
最后,cout的c到底代表什么,这个也是在我学习c++很长一段时间后才知道的,很简单我居然没想到,c代表的是控制台的console。