C结构体和共用体

结构体

前面的数组是一组具有相同类型数据的集合,但是在实际编程过程中,我们往往还需要一组不同类型的数组,比如学生的信息登记表,姓名为字符串,学号为整数,性别为字符串等等;这种情况下,因为每个数据的类型不同,这时候显然不能用数组来保存;

因此在C语言中,可以通过创建结构体来保存一组不同类型的数据,也就是说结构体中的成员的数据类型可以不同,结构体的定义语法为:

struct 结构体名{
    成员1;
    成员2;
    ...
};

也就是说,结构体是一种集合,里面包含了多个成员成员的数据类型可以不同,如下的例子:

struct stu{
    char *name;
    int num;
    int age;
    char group;
    float score;
};

成员可以是任意类型的数据,当然也可以是结构体;前面用到的 int float 等是C本身提供的数据类型,我们称之为基本数据类型,而结构体可以包含多个基本类型数据,也可以包含其他结构体,我们称之为复杂数据类型;

注意在定义结构体时,不能将结构体中的成员进行初始化!!

结构体变量

既然结构体是一种数据类型,那么就可以用定义的结构体类型来定义结构体变量,如下:

struct stu stu1, stu2;//注意最前面的 struct 不能省略

上面定义了两个结构体变量 stu1和stu2,类型为 struct stu;stu 就像是一个 “模板”,定义出来的变量都有相同的属性;也可以在定义结构体类型的同时定义结构体变量,如下:

struct stu{
    char *name;
    int num;
    int age;
    char group;
    float score;
} stu1, stu2;

struct {//没有写stu,这样做的话并没有结构体名,导致后面无法再定义该结构体的结构体变量
    char *name;
    int num;
    int age;
    char group;
    float score;
}stu1, stu2;

这两种方法都可以定义结构体变量,但是还是推荐先定义结构体,然后再定义结构体变量的方式,这种看起来比较简洁明了;

需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;在定义了结构体变量后,系统才会为之分配内存单元,理论上上面的结构体变量 stu1 占的字节个数为 4+4+4+1+8=21,但是实际上为24,如下:

这是因为计算机对内存的管理是以字为单位的,一个 word 为4个字节,所以对上面的 group 虽然理论上只占一个字节,但是这个 word 中的后三个字节并不会接着存放 score 成员,而是从下一个 word 开始存放,所以用 sizeof 运算符测量 stu1 的长度时,得到的是24;

结构体变量的初始化和引用

在定义结构体变量时,可以对它初始化,初始化可以对整个结构体一起进行,也可以对单个成员变量,如下:

如果在定义结构体变量时没有初始化,那么后面给结构体变量的成员赋值时只能单个赋值,而不能将结构体整体赋值,如下面的写法是错误的:

可以通过结构体变量来引用结构体中的成员,语法如下:

结构体变量名.成员名;

"." 是成员运算符,在所有运算符中优先级最高,如上面的第13行给成员 age 赋值为20;

注意不能企图通过结构体变量名来一起输出结构体中的所有成员,只能对每个成员进行单独操作!!同时,不能再scanf 函数中使用结构体变量名一揽子输入全部成员的值,只能一个一个输入;看下面例子:

 可以看到上面的scanf函数中,成员 num 和 score 都有取址符 &,而 name 没有,这是因为 name是数组名,本身就代表地址;

结构体数组

所谓结构体数组,就是指数组的每一个元素都是一个结构体;在实际应用中,结构体数组常用来表示一个拥有相同数据结构的群体,比如一个班的学生;定义结构体数组与定义数组的语法相同,看下面例子:

 结构体指针

所谓结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针;如果将结构体变量的起始地址存放在指针变量中,那么这个指针变量就指向该结构体变量;看下面例子:

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

可以看到,在给结构体指针赋值时,需要在结构体变量名前加取址符 & ,这是结构体和数组不一样的地方;

数组名表示数组的首地址,而结构体变量名不是结构体的首地址,要想获取结构体变量的地址,必须在前面加 &

如何通过结构体指针来访问成员?有两种方法:

(*pointer).member

或

pointer -> member

 第一种写法中,. 的优先级高于 *,所以 (*pointer) 两边的括号不能省略;

第二种写法常用, ->称为箭头,指针可以通过箭头来直接对结构体成员操作;

上面的结构体指针指向了一个结构体变量,同时也可以用结构体指针指向一个结构体数组,如下:

 

上面的第21行中给结构体指针ps赋初值为 sts,也就是第一个元素的起始地址,然后在第25行的for循环中每次使得 ps 加1,此时指向了下一个元素的起始地址;

注意

1)如果 ps 的初值为数组名,如上面的 sts,则 ps 指向 sts的第一个元素,其值为第一个元素的起始地址,ps++后就指向了下一个元素,这对任意指向数组的指针都是成立的;

2)上面的 ps 的类型为指向结构体类型 struct stu的指针变量,不能用它来指向结构体的某个成员,如下面的用法是不对的:

ps = sts[0].name;

如果要将某一个成员的地址赋给ps,可以使用强制类型转换,如下:

ps = (struct stu *)stu[0].name;

此时ps 的值为stu[0]元素的成员 name 的起始地址,可以使用 printf("%s\n", p)输出;

用结构体变量和结构体变量的指针作函数参数

将一个结构体的值传递给函数,有三个方法:

1)用结构体变量的成员作参数,如用 stu[1].name作函数实参,这与普通变量的传递是一致的,都是 值传递 方式,应当保持实参与形参的类型一致;

2)用结构体变量作实参,也是属于 值传递 方式,形参也必须是相同的结构体类型,这种方法在空间和时间上开销较大,很少使用;

3)用结构体指针作实参,将结构体变量的地址传递给形参,这种方法常用;

上面第26行函数的参数为结构体指针变量ps,然后在22行调用时传入的对应参数为结构体数组名,代表数组第一个元素的首地址,打印结果如下:

 共用体

与结构体类似的是共用体(union)或叫联合,定义格式为:

union 共用体名{
    成员列表
};

 结构体与共用体的区别

1)结构体的每个成员都会占用一段不同的内存,互相之间没有影响;而共用体的所有成员共同占用一段内存,修改一个成员会影响其余成员;、

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

也就是说,共用体是用一段内存来存放不同类型的变量;例如把一个短整型变量、一个字符型变量、一个实数型变量存放在同一个人地址开始的内存单元中,如下图:

 以上3个变量在内存中占用的字节数是不同的,但是起始地址都是从0x1000开始,也就是使用覆盖技术,后一个数据覆盖了前面的数据,这就是结构体类型;

 共用体变量

共用体也是一种自定义类型,可以通过它来创建共用体变量,如下:

union data{
    int n;
    char ch;
    double f;
};

union data a, b, c;//创建共用体变量 a b c

引用共用体变量的成员方法与结构体相同,都是使用 . ,格式如下:

结构体变量名.成员名

 

 

 上面的共用体 data 中成员 f 的类型为 double,占用最多的字节数8,所以 data 类型的变量也占用8个字节;

在使用共用体时需要注意以下几点:

1)共用体使用同一段内存来存放不同类型的成员,但是在每个瞬间只能存放其中一个成员,而不是几个,如上面第13行的打印结构,多次给共用体的成员赋值后,得到的最终值是最后的赋值;

2)可以对共用体进行初始化,但是初始化列表中只能有一个常量,如下:

union data{
    int n;
    char ch;
    double f;
};

union data a = {16};//正确
union data b = {16, 'a', 13.22};//错误

3)共用体变量的地址和它各个成员的地址都是同一个地址,如上面&a、&a.n、&a.ch、&a.f 都是同一个值

4)除了初始化外,不能对共用体变量赋值,只能对共用体成员赋值

枚举类型

如果一个变量只有几种可能的值,则可以定义为 枚举 类型;所谓枚举,就是把可能的值一一列举出来,变量的值仅限于列举出来的范围内;声明枚举变量的格式为:

enum 枚举名 {枚举元素列表};

eg:
enum Weekday {sun, mon, tue, wed, thu, fri, sat};//定义枚举类型
enum Weekday workday;//定义枚举变量

 

 说明:

1)c编译时将枚举元素列表中的每一个枚举元素当做常量处理,在定义完枚举类型后就不能对枚举元素赋值了,下面的语句是错误的:

enum Weekday {sun, mon, tue, wed, thu, fri, sat};//定义枚举类型
sun = 1;//不能给枚举常量赋值

2)对于枚举元素,每一个元素都代表一个整数,它们的值按照顺序依次增加,如上面的 sun 默认值为0,mon 默认值为1...

3)可以在定义枚举类型时给其中的枚举元素赋值,但是要注意前面的元素的值必须要小于后面的元素的值,如下:

enum Weekday {sun=1, mon=3, tue, wed, thu, fri=9, sat};//定义枚举类型

//tue 默认为4,sat默认为10

 用typedef 声明新类型

除了可以使用C提供的标准类型(如int char float)和程序编写者自己声明的结构体、共用体、枚举外,还可以使用 typedef 指定新的类型名来代替已有的类型名;有以下两种情况:

1)简单地用一个新的类型名来代替原有的类型名,如:

typedef int Integer;//指定用Integer来代替int
Integer a;//相当于 int a

 2)命名一个简单的类型名来代替复杂的类型名,如:

typedef int Num[100];//声明Num为整型数组类型名
Num a;//相当于 int a[100]

简单来说,就是按照变量定义的方式,将变量名换为新类型名,并且在最前面加上 typedef ,这就声明了新类型名代表原来的类型;

习惯上,通常把typedef 定义的类型名的第一个字母用大写表示,以便区别

 

 typedef 与 #define 的区别

ypedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西;如下:

#define INTERGE int
unsigned INTERGE n;  //没问题

typedef int INTERGE;
unsigned INTERGE n;  //错误,不能在 INTERGE 前面添加 unsigned

 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证;例如:

#define PTR_INT int *
PTR_INT p1, p2;

经过宏替换以后,第二行变为:
int *p1, p2;

这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型;相反,在下面的代码中:

typedef int * PTR_INT
PTR_INT p1, p2;

 p1、p2 类型相同,它们都是指向 int 类型的指针;

再来看下面一个例子:

#inlcude <stdio.h>

#define base 0x0012ff60
#define flash ((TestType)* base) //这里定义了一个结构体指针flash,并将结构体指针flash的值赋值为0x0012ff60

typedef struct
{
    int i;//假设int 类型占4个字节,那么 i 的地址为0x0012ff60,j的地址为0x0012ff64,依次类推
    int j;
    int k;
}TestType;

int main(void)
{
    flash -> i = 0;
    flash -> j = 1;
    flash -> k = 2;

    printf("%x \n", flash -> i);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值