结构体
结构体类型 -- C 语言支持的, 可由程序员自定义的数据类型
定义三个术语 -- 结构, 对象, 属性
结构:
将多个数据块集合到一起的数据类型.
各个数据块可以是不同的数据类型( int, char, ... , 包括结构).
对象:
结构的特定实例, 如
- int x; /* int 是整型, x 是数据类型 int 的特定实例 */
- my_st y; /* my_st 是一种自定义的结构, y 是数据类型 my_st 的特定实例 */
属性:
一个结构整合了多个数据块, 每个数据块就是属性, 存储着关于对象的信息.
结构描述对象所拥有的属性.
一个对象有着特定的属性值, 由此与同结构的不同对象相互区别.
对象存储在内存中, 而结构没有固定的属性值, 不存储于内存中.
结构体的使用
- /* point.h */
- #ifndef POINT_H
- #define POINT_H 1
- typedef struct {
- int x, y, z;
- } Point;
- #endif
- /* point.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- int main(int argc, char ** argv) {
- Point p1;
- p1.x = 3;
- p1.y = 6;
- p1.z = 9;
- printf("p1: ( %i, %i, %i )\n", p1.x, p1,y, p1.z);
- /*
- p1: ( 3, 6, 9)
- */
- return EXIT_SUCCESS;
- }
对象支持赋值运算
- /* point2.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- int main(int argc, char ** argv) {
- Point p1;
- p1.x = 3;
- p1.y = 6;
- p1.z = 9;
- Point p2 = { -1, -2, -3 };
- printf("p1: ( %i, %i, %i )\n", p1.x, p1.y, p1.z);
- printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
- /*
- p1: ( 3, 6, 9 )
- p2: ( -1, -2, -3 )
- */
- p2 = p1;
- printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
- /*
- p2: ( 3, 6, 9 )
- */
- p1.x++;
- p2.x--;
- printf("p1: ( %i, %i, %i )\n", p1.x, p1.y, p1.z);
- printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
- /*
- p1: ( 4, 6, 9 )
- p2: ( 2, 6, 9 )
- */
- return EXIT_SUCCESS;
- }
尽管结构体类型的变量支持赋值运算符 "=", 但他并没有 内置类型 的所有属性.
如, 无法使用 "==" 或 "!=" 去比较两个 Point 对象是否相等.
要对两个 Point 对象做比较, 需要写一个函数分别比较两个对象的每个属性.
对象作为函数的 argument
Point 对象作为函数的 argument 进行传递时, 与传递其他类型一样, 复制然后传递.
- /* point3_a.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- void printPt(Point p) {
- printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
- }
- int main(int argc, char ** argv) {
- Point p1 = { 3, 6, 9 };
- printPt(p1);
- /*
- Point: ( 3, 6, 9 )
- */
- return EXIT_SUCCESS;
- }
怎么证明是复制, 不是引用?
- /* point3_b.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- void printPt(Point p) {
- printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
- }
- void changePt(Point p) {
- p.x++;
- p.y++;
- p.z++;
- printPt(p);
- }
- int main(int argc, char ** argv) {
- Point p;
- p.x = 3;
- p.y = 6;
- p.z = 9;
- printPt(p);
- /*
- Point: ( 3, 6, 9 )
- */
- changePt(p);
- /*
- Point: ( 4, 7, 10 )
- */
- printPt(p);
- /*
- Point: ( 3, 6, 9 )
- */
- return EXIT_SUCCESS;
- }
借助指针, 可以实现在函数内部改变对象的属性, 并在函数返回后继续保持.
- /* point_ptr.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- void printPt(Point p) {
- printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
- }
- void changePt(Point * p) {
- p->x++;
- p->y++;
- p->z++;
- }
- int main(int argc, char ** argv) {
- Point pt;
- pt.x = 3;
- pt.y = 6;
- pt.z = 9;
- printPt(pt);
- /*
- Point: ( 3, 6, 9 )
- */
- changePt(&pt);
- printPt(pt);
- /*
- Point: ( 4, 7, 10 )
- */
- return EXIT_SUCCESS;
- }
"->" 取左侧变量存储的地址值指向的属性, p->x 和 (*p).x 是一样的.
"->" 只能和指向结构的指针连用, 其他情况用不合法.
- 当 p 的值是一个对象的地址, 用 p->x
- 当 p 是一个对象, 不是地址, 用 p.x
在堆内存中创建和销毁对象
构造函数, 创建并初始化一个新对象.
使用构造函数, 可以使函数更加易懂.
如果函数有三个 argument, 就提醒程序员创建的对象 Point 包含 3 个属性.
函数保证所有属性都被初始化, 不易出错.
- /* point_malloc.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include "point.h"
- Point * Point_construct(int a, int b, int c) {
- Point * p;
- p = malloc(sizeof(Point));
- if ( p == NULL ) {
- fprintf(stderr, "Error: malloc() failed\n");
- return NULL;
- }
- p->x = a;
- p->y = b;
- p->z = c;
- return p;
- }
- void Point_destruct(Point * p) {
- free(p);
- }
- void printPt(Point * p) {
- printf("Point: ( %i, %i, %i )\n", p->x, p->y, p->z);
- }
- int main(int argc, char ** argv) {
- Point * p1;
- p1 = Point_construct(3, 6, 9);
- if ( p1 == NULL )
- return EXIT_FAILURE;
- printPt(p1);
- /*
- Point: ( 3, 6, 9 )
- */
- Point_destruct(p1);
- return EXIT_SUCCESS;
- }
对象有属性是指针
当对象的属性是指针时, 需要特别注意内存是怎么分配和释放的.
如果不小心, 容易造成 内存泄露 或 重复释放相同内存 等问题.
如果对象有属性是指针, 通常要用到下面 4 个函数:
- 构造函数 为属性分配内存并赋值
- 析构函数 释放属性内存
- 代替 "=" 的复制构造函数
- 代替 "=" 的赋值函数
利用已存在的对象创建新对象, 又称克隆.
新对象的属性指向调用 malloc 分配得到的堆内存.
调整通过 构造函数 或 复制构造函数 生成的新对象.
因为对象已经过构建, 对象的属性已经存储在堆内存中, 必须先释放这部分内存再分配新内存.
- /* person.h */
- /*
- 人名长度未知, 用字符指针表示
- 如果名字用固定长度创建, 当名字比较短时, 浪费空间
- */
- #ifndef PERSON_H
- #define PERSON_H 1
- typedef struct {
- int year;
- int month;
- int date;
- char * name;
- } Person;
- Person * Person_construct(int y, int m, int d, char * name);
- void Person_destruct(Person * p);
- Person * Person_copy(Person * p);
- Person * Person_assign(Person * p1, Person * p2);
- void Person_print(Person * p);
- #endif
- /* person.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "person.h"
- Person * Person_construct(int y, int m, int d, char * pname) {
- Person * p;
- p = malloc(sizeof(Person));
- if ( p == NULL ) {
- fprintf(stderr, "Error: malloc() failed\n");
- return NULL;
- }
- p -> year = y;
- p -> month = m;
- p -> date = d;
- p -> name = malloc(sizeof(char) * (strlen(pname) + 1));
- if ( p -> name == NULL ) {
- fprintf(stderr, "Error: malloc() failed\n");
- free(p);
- return NULL;
- }
- strcpy(p -> name, pname);
- return p;
- }
- void Person_destruct(Person * p) {
- free(p -> name);
- free(p);
- }
- /* Person_copy 通过动态内存分配创建一个新对象 */
- Person * Person_copy(Person * p) {
- return Person_construct(p->year, p->month, p->date, p->name);
- }
- /* Person_assign 在复制属性之前需要先释放属性已占用的内存 */
- Person * Person_assign(Person * p1, Person * p2) {
- if ( strlen(p1->name) < strlen(p2->name) )
- free(p1->name);
- p1->year = p2->year;
- p1->month = p2->month;
- p1->date = p2->date;
- p1->name = strdup(p2->name);
- return p1;
- }
- void Person_print(Person * p) {
- printf("Name: %s ... ", p -> name);
- printf("Date of Birth: %i/%i/%i\n", p -> year, p -> month, p -> date);
- }
- /* person_test.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "person.h"
- int main(int argc, char ** argv) {
- Person * p1 = Person_construct(1989, 1, 1, "Amy");
- Person * p2 = Person_construct(1991, 12, 31, "Jennifer");
- /*
- 复制构造函数 Person_copy 为新对象分配了独立的空间
- 改变新对象的属性不会影响其他对象, 这种方式称为深拷贝
- */
- Person * p3 = Person_copy(p1);
- Person_print(p1);
- Person_print(p2);
- Person_print(p3);
- /*
- Name: Amy ... Date of Birth: 1989/1/1
- Name: Jennifer ... Date of Birth: 1991/12/31
- Name: Amy ... Date of Birth: 1989/1/1
- */
- /*
- 赋值函数 Person_assign 对原有对象作出修改
- 这里, p3 的原名字是 "Amy"
- 当 p2 被复制到 p3, p3->name 没有足够的空间存储新名字 "Jennifer"
- 所以, 函数先把 p3->name 释放, 然后调用 strdup 重新申请内存
- */
- p3 = Person_assign(p3, p2);
- Person_print(p3);
- /*
- Name: Jennifer ... Date of Birth: 1991/12/31
- */
- Person_destruct(p1);
- Person_destruct(p2);
- Person_destruct(p3);
- return EXIT_SUCCESS;
- }
深拷贝重新申请内存, 每个对象有自己独立的内存空间.
浅拷贝允许多个对象的属性同时指向一块相同的地址, 有时这样做有独特的好处, 也可以节省空间.
当程序变得复杂时, 建立一个包含所有属性的结构是不现实的, 混乱的.
应该将相似的属性建立一个结构, 再在别的结构中调用这个结构, 即层次化结构.
层次化有助于程序的组织.
文件中写入和读取对象
数据保存在内存中是易失的, 写入文件后可以长时间保存.
操作文件时, 常用的几个函数:
操作 | 文本文件 | 二进制文件 |
---|---|---|
打开 | fopen | fopen |
关闭 | fclose | fclose |
写入 | fprintf | fwrite |
读取 | fgetc, fgets, fscanf | fread |
调用 fopen 打开文件, 继而可以读写文件.
当不再需要读写文件时候, 记得调用 fclose 关闭文件, 释放资源.
fwrite 用于二进制数据的写入, 有 4 个参数: 对象地址, 对象大小, 对象数量, FILE 指针.
fwrite 的返回值是成功写入文件的对象的数量, 该值可能和第三个参数不同, 如硬盘空间不足时只能写入部分数据的情况.
程序每次调用 fwrite, 最好都判断一下二者是否相等.
- /* point_file.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include "point.h"
- Point Point_construct(int a, int b, int c) {
- Point pt;
- pt.x = a; pt.y = b; pt.z = c;
- return pt;
- }
- void Point_print(char * name, Point pt) {
- printf("%s: ( %i, %i, %i )\n", name, pt.x, pt.y, pt.z);
- }
- void Point_wt(char * filename, Point pt) {
- FILE * fp;
- fp = fopen(filename, "w");
- if ( fp == NULL ) {
- fprintf(stderr, "Error: fopen() failed\n");
- exit(EXIT_FAILURE);
- }
- fprintf(fp, "%i %i %i", pt.x, pt.y, pt.z);
- fclose(fp);
- }
- Point Point_rt(char * filename) {
- Point pt = Point(0, 0, 0);
- FILE * fp;
- fp = fopen(filename, "r");
- if ( fp == NULL ) {
- fprintf(stderr, "Error: fopen() failed\n");
- exit(EXIT_FAILURE);
- }
- if ( fscanf(fp, "%i %i %i", &pt.x, &pt.y, &pt.z) != 3 ) {
- fprintf(stderr, "Error: error with fscanf()\n");
- }
- fclose(fp);
- return pt;
- }
- void Point_wb(char * filename, Point pt) {
- FILE * fp;
- fp = fopen(filename, "wb");
- if ( fp == NULL ) {
- fprintf(stderr, "Error: fopen() failed\n");
- exit(EXIT_FAILURE);
- }
- if ( fwrite(&pt, sizeof(Point), 1, fp) != 1 ) {
- fprintf(stderr, "Error: error with fwrite()\n");
- }
- fclose(fp);
- }
- Point Point_rb(char * filename) {
- FILE * fp;
- Point pt;
- fp = fopen(filename, "rb");
- if ( fp == NULL ) {
- fprintf(stderr, "Error: fopen() failed\n");
- exit(EXIT_FAILURE);
- }
- if ( fread(&pt, sizeof(Point), 1, fp) != 1 ) {
- fprintf(stderr, "Error: error with fread()\n");
- }
- fclose(fp);
- return pt;
- }
- int main(int argc, char ** argv) {
- Point p1 = Point_construct(1, 10, 100);
- Point p2 = Point_construct(2, 20, 200);
- Point_print("p1", p1);
- Point_print("p2", p2);
- /*
- p1: ( 1, 10, 100 )
- p2: ( 2, 20, 200 )
- */
- Point_wt("point_txt.dat", p1);
- p2 = Point_rt("point_txt.dat");
- Point_print("p1", p1);
- Point_print("p2", p2);
- /*
- p1: ( 1, 10, 100 )
- p2: ( 1, 10, 100 )
- */
- p1 = Point_construct(4, 16, 256);
- p2 = Point_construct(5, 25, 625);
- Point_print("p1", p1);
- Point_print("p2", p2);
- /*
- p1: ( 4, 16, 256 )
- p2: ( 5, 25, 625 )
- */
- Point_wb("point_bin.dat", p1);
- p2 = Point_rb("point_bin.dat");
- Point_print("p1", p1);
- Point_print("p2", p2);
- /*
- p1: ( 4, 16, 256 )
- p2: ( 4, 16, 256 )
- */
- return EXIT_SUCCESS;
- }
文本文件和二进制文件的比较
当数据存为文本文件, 可以从终端用命令读取, 甚至可以通过文本编辑器查看和编辑.
但 Point_wt 和 Point_rt 必须一个个的处理属性, 处理顺序必须相同.
需要添加属性时, Point_wt 和 Point_rt 都要进行调整, 增加了出错的风险.
而 Point_wb 和 Point_rb 会自动处理属性的顺序问题, 添加属性到 Point, 无需任何调整, 因为 sizeof 会自动更新大小.
二进制文件的缺点在于:
- 文件不能直观的编辑和查看
- 文件可能因平台不同而有差异, 不同平台生成的二进制数据的大小和格式可能不同