HeadFirstC笔记_5 结构、联合与位字段:创建自己的结构

有时要传很多数据
C语言可以处理很多不同类型的数据:小数字、大数字、浮 点数、字符与文本。但现实世界中的事物往往需要一条以上
的数据来记录。比如:
     
      
      
  1. /* 打印目录项 */
  2. void catalog(const char *name, const char *species, int teeth, int age)
  3. {
  4. printf("%s is a %s with %i teeth. He is %i\n",
  5. name, species, teeth, age);
  6. }
  7. /* 打印贴在水缸上的标签 */
  8. void label(const char *name, const char *species, int teeth, int age)
  9. {
  10. printf("Name:%s\nSpecies:%s\n%i years old, %i teeth\n",
  11. name, species, teeth, age);
  12. }
  13. int main()
  14. {
  15. catalog("Snappy", "Piranha", 69, 4);
  16. label("Snappy", "Piranha", 69, 4);
  17. return 0;
  18. }
两个不同的函数都传了 相同的4条 数据,显然这种方式是累赘的。怎么才能解决这个问题?

用结构创建结构化数据类型
如果需要把一批数据打包成一样东西,就可以使用结构 (struct)。 struct 是structured data type(结构化数据类型)
的缩写。有了结构,就可以像下面这样把不同类型的数据写 在一起,封装成一个新的大数据类型:
    
     
     
  1. struct fish {
  2. const char *name;
  3. const char *species;
  4. int teeth;
  5. int age;
  6. };
结构与数组有些相似,除了以下两点:
1.结构的大小固定。
2.结构中的数据都有名字。

给结构赋值
和新建数组很像,你只需 要保证每条数据按照它们在结构中定义的顺序出现即可:
     
      
      
  1. struct fish snappy = { "Snappy", "Piranha", 69, 4};

问: fish结构会保存字符串吗?
答: 在这个例子中不会,这里的 fish结构中只保存了字符串指针,也 就是字符串的地址,字符串保存在存 储器中其他位置。

问: 但还是可以把整个字符串保存在结构中吧?
答: 对,只要把字符串定义成字 符数组就行了,像char name[20];。

只要把“鱼”给函数就行了
现在,你只要把新的自定义数据传给函数就行了,而不必传 递一大批零散的数据。
      
       
       
  1. /* 打印目录项 */
  2. void catalog(struct fish f)
  3. {
  4. ...
  5. }
  6. /* 打印贴在水缸上的标签 */
  7. void label(struct fish f)
  8. {
  9. ...
  10. }
  11. int main(){
  12. struct fish snappy = { "Snappy", "Piranha", 69, 4};
  13. catalog(snappy);
  14. label(snappy);
  15. }

把参数封装在结构中,代码会更稳定。
把数据放在结构中传 递有一个好处,就是 修改结构的内容时, 不必修改使用它的函数。比如 要在fish中多加一个字段:
         
          
          
  1. struct fish {
  2. const char *name;
  3. const char *species;
  4. int teeth;
  5. int age;
  6. // 新增字段
  7. int favorite_music;
  8. };
catalog()和label()知道有 人会给它们一条fish,但却不 知道fish中现在有了更多的数 据,它们也不关心,只要fish 有它们需要的所有字段就行了。
这就意味着,使用结构,不但 代码更好读,而且能够更好地 应对变化------>早期的面向对象思想体现。

使用“.”运算符读取结构字段
         
          
          
  1. struct fish snappy = { "Snappy", "piranha", 69, 4};
  2. printf("Name = %s\n", snappy.name); // snappy.name这是snappy中的name属性

问: 数组变量就是一个指向数组的指针,那么结构变量是一个指向结构的指针吗?
答: 不是,结构变量是结构本身的名字。

问: 结构就相当于其他语言中的类?
答: 它们很相似,但在结构中添加方法可就没那么 容易了。

存储器中的结构
在定义结构时,你并没有让计算机在存储器中创建任何东西,
只是给了计算机一个模板,告诉它你希望新的数据类型长什
么样子。
          
           
           
  1. struct fish {
  2. const char *name;
  3. const char *species;
  4. int teeth;
  5. int age;
  6. };
当定义新变量时,计算机则需要在存储器中为结构的实例创建 空间,这块空间必须足够大,以装下结构中的所有字段:
           
            
            
  1. struct fish snappy = { "Snappy", "Piranha", 69, 4};
那么当把一个结构变量赋给另一个结构变量时会发生什么?
计算 机会创建一个全新的结构副本,也就是说,计算机需要再分配一 块存储器空间,大小和原来相同,然后把每个字段都复制过去。
            
             
             
  1. struct fish snappy = { "Snappy", "Piranha", 69, 4};
  2. struct fish gnasher = snappy;
切记:为结构变量赋值相当于叫计算机复制数据。
注意:字符串字段复制的是指向字符串的
指针,而非字符串本身。
当把一个结构变量赋给
另一个结构变量,计算
机会复制结构的内容。如果结构中
含有指针,那么复制的仅仅是指针
的值,像这里,gnasher和snappy
的name和species字段指向相同字
符串。

结构中的结构
既然结构可以用现有数据类型创建数据类型,也就能用其他
结构创建结构。举个例子:
                     
                      
                      
  1. struct preferences {
  2. const char *food;
  3. float exercise_hours;
  4. };
  5. struct fish {
  6. const char *name;
  7. const char *species;
  8. int teeth;
  9. int age;
  10. struct preferences care; //结构中的结构,即嵌套
  11. };
上述代码向计算机描述了一个结构中的结构。你可以像之前 一样用数组语法创建变量,但现在可以在数据中包含结构中 的结构。
                      
                       
                       
  1. struct fish snappy = { "Snappy", "Piranha", 69, 4, { "Meat", 7.5}};
一旦把结构组合起来,就可以使用一连串的“.”运算符来访 问字段:
                       
                        
                        
  1. printf("Snappy 喜欢吃 %s", snappy.care.food);
  2. printf("Snappy 喜欢锻炼 %f hours", snappy.care.exercise_hours);
为什么要嵌套定义结构?
之所以要这么做是为了对
抗复杂性。通过使用结
构,我们可以建立更大的
数据块。通过把结构组合
在一起,我们可以创建
大的数据结构。本来你只
能用int、short,但有
了结构以后,就可以描述
十分复杂的东西,比如网
络流和视频图像。

用typedef为结构命名
在C语言中可以为结构创建别名,你只要在 struct 关键字前 加上 typedef ,并在右花括号后写上类型名,就可以在任何
地方使用这种新类型。cell_ phone  是结构名,phone  是类型名。 一般一个就够了,为了简便通常会省略结构名。
                           
                            
                            
  1. typedef struct cell_phone { // 这里的cell_phone 可以省略
  2. int cell_no;
  3. const char *wallpaper;
  4. float minutes_of_charge;
  5. } phone; // phone将成为“struct cell_phone”的别名。
  6. ... phone p = {5557879, "sinatra.png", 1.35};
示例代码:
                        
                         
                         
  1. #include <stdio.h>
  2. struct exercise {
  3. const char *description;
  4. float duration;
  5. };
  6. struct meal {
  7. const char *ingredients;
  8. float weight;
  9. };
  10. typedef struct {
  11. struct meal food;
  12. struct exercise exercise;
  13. } preferences;
  14. typedef struct fish { // 这里的fish 是结构名,如果有了类型名,这个结构名可以省略
  15. const char *name;
  16. const char *species;
  17. int teeth;
  18. int age;
  19. preferences care;
  20. } fish; // 这里的fish是类型名,后续可以直接像int ,long 那样去用
  21. void catalog(struct fish f) {
  22. printf("%s is a %s with %i teeth. He is %i '\n",
  23. f.name,f.species,f.teeth,f.age);
  24. }
  25. /* 打印贴在水缸上的标签 */
  26. void label(struct fish f) {
  27. printf("Name:%s\nSpecies:%s\n%i years old, %i teeth\n",
  28. f.name, f.species, f.teeth, f.age);
  29. printf("Feed with %4.2f lbs of %s and allow to %s for %2.2f hours\n",
  30. f.care.food.weight, f.care.food.ingredients,
  31. f.care.exercise.description,f.care.exercise.duration);
  32. }
  33. int main() {
  34. // struct fish snappy = {
  35. fish snappy = {
  36. "Snappy",
  37. "Piranha",
  38. 64,
  39. 4,
  40. { { "meat",0.2},{ "swim in the jacuzzi",7.5}}
  41. };
  42. catalog(snappy);
  43. label(snappy);
  44. return 0;
  45. }
运行结果:
 
问: 结构字段在存储器中是紧挨着摆放的吗?
答: 有时两个字段之间会有小的 空隙。

问: 为什么?
答: 计算机总是希望数据能对齐 字边界(word boundary)。如果计算 机的字长是32位,就不希望某个变量 (比如short)跨越32位的边界保存。

问: 所以计算机会留下一道空隙,然后在下一个32位字开始的地方保存short?
答: 是的。

问: 也就是说,每个字段都占用一整个字?
答: 不一定,计算机在两个字段 之间留出空隙仅仅是为了防止某个字 段跨越字边界。如果几个字段能放在 一个字中,计算机就会那么做。

问: 为什么计算机如此在意字边界?
答: 计算机按字从存储器中读 取数据,如果某个字段跨越了多个 字,CPU就必须读取多个存储器单元, 并以某种方式把读到的值合并起来。

问: 这样会很慢吗?
答: 会很慢。

问: 在Java那样的语言中,如果我把对象赋给变量,它不会复制对象,仅仅复制引用,为什么C语言不这样做?
答: 在C语言中,所有赋值都会 复制数据,如果你想复制数据的引用, 就应该赋指针。

如何更新结构
先看个例子:
                          
                           
                           
  1. #include <stdio.h>
  2. typedef struct {
  3. const char *name;
  4. const char *species;
  5. int age;
  6. } turtle;
  7. void happy_birthday(turtle t)
  8. {
  9. t.age = t.age + 1;
  10. printf("Happy Birthday %s! You are now %i years old!\n",
  11. t.name, t.age);
  12. }
  13. int main()
  14. {
  15. turtle myrtle = { "Myrtle", "Leatherback sea turtle", 99};
  16. happy_birthday(myrtle);
  17. printf("%s's age is now %i\n", myrtle.name, myrtle.age);
  18. return 0;
  19. }
运行结果:

奇怪的是,我明明在happy_birthday函数中将age的值+1了,可为什么还是99?

代码克隆了乌龟
原因是在C语言中,参数是按值传递给函数的。也就是说,当调用函 数时,传入函数的值会赋给形参,因此当把一个结构 赋给另一个时,
结构的值就被复制 到了新结构中。所以 这段代码等价于:
                           
                            
                            
  1. void happy_birthday(turtle t)
  2. {
  3. ...
  4. }
  5. ...
  6. happy_birthday(myrtle);
  7. // 上面代码等价于下面
  8. ==> turtle t = myrtle;
搞了半天 ,原来你修改的只是副本的值,原结构还是原封不动。那想修改原结构的值咋办?

你需要结构指针
如果想让函数更新结构变量,就不能
把结构作为参数传递,因为这样做仅仅是将数据的副本复制给
了函数。
取而代之,你可以传递结构的地址----指针,如:
                             
                              
                              
  1. void happy_birthday(turtle *t) //表示“有人要给我一个结构指针(地址)
  2. {
  3. ...
  4. }
  5. ...
  6. happy_birthday(&myrtle); // 你将把myrtle变量的地址传给函数
修改之前的错误代码:
                              
                               
                               
  1. #include <stdio.h>
  2. typedef struct {
  3. const char *name;
  4. const char *species;
  5. int age;
  6. } turtle;
  7. turtle happy_birthday(turtle *t) {
  8. (*t).age = (*t).age + 1;
  9. printf("Happy Birthday %s! You are now %i years old!\n",
  10. (*t).name, (*t).age);
  11. // 注意 (*t).age != *t.age = *(t.age)。
  12. // return t;
  13. }
  14. // t->age = (*t).age,“表示这是由指针t指向的结构体中的 age字段”,因此下面还可以这样写
  15. turtle happy_birthday_new(turtle *t) {
  16. t->age = t->age + 1; // 注意有的编译器这样写报错,要写成(*t)->age才行,如androidstudio的ndk
  17. printf("Happy Birthday %s! You are now %i years old!\n",
  18. t->name, t->age);
  19. // 注意 (*t).age != *t.age = *(t.age)。
  20. // return t;
  21. }
  22. int main() {
  23. turtle myrtle = { "Myrtle", "Leatherback sea turtle", 99};
  24. // myrtle = happy_birthday(myrtle);
  25. happy_birthday(&myrtle);
  26. printf("%s's age is now %i\n", myrtle.name, myrtle.age);
  27. return 0;
  28. }
运行结果:


注意(*t).age和*t.age的区别!
为什么 *t 外面一定要加括号? 因为 (*t).age 与 *t.age 完全是两个不同的表达式。
表达式 *t.age 等于 *(t.age) 。请思考一下表达式 *(t. age) 的含义。它代表“ t.age 这个存储器单元中的内容”, 但 t.age 不是存储器单元。

另一种表示结构指针的方法,它更易于阅读。
t -> age 代表 (*t).age。有的编译器是(*t)->age。
即:“指针->字段”等于“(*指针).字段”。ƒ“->”表示法省掉了括号,代码更易阅读。

代码示例:
怎么能让你得到字符串“GOLD!”
                               
                                
                                
  1. #include <stdio.h>
  2. typedef struct {
  3. const char *description;
  4. float value;
  5. } swag;
  6. typedef struct {
  7. swag *swag;
  8. const char *sequence;
  9. } combination;
  10. typedef struct {
  11. combination numbers;
  12. const char *make;
  13. } safe;
  14. int main() {
  15. swag gold = { "GOLD!", 1000000.0};
  16. combination numbers = {&gold, "6502"};
  17. safe s = { numbers, "RAMACON250"};
  18. printf("password= %s",s.numbers.swag->description);
  19. }
运行结果:
 
同一类事物,不同数据类型
可以用结构来模拟现实世界中错综复杂的事物,但有些数据不止
一种数据类型:
假如想记录某样东西的“量”,既可以用个数,也可以用重量, 或者用容积。所以可以在一个结构中创建多个字段:
                                    
                                     
                                     
  1. typedef struct {
  2. ...
  3. short count;
  4. float weight;
  5. float volume;
  6. ...
  7. } fruit;
这不是好主意,原因有以下几点:
1.结构在存储器中占了更多空间。 
2.用户可能设置多个值。 
3.没有叫“量”的字段。

要是能这样就好了:
定义一种叫“量”的数据类型,然后根据特 定的数据决定要保存个数、重量还是容积。
在C语言中,可以用联合做到这点。

联合可以有效使用存储器空间
每次创建结构实例,计算机都会在存储器中相继摆放字段,联合则不同。当定义联合时,计算机只为其中一个字段分 配空间。假设你有一个叫 quantity 的联合,它有三个字 段,分别是 count 、 weight 和 volume ,那么计算机就会 为其中最大的字段分配空间,然后由你决定里面保存
什么 值。无论设置了 count 、 weight 和 volume 中的哪个字段, 数据都会保存在存储器中同一个地方。
                                
                                 
                                 
  1. typedef union { // 联合看起来很像结构,但用的是union关键字
  2. short count;
  3. float weight;
  4. float volume;
  5. } quantity;

如何使用联合
1.C89方式
如果联合要保存第一个字段的值,就可以用C89表示
法,只要用花括号把值括起来,就可以把值赋给联合
中第一个字段。
                                 
                                  
                                  
  1. quantity q = { 4};
2.指定初始化器
指定初始化器(designated initializer)按名设置联合字 段的值:
                                  
                                   
                                   
  1. quantity q = {.weight=1.5};
3.“点”表示法
第三种设置联合值的方法是在第一行创建变量,然后
在第二行设置字段的值。
                                     
                                      
                                      
  1. quantity q;
  2. q.volume = 3.7;
切记,无论用哪种方法设置联合的值,都只会保存一
条数据。联合只是提供了一种让你创建支持不同数据
类型的变量的方法。

问: 为什么联合的大小取决于最长
的字段?
答: 计算机需要保证联合的大小固
定。唯一的办法就是让它足够大,任何
一个字段都能装得下。

问: 为什么C89表示法只能设置
第一个字段?如果我传给联合float值,
为什么不把它设为第一个float字段?
答: 这么做是为了避免歧义。假
设你有一个float字段和一个double
字段,那么计算机应该把{2.1}保存成
float还是double呢?
每次都把值保
存在第一个字段中,你就知道数据是怎
么初始化的。

注意:
可以用“指定初始化器”按名 设置结构和联合字段,它属于 C99标准。绝大多数现代编译 器都支持“指定初始化器”。
                                                                 
                                                                  
                                                                  
  1. typedef struct {
  2. const char *color;
  3. int gears;
  4. int height;
  5. } bike;
  6. bike b = {.height=17, .gears=21};

联合常和结构一起用
创建联合相当于创建新的数据类型,也就是说可以在任何地 方使用它的值,就像使用整型或结构那样的数据类型。例如,
可以把联合和结构结合起来:
                                                                  
                                                                   
                                                                   
  1. typedef struct {
  2. const char *name;
  3. const char *country;
  4. quantity amount; // 联合
  5. } fruit_order;
  6. // 这里连用了两次指定标识符。.amount用来初始化结构中的字段,.weight用来初始化.amount中的字段。
  7. fruit_order apples = { "apples", "England", .amount.weight=4.2};
  8. printf("This order contains %2.2f lbs of %s\n", apples.amount.weight, apples.name);

问题:可以在联合中保存各种可能的值,但保存以后,就无法知道它的类型。
编译器不会记录你在联合中设置或读取过哪些字段。我们完全 可以设置一个字段,读取另一个字段,但有时这会造成很严重 的后果。
                                                                   
                                                                    
                                                                    
  1. #include <stdio.h>
  2. typedef union {
  3. float weight;
  4. int count;
  5. } cupcake;
  6. int main() {
  7. cupcake order = { 2}; // 程序员无意中设置了weight,而不是count。
  8. printf("Cupcakes quantity: %i\n", order.count); // 明明设置了weight,却读取了count。
  9. return 0;
  10. }
运行结果:

 很明显不是想要的结果。
你需要某种方法记录我们在联合中保存了什么值。C 程序员常用的一种技巧是创建枚举。

枚举变量保存符号
有时你不想保存数字或文本,而是想保存一组符 号。如果你想记录一周中的某一天,只想保存MON DAY、TUESDAY、WEDNESDAY……这些符号。你不需 要保存文本,因为一共只有七种不同的取值。 这就是为什么要发明枚举的原因。
有了枚举,就可以创建一组这样的符号:
                                                                      
                                                                       
                                                                       
  1. enum colors { RED, GREEN, PUCE}; // 可以用typedef为类型起个名字。 
  2. //注意:结构与联合用分号(;)来分割数据项,而枚举用逗号。
用 enum colors 类型定义的变量只能设为列表中的某个 关键字。可以像下面这样定义 enum colors 变量:
                                                                      
                                                                       
                                                                       
  1. enum colors favorite = PUCE;
在幕后,计算机会为列表中的每个符号分配一个数字,枚 举变量中也只保存数字 。枚举不仅让 代码更易于阅读,同时它也能够防止你把值保存错误。
比如你把PUCE写成PUSE就不会编译通过.

代码示例:
                                                                       
                                                                        
                                                                        
  1. #include <stdio.h>
  2. typedef enum {
  3. COUNT,POUNDS,PINTS
  4. } unit_of_measure;
  5. typedef union {
  6. short count;
  7. float weight;
  8. float volume;
  9. } quantity;
  10. typedef struct {
  11. const char *name;
  12. const char *country;
  13. quantity amount;
  14. unit_of_measure units;
  15. } fruit_order;
  16. void display(fruit_order order) {
  17. printf("This order contains ");
  18. if(order.units == PINTS)
  19. printf("%2.2f pints of %s\n",order.amount.volume,order.name);
  20. else if(order.units == POUNDS )
  21. printf("%2.2f lbs of %s\n", order.amount.weight, order.name);
  22. else
  23. printf("%i %s\n", order.amount.count , order.name);
  24. }
  25. int main() {
  26. fruit_order apples = { "apples", "England", .amount.count=144, COUNT };
  27. fruit_order strawberries = { "strawberries", "Spain", .amount.weight =17.6, POUNDS};
  28. fruit_order oj = { "orange juice", "U.S.A.", .amount.volume=10.5, PINTS };
  29. display(apples);
  30. display(strawberries);
  31. display(oj);
  32. return 0;
  33. }
运行结果:
 
有时你想控制某一位
假设你需要一个结构,其中有很多表示“是”或“非”的值,
可以用一些 short 或 int 来创建结构:
                                                                        
                                                                         
                                                                         
  1. typedef struct {
  2. short low_pass_vcf;
  3. short filter_coupler;
  4. short reverb;
  5. short sequential;
  6. ...
  7. } synth;
这样做完全可行,但问题是,真/假的值只需要一位就能表 示, short 字段占了多得多的空间,太浪费了,要是结构的字 段的值能只用一位表示就好了。
这就是发明位字段( bitfield )的原因。

位字段的位数可调
可以用位字段(bitfield)指定一个字段有多少位。例
如,可以把结构写成这样:
                                                                        
                                                                         
                                                                         
  1. typedef struct {
  2. unsigned int low_pass_vcf:1; // :1 表示该字段只使用1位存储空间。
  3. unsigned int filter_coupler:1; // 注意:每个字段都必须是unsigned int
  4. unsigned int reverb:1;
  5. unsigned int sequential:1;
  6. ...
  7. } synth;
如果你有一连串的位字段,计算机会放在一起,以节省 空间,也就是说如果有8个1位的位字段,计算机就会把 它们保存在一个字节中。
注意:只有出现在同一个结构中,位字段才能节省空间。
如果编译器发现结构中 只有一个位字段,还是会把它填充 成一个字,这就是为什么位字段总 是组合在一起。

如何选择位数?
位字段不仅可以用来保存一连串真/假值,还可以用来保存小范围的数字, 例如一年中的十二个月。假设想在某个结构中保存月份(0到11的值),
就可以用一个4位的位字段来保存,为什么?因为4位可以保存0到15,而 3位只能保存0到7。
                                                                        
                                                                         
                                                                         
  1. unsigned int month_no:4;

问: 位字段就是为了节省空间的吗?
答: 不仅仅是为了节省空间,如 果需要读取低层的二进制信息,位字 段就会非常有用。

问: 能举个例子吗?
答: 比如要读写某类自定义二进 制文件。


猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53609211