C语言基础(八)
系列合集 初窥C语言
九、结构体与共用体
9.1 概述
结构体:一种数据结构,将不同类型的数据组合成一个有机的整体以便使用。
声明一个结构体类型的一般形式:
// struct 结构体名 {成员列表};
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
};
9.2 定义结构体类型变量的方法
先声明结构体类型再定义变量名:
struct 结构体名
{
成员表列} 变量名表列;
将一个变量定义为标准类型与定义为结构体类型不同之处在于后者不仅要求指定变量为结构体类型,而且要求指定为某一特定结构体类型。
直接定义结构体类型变量(匿名):
struct
{
成员表列} 变量名表列;
1)类型与变量是不同的概念。只能对变量赋值、存取和运算,而不能对一个类型赋值、存取和运算。在编译时,对类型是不分配空间的,只对变量分配空间。
2)对结构体的成员(或“域”),可以单独使用,它的作用地位相当于普通变量。
3)成员名可以与程序中的变量名相同,二者不代表同一对象。
4)成员也可以是一个结构体变量。
9.3 结构体变量的引用
1)结构体变量不能整体输入输出
printf("%d,%s,%c,%d,%f\n",student); //这是错误的
2)只能对结构体变量中的各个成员分别进行输入输出。应用结构体成员的方式:结构体变量名.成员名
3)如果成员本身还是结构体类型,则要继续使用“ . ”,逐级找到最低一级成员,对其进行赋值存取等操作
4)对结构体变量的成员可以像普通变量一样进行各种运算(根据其类型决定可进行的运算)
5)可以引用结构体变量成员的地址,也可以引用结构体变量的地址,但是不能使用类似如下语句整体读入结构体变量
scanf("%d,%s,%c,%d,%f",&student);
9.4 结构体变量的初始化
和其他类型的变量一样,对结构体变量可以在定义时指定初始值。例如:
#include <stdio.h>
int main()
{
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
}a = {
12345,"tom",'M',18,98.0};
printf("NO.:%ld\nname:%s\nsex:%c\nage:%d\nscore:%f\n",a.num,a.name,a.sex,a.age,a.score);
}
9.5 机构体数组
和定义结构体变量的方式相仿:
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
};
struct student stu[3];
直接定义一个结构体数组:
struct student //可以匿名
{
int num;
...
}stu[3];
结构体数组在变量定义时初始化:
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
}stu[3] = {
{
1001,"Tom",'M',18,98.0},
{
1002,"James",'M',19,97.0},
{
1003,"Emily",'F',20,99.0}};
定义数组stu时,元素的个数可以不指定,可以写成:
stu[] = {
{
...},{
...},{
...} };
9.6 指向结构体类型数据的指针
9.6.1 指向结构体变量的指针
struct student
{
int num;
char name[20];
char sex;
float score;
}stu_1, *p;
p = &stu_1;
stu_1.num = 1001;
strcpy(stu_1.name, "Tom");
p -> sex = 'M';
p -> score = 98.0;
printf("NO.:%ld\nname:%s\nsex:%c\nscore:%f\n",stu_1.num,stu_1.name,stu_1.sex,stu_1.score);
printf("NO.:%ld\nname:%s\nsex:%c\nscore:%f\n",(*p).num,(*p).name,(*p).sex,(*p).score);
在C语言中,为了方便使用和使之直观,可以吧(*p).num改用 p ->num来代替。它表示p所指向的结构体变量中的num成员。同样(*p).score等价于p -> score。
以下三种形式等价:
1)结构体变量.成员名
2)(*p).成员名
3)p -> 成员名
如p -> num,其中->称为指向运算符,有如下几种运算:
p -> n :得到p指向的结构体变量中的成员n的值
p -> n++ :得到p指向的结构体变量中的成员n的值加1,然后在使用
++p -> n :得到p指向的结构体变量中的成员n的值,用完该值后,使它加一
9.6.2 指向结构体数组的指针
#include <stdio.h>
struct student
{
int num;
char name[20];
char sex;
float score;
}stu[3] = {
{
1001,"Tom",'M',98.0},
{
1002,"James",'M',97.0},
{
1003,"Emily",'F',99.0}};
int main(){
struct student *p;
printf("No. \t name \t sex score\n");
for(p = stu; p < stu + 3; p++){
printf("%5d %-10s %c %f \n", p -> num,p -> name, (*p).sex,(*p).score);
}
}
1)如果p的初值为stu,即指向第一个元素,则p加1后p就指向下一个元素的起始地址。
2)(++p) -> num和(p++) -> num的区别:
(++p) -> num先使p加1,然后得到它指向的元素中num成员值,
(p++) -> num先得到 p -> num 的值,再使p加1,指向stu[1]。
3)程序已经定义了p是指向struct student类型数据的指针变量,用来指定一个struct student型的数据,不应用来指向stu数组元素中的某一成员。
p = stu[1].name; //编译时会给出警告,表示首地址不匹配
//如果要将某一成员的地址赋给p,可以用强制类型转换,先将成员地址转换成p类型
p = (struct student *)stu[0].name;
9.6.3 用结构体变量和指向结构体的指针做函数参数
将一个结构体变量的值传递给另一个函数,有3个办法:
1)用结构体变量的成员做参数。例如,用stu[1].num或stu[2].name做函数实参,将实参传给形参。用法和用普通变量作实参一样,属于“值传递”方式。
2)用结构体变量做参数。用结构体变量做实参时,采取的是“值传递”的方式,将结构体变量所占的内存单元全部顺序传递给形参。形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在时间和空间上开销较大,如果结构体的规模很大时,开销是很可观的。此外,由于采用值传递方式,如果在执行被调用函数期间改变了形参的值,该值不能返回主调用函数,这往往造成使用上的不便,一般较少使用这种方式。
3)用指向结构体变量(或数组)的指针作为实参,将结构体变量(或数组)的地址传给形参。
9.7 用指针处理链表
9.7.1 链表
链表是一种常见的重要的数据结构,它是动态地进行存储分配的一种结构。
1)链表有一个“头指针”变量,存放一个地址。该地址指向下一个元素。
2)链表中每一个元素称为“结点”,每个结点都有应该包括两个部分:一为用户需要用的实际数据;二为下一个节点地址。
3)链表中各元素在内存中可以不连续存放。要找某一元素,必须先找到上一个元素,根据它提供的下一元素地址才能找到下一元素。
4)链表的数据结构,必须利用指针变量才能实现。即:一个结点中应包含一个指针变量,用它存放下一结点的地址。
9.7.2 建立一个简单的链表
建立一个简单的链表,它由三个学生数据的结点组成。输出各节点中的数据。
#include <stdio.h>
struct student
{
int num;
float score;
struct student *next;
};
int main(){
struct student a,b,c,*head,*p;
a.num = 1001; a.score = 60;
b.num = 1002; b.score = 70;
c.num = 1003; c.score = 80;
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;
p = head;
do{
printf("%d%5.1f\n",p->num, p->score);
p = p->next;
}while(p != NULL);
}
9.7.3 处理动态链表所需函数
(1)malloc函数
void *malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size的连续空间。此函数的值(即返回值)是一个指向分配域起始空间的指针。如果此函数未能成功执行(如内存空间不足),则返回空指针。
(2)calloc函数
void *calloc(unsigned n, unsigned size);
其作用是在内存的动态区存储中分配n个长度为size的连续空间。函数返回一个指向分配域起始地址的指针;如分配不成功,返回NULL。
(3)free函数
void free(void *p);
其作用是释放由p指向的内存区,使这部分内存区能被其他变量所使用。p是最近一次调用calloc或malloc函数时返回的值。free函数无返回值。
9.7.4 建立动态链表
建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各节点数据,并建立起前后相链的关系。
如:写一函数建立一个有3名学生数据的单向动态链表。
#include <malloc.h>
#define LEN sizeof(struct student)
struct student
{
int num;
float score;
struct student *next;
};
int n;
struct student *creat(void){
struct student *head;
struct student *p1, *p2;
n = 0;
p1 = p2 = (struct student *)malloc(LEN);
scanf("%d,%f",&p1->num, &p2->score);
head = NULL;
while(p1->num != 0){
n = n + 1;
if(n == 1) head = p1;
else p2->next = p1;
p2 = p1;
p1 = (struct student *)malloc(LEN);
scanf("%d,%f",&p1->num, &p2->score);
}
p2->next = NULL;
return(head);
}
9.7.5 输出链表
首先,知道链表第一个结点的地址,也就是知道head的值;然后,设一个指针变量p,先指向第一个节点,输出p所指向的结点;而后,使p后移一个结点,再输出。直到链表的尾结点。
void print(struct student *head){
struct student *p;
printf("there %d records are : \n",n);
p = head;
while(p != NULL){
printf("%d%f\n",p->num,p->score);
p = p->next;
}
}
9.7.6 对链表的删除操作
例如:一队小孩(A,B,C,D,E)手拉手,如果其中一个小孩(C)想离队,而要求队形保持不变。只要将C的手从两边脱开,B改为拉D的手即可。
再例如:输入99103表示删除学号为99103的结点
1)如果删除的是第一个结点。这时需将头指针指向原有链表的第二元素。
2)要删除的不是第一个结点
代码部分:
struct student *del(struct student *head, long num){
struct student *p1, *p2;
if(head == NULL) printf("\nlist null\n");
p1 = head;
while(num != p1->num && p1->next != NULL){
p2 = p1;
p1 = p1->next;
}
if(num == p1->next){
if(p1 == head) head = p1->next;
else p2->next = p1->next;
printf("delete:%ld\n",num);
n = n - 1;
}
else printf("%ld not been found\n",num);
return(head);
}
9.7.7 对链表的插入操作
对链表的插入操作是指将一个结点插入到一个已有的链表中。
例如:假设有一群学生,按身高顺序(由低到高)手拉手排队,现在来了一位新同学,要求按身高顺序插入到队列中。
分析:先要确定插入到什么位置,在进行插入,最后确定插入的位置是否在第一个结点之前,或是链表之尾。
struct student *insert(struct student *head, struct student *stud){
struct student *p0, *p1, *p2;
p1 = head;
p0 = stud;
if(head == NULL){
head = p0;
p0->next = NULL;
}else{
while((p0->num > p1->num) && (p1->next != NULL)){
p2 = p1;
p1 = p1->next;
}
if(p0->num <= p1->num){
if(head == p1) head = p0;
else p2->next = p0;
p0->next = p1;
}else{
p1->next = p0;
p0->next = NULL;
}
}
n = n + 1;
return(head);
}
9.8 共用体
9.8.1 共用体的概念
几种不同类型的变量存放到同一段内存单元中,变量在内存中所占的字节数不同,但都从同一地址开始存放。也就是使用覆盖技术,几个变量互相覆盖。这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。
定义使用共用体类型变量的一般形式:
union 共用体名
{
成员列表}变量列表;
//例如
union data{
int i;
char ch;
float f;
}a,b,c;
//也可以将类型声明和变量定义分开
union data{
int i;
char ch;
float f;
};
union data a,b,c;
//也可以直接定义共用体变量
union {
int i;
char ch;
float f;
}a,b,c;
结构体变量所占内存长度为各成员占的内存长度总和。共用体所占内存空间为成员分量最长的分量所占的内存空间
9.8.2 共用体的变量的引用方式
只有定义了共用体变量才能使用它。
不能引用共用体变量,只能引用共用体变量中的成员。例如:
a.i //引用共用体变量中的整型变量i
printf("%d",a); //错误的
9.8.3 共用体类型的特点
1)同一个内存段可以用来存放几种不同类型的成员,但在一瞬间只能存放其中一种,而不是同时存放其中几种。也就是说,每一瞬间只有一个成员起作用,其他的成员不起作用,即不是同时都存在和都起作用。
2)共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。
3)共用体变量的地址和它的成员的地址是同一地址。
4)不能对共用体变量名赋值,也不能企图引用变量名来得到一个值,又不能定义共用体变量时对它初始化。
5)不能用共用体变量作为函数参数,也不能使函数带回共用体变量,但可以使用指向共用体变量的指针。
6)共用体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体的成员。
例如:设有若干个人员的数据,其中有学生数据和教师数据。学生数据中包括:姓名,号码,性别,职业,班级。教师数据包括:姓名,号码,性别,职业,职务。
num | name | sex | job | class position |
---|---|---|---|---|
101 | Li | f | s | 501 |
102 | Wang | m | t | prof |
#include <stdio.h>
struct{
int num;
char name[10];
char sex;
char job;
union{
int class_;
char position[10];
}category;
}person[2];
main(){
int n, i;
for(i = 0; i < 2; i++){
scanf("%d %s %c %c",&person[i].num, &person[i].name, &person[i].sex, &person[i].job);
if(person[i].job == 's') scanf("%d", &person[i].category.class_);
else if(person[i].job == 't') scanf("%s", &person[i].category.position);
else printf("input error");
}
printf("\nNo. Name sex job class/position\n");
for(i = 0; i < 2; i++){
if(person[i].job == 's') printf("%d %s %c %c %d\n",person[i].num,person[i].name, person[i].sex, person[i].job,person[i].category.class_);
else printf("%d %s %c %c %s\n",person[i].num,person[i].name, person[i].sex, person[i].job,person[i].category.position);
}
}
9.9 枚举类型
枚举是指将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。声明枚举类型用enum开头。例如:
enum weekday{
sun,mon,tue,wed,thu,fri,sat};
enum weekday workday,weekend;
//workday和weekend定义为枚举变量,它们的值只能是sun到sat之一
weekday = mon;
weekend = sun;
//也可以直接定义枚举变量
enum{
sun,mon,tue,wed,thu,fri,sat}workday,weekend;
1)在C编译中,对枚举元素按常量处理,它们不是变量,不能对其赋值,类似于: sun = 0;是错误的。
2)枚举元素作为常量是有值的,C语言编译按定义时的顺序使它们的值为0,1,2…,在上面的定义中,sun的值为0,mon的值为1,以此类推。如果有赋值语句: workday = mon;workday变量的值为1.这个整数是可以被诸如printf等函数输出的。也可以改变枚举元素的值,在定义时指定。
enum{
sun=7,mon=1,tue,wed,thu,fri,sat}workday,weekend;
//最后输出sun = 7,mon = 1,后面依次+1,sat = 6
3)枚举值可以用来,如:
if(workday == mon)...
枚举值的比较规则是按其在定义时的顺序号比较,如果定义时未人为指定,则第一个枚举元素的值认作0。
4)一个整数不能直接赋给一个枚举变量。
workday = 2;
//是不对的,它们属于不同的类型,应先进行强制类型转换。
workday = (enum weekday)2;
//相当于把值为2的枚举元素赋给workday
workday = (enum weekday)(5-3);
//赋一个表达式也是可以的
练习:
口袋中有红,黄,蓝,白,黑5个颜色的球若干,每次从口袋中先后取出3个球,问得到3种不同色的球的可能取法,输出每种排列的情况。
#include<stdio.h>
int main()
{
enum Color {
red, yellow, blue, white, black};
int i, j, k, pri;
int n, loop;
n = 0;
for(i = red; i <= black; i++)
for(j = red; j <= black; j++)
if(i != j)
{
for(k = red; k <= black; k++)
if((k != i) && (k != j))
{
n = n + 1;
printf("%-4d", n);
for(loop = 1; loop <= 3; loop++)
{
switch(loop)
{
case 1: pri = i; break;
case 2: pri = j; break;
case 3: pri = k; break;
default: break;
}
switch(pri)
{
case red:printf("%-10s", "red"); break;
case yellow:printf("%-10s", "yellow"); break;
case blue:printf("%-10s", "blue"); break;
case white:printf("%-10s", "white"); break;
case black:printf("%-10s", "black"); break;
default: break;
}
}
printf("\n");
}
}
printf("\ntotal: %5d\n", n);
return 0;
}
9.10 用typedef定义类型
可以用typedef声明新的类型名来代替已有的类型名。例如:
typedef int INTEGER;
//以下两句是等价的
int i,j;
INTEGER i,j;
声明一个新的类型名的方式是:
按定义变量的方式写出定义体(如:int i;),将变量名换成新的类型名,再在前面加typedef,之后就可以用新类型名去定义变量。
例如:(对数组类型的定义类型)
按定义变量的方式写出定义体 : int n[100],将变量名换成新的类型名int NUM[100],再在前面加typedef, typedef int NUM[100],之后就可以用新类型名去定义变量,NUM n。
习惯上把用typedef声明的类型名用大写字母表示,以便于系统提供的标准类型标识符区分。
注意:
1)用typedef可以声明各种类型名,但不能定义变量。用typedef可以声明数组类型,字符串类型,使用比较方便。
typedef int ARR[10];
ARR a,b,c,d;
//typedef可以将数组类型和数组变量分开,利用数组类型可以定义多个数组变量
2)用typedef只是对已经存在的类型增加一个类型名,而不是创造一个新的类型
3)typedef和#define有相似之处
typedef int COUNT;
#define int COUNT
define 是宏定义,在预编译时处理,typedef是类型定义 相当于给变量起别名,是在编译时处理。 define 是简单的字符串替换 ,而typedef是真正意义上的类型定义。#define 后面没有分号,typedef后面有分号。
4)当不同的源文件中用到同一类型数据时,常用typedef声明一些数据类型,把它们单独放到一个文件中,然后在需要用到它们的文件中用#define命令把它们包含进来。
5)使用typedef有利于程序的通信与移植。有时程序会依赖于硬件特性,用typedef便于移植。
例如:有的计算机系统int型数据用两个字节,数值范围为-32768~32767,而另外一些机器则以4字节存放一个整数,数值范围为+21亿。如果把一个C程序以4个字节存放整数的计算机系统移植到以2个字节存放整数的系统,按一帮方法需要定义变量中的每个int改为long。如果程序中有多处使用int定义变量,则需要改动很多次,但使用typedef可以定义为:
typedef int INTEGER;
typedef long INTEGER;