天天看点

C语言—结构体、共用体、枚举类型

定义与使用

用户自己建立的由不同类型数据组成的组合型的数据结构,称为结构体,声明一个结构体如下:

struct Student{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
};                   //分号不能省略      

结构体中可以使用另一个结构体作为成员变量。

定义结构体类型变量,如下:

1、先声明结构体类型,再定义结构体类型变量

struct Student{
......
};
struct Student student1,student2;      

2、在声明类型的同时定义变量

struct Student{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
} stu1, stu2;      

3、不指定类型名而直接定义结构体类型变量

struct{  //没有写Student
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
} stu1, stu2;      

这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。

结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。

结构体的各个成员在内存中是连续存储的,和数组非常类似。

在编译器的具体实现中,各个成员之间可能会存在缝隙。

结构体变量初始化和引用

1、在定义结构体变量的时候可以进行整体赋值

struct Studeent{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };      

2、单个赋值

结构体使用成员运算符.来获取成员变量,成员运算符在所有运算符中优先级最高。通过这种方式可以获取成员的值,也可以给成员赋值,如下:

struct Student{
        char *name;  //姓名
        int num;  //学号
        int age;  //年龄
        char group;  //所在小组
        float score;  //成绩
    } stu1;
    //给结构体成员赋值
    stu1.name = "Tom";
    stu1.num = 12;
    stu1.age = 18;
    stu1.group = 'A';
    stu1.score = 136.5;      

结构体数组

所谓结构体数组,是指数组中的每个元素都是一个结构体。在实际应用中,C语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。

定义结构体数组和定义结构体变量的方式类似,如下:

struct Student{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {             //class为数组名
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};      

结构体指针

当一个指针变量指向结构体时,我们就称它为结构体指针。

指针变量的基类型必须与结构体变量的类型相同。

如下:

//结构体
struct Student{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct Student *pstu = &stu1;      

也可以在定义结构体的同时定义结构体指针,如下:

struct Student{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;      

结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName      

或者:

pointer->memberName      

第一种写法中,.的优先级高于*,(*pointer)两边的括号不能少。

第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。

结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

#include <stdio.h>
struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}stus[] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};
void average(struct stu *ps, int len);
int main(){
    int len = sizeof(stus) / sizeof(struct stu);
    average(stus, len);
    return 0;
}
void average(struct stu *ps, int len){
    int i, num_140 = 0;
    float average, sum = 0;
    for(i=0; i<len; i++){
        sum += (ps + i) -> score;
        if((ps + i)->score < 140) num_140++;
    }
    printf("sum=%.2f\naverage=%.2f\nnum_140=%d\n", sum, sum/5, num_140);
}      

指针实现链表

链表节点的声明与定义:

struct Student{
int num;
float score;
struct Student *next;
} a,b,c;
struct Student *head;
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;      

上述方式为静态链表的使用,实际中,我们使用更多的是动态链表,所谓建立动态链表是指程序在执行过程中从无到有地建立起一个链表,即一个一个地开辟节点和输入各节点数据,并建立起前后相连关系。

创建链表

创建链表需要两个指针,一个作为先行指针(*p1),开辟内存并保存结构体的值;

一个作为缓存指针(*p2),保留先行指针的所有值并且将它的next指向先行指针。

构建链表时,先行指针赋一个值,后行指针保存一个值并且后行指针的next指向先行指针。

赋值终止时,先行指针的next指向NULL,同时将先行指针赋值给后行指针,链表即构建完毕。

struct Student * create(void )
{
  struct Student *p1,*p2,*head=NULL; 
  p2=p1=(struct Student *)malloc(LEN);                      //开辟内存 
  scanf("%d %s %f %f",&p1->ID,&p1->name,&p1->height,&p1->weight);
  if(p1->ID==0)return(head);
  else head=p1;
  while(p1->ID!=0)
  {
    p2->next=p1;
    p2=p1;
    p1=(struct Student *)malloc(LEN);                          //开辟内存 
    scanf("%d %s %f %f",&p1->ID,&p1->name,&p1->height,&p1->weight);
  }
  p2->next=NULL;
  return(head);                       //返回链表头指针 
}      

共用体

使用同一段内存单元存放不同类型的变量,或者说使几个不同的变量共享同一段内存的结构,称为“共用体”类型的结构,声明与定义如下:

union Data{
    int n;
    char ch;
    double f;
};
union Data a, b, c;      

其使用和结构体相似,但是初始赋值只能赋一个变量的值;

结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

常见应用场景:老师学生表

枚举类型

所谓“枚举”就是把可能的值一一列举出来,变量的值只限于列举出来的值的范围内。

枚举类型的定义形式为:

enum typeName{ valueName1, valueName2, valueName3, ...... };   //;不可少      

例如,列出一个星期有几天:

enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };      

可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。

我们也可以给每个名字都指定一个值:

enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };      

更为简单的方法是只给第一个名字指定值:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };      

这样枚举值就从 1 开始递增,跟上面的写法是等效的。

枚举是一种类型,通过它可以定义枚举变量:

enum week a, b, c;      

也可以在定义枚举类型的同时定义变量:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;      

有了枚举变量,就可以把列表中的值赋给它:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;      

或者:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;      

需要注意的是:

1) 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。

2) Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。

3)枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。

用typedef声明新类型名

C语言允许用户使用 typedef 关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等。一旦用户在程序中定义了自己的数据类型名称,就可以在该程序中用自己的数据类型名称来定义变量的类型、数组的类型、指针变量的类型与函数的类型等。

在实际使用中,typedef 的应用主要有如下4种:

1、为基本数据类型定义新的类型名

也就是说,系统默认的所有基本类型都可以利用 typedef 关键字来重新定义类型名,示例代码如下所示:

typedef unsigned int COUNT;      

2、为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称

以结构体为例,下面我们定义一个名为 Point 的结构体:

struct Point
{
    double x;
    double y;
    double z;
};      

在调用这个结构体时,我们必须像下面的代码这样来调用这个结构体:

struct Point oPoint1={100,100,0};
struct Point oPoint2;      

在这里,结构体 struct Point 为新的数据类型,在定义变量的时候均要向上面的调用方法一样有保留字 struct,而不能像 int 和 double 那样直接使用 Point 来定义变量。现在,我们利用 typedef 定义这个结构体,如下面的代码所示:

typedef struct tagPoint
{
    double x;
    double y;
    double z;
} Point;      

在上面的代码中,实际上完成了两个操作:

1、定义了一个新的结构类型,代码如下所示:

struct tagPoint
{
    double x;
    double y;
    double z;
} ;      

其中,struct 关键字和 tagPoint 一起构成了这个结构类型,无论是否存在 typedef 关键字,这个结构都存在。

2、使用 typedef 为这个新的结构起了一个别名,叫 Point,即:

typedef struct tagPoint Point      

因此,现在你就可以像 int 和 double 那样直接使用 Point 定义变量,如下面的代码所示:

Point oPoint1={100,100,0};
Point oPoint2;      

为了加深对 typedef 的理解,我们再来看一个结构体例子,如下面的代码所示:

typedef struct tagNode
{
    char *pItem;
    pNode pNext;
} *pNode;      

从表面上看,上面的示例代码与前面的定义方法相同,所以应该没有什么问题。但是编译器却报了一个错误,为什么呢?莫非 C 语言不允许在结构中包含指向它自己的指针?

其实问题并非在于 struct 定义的本身,大家应该都知道,C 语言是允许在结构中包含指向它自己的指针的,我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢?其实,根本问题还是在于 typedef 的应用。

在上面的代码中,新结构建立的过程中遇到了 pNext 声明,其类型是 pNode。这里要特别注意的是,pNode 表示的是该结构体的新别名。于是问题出现了,在结构体类型本身还没有建立完成的时候,编译器根本就不认识 pNode,因为这个结构体类型的新别名还不存在,所以自然就会报错。因此,我们要做一些适当的调整,比如将结构体中的 pNext 声明修改成如下方式:

typedef struct tagNode
{
    char *pItem;
    struct tagNode *pNext;
} *pNode;      

或者将 struct 与 typedef 分开定义,如下面的代码所示:

typedef struct tagNode *pNode;
struct tagNode
{
    char *pItem;
    pNode pNext;
};      

在上面的代码中,我们同样使用 typedef 给一个还未完全声明的类型 tagNode 起了一个新别名。不过,虽然 C 语言编译器完全支持这种做法,但不推荐这样做。建议还是使用如下规范定义方法:

struct tagNode
{
    char *pItem;
    struct tagNode *pNext;
};
typedef struct tagNode *pNode;      

3、为数组定义简洁的类型名称

它的定义方法很简单,与为基本数据类型定义新的别名方法一样,示例代码如下所示:

typedef int INT_ARRAY_100[100];
INT_ARRAY_100 arr;      

4、为指针定义简洁的名称

对于指针,我们同样可以使用下面的方式来定义一个新的别名:

typedef char* PCHAR;
PCHAR pa;      

对于上面这种简单的变量声明,使用 typedef 来定义一个新的别名或许会感觉意义不大,但在比较复杂的变量声明中,typedef 的优势马上就体现出来了,如下面的示例代码所示:

int *(*a[5])(int,char*);      

对于上面变量的声明,如果我们使用 typdef 来给它定义一个别名,这会非常有意义,如下面的代码所示:

// PFun是我们创建的一个类型别名
typedef int *(*PFun)(int,char*);      

// 使用定义的新类型来声明对象,等价于​

​int*(*a[5])(int,char*);​

PFun a[5];      

小心使用 typedef 带来的陷阱

接下来看一个简单的 typedef 使用示例,如下面的代码所示:

typedef char* PCHAR;
int strcmp(const PCHAR,const PCHAR);      

在上面的代码中,“const PCHAR” 是否相当于 “const char*” 呢?

答案是否定的,原因很简单,typedef 是用来定义一种类型的新别名的,它不同于宏,不是简单的字符串替换。因此,“const PCHAR”中的 const 给予了整个指针本身常量性,也就是形成了常量指针“char*const(一个指向char的常量指针)”。即它实际上相当于“char*const”,而不是“const char*(指向常量 char 的指针)”。当然,要想让 const PCHAR 相当于 const char* 也很容易,如下面的代码所示:

typedef const char* PCHAR;
int strcmp(PCHAR, PCHAR);      

其实,无论什么时候,只要为指针声明 typedef,那么就应该在最终的 typedef 名称中加一个 const,以使得该指针本身是常量。

typedef static int INT_STATIC;      

继续阅读