一.什么是结构体
结构体是C语言中的自定义数据类型;
结构是一些值的集合,这些值称为成员变量;
结构的每个成员可以是不同类型的变量,如:标量,数组,指针,甚至是其他结构体
二.结构体类型的声明
1.结构的声明
格式:
struct tag
{
member_list;//成员列表,可有一个或者多个成员
}valuable_list;//变量列表
举例:描述一个学生(名字,年龄,成绩)
#include<stdio.h>
struct stu {
char name[20];
int age;
float score;
}s3, s4;//全局变量
int main() {
struct stu s1 = { "zhangsan",20,90.5f };//局部变量初始化
struct stu s2;///局部变量
return 0;
}
2.结构的特殊声明
在声明结构时,可以不完全声明;
#include<stdio.h>
struct {
int a;
char b;
float c;
}x;
struct {
int a;
char b;
float c;
}a[20],*p;
int main() {
p=&x;//error
return 0;
}
warning:编译器会把上面的两个声明当成完全不同的两个类型,是非法的;
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次
3.结构的自引用
1.结构体的自引用即自己引用自己
struct node {
int data;
struct node* next;
};
2.定义结构体不要使用匿名结构体
typedef struct node {
int data;
struct node* next;
}node;
三.结构体变量的定义和初始化
1.变量的定义
struct point
{
int x;
int y;
}p1;//声明结构体的同时定义变量p1
struct point p2;//定义结构体变量p2
2.初始化
struct stu {
char name[20];
int age;
float score;
}s3, s4;//全局变量
struct stu s1 = { "lisi",16,95 };//直接初始化
struct stu s2 = { .age = 96,.name = "wangwu",.score=90.05f};//指定顺序初始化
3.嵌套初始化
struct Node {
int data;
struct point p;
struct Node* next;
}n1 = { 10,{4,5},NULL };//结构体嵌套循环
struct Node n2 = { 20,{5,6},NULL };//结构体嵌套循环
四.结构成员访问操作符
1.结构体成员的直接访问
通过点操作符(.)访问,点操作符接受两个操作数
格式:结构体名称.需要访问的结构体成员
#include<stdio.h>
struct Node {
int data;
struct point p;
struct Node* next;
}n1 = { 10,{4,5},NULL };
int main() {
printf("%d", n1.data);
return 0;
}
2.结构体成员的间接访问
有时候我们得到的不是一个结构体,而是一个指向结构体的指针;
格式:指针->要访问的结构体成员
#include<stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point p = { 3,4 };
struct Point* ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("%d %d\n", ptr->x, ptr->y);
return 0;
}
五.结构体内存对齐
1.对齐规则
(1)对齐规则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处;
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
(1)对齐数==编译器默认的一个对齐数与该成员变量的较小值
(2)VS中默认对齐数为8
(3)Linux中gcc没有默认对齐数,对齐数就是该成员变量自身的大小
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4.对于嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处;结构体的整体大小就是所有最大对齐数(含嵌套结构体成员中的对齐数)的整数倍
(2)解析
不含嵌套结构体的情况
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main() {
printf("%zd\n", sizeof(struct S3));//16
return 0;
}
1.d先对齐到偏移量为0的地址处,sizeof(double)=8
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
2.sizeof(char)=1,此时偏移量为8,8是1的整数倍,c正好对齐到偏移量为8的地址处
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
3.sizeof(int)=4,此时偏移量为9,9不是4的整数倍,d对齐到偏移量为12的地址处
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
4.此时对齐数为16,结构体的最大对齐数为8,16是8的整数倍,故结构体S3的大小为16个字节
含嵌套结构体的情况
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s;
double d;
};
int main() {
printf("%zd\n", sizeof(struct S4));//32
return 0;
}
(为节省空间此处只体现了对齐数,没有严格画出内存分配情况)
1.c1先对齐到偏移量为0的地址处,sizeof(char)=1
0 |
1 |
8 |
23 |
24 |
31 |
32 |
2.sizeof(struct S3)=16,此时偏移量为1,1不是8(嵌套的结构体S3的成员中的最大对齐数)的整数倍,嵌套的结构体S3对齐到偏移量为8的地址处
0 |
1 |
8 |
23 |
24 |
31 |
32 |
3.此时偏移量为24,sizeof(double)=8,24是8的整数倍,d对齐到偏移量为24的地址处
0 |
1 |
8 |
23 |
24 |
25 |
30 |
31 |
32 |
4.结构体S4的最大对齐数为8,32是8的整数倍,故结构体S4的大小为16个字节
2.为什么存在内存对齐
总体来说:结构体存在内存对齐是拿时间换空间的做法
1.平台/移植原因:不是所有的硬件平台都能访问任意地址地上的任意数据的
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存仅需要一次访问
怎么做到既满足对齐,又节省空间——让占用空间小的成员尽量集中在一起
3.修改默认对齐数
#pragma这个预处理指令可以改变编译器的默认对齐数
未修改默认对齐数:
#include<stdio.h>
struct M {
char c1;
int i;
char c2;
};
int main() {
printf("%zd\n", sizeof(struct M));//12
return 0;
}
修改默认对齐数:
#include<stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct M {
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main() {
printf("%zd\n", sizeof(struct M));//6
return 0;
}
六.结构体传参
1.举例
#include<stdio.h>
struct S {
int data[1000];
int num;
};
struct S s = { {1,2,3,4,5},100 };
//结构体传参
void print1(struct S s) {
}
//结构体地址传参
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main() {
print1(s);//传结构体
print2(&s);//传结构体地址
return 0;
}
2.结构体传参时,需要传结构体的地址
原因:函数传参的时候,参数是需要压栈的,会有时间和空间上的开销;如果传递一个结构体对象的时候,结构体过大,参数压栈的开销比较大,会导致性能下降
七.结构体实现位段
1.什么是位段
位即二进制位;
位段的声明和结构体是类似的,但是有两点不同:
1.位段成员必须是int,unsigned int,signed int(在c99中位段成员的类型也可以选择其他类型)
2.位段成员后面有一个冒号和数字
#include<stdio.h>
struct A {
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main() {
printf("%zd", sizeof(struct A));//8
return 0;
}
2.位段的内存分配
1.位段的成员可以是int,unsigned int,char等类型;
2.位段上的空间是按照需要以4个字节(int)或一个字节(char)的方式来说开辟的;
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段;
解析:
#include<stdio.h>
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;
printf("%zd\n", sizeof(s));
return 0;
}
(假设标准为:内存从左向右使用;如果剩余的空间不够下一个成员使用就浪费)
1.首先开辟一个字节的空间,用来储存a
a=10(十进制)==1010(二进制);但是因为a设置的是3个bit,故取后三位存储
0 | 1 | 0 |
2.因为a储存完后,还剩下5个bit的空间,可以用来储存b
b=12(十进制)==1100(二进制);b设置的是4个字节,正好全部储存
1 | 1 | 0 | 0 | 0 | 1 | 0 |
3.a,b储存完剩下的空间不够储存c,舍弃剩下的空间,再开辟一块一个字节的空间,用来储存c
c=3(十进制)==0011(二进制);c设置的是5个bit,正好全部储存
0 | 0 | 0 | 1 | 1 |
4.c储存完剩下的空间不够储存d,舍弃剩下的空间,再开辟一块一个字节的空间,用来储存d
d=4(十进制)==0100(二进制);d设置的是4个bit,正好全部储存
0 | 1 | 0 | 0 |
5.整个位段的内存情况如下
0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
3.位段的跨平台问题
1.int位段被当成有符号数还是无符号数是不确定的
2.位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,;如果写成27,在16位机器上会出问题)
3.位段中的成员在内存分配中从左向右分配还是从右向左分配尚不明确
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段存储后剩余的位时,是舍弃还是继续使用尚不明确
总结:跟结构相比,位段可以达到同样的效果,并且可以很好地节省空间,但是存在跨平台的问题
4.位段的应用
可以节省空间,使网络传输的数据报大小较小
5.使用位段的注意事项
位段的几个成员共有一个字节,这样有些成员的起始位置并不是某个字节个起始位置,那么这些位置处是没有地址的(内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的),因此不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入值放在一个变量中,然后赋值给位段的成员
#include<stdio.h>
struct S {
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main() {
struct A sa = { 0 };
//scanf("%d", &sa._a);//err,不允许采用位域的地址
//正确
printf("%d\n", sa._a);//默认初始值为0
int b = 0;
scanf("%d", &b);//输入1(要注意输入的数字大小!)
sa._a= b;
printf("%d\n", sa._a);//输出1
return 0;
}