结构体

结构体类型 -- C 语言支持的, 可由程序员自定义的数据类型


定义三个术语 -- 结构, 对象, 属性

结构:
将多个数据块集合到一起的数据类型.
各个数据块可以是不同的数据类型( int, char, ... , 包括结构).

对象:
结构的特定实例, 如

  1. int x; /* int 是整型, x 是数据类型 int 的特定实例 */
  2. my_st y; /* my_st 是一种自定义的结构, y 是数据类型 my_st 的特定实例 */

属性:
一个结构整合了多个数据块, 每个数据块就是属性, 存储着关于对象的信息.

结构描述对象所拥有的属性.
一个对象有着特定的属性值, 由此与同结构的不同对象相互区别.
对象存储在内存中, 而结构没有固定的属性值, 不存储于内存中.

结构体的使用


  1. /* point.h */
  2.  
  3. #ifndef POINT_H
  4. #define POINT_H 1
  5.  
  6. typedef struct {
  7. int x, y, z;
  8. } Point;
  9.  
  10. #endif
  1. /* point.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. int main(int argc, char ** argv) {
  8. Point p1;
  9. p1.x = 3;
  10. p1.y = 6;
  11. p1.z = 9;
  12. printf("p1: ( %i, %i, %i )\n", p1.x, p1,y, p1.z);
  13. /*
  14. p1: ( 3, 6, 9)
  15. */
  16. return EXIT_SUCCESS;
  17. }

对象支持赋值运算


  1. /* point2.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. int main(int argc, char ** argv) {
  8. Point p1;
  9. p1.x = 3;
  10. p1.y = 6;
  11. p1.z = 9;
  12. Point p2 = { -1, -2, -3 };
  13. printf("p1: ( %i, %i, %i )\n", p1.x, p1.y, p1.z);
  14. printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
  15. /*
  16. p1: ( 3, 6, 9 )
  17. p2: ( -1, -2, -3 )
  18. */
  19.  
  20. p2 = p1;
  21. printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
  22. /*
  23. p2: ( 3, 6, 9 )
  24. */
  25.  
  26. p1.x++;
  27. p2.x--;
  28. printf("p1: ( %i, %i, %i )\n", p1.x, p1.y, p1.z);
  29. printf("p2: ( %i, %i, %i )\n", p2.x, p2.y, p2.z);
  30. /*
  31. p1: ( 4, 6, 9 )
  32. p2: ( 2, 6, 9 )
  33. */
  34.  
  35. return EXIT_SUCCESS;
  36. }

尽管结构体类型的变量支持赋值运算符 "=", 但他并没有 内置类型 的所有属性.
如, 无法使用 "==" 或 "!=" 去比较两个 Point 对象是否相等.
要对两个 Point 对象做比较, 需要写一个函数分别比较两个对象的每个属性.

对象作为函数的 argument


Point 对象作为函数的 argument 进行传递时, 与传递其他类型一样, 复制然后传递.

  1. /* point3_a.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. void printPt(Point p) {
  8. printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
  9. }
  10.  
  11. int main(int argc, char ** argv) {
  12. Point p1 = { 3, 6, 9 };
  13. printPt(p1);
  14. /*
  15. Point: ( 3, 6, 9 )
  16. */
  17.  
  18. return EXIT_SUCCESS;
  19. }

怎么证明是复制, 不是引用?

  1. /* point3_b.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. void printPt(Point p) {
  8. printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
  9. }
  10.  
  11. void changePt(Point p) {
  12. p.x++;
  13. p.y++;
  14. p.z++;
  15. printPt(p);
  16. }
  17.  
  18. int main(int argc, char ** argv) {
  19. Point p;
  20. p.x = 3;
  21. p.y = 6;
  22. p.z = 9;
  23. printPt(p);
  24. /*
  25. Point: ( 3, 6, 9 )
  26. */
  27.  
  28. changePt(p);
  29. /*
  30. Point: ( 4, 7, 10 )
  31. */
  32.  
  33. printPt(p);
  34. /*
  35. Point: ( 3, 6, 9 )
  36. */
  37.  
  38. return EXIT_SUCCESS;
  39. }

借助指针, 可以实现在函数内部改变对象的属性, 并在函数返回后继续保持.

  1. /* point_ptr.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. void printPt(Point p) {
  8. printf("Point: ( %i, %i, %i )\n", p.x, p.y, p.z);
  9. }
  10.  
  11. void changePt(Point * p) {
  12. p->x++;
  13. p->y++;
  14. p->z++;
  15. }
  16.  
  17. int main(int argc, char ** argv) {
  18. Point pt;
  19. pt.x = 3;
  20. pt.y = 6;
  21. pt.z = 9;
  22. printPt(pt);
  23. /*
  24. Point: ( 3, 6, 9 )
  25. */
  26.  
  27. changePt(&pt);
  28. printPt(pt);
  29. /*
  30. Point: ( 4, 7, 10 )
  31. */
  32.  
  33. return EXIT_SUCCESS;
  34. }

"->" 取左侧变量存储的地址值指向的属性, p->x 和 (*p).x 是一样的.
"->" 只能和指向结构的指针连用, 其他情况用不合法.

  • 当 p 的值是一个对象的地址, 用 p->x
  • 当 p 是一个对象, 不是地址, 用 p.x

在堆内存中创建和销毁对象


构造函数, 创建并初始化一个新对象.
使用构造函数, 可以使函数更加易懂.
如果函数有三个 argument, 就提醒程序员创建的对象 Point 包含 3 个属性.
函数保证所有属性都被初始化, 不易出错.

  1. /* point_malloc.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "point.h"
  6.  
  7. Point * Point_construct(int a, int b, int c) {
  8. Point * p;
  9. p = malloc(sizeof(Point));
  10. if ( p == NULL ) {
  11. fprintf(stderr, "Error: malloc() failed\n");
  12. return NULL;
  13. }
  14. p->x = a;
  15. p->y = b;
  16. p->z = c;
  17. return p;
  18. }
  19.  
  20. void Point_destruct(Point * p) {
  21. free(p);
  22. }
  23.  
  24. void printPt(Point * p) {
  25. printf("Point: ( %i, %i, %i )\n", p->x, p->y, p->z);
  26. }
  27.  
  28. int main(int argc, char ** argv) {
  29. Point * p1;
  30. p1 = Point_construct(3, 6, 9);
  31. if ( p1 == NULL )
  32. return EXIT_FAILURE;
  33.  
  34. printPt(p1);
  35. /*
  36. Point: ( 3, 6, 9 )
  37. */
  38.  
  39. Point_destruct(p1);
  40. return EXIT_SUCCESS;
  41. }

对象有属性是指针


当对象的属性是指针时, 需要特别注意内存是怎么分配和释放的.
如果不小心, 容易造成 内存泄露 或 重复释放相同内存 等问题.

如果对象有属性是指针, 通常要用到下面 4 个函数:

  • 构造函数 为属性分配内存并赋值
  • 析构函数 释放属性内存
  • 代替 "=" 的复制构造函数
  • 利用已存在的对象创建新对象, 又称克隆.
    新对象的属性指向调用 malloc 分配得到的堆内存.

  • 代替 "=" 的赋值函数
  • 调整通过 构造函数 或 复制构造函数 生成的新对象.
    因为对象已经过构建, 对象的属性已经存储在堆内存中, 必须先释放这部分内存再分配新内存.

  1. /* person.h */
  2.  
  3. /*
  4. 人名长度未知, 用字符指针表示
  5. 如果名字用固定长度创建, 当名字比较短时, 浪费空间
  6. */
  7.  
  8. #ifndef PERSON_H
  9. #define PERSON_H 1
  10.  
  11. typedef struct {
  12. int year;
  13. int month;
  14. int date;
  15. char * name;
  16. } Person;
  17.  
  18. Person * Person_construct(int y, int m, int d, char * name);
  19. void Person_destruct(Person * p);
  20. Person * Person_copy(Person * p);
  21. Person * Person_assign(Person * p1, Person * p2);
  22. void Person_print(Person * p);
  23.  
  24. #endif
  1. /* person.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include "person.h"
  7.  
  8. Person * Person_construct(int y, int m, int d, char * pname) {
  9. Person * p;
  10. p = malloc(sizeof(Person));
  11. if ( p == NULL ) {
  12. fprintf(stderr, "Error: malloc() failed\n");
  13. return NULL;
  14. }
  15. p -> year = y;
  16. p -> month = m;
  17. p -> date = d;
  18. p -> name = malloc(sizeof(char) * (strlen(pname) + 1));
  19. if ( p -> name == NULL ) {
  20. fprintf(stderr, "Error: malloc() failed\n");
  21. free(p);
  22. return NULL;
  23. }
  24. strcpy(p -> name, pname);
  25. return p;
  26. }
  27.  
  28. void Person_destruct(Person * p) {
  29. free(p -> name);
  30. free(p);
  31. }
  32.  
  33. /* Person_copy 通过动态内存分配创建一个新对象 */
  34. Person * Person_copy(Person * p) {
  35. return Person_construct(p->year, p->month, p->date, p->name);
  36. }
  37.  
  38. /* Person_assign 在复制属性之前需要先释放属性已占用的内存 */
  39. Person * Person_assign(Person * p1, Person * p2) {
  40. if ( strlen(p1->name) < strlen(p2->name) )
  41. free(p1->name);
  42. p1->year = p2->year;
  43. p1->month = p2->month;
  44. p1->date = p2->date;
  45. p1->name = strdup(p2->name);
  46. return p1;
  47. }
  48.  
  49. void Person_print(Person * p) {
  50. printf("Name: %s ... ", p -> name);
  51. printf("Date of Birth: %i/%i/%i\n", p -> year, p -> month, p -> date);
  52. }
  1. /* person_test.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include "person.h"
  7.  
  8. int main(int argc, char ** argv) {
  9. Person * p1 = Person_construct(1989, 1, 1, "Amy");
  10. Person * p2 = Person_construct(1991, 12, 31, "Jennifer");
  11.  
  12. /*
  13. 复制构造函数 Person_copy 为新对象分配了独立的空间
  14. 改变新对象的属性不会影响其他对象, 这种方式称为深拷贝
  15. */
  16. Person * p3 = Person_copy(p1);
  17. Person_print(p1);
  18. Person_print(p2);
  19. Person_print(p3);
  20. /*
  21. Name: Amy ... Date of Birth: 1989/1/1
  22. Name: Jennifer ... Date of Birth: 1991/12/31
  23. Name: Amy ... Date of Birth: 1989/1/1
  24. */
  25.  
  26. /*
  27. 赋值函数 Person_assign 对原有对象作出修改
  28. 这里, p3 的原名字是 "Amy"
  29. 当 p2 被复制到 p3, p3->name 没有足够的空间存储新名字 "Jennifer"
  30. 所以, 函数先把 p3->name 释放, 然后调用 strdup 重新申请内存
  31. */
  32. p3 = Person_assign(p3, p2);
  33. Person_print(p3);
  34. /*
  35. Name: Jennifer ... Date of Birth: 1991/12/31
  36. */
  37.  
  38. Person_destruct(p1);
  39. Person_destruct(p2);
  40. Person_destruct(p3);
  41. return EXIT_SUCCESS;
  42. }

深拷贝重新申请内存, 每个对象有自己独立的内存空间.
浅拷贝允许多个对象的属性同时指向一块相同的地址, 有时这样做有独特的好处, 也可以节省空间.

当程序变得复杂时, 建立一个包含所有属性的结构是不现实的, 混乱的.
应该将相似的属性建立一个结构, 再在别的结构中调用这个结构, 即层次化结构.
层次化有助于程序的组织.

文件中写入和读取对象


数据保存在内存中是易失的, 写入文件后可以长时间保存.
操作文件时, 常用的几个函数:

操作文本文件二进制文件
打开fopenfopen
关闭fclosefclose
写入fprintffwrite
读取fgetc, fgets, fscanffread

调用 fopen 打开文件, 继而可以读写文件.
当不再需要读写文件时候, 记得调用 fclose 关闭文件, 释放资源.
fwrite 用于二进制数据的写入, 有 4 个参数: 对象地址, 对象大小, 对象数量, FILE 指针.
fwrite 的返回值是成功写入文件的对象的数量, 该值可能和第三个参数不同, 如硬盘空间不足时只能写入部分数据的情况.
程序每次调用 fwrite, 最好都判断一下二者是否相等.

  1. /* point_file.c */
  2.  
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <string.h>
  6. #include "point.h"
  7.  
  8. Point Point_construct(int a, int b, int c) {
  9. Point pt;
  10. pt.x = a; pt.y = b; pt.z = c;
  11. return pt;
  12. }
  13.  
  14. void Point_print(char * name, Point pt) {
  15. printf("%s: ( %i, %i, %i )\n", name, pt.x, pt.y, pt.z);
  16. }
  17.  
  18. void Point_wt(char * filename, Point pt) {
  19. FILE * fp;
  20. fp = fopen(filename, "w");
  21. if ( fp == NULL ) {
  22. fprintf(stderr, "Error: fopen() failed\n");
  23. exit(EXIT_FAILURE);
  24. }
  25. fprintf(fp, "%i %i %i", pt.x, pt.y, pt.z);
  26. fclose(fp);
  27. }
  28.  
  29. Point Point_rt(char * filename) {
  30. Point pt = Point(0, 0, 0);
  31. FILE * fp;
  32. fp = fopen(filename, "r");
  33. if ( fp == NULL ) {
  34. fprintf(stderr, "Error: fopen() failed\n");
  35. exit(EXIT_FAILURE);
  36. }
  37. if ( fscanf(fp, "%i %i %i", &pt.x, &pt.y, &pt.z) != 3 ) {
  38. fprintf(stderr, "Error: error with fscanf()\n");
  39. }
  40. fclose(fp);
  41. return pt;
  42. }
  43.  
  44. void Point_wb(char * filename, Point pt) {
  45. FILE * fp;
  46. fp = fopen(filename, "wb");
  47. if ( fp == NULL ) {
  48. fprintf(stderr, "Error: fopen() failed\n");
  49. exit(EXIT_FAILURE);
  50. }
  51. if ( fwrite(&pt, sizeof(Point), 1, fp) != 1 ) {
  52. fprintf(stderr, "Error: error with fwrite()\n");
  53. }
  54. fclose(fp);
  55. }
  56.  
  57. Point Point_rb(char * filename) {
  58. FILE * fp;
  59. Point pt;
  60. fp = fopen(filename, "rb");
  61. if ( fp == NULL ) {
  62. fprintf(stderr, "Error: fopen() failed\n");
  63. exit(EXIT_FAILURE);
  64. }
  65. if ( fread(&pt, sizeof(Point), 1, fp) != 1 ) {
  66. fprintf(stderr, "Error: error with fread()\n");
  67. }
  68. fclose(fp);
  69. return pt;
  70. }
  71.  
  72. int main(int argc, char ** argv) {
  73. Point p1 = Point_construct(1, 10, 100);
  74. Point p2 = Point_construct(2, 20, 200);
  75. Point_print("p1", p1);
  76. Point_print("p2", p2);
  77. /*
  78. p1: ( 1, 10, 100 )
  79. p2: ( 2, 20, 200 )
  80. */
  81.  
  82. Point_wt("point_txt.dat", p1);
  83. p2 = Point_rt("point_txt.dat");
  84. Point_print("p1", p1);
  85. Point_print("p2", p2);
  86. /*
  87. p1: ( 1, 10, 100 )
  88. p2: ( 1, 10, 100 )
  89. */
  90.  
  91. p1 = Point_construct(4, 16, 256);
  92. p2 = Point_construct(5, 25, 625);
  93. Point_print("p1", p1);
  94. Point_print("p2", p2);
  95. /*
  96. p1: ( 4, 16, 256 )
  97. p2: ( 5, 25, 625 )
  98. */
  99.  
  100. Point_wb("point_bin.dat", p1);
  101. p2 = Point_rb("point_bin.dat");
  102. Point_print("p1", p1);
  103. Point_print("p2", p2);
  104. /*
  105. p1: ( 4, 16, 256 )
  106. p2: ( 4, 16, 256 )
  107. */
  108.  
  109. return EXIT_SUCCESS;
  110. }

文本文件和二进制文件的比较


当数据存为文本文件, 可以从终端用命令读取, 甚至可以通过文本编辑器查看和编辑.
但 Point_wt 和 Point_rt 必须一个个的处理属性, 处理顺序必须相同.
需要添加属性时, Point_wt 和 Point_rt 都要进行调整, 增加了出错的风险.
而 Point_wb 和 Point_rb 会自动处理属性的顺序问题, 添加属性到 Point, 无需任何调整, 因为 sizeof 会自动更新大小.

二进制文件的缺点在于:

  • 文件不能直观的编辑和查看
  • 文件可能因平台不同而有差异, 不同平台生成的二进制数据的大小和格式可能不同

《 C 语言程序设计进阶教程》