运行库:Windows下MSVC CRT运行库封装fread()函数解析
在介绍运行库的过程中,强调过运行库是具体语言实现的程序和操作系统之间的抽象层。经验表明,任何系统级的软件工程,IO功能的封装历来是最具有挑战性的。以下以Windows下MSVC CRT运行库中封装的文件读取函数fread()的实现思路为demo,演示运行库的功能封装。
#define size_t unsigned intsize_t fread(void * buffer,size_t elementSize,size_t count,FILE *stream
)
//fread函数功能是从文件流stream中读取count个大小为elementSize个字节的数据,存储在buffer,返回值为实际读取的字节数Windows API的ReadFILE()
BOOL ReadFile(HANDLE hFile,LPVOID lpBuffer,DWORD nNumberofBytesToRead,LPDWORD lpNumberofBytesRead,LPOVERLAPPED lpOverlapped
);
//hFile为要读取的文件句柄,对应的是fread函数中stream参数
//lpBuffer是存储缓冲区的其实地址,对应fread函数中的buffer
//nNumberofBytesToRead代表要读取字节总数,等于fread函数中count * elementSize
//lpNumberOfBytesRead代表一个指向DWORD类型的指针,用来表示读取了多少个字节
//lpOverlapped没用/*
fread函数的调用顺序
fread -> fread_s -> _fread_nolock_s -> _read
MSVC的fread函数定义在crt/fread.c
*/
size_t __cdecl fread(void *buffer,size_t elementSize,size_t count,FILE *stream
)
{/* assumes there is enough space in the destination buffer */return fread_s(buffer, SIZE_MAX, elementSize, count, stream);
}/* define locking/unlocking version */
//fread_s中的s表示safe,相比于fread函数多了bufferSize形参,即用于指定参数buffer的大小
//在fread_s中通过指定bufferSize来防止越界
size_t __cdecl fread_s(void *buffer,size_t bufferSize,size_t elementSize,size_t count,FILE *stream
)
{size_t retval = 0;if (elementSize == 0 || count == 0){return 0;}// only checking for stream == NULL here for _lock_str()// the rest of the validation is in _fread_nolock_s()if (stream == NULL ){if (bufferSize != SIZE_MAX){memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);//作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法,函数原型void* memset(void* buffer, int c, size_t size)}_VALIDATE_RETURN((stream != NULL), EINVAL, 0);}_lock_str(stream); //使用_lock_str()对文件进行加锁__try{/* do the read; _fread_nolock_s will make sure we do not buffer overrun */retval = _fread_nolock_s(buffer, bufferSize, elementSize, count, stream);}__finally{_unlock_str(stream);//对文件所加的锁进行解锁}return retval;
}typedef struct _iobuf{char *_ptr;//指向缓冲的第一个未使用的字节int _cnt;//记录文件缓冲区的剩余未读字节数,一般为了提高效率,减少I/O次数,会//为文件对象配备提前的文件缓存,这些缓存都是提前存储了数据对象的char *_base;//指向一个字符数组,即这个文件对应的缓冲int _flag;//记录FILE结构所代表的打开文件的一些属性/*这一位的标志总共有3个标志#define _IOYOURBUF 0x0100 //代表是用户通过setbuf手动为该FILE绑定的buffer#define _IOMYBUF 0x0008 //代表这个文件使用内部的缓冲#define _IONBF 0x0004 //代表文件使用一个单字节的缓冲,在这种情况下,将不启用base指向的字符数组,而直接用下面_charbuf作为单字节*/int _file;int _charbuf;int _bufsiz;//记录着这个文件缓存的大小char *_tmpfname;
}FILE;/* define the normal version */
//_fread_nolock_s函数的内容实现,是fread读写的实际操作函数
size_t __cdecl _fread_nolock_s(void *buffer,size_t bufferSize,size_t elementSize,size_t num,FILE *stream
)
{char *data; /* point inside the destination buffer to where we need to copy the read chars 目标存储buffer下一个写入的地方指针*/size_t dataSize; /* space left in the destionation buffer (in bytes) 目标存储buffer当前还剩下的可用字节数*/size_t total; /* total bytes to read 想要读取的内容的字节总数*/size_t count; /* num bytes left to read 当前还剩下没读取的字节数*/unsigned streambufsize; /* size of stream buffer记录文件缓冲区的大小 */unsigned nbytes; /* how much to read now 单次要读取的字节数*/unsigned nread; /* how much we did read 记录单次读取到的字节数*/int c; /* a temp char *//* initialize local vars */data = buffer;dataSize = bufferSize;if (elementSize == 0 || num == 0){return 0;}/* validation */_VALIDATE_RETURN((buffer != NULL), EINVAL, 0);if (stream == NULL || num > (SIZE_MAX / elementSize)){if (bufferSize != SIZE_MAX){memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);}_VALIDATE_RETURN((stream != NULL), EINVAL, 0);_VALIDATE_RETURN(num <= (SIZE_MAX / elementSize), EINVAL, 0);}count = total = elementSize * num;if (anybuf(stream)) //这里的anybuf是定义在file2.h中的宏//#define anybuf(s) ( (s)->_flag & (_IOMYBUF|_IONBF|_IO_YOURBUF)//即判断FILE * stream中是否已经被分配了缓冲{/* already has buffer, use its size */streambufsize = stream->_bufsiz;}else{/* assume will get _INTERNAL_BUFSIZ buffer */streambufsize = _INTERNAL_BUFSIZ;//FILE *stream没有使用缓冲,则启动默认缓冲,这个//缓冲的大小为_INTERNAL_BUFSIZ,即4096字节}/* 参数初始化完成,进入循环读取的步骤 */while (count != 0) {/* if the buffer exists and has characters, copy them to userbuffer */if (anybuf(stream) && stream->_cnt != 0)//FILE* stream启用了缓冲,并且缓冲区剩余//未读字节数总数不为0,则可以先把缓存中数据读取出来,然后再去继续调用磁盘I/O{if(stream->_cnt < 0)//如果stream缓冲区可用字节数为负数,则通过断言退出{_ASSERTE(("Inconsistent Stream Count. Flush between consecutive read and write", stream->_cnt >= 0));stream->_flag |= _IOERR;return (total - count) / elementSize;}/* how much do we want? */nbytes = (count < (size_t)stream->_cnt) ? (unsigned)count : stream->_cnt;if (nbytes > dataSize){if (bufferSize != SIZE_MAX){memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);}_VALIDATE_RETURN(("buffer too small", 0), ERANGE, 0)}memcpy_s(data, dataSize, stream->_ptr, nbytes);//将文件stream绑定的磁盘缓冲内//批量读入本次fread指定的读缓冲中/* update stream and amt of data read */count -= nbytes; //更新剩余要读字节总数stream->_cnt -= nbytes; //磁盘stream->_ptr += nbytes;data += nbytes; //缓冲区可使用区域起始地址右移dataSize -= nbytes;//缓冲区可用区域字节总数减少}//如果文件对应的缓存已经读取完毕,仍没有满足读取长度要求,则进入调用_read()进行磁盘I/O//剩余要读取的字节总数大于文件配备的缓存大小else if (count >= streambufsize){//尽可能多地一次性读取时文件缓存尺寸整数倍的数据,直接通过_read()输入到最终目标buffernbytes = ( streambufsize ? (unsigned) (count -count % bufsize) : (unsigned)count );nread = _read(_fileno(stream), data, nbytes);//_read()用于直接从文件中读取数据,跳过文件缓存,直接向目标buffer拷贝整数倍缓存块//大小的字节数 if (nread == 0) {/* end of file -- out of here */stream->_flag |= _IOEOF; //到了文件的末尾return (total - count) / elementSize; //返回读取的元素个数}else if (nread == (unsigned)-1) {/* error -- out of here */stream->_flag |= _IOERR;return (total - count) / elementSize;}/* update count and data to reflect read */count -= nread; //要读取的剩余字节总数更新data += nread; //目标存储buffer中下一个空白字节的位置右移dataSize -= nread;//目标存储buffer可用剩余字节数更新}else //如果要读的字节总数不大于文件配备的缓存大小,则先填充stream的缓存,然后从缓存中读取要用的数据{/* less than streambufsize chars to read, so call _filbuf tofill buffer */if ((c = _filbuf(stream)) == EOF) {/*文件缓存填充_filbuf(stream)的核心过程还是调用_read():stream->_cnt = _read(_fileno(stream), stream->_base, stream->_bufsiz);#define _fileno(stream) ( stream -> _file) 即返回该FILE指针对应的文件在打 开文件列表中的的下标_file*//* error or eof, stream flags set by _filbuf */return (total - count) / elementSize;}/* _filbuf returned a char -- store it */if (dataSize == 0){if (bufferSize != SIZE_MAX){memset(buffer, _BUFFER_FILL_PATTERN, bufferSize);}_VALIDATE_RETURN(("buffer too small", 0), ERANGE, 0)}*data++ = (char) c;--count;--dataSize;/* update buffer size */streambufsize = stream->_bufsiz;}}/* 如果读取成功,正常执行,则返回读取元素个数 */return num;
}/*_read()函数位于crt/src/read.c
大致内容如下*/int __cdecl _read(int fh, void *buf, unsigned cnt )
{int bytes_read; //已经拷贝的字节数char *buffer; //迁移拷贝数据的目标区域指针int os_read; //bytes read on OS call调用Windows API ReadFile获取的字节数char *p, *q; //pointers into bufferchar peekchr; //peek-ahead character为了判断CR LF组合,预读取的字符存储位置ULONG filepos; //file position after seek文件指针,代表在整个文档里,下一个未被读取的字节位置ULONG dosretval; //o.s. return value Windows操作系统返回的错误值,_read()要将这些错误值转换成crt运行库的错误版本bytes_read = 0;buffer = buf;//处理文件配备的单字节缓冲情况
/*根据文件下标fh从打开文件列表中找出该文件对应的ioinfo信息,是句柄osfhnd、属性osfile、管道单字符缓冲pipech,该判断语句使得该部分功能只对设备FDEV和管道文件FPIPE有效。
#define _osfile(i) ( _pioinfo(i) -> osfile)
#define _pipech(fh) ( _pioinfo(i) -> pipech)
ioinfo结构提供了衣蛾单字节缓冲pipech用于处理一些特殊情况
pipech = LF即代表\n,代表此时单字符管道缓冲无效,这样设计的原因,在于文件对象是管道文件或是设备数据时的用途下,永远不会被赋值为LF
*/if ((_osfile(fh) & (FPIPE|FDEV)) && _pipech(fh) != LF) {*buffer++ = _pipech(fh);++bytes_read;--cnt;_pipech(fh) = LF; }//处理完单字符通道缓冲问题,正式进入调用Windows API ReadFile()的步骤if ( !ReadFile( (HANDLE)_osfhnd(fh), buffer, cnt, (LPWORD)&os_read, NULL) ){if ( (dosretval = GetLastError()) == ERROR_ACCESS_DENIED ){errno = EBADF;_doserrno = dosretval;return -1;}else if ( dosretval == ERROR_BROKEN_PIPE ){return 0;}else{_dosmaperr(dosretval);return -1;}}/*文件换行操作--兼容性调整Windows系统下的文本文件回车的存储方式是0x0D 0x0A (CR LF),即字符串表达形式\r\nLinux/Unix系统回车用\n表示,Mac OS系统回车用\r表示但是在C语言中,回车始终是用\n来表示的,故而需要在读取文件时,将ReadFile返回的buffer中内容遍历一下,将回车符都统一转换成\n*/if (_osfile(fh) & FTEXT) //检查文件是否是以文本模式打开的,如果不是,就什么都不需要处理{ //当本次读文件的第一个字符是一个LF,意味着一个\r\n可能正好被两次读给分割了//这时需要在_osfile(fh)的osfile字段中添加FCRLF,用于表示这种可能的情况if ( (os_read != 0) && (*(char *)buf == LF) )_osfile(fh) |= FCRLF; //FCRLF(0x04)在文本模式中,行缓冲已遇到回车符else_osfile(fh) &= ~FCRLF;/*处理p当前指向的字符,p和q后移1.*p是CRTL-Z:表明文本已经结束,退出循环2.*p是CR(\r)之外的字符:把p指向的字符赋值到q指向的位置,p和q各自后移一个字节(*q++ = *p++).3.*p是CR(\r),且*(p+1)不是LF(\n),则依旧同24.*p是CR(\r)且*(p+1)是LF(\):则p后移2个字节,将q指向的位置写成LR(\n),q后移一个字节(p += 2; *q++ = '\n';)*/p = q = buf; while (p < (char *)buf + bytes_read){if (*p == CTRLZ) {if ( !(_osfile(fh) & FDEV) ) //非设备文件,检查到CTRLZ,在osfile字段表明一下,文件已经到达末尾;若是设备文件,则直接退出_osfile(fh) |= FEOFLAG;//FEOFLAG(0x02)已到达文件末尾break;}else if (*p != CR) //没有遇到CR,直接复制*q++ = *p++;else{//遇到CR,检查下一个字符是否是LFif (p < (char *)buf + bytes_read - 1){//CR不处于缓冲区域的末尾,即缓冲区后面至少还有一个字符if (*(p+1) == LF){p += 2; *q++ = LF;}else*q++ = *p++;}/*CR已经处于缓冲区的末尾了,如果想查看有没有CR LF组合的可能性
需要再从文件对象中读取一个字符,这时就要分为两种情况了,终于涉及了ioinfo结构中那个神秘的字段pipech
普通的磁盘文件一种(存在那里可以随时调用,反复回看,只要回调文件指针1个字节就可以了,所以额外读取的这个字符没必要专门利用pipech存起来),另一种是设备文件和管道文件(这类文件是不能回退的,所以一旦你要额外读取一个字符,这个字符显然不能丢了,所以这个字符会被存放在pipech字段)*/else {++p;dosretval = 0;if ( !ReadFile( (HANDLE)_osfhnd(fh), &peekchr, 1, (LPDWORD)&os_read, NULL ) )dosretval = GetLastError();if (dosretval != 0 || os_read == 0){ //这次额外的读取出现错误或者本次读取并没有读到内容,则默认为CR LF组合是不会出现了,保留CR*q++ = CR;}else {if (_osfile(fh) & (FDEV|FPIPE)){//管道或设备文件if (peekchr == LF)*q++ = LF;else {//如果预读的字符不是LF,则使用pipech存储字符*q++ = CR;_pipech(fh) = peekchr;}}else {//普通文件if (q == buf && peekchr == LF) {*q++ = LF;}else {//如果预读的字符不是LF,则使用seek回退文件指针filepos = _lseek_lk(fh, -1, FILE_CURRENT);if (peekchr != LF)*q++ = CR;}}}}}}bytes_read = (int)(q- (char *)buf);}
}
运行库:Windows下MSVC CRT运行库封装fread()函数解析相关推荐
- windows 调用linux .a lib,动态链接库及静态链接库(windows下的.dll .lib和linux下的.so .a)...
动态链接库及静态链接库(windows下的.dll .lib和linux下的.so .a) 库有动态与静态两种,动态通常用.so为后缀,静态用.a为后缀.例如:libhello.so libhello ...
- 如果Windows下Quick软件运行时显示无权限打开exe
如果Windows下Quick软件运行时显示无权限打开exe, 问题在于你的windows进程里已经存在此软件的exe进程了,需要将其杀死,才可以再次运行此软件
- Win 管理器 运行命令 大全(Windows下的DOS运行命令)
DOS 运行命令管理器字符大全 Win 管理器 运行命令 大全(Windows下的DOS运行命令) Nslookup-------IP 地址侦测器 explorer-------打开资源管理器 de ...
- Windows下使用DCMTK开源库对DICOM协议的医学图像进行解析与显示
DICOM(Digital Imaging and Communications in Medicine),是用于医学影像处理.储存.打印.传输的一组通用标准协定.目前,被广泛应用于放射医疗,心血管成 ...
- GTK在Windows下开发和运行的一些设置问题
下面讲的Windows下开发,是以Visual Studio 2012为例,其他版本的Visual Studio应该与之类似. 下面的方法对于gtk2和gtk3都一样.现在好像gtk不能下载all i ...
- Windows下解决依赖动态库问题:bat脚本实现自动复制dll文件
1. 问题 Windows下,exe文件在设计实现时可能依赖某些动态库(*.dll文件),这些在调试台调试或在本机运行因为指定了包含库文件或者指定了环境变量,使得运行时可以找到并调用这些文件.但是环境 ...
- windows下编译以及运行cryptominisat 求解器(sat求解器)
cryptominisat是由msoos所开发的一款sat求解器,sat的具体问题另外一篇博客里有详细介绍点击打开链接,本篇文章只介绍如何在windows下运行cryptominisat,将自己遇到的 ...
- windows下的eclipse运行hdfs程序时报错 (null) entry in command string: null chmod 0644【笔记自用】
运行下面代码时,报了 (null) entry in command string: null chmod 0644 解决方案 下载hadoop.dll文件 并拷贝到C:\windows\system ...
- Windows下 jupyter notebook 运行multiprocessing 报错的问题与解决方法
文章目录 测试用的代码 错误 解决方法 测试用的代码 下面每一个对应一个jupyter notebook的单元格 import time from multiprocessing import Pro ...
最新文章
- python使用箱图法和业务规则进行异常数据处理并检查预测使用的数据特征是否有字段缺失的情况并补齐
- hibernate延迟加载(get和load的区别)
- Linux中断处理与定时器
- 深圳php和java,深圳java技术培训学习(Java和PHP区别)
- Oracle注册表修改 乱码编码
- Android进程间通信(复习笔记)
- 计算机项目教学法探讨,项目教学法在计算机教学中应用计算机教学论文计算机论文...
- MTK 驱动开发(17)---LCD MIPI
- oracle统计信息
- threejs 热力图做成材质_冷轧钢与热轧钢有什么区别?
- 数字图像处理实践(一)
- 对Retinex算法的一些理解
- 软件项目管理-会议记录模板
- MPU和MCU的区别
- numeric比较大小 数据库_SQL数据库中Numeric(10,2)是什么意思?
- Adapterdemo
- 麻雀要革命 第四章 旋转!追逐游戏的命运齿轮 第一节
- 大数乘法——大数问题
- linux的音频处理软ubuntu,Ubuntu18.04下的音频录制和编辑软件Ardour及QjackCtl(jackd gui)...
- 二叉树、二叉搜索树,平衡二叉树(旋转)红黑树(红黑规则)
热门文章
- 汽车CAN总线硬件电路原理
- 生么用C语言函数做万年历菜单,用C语言做万年历
- mysql获取部门的上级部门_查询所有上级部门的SQL
- root lg android tv,LG V10(双4G)一键ROOT教程,亲测可用
- 一体化计算机接口,机电一体化计算机接口设计要求.ppt
- 1计算机安全保密绪论_笔记
- const int *p 和 int *const p
- 全球及中国反射全息光栅行业发展态势及前景动态预测报告*2022-2027
- 服务器显示器接鼠标和键盘,接口、显示器及鼠标键盘设计
- android简单时间选择器TiemPicker