2022-07-16-106期-文件操作

柔性数组的特点

C99中,结构体中的最后一个元素允许是未知大小的数组,这种数组叫做柔性数组。

 柔性数组首先必须是一个结构体中的成员变量。

typedef struct st_type
{
	int i;
	int a[0];
}type_a;

这里的a[0]表示的就是数组是未知大小的数组,上面的int a[0]叫做柔性数组成员。

 

上面这种写法只能在部分编译器上使用,在其他编译器上,我们可以这样写。

typedef struct st_type
{
	int i;
	int a[];
}type_a;

 

柔性数组的特点

1:结构体中柔性数组的前面必须至少有一个其他成员

例如

typedef struct st_type
{
	int a[];
}type_a;

我们的这种写法就是错误的。

原因是:柔性数组前面必须至少有一个其他成员。

 

2:sizeof返回的这种结构的大小不包括柔性数组的内存。

#include<stdio.h>
struct s
{
	int b;
	int a[];
};
int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

我们这里sizeof求这种结构体,我们进行编译

406151bbb9184cf9b84a4be9613066db.png

 最终的结果是4,没有包含柔性数组的大小

 

3:包含柔性数组成员的结构体用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

struct s
{
	int b;
	int a[];
};
int main()
{
	struct s* ps = (struct s*)malloc(sizeof(struct s) + 40);
	return 0;
}

我们使用malloc函数进行内存分配包含柔性数组的结构体时,应该包含两部分:第一部分是sizeof求出的结构体的大小(不包括柔性数组),第二种是为柔性数组申请的动态内存的大小。

 

当前代码的内存布局图像:

3c8fa15b10cd47c4b5a419e6e9f91887.png

4代表的是结构体所占空间的大小(不带柔性数组),40是柔性数组所占空间的大小。

 完整的柔性数组的使用应该这样写

#include<stdlib.h>
#include<stdio.h>
struct s
{
	int b;
	int a[];
};
int main()
{
	struct s* ps = (struct s*)malloc(sizeof(struct s) + 40);
	ps->b = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->a[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", ps->a[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

 

那么,我们柔性数组的柔性体现在哪里呢?

答:我们这里使用的是动态内存,所以当内存出现不足时,我们可以使用realloc函数进行追加动态空间:

完整的写法如下

#include<stdlib.h>
#include<stdio.h>
struct s
{
	int b;
	int a[];
};
int main()
{
	struct s* ps = (struct s*)malloc(sizeof(struct s) + 40);
	if (ps = NULL)
	{
		return 1;
	}
	ps->b = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->a[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", ps->a[i]);
	}
	struct s*ptr=(struct s*)realloc(ps, sizeof(struct s) + 80);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	free(ps);
	ps = NULL;
	return 0;
}

 

还有另一种写法:

6b730d7444914ba7b09d829aafdc0148.png

这种写法是这样的:因为我们之前设计的n和arr都是在堆区上进行动态内存分配的,所以这里也要把n和arr放在堆区上。

struct S
{
	int n;
	int *arr;
};
int main()
{
	struct S*ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		return 1;
	}
	ps->n = 100;
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", ps->arr[i]);
	}
	int*ptr=(int*)realloc(ps->arr, 80);
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	free(ps->arr);
	free(ps);
	ps = NULL;
	return 0;
}

 

这两种方法第一种更好,原因是什么?

答:第一种只调用了一次malloc函数,只需要free释放一次空间,第二种需要调用两次malloc函数,需要free释放两次空间,就容易导致内存泄漏。

2:我们之前讲过,当我们大量使用malloc函数,对应的内存空间的布局是这样的:

8ac9acb8a3124c33839fc717fa62bed9.png

 我们可以发现,大量使用malloc函数会产生内存碎片,内存碎片会导致内存利用率不高。

 

 

下一个章节:c语言文件操作

586444f2fc2042cab3f8aa3a84df214b.png

 

 第一个问题906405e983754d159c1a53b65f9f428c.png

 8c75af56c4a24064bf8c74a9c84faca1.png

 b127285bb6cb413b94bf736eaa457a84.png

 aa4f80b67a194b7c970b23b5794f7a1d.png

 2d0bea6c58ca4d8284997bb472841872.png

 

本章讨论的就是数据文件。

 27a7bf942aa34cd6979d15f1c2c4abbd.png

 操作文件的基本工程

f43f6b097ef54af8b41b9d6f83f5b89e.png

 

9fcc41e882384bdd913993758c9a2575.png

 e895b4da7d224259945995d09365aaf8.png

 FILE本质上是一个结构体。

023f73c4451e486cac0f281618c67d96.png

 

FILE转到定义

8a41de95537440f7b51e42d87e585b40.png

 

c语言中打开文件的函数叫做fopen,当打开文件时,我们会再内存中创建一个文件信息区,并把FILE*F  也就是文件信息区的起始地址返回。

 

0cf8ca0534c94326ad1388d31d3614a6.png

 

 

77963d93a7544997a2fc255d98e83bab.png

 这里的意思是我们通过文件指针能够找到对应的文件信息区,进而找到相关联的文件。

 

 

c9fe7f31e7744893a3dcf9612e207f0f.png

 

fa01e6d87145499889130021ce25564d.png

 

 当我们要打开一个文件时:

 

 2244422c9acc47e79f23f5b64366013d.png

 

 表示打开文件test.txt,以读的形式打开。

fopen函数实现的是以下的步骤

05114202d23c4ea3b54d79d596099c36.png

 fopen函数打开失败时,会返回空指针

完整的写法

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	FILE*pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
	}
	//读文件

	//结束文件
	fclose(pf);
	pf = NULL;
	return 0;
}

fclose是关闭文件,和free十分相似,用完之后,都需要置为空指针。

 

假如我们想打开别的路径下的文件,例如桌面上的文件。

#include<string.h>
#include<errno.h>
int main()
{
	FILE*pf = fopen("C:\\Users\\ASUS\\Desktop\\test.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//读文件
	
	//结束文件
	fclose(pf);
	pf = NULL;
	return 0;
}

需要注意的是:\在这里会变成转义字符,所以我们用两个\。

 

 

c8a72ca86acf4a27a16a31d8d4a099e4.png

 

文件的顺序读写

2bcb696f99d0414fa12013481fd6b362.png

 

 

为什么文件操作结束后要关闭文件?

答:文件也相当与资源,我们打开一个文件就是打开一个资源,一个程序能打开资源的数目是有限的。2:我们在写文件后,假如没有关闭,可能会导致丢失数据。

 

如何读字符和写字符呢?

首先,写字符。

int main()
{
	FILE*pf = fopen("test.wet", "w");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	fputc('a', pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

我们先看对应的函数 fputc

971df08fe34841d4a0a01814db8c7e7b.png

 第一个参数表示字符的ASCII码值,第二个参数表示把字符放在什么文件位置。

我们对这段函数进行分析:我们首先使用fopen打开文件,打开的方式是以写的形式,返回一个FILE*的指针pf指向该文件,打开文件是有可能失败的,失败的时候,返回的是NULL。当失败的时候,我们打印出对应的错误信息,退出函数。调用函数fputc,也就是字符输出函数,我们把‘a’输出到对应的pf所指向的文件中去,然后使用关闭文件的函数fclose,这个函数和free非常类似,我们用完之后要把其置为空指针。

 

 

接下来,我们实验读的写法:

7f04aac2d0714a0cada5f15d49e66e8e.png

 

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	int*pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	char i = 0;
	for (i = 'a'; i < 'z'; i++)
	{
		fputc(i, pf);
	}
	int ch=fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

这相当于读一次的结果,假如要读多次的话:

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	int*pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	char i = 0;
	for (i = 'a'; i < 'z'; i++)
	{
		fputc(i, pf);
	}
	int ch=fgetc(pf);
	printf("%c", ch);
	ch = fgetc(pf);
	printf("%c", ch);
	ch = fgetc(pf);
	printf("%c", ch);
	ch = fgetc(pf);
	printf("%c", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

 

d46d6ba2ee744af69d491db900046475.png

 如果读完所有的数据的话,我们会返回一个EOF

假如我们要把对应的文件全部读出来。

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main()
{
	int*pf = fopen("text.txt", "w");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	char i = 0;
	for (i = 'a'; i < 'z'; i++)
	{
		fputc(i, pf);
	}
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c ", ch);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

 

 

如何写一行数据呢?

c047dc2b0a6048ba8d905d5776943646.png

 函数作用:写字符串到文件

第一个参数是我们写的字符串,第二个参数是文件地址。

int main()
{
	FILE*pf = fopen("text.wet", "w");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 1;
	}
	fputs("hello", pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

接下来是读一行数据:

74554669ad0948f9b78d748789a307b8.png

 第一个参数表示要读的字符串的地址,第二个参数表示读几位(这里包含\0),第三个参数表示读的文件的位置。

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	FILE*pf = fopen("text.wet", "r");
	if (pf == NULL)
	{
		printf("%s", strerror(errno));
		return 1;
	}
	char arr[20];
	fgets(arr, 5, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

注意:假如文件中保存的字符串为hello,我们这里只能打印出hell,因为\0会额外占一个空间。

这个函数的特点是:假如读取成功,返回arr,也就是字符串的首地址,假如读取失败,返回的是空指针。

 

我们介绍一个函数perror

ce3722684a114dd0a2b83b356c47d264.png

 打印对应的错误信息。

如何实现

int main()
{
	FILE*pf = fopen("text.wet", "r");
	if (pf == NULL)
	{
		perror("fopen:");
	}

 

 

 

接下来,我们介绍fprintf函数:

4dc41f2084c3444f9b1cbd98519e66f6.png

 fprintf表示的是把我们的格式化信息转化为字符串打印到我们的文件中去。

我们用结构体的方式进行书写:

struct S
{
	char arr[10];
	int age;
	float score;
};
int main()
{
	struct S s = { "zhangsan", 25, 50.5f };
	FILE*pf = fopen("test.txt", 'w');
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fprintf(pf, "%s %d %f", s.arr, s.age, s.score);
	fclose(pf);
	pf = NULL;
	return 0;
}

接下来,我们以读的形式写一下函数:

aa4899a8c8e54f4998b439495d3b5626.png

 

struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { 0 };
	FILE*pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen:");
	}
	fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
	fclose(pf);
	pf = NULL;
	return 0;
}

fscanf和fprintf相较于scanf和printf都是第一个参数有一个pf

 

 

1e6cccc3ef8f4cae8c1638d7f6d14964.png

 

cec20dc31c094511b3600288ecf29f6b.png

 所以我们调用printf和scanf函数是不需要打开的。

 

如何使用fprintf函数把数据打印到键盘上:

struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { 0 };
	FILE*pf = fopen("text.txt", "r");
	if (pf == NULL)
	{
		perror("fopen:");
	}
	fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
	fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
	fclose(pf);
	pf = NULL;
	return 0;
}

 

 

接下来,我们介绍用二进制进行写入的方法

1bbe8cbe30264c5392fb541fb61b6f08.png

fwrite的作用是以二进位制的形式写到文件里

第一个参数代表的我们要写入的块的地址

第二个参数:我们的一个块所占字节数的大小。。

第三个参数:我们要写入几个这样的块的元素

第四个参数:我们对应的文件指针pf

struct S
{
	char name[20];
	int age;
	float score;
};
int main()
{
	struct S s = { "zhangsan", 25, 50.5f };
	FILE*pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fwrite(&s, sizeof(struct S), 1, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

 

接下来,我们尝试用二进位制的形式读

b99bc3489ce044a491fc1dbd7e72b2f9.png

 fread函数0406106bc92d4534914d151d7f925df7.png

读流中的数据块

struct S
{
	char name[20];
	int age;
	float f;
};
int main()
{
	struct S s = { 0 };
	FILE*pf = fopen("test.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fread(&s, sizeof(struct S), 1, pf);
	fclose(pf);
	pf = NULL;
	return 0;
}

 

 

f0a9fa5ba12e425c8b0a55a840b54979.png

 c296c7ef4df941e094d6faafffedeb53.png

 

7267df99a5b74ec3bd16601a6a852dd3.png

 sprintf函数的意思是:把一个格式化的数据写在字符串中,本质是把一个格式化的数据转化成字符串。

第一个参数代表我们要把对应的字符串存储的位置。

第二个参数代表我们要转化成字符串的数据。

struct S
{
	char arr[10];
	int age;
	float score;
};
int main()
{
	struct S s = { "zhangsan", 20, 55.5f };
	char buf[100] = { 0 };
	sprintf(buf, "%s %d %f", s.arr, s.age, s.score);
	printf("%s\n", buf);
	return 0;
}

我们进行编译,对应的结果f6eb660408c1427da052457842cc9db1.png

 注意:这里的结果是把结构体s对应的格式化数据转化成了”zhangsan 20 55.500000“字符串。

 

a24ec255c16f446aa31d29f0fbb10e35.png

 

sscanf函数:

b869fd8d5af7495ba517c00d1f021cd9.png

从字符串中读取格式化数据

struct S
{
	char arr[10];
	int age;
	float score;
};
int main()
{
	struct S s = { "zhangsan", 20, 55.5f };
	struct S tmp = { 0 };
	char buf[100] = { 0 };
	sprintf(buf, "%s %d %f", s.arr, s.age, s.score);
	printf("%s\n", buf);
	sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.score));
	return 0;
}

 总结:这里的sprintf的意思是把结构体s的格式化数据转换为字符串到buf

sscanf的意思是从字符串buf获取格式化数据到tmp中。

 

接下来,我们讲一个函数,fseek

首先,我们写一串代码:

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<errno.h>
int main()
{
	FILE*pf = fopen("test.txt", "w");
	if (pf== NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int ch = fgetc(pf);
	printf("%c\n", ch);
	ch = fgetc(pf);
	printf("%c\n", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

这个函数是读取文件“test.txt”中的前两个字符,我们先手动在文件中进行写入。

5c8249af900f477ea044576938cbd033.png

我们在文件中写入abcdef 。因为我们的代码能够读取该文件的前两个字符,所以打印出来的结果为

a

b

但是假如我们不想打印a,b。我们想直接打印c的话,该怎么办呢?

这时候,fseek函数就派上用场了

bdcc36086c164fe99b801af6ee2b4d85.png

 这三个参数分别是流,偏移量,起始位置。

e85cb8603a9344a68270b630867c065b.png

fseek函数的第三个参数有三种取值。第一种是为文件的起始位置。第二种是文件的当前位置。

第三种是文件的结束位置。

 

这时候,我们发现第二个参数偏移量和第三个参数起始位置也是有关系的,假如起始位置在文件的起始位置处的话,我们需要偏移两个单位,假如起始位置在文件的结束位置的话,我们需要偏移负4个单位。

假如我们读完c想要读f,还是这时候有三种方法:第一种是起始位置的文件的起始位置,这时候我们需要偏移4个单位。第二种是起始位置不变,也就是c的位置,我们需要偏移2个单位。第三种是文件的结束位置,我们需要偏移-1单位,读取f。

 

注意:fseek只是定位,读取的时候,还是要和fgetc进行配合使用的

	fseek(pf, SEEK_SET, 2);
	int ch = fgetc(pf);

 

那么,如何求取偏移量呢?

我们引入函数ftell

aca38fe257dd4849a937a38470833d7c.png

 返回的是文件指针相对于起始位置的偏移量。

 

 

当我们的文件指针进行不断定位,位置已经不明确的时候,我们介绍函数rewind

1396949bcf2f4809a2ea58a0dc46925e.png

 

rewind可以让文件指针回到文件的起始位置。

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值