C语言-第12章-结构、联合和枚举

11.0 记录体重指数

为了计算一个人的体重指数,需要声明两个变量分别保存一个人的身高和体重,如果程序中需要记录这样的多对身高和体重,很容易把它们搞混,比如用A的体重和B的身高来计算A的体重指数。如果可以把表示一个人的身高和体重声明在一起,那么搞混的几率就大大降低了。结构体是记录一个“对象”多个不同“属性”的数据结构。可以先声明结构体类型,再创建这中类型的变量。

struct body{
  float weight;
  float height;
};

int main(){
  struct body a;
  scanf("%f", &a.weight);
  scanf("%f", &a.height);
  printf("weight = %.2f, height = %.2f, bmi = %.2f\n", a.weight, a.height, a.weight / (a.height * a.height));
  getchar();
  getchar();
  return 0;
}

如果在程序中需要记录多个人的体重指数的话,甚至可以声明元素为结构体的数组,比如

struct body a[10];

11.1 结构

数组是目前我们学到的一种数据结构,它可以存储相同类型的变量,而且以下标来访问其中的元素。它用来存储数量较多的同类型的数据。还有种常见的场景是需要将不同类型的数据放在一起,比如某个学生信息,由姓名、年龄、性别等属性组成,要将这些不同类型的东西存储在一起,我们需要结构类型。


11.1.1 声明结构体变量

要使用结构,可以直接声明结构体变量


#define NAME_LEN 25

struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student1, student2;

上述代码声明了两个结构体变量,person1和person2,每个结构体变量里面含有3个成员:id(学号)、name(名字)、性别(gender)。这里声明结构变量和声明其它基本类型的变量形式是一样的,struct{…}声明了类型,而student1和student2是具有这种类型的变量。

结构变量的成员在内存中是按照声明的顺序存储的。为了说明student1在内存中的存储形式,假设:(1)student1存储在地址为2000的内存单元中,(2)每个整数在内存中占4个字节,(3)NAME_LEN值是25,(4)成员之间没有间隙。根据这些假设,student1在内存中的样子如下所示:

在这里插入图片描述

或者用更简洁的形式描述:

在这里插入图片描述

结构成员的值稍后会放入盒子中。现在留空。

11.1.2 结构变量的使用

对数组的操作实际上是对数组元素操作,同样的,对结构变量的操作实际上是对它的成员的操作,为了访问结构变量的成员,用

结构体变量名.成员名

的形式来访问。比如,

student1.id = 1232434;
strcpy(student1.name, "mike");
student1.gender = 'f';

结构变量相当于它的成员变量的容器。对成员变量的操作与同类型单独声明的变量的操作没有区别。这与对数组中的元素的处理是一样的, 只是数组的各元素是同一种类型。

注意,用于访问结构成员的句点实际上是一个C语言的运算符。参考运算符表可知,它的运算优先级与后缀++和后缀–运算符一样,所以句点运算符的优先级几乎高于所有其它运算符。比如

scanf("%d", &student1.id);

表达式&student1.id包含两个运算符,.运算符的优先级高于&运算符,所以表达式等价于&(student1.id),含义是取student1的成员id的地址。
从语言级别来说,C不支持数组的整体操作,但是对结构体整体是支持的,C支持结构体变量的赋值操作

student2 = student1;

这个语句的效果是把student1.id赋值给student2.id,把student1.name赋值给student2.name,把student1.gender赋值给student2.gender,依此类推。
但是除了赋值以外,C语言没有提供其他用于整个结构的操作。特别是不能使用运算符==和!=来判定两个结构相等还是不等。

11.1.3 结构体变量的初始化

同数组一样,结构变量也可以在声明的同时进行初始化。而且结构变量的初始化形式与数组的初始化类似,用大括号把结构成员的初始值括起来,用逗号隔开,

struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student1 = {345, "mike", 'm'},
 student2 = {356, "marie", 'f'};

这样,student1的id成员的值是345,student1的name成员存储了mike字符串(还有一个空字符表示字符串的结束),gender成员是’m’。

在这里插入图片描述

同数组一样,初始化列表中没有列出的成员会默认初始化为0。

struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student1 = {345},

比如,上述声明的student1,其name成员和gender成员的各二进制位都是0,所以name是空的字符串,gender的ASCII码值是0,即空字符。

在这里插入图片描述

11.1.4 声明结构体类型

struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student1 ;

的形式声明结构变量student1,相当于一种临时的形式,它存在一些问题。主要体现在,如果再以同样的成员类型和成员名声明另一个结构变量student2

struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student2 ;

按照C语言的规则,虽然student1和student2的各个成员类型和名称都相同,但是它们并不是相同的类型,它们之间不能相互赋值。而且,因为student1和student2的类型都没有名字,所以不能把它们用作定义函数时的参数。
为了解决这些问题,需要定义结构类型,即给结构类型起一个名字,而不光是给特定的结构变量起名。C语言提供了两种命名结构的方法:可以声明“结构标记”,也可以使用typedef来定义类型名。

方法1. 声明结构标记

struct student{
  int id;
  char name[NAME_LEN + 1];
  char gender;
};

上面的代码声明了结构标记student,注意,右大括号后面的分号是必不可少的,它表示声明结束。一旦创建了结构标记,就可以用它来声明变量了:

struct student student1, student2 = {345, "mike", 'm'};

注意,不能漏掉单词struct来写这个声明,

student student1; // 错误

一般情况下,可以将结构声明像定义宏一样,放在所有函数定义的外边,同时将它放在源程序文件的最前面,这样在结构声明之后就可以使用它了。

#include <stdio.h>
#include <string.h>

#define NAME_LEN 20

struct student{
  int id;
  char name[NAME_LEN + 1];
  char gender;
};  

int main(){
    struct student student1, student2 = {345, "mike", 'm'};
	...
	getchar();
	getchar();
	return 0;
}
...  // 定义其它函数,可能也会用到结构声明

方法2. 定义结构类型

可以用typedef来定义真实的类型名。typedef可以为类型取一个新的名字。(参考C typedef
可以按照下面的方式,定义名为Student的类型

typedef struct{
  int id;
  char name[NAME_LEN + 1];
  char gender;
} student; 

注意,类型名student出现在最后,而不是struct的后面。右大括号后面不需要添加分号。

定义类型名后,可以像使用内置类型那样使用student。例如,

student student1, student2 = {345, "mike", 'm'};

注意,使用类型名student时,无需再写struct student了。

需要命名结构时,通常既可以选择声明结构标记也可以使用typedef。后续的例子中,还是使用结构标记。

11.1.5 结构变量作为函数参数和返回值

函数可以有结构类型的实际参数和返回类型,比如,可以定义一个函数显示结构成员的信息

void print_student(struct student s){
  printf("id : %d, ", s.id);
  printf("name : %s, ", s.name);
  if (s.gender == 'm'){
    printf("gender : male");
  }else if (s.gender == 'f'){
    printf("gender : female");
  }else{
    printf("gender : unknown");
  }
  printf("\n");
}

调用print_student时,需要传入一个有值的结构变量

printf_student(student1);

就像普通类型的形式参数一样,随着函数的调用,形式参数s作为传入的实际参数student1的副本被创建出来,形式参数s和实际参数student1是两个不同的结构变量。实际参数student1被复制到形式参数s中。
函数还可以返回结构变量,比如,输入学号,姓名和性别,构造一个存储这些信息的结构变量,这个功能可以定义成函数,

struct student build_student(int id, const char *name, char gender){
  struct student s;
 
  s.id = id;
  strcpy(s.name, name);
  s.gender = gender;
  
  return s;
}

注意,形式参数名和结构student的成员名相同是合法的,结构拥有自己的名字空间。下面是build_student可能的调用方法,

student1 = build_student(345, "andy", 'f');

同样的,上述调用语句会将返回的结构变量赋值给student1。

11.1.6 结构指针作为函数参数

无论是参数还是返回值是结构类型,在调用过程中都会产生结构的副本并拷贝整个结构变量,即结构变量各个成员的值,就像基本类型的参数和返回值那样。所以从效率上来说不大经济。可以传结构变量的地址来减少函数调用的开销,而且附加的好处是函数内部可以修改外部结构变量。

结构指针

结构变量的地址是结构首个成员在内存中的地址(类似于数组),可以声明结构类型的指针,让它存储该地址

struct student *p;
p = &student1;

在这里插入图片描述

同指向基本类型变量的指针一样,在p指向结构变量后,*p即是那个结构变量的别名,可以通过p来访问它指向的结构变量

(*p).id = 345;
strcpy((*p).name, "andy");
(*p).gender = 'f';

注意,由于句点运算符.的优先级高于间接寻址运算符*,故要用小括号把*p括起来,否则*p.id会被编译器看成*(p.id),而p不是结构变量,而且id也不是指针变量,所以这都是错误的用法。为了方便通过指针来访问结构的成员,C语言还有->运算符。

p->id = 345;
strcpy(p->name, "andy");
p->gender = 'f';

由于->运算符的优先级和.一样,是最高的,所以表达式p->id = 345的运算顺序是(p->id) = 345,即取p指向的结构变量的id成员,然后将345赋值给它。

结构指针作为函数参数

使用结构指针,显示结构成员的函数可以写为,

void print_student_with_pointer(const struct student *p){
  printf("id : %d, ", p->id);
  printf("name : %s, ", p->name);
  if (p->gender == 'm'){
    printf("gender : male");
  }else if (p->gender == 'f'){
    printf("gender : female");
  }else{
    printf("gender : unknown");
  }
  printf("\n");
}

调用时,传入实际结构变量的地址即可

print_student_with_pointer(&student1);

返回结构变量的函数build_student可以改为为实际参数的结构变量赋值的函数assign_student,

void assign_student(struct student *p, int id, const char *name, char gender){
 
  p->id = id;
  strcpy(p->name, name);
  p->gender = gender;

}

调用语句可以为

assign_student(&student1, 345, "andy", 'f');

用传递指针的方式,避免了创建结构变量的副本和值拷贝,降低了空间和时间消耗。

11.1.7 嵌套的数组和结构

结构和数组的组合没有限制。数组可以包含结构作为元素,结构也可以包含数组和其它结构作为其成员。我们已经看过数组嵌套在结构中的例子(name数组包含在student结构中),下面来看成员是结构的结构,以及元素是结构的数组的例子。


11.1.7.1 嵌套的结构

结构的成员可以是结构,比如声明了如下的结构用来存储一个外国人的名字。

struct person_name{
  char first[FIRST_NAME_LEN + 1];
  char middle_initial;
  char last[LAST_NAME_LEN + 1];
};

在更大的结构体中可以包含struct person_name作为成员

struct student{
  int id;
  struct person_name name;
  char gender;
};

student结构体变量的name成员本身还是结构体变量

struct student student1;
student1.id = 345;
strcpy(student1.name.first, "Fred");
...

使name成为结构的好处之一就是可以把名字作为统一的数据单元来处理。比如,如果编写函数来显示名字,只需要传递一个实际参数,而不是三个实际参数,

display_name(student1.name);  // 原型:void display_name(struct person_name s);

同样的,把信息从struct person_name复制给结构student的成员name只需要一次而不是三次赋值,

struct person_name new_name;
...
student1.name = new_name;

11.1.7.2 结构数组

如果要同时存储多个结构,可以声明结构数组,比如,假设struct student已经定义好,

struct student{
  int id;
  char name[NAME_LEN + 1];
  char gender;
};

要存储多个学生信息,可以声明元素为struct student结构的数组

struct student students[100];

这样,相当于我们有了100个结构变量,即students[0],students[1],…,students[99]。对结构变量的操作可以直接应用到任何一个数组元素上,比如,设置students[0]的id,

students[0].id = 345;

比如,设置students[7]的name为"kevin",

strcpy(students[7].name,"kevin");

调用print_student显示students[18]的信息

print_student(students[18]);

11.2 联合

可参考C联合(共用体)

和结构一样,联合也是由一个或多个成员组成的,这些成员可能具有不同的类型。但是编译器只为联合分配最大成员所需要的内存空间,各个成员会共用这个内存空间。比如,下面声明一个联合变量u,

union{
  int i;
  double d;
}u; 

这个和声明结构变量非常类似,比如声明结构变量s,

struct{
  int i;
  double d;
}s;

结构变量s和联合变量u只有一处不同:s的成员存储在不同的内存地址中,而u的成员存储在同一n内存地址中。下图展示了s和u在内存中的存储情况(假设int占4个字节,double占8个字节)

在这里插入图片描述

在结构变量s中,成员i和d占不同的内存单元,s总共占4+8=12个字节。而在联合变量u中,最大的成员d占8个字节,故u占8个字节。成员i和d共用4个字节。所以联合变量在某个时刻只能存储一个成员,另外的成员就没有意义。

访问联合的成员还是用逗号运算符,

u.i = 30;
u.d = 4.5;

由于i和d存储在一个地方,所以如果存储一个值到其中一个成员,另一个成员的值就会被“破坏”了,

u.i = 30;
printf("i = %d\n", u.i);
u.d = 4.5;
printf("d = lf%\n", u.d);
printf("i = %d\n", u.i);

在这里插入图片描述
可以把联合想成是存储i或者存储d的地方,而不是同时存储二者的地方。

联合的性质和结构的性质几乎一样,所以可以用声明结构标记和类型的方法来声明联合的标记和类型。像结构一样,联合可以使用运算符=进行复制,也可以传递给函数,也可以由函数返回。

联合的初始化方式甚至也和结构的初始化很l类似。但是,只有联合的第一个成员可以获得初始值。比如,可以用下列方式将u的成员i初始化为0,

union {
  int i;
  double d;
}u = {0};

利用联合可以创建不同类型的混合数据结构,比如

typedef union{
  int i;
  double d;
} Number;

接下来,创建一个数组,其元素类型是Number

Number number_array[100];

这样,数组的每个元素即可以是int类型的值,也可以是double类型的值。

number_array[0].i = 40;
number_array[1].d = 3.243;

联合的一个问题是在随后的程序中不确定到底最后使用了联合的哪个成员,即联合的哪个成员是有效的,比如下面的函数用来显示联合变量的有效值:

void print_number(Number n){
  if (n包含一个整数){
    printf("%d", n.i);
  }else{
    printf("%lf", n.g);
  }
}

显然,我们要能够确定n到底哪个成员是有效的。
为了记录此信息,可以把联合嵌入一个结构体中,结构体用一个成员标记当前联合体中哪个成员是有效的。

#define INT_KIND 0
#define DOUBLE_KIND 1

typedef struct {
  int kind;
  union{
    int i;
    double d;
  }u;
} Number;

Number有两个成员kind和u,kind的值根据u里面存储了整数值标记为INT_KIND,否则,标记为DOUBLE_KIND。

Number n;
n.u.i = 55;
n.kind = INT_KIND;

函数print_number可以借助kind成员判断联合的哪个成员是有效的

void print_number(Number n){  // 这里,Number是包含kind成员的结构体
  if (n.kind == INT_KIND){
    printf("%d", n.u.i);
  }else{
    printf("%lf", n.u.g);
  }
}

11.3 枚举

可参考C枚举

在一些程序中,我们需要标志若干属于同一个类型,但是不同的“值”。比如表示布尔值“真”和“假”,性别“男”和“女”,各个月份等等。简单的做法是声明整数变量,存储不同的整数来用不同的值对应这些“值”,比如C语言产生布尔值的表达式会以1表示逻辑值“真”,0表示逻辑值“假”。也可以用1,2,…,12分别表示各月。但是用整数表示这些是有问题的,比如“男”和“女”应该和哪个整数对应,如果是和0和1对应,那么在后续的程序中编程人员和阅读程序的人员也可能忘记了这种对应关系或者搞混了。

int g;
g = 0;  // 表示男性

还可以用宏来给这些起名字,比如

#define GENDER int
#define MALE 0
#define FEMALE 1

这些宏可以让代码更加易读

GENDER g;
g = MALE;

但是这些宏没有类型上的关联,不能为阅读程序的人指出这些宏表示了具有相同“类型”的值。而且如果宏比较多,那么为每个宏设置一个值也比较麻烦,而且预处理器会删除我们定义的名字,所以在调试期间没法使用这些名字。
可以使用枚举类型的变量存储同一类型的不同的值。比如,

enum {MALE, FEMALE} g;
g = MALE;

其中enum是关键字,就像struct、union那样,大括号里面列举了不同“值”的名字(类似于宏),这些值可以赋值给变量g。这些值称为枚举常量,类似于用#define指令创建的常量,但是它们又不完全一样。比如,如果将MALE和FEMALE之外的值赋值给g,编译器可以检测到这个有隐患的用法,

enum {MALE, FEMALE} g;
g = 10;  // 编译器可以检测到异常

而对于宏来说,是无法检测到的

GENDER g;
g = 10;

而且,枚举常量遵循C语言的作用域规则:如果枚举声明在函数体内,那么它的常量对外部函数来说是不可见的。

11.3.1 枚举标记和类型名

与命名结构和联合的原因相同,我们也常常需要创建枚举的名字。与结构和联合一样,可以用两种方法命名枚举:通过声明标记的方法,或者使用typedef来创建独一无二的类型名。

1.枚举标记

比如,可以标记gender

enum gender {MALE, FEMALE};

那么可以按照下面的写法来声明变量g

enum gender g;

2.typedef命名类型

typedef enum {MALE, FEMALE} Gender;

这样Gender就是类型名,可以直接使用它声明枚举变量g

Gender g;

像在C89中,没有布尔类型,可以用typedef来命名枚举来创建布尔类型

typedef enum {FALSE, TRUE} Bool;

C99中有内置的布尔类型,所以C99程序员不需要这样定义Bool类型。

11.3.2 枚举值是整数

在系统内部,C语言会把枚举变量和枚举常量当做整数来处理。默认情况下,编译器会把整数0,1,2,…赋值给枚举常量(默认情况下,如果没有指定整数值,在声明枚举类型时,首个枚举值是0,随后的各个枚举值会自动加1)。例如,在枚举gender

enum gender {MALE, FEMALE};

下,MALE和FEMALE分别用0和1来表示。

下列枚举周一到周日的枚举常量的值是0,1,2,…7。

enum weekday {MON, TUE, WED, THU, FRI, SAT, SUN};

我们可以为枚举常量选择不同的值。比如,让周一从1开始。

enum weekday {MON=1, TUE=2, WED=3, THU=4, FRI=5, SAT=6, SUN=7};

由于后一个枚举常量会自动加1,除为第一个MON赋值,我们可以省略其余的赋值,

enum weekday {MON=1, TUE, WED, THU, FRI, SAT, SUN};

也可以安排为枚举常量设置任意的值,

enum weekday {MON, TUE, WED = 10, THU, FRI = 20, SAT, SUN = 30};

这样,各个枚举常量的值分别是:MON是0,TUE是1,WED是10,THU是11,FRI是20,SAT是21,SUN是30。

由于枚举值只不过是一些稀疏分布的整数,所以C允许把它们与普通整数进行混合:

int i;
enum {CLUBS, DIAMONDS, HEARTS, SPADES} s;

i = DIAMONDS;  // i是1
s = 0; // s是0,相当于CLUBS
s++;  // s是1,相当于DIAMONDS
i = s + 2; // i是3 

注意,如果把4赋给s,那么s就不能和枚举常量对应了。所以对枚举变量的处理最好还是与枚举常量相关联,避免不必要的错误。

在联合小节定义的结构体Number中,可以把成员kind声明为枚举类型而不是int,

typedef struct {
  enum {INT_KIND, DOUBLE_KIND} kind;
  union{
    int i;
    double d;
  }u;
} Number;

这种新结构和旧结构的用法完全一样。这样做的好处是不需要单独定义宏INT_KIND, DOUBLE_KIND,而且说明了kind是枚举变量,只能(应该)取两种可能的值:INT_KIND, DOUBLE_KIND。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值