首页 > 编程知识 正文

c底层开发是什么,c语言的底层是什么实现的

时间:2023-05-03 16:59:37 阅读:119742 作者:2964

说明一下,我在用g 7.1.0编译器。 标准库的源代码也是这个版本。

本文介绍了实现c标准IO的基础的结构,以及cin和cout的具体实现。

在阅读本文之前,我们建议您阅读以前的文章,至少了解标准IO各级之间的关系。

c标准输入输出流关系整理

1 .标准IO的基础结构通过通读c标准IO的源代码,总结了其基础实现结构。 图:

分为外部设备、缓冲器、程序三个层次结构,说明如下。

外部设备是指物理或逻辑设备,如键盘、屏幕和文件。 缓冲区是在数据未同步到外部设备之前存储数据的存储器。 程序是我们代码生成的过程。 首先,我们来看看ostream:put函数输出字符的示例。 下面介绍put函数的具体实现。

1.1首先寻找底层实现(丁丁)底部小标记: tcc是template cc,cc是c实现文件的后缀,t表示模板的实现,所以tcc与不是模板的其他实现文件区别开来

在ostream.tcc中找到put函数的实现代码。

templatetypename _CharT,typename _traitsbasic_ostream_chart,_ traits basic _ ostream _ chart,_ traits 33365365306; _ _ try { constint _ type _ _ put=this-rd buf (-s putc ) __c ); if (traits _ type :3360 eq _ int _ type (_ put,traits_type:eof ) ) _ err|=IOs _ base 3365292; __throw_exception_again; }_catch(…) this-_ m _ setstate (IOs _ base 33603360 badbit ); }if(_err ) this-setstate(__err ); }返回* this; }例如,在输出字符时,put函数是调用缓冲基类basic_streambuf的sputc成员函数,其实现如下:

int_typesputc(char_type__c ) {int_type __ret; //pptr返回指向缓冲器中下一位置的指针,而epptr返回指向缓冲器结束位置的指针if(_builtin_expect(this-pptr ) this-epptr ),true ) * thiltin _ expect _ _ ret=traits _ type :3360 to _ int _ type (_ _ c ); }else //overflow为缓冲区溢出处理_ ret=this-overflow (traits _ type :3360 to _ int _ type ) _ c ); return __ret; }这样,sputc函数的作用很明显。 有两个分支。

一种是如果缓冲器的当前位置还未满,则直接将字符写入缓冲器;

第二,如果当前缓冲区已满,则进行缓冲区溢出处理。

很明显,在这两种情况下,每个输出类的实现方式都不同。 基本的ostream暂且不论,让我们来看看ostringstream和ofstream这两个类在实现时的异同。

第一,ostringstream和ofstream在实现上是相同的,它们都只是将字符写入缓冲区并将位置向后移动一位,没有什么特殊之处。

但是,第二点是,ostringstream是被调用的stringbuf的overflow成员函数,如果原始缓冲器不存在,则重新申请更大的临时缓冲器,复制源缓冲器中的所有数据

另一方面,ofstream是一个调用filebuf的overflow成员函数,用于检测当前是否已写入缓冲区末尾。 很明显,在第二点,缓冲区已满,所以一定会写入末尾。 此时,调用系统的write函数将当前缓冲区的所有内容刷新为文件,并重新初始化缓冲区指针的位置等。 请注意filebuf

小贴士:很显然,对于上面第二点,调用overflow函数,是使用了c++中多态,对于streambuf::overflow,它是一个虚函数,真正的实现是在stringbuf和filebuf里面。

到这里,put函数的具体实现我们就探究完了,大致上也探了探标准库底层实现的底子,但我们还是对于三层结构的实现不是那么清晰,下面就来具体的说一说。

1.2 详解标准IO底层结构 1.2.1 stringbuf的底层结构

对于istringstream、ostringstream、stringstream这三个类而言,他们都是基于stringbuf来实现缓冲区的,所以说白了他们的底层实现直接看stringbuf的底层实现就ok了,那么stringbuf是基于什么来实现缓冲区的呢。

先来看一张图,如下:

注意,这里箭头指示代表使用关系,并不是继承关系,所以我这里用了比较透明的线,后续同理。

那么现在就很明显了,stringbuf使用的是标准库中的string来作为缓冲区,如果说读取数据的话,很明显string的大小是不会变化的,但如果是写入string的话,在构造的时候也会调用string的构造,它一开始是一个空字符串,当开始写入第一个字符的时候,默认会给string对象申请一块大小为512个字节的动态内存,后续写入,就直接写入动态内存,当512个字节写完后,就会在当前内存大小基础上乘以2,然后申请一块新的内存,再把之前的数据全部复制到新的内存中来,再在新内存的后面写入要保存的字符。

那对于stringbuf的三层结构而言,它的缓冲区就是申请的内存,外部设备就是string,在逻辑上而言,他们是两层不同的皮,但实际上就实现来讲,我们对string申请的内存进行读写,其实就是对string进行读写,从这个角度而言,stringbuf可以说是三层结构,也可以说是两层结构,就看我们个人怎么理解了,这里不多做讨论。

1.2.2 filebuf的底层结构

同样的,对于fstream相关类而言,它的底层实现是基于filebuf的,filebuf又比stringbuf稍显复杂一些,先来看图:

filebuf在调用open函数的时候会new一块char类型的动态内存,大小为BUFSIZ,BUFSIZ是系统文件里面定义的一个专门用于缓冲区的默认size,filebuf写数据的时候,是先写到这一块动态内存中去,当写满以后,会把FILE*转换为文件描述符,然后利用write函数直接写到文件中去,再对缓冲区当前写位置进行初始化,读数据则会先把数据读到缓冲区,直到当前缓冲区全部读完,才会重新从文件再次读取,对于filebuf而言,它的缓冲区大小是固定的,不会进行扩充。

所以这里对于filebuf,缓冲区就是申请的这一块动态内存,外部设备就是文件了,filebuf不论是从逻辑上还是实现上看,它都是标准的三层结构

1.2.3 iostream的底层实现

对于istream,ostream,iostream而言,他们的缓冲区使用的是streambuf,但streambuf的构造函数是保护类型的,所以它是没有办法直接生成一个对象的,也是可以理解的,因为streambuf既没有提供缓冲区,也没有提供一个外部设备,所以它本来也是不能直接使用的,它只是作为一个基类供stringbuf和filebuf调用。

如果想使用istream,ostream,iostream,那么就需要给他们传入一个可用的缓冲区对象,例如filebuf对象,这样才是可用的,但这样还不如直接使用fstream,所以对于这三个基本模板类而言,既然不可直接使用,那就不存在两层结构还是三层结构了。

2. 标准IO全局变量cin、cout的实现

上一小节说了,iostream类是不可直接使用的,但是我们又知道cin是istream类型的,cout是ostream类型,而且实际上标准IO中还定义了另外两个ostream类型的cerr和clog,那么他们为什么又可以直接使用呢。

在iostream头文件中,定义了这样一个全局静态变量:

static ios_base::Init __ioinit;

ios_base::Init是一个类类型,定义在ios_base.h头文件中,它的构造函数实现如下:

ios_base::Init::Init() { if (__gnu_cxx::__exchange_and_add_dispatch(&_S_refcount, 1) == 0) {// Standard streams default to synced with "C" operations._S_synced_with_stdio = true;new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);// The standard streams are constructed once only and never// destroyed.new (&cout) ostream(&buf_cout_sync);new (&cin) istream(&buf_cin_sync);new (&cerr) ostream(&buf_cerr_sync);new (&clog) ostream(&buf_cerr_sync);cin.tie(&cout);cerr.setf(ios_base::unitbuf);// _GLIBCXX_RESOLVE_LIB_DEFECTS// 455. cerr::tie() and wcerr::tie() are overspecified.cerr.tie(&cout);#ifdef _GLIBCXX_USE_WCHAR_Tnew (&buf_wcout_sync) stdio_sync_filebuf<wchar_t>(stdout);new (&buf_wcin_sync) stdio_sync_filebuf<wchar_t>(stdin);new (&buf_wcerr_sync) stdio_sync_filebuf<wchar_t>(stderr);new (&wcout) wostream(&buf_wcout_sync);new (&wcin) wistream(&buf_wcin_sync);new (&wcerr) wostream(&buf_wcerr_sync);new (&wclog) wostream(&buf_wcerr_sync);wcin.tie(&wcout);wcerr.setf(ios_base::unitbuf);wcerr.tie(&wcout);#endif// NB: Have to set refcount above one, so that standard// streams are not re-initialized with uses of ios_base::Init// besides <iostream> static object, ie just using <ios> with// ios_base::Init objects.__gnu_cxx::__atomic_add_dispatch(&_S_refcount, 1); } }

以cin为例,可以看到,实际上是在构造的时候传入了一个stdio_sync_filebuf类型的对象,那我们知道istream只接受streambuf类型的对象,所以可以猜测到stdio_sync_filebuf应该是继承于streambuf的,找到stdio_sync_filebuf.h头文件,看到stdio_sync_filebuf果然是继承于basic_streambuf的。

对于类stdio_sync_filebuf而言,它是不存在缓冲区的,只是它会根据传入的文件指针stdin、stdout、stderr来与外部设备键盘和屏幕扯上关系,所以对于cin而言,它是通过stdin直接从键盘进行读取,而cout则是通过stdout直接输出到屏幕。

所以从结构上而言,cin、cout、cerr、clog都是只有程序和外部设备两层结构,但还有一点疑惑,我们根据代码,实际上他们都是打开了文件,然后对文件进行了读写,那怎么会显示在外部设备上呢。

根据操作系统的不同,标准输入和输出也是实现不同的,这里我们以linux系统为例,来进行说明。

在linux中,有三个标准的输入和输出文件,分别是stdin,stdout,stderr,他们都在/dev目录下,由上一章可知,cout实际上打开了/dev/stdout这个文件,而/dev/stdout又是一个软链接,它链接的是/proc/self/fd/1这个文件,而/proc/self/fd/1又链接到了/dev/pts/0这个文件,/dev/pts/0这个文件实际上代表的是当前打开的终端,以当前终端为例,关系图如下:

这样看来,每个程序的输入输出,其实接收的都是当前终端的输入和输出,关于这一点,就写到这里,不再展开说明了。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。