在C语言当中,除了我们常用的几个基本的数据类型之外,还有一种类型叫自定义类型。比如:我们要描述一个学生。这个学生有姓名,性别,年龄,身高等。单独用基本的数据类型是不能完全描述的。这个时候就要使用我们自定义的类型来进行描述。自定义的类型有结构体,枚举和联合体。
一、结构体
结构体时一些值得集合,这些值被称为成员变量,每一个成员变量可以有不同的类型。
1,声明
struct tag//这里的tag表示标签,不是变量名
{
member-list;//成员变量
}variable-list;//变量列表,用来定义变量。注意这里的分号
比如,描述一个人:
struct person { char name[20]; char sex[5]; int age; char nation[20]; };
这里的变量列表可以没有。在变量列表里面定义的变量是全局变量。
2,定义且初始化
要定义,可以直接在变量列表里面,也可以在main函数里面定义。
struct person { char name[20]; char sex[5]; int age; char nation[20];
}person1;//结构体变量person1 struct person person2;//结构体变量person2 int main(void) {
struct person person3;//结构体变量person3 struct person
person4[2];//结构体数组变量person4,有两个元素,都是结构体类型 return 0; }
要初始化也很简单。
#include<stdio.h> struct person { char name[20]; int age; }person1 =
{"zhangsan", 18};//结构体变量person1 struct person person2 = {"lisi",
18};//结构体变量person2 int main(void) { struct person person3 = {"wangwu",
18};//结构体变量person3 struct person person4[2] = {"abc", 18, {"def",
18}};//加不加{}都可以 return 0; }
有的时候,结构体还会出现嵌套的情况:
struct person { char name[20]; int age; }person1 = {"zhangsan",
18};//结构体变量person1 struct people { struct person person; char nation[20]; };
对其进行初始化:
struct people people = { {"zhangsan",18}, "XXX" };//嵌套结构体的初始化
我们在声明结构体的时候,也可以不完全声明,这个时候省略掉标签。
struct { int a; char b; float c; }a;
有一种情况是不可以省略标签的,即使用typedef关键字的时候。typedef关键字能够为一种数据类型定义一个新名字。如果省略,就会报错。
typedef struct S { int data[1000]; int num; }s; s s1 = { {1,2,3,4}, 1000 };
3,结构体成员的访问
结构体成员的访问需要通过点(.)操作符来进行访问的。下面我们来打印这几个初始化的变量。
#include<stdio.h> struct person { char name[20]; int age; }person1 =
{"zhangsan", 18};//结构体变量person1 struct people { struct person person; char
nation[20]; }; struct person person2 = {"lisi", 18};//结构体变量person2 int
main(void) { struct person person3 = { "wangwu", 18 };//结构体变量person3 struct
person person4[2] = {"abc", 18, {"def", 18}};//加不加{}都可以 struct people people =
{ {"zhangsan",18}, "XXX" };//嵌套结构体的初始化 printf("%s\n", person1.name);
printf("%d\n", person1.age); printf("--------------\n"); printf("%s\n",
person2.name); printf("%d\n", person2.age); printf("--------------\n");
printf("%s\n", person3.name); printf("%d\n", person3.age);
printf("--------------\n"); printf("%s\n", person3.name); printf("%d\n",
person3.age); printf("--------------\n"); printf("%s\n", person4[1].name);
printf("%d\n", person4[1].age); printf("--------------\n"); printf("%s\n",
people.person.name); printf("%s\n", people.nation); return 0; }
现在,我们来看看结构体指针访问指向的变量的成员。有以下代码:
struct Stu { char name[20]; int age; }; void print(struct Stu* ps) {
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员,为了简化,使用->操作符来替代(*).操作。 printf("name = %s age = %d\n",
ps->name, ps->age); } int main() { struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参 return 0; }
#include<stdio.h> struct person { char name[20]; int age; }; struct people {
struct person* person; char nation[20]; }; int main(void) { struct person
person = { "zhangsan", 18 }; struct people people = { &person ,"XXX"};
printf("%s\n", people.person->name); printf("%d\n", people.person->age);
printf("%s\n", people.nation); return 0; }
4,结构体传参
struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4}, 1000 };
//结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void
print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s);
//传结构体 print2(&s); //传地址 return 0; }
在传参的时候,参数需压栈。传递一个结构体对象的时候,由于结构体过大,参数在压栈的时候的系统开销比较大,导致性能的下降。另外,在计算结构体的大小的时候,如果是传的结构体的值,会导致结构体的大小无限的增大。所以,结构体在传参要传地址。
5,结构体内存对齐
现在,我们来讨论结构体的内存大小。
5.1,来看下面这段代码,计算它的大小(*):
struct S1 { char c1; int i; char c2; }; int main(void) { printf("%d\n",
sizeof(struct S1)); return 0; }
我们发现,这个结构体的大小并不是我们认为的6个字节,而是12个字节。
结构体内存的计算如下:
1,第一个成员在与结构体变量偏移量为0的地址处。
2,其他的成员变量要对齐到对齐树的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8,有的默认为4。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
简单来说,就是:在内存里面,第一个变量放在开始的0地址处,其他的要放在什么地址处,要看对齐数。对齐数是默认对齐数和成员大小的最小值。按对齐数放了之后,这个结构体的大小是最大的对齐数的整数倍。
计算下面嵌套结构体的大小:
struct S1 { char c1;//1 int i;//4 };//大小为8 struct S2 { char c1;//1 struct S1
s1;//8 int d;//4 }; int main(void) { printf("%d\n", sizeof(struct S2)); return
0; }
在计算的时候,不可以直接把成员变量的大小直接加起来,在取对齐数的最大值倍数处。比如第一个计算(*)处:加起来是6,在取对齐数(4)的整数倍,结果是8,这显然和计算的结果不符合。
在来看最后一道:
struct S { short s1;//2 char c1;//1 int s2;//4 }; int main(void) {
printf("%d\n", sizeof(struct S)); return 0; }
如果做的熟练了,也不必要画出图来,直接进行计算就可以了。有的时候会遇到short和char在一起的情况,这个时候,可以直接看成4个大小。
5.2,存在内存对齐的原因:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。这种做法是很常见的。
我们在设计结构体的时候,既要满足对齐,又要节省空间,那么就让占用空间小的成员尽量集中在一起。如果我们将(*)处的顺序变一下,比如两个char型放在一起,这个结构体的大小就会发生变化。
5.3,修改默认对齐数
#pragma pack(1)//设置默认对齐数为1 struct S2 { char c3; int i2; char c4; }; int main()
{ printf("%d\n", sizeof(struct S2));//6 return 0; }
还原为默认:
#pragma pack()//取消设置的默认对齐数,还原为默认
二、位段
1,定义和声明
结构体可以实现位段。位段的声明和结构体类似。位段是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。采用位段结构既能够节省空间,又方便于操作。 比如位段A:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
位段的成员名后边有一个冒号和一个数字。后面数字的单位是比特。位段的成员必须是整型家族。
2,位段的内存分配
位段的空间上是按照需要以4个字节( int )或者1个字节( char
)的方式来开辟的。前面的位段A种,_a是int类型,是以4个字节开辟的。那么A的大小是多少(32位)?
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() {
//输出的结果是什么? printf("%d\n", sizeof(struct A)); return 0; }
这是如何计算的?
在这里,_a开辟了4个字节的大小,但是_a只需要两个比特就够了。在32位下,还有30个比特。_b需要5个比特,还剩下25比特,_c需要10个比特,这样就剩下15个比特,_d需要30个比特,剩下的15个比特不够,就在开辟4个字节大小的空间来存放_b。这里的图假设是从左开始放。
我们来看这一道题(在VS上是从右开始放):
struct S { char a : 2; char b : 3; char c : 4; char d : 5; }; int main(void) {
struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4;//在内存中是什么样的 return 0; }
3,位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
三、枚举
枚举,就是一一列举。将可能的值进行一一列举。
1,定义:
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Day//星期 { Mon,
Tues, Wed, Thur, Fri, Sat, Sun }; int main(void) { printf("%d\n", Mon);
printf("%d\n", Tues); printf("%d\n", Wed); printf("%d\n", Thur); printf("%d\n",
Fri); printf("%d\n", Sat); printf("%d\n", Sun); return 0; }
以上定义的 enum Day是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量
。这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
enum Day//星期 { Mon, Tues, Wed, Thur = 6, Fri, Sat, Sun };
2,优点
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
3,使用
在switc语句当中,如果我们的分支太多,case后面的标签又是数字,这个时候,要知道这个数字代表什么,就需要去找。但是如果我们使用enum,就可以把数字换成让人一眼便知的常量名,大大提高代码的可读性。
enum Game { EXIT, GAME_BEGIN, GAME_DISCONTINUE }; int main(void) { switch (1)
{ case GAME_BEGIN: printf("你开始了游戏!\n"); case GAME_DISCONTINUE:
printf("你中止游戏!\n"); case EXIT: printf("你退出了游戏!\n"); } return 0; }
四、联合体(共用体)
1,定义:
联合体也是一种特殊的自定义类型,这种类型定义的变量包含一系列的成员。这些成员是公用一块空间,所以也叫共用体。联合体的定义和结构体类似。
//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un;
2,特点:
联合体是共用一块空间的,这样,这个联合变量的大小,至少是最大成员的大小,才能存放最大的那个成员。
#include<stdio.h> union Un { int i; char c; }; union Un un; int main(void) {
// 下面输出的结果是一样的吗? printf("%p\n", &(un.i)); printf("%p\n", &(un.c));
//下面输出的结果是什么? un.i = 0x44; un.c = 0x55; printf("%x\n", un.i); return 0; }
从这里可以看出联合体的特点:共用一块空间。正因为如此,所以在打印un.i的时候,会发现结果是0x55。这里的0x44被覆盖了。由联合体的特点,可以用来判断当前计算机的大小端存储。
union Un { int i; char ch; }; int main(void) { union Un un; un.i = 1;
printf("%d\n", un.ch);//1,是小端 return 0; }
3,联合体大小的计算
联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1 { char c[5];//共5个元素,每个占1个字节,总的大小为5 int i;//4 }; int main(void) {
printf("%d\n", sizeof(union Un1));//8,是最大对齐数4的倍数。注意不是5,这里的5是数组总的大小 return 0; }
union Un2 { short c[7];//2*7 int i;//4 }; int main(void) { printf("%d\n",
sizeof(union Un2));//16 return 0; }