C语言强化 day05 结构体与文件操作
1. 结构体
1.1 结构体嵌套指针综合练习
在该练习中,我们会创建一个学生结构体和一个教师结构体,其结构体分别为:
// 学生结构体
struct Student
{
unsigned int len;
char **name;
};
// 教师结构体
struct Teacher
{
char *name;
struct Student *stu;
};
在栈上定义一个二级教师结构体指针,然后在堆区开辟一个教师结构体指针数组,每个教师结构体指针指向一个教师结构体,每个教师结构体指向一个学生结构体。在学生结构体中name
指向一个字符指针数组,每个字符指针指向一个学生的姓名,其内存结构关系图如下:
在堆区开辟内存的时候,我们要从上往下开辟空间。而在释放空间的时候,我们是从下往上开始释放。
实现代码如下:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#define NAME_WIDTH 256
// 学生结构体
struct Student
{
unsigned int len;
char **name;
};
// 教师结构体
struct Teacher
{
char *name;
struct Student *stu;
};
// 内存开辟
void allocateSpace(struct Teacher ***_teachers, unsigned int len)
{
srand((unsigned int)time(NULL));
// 开辟教师结构体数组
struct Teacher **teachers = malloc(sizeof(struct Teacher *) * len);
// 创建每个教师的信息
for (unsigned int i = 0; i < len; i++)
{
teachers[i] = malloc(sizeof(struct Teacher));
teachers[i]->name = malloc(sizeof(char) * NAME_WIDTH);
sprintf(teachers[i]->name, "Teacher%02d_Name", i + 1);
teachers[i]->stu = malloc(sizeof(struct Student));
// 创建学生的信息
teachers[i]->stu->len = rand() % 5 + 3;
teachers[i]->stu->name = malloc(sizeof(char *) * teachers[i]->stu->len);
for (unsigned int j = 0; j < teachers[i]->stu->len; j++)
{
teachers[i]->stu->name[j] = malloc(sizeof(char)* NAME_WIDTH);
sprintf(teachers[i]->stu->name[j], " Teachers%02d_Name_Student%02d_Name", i + 1, j + 1);
}
}
*_teachers = teachers;
}
// 输出教师的信息
void printTeachersInfo(struct Teacher **teachers, unsigned int len)
{
for (unsigned int i = 0; i < len; i++)
{
printf("%s\n", teachers[i]->name);
for (unsigned int j = 0; j < teachers[i]->stu->len; j++)
printf(" %s\n", teachers[i]->stu->name[j]);
}
}
// 释放内存
void freeSpace(struct Teacher ***_teachers, unsigned int len)
{
// 非法释放
if (_teachers == NULL)
return;
if (len <= 0)
return;
// 释放教师信息
for (unsigned int i = 0; i < len; i++)
{
// 释放学生结构体指向的内存
for (unsigned int j = 0; j < (*_teachers)[i]->stu->len; j++)
{
free((*_teachers)[i]->stu->name[j]);
(*_teachers)[i]->stu->name[j] = NULL;
}
free((*_teachers)[i]->stu->name);
(*_teachers)[i]->stu->name = NULL;
free((*_teachers)[i]->stu);
(*_teachers)[i]->stu = NULL;
// 释放教师的名字以及结构体的内容
free((*_teachers)[i]->name);
(*_teachers)[i]->name = NULL;
free((*_teachers)[i]);
(*_teachers)[i] = NULL;
}
free(*_teachers);
*_teachers = NULL;
}
int main(int argc, char* argv[])
{
unsigned int len = 3;
struct Teacher **teachers = NULL;
allocateSpace(&teachers, len);
printTeachersInfo(teachers, len);
freeSpace(&teachers, len);
system("pause");
return 0;
}
#endif
1.2 结构体偏移量
在结构体定义之后,结构体中的成员内存布局也就确定了下来。关于结构体偏移量,在基础的部分就有提及,此处只是对一些访问的方式做一些强化而已。
使用结构体的偏移量实际上是使用该成员变量的地址去进行访问,这需要我们对内存布局要有深刻的理解,也需要对数据之间的强制类型转换灵活使用。关于偏移量的时候,我们或许会时不时遇到关于offsetof(结构体类型, 成员)
这个宏函数,该宏函数位于stddef.h
头文件中。下面来看一个关于结构体偏移量的例子:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#include <stddef.h>
struct Student
{
char *name;
unsigned int age;
double score;
};
void test01()
{
struct Student s1;
struct Student *ps = &s1;
printf("age属性的偏移量为: %d\n", (char *)&(ps->age) - (char *)ps);
printf("age属性的偏移量为: %d\n", (int)&(ps->age) - (int)ps);
printf("age属性的偏移量为: %d\n", offsetof(struct Student, age));
}
// 通过偏移量操作内存
void test02()
{
struct Student s1 = { "弗洛伊德", 63, 26.36 };
printf("s1.age = %u\n", *((unsigned int *)((char *)&s1 + offsetof(struct Student, age))));
printf("s1.age = %u\n", *(unsigned int *)((int *)&s1 + 1));
}
struct Teacher
{
char *name;
unsigned int age;
struct Student stu;
};
// 结构体嵌套结构体操作偏移量
void test03()
{
struct Teacher t = { "迪杰斯特拉", 50, "小白", 24, 99.5 };
unsigned int offset1 = offsetof(struct Teacher, stu);
unsigned int offset2 = offsetof(struct Student, age);
// 学生的年龄
printf("t.stu.age = %u\n", *(unsigned int *)((char *)&t + offset1 +offset2));
printf("t.stu.age = %u\n", ((struct Student *)((char *)&t + offset1))->age);
// 学生的姓名与成绩
printf("t.stu.name = %s\n", *(char **)((char *)&t + offset1));
printf("t.stu.score = %.2lf\n", *((double *)((char *)&t + offset1) + 1));
}
int main(int argc, char* argv[])
{
test01();
test02();
test03();
system("pause");
return 0;
}
#endif
1.3 内存对齐
在我们定义结构体的时候,会自动进行内存对齐操作。默认对齐的模数是结构体中占用内存空间最大的成员的字节数,如果是结构体嵌套结构体则将内层结构体直接展开。如下述代码:
typedef struct _Student
{
int a;
char b;
double c;
float d;
} Student;
其中最大的成员是c
,占用8个字节,所以对齐模数是8。在下述代码:
typedef struct _Student2
{
char a;
Student b;
float c;
}Student2;
此时是结构体嵌套结构体,则需要对内层的Student
结构体进行展开,此时所有的成员中内存最大的是b.c
,占用8个字节,所以此时的对齐模数也是8。
自定义数据类型的对齐规则是第一个属性开始从0开始偏移,第二个属性开始,要放在该类型的大小与对齐模数相比的最小值的整数倍的位置。当所有的属性都计算完之后,再整体做第二次偏移,将整体的计算结果放在最大类型与对齐模数比的最小值的整数倍上。
关于结构体字节数的计算,在基础阶段已经提过,此处不再赘述。如果我们不指定对齐模数,也可以通过预处理命令进行查看对齐模数,其命令是#pragma pack(show)
进行查看,编译的时候会将对齐模数显示在警告信息之中。当然,对齐模数也是可以修改的,其修改操作的指令时#pragma pack(n)
,其中n
是对齐模数,必须是2的整数次方才行。内存对齐可以提高程序的访问效率,以空间换时间。下面给出一个关于内存对齐的代码示例:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#pragma pack(show) // 查看当前的对齐模数,对齐模数是可以改的,需要是2的N次方
typedef struct _Student
{
int a;
char b;
double c;
float d;
} Student;
void test01()
{
printf("sizeof Student = %u\n", sizeof(Student));
}
// 结构体嵌套结构体的时候,以子结构体的最大类型与对齐模数比的整数倍即可
typedef struct _Student2
{
char a;
Student b;
double c;
}Student2;
void test02()
{
printf("sizeof Student2 = %u\n", sizeof(Student2));
}
// 对齐模数可以更改
#pragma pack(1)
typedef struct _Student3
{
char a;
int b;
} Student3;
void test03()
{
printf("sizeof Student3 = %u\n", sizeof(Student3));
}
int main(int argc, char* argv[])
{
test01();
test02();
test03();
system("pause");
return 0;
}
#endif
2. 文件操作
2.1 文件读写回顾以及注意事项
对于文件操作,首先就要说的就是文件读写。文件读写可以按照字符进行操作,也可以按照一行或者块进行操作。除此之外,还可以按照指定的格式进行操作,以及对随机位置对文件进行读写操作。这些操作的函数在基础阶段均已经提过,此处只提供代码进行回顾。文件读写代码如下:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 按照字符读写文件 fgetc fputc
void test01()
{
char *filename = "04_test01.txt";
// 写文件
FILE *f_write = fopen(filename, "w+");
if (f_write == NULL)
{
perror("fopen error");
return;
}
char buf[] = "This is test01 file!";
for (unsigned int i = 0; i < strlen(buf); i++)
{
fputc(buf[i], f_write);
}
fclose(f_write);
// 读文件
FILE *f_read = fopen(filename, "r+");
if (f_read == NULL)
{
perror("fopen error");
return;
}
char ch;
while ((ch = fgetc(f_read)) != EOF)
{
putchar(ch);
}
putchar('\n');
fclose(f_read);
}
// 按照行读写文件 fgets fputs
void test02()
{
char *filename = "04_test02.txt";
// 写文件
FILE *f_write = fopen(filename, "w");
if (f_write == NULL)
{
perror("fopen error");
return;
}
char *poem[] = {
"远看山有色\n",
"近听水无声\n",
"春去花还在\n",
"人来鸟不惊\n"
};
unsigned int len = sizeof(poem) / sizeof(char *);
for (unsigned int i = 0; i < len; i++)
{
fputs(poem[i], f_write);
}
fclose(f_write);
// 读文件
FILE *f_read = fopen(filename, "r");
if (f_read == NULL)
{
perror("fopen error");
return;
}
char buf[1024] = { 0 };
while (!feof(f_read))
{
memset(buf, 0, sizeof buf);
fgets(buf, 1024, f_read);
printf("%s", buf);
}
fclose(f_read);
}
// 按照块进行读写
struct Hero
{
char name[64];
unsigned int hp;
unsigned int pp;
};
void test03()
{
char *filename = "04_test03.txt";
struct Hero heros[] = {
{"妲己", 56, 34},
{"王昭君", 56, 54},
{"武则天", 65, 48},
{"甄姬", 65, 21},
{"小乔", 57, 95},
{"貂蝉", 100, 100}
};
// 写文件
FILE*f_write = fopen(filename, "wb");
if (f_write == NULL)
{
perror("fopen error");
return;
}
unsigned int len = sizeof heros / sizeof(struct Hero);
for (unsigned int i = 0; i < len; i++)
{
fwrite(&heros[i], sizeof(heros[i]), 1, f_write);
}
fclose(f_write);
// 读文件
FILE *f_read = fopen(filename, "rb");
if (f_read == NULL)
{
perror("fopen error");
return;
}
struct Hero myHeros[10];
fread(myHeros, sizeof(struct Hero), len, f_read);
fclose(f_read);
for (unsigned int i = 0; i < len; i++)
{
printf("英雄: %6s, HP: %-3u, PP: %-3u\n", myHeros[i].name, myHeros[i].hp, myHeros[i].pp);
}
}
// 按照格式化读取文件 fprintf fscanf
void test04()
{
char *filename = "04_test04.txt";
// 写文件
FILE *f_write = fopen(filename, "w");
if (f_write == NULL)
{
perror("fopen error");
return;
}
fprintf(f_write, "hello %d年 %d月 %d日", 2023, 10, 18);
fclose(f_write);
// 读文件
FILE *f_read = fopen(filename, "r");
if (f_read == NULL)
{
perror("fopen error");
return;
}
char buf[1024] = { 0 };
while (!feof(f_read))
{
memset(buf, 0, sizeof buf);
fscanf(f_read, "%s", buf);
printf("%s\n", buf);
}
fclose(f_read);
}
// 按照随机位置读写文件
void test05()
{
char *filename = "04_test05.txt";
struct Hero heros[] = {
{ "妲己", 56, 34 },
{ "王昭君", 56, 54 },
{ "武则天", 65, 48 },
{ "甄姬", 65, 21 },
{ "小乔", 57, 95 },
{ "貂蝉", 100, 100 }
};
// 写文件
FILE*f_write = fopen(filename, "wb");
if (f_write == NULL)
{
perror("fopen error");
return;
}
unsigned int len = sizeof heros / sizeof(struct Hero);
for (unsigned int i = 0; i < len; i++)
{
fwrite(&heros[i], sizeof(heros[i]), 1, f_write);
}
fclose(f_write);
// 读文件
FILE *f_read = fopen(filename, "rb");
if (f_read == NULL)
{
perror("fopen error");
return;
}
struct Hero tmpHero;
// 逆序输出结构体信息
fseek(f_read, -(long)sizeof(struct Hero), SEEK_END);
for (unsigned int i = 0; i < len; i++, fseek(f_read, -(long)sizeof(struct Hero) * 2, SEEK_CUR))
{
fread(&tmpHero, sizeof tmpHero, 1, f_read);
printf("英雄: %6s, HP: %-3u, PP: %-3u\n", tmpHero.name, tmpHero.hp, tmpHero.pp);
}
rewind(f_read);
// 顺序输出结构体信息
for (int i = 0; i < len; i++)
{
fread(&tmpHero, sizeof tmpHero, 1, f_read);
printf("英雄: %6s, HP: %-3u, PP: %-3u\n", tmpHero.name, tmpHero.hp, tmpHero.pp);
}
fclose(f_read);
}
int main(int argc, char* argv[])
{
test01();
test02();
test03();
test04();
test05();
system("pause");
return 0;
}
#endif
相信对文件读写的基本操作没有什么问题,接下来要谈论以下关于文件读写中需要注意的两点事项:
- 使用
feof()
按字符读取会产生滞后性。也就是会多读出来一个EOF
。 - 将指针写入文件时无意义的,应该将指针指向的内存的值写入文件。
接下来看一个例子:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 注意事项1: 使用feof按字符读写会产生滞后性
void test01()
{
char *filename = "05_test01.txt";
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
char ch = 0;
#if 0 // 此时会多读出来一个EOF|
while (!feof(fp))
{
ch = fgetc(fp);
putchar(ch);
}
#endif
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
fclose(fp);
}
// 注意事项: 将指针存入文件是无意义的,应该将指针指向的内容存入文件中
struct Student
{
char *name;
unsigned int age;
};
int main(int argc, char* argv[])
{
test01();
system("pause");
return 0;
}
#endif
在上述代码的结构体中,name
成员是用指针进行定义的,而我们进行文件读写的时候将指针的值写入内存中后,而后续我们在对文件读写的时候将可能不会读到正确的字符串,因为该指针指向的内存空间可能是无效的内存空间或者是其它内容存放的内存空间。
2.2 配置文件的读写与加密
在这里我们做一个配置文件读写的案例,对一个配置文件进行操作,首先配置文件的内容如下:
其中文件#
开头的为注释行,并非配置文件的部分。接下里要做的是对配置文件进行加密与解密操作,加密的过程如下:
- 首先按照按照字符方式进行文件操作获取每一个字符,比如获取到的字符为
ch
。 - 将
ch
转换为short
类型,并且将最高位置为1
,其操作就是ch|0x8000
。此时的ch
一共有16
位二进制位,且为负数。 - 由于
ch
的最低4
位非有效数据位,此时可以加上一个0~15
的随机数,这样就实现了一个字符类型的数据使用两个字符进行存储加密。
解密的过程则与上述过程相反。在后续的配置文件读取的过程中,需要对配置文件进行解密,并且对配置文件中的有效行进行筛选,能够根据指定的key
值输出对应的value
。
接下来思考一下如何写这个文件,首先我们需要加密解密,所以需要定义一个加密解密的头文件,即code.h
,并且需要将函数的实现与头文件相分离,所以需要code.c
文件。我们再将配置文件读写操作用config.h
进行封装,将实现部分放在config.c
文件中。由于我们刚开始就是对加密的配置文件进行操作,所以在开始的时候就需要将配置文件明文转化为密文,转换过程此处不提,自行调用后续代码中的相关函数即可。
首先两个头文件的内容分别如下:
code.h
#pragma once
// 加密操作
int encode(const char *srcFile, const char *dstFile);
// 解密操作
int decode(const char *srcFile, const char *dstFile);
config.h
#pragma once
typedef struct _ConfigInfo
{
char key[1024];
char value[1024];
} ConfigInfo;
// 获取配置文件有效行数
unsigned int getFileLength(const char *filename);
// 判断是否为有效行
int isValidLine(const char *line);
// 解析数据
void parseFile(const char *filename, const unsigned int len, ConfigInfo ** configInfo);
// 根据属性获取信息
char * getInfoByKey(const char *key, const ConfigInfo * const configInfo, const unsigned int len);
// 释放内存
void freeConfigInfo(ConfigInfo **config);
上述头文件的实现部分如下述代码:
code.c
#define _CRT_SECURE_NO_WARNINGS
#include "code.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 加密操作
int encode(const char *srcFile, const char *dstFile)
{
if (srcFile == NULL || dstFile == NULL)
{
return -1;
}
FILE *srcFilePointer = fopen(srcFile, "r");
if (srcFilePointer == NULL)
{
perror("scrFile fopen error");
return -1;
}
FILE *dstFilePointer = fopen(dstFile, "w");
if (dstFilePointer == NULL)
{
perror("dstFile fopen error");
return -1;
}
srand((unsigned int)time(NULL));
char tmpCh = 0;
unsigned short dstCh = 0;
while ((tmpCh = fgetc(srcFilePointer)) != EOF)
{
dstCh = (unsigned char)tmpCh;
dstCh <<= 4;
dstCh |= 0x8000;
dstCh += rand() % 16;
fputc((char)(dstCh >> 8), dstFilePointer);
fputc((char)(dstCh % 0x100), dstFilePointer);
}
fclose(srcFilePointer);
fclose(dstFilePointer);
return 0;
}
// 解密操作
int decode(const char *srcFile, const char *dstFile)
{
FILE *srcFilePointer = NULL;
FILE *dstFilePointer = NULL;
if (srcFile == NULL || dstFile == NULL)
return -1;
srcFilePointer = fopen(srcFile, "r");
if (srcFilePointer == NULL)
{
perror("srcFile fopen error");
return -1;
}
dstFilePointer = fopen(dstFile, "w");
if (dstFilePointer == NULL)
{
perror("dstFile fopen error");
return -1;
}
char highCh = 0;
char lowCh = 0;
char decodeCh = 0;
short encodeCh = 0;
while ((highCh = fgetc(srcFilePointer)) != EOF)
{
lowCh = fgetc(srcFilePointer);
encodeCh = ((unsigned char)highCh << 8) + (unsigned char)lowCh;
encodeCh = encodeCh << 4; // 去掉负号与随机数
decodeCh = (unsigned char)(encodeCh >> 8) % 0x100;
fputc((char)decodeCh, dstFilePointer);
}
fclose(srcFilePointer);
fclose(dstFilePointer);
return 0;
}
config.c
#define _CRT_SECURE_NO_WARNINGS
#include "config.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
// 获取文件的有效行数
unsigned int getFileLength(const char *filename)
{
if (filename == NULL)
{
return 0;
}
FILE *file = fopen(filename, "r");
if (file == NULL)
{
perror("fopen error");
return 0;
}
char buf[4096] = { 0 };
unsigned int fileLineCount = 0;
while (fgets(buf, 4096, file) != NULL)
{
if (isValidLine(buf))
{
fileLineCount += 1;
}
}
fclose(file);
return fileLineCount;
}
// 判断是否为有效行
int isValidLine(const char *line)
{
if (strchr(line, ':'))
{
return 1;
}
return 0;
}
// 对配置文件进行解析
void parseFile(const char *filename, const unsigned int len, ConfigInfo ** configInfo)
{
if (filename == NULL || len <= 0)
{
return;
}
ConfigInfo *info = malloc(sizeof(ConfigInfo)* len);
FILE *file = fopen(filename, "r");
if (file == NULL)
{
perror("fopen error");
return;
}
char buf[4096] = { 0 };
unsigned int index = 0;
while (fgets(buf, 4096, file) != NULL)
{
// 合法数据再解析
if (isValidLine(buf))
{
memset(info[index].key, 0, sizeof info[index].key);
memset(info[index].value, 0, sizeof info[index].value);
char *colonPos = strchr(buf, ':');
strncpy(info[index].key, buf, colonPos - buf);
strncpy(info[index].value, colonPos + 1, strlen(colonPos + 1) - 1);
index++;
memset(buf, 0, sizeof buf);
}
}
fclose(file);
*configInfo = info;
}
// 根据key获取相应的value
char * getInfoByKey(const char *key, const ConfigInfo * const configInfo, const unsigned int len)
{
for (unsigned int i = 0; i < len; i++)
{
if (strcmp(key, configInfo[i].key) == 0)
{
return configInfo[i].value;
}
}
return NULL;
}
// 释放配置内存的空间
void freeConfigInfo(ConfigInfo **config)
{
if (*config == NULL)
return;
free(*config);
*config = NULL;
}
在上述代码中我们解析部分使用的是strncpy()
函数,简单的方法可以使用函数sscanf()
。在主函数文件中我们的代码如下:
配置文件读写.c
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#include "config.h"
#include "code.h"
int main(int argc, char* argv[])
{
char *filename = "config.txt";
char *tmpfile = "tmp";
int ret = decode(filename, tmpfile);
if (ret) return ret;
unsigned int len = getFileLength(tmpfile);
ConfigInfo *configInfos = NULL;
parseFile(tmpfile, len, &configInfos);
// 测试数据
printf("英雄ID: %s\n", getInfoByKey("heroID", configInfos, len));
printf("英雄姓名: %s\n", getInfoByKey("heroName", configInfos, len));
printf("英雄攻击力: %s\n", getInfoByKey("heroAtk", configInfos, len));
printf("英雄防御力: %s\n", getInfoByKey("heroDef", configInfos, len));
printf("英雄简介: %s\n", getInfoByKey("heroInfo", configInfos, len));
freeConfigInfo(&configInfos);
remove(tmpfile);
system("pause");
return 0;
}
#endif
在该程序中我们获取到了加密文件filename
,并且调用了解密模块将该文件转化为了明文文件tmpFile
,之后对tmpFile
进行操作进行文件读写解析等,最后我们使用remove()
函数,目的是删除掉解密出来的明文文件tmpFile
,这样在程序运行结束不会保留明文文件。