<C语言>数据文件自动生成(进阶优化改造)

功能描述:基于上一篇“数据文件自动生成的实现(多模块进阶)”文章,对工程项目进行包括使用.ini文件存储配置参数、增加.dat文件(二进制文件)的方式记录随机生成数据、利用结构体数组暂存随机生成数据和调用程序计时函数对程序计时等功能的优化改造。
关键词:.ini配置文件;结构体数组;.dat二进制文件;程序计时
读者注意:本文章基于此前两篇文章写作,若对部分代码有疑惑,可查看
<C语言>数据文件自动生成的实现
<C语言>数据文件自动生成(多模块进阶)


0 绪论

一个程序要走向实用是需要和编译器、编译环境脱离开的。我们的程序在编译环境下创建,每次对程序的更改,哪怕只是某个数值的变化都需要编译器重新编译才能使用,这种开放式的程序给了用户极大的权限和自由,但不恰当的操作也会导致系统崩溃,所以一般实用程序为了使用安全(或保护版权),对外都是全封闭的。而满足用户对程序某些参数进行配置的需求,是通过使用配置文件来实现的。这就好比使用智能手机时,在同一套程序下,可以使用设置工具改变一些使用习惯的过程(程序的某些参数发生了变化)。

1 通过.ini文件获取配置参数

  • .ini 文件是Initialization File的缩写,即初始化文件,是 windows 的系统配置文件所采用的存储格式,统管 windows 的各项配置;
  • 配置文件有很多如INI配置文件,XML配置文件,还有就是可以使用系统注册表等。 当然INI配置文件的后缀名也不一定是 .ini ,也可以是 .cfg,.conf 或者是 .txt;
  • .ini 文件同 .txt 文件等都使用ASCII编码,用户可以在记事本或写字板对其内容进行读写操作;
  • INI配置文件有其经典格式:包含 parameters,sections 和 comments 三个基本要素,本工程中只是最简单的利用 .ini 文件存储个别数值,当作一般 .txt 文件使用,所以在这里不作过多探究。

将 .ini 文件获取配置参数的程序封装为一个函数,并单独作为一个源文件。
实际上这种极简单的配置文件类似于此前说到的 cmd 命令窗口,两者都作为一种程序内外交互的方式,若其格式不规范会造成程序无法正常运行。此前工程引入了大量程序以确保命令行参数的规范性问题,此处也应对配置文件的内容进行一定的鉴别。
具体代码如下:

//获取配置文件信息给结构体初始化默认值,CONF* conf(配置参数结构体指针),char* Ini_Path(配置文件文件名指针)
void getInfoParam(CONF* conf, char* Ini_Path)
{
	char str[MAX_STR_LEN] = {0};
	int flag = 0;//规范性标志位,为0时表示规范
	int hang = 0;//配置文件有效行数值

	FILE *fp = NULL;
	fp = fopen(Ini_Path, "r");//ini文件是用ASCII值的文件,用"r"
	if(!fp) 
	{ 
		printf("打开配置文件失败,结构体各分量已使用默认值!\n");
		flag = 1;
	}
	else
	{
		while(!feof(fp))
		{
			fgets(str, sizeof(str), fp);
			if(*str != 0 && strcmp(str, "\n") != 0)//筛除空行
			{
				hang++;
			}
			memset(str, 0, sizeof(str));//清除字符串str中的数据
		}
		if(hang >= 8)
		{
			rewind(fp);//指针回到文件打开初始位置
			strcpy(conf->filesavepath, fgets(str, MAX_STR_LEN, fp));
			strcpy(conf->filename, fgets(str, MAX_STR_LEN, fp));
			conf->maxvalue1 = strToNumber(fgets(str, MAX_BUF, fp));
			conf->minvalue1 = strToNumber(fgets(str, MAX_BUF, fp));
			conf->maxvalue2 = strToNumber(fgets(str, MAX_BUF, fp));
			conf->minvalue2 = strToNumber(fgets(str, MAX_BUF, fp));
			conf->recordcount1 = strToNumber(fgets(str, MAX_BUF, fp));
			conf->recordcount2 = strToNumber(fgets(str, MAX_BUF, fp));
			
			if(strcmp(conf->filesavepath, "\n") == 0 || strcmp(conf->filename, "\n") == 0 || conf->maxvalue1 == 0 || conf->minvalue1 == 0 || conf->maxvalue2 == 0 || conf->minvalue2 == 0 || conf->recordcount1 == 0 || conf->recordcount2 == 0)//筛除空行赋值
			{
				printf("配置文件参数值有误,结构体各分量已使用默认值!\n");
				flag = 1;
			}
			else if(conf->maxvalue1 <= conf->minvalue1 || conf->maxvalue2 <= conf->minvalue2 || conf->recordcount1 <= conf->recordcount2)//筛除不正确的值关系
			{
				printf("配置文件参数值有误,结构体各分量已使用默认值!\n");
				flag = 1;
			}
			else
			{
				printf("配置文件配置结构体分量初始值成功!\n");
			}
		}
		else
		{
			printf("配置文件参数数目不足,结构体各分量已使用默认值!\n");
			flag = 1;
		}
		fclose(fp);
	}
	//配置文件出错,结构体初始默认值
	if(flag == 1)
	{
		strcpy(conf->filesavepath, "Lab4Data\\");
		strcpy(conf->filename, "Lab4.txt");
		conf->number = 0;
		conf->maxvalue1 = 20;
		conf->minvalue1 = 1;
		conf->maxvalue2 = 100;
		conf->minvalue2 = 0;
		conf->recordcount1 = MAX_NUM;
		conf->recordcount2 = MIN_NUM;
	}
}

由代码及注释可以看出,为保证程序的运行,在配置文件的内容不规范时直接进行了报错。而配置文件的错误区分三种情况:一是配置文件行数(参数个数)小于结构体所需参数个数,为避免空行对行数的影响,判断时做空行筛除处理;二是配置文件在有效行数(有效参数个数)大于等于结构体所需参数个数时,给结构体成员按序赋值,为避免空行对结构体成员赋无效值,对所有结构体成员值做筛除处理;三是在满足上述两点情况下逻辑判断三组 int 值的大小,最大值不可小于等于最小值。
满足以上三点才认为配置文件内容规范正确,这极大保障了程序的安全运行,若配置文件内容不符合要求,程序设定结构体各成员使用程序内给出的初始默认值以继续运行。

2 利用结构体数组暂存数据

仍然使用 typedef 关键字定义结构体,结构体 DataItem 内部存储的是三元组的三个数据,多个结构体组成结构体数组,结构体数组依次存入 conf.number 条三元组数据。
结构体定义代码如下:

typedef struct DataItem
{
	int item1; //数据记录三元组的第一个元素
	int item2; //数据记录三元组的第二个元素
	int item3; //数据记录三元组的第三个元素
}DATAITEM;

在此前的文章中,使用的是二维数组的方式来缓存随机生成的数据记录,因为二维数组只有行数不定,而列数始终为3,所以使用结构体数组非常合适。这里优先使用结构体数组,因为二维数组的动态内存申请函数 malloc的使用和释放都是是分为两步进行的,而结构体数组只需要一步即可(去掉for循环过程),将两者代码作对比如下:

	//二维数组动态内存申请
	int **a = (int**)malloc(sizeof(int*) * n);       //申请n行动态内存分配空间
	for (int i = 0; i < n; i++)
	{
		a[i] = (int *)malloc(sizeof(int) * 3);       //申请3列动态内存分配空间
	}
	
	//二维数组动态内存释放
	for (int i = 0; i < n; i++)       
	{
		free(a[i]);                                  //逐个释放指针内存
	}
	free(a);                                         //释放指向指针的二级指针内存
    //结构体数组态动态内存申请
    DATAITEM *s, *p;
	s = (DATAITEM*)malloc(sizeof(DATAITEM) * conf.number);//申请conf.number个动态结构体数组
	p = s;
	
	//结构体数组态内存释放
	free(s); 

注意到使用了两个指针 s 和 p 指向结构体数组,指针 s 申请了动态内存空间,不让指针 s 进入到 for 循环而使用指针 p 进入循环,是因为指针 p 在循环里每循环一次执行 p++ 时,其值都会被修改,每次给指针 p 的成员赋值后都存入循环外的指针 s 即可给结构体数组依次赋值。

    DATAITEM *s, *p;
	s = (DATAITEM*)malloc(sizeof(DATAITEM) * conf.number);//申请conf.number个动态结构体数组
	p = s;
	for(int i = 0; i < conf.number; i++)
	{
		p->item1 = random(conf.maxvalue1, conf.minvalue1);
		p->item2 = random(conf.maxvalue1, conf.minvalue1);
		while(p->item2 == p->item1)   //取第二项值不同于第一项值
		{
			p->item2 = random(conf.maxvalue1, conf.minvalue1);
		}
		p->item3 = random(conf.maxvalue2, conf.minvalue2);
		p++;
	}

3 使用.dat文件以二进制存储数值(字节序)

在前两篇文章中都是使用了 .txt 文件存储文本文件,使用 .dat 文件存储二进制文件的操作与其是极为相似的,该工程可以同时生成这两种格式的文件,故将两者程序对比如下:

    //生成.txt文本文件
    FILE *fpt = NULL;
	fpt = fopen(buffer,"w");
	if(!fpt)
	{
		printf("文件打开失败!\n");
	}
	else  //文件打开/创建成功
	{
		start = clock();
		fprintf(fpt, "%d\n", conf.number);//在文件中打印条数信息
		//将结构体数组s中的元素打印到文件中
		for (int i = 0; i < conf.number; i++)
		{
			fprintf(fpt, "%d,%d,%d", s[i].item1, s[i].item2, s[i].item3);
			fprintf(fpt,"\n");
		}
		fclose(fpt); //关闭文件
		printf("输出文本文件‘%s’生成成功!\n",conf.filename);
		end = clock();
		printf("生成文本文件时间:%fs\n", (double)(end - start) / CLOCKS_PER_SEC);
	}
    //生成.dat二进制文件
    FILE *fpd = NULL;
	fpd = fopen(new_buffer,"wb");
	if(!fpd)
	{
		printf("文件打开失败!\n");
	}
	else  //文件打开/创建成功
	{
		start = clock();
		fwrite((char*)&conf.number, sizeof(int), 1, fpd);//在文件中打印条数信息
		char ch[]="\n\r";
		fwrite(ch, 2, 1, fpd);

		//将结构体数组s中的元素打印到文件中
		for (int i = 0; i < conf.number; i++)
		{
			fwrite(&s[i], sizeof(int), 3, fpd);
			char ch[]="\n\r";
			fwrite(ch, 2, 1, fpd);
		}
		fclose(fpd); //关闭文件
		printf("输出二进制文件‘%s’生成成功!\n", new_filename);
		end = clock();
		printf("生成二进制文件时间:%fs\n", (double)(end - start) / CLOCKS_PER_SEC);
	}

可以看到,首先是文件打开/创建的方法不同,.txt 文本文件使用"w"进行写操作, .dat 二进制文件使用"wb"进行写操作。然后两者的数据写入函数不同,在 .txt 文本文件中写入数据调用的是 fprintf() 函数,而在 .dat 二进制文件中写入数据调用的是 fwrite() 函数。fprintf() 函数更像我们平时使用的 printf() 函数,换行操作可以直接用“\n”实现,符号写入也可以直接写入,而 fwrite() 函数需要单独写入"\n\r"这个两字节长度的符号才可以。因为二进制文件的不具备直接阅读性,所以可省略所有可视化符号的写入,程序中的换行符亦不需要("\n"是换行符,"\r"是光标移动到行首)。

对于整数的二进制存,二进制文件有字节序的概念,字节序即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。
常见的字节序是小端模式(Little endian)大端模式(Big endian)。小端模式最符合人的思维,地址低位存储值的低位,地址高位存储值的高位;大端模式最直观,地址低位存储值的高位,地址高位存储值的低位,把内存地址从左到右按照由低到高的顺序写出,把值按照通常的高位到低位的顺序写出。
对于操作系统之上的应用程序来说,字节序实际上是由操作系统和编译器决定的。而操作系统和编译器所提供给应用程序的运行环境的默认字节序一定是CPU支持的字节序。

可以构造一个函数用来检测这两种常用字节序,函数如下所示:

void test_endian()
{
    uint32_t i = 0x04030201;
    unsigned char* cp=(unsigned char*)&i;
    if(*cp == 1)
        printf("本实验是小端字节序(little-endian)\n");
    else if(*cp == 4)
        printf("本实验是大端字节序(big-endian)\n");
    else
        printf("字节序未知\n");
}

其中 uint32_t 在标准头文件 stdint.h 中定义。

4 调用程序计时函数对程序计时

这里主要介绍C语言对程序计时的三种常用方法;

方法一clock() 函数。函数原型:clock_t clock(void)
其中clock函数返回从开始这个程序到调用的clock()函数之间的CPU时钟计时单元(click tick)数。 返回值类型是clock_t。其中CLOCKS_PER_SEC是一个已宏定义的常数(通常为1000),表示1秒钟有多少个时钟计时单元。精确值:精确到毫秒。

#include <time.h>
int main()
{
    clock_t start, end;
    start = clock();
    /*...需要计时的代码...*/
    end = clock();
    printf("time=%f\n", (double)(end - start) / CLOCKS_PER_SEC);
    return0;
}

方法二time() 函数,difftime() 函数。函数原型:time_t time(time_t, * timer),double difftime(time_t, time_t)
在C语言中用 time() 函数获取自Unix标准时间戳(1970年1月1日0点0分0秒,GMT)到当前的秒数。 difftime(t2 - t1) 要比 t2 - t1 更准确。diffime会根据机器进行转换。精确值:精确到秒

#include <time.h>
int main()
{
    time_t start, end;
    start = time(NULL);
    /*...需要计时的代码...*/
    end = time(NULL);
    printf("time = %d秒\n", difftime(end, start));
    return0;
}

方法三gettimeofday() 函数。函数原型:int gettimeofday(struct timeval *tv, struct timezone *tz)
这个函数是 linux 系统专属函数,可以精确到微秒,其中参数 tv 是保存获取时间的结果类型,参数 tz 用于保存时区结果(若不使用可以传入NULL) 。精确值:精确到微秒。

#include <time.h>
int mian()
{
    struct timeval start, end;
    gettimeofday(&start, NULL);
    /*...需要计时的代码...*/
    gettimeofday(&end, NULL);
    longtimeuse = 1000000*(end.tv_sec - start.tv_sec) + end.tv_usec-start.tv_usec;
    printf("time =%f 秒\n", timeuse/1000000)
    return0;
}

方法一能使用于大多操作系统,本工程也使用该函数进行程序计时,具体使用代码在文本文件和二进制文件生成代码中可以看到;
方法二统计的时间精确到秒,针对的是运行时间较长,或者有明显的时间差的程序,可移植性好,性能稳定,在设定随机种子已多次使用;
方法三是 linux 系统专属使用方法。
本工程使用方法一对生成数据存入结构体、生成文本文件、生成二进制文件三个过程的时间进行了信息打印,在对应过程位置可看到相应秒数。

5 测试运行

区别于前两篇文章,本工程在命令行参数输入之外还有配置文件的读入,在程序处理顺序上,对命令行参数的处理滞后于对配置文件的处理。测试分为两部分进行:

5.1 配置文件测试

情况1:行数为8行,有效行数为7行(中间空行)。应输出“配置文件参数数目不足……”
在这里插入图片描述
情况2:行数为8行,有效行数为7行(作结束行空行)。应输出“配置文件参数数目不足……”
在这里插入图片描述
以上两情况输出如下所示:
在这里插入图片描述


情况3:行数为9行,有效行数为8行(字符为“\n”)。应输出“配置文件参数值有误……”
在这里插入图片描述
情况4:行数为9行,有效行数为8行(数值为0)。应输出“配置文件参数值有误……”
在这里插入图片描述
情况5:行数为8行,有效行数为8行(第一组最大最小值相等)。应输出“配置文件参数值有误……”
在这里插入图片描述
以上三情况输出如下所示:
在这里插入图片描述


情况6:行数为8行,有效行数为8行(格式规范)。应输出“配置文件配置结构体分量初始值成功!”
在这里插入图片描述
以上情况输出如下所示:
在这里插入图片描述


由以上6种情况可以看到,只有当有效行数大于等于8时,配置文件才会依次给结构体成员赋值,且能确保结构体的8个成员都能取到有效值(非空非0),还要在各取值区域符合大小关系时,配置文件才会配置结构体分量初始值成功。
为了使配置文件在有误状态下程序依然能够继续运行下去,当判断到任何错误情况,程序都会默认使用内部设定的结构体成员初始化默认值,所以在错误情况打印信息中能看到“结构体各分量已使用默认值!”的字样,以上系列判断措施都是对程序健壮性的增强。

5.2 命令行参数测试

以下测试是基于配置文件规范正确的情况下进行的,即使考虑配置文件一旦有误,结构体各分量也会使用初始化默认值(和规范正确配置文件取同值,多一个 conf->number 的赋值无影响)。
命令行参数输入情况如下:
1.“Lab4 r . .\Debug\test1 a” 一次输入,应:随机生成[50, 200]间的记录条数,路径存在,生成数据存入“test1.txt” 和 “test1.dat”;
2.“Lab4 r . .\Debug\test2 d” 一次输入,应:随机生成[50, 200]间的记录条数,路径存在,生成数据存入“test2.dat”;
3.“Lab4 r . .\Debug\test3 t” 一次输入,应:随机生成[50, 200]间的记录条数,路径存在,生成数据存入“test3.txt”;
4.“Lab4 r . .\Debug\test4 qa” 一次输入,应:随机生成[50, 200]间的记录条数,路径存在,默认生成两种文件,生成数据存入“test4.txt” 和 “test4.dat”;
5.“Lab4 r . .\Debug\test5” 一次输入,应:随机生成[50, 200]间的记录条数,路径存在,生成数据存入“test5.txt” 和 “test5.dat”;
6.“Lab4 . .\Debug\test6 r a” 、“r” 依次输入,应:随机生成[50, 200]间的记录条数,路径使用当前工作目录,生成数据存入“r.txt” 和 “r.dat”;
7.“Lab4 . .\Debug\test7 r a c” 一次输入,应:输入参数超限制,系统无法处理;
8.“Lab4” 、“r” 、“. .\Debug\test8”依次输入,应:随机生成[50, 200]间的记录条数,路径使用当前工作目录,生成数据存入“test8.txt” 和 “test8.dat”;

测试1~8结果如下四图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由测试结果可见各打印信息与逻辑推断相同,生成的各值均在上下限范围内(包括随机生成条数和各元素数据数值),对于二进制文件需要特定软件打开,此处不进行查看。

6 工程代码

工程项目下载链接:https://download.csdn.net/download/qq_41804982/17813271

本工程对各功能模块进行了函数封装,格式规范、注释详细、易于阅读。

7 写在后面

【相对路径】对相对路径的正确输入格式做一下说明:正确格式为". .\Lab4\Debug\test.txt “,文件C语言中单斜杠“\”一般用于转义字符,若表示路径中的单斜杠,最好使用“\ \”表示,更加安全,所以用”. .\ \Lab4\ \Debug\ \test.txt "更好。因为该软件在编写会自动转义输出,为了展现时不出错,在相同符号间添加了空格隔开,所以读者看到的以上路径中会有空格,在使用时请自行去除空格。
【测试工作】到现在作者愈发意识到测试工作的重要性,每次写文章,测试运行这一块总是最耗时耗力的,测着测着就会发现有意外发生,然后又去代码里看出错的地方,有时错误比较隐蔽,还需要监视各个变量才能查找出来。这之后又重头进行新一轮测试,以让读者看到的测试过程无误呈现。当然,限于水平,仍有一些错漏之处在所难免,若作者对读者形成了误导,敬请谅解。

8 参考资料

<C语言>数据文件自动生成的实现https://blog.csdn.net/qq_41804982/article/details/114909367
<C语言>数据文件自动生成(多模块进阶)https://blog.csdn.net/qq_41804982/article/details/115958867

C语言读取.ini配置文件:
https://www.jianshu.com/p/6088c3c2488d
https://blog.csdn.net/chexlong/article/details/6818017
https://blog.csdn.net/weixin_33737134/article/details/94273057
结构体数组:
https://bbs.csdn.net/topics/280016730
二进制文件:
https://baike.baidu.com/item/fwrite/10942398?fr=aladdin https://blog.csdn.net/Megurine_Luka_/article/details/104451789
字节序判断:
https://blog.csdn.net/earbao/article/details/53668806
程序计时函数:
https://www.douban.com/group/topic/127994831/

感谢众多网友的系列参考资料,在此向所列资料作者致谢,也向一些缺漏的作者致歉。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自矜不待

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值