目录
一,结构体
1.1 结构体声明
struct tag //tag代表结构体类型名称
{
member-list; //参数列表
}variable-list; //表示要实例化对象的变量
也可以不带上面的tag,这就是匿名结构体,匿名结构体生成变量只能在结构体后面生成,并且是全局变量,不能在函数内部再次使用
struct
{
int a;
char c;
float f;
}x; //匿名对象创建变量 -- 只能用一次(很少用)
struct
{
int a;
char b;
float c;
}a[20], * p;
p = &x;//报错,因为编译器会把上面两个声明当成完全不同的两个类型
1.2 结构体自引用
C语言允许在结构体中包含一个类型为改结构体本身的成员,数据结构里的链表的节点类型就是以此为基础
struct Node
{
int data;
struct Node next; //不推荐这样使用,会造成大量重复数据造成空间浪费
};
typedef struct
{
int data;
Node* next; //这样也不行,因为typedef要的是完整的类型,Node此时还未定义,直接用会报错
}Node;
typedef struct Node
{
int data;
struct Node* next; //这样写才是正确的,这也是数据结构单链表节点的标准写法
}Node;
1.3 结构体变量的定义和初始化
这个我觉得还是直接上代码比文字解释要清楚得多,啊哈哈
struct SN
{
char c;
int i; //可以自己改变初始化顺序
}sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'}; //全局变量
struct S1
{
double d;
struct SN sn;
int arr[10];
};
void main1()
{
struct SN sn3, sn4; //局部变量
printf("%c %d\n", sn2.c, sn2.i);
//结构体里有结构体
struct S1 s = { 3.14, {'a', 99}, {1,2,3} };
printf("%lf %c %d\n", s.d, s.sn.c, s.sn.i);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", s.arr[i]);
}
}
1.4 结构体内存对齐
1.4.1 打印结构体大小
先看以下代码以及计算结果
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
int i;
char c1;
char c2;
};
void main2()
{
struct s1 s1 = { 0 };
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
}
我们通过sizeof计算结构体大小,看s1,里面有两个char类型的变量一个int类型的变量,s2也一样,所以按照常识,两个打印应该都会打印6,但是结果却是下面这样
结果和我们想的完全不一样,所以我们可以知道,结构体大小的计算有一套属于它自己的规则,我们把这个规则称为——内存对齐
先看图,大致理解下上面是咋搞的
1.4.2 内存对齐规则
①结构体的第一个成员永远放在相较于结构体变量起始位置的偏移量为0的位置。
②从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处。
③对齐数:结构体成员自身的大小和默认对齐数的较小值,VS默认对齐数位8,gcc没有默认对齐数,对齐数就是结构体成员的自身大小。
④结构体的总大小必须是最大对齐数的整数倍,最大对齐数是:所有成员的对齐数中最大的值。
⑤如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍数,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
1.4.3 计算偏移量
struct s1
{
char c1;
int i;
char c2;
};
void main2()
{
struct s1 s1 = { 0 };
printf("%d\n", offsetof(struct s1, c1));//可以计算结构体成员相较于结构体起始位置的偏移量
printf("%d\n", offsetof(struct s1, i));
printf("%d\n", offsetof(struct s1, c2));
}
1.4.4 结构体嵌套情况
struct s3
{
double d;
char c;
int i;
};
struct s4
{
char c1;
struct s3 s3;
double d;
};
void main3()
{
printf("%d\n", sizeof(struct s3));//16
printf("%d\n", sizeof(struct s4));//32
}
1.4.5 为什么存在内存对齐?
总的来说,结构体的内存对齐是拿空间换时间的做法
①平台原因:不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处去某些特定类型的数据,否则抛出硬件异常
②性能原因:数据结构应该尽可能在自然边界上对齐,原因在于为了访问未对齐的内存,处理器需要做两次内存访问,时间成本增加
1.4.6 修改对齐数
#pragma pack(8)//设置默认对齐数为8
struct ss1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct ss2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
void main4()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct ss1));//12
printf("%d\n", sizeof(struct ss2));//6
}
1.5 结构体传参
struct S
{
int data[100];
int num;
};
void print1(struct S tmp)
{
printf("%d\n", tmp.num);
}
void print2(const struct S* ps)
{
printf("%d\n", ps->num);
}
void main5()
{
struct S s = { {1,2,3}, 100 };
print1(s); //第一种把s传给tmp,因为是两块独立的空间,所以修改tmp不会修改s
print2(&s); //一般选第二种,节约空间,但因为修改ps也会修改s不安全,加上const就保证安全了
}
二,位段
2.1 啥是位段
位段得成员必须是int,unsigned int,signed int,位段得成员们后面有一个冒号加一个数字,数字代表二进制位数,如下列代码,A就是一个位段类型
struct A
{
int _a : 2;//二进制位,占2个比特位
int _b : 5;
int _c : 10;
int _d : 30;
};
printf("%d\n", sizeof(struct A)); //打印8
可以看到,如果没有冒号加数字,直接四个int的话应该会打印16,所以我们可以认为位段在某种意义上节省了空间消耗,所以下面详细讲解位段是如何做到空间节省的
2.2 位段的内存分配
先看下面代码以及打印结果,通过现象看本质
struct s
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
void main7()
{
struct s s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(s)); //打印3
}
先把图摆出来,然后下面是对该图的解释
前面说过冒号后面是该变量能存储数据的二进制位大小,a后面是3,那么就是000,能代表0--7的整数,b后面是4,那么就是0000,能代表0--15的整数,c和d也是这样。
然后因为VS默认对齐数是8,所以在定义s变量时初始给8个二进制位数据,也就是一个字节00000000,a赋值为10,10的二进制位为1010,由于a只有三个比特位,所以只能存010,现在就是00000010。
b赋值为12,12的二进制为1100,带到内存中就是01100010,然后c存3,3的二进制为11,但是原本的空间已经存不下了,于是再开辟一字节空间然后把3存进去01100010 00000011,由于c的后面是5,所以开辟五个字节。
最后要存4,4的二进制为100,第二块空间已经存不下了,所以再开辟一字节存4,所以最终的储存样子为01100010 00000011 00000100
换成十六进制为0x62 0x03 0x04,所以再内存中就是这样存的
2.3 位段跨平台问题
①int位段被当成有符号数还是无符号数是不确定的
②位段中最大的数目不能确定。(16位机器最大16,32位机器最大32,如果在32位机器写上27,那么在16位机器上就会出问题)
③位段中的成员在内存中从左向右分配,还是从右向左分配标准未定义
④当一个结构抱哈两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位重新开辟还是利用剩余的位,这是不确定的
三,枚举
3.1 枚举的定义
顾名思义,就是把可能的值一一列举,下面就是枚举的定义
enum Dat //星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Su
}
enum Sex //性别
{
MALE,
FEMALE,
SECRET
}
上面在这些enum Day,enum Sex都是枚举类型,{}里面的叫做枚举常量,这些枚举常量可能取值都是有值的,默认从0开始,一次递增一,类似MySQL数据库中的auto _increment子增长约束
enum Color
{
RED,//0
GREEN,//1
BLUE//2
};
void main8()
{
//如果给RED赋值4,那么打印456,给GREEN赋值8,那么打印089
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
}
3.2 枚举的优点
①增加代码可读性和可维护性
②和#define定义的标识符比较枚举有类型检查,更加严谨(C++)
③防止命令污染(封装)
④便于调试
⑤使用方便,一次性可以定义多个常量
第二点有点不好理解,如下代码
#define RED 0 //无类型检查
enum Color cc = 3;
//C++编译报错,因为enum也是一种类型,无法把int赋值给enum,所以enum有类型检查,保证安全性
四,联合体
4.1 联合类型的定义
联合体union是一张特殊的自定义类型,这种类型里面也包含一系列成员变量,特征是这些成员公用同一块空间
union Un
{
char c;
int i;
}
void main30()
{
union Un un;
printf("%d\n", sizeof(un)); //打印4
}
4.2 联合的特点
联合体的成员共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小,因为联合至少得用能力保存最大的那个成员变量
union Un
{
int i;
char c;
};
void main9()
{
printf("%d\n", sizeof(union Un)); //打印4
union Un un = { 0 };
un.i = 0x11223344;
un.c = 0x55;
printf("%p\n", &un); //三个打印结果一样
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
}
4.3 使用联合体来判断大小端
两种方法,一种直接判断,一种通过函数返回值判断
void main10()
{
union Un u = { 0 };
u.i = 1;
if (u.c == 1)
printf("小端\n");
else
printf("大端\n");
}
int check_sys()
{
union
{
int i;
char c;
}un = {.i = 1};
return un.c;
}
void main11()
{
//if(*(char*)& == 1)
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
}
4.4 联合体大小的计算
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
void main12()
{
printf("%d\n", sizeof(union Un1)); //8 -- 联合体的大小也是存在内存对齐滴
printf("%d\n", sizeof(union Un2)); //16
}
联合体大小的计算也存在内存对齐哦