目录
[① 声明](#① 声明)
[② 创建和初始化](#② 创建和初始化)
[③ 特殊声明](#③ 特殊声明)
[④ 自引用](#④ 自引用)
[⑤ 成员访问](#⑤ 成员访问)
[⑥ 传参](#⑥ 传参)
[⑦ 实现位段](#⑦ 实现位段)
[① 对齐规则](#① 对齐规则)
[② 存在的原因](#② 存在的原因)
[③ 修改默认对齐数](#③ 修改默认对齐数)
[④ offsetof](#④ offsetof)
[① 定义](#① 定义)
[② 优点](#② 优点)
[③ 使用](#③ 使用)
[① 定义](#① 定义)
[② 特点](#② 特点)
[③ 大小](#③ 大小)
[④ 判断大小端](#④ 判断大小端)
结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
① 声明
cpp
struct tag
{
//结构的成员可以是标量、数组、指针,甚至是其他结构体
member-list;
}variable-list;
② 创建和初始化
cpp
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值
struct Point p3 = {a, b};
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {1,3}, NULL}; //结构体嵌套初始化
//结构体嵌套初始化
struct Node n2 = {20, {2, 6}, NULL};
struct Stu //类型声明
{
char name[20];//名字
int age; //年龄
};
//初始化:定义变量的同时赋初值
struct Stu s = {"tianci", 13};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s1 = { "天赐", 20 };
printf("name: %s\n", s1.name);
printf("age : %d\n", s1.age);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "良缘" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
return 0;
}
③ 特殊声明
cpp
//在声明结构的时候,可以不完全的声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
但是如果 p = &x;(将x地址赋给p),编译器会报出警告,因为编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
④ 自引用
cpp
//在结构中包含⼀个类型为该结构本⾝的成员
//定义⼀个链表的节点
struct Node
{
int data;
struct Node* next;
};
在刚开始结构体⾃引⽤使⽤的过程中,会容易出现问题
cpp
struct Node
{
int data;
struct Node next;
};
typedef struct
{
int data;
Node* next;
}Node;
上述两段代码都出现了典型的错误
① 仔细分析,如果我要计算它的大小,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。
② 在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使⽤Node类型来创建成员变量,所以这也是不合理的。
cpp
//建议定义结构体不要使⽤匿名结构体
typedef struct Node
{
int data;
struct Node* next;
}Node;
⑤ 成员访问
cpp
struct Stu
{
char name[20];
int age;
};
void print1(struct Stu ps1)
{
printf("name = %s age = %d\n", ps1.name, ps1.age);
}
void print2(struct Stu* ps2)
{
printf("name = %s age = %d\n", (*ps2).name, (*ps2).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps2->name, ps2->age);
}
int main()
{
struct Stu s1 = {"tianci", 20};
print(s1);//结构体传参
struct Stu s2 = {"liangyuan", 21};
print(&s2);//结构体地址传参
return 0;
}
总结:
① 直接访问: 结构体变量访问成员用.
② 间接访问: 结构体指针访问成员用->
⑥ 传参
cpp
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;
}
print1 和 print2 函数哪个好些?⾸选print2函数,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
⑦ 实现位段
结构体具有实现位段的能力
概念
位段的声明和结构是类似的,有两个不同:
① 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
② 位段的成员名后边有⼀个冒号和⼀个数字。
cpp
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
int main()
{
//A是⼀个位段类型
printf("%d\n", sizeof(struct A));
return 0;
}
应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。
注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
另外位段的内存分配在不同平台分配的方式是不一样的,所以位段具有跨平台问题,大家感兴趣的可以去研究一下,这里就不做过多介绍了。
内存对齐
对于结构体的基本概念介绍完了,下面我们来计算一下结构体的大小。
结构体内存对齐是一个非常非常重要的知识点!!!热门考点!!!
① 对齐规则
① 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处。
② 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
(VS 中默认的值为8,Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩)
③ 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
④ 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
cpp
struct S1
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S1));
struct S2
{
char c1;
struct S1 s1;
double d;
};
printf("%d\n", sizeof(struct S2));
大家可以算一算S2的大小然后跑一下代码来检验是否准确。
② 存在的原因
在结构体中明明内存对齐浪费空间,为什么还要这么做呢?
其实那是因为我要读取i,如果按照内存对⻬规则存储数据,假设一次从整数倍位置可以读取4byte,那么我只需要读取一次就完成了,而如果不按照内存对⻬规则存储数据,那就需要读取两次,并且还要将数据拼接在一起。
为什么存在内存对⻬?(参考大部分材料)
① 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
② 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
cpp
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
虽然S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。
所以我们在设计结构体的时候,尽量让占⽤空间⼩的成员尽量集中在⼀起,这样既可以满⾜对⻬,⼜可以节省空间!
③ 修改默认对齐数
结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
cpp
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S));
//输出结果为6
return 0;
}
④ offsetof
offsetof是一个宏,用于计算结构体成员相较于结构体变量起始位置的偏移量,使用时需要包含<stddef.h>文件。
cpp
struct S
{
char c1;
char c2;
int i;
};
int main()
{
printf("%zd\n", offsetof(struct S, c1));
printf("%zd\n", offsetof(struct S, c2));
printf("%zd\n", offsetof(struct S, i));
//输出结果为0、1、4
}
枚举
① 定义
cpp
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE = 1,
FEMALE = 3,
SECRET = 9
};
代码中定义的 enum Day , enum Sex , enum Color 都是枚举类型,{}中的内容是枚举类型的可能取值,也叫枚举常量 。
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
② 优点
可能会有人问,这不就是#define 定义常量的作用吗?
那么枚举肯定是有它的特别之处
① 增加代码的可读性和可维护性
② 和#define定义的标识符比较枚举有类型检查,更加严谨
③ 便于调试
④ 使用方便,一次可以定义多个常量
③ 使用
cpp
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
//这样赋值是错误的
//enum Color clr = 5;
//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异
enum Color clr = GREEN;
联合体
联合体类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
① 定义
cpp
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算联合变量的大小
printf("%d\n", sizeof(un));
//输出结果为4
② 特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
cpp
union Un
{
int i;
char c;
};
union Un un;
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//输出结果一样
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//输出结果为11223355
③ 大小
计算联合体大小
① 联合的大小至少是最大成员的大小
② 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
cpp
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
//输出结果为8
printf("%d\n", sizeof(union Un2));
//输出结果为16
④ 判断大小端
cpp
union Un
{
char c;
int i;
};
int main()
{
union Un un = { 0 };
un.i = 1;
if (un.c == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
本篇文章到这里就结束啦,自定义类型的内容介绍完啦,大家一定要理解结构体,因为后面还会大量接触接触体,特别是在数据结构阶段,另外文章中内存对齐是本章的重点!!!希望对大家有所帮助! 下篇文章见啦,希望大家多多来支持一下!
感谢大家的三连支持!