C++中endl的本质是什么

1. endl的本质

自从在C语言的教科书中利用Hello world程序作为学习的起点之后,很多程序设计语言的教科书都沿用了这个做法。我们写过的第一个C++程序可能就是这样的。

#include <iostream>
using namespace std;

int main(){
 cout<<"Hello world"<<endl;
}

学习过C语言的程序猿自然会把输出语句与C语言中的输出语句联系起来,也就是说: cout<<”Hello world”<<endl;相当于printf(“Hello world\n”);由于endl会导致输出的文字换行,自然而然地我们会想到endl可能就是换行符’\n’。

但是,如果我们定义char c=endl;会得到一个编译错误,这说明endl并不是一个字符,所以应该到系统头文件中去查找endl的定义。通过VS2012转到定义,找到了endl的定义如下:

template<class _Elem,class _Traits> inline basic_ostream<_Elem, _Traits>&
__CLRCALL_OR_CDECL endl(basic_ostream<_Elem, _Traits>& _Ostr)
{   // insert newline and flush stream
    _Ostr.put(_Ostr.widen('\n'));
    _Ostr.flush();
    return (_Ostr);
}

从定义中看出,endl是一个函数模板,它实例化之后变成一个模板函数,其作用如这个函数模板的注释所示,插入换行符并刷新输出流。其中刷新输出流指的是将缓冲区的数据全部传递到输出设备并将输出缓冲区清空。

2.cout<< endl的介绍

endl是一个函数模板,再被使用时会实例化为模板函数。但是函数调用应该使用一对圆括号,也就是写成endl()的形式,而在语句cout<<”Hello world”<<endl;中并没有这样,原因何在?

在头文件iostream中,有这样一条申明语句:extern ostream& cout;这说明cout是一个ostream类对象。而<<原本是用于移位运算的操作符,在这里用于输出,说明它是一个经过重载的操作符函数。如果把endl当做一个模板函数,那么cout<<endl可以解释成cout.operator<<(endl);由于一个函数名代表一个函数的入口地址,所以在cout的所属类ostream中应该有一个operator<<()函数的重载形式接受一个函数指针做参数。

查找ostream类的定义,发现其实是另一个类模板实例化之后生成的模板类,即:

typedef basic_ostream<char, char_traits<char> > ostream;

所以,实际上应该在类模板basic_ostream中查找operator<<()的重载版本。在头文件ostream中查找basic_ostream的定义,发现其中operator<<作为成员函数被重载了17次,其中的一种:

typedef basic_ostream<_Elem, _Traits> _Myt;

_Myt& __CLR_OR_THIS_CALL operator<<(_Myt& (__cdecl *_Pfn)(_Myt&))
{   // call basic_ostream manipulator
    _DEBUG_POINTER(_Pfn);
    return ((*_Pfn)(*this));
}

在ostream类中,operator<<作为成员函数重载方式如下:

ostream& ostream::operator<<(ostream& (*op)(ostream&))  
{  
    return (*op)(*this);  
}  

这个重载正好与endl函数的申明相匹配,所以<<后面是可以跟着endl 。也就是说,cout对象的<<操作符接收到endl函数的地址后会在重载的操作符函数内部调用endl函数,而endl函数会结束当前行并刷新输出缓冲区。

为了证明endl是一个 函数模板,或者说endl是一个经过隐式实例化之后的模板函数,我们把程序改造如下:

#include <iostream>
using namespace std;

int main(){
cout<<"Hello world"<<&endl;
}

这个程序可以正常运行,并且结果完全同上一个程序。原因是对于一个函数而言,函数名本身就代表函数的入口地址,而函数名前加&也代表函数的入口地址。

3.endl其实是IO操纵符

实际上,endl被称为IO操纵符,也有翻译成IO算子。IO操作符的本质是自由函数,他们并不封装在某个类的内部,使用时不采用显示的函数调用的形式。在< iostream>头文件中定义的操纵符有:

    endl:输出时插入换行符并刷新流
    endls:输出时在字符 插入NULL作为尾符
    flush:刷新缓冲区,把流从缓冲区输出到目标设备,并清空缓冲区
    ws:输入时略去空白字符
    dec:令IO数据按十进制格式
    hex:令IO数据按十六进制格式
    oct:令IO数据按八进制格式

在< iomanip>头文件中定义的操作符有:

    setbase(int)
    resetiosflags(long)
    setiosflags(long)
    setfill(char)
    setprecision(int)
    setw(int)

这些格式控制符大致可以替代ios的格式函数成员的功能,且使用比较方便。例如,为了把整数345按16进制输出,可以采用两种方式:

    int i=345;
    cout.setf(ios::hex,ios::basefield);
    cout<<i<<endl;

或者:

cout<<hex<<i<<endl;

可以看出采用格式操纵符比较方便,二者的区别主要在于:格式成员函数是标准输出对象cout的成员函数,因此在使用时必须和cout同时出现,而操纵符是自由函数,可以独立出现,使用格式成员函数要显示采用函数调用的形式,不能用IO运算符”<<”和”>>”形成链式操作。

4.自定义格式操纵符

除了利用系统预定义的操纵符来进行IO格式的控制外,用户还可以自定义操纵符来合并程序中频繁使用的IO读写操作。定义形式如下:

输出流自定义操纵符:

ostream &操纵符名(ostream &s)
{
  自定义代码
  return s;
}

输入流自定义操纵符:

istream &操纵符名(istream &s{
  自定义代码
  return s;
}

示例代码如下:

#include <iostream>
#include <iomanip>
using namespace std;

 std::ostream& OutputNo(std::ostream& s)//编号格式如:0000001
 {
     s<<std::setw(7)<<std::setfill('0')<<std::setiosflags(std::ios::right);
 return s;
 }

 std::istream& InputHex (std::istream& s)//要求输入的数为十六进制数
 {
     s>>std::hex;
    return s;
 }

int main()
{
    std::cout<<OutputNo<<8<<std::endl;
    int a;
    std::cout<<"请输入十六进制的数:";
    std::cin>> InputHex >>a;
    std::cout<<"转化为十进制数:"<<a<<std::endl;
    return 0;
}
程序运行结果: 
0000008
请输入十六进制的数:ff
转化为十进制数:255

程序中OutputNo和InputHex都是用户自定义的格式操纵符,操作符的函数原型必须满足cout对象的成员函数operator<<()的重载形式:

ostream& ostream::operator<<(ostream& (*op)(ostream&));

所以只要编写一个返回值为std::ostream&,接收一个类型为std::ostream&参数的函数,就可以把函数的入口地址传递给cout.operator<<(),完成格式操纵符的功能。


参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[P326-P329] [2]C++之IO格式控制

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏积累沉淀

JDK动态代理的底层实现原理

JDK动态代理的底层实现原理      动态代理是许多框架底层实现的基础,比如Spirng的AOP等,其实弄清楚了动态代理的实现原理,它就没那么神奇了,下面就来...

60970
来自专栏LuckQI

一文了解Mongodb使用的语法

在使用数据库之前,我们需要先了解下其基本的数据结构类型。防止我们出现类型不匹配的问题。 支持的数据类型补充的是本人在开发中经常使用的。还有更多的数据类型可以参考...

17660
来自专栏Android机动车

java内部存储简述

在实际项目中,会涉及到很多大量数据的访问,存储或者是计算,这个时候如果可以用合适的容器来存储这些数据,就会达到事半功倍的效果,也就是说,当你的程序遇到瓶颈的时候...

15230
来自专栏强仔仔

AngularJS系列之表达式

这节介绍一下AngularJS中表示式的用法。使用表达式可以把数据绑定到HTML中去,使用起来非常方便。不过在使用之前得先引用AngularJS文件,这个文件可...

19370
来自专栏mukekeheart的iOS之旅

《JavaScript高级程序设计》学习笔记(3)——变量、作用域和内存问题

欢迎关注本人的微信公众号“前端小填填”,专注前端技术的基础和项目开发的学习。 本节内容对应《JavaScript高级程序设计》的第四章内容。 1、函数:通过函数...

29660
来自专栏WD学习记录

牛客网 python (1)

1. python my.py v1 v2 命令运行脚本,通过 from sys import argv如何获得v2的参数值? 

21510
来自专栏林德熙的博客

dotnet 设计规范 · 结构体定义

易变的属性指的是在调用属性返回值的时候返回的是新的实例,易变的属性会有很多的问题。

8410
来自专栏Python小屋

Python编写只允许实例化一个对象的类

>>> class T: __total = 0 def __init__(self, value): if T.__total != 0: r...

34880
来自专栏大内老A

ASP.NET MVC基于标注特性的Model验证:DataAnnotationsModelValidatorProvider

DataAnnotationsModelValidator最终是通过它对应的ModelValidatorProvider,即DataAnnotationsMod...

22070
来自专栏黑泽君的专栏

java基础学习_反射、装饰模式、JDK新特性_day27总结

10520

扫码关注云+社区

领取腾讯云代金券