结构的声明
在声明一个结构体的时候可以直接定义变量如stu1。也可以省略,之后在主函数中单独定义如stu2。其中stu1和stu2都是结构体的类型名。
如果在声明结构体时忘记写名称就成了匿名的结构体类型,该类型只能被使用一次。
struct stu1
{
char name[10];//成员列表
int age;
}s1,s2;//变量列表 - 全局变量
struct stu2
{
char name[10];
int age;
};
struct//匿名结构体类型
{
char name[10];
int age;
}s5;
int main()
{
struct stu1 s3;//局部变量
struct stu2 s4;
return 0;
}
结构体的自引用
正确的写法是在结构体内引用同类型的指针而不是类型本身。这种写法在数据结构中会经常用到。
struct Node
{
int data;
struct Node* next;
};
typedef简化,可以使在定义结构体变量时直接用重定义的名定义。
typedef struct Node
{
int data;
struct Node* next;
}Node;
int main()
{
struct Node n1;
Node n2;
return 0;
}
结构体变量的定义和初始化
- 在创建类型时直接创建并初始化,如p1
- 在创建类型时直接创建变量,在主函数中初始化,如p2
- 在主函数中定义变量并初始化,如p3
- 在主函数中定义变量之后再初始化,如p4
struct point
{
int x;
int y;
}p1 = { 1,2 },p2;
int main()
{
struct point p3 = { 3,4 };
struct point p4;
p2.x = 5;
p2.y = 6;
p4.x = 7;
p4.y = 8;
printf("%d %d\n", p1.x, p1.y);
printf("%d %d\n", p2.x, p2.y);
printf("%d %d\n", p3.x, p3.y);
printf("%d %d\n", p4.x, p4.y);
return 0;
}
结果如下:
结构体嵌套
在结构体内嵌套结构体
struct score
{
int Chinese;
int math;
};
struct stu
{
char name[20];
int age;
struct score s;
};
int main()
{
struct stu s1 = { "zhangsan", 18, { 100,100 } };
printf("%s %d %d %d\n", s1.name, s1.age, s1.s.Chinese, s1.s.math);
return 0;
}
结构体内存对齐
先看一段代码
struct S1
{
char c1;
int a;
char c2;
};//
struct S2
{
char c1;
char c2;
int a;
};//
int main()
{
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//8
return 0;
}
这段代码的结果是多少呢,答案是12和8。那么明明一个char占1字节,一个int占4字节,而这两个结构体内容没区别只是顺序不一样,却是一个占了12个字节,一个占了8个字节。这是怎么一会事呢,接下来就给大家介绍结构体的内存对齐的概念。
对齐规则
- 结构体的第一个成员在结构体变量偏移量为0的地址处
- 其他成员变量要对齐到对齐数的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。
- 结构体的总大小为每个成员变量对齐数的最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数 (含嵌套结构体的对齐数)的整数倍。
什么意思呢,如图所示
- 对于S1:c1直接放在偏移量为0的位置,占一个字节;a的大小是4字节而vs默认对齐数是8,较小值为4,所以a要对齐到4的整数倍的地址处,存放进偏移量从4开始的4个字节处;c2的大小是1比8小,所以c2要对齐1的整数倍,也就是偏移量为8的地址处;而整个结构体的大小是各个成员最大对齐数的整数倍,S1的最大对齐数就是4,所以整个S1的大小就是12个字节。
- 对于S2:c1依然存放在偏移量为0的位置,占一个字节;c2的大小是1比8小,所以c2要对齐1的整数倍,也就是偏移量为1的地址处;a的大小是4比8小,所以a要对齐到4的整数倍,也就是从偏移量为4的位置开始存4个字节;S2的总大小就应该是4的整数倍,也就是8个字节。
如果是嵌套呢,猜猜看S4的大小是多少
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S3));//16
printf("%d\n", sizeof(struct S4));//32
return 0;
}
话不多说直接看图
- 对于S3:d的对齐数是8,直接放在偏移量为0的地址处,占8个字节;c的大小是1直接放在偏移量为8的位置占一个字节;i的大小是4比默认值8小,所以a的对齐数是4,放在4的整数倍处,也就是偏移量为12的位置占4个字节;S3的总大小应该是其最大对齐数的整数倍,也就是d的8的整数倍,即16个字节。
- 对于S4:c1直接放在偏移量为0的地址处,占1个字节;s3要对齐数到s3成员的最大对齐数的整数倍,也就是8的整数倍,即偏移量为8的位置开始的16个字节;d的大小是8,默认对齐数也是8,因此d要放在8的整数倍,也就是偏移量为24的地址处,占8个字节;S4的总大小就是其成员的最大对齐数的整数倍,也就是8的整数倍即32个字节。
为什么要存在内存对齐
- 结构体内存对齐是为了提高内存访问的效率和性能。当结构体中的成员变量按照自然对齐原则进行排列时,可以减少内存访问时需要的读取次数,从而提高内存访问速度。
- 此外,结构体内存对齐还可以避免因为内存对齐不当而导致的内存访问错误。在一些体系结构中,如果访问未对齐的内存地址,可能会导致性能下降或者程序崩溃。
如何减少浪费空间
- 在设计结构体时尽量让空间小的成员集中在一起,避免更多空间的浪费。
修改默认对齐数
利用#pragma pack()修改默认对齐数
#pragma pack(4)
struct S
{
int i;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
此时的默认对齐数修改为4,那么S的大小就应该是12了,如图:
结构体传参
struct S
{
int age;
char name[20];
};
void print1(struct S s)
{
printf("%d %s\n", s.age, s.name);
}
void print2(struct S* s)
{
printf("%d %s\n", s->age, s->name);
}
int main()
{
struct S s1 = { 18,"zhangsan" };
print1(s1);
print2(&s1);
return 0;
}
以上两种方法都可以,其中print1为值传递,print2为地址传递,推荐第二种方法,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销,简单来讲就是传值会浪费更多的资源,而地址的大小是固定的,所以结构体传参时,要传地址。
结构体实现位段
什么是位段
- 位段的成员必须是int,unsigned int或者signed int,或者char类型
- 位段的成员后边需要加一个冒号和一个数字,这个数字不能超过类型的大小
struct S
{
int _a : 2;
int _b : 10;
int _c : 15;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
位段是可以节省空间的。冒号后面,就是成员变量所占的比特位数。该结构体只占了8个字节,要比直接是四个int的16个字节少占8字节的空间
位段的内存分配
位段的空间是按类型的字节开辟的,int类型先开辟4字节,char类型先开辟1字节。简单来讲就是,对于int类型,先开辟4个字节也就是32位,再根据用户定义的成员从低到高1个成员1个成员的存,不够了就再开辟1个字节。char类型也是相同的道理,只不过是先开辟1个字节。同时也要满足结构体的总大小是最大对齐数的整数倍这个规则。
上图理解
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
}
- a=10的二进制是1010,但是a只占3位,所以只能放010进去
- b=12的二进制是1100,占4位,接着放在第1个字节内
- c=3的二进制是11,占5位,需要补3位0,此时第1个字节已经不够了,所以需要新的字节
- d=4的二进制是100,占4位,补1位0,此时第2个字节也放不下d了,因此还要再新字节
验证:01100010 00000011 00000100 转换成十进制是 62 03 04 而在内存中存的正是这个
位段的跨平台问题(上图只代表vs2022)
- 位段的不确定性因素有很多,不具备可以移植性。
- int位段被当成有符号数还是无符号数是不确定的。
- 位段中的最大数目在不同位的机器上是不能确定的。
- 位段中的成员在内存中从右向左还是从左向右,分配尚未确定。
- 第某个字节无法容纳下一个内容时,剩余的位到底是舍弃还是利用也无法确定。