Windows系统下使用C语言编写单线程的文件备份程序
写在最前方
源路径:即From-Path,你准备要备份的资料
目的路径:即To-Path,你准备要存储备份的资料的地方
稍微回想一下,上一次写的代码,本次的任务是遍历目录及其子目录,那么这回要干的就是将上次遍历过的数据,挪一下窝,到我们想要他们去的位置。
这涉及到两个操作,遍历和拷贝,前一个动作我们在上一回已经实现了,只需做小小的改动,就能够使用。后一个动作也是需要靠WindowsAPI来完成,至于哪些,稍后再提。
现在先让我们完成一个魔法,3,2,1!:
do{ puts("-------------------------------------------------"); fprintf(stdout,"TheDefaultPathis:%s\n",DEFAULT_TO_PATH); fprintf(stdout,"NowThePathis:%s\n",get_backup_topath()); puts("-------------------------------------------------"); puts("ThatisaSystemBackUpSoftwareforWindows!"); puts("Listofthesoftwarefunction:"); puts("1.BackUp"); puts("2.SetBackUpTO-PATH"); puts("3.ShowTO-PATHHistory"); puts("4.ReadMe"); puts("5.Exit"); puts("-------------------------------------------------");
对界面稍微有了一些改动。
新增了第三行和第四行的系统默认目的路径和当前使用的目的路径。
新增了倒数第四行的查看目的路径历史纪录的功能。
在main函数外头需要externDEFAULT_TO_PATH;因为引用了setPath.c里的一个全局变量。
写在中间
我们曾经提到要让函数的功能更加清晰,为了达到这个目的,应该把可能用到的一些原生库函数包裹一下,让可能发生的错误尽量掌握在我们自己的手里
安全函数
新建safeFunc.hsafeFunc.c
考虑一下我们需要包裹的函数:malloc,free,fopen三个库函数。
为了不让后方的多线程实现产生更多的以后,不单独使用全局错误输出。
让我来将他们实现一下
我不会省略一些看似不必要的东西,例如注释,而是完整的呈现出来,如果觉得篇幅过长,可以选择跳跃的阅读。
魔法来了,3,2,1!
#include<stdio.h>/*size_t*/ #include<stdlib.h> #include<setjmp.h> #defineTRY_TIMES3 typedefstruct_input_para{ char*file;/*待打开或创建的文件名*/ char*mode;/*打开的模式*/ }params; jmp_bufmalc_jmp;/*Malloc_s*/ jmp_buffopn_jmp;/*Fopen*/ /** *@version1.02015/10/01 *@authorwushengixin *@param...参看结构体说明 可传入任意的个数的,形式为.file="xxx",.mode="x"的参数 *function用于使用默认参数,并调用函数Fopen进行打开操作 */ #defineFopen_s(...)Fopen((params){.file=NULL,.mode="r",__VA_ARGS__}) FILE*Fopen(constparamsfile_open); /** *@version1.02015/10/01 *@authorwushengxin *paramsizes输入需要分配的大小 *function用于隐藏一些对错误的处理,并调用malloc库函数分配空间 */ void*Malloc_s(size_tsizes); /** *@version1.02015/10/01 *@authorwushengxin *@paraminput外部传入的等待释放的指针 *function用于隐藏一些对错误的处理,并调用free库函数进行释放指针 */ voidFree_s(void*input);
里面用到了一些新的特性,如果使用GCC/Clang作为编译器的,记得要开启-std=c11支持。
这几个函数就不再详细解释,而是简略说几个,接下来放上实现代码:
FILE*Fopen(constparamsfile_open) { inttimes=0; FILE*ret_p=NULL; if(file_open.file==NULL) { fputs("TheFileNameisEMPTY!ComfirmitandTryAgain",stderr); returnret_p; } setjmp(fopn_jmp);/*fopn_jmpTothere*/ ret_p=fopen(file_open.file,file_open.mode); if(ret_p==NULL) { if(times++<TRY_TIMES) longjmp(fopn_jmp,0);/*fopn_jmpFromhere*/ fprintf(stderr,"TheFile:%sOpenwithMode(%s)Fail!\n",file_open.file,file_open.mode); } returnret_p; } void*Malloc_s(size_tsizes) { inttimes=0; void*ret_p=NULL; if(sizes==0) returnNULL; setjmp(malc_jmp);/*malc_jmpToThere*/ ret_p=malloc(sizes); if(ret_p==NULL) { if(times++<TRY_TIMES)/*malc_jmpFromHere*/ longjmp(malc_jmp,0); fputs("AllocateMemoryFail!",stderr); } returnret_p; } voidFree_s(void*input) { if(input==NULL) { #if!defined(NOT_DEBUG_AT_ALL) fputs("SentANULLpointertotheFree_sFunction!",stderr); #endif return; } free(input); input=NULL; }
第一个函数是用外部定义的宏`Fopen_s`启动它,这里没有实现隐藏它。
最后一个函数中使用了预处理的机制,如果在头文件中定义了`#defineNOT_DEBUG_AT_ALL`,这个输出将不在出现
安全函数已经撰写完成,接下来就是干正事了
setPath.h
我们首先要将程序里保存上默认的目的路径,首先想到用常量#define...
其次应该要确保当前目的路径不被其他非法的渠道访问,那就应该用一个static字符数组存储。
接下来就是要提供一个函数当作接口(这里用了接口这个术语不知道合不合适),来获取当前实际在使用的目的路径get_backup_topath。
这里还需要将之前实现过的repl_str,再次实现一次,因为之前的显示功能只是测试,并不会实际应用到程序当中。
完成这两个功能函数以后,再去考虑实现怎么样设置路径,存储路径,以及使用文件流操作来缓存历史目的路径
#include"safeFunc.h" #defineSELF_LOAD_DEFAULT_PATH"C:/" #defineMIN_PATH_NAME_MAX_PATH/*最小的限制*/ #defineLARGEST_PATH_NAME32767/*路径的最大限制*/ /* *@version1.02015/10/02 *@authorwushengxin *@function用于返回当前使用的目的路径 */ constchar*get_backup_topath(); /** *@version1.02015/09/28 *@authorwushengxin *@paramsrc外部传入的,用于调整 *@function用于替换路径中的/为\的 */ voidrepl_str(char*src);
对应的实现中,会定义一个静态的字符数组,且在头文件中能够看见,很多是在`showFiles`里定义过的。
定义过的函数,例如`repl_str`需要把`showFiles.c`中的**实现**,使用`#if0...#endif`进行注释掉,不然会发生重定义的错误。
setPath.c
#include"setPath.h" staticcharto_path_buf[LARGEST_PATH_NAME]=SELF_LOAD_DEFAULT_PATH; constchar*DEFAULT_TO_PATH=SELF_LOAD_DEFAULT_PATH; constintLARGEST_PATH=LARGEST_PATH_NAME; constchar*get_backup_topath() { returnto_path_buf; } voidrepl_str(char*src) { size_tlength=strlen(src); for(size_ti=0;i<=length;++i) { if(src[i]=='/') src[i]='\\'; } return; }
有了上面的代码,主界面就再次能够无误运行了,那么剩下的就是实现,设置目的路径,存储目的路径到本地,显示目的路径,分别对应主界面的2,3。
怎么实现比较好,再开始之前,分析一下会遇到的情况:
我们在得到目的路径之后,会将其拷贝给默认路径to_path_buf,并且将其存储到本地缓存文件中,以便下次程序开始时可以直接使用上一次的路径
还可以使用另一个文件存储所有用过的历史路径,包含时间信息。
那么这就要求我们首先实现存储目的路径的功能,其次再实现设置目的路径的功能,最后实现显示目的路径的功能
注:两个看似无用的全局变量(const)是为了其他文件的可见性而设立的,且相对于#define能够省一些无足轻重的空间。
存储目的路径store_hist_path
setPath.h
#include<time.h> /** *@version1.02015/10/02 *@versionwushengxin *@parampath需要存储的路径 *@function用于存储路径到本地文件"show_hist"和"use_hist" */ voidstore_hist_path(constchar*path); setPath.c voidstore_hist_path(constchar*path) { time_tctimes; time(&ctimes);/*获取时间*/ FILE*input_use=Fopen_s(.file="LastPath.conf",.mode="w");/*每次写入覆盖*/ FILE*input_show=Fopen_s(.file="PathHistory.txt",.mode="a"); if(!input_show||!input_use) { #if!defined(NOT_DEBUG_AT_ALL) fputs("Open/CreatetheFileFail!",stderr); #endif return; } fprintf(input_use,"%s\n",path);/*写入*/ fprintf(input_show,"%s%s",path,ctime(&ctimes)); fclose(input_show); fclose(input_use); return; }
`time`和`ctime`函数的使用网路上的介绍更加全面,这里不做解释。
完成了存储的函数之后,便是实现从键盘读取并且设置默认路径
设置目的路径set_enter_path
在此处需要停下来在此思考一下,如果用户输入了错误的路径(无效路径或者恶意路径),也应该被读取吗?所以应该增加一个检查,用于确认路径的有效性。
setPath.h
#include<string.h> #include<io.h>/*_access*/ enum{NOT_EXIST=0,EXIST=1}; /** *@version1.02015/10/02 *@authorwushengxin *@function用于读取从键盘输入的路径并将之设置为默认路径,并存储。 */ voidset_enter_path(); /** *@version1.02015/10/02 *@authorwushengxin *@parampath用于检查的路径 *@function用于检查用户输入的路径是否是有效的 */ intis_valid_path(constchar*path);
setPath.c
intis_valid_path(constchar*path) {/*_access后方有解释*/ if(_access(path,0)==0)/*是否存在*/ returnEXIST; else returnNOT_EXIST; } voidset_enter_path() { intintJudge=0;/*用来判断是否决定完成输入*/ chartmpBuf[LARGEST_PATH_NAME];/**临时缓冲区**/ while(1) { printf("EnterThePathYouwant!\n"); fgets(tmpBuf,LARGEST_PATH_NAME*sizeof(char),stdin);/*获取输入的路径*/ sscanf(tmpBuf,"%s",to_path_buf); if(is_valid_path(to_path_buf)==NOT_EXIST) { fprintf(stderr,"YourEnterisEmpty,SoLoadtheDefaultPath\n"); fprintf(stderr,"%s\n",SELF_LOAD_DEFAULT_PATH); strcpy(to_path_buf,SELF_LOAD_DEFAULT_PATH); } fprintf(stdout,"YourEnteris\"%s\"?(1foryes,0forno)\n",to_path_buf); fgets(tmpBuf,LARGEST_PATH_NAME*sizeof(char),stdin); sscanf(tmpBuf,"%d",&intJudge);/*获取判断数的输入*/ if(intJudge!=0) { if(to_path_buf[strlen(to_path_buf)-1]!='/') strcat(to_path_buf,"/");/*如果最后一个字符不是'/',则添加,这里没考虑是否越界*/ store_hist_path(to_path_buf); break; }/*if(intJudge)*/ }/*while(1)*/ return; }/*set_enter_path*/
这一组函数的功能稍微复杂,大体来说便是`读取路径输入->检查路径有效性->读取判断数->是否结束循环`
其中`_access`函数有些渊源,因为这个函数被大家所熟知的是这个形式`access`,但由于这个形式是**POSIX**标准,故**Windows**将其实现为`_access`,用法上还是一样的,就是名字不同而已。
显示历史路径show_hist_path
setPath.h
/** *@version1.02015/10/02 *authorwushengxin *function用于在窗口显示所有的历史路径 */ voidshow_hist_path();
setPath.c
voidshow_hist_path() { system("cls"); charoutBufName[LARGEST_PATH_NAME]={'\0'}; FILE*reading=Fopen_s(.file="PathHistory.txt",.mode="r"); if(!reading) return; for(inti=1;i<=10&&(!feof(reading));++i) { fgets(outBufName,LARGEST_PATH_NAME*sizeof(char),reading); fprintf(stdout,"%2d.%s",i,outBufName); } fclose(reading); system("pause"); return; }
剩下最后一个收尾工作
初始化路径
每次程序启动的时候,我们都会读取本地文件,获取上一次程序使用的最后一个路径,作为当前使用的目的路径
初始化目的路径init_path
setPath.h
/** *@versions1.02015/10/02 *@authorwushengxin *@function用于每次程序启动时初始化目的路径 */ voidinit_path();
setPath.c
voidinit_path() { intlen=0; charlast_path[LARGEST_PATH_NAME]={'\0'}; FILE*hist_file=Fopen_s(.file="LastPath.conf",.mode="r"); if(!hist_file)/*打开失败则不初始化*/ return; fgets(last_path,LARGEST_PATH_NAME,hist_file); len=strlen(last_path); if(len>1) { last_path[len-1]='\0';/*消除一个多余的‘\n'*/ strcpy(to_path_buf,last_path); } return; }
这样就大功告成了,对于这个函数中的后`8`行代码,没使用惯用的`fgets配合sscanf`是因为如果这么干的话,需要搭配一个`memset`函数清零,后面会有解释。
对于memset的解释
这个函数对于大的内存块的初始化实际上是很慢的,当然我们这个30KB左右大概的内存可能影响还没有那么大,但是上兆以后,调用memset就是一种性能问题了,很多情况下,编译器在开启高优化等级之后会自动帮你取消memset的隐式调用
什么隐式调用,例如init_path的第二行代码,声明并且用花括号初始化这个数组的时候,就会调用隐式memset。
写在中间
上面完成了界面的大部分功能,剩下的便是备份这个主要功能。
在完成备份之前,首先想想要如何构造这个备份模型
既然是备份,如果不想扩展为多线程的形式,参考第一次写的遍历函数(show_structure)直接找到文件便调用WindowsAPI(稍后介绍)进行复制即可,不需要讲待备份的文件路径保存下来。
如果要考虑多线程扩展,我们就需要从长计议。
对于一个备份模型,最好的莫过于使用一个队列,依旧实行的是遍历模式,但是将找到的文件路径保存,并放入一个先进先出的队列中,这样我们就能够保证在扩展成多线程的时候,可以有一个很清晰的模型参考。
那么现在的任务就是实现这个用于备份的队列模型。
队列模型
- 应该有一个容器空间:用于存放路径
- 有队首队尾标志
- O(1)复杂度的检查队列是否为空的接口或标志
- O(1)复杂度的返回容器容量的接口或标志,容器容量应该固定不变
使用一些面向对象的黑魔法,保存一些操作函数防止代码混乱。
- 初始化函数
- 释放函数
- 弹出操作函数
- 压入操作函数
- 队列实体
考虑到要存储的是字符串,并且由于WindowsAPI的参数需求,对于一个文件,我们需要存储的路径有两个<源路径,目的路径>,对此应该再使用一个路径模型结构体包裹他们,则空间的类型就相应改变一下
新建Queue.hQueue.c
Queue.h
typedefstruct_vector_queuequeue; typedefstruct_combinecombine; |返回值||函数类型名||参数类型| typedefint(*fpPushBack)(queue*__restrict,constchar*__restrict,constchar*__restrict); typedefconstcombine*(*fpPopFront)(queue*); typedefvoid(*fpDelete)(queue*);
五个typedef不知道有没有眼前一懵。,希望能够很好的理解
前两个是结构体的声明,分别对应着队列模型和路径模型。
后两个是函数指针,作用是放在结构体里,使C语言的结构体也能够拥有一些简单的面向对象功能,例如成员函数功能,原理就是可以给这些函数指针类型的变量赋值。稍后例子更加明显。试着解读一下,很简单的。
struct_combine{ char*src_from_path;/*源路径*/ char*dst_to_path;/*目的路径*/ }; struct_vector_queue{ combine**path_contain;/*存储路径的容器主体*/ unsignedintrear;/*队尾坐标*/ unsignedintfront;/*队首坐标*/ intempty;/*是否为空*/ unsignedintcapcity;/*容器的容量*/ fpPushBackPushBack;/*将元素压入队尾*/ fpPopFrontPopFront;/*将队首出队*/ fpDeleteDelete;/*析构释放整个队列空间*/ }; /** *@version1.02015/10/03 *@authorwushengxin *@paramobject外部传入的对象指针,相当于this *@function初始化队列模型,建立队列实体,分配空间,以及设置属性。 */ intnewQueue(queue*object);
可以看到,上方的函数指针类型,被用在了结构体内,此处少了一个初始化函数,是因为不打算把他当作成员函数(借用面向对象术语)
在使用的时候可以直接obj_name.PushBack(...,...,...);
更详细的可以看后面的实现部分。成为成员函数的三个函数,将被实现为static函数,不被外界访问。
queue.c
intnewQueue(queue*object) { queue*loc_que=object; combine**loc_arr=NULL; loc_arr=(combine**)Malloc_s(CAPCITY*sizeof(combine*)); if(!loc_arr) return1; loc_que->capcity=CAPCITY;/*容量*/ loc_que->front=0;/*队首*/ loc_que->rear=0;/*队尾*/ loc_que->path_contain=loc_arr;/*将分配好的空间,放进对象中*/ loc_que->PushBack=push_back; loc_que->PopFront=pop_front; loc_que->Delete=del_queue; return0; }
在初始化函数中,可以看到,设置了队首队尾以及容量,分配了容器空间,配置了成员函数。
最后三句配置函数的语句中,push_back,pop_front,del_queue在后方以static函数实现。
但是由于没有声明,所以切记要将三个static函数的实现放在newQueue的前方
/** *@version1.02015/10/03 *@authorwushengxin *@paramobject外部传入的对象指针相当于this *@function释放整个队列实体的空间 */ staticvoiddel_queue(queue*object) { Free_s(object->path_contain); return; } /** *@version1.02015/10/03 *@authorwushengxin *@paramobject外部传入的对象指针相当于this src源路径 dst目的路径 *@function将外部传入的<源路径,目的路径>存入队列中 */ staticintpush_back(queue*__restrictobject,constchar*__restrictsrc,constchar*__restrictdst) { inttimes=0; char*loc_src=NULL;/*本地变量,尽量利用寄存器以及缓存*/ char*loc_dst=NULL; combine*loc_com=NULL; queue*loc_que=object; size_tlen_src=strlen(src);/*获取路径长度*/ size_tlen_dst=strlen(dst); size_trear=loc_que->rear;/*获取队尾*/ size_tfront=loc_que->front;/*获取队首*/ loc_src=Malloc_s(len_src+1);/*分配空间*/ if(!loc_src) return1; loc_dst=Malloc_s(len_dst+1); if(!loc_dst) return2; strcpy(loc_src,src); strcpy(loc_dst,dst); loc_com=Malloc_s(sizeof(combine)); if(!loc_com) return3; loc_com->dst_to_path=loc_dst; loc_com->src_from_path=loc_src; loc_que->path_contain[rear++]=loc_com;/*将本地路径加入实体*/ loc_que->rear=(rear%CAPCITY);/*用数组实现循环队列的步骤*/ if(loc_que->rear==loc_que->front) loc_que->empty=0; return0; } /** *@version1.02015/10/03 *@authorwushengxin *@paramobject外部传入的对象指针 */ staticconstcombine*pop_front(queue*object) { size_tloc_front=object->front;/*获取当前队首*/ combine*loc_com=object->path_contain[loc_front];/*获取当前文件名*/ object->path_contain[loc_front]=NULL;/*出队操作*/ object->front=((object->front)+1)%20;/*完成出队*/ if(object->front==object->rear) object->empty=1; else object->empty=0; returnloc_com; }
一个一个的说这些函数
del_queue:释放函数,直接调用Free_s
push_back:压入函数,将外部传入的两个原始的没有组成的路径字符串,组合成一个combine,并压入路径,每次都判断并置是否为空标志位,实际上这个函数中有累赘代码的嫌疑,应该再分出一个函数,专门用来分配三个空间,防止这个函数过长(接近40行)
pop_front:弹出函数,将队列的队首combine弹出,用于复制,但是这里有一个隐患,就是要将释放的工作交给外者,如果疏忽大意的话,隐患就是内存泄漏。
没有特地的提供一个接口,用来判断是否为空,因为当编译器一优化,也会将这种接口给优化成直接使用成员的形式,某种形式上的内联。
队列模型设计完毕,可以开始设计备份模型
备份模型可以回想一下之前的遍历函数,大体的结构一样,只是此处为了扩展成多线程,需要添加一些多线程的调用函数,以及为了规格化,需要添加一个二级界面
先设计一下二级界面
二级界面
思考一下,这个界面要做什么
选择是否开始备份
并且源路径需要在此处输入
返回上一级
新建backup.hbackup.c文件
在主界面选择1以后就会调用二级界面的函数
列出二级界面的选项
1StartBackup
2BackTolastlevel
backup.h
/** *@version1.02015/10/03 *@authorwushengxin *function显示二级界面 */ voidsec_main_windows(); backup.c voidsec_main_windows() { chartmpBuf[256]; intselects; do{ setjmp(select_jmp); system("cls"); puts("-------------------1.BackUp------------------"); puts("ForThisSelect,YoucanchooseTwoOptions:"); puts("1.StartBackup(TheDirectoryPathThatYouEnterLATER)"); puts("2.BackTolastlevel"); puts("-----------------------------------------------"); fprintf(stdout,"EnterYourSelection:"); fgets(tmpBuf,256,stdin); sscanf(tmpBuf,"%d",&selects); if(selects!=1&&selects!=2) { fprintf(stdout,"\nYourSelect\"%s\"isInvalid!\nTryAgain\n",tmpBuf); longjmp(select_jmp,1); } switch(selects) { jmp_bufenter_path_jmp; case1: { chartmpBuf[LARGEST_PATH],tmpPath[LARGEST_PATH];/*使用栈分配空间,因为只用分配一次*/ setjmp(enter_path_jmp);/*enterjumptothere*/ puts("EntertheFullPathYouwanttoBackUp(e.g:C:/Programing/)"); fprintf(stdout,"OrEnterqtobacktoselect\nYourEnter:"); fgets(tmpBuf,LARGEST_PATH,stdin); sscanf(tmpBuf,"%s",tmpPath); if(_access(tmpPath,0)!=0)/*检查路径是否存在,有效*/ { if(tmpPath[0]=='q'||tmpPath[0]=='Q') longjmp(select_jmp,0);/*回到可以选择返回的界面*/ fprintf(stderr,"ThePathYouEnterisNotExit!\nTryAgain:"); longjmp(enter_path_jmp,0);/*enterjumpfromhere*/ } } break; case2: return; default: break; }/*switch*/ }while(1); return; }
这个函数只说几点,首先是`switch`的`case1`,之所以用**花括号**包裹起来的原因是,这样才能在里面定义**本地变量**,直接在冒号后面定义是**编译错误**,这个特性可能比较少用,这里提一下,前面也有说过。
写在最后方
剩下的就是编写主要的功能函数和线程调用函数了。