C 标准I/O库粗略实现
本文同时发表在 https://github.com/zhangyachen/zhangyachen.github.io/issues/123
写一下fopen/getc/putc等C库的粗略实现,参考了K&R,但是有几点根据自己理解的小改动,下面再具体说一下_
写这篇文章主要是帮助自己理解下标准I/O库大体是怎么工作的。
fopen与open之间的关系
操作系统提供的接口即为系统调用。而C语言为了让用户更加方便的编程,自己封装了一些函数,组成了C库。而且不同的操作系统对同一个功能提供的系统调用可能不同,在不同的操作系统上C库对用户屏蔽了这些不同,所谓一次编译处处运行。这里open为系统调用,fopen为C库提供的调用。
C库对的读写操作封装了一个缓冲区。试想假如用户频繁的对文件读写少量字符,会频繁的进行系统调用(read函数),而系统调用比较耗时。C库自己封装了一个缓冲区,每次读取特定数据量到缓冲区,读取时优先从缓冲区读取,当缓冲区内容被读光后才进行系统调用将缓冲区再次填满。
FILE结构体
上面我们看到一个结构体,里面有5个参数,分别记录了:缓冲区剩余的字符数cnt、下一个字符的位置ptr、缓冲区的位置base、文件访问模式flag、文件描述符fd。
其中文件描述符就是系统调用open返回的文件描述符fd,是int类型。ptr与base上面图中已经展示了。cnt是缓冲区剩余字符数,当cnt为0时,会系统调用read来填满缓冲区。flag为文件访问模式,记录了文件打开方式、是否到达文件结尾等。
结构体的具体定义如下,对应调用fopen返回的文件指针FILE *fp = fopen(xxx,r)
:
typedef struct _iobuf{
int cnt; //缓冲区剩余字节数
char *base; //缓冲区地址
char *ptr; //缓冲区下一个字符地址
int fd; //文件描述符
int flag; //访问模式
} FILE; //别名,与标准库一致
结构体中有flag字段,flag字段可以是以下几种的并集:
enum _flags {
_READ = 1,
_WRITE = 2,
_UNBUF = 4, //不进行缓冲
_EOF = 8,
_ERR = 16
};
我们注意到其中有一个字段,标识不进行缓冲,说明此种情况下每一次读取和输出都调用系统函数。一个例子就是标准错误流stderr : 当stderr连接的是终端设备时,写入一个字符就立即在终端设备显示。
而stdin和stdout都是带缓冲的,明确的说是行缓冲。本文不考虑行缓冲,默认都是全缓冲,即缓冲区满了才刷新缓冲区。(详细可以参考《UNIX环境高级编程》标准I/O库章节)。
现在我们可以初始化stdin、stdout与stderr:
FILE _iob[OPEN_MAX] = {
{0,NULL,NULL,_READ,0},
{0,NULL,NULL,_WRITE,1},
{0,NULL,NULL,_WRITE|_UNBUF,2}
};
_ferror/_feof/_fileno
//判断文件流中是否有错误发生
int _ferror(FILE *f){
return f-> flag & _ERR;
}
//判断文件流是否到达文件尾
int _feof(FILE *f){
return f-> flag & _EOF;
}
//返回文件句柄,即open函数的返回值
int _fileno(FILE *f){
return f->fd;
}
_fopen
FILE *_fopen(char *file,char *mode){
int fd;
FILE *fp;
if(*mode != 'r' && *mode != 'w' && *mode != 'a') {
return NULL;
}
for(fp = _iob; fp < _iob + OPEN_MAX; fp++) { //寻找一个空闲位置
if (fp->flag == 0){
break;
}
}
if(fp >= _iob + OPEN_MAX){
return NULL;
}
if(*mode == 'w'){
fd = creat(file,PERMS);
}else if(*mode == 'r'){
fd = open(file,O_RDONLY,0);
}else{ //a模式
if((fd = open(file,O_WRONLY,0)) == -1){
fd = creat(file,PERMS);
}
lseek(fd,0L,2); //文件指针指向末尾
}
if(fd == -1){
return NULL;
}
fp->fd = fd;
fp->cnt = 0; //fopen不分配缓存空间
fp->base = NULL;
fp->ptr = NULL;
fp->flag = *mode == 'r' ? _READ : _WRITE;
return fp;
}
fopen的处理过程:
- 判断打开模式的合法性。
- 在_iob中寻找一个空闲位置,找不到的话说明程序打开的文件数已经到达的最大值,不能再打开新的文件。
- 如果是w模式,创建一个新文件。如果是r模式,以只读方式打开文件。如果是a模式,首先打开文件,如果打开失败则创建文件,否则通过系统调用lseek将文件指针置到末尾。
- 对FILE结构体进行初始化,注意fopen不会分配缓冲区。
_getc
getc的作用是从文件中返回下一个字符,参数是文件指针,即FILE:
int _getc(FILE *f){
return --f->cnt >= 0 ? *f->ptr++ : _fillbuf(f);
}
对照上面的图示:当缓冲区中还有剩余字符待读取时,读取该字符并返回,并将缓冲区指针向后移动一个char单位,否则就调用_fillbuf函数填满缓冲区,_fillbuf的返回值就是待读取的字符。
这里有一个问题:当读取到最后一个字符时,cnt为0,但是ptr已经越界了,如下图:
这种情况虽然是非法的,但是C语言中保证:数组末尾之后的第一个元素(即&arr[n],或者arr + n)的指针算术运算可以正确执行。下面的例子也是上述的一种应用场景:
int a[10] = {1,2,3,4,5,6,7,8,9,10};
for(int *x = a;x < a + 10; x++){
printf("%d\n",*x);
}
当for循环到最后一步时,x也指向了a[10],虽然是非法的,但是C语言保证可以正确执行,只要不出现如下情况就ok:
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int *x;
for(x = a;x < a + 10; x++){
printf("%d\n",*x);
}
*x = 11; //越界进行值访问
_fillbuf
我们看下_getc中的_fillbuf的实现,_fillbuf是当缓冲区没有可以读取的字符时,通过系统调用read读取一定字节的数据填满缓冲区,供之后使用:
int _fillbuf(FILE *f){
int bufsize;
if((f->flag & (_READ | _EOF | _ERR)) != _READ){ //判断文件是否可读
return EOF;
}
bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base == NULL){ //没有分配过缓冲区
if((f->base = (char *)malloc(bufsize)) == NULL){
return EOF;
}
}
f->ptr = f->base;
int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read
if(n == 0){ //到达文件结尾
f->base = NULL;
f->cnt = 0;
f-> flag |= _EOF;
return EOF;
}else if(n == -1){ //出错
f->cnt= 0;
f->flag |= _ERR;
return EOF;
}else{
f->cnt = --n;
return *f->ptr++;
}
}
_fillbuf的处理过程:
- 判断文件是否可读
- 判断调用read函数时应该读取的字节数,当文件设置了无缓冲时,读取1个字节,否则读取BUFSIZ个字节,BUFSIZ在<stdlib.h>中定义,定义了在该操作系统条件下缓冲区的最佳大小。
- 判断是否分配过缓冲区(fopen不会分配缓冲区,会再第一次调用getc时分配)。
- 调用系统函数read。
- 判断read返回值,分为到达文件结尾、出错和正常读取三种情况。
- 正常情况下返回缓冲区第一个字符给getc函数,并将cnt减1。
这里注意,到达文件结尾和出错都是返回EOF,区别是前者会将flag的_EOF位置1,后者会将flag的_ERR位置1,上游可以通过feof
和ferror
函数进行判断(这两个函数在上面已经实现过了)。
The character read is returned as an int value.
If the End-of-File is reached or a reading error happens, the function returns EOF and the corresponding error or eof indicator is set. You can use either ferror or feof to determine whether an error happened or the End-Of-File was reached.
_putc
int _putc(int x,FILE *f){
return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}
与_getc的实现相似,将写入的字符放到ptr指向的位置,并将ptr向后移动一位。当缓冲区满时,调用_flushbuf将缓冲区内容刷新到文件中。
_flushbuf
int _flushbuf(int x,FILE *f){
if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){ //判断文件是否可写
return EOF;
}
int n;
int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base != NULL){
n = write(f->fd,f->base,f->ptr - f->base); //判断需要写入多少字节
if(n != f->ptr - f->base){
f->flag |= _ERR;
return EOF;
}
}else{
if((f->base = (char *)malloc(bufsize)) == NULL){
f->flag |= _ERR;
return EOF;
}
}
if(x != EOF){
f->cnt = bufsize - 1;
f->ptr = f->base;
*f->ptr++ = x;
}else{ //当写入EOF时,代表强制刷新缓冲区内容到文件中
f->cnt = bufsize;
f->ptr = f->base;
}
return x;
}
_flushbuf的处理过程:
- 判断文件是否可写。
- 当已分配过缓冲区时,将缓冲区的内容通过系统调用write写入文件中。
- 当没有分配过缓冲区时,分配缓冲区。
- 判断当写入的字符为EOF时,说明调用此函数的目的为强制刷新缓冲区,不写入字符。将cnt赋值为BUFSIZ,ptr赋值为缓冲区首地址base。
- 当写入字符不为EOF时,说明缓冲区已满,需要将缓冲区刷新到文件中。cnt为BUFSIZE - 1,将写入的字符x放到到缓冲区的第一格,然后将ptr向后移动一个char单位。
注意,调用write函数时,写入的字节数不能写死为1或者BUFSIZ:
n = write(f->fd,f->base,f->ptr - f->base); //判断需要写入多少字节
如上图,我们需要写入base至ptr之间的数据,而不是BUFSIZ,因为我们可能会强制刷新缓冲区而不是等到缓冲区满了才刷新缓冲区。
还有一点:当我们想要强制刷新缓冲区时,第一个参数x该传入什么呢?K&R传递的是字符0,但是我认为这样会污染缓冲区,所以我的实现是传入一个特殊字符EOF,根据EOF来做不同的处理:
if(x != EOF){
f->cnt = bufsize - 1;
f->ptr = f->base;
*f->ptr++ = x;
}else{ //当写入EOF时,代表强制刷新缓冲区内容到文件中
f->cnt = bufsize;
f->ptr = f->base;
}
当缓冲区满时,刷新缓冲区后缓冲区的表现:
当强制刷新缓冲区时,缓冲区的表现:
但是按照K&R的方式来强制刷新缓冲区时,缓冲区的表现:
这样会污染缓冲区,所以我的实现是传入EOF来强制刷新缓冲区。
_fflush
_fflush(FILE *f)的作用是把缓冲区内容写入文件。当参数为空时,会刷新所有文件:
int _fflush(FILE *f){
int res = 0;
if(f == NULL){
for(int i = 0; i < OPEN_MAX; i++){ //当参数为NULL时,刷新所有的文件流
if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){ //有一个出错即返回-1
res = EOF;
}
}
}else{
if(f->flag & _WRITE){
_flushbuf(EOF,f);
}else{
res = EOF;
}
}
if(f->flag & _ERR){ //出错
res = EOF;
}
return res;
}
_fflush的处理过程:
- 判断参数是否为空,如果为空的话,遍历_iob数组,将所有文件流都强制刷新。
- 如果参数不为空,判断文件是否可写,再调用_flushbuf进行刷新,注意此处传递给_flushbuf的参数是EOF。还有一点就是判断_flushbuf是否出错不是判断返回值是否为-1,因为参数为EOF时的返回值也为-1,所以此处用flag & _ERR判断是否出错。
注意,这里我们只针对可写的文件流进行操作,忽略了只读的文件流:
If the stream was open for reading, the behavior depends on the specific implementation. In some implementations this causes the input buffer to be cleared.
针对只读的文件流,不同系统处理的方式不一样,有的系统会清空缓冲区。
_fclose
int _fclose(FILE *f){
int ret;
if((ret = _fflush(f)) != EOF){
free(f->base);
f->base = NULL;
f->ptr = NULL;
f->fd = 0;
f->flag = 0;
f->cnt=0;
}
return 0;
}
fclose调用fflush函数,保证在文件关闭前将缓冲区中的内容刷到文件中,并且释放掉缓冲区的内存空间。
_fseek
关于fseek的介绍请看fseek
int _fseek(FILE *f,long offset,int origin){
int rc;
if(f->flag & _READ) {
if(origin == 1) {
offset -= f->cnt;
}
rc = lseek(f->fd,offset,origin);
f->cnt = 0; //将缓冲区剩余字符数清0
}else if(f->flag & _WRITE) {
rc = _fflush(f); //强制刷新缓冲区
if(rc != EOF) {
rc = lseek(f->fd,offset,origin);
}
}
return rc == -1 ? EOF : 0;
}
当文件流为可读时,见下图:
由于有缓冲区的存在,我们直觉上的文件指针位置和真实的文件指针位置是不同的,差了cnt个单位长度。所以当我们设置移动offset个长度时,真实的文件指针需要移动offset-cnt个单位长度(offset为正数或者负数)。
之后我们需要将cnt置为0,以便下次读取时将缓冲区的数据更新。
当origin为0或者2时,直接调动lseek即可。
而当文件流为可写时,见下图:
真实的文件指针位置与我们直觉上的文件指针位置差了ptr - base个单位长度,即我们新写入缓冲区的内容长度,所以我们直接调用_fflush即可。(K&R中直接调用的write,但是我觉得这样没有重置ptr指针的位置和cnt,这样的话base与ptr之间的内容会被刷入到文件中两次)。
当文件是以a模式打开时,fseek无效:
a+ Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file
position for reading is at the beginning of the file, but output is always appended to the end of the file.
_getchar
int _getchar(){
return _getc(stdin);
}
我们可以发现,_getchar调用的就是_getc,只不过_getc可以传入任意的文件指针,而对_getchar来说,_getc传入的是stdin,也就是{0,NULL,NULL,_READ,0}
。
- 当调用getchar时,首先去stdin结构体中的缓存取数据,如果缓存为空,会在_fillbuf中的
int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read
处阻塞住,等待用户输入字符。 - 当标准输入(stdin)连接的是终端时,终端I/O会采用规范模式输入处理:对于终端输入以行为单位进行处理,对于每个读请求,终端设备输入队列会返回一行(用户输入的字符会缓存在终端输入队列中,直到用户输入一个行定界符,输入队列中的数据会返回给read函数)。
- 这一行数据会缓存在标准I/O缓冲区中,下次调用getchar时会返回缓冲区第一个字符。当缓冲区数据被读光时,重复上述过程。
_putchar
int _putchar(int x){
return _putc(x,stdout);
}
我们可以发现,_putchar调用的就是_putc,只不过_putc可以传入任意的文件指针,而对_putchar来说,_putc传入的是stdout,也就是{0,NULL,NULL,_WRITE,1}
。
- 调用putchar时,数据会缓存在stdout中的缓冲中。
- 当stdout的缓冲被装满时,会调用write将数据写入到stdout中,stdout将数据写入到终端设备输出队列中,输出队列将数据写入到终端。
完整代码
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define EOF -1
#define BUFSIZ 1024
#define OPEN_MAX 20 //打开的最大文件数
#define PERMS 0666
typedef struct _iobuf{
int cnt; //缓冲区剩余字节数
char *base; //缓冲区地址
char *ptr; //缓冲区下一个字符地址
int flag; //访问模式
int fd; //文件描述符
} FILE; //别名,与标准库一致
extern FILE _iob[OPEN_MAX];
//八进制
enum _flags {
_READ = 01,
_WRITE = 02,
_UNBUF = 04, //不进行缓冲
_EOF = 010,
_ERR = 020
};
FILE _iob[OPEN_MAX] = {
{0,NULL,NULL,_READ,STDIN_FILENO},
{0,NULL,NULL,_WRITE,STDOUT_FILENO},
{0,NULL,NULL,_WRITE|_UNBUF,STDERR_FILENO}
};
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
int _ferror(FILE *f){
return f-> flag & _ERR;
}
int _feof(FILE *f){
return f-> flag & _EOF;
}
int _fileno(FILE *f){
return f->fd;
}
//返回第一个字符
int _fillbuf(FILE *f){
int bufsize;
if((f->flag & (_READ | _EOF | _ERR)) != _READ){ //判断文件是否可读
return EOF;
}
bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base == NULL){ //没有分配过缓冲区
if((f->base = (char *)malloc(bufsize)) == NULL){
return EOF;
}
}
f->ptr = f->base;
int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read
if(n == 0){ //到达文件结尾
f->base = NULL;
f->cnt = 0;
f-> flag |= _EOF;
return EOF;
}else if(n == -1){ //出错
f->cnt= 0;
f->flag |= _ERR;
return EOF;
}else{
f->cnt = --n;
return *f->ptr++;
}
}
int _flushbuf(int x,FILE *f){
if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){
return EOF;
}
int n;
int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base != NULL){
n = write(f->fd,f->base,f->ptr - f->base); //判断需要写入多少字节
if(n != f->ptr - f->base){
f->flag |= _ERR;
return EOF;
}
}else{
if((f->base = (char *)malloc(bufsize)) == NULL){
f->flag |= _ERR;
return EOF;
}
}
if(x != EOF){
f->cnt = bufsize - 1;
f->ptr = f->base;
*f->ptr++ = x;
}else{ //当写入EOF时,代表强制刷新缓冲区内容到文件中
f->cnt = bufsize;
f->ptr = f->base;
}
return x;
}
/**
* @brief _fflush
* @param f
* @return
*/
int _fflush(FILE *f){
int res = 0;
if(f == NULL){
for(int i = 0; i < OPEN_MAX; i++){ //当参数为NULL时,刷新所有的文件流
if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){ //有一个出错即返回-1
res = EOF;
}
}
}else{
if(f->flag & _WRITE){
_flushbuf(EOF,f);
}else{
res = EOF;
}
}
if(f->flag & _ERR){ //出错
res = EOF;
}
return res;
}
int _fclose(FILE *f){
int ret;
if((ret = _fflush(f)) != EOF){
free(f->base);
f->base = NULL;
f->ptr = NULL;
f->fd = 0;
f->flag = 0; //@TODO
}
return 0;
}
int _fseek(FILE *f,long offset,int origin){
int rc;
if(f->flag & _READ) {
if(origin == 1) {
offset -= f->cnt;
}
rc = lseek(f->fd,offset,origin);
f->cnt = 0; //将缓冲区剩余字符数清0
}else if(f->flag & _WRITE) {
rc = _fflush(f); //强制刷新缓冲区
if(rc != EOF) {
rc = lseek(f->fd,offset,origin);
}
}
return rc == -1 ? EOF : 0;
}
int _getc(FILE *f){
return --f->cnt >= 0 ? *f->ptr++ : _fillbuf(f);
}
int _putc(int x,FILE *f){
return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}
int _getchar(){
return _getc(stdin);
}
int _putchar(int x){
return _putc(x,stdout);
}
FILE *_fopen(char *file,char *mode){
int fd;
FILE *fp;
if(*mode != 'r' && *mode != 'w' && *mode != 'a') {
return NULL;
}
for(fp = _iob; fp < _iob + OPEN_MAX; fp++) { //寻找一个空闲位置
if (fp->flag == 0){
break;
}
}
if(fp >= _iob + OPEN_MAX){
return NULL;
}
if(*mode == 'w'){
fd = creat(file,PERMS);
}else if(*mode == 'r'){
fd = open(file,O_RDONLY,0);
}else{ //a模式
if((fd = open(file,O_WRONLY,0)) == -1){
fd = creat(file,PERMS);
}
lseek(fd,0L,2); //文件指针指向末尾
}
if(fd == -1){
return NULL;
}
fp->fd = fd;
fp->cnt = 0; //fopen不分配缓存空间
fp->base = NULL;
fp->ptr = NULL;
fp->flag = *mode == 'r' ? _READ : _WRITE;
return fp;
}
int main(int argc,char *argv[]){
FILE *f = _fopen("zyc.txt","a");
/*char c;
for(int i = 0; i < 10; i++){
c = _getc(f);
}*/
/*for(int i = 0; i < 9; i++){
_putc('6',f);
}
_fseek(f,-5,1);
for(int i = 0; i < 9; i++){
_putc('8',f);
}
_fclose(f);*/
int c;
while((c = _getchar()) != '\n'){
_putchar(c);
}
_fclose(stdout);
return 0;
}
上面提到的部分函数在Answer to Exercise 8-3, page 179中有更详细的实现。
参考资料:
- C程序设计语言(第2版•新版)第8章 UNIX系统接口
- Macros vs Functions
- 从"read"看系统调用的耗时
- stdin/stdout/stderr的缓冲方式疑问
- Answer to Exercise 8-3, page 179
- 缓存与IO
- UNIX环境高级编程(第3版) 第18章 终端I/O
C 标准I/O库粗略实现的更多相关文章
- Linux C 文件操作,系统调用 -- open()、read() 和 标准I/O库 -- fopen()、fread()
函数汇总: open().write().read().close() fopen().fwrite().fread().fclose() 一.什么是文件 在讲述文件操作之前,我们首先要知道什么是文件 ...
- Xcode6.1标准Framework静态库制作方法。工程转Framework,静态库加xib和图片。完美解决方案。
http://www.cocoachina.com/bbs/read.php?tid-282490.html Xcode6.1标准Framework静态库制作方法.工程转Framework,静态库加x ...
- UNIX环境高级编程笔记之标准I/O库
一.总结 文件I/O一章讲了不带缓冲的I/O,本章讲的是带缓冲的I/O.不带缓冲针对的是内核的系统调用,而带缓冲针对的是用户空间的标准库函数,是基于带缓冲的I/O实现的.不带缓冲的I/O通过文件描述符 ...
- 标准I/O库之缓冲
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数.它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦. 标准I/O提供了三种类型的缓冲: (1) ...
- 标准I/O库之流和FILE对象
对于标准I/O库,它们的操作是围绕流(stream)进行的.当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联. 对于ASCII字符集,一个字符用一个字节表示.对于国际字符集,一个字 ...
- 标准I/O库之临时文件
ISO C标准I/O库提供了两个函数以帮助创建临时文件. #include <stdio.h> char *tmpnam( char *ptr ); 返回值:指向唯一路径名的指针 FILE ...
- 标准I/O库之标准I/O的效率
程序清单5-1 用getc和putc将标准输入复制到标准输出 #include "apue.h" int main( void ) { int c; while(( c = get ...
- 标准I/O库之读和写流
一旦打开了流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读.写操作: (1)每次一个字符的I/O.一次读或写一个字符,如果流是带缓冲的,则标准I/O会处理所有缓冲. (2)每次一行的I/O ...
- 标准I/O库之打开和关闭流
下列三个函数打开一个标准I/O流. #include <stdio.h> FILE *fopen( const char *restrict pathname, const char *r ...
随机推荐
- 关于svg
动画:css3动画,canvas(js动画),svg(html动画). svg基本元素 version: 表示 <svg> 的版本,目前只有 1.0,1.1 两种 xmlns:http:/ ...
- SpringBoot+Redis环境搭建
写在正文前的絮叨: 其实这个环境的搭建是很简单的,照着官网给的说明很快就可以搭建测试出来.为什么又要写出来呢?只是为了记录.保留.分享这其中遇到的坑. 这个环境之前在架构一个简单系统时,也曾经搭建过, ...
- jQuery操作input改变value属性值
今天写了一个表单元素,在用户点击的时候会清空input中的内容,当鼠标点击其他地方的时候会把输入的值保存为input的value值 类似于这样的效果 当用户点击的时候文字消失. html代码 < ...
- Linux 链接详解(2)
可执行文件加载执行过程: 上一节我们说到ELF文件格式,静态库的符号解析和重定位的内容.这一节我们来分析一下可执行文件. 由上一节我们知道可执行文件也是ELF文件,当程序被加载器加载到内存时是按照EL ...
- 为什么选择Django?
Web开发是Python语言应用领域的重要部分,也是工作岗位比较多的领域.如果你对基于Python的Web开发有兴趣,正打算开始学习使用Python做Web开发,或者已经是一个Web开发者有工作需要, ...
- MySql基础入门-mysql的结构层次
了解MySql必须牢牢记住其体系结构图,Mysql是由SQL接口,解析器,优化器,缓存,存储引擎组成的. 1.Connectors指的是不同语言中与SQL的交互 2.Management Servei ...
- 【Spring】多数据源导致自动化配置失败问题
一.问题描述 笔者根据需求在开发过程中,需要在原项目的基础上(单数据源),新增一个数据源C,根据C数据源来实现业务.至于为什么不新建一个项目,大概是因为这只是个小功能,访问量不大,不需要单独申请个服务 ...
- DOMContentLoaded、readystatechange、load、ready详谈
对前端同学而言,loade,unload,DOMContentLoaded等页面加载过程中会触发的事件肯定是都接触过,不过要是具体问各个事件的区别,我就不是那么能清晰的解答上来的了.正好刚刚在无阻塞脚 ...
- always中的敏感变量
always语句下如果有判断语句if,那么if语句中的条件必须有always中的敏感变量. 否则错误提示为:Error (10200): Verilog HDL Conditional Stateme ...
- Python3 词汇助手 有道翻译助手 有道导出文件格式转换
根据有道翻译软件的功能,结合实际用途,基于Python3.6写了一个有道翻译助手软件. 测试文件及源代码已上传至:https://github.com/MMMMMichael/Translation- ...