柔性数组的特点
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求这种结构体,我们进行编译
最终的结果是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求出的结构体的大小(不包括柔性数组),第二种是为柔性数组申请的动态内存的大小。
当前代码的内存布局图像:
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;
}
还有另一种写法:
这种写法是这样的:因为我们之前设计的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函数,对应的内存空间的布局是这样的:
我们可以发现,大量使用malloc函数会产生内存碎片,内存碎片会导致内存利用率不高。
下一个章节:c语言文件操作
第一个问题
本章讨论的就是数据文件。
操作文件的基本工程
FILE本质上是一个结构体。
FILE转到定义
c语言中打开文件的函数叫做fopen,当打开文件时,我们会再内存中创建一个文件信息区,并把FILE*F 也就是文件信息区的起始地址返回。
这里的意思是我们通过文件指针能够找到对应的文件信息区,进而找到相关联的文件。
当我们要打开一个文件时:
表示打开文件test.txt,以读的形式打开。
fopen函数实现的是以下的步骤
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;
}
需要注意的是:\在这里会变成转义字符,所以我们用两个\。
文件的顺序读写
为什么文件操作结束后要关闭文件?
答:文件也相当与资源,我们打开一个文件就是打开一个资源,一个程序能打开资源的数目是有限的。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
第一个参数表示字符的ASCII码值,第二个参数表示把字符放在什么文件位置。
我们对这段函数进行分析:我们首先使用fopen打开文件,打开的方式是以写的形式,返回一个FILE*的指针pf指向该文件,打开文件是有可能失败的,失败的时候,返回的是NULL。当失败的时候,我们打印出对应的错误信息,退出函数。调用函数fputc,也就是字符输出函数,我们把‘a’输出到对应的pf所指向的文件中去,然后使用关闭文件的函数fclose,这个函数和free非常类似,我们用完之后要把其置为空指针。
接下来,我们实验读的写法:
#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;
}
如果读完所有的数据的话,我们会返回一个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;
}
如何写一行数据呢?
函数作用:写字符串到文件
第一个参数是我们写的字符串,第二个参数是文件地址。
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;
}
接下来是读一行数据:
第一个参数表示要读的字符串的地址,第二个参数表示读几位(这里包含\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
打印对应的错误信息。
如何实现
int main()
{
FILE*pf = fopen("text.wet", "r");
if (pf == NULL)
{
perror("fopen:");
}
接下来,我们介绍fprintf函数:
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;
}
接下来,我们以读的形式写一下函数:
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
所以我们调用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;
}
接下来,我们介绍用二进制进行写入的方法
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;
}
接下来,我们尝试用二进位制的形式读
fread函数
读流中的数据块
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;
}
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;
}
我们进行编译,对应的结果
注意:这里的结果是把结构体s对应的格式化数据转化成了”zhangsan 20 55.500000“字符串。
sscanf函数:
从字符串中读取格式化数据
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”中的前两个字符,我们先手动在文件中进行写入。
我们在文件中写入abcdef 。因为我们的代码能够读取该文件的前两个字符,所以打印出来的结果为
a
b
但是假如我们不想打印a,b。我们想直接打印c的话,该怎么办呢?
这时候,fseek函数就派上用场了
这三个参数分别是流,偏移量,起始位置。
fseek函数的第三个参数有三种取值。第一种是为文件的起始位置。第二种是文件的当前位置。
第三种是文件的结束位置。
这时候,我们发现第二个参数偏移量和第三个参数起始位置也是有关系的,假如起始位置在文件的起始位置处的话,我们需要偏移两个单位,假如起始位置在文件的结束位置的话,我们需要偏移负4个单位。
假如我们读完c想要读f,还是这时候有三种方法:第一种是起始位置的文件的起始位置,这时候我们需要偏移4个单位。第二种是起始位置不变,也就是c的位置,我们需要偏移2个单位。第三种是文件的结束位置,我们需要偏移-1单位,读取f。
注意:fseek只是定位,读取的时候,还是要和fgetc进行配合使用的
fseek(pf, SEEK_SET, 2);
int ch = fgetc(pf);
那么,如何求取偏移量呢?
我们引入函数ftell
返回的是文件指针相对于起始位置的偏移量。
当我们的文件指针进行不断定位,位置已经不明确的时候,我们介绍函数rewind
rewind可以让文件指针回到文件的起始位置。