一、结构体
1、结构体类型的声明
<1>结构体:结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
<2>格式:
struct tag
{
member-list;
}variable-list;//全局变量
<3>在结构体类型后面定义的变量是全局变量,在main函数中或者函数中创建的变量是局部变量
<4>特殊的声明:在声明结构的时候,可以不完全的声明(省略标签)//匿名结构体类型
如:
struct
{
int a;
char b;
float c;
}x;//省略了标签的结构体一般是只可以使用一次
再如:
struct
{
int a;
char b;
float c;
}* p;
那么p=&x;//这是不对的,因为结构体省略了标签,编译器会将两个声明当成是完全不同的两个声明,所以这是非法的。
2、结构的自引用
<1>结构体的成员列表中可以放结构体类型的成员(除自身之外的结构体)
<2>如果要放一个与自身结构体相关的成员,正确的方式如下:
如:
struct Node
{
int data;
struct Node* next;
};
如下一种写法是不对的:
typedef struct
{
int data;
Node* next;
}Node;
因为将这个匿名结构体重命名为Node是在有了这个结构体之后,而这个结构体的成员就使用了这个重命名,所以这个写法是错误的,因此做了如下改进:
typedef struct Node
{
int data;
struct Node* next;
}Node;
这个写法是正确的。
3、结构体变量的定义和初始化
①如:
struct Node
{
int data;
char ch;
}n1={10,'m'}; //结构体类型变量的定义和初始化,这是在声明的同时定义并初始化的(全局)变量
struct Node n2={9,'x'}; //结构体类型变量的定义和初始化,这是在函数中定义的(局部)变量。
②如果只定义了变量并未初始化,那么在后来初始化时就不能写成n2={9,‘x’};而是需要将结构体中的内容逐一初始化。
4、结构体内存对齐(由计算结构体大小引出)
<1>结构体内存对齐的规则
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值
VS中默认的值为8
③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
有如下分析:
第一个
#include<stdio.h>
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n",sizeof(struct s1));//结果为12
return 0;
}
原因分析:1 _ _ _;1 1 1 1;1 _ _ _
c1在与结构体变量偏移量为0的地址处,而 i 的大小是4,VS的默认对齐数为8,所以对齐数为4,所以 i 要放在偏移量为4的整数倍的地址中去,所以 i 放在了与结构体变量偏移量为4的地址处,而c2的大小为1,VS的默认对齐数为8,所以对齐数为1,而放完前两个变量之后到了偏移量为9的地址处,而9又恰好是1的整数倍,所以此位置放上了1,而要求结构体的总大小为最大对齐数的整数倍,最大对齐数为4,所以要到偏移量为11处才是整个结构体的大小,为十二个字节。
第二个
#include<stdio.h>
struct s3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n",sizeof(struct s3));//结果为16
return 0;
}
第三个
#include<stdio.h>
struct s3
{
double d;
char c;
int i;
};
struct s4
{
char c1;
struct s3 s3;
double d;
};
int main()
{
printf("%d\n",sizeof(struct s4));//结果为32
return 0;
}
<2>结构体内存对齐的原因(内存对齐就是拿空间换时间)
①平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次。
<3>既满足对齐又节省空间的方法
让占用空间小的成员尽量集中在一起
如:
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
<4>修改默认对齐数
①我们可以用#pragma这个预处理指令来修改我们的默认对齐数。
②应用在对齐方式不合适的时候,那时我们便可以自己更改默认对齐数。
③修改的对齐数一般是2的次方倍。
如:
#include<stdio.h>
#pragma pack(8)//设置默认对齐数
struct s1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消默认的对齐数,将对齐数还原为原本默认的。
#pragma pack(1)
struct s2
{
char c1;
char c2;
int i;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct s1));//结果为12
printf("%d\n", sizeof(struct s2));//结果为6
return 0;
}
<5>offsetof
我们可以使用这个函数来计算结构体的每个成员变量相对于结构体变量的偏移量
5、结构体传参
<1>结构体传参也分为传值和传址
<2>一般我们选用传址,因为函数传参的时候参数是需要压栈的,因此会有时间和空间上的系统开销,如果传递的结构体对象比较大,将会导致系统开销大,从而导致性能的下降。
6、结构体实现位段(位段的填充&可移植性)
<1>位段:位段的声明和结构是类似的,但是有两点的不同
①位段的成员必须是char、int 、unsigned int或signed int
②位段的成员名后边有一个冒号和一个数字。
<2>位段的内存分配
①位段的成员可以是char、int、unsigned int或者是signed int的类型
②位段的空间是按照需要,以四个字节(int类型)或者1个字节(char类型)来开辟的。
③位段涉及很多不确定的因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
如下代码:
#include<stdio.h>
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
printf("%d\n",sizeof(struct A));//结果为8
return 0;
}
代码分析:首先开辟了四个字节的空间,a占了两个比特位,b占了5个比特位,c占了10个比特位,当d占据30个比特位的时候前面开辟的四个字节空间明显不足了,因此需要再开辟四个字节的空间来存放d
<3>位段的跨平台问题
①int位段被当成有符号数还是无符号数是不确定的。
②位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器上会出现问题)
③位段中的成员是从左向右分配内存还是从右向左分配内存标准尚未定义。
④当一个位段包含两个位段成员时,一个位段成员占据一定比特位之后还剩余一定的比特位,而存放下一个位段成员时剩余的比特位是不足的,因此需要开辟新的空间,而开辟新的空间之后,存放下一个位段成员是只用新开辟的空间还是先用上次剩下的,这个是C语言标准所没有规定的
<4>与结构体的比较
与结构体相比较,位段可以实现相同的效果,并且可以更加节省空间,但是位段是存在跨平台问题的。
<5>位段的应用
在网络中,需要将数据进行包装,而使用位段可以减少包装所耗费的空间。
二、枚举
1、枚举类型的定义
<1>枚举:将可能的取值一一列举出来
<2>枚举类型的定义:
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Color
{
Red,
Green,
Blue
};
<3>enum Day和enum Color都是枚举类型,而{}中的内容是枚举类型的可能取值,它们也叫枚举常量。
<4>枚举常量都是有默认值的,如果不赋初值,第一个枚举常量默认值为0,往下依次增1,如果给某一枚举常量赋初值,那么下面的枚举常量根据赋的初值依次增1
<5>枚举类型的内存空间大小为4byte
如下:
#include<stdio.h>
enum Day
{
Mon,
Tues=3,
Wed,
Thur,
Fri=10,
Sat,
Sun
};
int main()
{
enum Day yyy = Mon;
printf("%d\n",yyy);
yyy = Tues;
printf("%d\n", yyy);
yyy = Wed;
printf("%d\n", yyy);
yyy = Thur;
printf("%d\n", yyy);
yyy = Fri;
printf("%d\n", yyy);
yyy = Sat;
printf("%d\n", yyy);
yyy = Sun;
printf("%d\n", yyy);
return 0;
}//结果为0 3 4 5 10 11 12
2、枚举的优点(与用#define定义常量相比较)
<1>增加代码的可读性和可维护性
<2>和#define定义的标识符相比枚举有类型检查,更加严谨
<3>防止了命名污染(封装)
<4>便于调试
<5>使用方便,一次可以定义多个常量(用#define定义时一次只能定义一个常量)
3、枚举的使用
enum Day
{
Mon,
Tues=3,
Wed,
Thur,
Fri=10,
Sat,
Sun
};
int main()
{
enum Day day = Mon;//对的,只能用枚举常量来给枚举变量赋值
enum Day day = 0;//不对,因为直接赋值会存在类型的差异,变量是枚举类型,而赋的值是整型
return 0;
}
三、联合(共用体)
1、联合类型的定义
<1>联合(也叫共用体):联合是一种特殊的自定义类型,其类型定义的变量中包含一系列的成员,特征是所有的成员公用同一块空间。
有如下代码:
union Un
{
char c;
int i;
};
int main()
{
union Un un;
printf("%d\n",sizeof(un));//结果为4
return 0;
}
2、联合的特点
联合的成员是公用同一块空间的,所以一个联合变量的大小至少是最大成员的大小(因为联合至少要有能力能够保存那个数据)
3、联合大小的计算
<1>联合的大小至少是最大成员的大小
<2>当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
有如下例子:
#include<stdio.h>
union Un
{
char c[5];//大小为1,默认对齐数为8所以对齐数为1
int i;
};
int main()
{
union Un un;
printf("%d\n",sizeof(un));//结果为8
return 0;
}
4、联合体的小应用
判断计算机的大小端存储
①思路一
#include<stdio.h>
int main()
{
int a = 1;
if (*((char*)&a) == 1)
printf("小端存储\n");
else
printf("大端存储\n");
return 0;
}
②思路二
union Un
{
char c;
int i;
};
int main()
{
union Un un;
un.i = 1;
if ((int)un.c == 1)
printf("小端存储\n");
else
printf("大端存储\n");
return 0;
}