自定义类型:结构体、枚举、联合

一.结构体

1.结构体的基本概念

结构体是不同类型元素的集合。

2.结构体的声明

struct tag

{

member_list;

}variiable-list;

其中,struct是关键字;tag相当于结构体的标签,可以省略;member-list是成员列表,至少要有一个成员;variable-list是变量列表,可以定义结构体变量,这里也可以不写。注意:大括号后面的“;”不能省略!

例如:

struct Stu

{

   int num[20];

   char sex;

   char name[30];

};//在主函数中定义struct Stu zhangsan即定义了一个结构体变量zhangsan.

又如:

struct

{

 int num[20];

   char sex;

   char name[30];

}x,*p,**pp;//在这里定义了一个匿名结构体,其中,x是结构体变量,p是指向结构体变量的指针,pp是指向结构体变量指针的指针变量。在主函数中,可以定义p=&x;pp=&p.
注意:如果两个结构体完全相同,仍然要把它们当作两个结构体。如果在一个结构体里定义了一个结构体变量,另一个定义结构体指针变量,这个指针是不能指向那个结构体变量的。如:

struct Stu

{

   int num[20];

   char sex;

   char name[30];

}stu1;

struct Stu

{

   int num[20];

   char sex;

   char name[30];

}*p;
p=&stu1;//这种定义是不对的。
3.结构体的自定义
结构体的自定义就是指在结构体内包含一个类型为结构体本身的成员。如何定义这种结构体呢?
方案一:

struct Stu

{

   int num[20];

   char sex;

   struct Stu stu1;//这种定义不对,因为定义变量就要开辟空间,而在这里,由于之前未被定义,所占内存大小不确定。

}
方案二:

struct Stu

{

   int num[20];

   char sex;

   struct Stu * p;//定义了一个结构体指针变量,可以确定开辟空间的大小,可行。

}

4.结构体成员的访问

访问方式有两种,一种是结构体变量点操作符(.),另一种是结构体指针指向操作符(->)。

如:

struct Stu

{

   int num[20];

   char sex;

   char name[30];//char *name;

}*p,s;
.成员访问方式:strcpy(s.name,"zhangsan")//注意,s.name="zhangsan"和s.name[32]这种赋值是错误的。但是如果定义为字符指针变量,就可以用s.name="zhangsan";这种方式。

->指向操作符:p->name和(*p).name都表示name这个成员。

5.结构体变量的定义和初始化

结构体可以整体初始化,但不能整体赋值,与数组类似。

结构体的定义可以在定义结构体类型时的可变参数列表定义变量,也可以在主函数中定义。

(1)在定义可变参数列表中定义

struct Stu

{

  char name[32];

  int age;

  char sex;

}stu1;//定义结构体变量stu1 

(2)在主函数中定义

struct Stu

{

  char name[32];

  int age;

  char sex;

}

int main()

{

 struct Stu stu1;//定义结构体变量stu1,变量的类型是struct Stu

.........

}

有如下几种方式:

(1)在定义可变参数列表时初始化

struct Stu

{

  char name[32];

  int age;

  char sex;

}stu1={"zhangsan",19,'m');

(2)在主函数中定义变量时初始化

struct Point

{

   int x;

   int y;

};

int main()

{

  struct Point p1={3,4};

   ......

}

6.结构体的内存对齐

(1)存在内存对齐问题的原因:

平台原因(硬件原因):不会所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址上取某些特定类型的数据,否则抛出硬件异常。

(2)性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次。如:当规定只能从4的倍数字节处访问,定义char a,int b;内存对齐和不对齐的情况下对b的访问如下:

内存不对齐:                                                                                  内存对齐:

自定义类型:结构体、枚举、联合                   自定义类型:结构体、枚举、联合

可以看到,当内存不对齐时,CPU对b的访问需要两次,而当内存对齐时,CPU对b的访问只需要一次。内存对齐可以减少访问次数,提高性能。本质上是以牺牲空间为代价,减少访问的时间。

结构体的内存对齐规则:

1.第一个成员在结构体变量偏移量为0的地址处。

2.其它成员变量要对齐到对齐数的整数倍处。其中,对齐数=min{编译器默认的一个对齐数,该成员大小}。VS中默认数是8,Linux默认为4.

3.结构体总大小为最大对齐数的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍。

例题:(在VS下)

1.struct S1

{

 char c1;对齐数是1    1

 int i;//对齐数是4    1+3=4

 char c2;//对齐数是1   1+3+4+1=9

}//9不是4的整数倍。所以结构体的大小是12.

2.struct S3

{

   double d;//对齐数是8  8

   char c;//对齐数是1  1+8=9

  int i;//对齐数是4   1+8+3+4=16,16是8的倍数。

};//所以结构体S3的内存大小是16

struct S4

{

  struct S3 arr[3];//每个元素的对齐数是16,最大对齐数是8,16*3=48

 char x;//对齐数是1,48+1=49

 struct S3 y;//对齐数是16,最大对齐数是8, 49+7+16=72 

 char a[3];//每个元素的对齐数是1,72+1*3=75

 double d;//对齐数是8,75+5+8=88

};//结构体S4 的内存大小是88个字节。

拓展:修改默认对齐数:如:#pragma pack(1)//把默认对齐数修改为1。注意:修改的数只能是1,2,4,8,16.

#pragma (1)

#pragma()//恢复为原来的默认对齐数

7.结构体传参

在结构体传参时,不会发生降维问题。而在传参时就会开辟新的空间,形成临时变量。如果结构体过大,则会造成很大的开销,所以建议结构体传参时最好传结构体指针。

二.位段

1.位段与结构体的区别

位段的声明和结构体类似,但是有所区别,体现在1、位段的成员只能是整型(int、char、long、unsigned int)2、位段的成员名后边有一个冒号和一个数字。

2.举个例子

如:

struct A

{

  int _a:2;//表示_a只占32个比特位中的2个比特位

  int _b:5;//表示_b只占32个比特位的5个比特位

  int _c:10;//表示_c只占32个比特位的10个比特位

  int _d:30;//表示_d只占32个比特位的30个比特位

};

位段的存储是一种压缩存储,严格依赖于计算机的内部结构、硬件设置。

以下是上述例子的一种存储情况:

自定义类型:结构体、枚举、联合

可以看到,在给a,b,c分配完空间后,剩下的空间不够分配d,所以重新开辟4个字节的空间,所以该位段只占8个字节。

在位段中的读取:

自定义类型:结构体、枚举、联合

val._a=0,化为二进制是00(因为只占两个比特位),在读取时,还是00,所以输出为0;val._b=511,化为二进制是1 1111 1111,因为b只占5个字节,所以实际上在内存中的存储应该是11111,在读取时,因为最高位是1,所以按负数来读,减1:11110,取反:10001,所以读val._b是-1;val._c=0,在内存中的存储是00 0000 0000,读取时还是00 0000 0000,所以val._c=0.

3.位段的几点说明

(1)位段涉及很多不确定因素,它的可移植性较差,位段是不跨平台的。

(2)位段的成员是整型家族:int、char、unsigned char、unsigned int、long等。

(3)位段的空间是按照定义以4个或1个或8个字节来开辟空间的。

(4)位段的跨平台问题:a.int位段被当成有符号数还是无符号数是不确定的。

b.位段中最大位的数目是不确定的。(如果是16位机器,则最大位是16;如果写成大于16的数字,则会出问题。如果是32位机器,则最大位是32。)

c.位段中的成员是从左往右还是从右往左分配也要根据平台而定。

d.当一个结构包含两个位段,第二个位段较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这也是不确定的。

三.枚举

1.定义:

enum tag

{

  枚举常量列表(每个枚举常量占一行,常量间用“,”隔开,最后一个不用写“,”

}变量列表;

如:enum Sex

{

   MALE,//默认是0

   FEMALE,//默认是1

   SECRET//默认是2

};

又如边定义边赋值:

enum Sex

{

   MALE=1,

  FEMALE=4,

 SECRET=8

};

2.关于枚举的几点说明:

a.枚举类型的变量只能被赋值为常数,可以是枚举定义中的常数,也可以自己赋值。

b.如果在枚举定义时,各枚举常量未被赋值,则默认第一个常量是0,从后依次加1.

c.宏和枚举的区别:

宏是简单的文本替换,没有语法上的检查。

枚举是一种类型,有类型上的检查。

四.联合

1.联合的定义:

union tag

{

  成员列表;

}变量列表;

如:

union Un

{

  int i;

  char c;

};

2.联合的特点:

联合体共用一段空间,联合体的大小至少是最大成员的大小。当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。

联合体有多个变量,谁用的时候谁就是第一个变量,从第一个字节开始。所以所有的变量的地址相同。

3.联合体的应用:可以判断大小端。

自定义类型:结构体、枚举、联合