点击链接加入群【C语言】:http://jq.qq.com/?_wv=1027&k=2H9sgjG


    

上一小节讨论的知识是个小重点,我们再在这里就不再做回顾了,如果你没有掌握好的话,你一定要自己再看看,而且要把知识点弄清楚。

这小节,我们来讨论一下另外一个重点,也称为一种机制,什么机制呢?叫缓冲,哈哈,这个概念很重要,恩,不管是在将来的windows编程中用的双缓冲技术,还是在游戏编程中用的多级缓冲技术,都是这种思想,我们先来看一下IO缓冲。

首先大家要知道的一件事是CPU的速度非常快,内存的速度次之,而硬盘的速度最慢,因为它是机械操作,大家可以百度一下关于硬盘的视频,看一下其内部是怎么构成的,你也就会理解为什么它的速度很慢了。

当我们要将数据从内存中保存到硬盘上的时候,操作系统并不会经常访问硬盘,比如我们向文件写入一个字节,那么系统就访问硬盘,也就是进行IO操作,这样会使我们的系统效率变慢,那么怎么办呢?我们可以设置一个缓冲区(其实就是一块内存啦),当我们写数据的时候,数据其实是写入到缓冲区里了,当缓冲区被写满了,并且又有新数据到来的时候,操作系统一次性的将数据写入到硬盘里,这样就可以缓解一下我们的速度瓶颈问题啦。其实这种思想到处可见哦。

那么我们来看看,关于文件的一些操作。

#include <stdio.h>

int main()

{

FILE * fp= NULL;

return 0;

}

在FILE上右击,选择转到宣言,看一下FILE到底是什么,如下

#define EOF   (-1)



#ifndef _FILE_DEFINED

struct _iobuf {

char *_ptr;

int  _cnt;

char *_base;

int  _flag;

int  _file;

int  _charbuf;

int  _bufsiz;

char *_tmpfname;

};

typedef struct _iobuf FILE;


首先我们看到一个EOF,是(-1) ,是一个宏定义,为什么加括号呢?

其实在进行宏定义的时候,我们最好用括号括起来,因为爱,所以爱。。。扯偏了,,因为,我举一个例子大家就会明白了

如果#define MUL(a,b)  a*b

那么 MUL(1,2) 被展开成 1*2

但是MUL(1+2,2+3) 会被展开成 1+2*2+3 ,很显然结果不正确啦,因为宏定义在被预处理的时候是进行替换哦,不要忘记哦。那么加上括号后呢?大家自己测试吧

好了,那么我们看到定义了一个结构体 _iobuf ,然后为它个结构体起了一个别名,叫FILE,哦,原来FILE是一个结构体啊,那么这样就简单了。

你听说过标准输入和标准输出吗?我们也来看看它们是什么吧。

#include <stdio.h>

int main()

{

stdin;

return 0;

}

然后在stdin上右击转到定义,看一下它是什么,关于以后再说类似于这种转到定义的时候,我不会再贴代码啦,大家自己应该会操作了吧。

#define stdin (&_iob[0])

#define stdout (&_iob[1])

#define stderr (&_iob[2])

原来是3个宏呀,是一个数组的3个元素的首地址,看到了吧,是地址哦。那么我们就把它们打印出来看看吧。

#include <stdio.h>

int main()

{

printf("stdin : %08X\n",stdin);

printf("stdout: %08X\n",stdout);

printf("stderr: %08X\n",stderr);

return 0;

}

结果如下

stdin : 00426F30

stdout: 00426F50

stderr: 00426F70

不明白%08X 是什么意思吗? 我来告诉你,%X,表示后面的数据以大写16进制格式进行输出,8表示输出数据占8个位置,用来对齐啦,0表示不满8个用0表示,好像是这样的,我也忘记啦,如果大家想知道的更详细,可以在msdn的索引中输入 printf ,然后在索引中printf 的下面还有几个项,如printf type fields ,printf format specification fields 等。大家可以双击相关信息进行查询。不过这些不是什么重点,我们知道如何查找就可以,大家千万不要因为不知道某些格式而伤心,更不要强制自己去记住更多格式。好啦,我们已经知道了 标准输入输出和标准错误是什么东西啦,那么 它们代表的是哪个数组的元素的首地址,我们得看看哦,是一个叫 _iob的数组,转到它的定义看看。

_CRTIMP extern FILE _iob[]; 我们先不管前面的那个东西是什么,就看 FILE 就应该知道它是一个FILE结构的数组啦,开心吧。哈哈,这里又引入了一个新词,叫extern,那么它是什么意思呢?

我们一个项目中可以包含多个源文件哦,A.CPP中定义的变量,要想在B.CPP中进行访问,直接访问是不行的,需要在B.CPP中进行声明,就要用这个东西啦。所以在这里看到它的意思是说 _iob[],这个数组并不是在本文件中,而是在其它文件中定义的。

好啦,我们已经知道了,那么我们来看看标准输入吧

调用getchar,如下

#include <stdio.h>

int main()

{

getchar();

return 0;

}

在return 0 和 getchar() 两行都下一个断点,然后按F5调试,程序会断在getchar那里,便是还没有调用getchar,再按F5,cmd窗口提示我们进行输入内容,我们在这里输入

abcdefghijklmn,然后回车,程序运行到return 0; 断下来,我们就在这个时候来看一下我们标准输入里面的值,在内存窗口中地址中输入我们之前输出的 stdin的值,

00426F30 A1 81 42 00

00426F34 0E 00 00 00

00426F38 A0 81 42 00

00426F3C 01 01 00 00

00426F40 00 00 00 00

00426F44 00 00 00 00

00426F48 00 10 00 00

00426F4C 00 00 00 00

如图所示,我们可以看到,stdin就是00426F30,上表所示就是 _iob[0] 这个标准输入流对象啦,我们对照之前看到的结构来看一下下面的一些字段的意义

char * _base 这个指向的是我们的输入缓冲区的首地址,也就是004281A0 是我们的输入缓冲区的首地址,那么第一个字段_ptr指向的是当前对缓冲区的访问位置,是通过这个指针来标识的,比如,getchar会访问一个字节,然后指针就偏移了一个字节,那么再将进行访问,就会从_ptr指向的位置进行访问,,0E指的是什么呢?我们输入了14个字符,大概你也就明白它是啥了,当前缓冲区中的可读取的数据的数量,flag是一些标志,不用太详细的讨论啦,_file是文件流的值,比如,stdin = 0 stdout=1 stderr=2,后面有一个bufsiz是指缓冲区的大小,其它 我们不用关心啦,毕竟微软的东西不是开源的,我们要想搞清楚每一个东西还是比较困难滴啦,在这里,我做的这些介绍有助于帮助你理解文件流及缓冲机制。

好啦,所以,如果这个时候我们再进行读缓冲区操作,会在_ptr这个位置开始读。如

#include <stdio.h>

int main()

{

printf("%X\n",stdin);  //加上这句话,主要是因为我们的标准输入和标准输出地址会变化

char c = getchar();

c = getchar();

c = getchar();

c = getchar();

c = getchar();

return 0;

}

这段代码我们在每一行都下断点,然后调试,然后输入 abcde,每一步都按F5,会发现,只有第一个getchar会被调用,而其它的都是直接从_ptr那个位置开始读,然后再将它向后偏移1个单位

00428600 61 62 63 64  //我们可以看到我们在输入abcde并回车的时候 ,缓冲区中其实还有两上0A 0A ,我也不知道为什么要用2个,

00428604 65 0A 0A 00  //但事实上就是两个,我们要学会接受现实,骚年,哈哈

好啦,如果你上一输入的是ABC,而不是abcde,那么再试试上面的代码,c的值会依次为A B C ,读完C 的时候,看一上标准输入前3个数据

00424A30 03 86 42 00  //当前偏移位置

00424A34 01 00 00 00  //当前还有一个字节数据可以读 ,其实它就是0A

00424A38 00 86 42 00  //基址

所以,下一次就会读到0A,

00424A30 04 86 42 00

00424A34 00 00 00 00  //当前没有数据可读了

00424A38 00 86 42 00

下次再调用getchar,会发生什么呢? 我们继续运行程序,会发现数据并没有改变,但是当我们输入数据之后,比如defg,那么指针的值就会再次改变

00424A30 01 86 42 00

00424A34 04 00 00 00  //d e f g

00424A38 00 86 42 00

而缓冲区中的数据也换成了

0428600 64 65 66 67 defg

0428604 0A 0A 00 00 ....


这个过程很适合大家多测试几次,可以加深大家对应用层面的数据处理的一些细节,既然我们已经知道了这些东西,那么我们感觉的是,这些操作完全可以由指针操作,而且应该不是很困难,主要也就是对缓冲区的操作及FILE结构中的数据调整


其实是这样的,而且系统也提供了一些宏来实现这些操作,我们这次在转到getchar的定义,会弹出提示框,问你是要转到宏还是要转到函数,我们选择转到宏

#define feof(_stream)   ((_stream)->_flag & _IOEOF)

#define ferror(_stream)  ((_stream)->_flag & _IOERR)

#define _fileno(_stream) ((_stream)->_file)

#define getc(_stream)   (--(_stream)->_cnt >= 0 \

? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))

#define putc(_c,_stream) (--(_stream)->_cnt >= 0 \

? 0xff & (*(_stream)->_ptr++ = (char)(_c)) : _flsbuf((_c),(_stream)))

#define getchar()     getc(stdin)

#define putchar(_c)    putc((_c),stdout)


会看到如上一段代码,都是宏定义,比如 getchar() = getc(stdin) ,而getc(stdin) =

(--(stdin)->_cnt >= 0 ? 0xff & *(stdin)->_ptr++ : _filbuf(stdin))

我们不用仔细研究它,就看一个大概,对_cnt进行操作,偏移_ptr,对缓冲区进行操作,大概就是这些东西啦,所以说,我们的操作就是在操作那个缓冲区和FILE结构体中的数据成员,是不是很好理解呢,我们并不是说大家要把这些东西研究的多深,关键是大家要知道这个过程,这对大家的帮助是非常大的哦。


其实现在大家再去看一些关于文件或IO的操作函数,我想,应该不是很困难的事情了。

我在这里给大家提供MSDN的两个入口,一个是在索引中输入 _close 然后打开其页面,在页面中找 Low-Level I/O Routines ,,打开,可以看一些低级别IO函数,再有一个就是输入 fclose,然后打开,会有提示让你选择是C库的还是VC的,我们选择VC的,在其页面中找 Stream I/O Routines ,打开,里面有很多函数可以看看。

给大家提供这两个入口主要是为了让大家有个基本认识,MSDN中很多函数都是一个集合,就像 Stream I/O Routines 和Low-Level I/O Routines,我们可以将函数放在一起来进行学习。

当IO出现错误的时候,会将_flag置一些标志,我们可以用 ferror来测试一个流是否出现错误,也可以用 clearerr一清除某个流上的错误,为什么会出现错误?比如你要向一个只读文件中写数据,这就会出错,我们还可以用ftell 来查询 _ptr 现在在什么位置,它返回的是一个偏移量,就是_ptr 与 _base的偏移量,可以用 fseek 和rewind来移动_ptr指针,fread和fwrite读取或向文件中写入数据,setvbuf函数设置缓冲区和缓冲区大小,fileno用一返回一个流对象的 _file字段值,fflush将缓冲区数据写入到硬盘文件中 ,fopen用来打开一个文件,并获取FILE结构及缓冲区,然后就可以像上面讲的进行各种操作啦,fclose 用来关闭文件。


最后再来说几个问题

fgetc和fgetchar的区别:Read a character from a stream (fgetc, fgetwc) or stdin (_fgetchar, _fgetwchar). 你应该可以读懂的,前者是要指定一个流,后者是从stdin读。其它函数依此类推。


再回到宏那里,我们的代码现在实际上是在使用宏进行的操作,也就是调用的 _filbuf(stdin)的,

5:    char c = getchar();

0040103A  mov     eax,[__iob+4 (00424a34)]

0040103F  sub     eax,1

00401042  mov     [__iob+4 (00424a34)],eax

00401047  cmp     dword ptr [__iob+4 (00424a34)],0

0040104E  jl     main+61h (00401071)

00401050  mov     ecx,dword ptr [__iob (00424a30)]

00401056  movsx    edx,byte ptr [ecx]

00401059  and     edx,0FFh

0040105F  mov     dword ptr [ebp-8],edx

00401062  mov     eax,[__iob (00424a30)]

00401067  add     eax,1

0040106A  mov     [__iob (00424a30)],eax

0040106F  jmp     main+71h (00401081)

00401071  push    offset __iob (00424a30)

00401076  call    _filbuf (00401240)    //这里调用的是 _filbuf


那么如果我们就想调用 getchar函数怎么办呢? 很好办,#undef,解除宏定义

4:    #undef getchar

5:    char c = getchar();

00401028  call    getchar (004010b0)

0040102D  mov     byte ptr [ebp-4],al


好理解吧,最后我们再来讨论一下指针的小用处

我们都知道函数返回值只能有一个,而C中又是不能返回数组的,怎么办呢?我们可以通过参数来带回值,举个例子

#include <stdio.h>

void sum(int a,int b,int * psum)

{

*psum = a+b;

}

int main()

{

int s = 0;

sum(1,2,&s);

return 0;

}

程序虽小,但是意义重大,我们以后再学WINDOWS程序设计的时候,会经常用到这种思想哦,为了让别人更容易理解我们写的代码,我想给它加点提示,看代码

#include <stdio.h>

#define IN

#define OUT

void sum(IN int a,IN int b,OUT int * psum)

{

*psum = a+b;

}

int main()

{

int s = 0;

sum(1,2,&s);

return 0;

}

好,能看明白吧,我们使用了两个空宏,那么在预处理的时候,这些宏就会被换成空,也就相当于没有,但是别人在看函数声明的时候,很容易就看的懂,前两个参数是输入参数,而最后一个是要带出值的参数。其实在微软的库函数中很多也给也这样的提示呢。这个大家以后遇到就不要再说看不懂啦,还有就是我们在看MSDN中的一些参数说明的时候,参数说明上会有一个 in 或 out 也是表示的这个意思哦。


好了,这一小节大概就介绍这么多吧,其实还有许多函数,我们并没有介绍到,不过没有关系,我们没有那么大的精力反、把所有的函数都弄清楚,我们要学会GOOGLE 百度,举一个例子,也是一个小任务吧,大家自己搜索一下 如何获取 应用程序模块的首地址,我们不是讲过嘛,一个程序不只是一个模块,可能会有好几个,还有一些系统的动态链接库(DLL)。

我来演示一下大概流程,百度 或 GOOGLE,获取应用程序模块  然后大差不差的 大家会找到一些相关的信息,在众多信息中会看到一个函数名 getmodulehandle的函数,我们可以看一下别人写的代码,但是更多的是我们应该查看MSDN中的说明文档

The GetModuleHandle function retrieves a module handle for the specified module if the file has been mapped into the address space of the calling process.

这样就明白了吧,在MSDN的下面会有说明这个函数的使用需要哪些头文件,如 windows.h,所以你就明白了。

#include <windows.h>

#include <stdio.h>

int main()

{

HMODULE h = GetModuleHandle(NULL);

printf("%X\n",h);

return 0;

}

大家自己看 为什么传 NULL 为参数 ,以及看 HMODULE 是什么东西,转到定义哦。

好了,就介绍到这里