C语言--结构体&文件
导言
假设我们现在要处理一堆学生数据信息,如果使用前面学过的数据类型来表示学生信息,由于学生信息中各项内容的数据类型有所不同,因此,需要为每一项内容分别定义一个变量或数组。当要访问某个学生信息时,只能分别访问这些分离的变量或数组。这会给操作带来很多不便之处。更重要的是,这几项内容同属于某个学生,它们之间是有内在联系的,为每一项内容分别定义变量或数组的方法割裂了它们之间的关联关系。请思考,我们能不能用某种方法,能够将把有内在联系的不同类型的数据汇聚成一个整体,使它们相互关联?
结构类型是一种允许我们把一些数据分量聚合成一个结构中,它包含的每个数据分量都有名字,这些数据分量称为结构成员或者结构分量,结构成员可以是C语言中的任意变量类型,程序员可以使用結构类型来创建适合于问题的数据聚合。像数组和指针一样,结构也是一种构造数据类型,它与数组的区别在于:数组中所有元素的数据类型必须是相同的,而结构中各成员的数据类型可以不同。因此,在学习结构时要注意与数组进行类比,这样更助于理解与掌握。
结构体
定义
在C语言中,整型、实型等基本数据类型是被系统预先定义好了的,我们用其直接定义变量。而结构类型是由用户根据需要,按规定的格式自行定义的数据类型。结构类型定义的一般形式为:
struct tag {
member-list
member-list
member-list
...
};
tag 是结构体标签,即结构名,member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。
- 关键字struct和它后面的结构名一起组成一个新的数据类型名。结构的定义以分号结束,这是因为C语言中把结构的定义看做一条语句。
嵌套定义
如果把数组比喻成整齐的队列,那么结构体就可以被比喻为一个团队,这个团队是一个包容的团队,它把不同类型,不同功能的数据紧密联系为一个整体,让这些本来毫无内在联系的数据有了关联,共同来为我们的数据处理提供方便。在实际生活中,一个较大的实体可能由多个成员构成,而这些成员中有些又有可能由一些更小的成员构成。一个结构的成员是由合法的C语言数据类型和变量名组成的,进一步地说,在定义结构成员时所用的数据类型也可以是结构类型,这样就形成了结构类型的嵌套。
- 在定义嵌套的结构类型时,必须先定义成员的结构类型,再定义主结构类型。
结构变量定义
一般情况下,结构名,结构成员定义,结构变量在一次结构体定义中,至少要出现两部分:
struct
{
int a;
char b;
double c;
} s1;
无类型名定义,此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c,同时又声明了结构体变量s1,但是这个结构体并没有标明其标签。
struct SIMPLE
{
int a;
char b;
double c;
};
struct SIMPLE t1, t2[20], *t3;
单独定义,此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c,结构体的标签被命名为SIMPLE,没有声明变量。在定义结构体之后,用SIMPLE标签的结构体,另外声明了变量t1、t2、t3。
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
混合定义,在定义结构体类型的同时定义了结构变量。
- 要注意的是,由于无类型名定义时,没有给出结构名,在此定义语句后面无法再定义这个类型的其他结构变量,除非把定义过程再写一遍。一般情况下,除非变量不会再增加,还是建议采用前两种结构变量的定义形式。
初始化
和其它类型变量一样,对结构体变量可以在定义时指定初始值。例如:
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
结构体变量的使用
在上文,我把结构体比喻为一个团队,那么当我们要找出这个团队的某位成员时,就需要先把这位成员叫出来,这个时候我们就可以把对结构体变量的使用比喻为点名,这是我们就需要通过某种方式来实现给结构体点名的效果。
为了访问结构的成员,我们使用成员访问运算符“.”。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号,格式为:
结构变量名.结构成员名
在C语言中,对结构变量成员的使用方法和同类型的变量完全相同,而嵌套结构成员的引用方法和一般成员的引用方法类似,也是采用结构成员的操作符来进行的,例如:
strcpy( Book1.title, "C Programming");
Book1.book_id = 6495407;
printf( "Book 1 title : %s\n", Book1.title);
printf( "Book 1 book_id : %d\n", Book1.book_id);
- 由于结构成员运算符的优先级属于最高级别,所以一般情况下都是优先执行,即和一般运算符混合运算时,结构成员运算符优先。
结构体变量的整体赋值
如果两个结构变量具有相同的结构变量,则允许将一个结构体变量的值直接赋给另一个结构变量。赋值时,将赋值符号右边结构变量中相应的成员都赋值给另一个结构变量,这是结构中唯一的整体操作方式。例如:
struct Books Book1;
struct Books Book2;
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
Book2 = Book1; //整体赋值
这个问题也不难理解,因为结构体在本质上也是变量,只是它是多个变量的集合体,因此它的操作和一般的变量是一样的,赋值的时候,它的本质就是把结构体变量中每一个成员变量都赋值进另一个结构体中。
- 只有相同类型的变量之间才能直接赋值。
结构变量作为函数参数
在一个由多个函数组成的C语言程序中,如果程序中含有结构类型的数据,就有可能需要用结构变量作为函数的参数或返回值,以便在函数间传递数据。例如:
void printBook( struct Books book )
{
printf( "Book title : %s\n", book.title);
printf( "Book author : %s\n", book.author);
printf( "Book subject : %s\n", book.subject);
printf( "Book book_id : %d\n", book.book_id);
}
printBook( Book1 );
结构变量作为函数参数的特点是,可以传递多个数据且参数形式较简单。但是,我们知道结构体是一个变量的集合体,因此对于成员较多的大型结构,本质上也是把结构中的每一个成员都传进函数,因此参数传递时所进行的结构数据复制使得效率较低。
结构体数组
我们还是沿用上面的比喻,这就像是一支军队,我们结构体数组名就像是一个长官,这个长官下属有n个小队,而这n个小队也自然有各自的队长,结构体数组的每个单元就是一个队长,这个队长下属有很多队员,这些队员就是结构体里的各个成员了。
结构数组是结构与数组的结合体,与普通数组的不同之处在于每个数组元素都是一个结构类型的数据,包括多个成员项。结构数组的定义方法与结构变量类似,每个数组元素都是对应结构类型的变量,这样就可以存储50个学生的信息。也可以同时对其进行初始化,其格式与二维数组的初始化。由于每个结构数组元素的类型都是结构类型,因此利用下标和“.”运算符以引用结构数组元素的成员。对结构数组元素成员的引用是通过使用数组下标与结构成员操作符“.”相结合的方式来实现的,其一般格式为:
结构数组名[下标].结构成员名
此外,由于结构数组中的所有元素都属于相同的结构类型,因此,数组元素之间可以直接赋值。
结构体数组排序
请看例题:实验9-8-结构 通讯录排序
伪代码:
定义结构体变量friends,包含成员name[10]存储姓名信息,birthday存储生日信息,telephone[17]存储电话号码数据;
定义变量number接收有几个通讯录信息;
定义变量i1, i2作循环控制变量;
定义结构体数组friends fri[11];
输入变量number;
for i1 = 0; i1 < number; i1++ do //输入多个通讯录信息,存储入结构体数组
scanf("%s %ld %s", &fri[i1].name, &fri[i1].birthday, &fri[i1].telephone);
end for
for i1 = 0; i1 < number; i1++ do
{
for (i2 = 0; i2 < number - i1 - 1; i2++ do //对结构体数组按照生日进行冒泡排序
{
if fri[i2].birthday > fri[i2+1].birthday do
{
fri[10] = fri[i2];
fri[i2] = fri[i2+1];
fri[i2+1] = fri[10];
}
end if
}
end for
end for
}
for ( i1 = 0; i1 < number; i1++) //遍历输入结构体数组
printf("%s %ld %s\n", fri[i1].name, fri[i1].birthday, fri[i1].telephone);
end for
代码实现:
结构指针
指针可以指向任何一种类型的变量,而结构体也是C语言中的一种合法变量,因此,指针也可以指向结构变量,这就是结构指针。即:结构指针就是措向结构类型变量的指针。我们可以定义结构指针变量,在指针变量中存储结构变量的地址,为了查找结构变量的地址,请把 & 运算符放在结构名称的前面。例如:
struct Books *struct_pointer;
struct_pointer = &Book1;
结构类型的数据往往由多个成员组成,结构指针的值实际上是结构变量的首地址 即第一个成员的地址。有了结构指针的定义,我们能通过结构指针针变量间接访问它所指向的结构变量中的各个成员。为了使用指向该结构的指针访问结构的成员,我们一般使用 -> 指向运算符,例如:
struct_pointer->title;
结构指针作为函数参数
结构变量也可以作为函数参数,在参数传递时,把实参结构中的每一个成员值传递给形参结构的成员。但是,当结构成员数量众多时,在参数传递过程中就需要消耗很多空间。而使用结构指针作为函数参数只要传递一个地址,因此,这么做能极大地提高参数传递的效率。例如:
void printBook( struct Books *book )
{
printf( "Book title : %s\n", book->title);
printf( "Book author : %s\n", book->author);
printf( "Book subject : %s\n", book->subject);
printf( "Book book_id : %d\n", book->book_id);
}
struct Books Book1;
printBook( &Book1 );
共用体
我们再来做个比喻,假设你有一把瑞士军刀,这把刀集成了圆珠笔、牙签、剪刀、平口刀、开罐器、螺丝刀、镊子等,因此虽然这只是一把刀,但是你可以拿来做很多事情,要使用这些工具时,只要将它从刀身的折叠处拉出来,就可以使用。但是很明显,你不能拿瑞士军刀去砍树,因为这么做超出了瑞士军刀的能力范围。
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。共用体与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间,一个结构体变量的总长度大于等于各成员长度之和。而在共用体中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。这里所谓的共享不是指把多个成员同时装入一个共用体变量内,而是指该共用体变量可被赋予任一成员值,但每次只能赋一种值,如定义为一个可装入“班级”或“教研室”的共用体后,就允许赋予整型值(班级)或字符型(教研室)。要么赋予整型值,要么赋予字符型,不能把两者同时赋予它。
定义共用体
为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的,共用体占用的内存应足够存储共用体中最大的成员。
访问共用体
为了访问共用体的成员,我们使用成员访问运算符“.”。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。您可以使用 union 关键字来定义共用体类型的变量。
例如我们先定义一个共用体:
union Data
{
int i;
float f;
char str[20];
};
执行以下操作:
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
你就会看到这样的结果:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
在这里,我们可以看到共用体的 i 和 f 成员的数据不正常,这是因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们给代码稍加修改:
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
运行代码,你会看到:
data.i : 10
data.f : 220.500000
data.str : C Programming
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员,因此我们要非常注意共用体访问成员的准确性。
枚举
枚举类型用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。这种变量能设置为已经定义的一组之中的一个,有效地防止用户提供无效值。该变量可使代码更加清晰,因为它可以描述特定的值。在C语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的。
枚举语法定义格式为:
enum 枚举名 {枚举元素1,枚举元素2,……};
接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
这个看起来代码就比较繁琐了,因此我们使用枚举的方式定义:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
- 第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推,我们可以在定义枚举类型时改变枚举元素的值。
与结构体类似,我们可以通过以下三种方式来定义枚举变量:
1、先定义枚举类型,再定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
2、定义枚举类型的同时定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
3、省略枚举名称,直接定义枚举变量
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
文件
许多程序在实现过程中,依赖于把数据保存到变量中,而变量是通过内存单元存储数据的,数据的处理完全由程序控制。当一个程序运行完成或终止运行,所有变量的值不再保存。另外,一般的程序都会有数据输入与输出,如果输人输出数据量不大,通过键盘和显示器即可解决。当输入输出数据量较大时,就会受到限制,带来不便。文件是解决上述问题的有效办法,它通过把数据存储在磁盘文件中,得以长久保存。当有大量数据输入时,可通过编辑工具事先建立输入数据的文件,程序运行时将 不再从键盘输入,而从指定的文件上读人,从而实现数据一次输人多次使用。同样,当有大量数据输出时,可以将其输出到指定文件,不受屏幕大小限制,并且任何时候都可以查看结果文件。一个程序的运算结果还可以作为其他程序的输入,进行进一步加工。
文件指针
在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。定义说明文件指针的一般形式为:
FILE *指针变量标识符;
其中FILE应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在使用文件时,需要在内存中为其分配空间,用来存放文件的基本信息,给结构体类型是由系统定义的,C语言规定该类型为FILE型。
打开文件
您可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:
FILE *fopen( const char * filename, const char * mode );
在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
r+ | 打开一个文本文件,允许读写文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"。
关闭文件
关闭文件
为了关闭文件,请使用 fclose( ) 函数。函数的原型如下:
int fclose( FILE *fp );
如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。C标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。
写入文件
fputc()函数:把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。函数原型:
int fputc( int c, FILE *fp );
fputs()函数:我们可以使用这个函数来把一个以 null 结尾的字符串写入到流中,利用这个函数把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。函数原型:
int fputs( const char *s, FILE *fp );
fprintf()函数:根据指定的格式(format),向输出流(stream)写入数据(argument),函数原型:
int fprintf (FILE* stream, const char*format, [argument]);
读取文件
fgetc()函数:从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。函数原型:
int fgetc( FILE * fp );
fgets()函数:从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串,如果这个函数在读取最后一个字符之前就遇到一个换行符 '\n' 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。函数原型:
char *fgets( char *buf, int n, FILE *fp );
fscanf()函数:函数来从文件中读取字符串,但是在遇到第一个空格、换行符或制表符时,它会停止读取。函数原型:
int fscanf(FILE *fp, const char *format, ...);
feof()函数
feof是C语言标准库函数,其原型在stdio.h中,其功能是检测流上的文件结束符,如果文件结束,则返回非0值,否则返回0(即,文件结束:返回非0值,文件未结束,返回0值)。
程序实战
左转我另一篇博客C语言程序设计——成语学习系统程序
参考资料
《C语言程序设计(第3版)》——何钦铭、颜辉
菜鸟教程