目录
前言
1.什么是结构体
2.结构体类型的声明
2.1结构体的声明
2.2结构体的创建和初始化
2.3结构成员访问操作符
2.3.1结构体成员直接访问
2.3.2结构体成员的间接访问
2.4结构体变量的重命名
2.5结构体的特殊声明
2.6结构的自引用
3.结构体内存对齐
3.1对齐规则
3.2为什么存在内存对齐
3.3修改默认对齐数
4.结构体传参
5.结构体实现位段
5.1位段的声明
5.2位段的内存分配
5.3位段的注意事项
前言
在学习结构体之前,我们还学习了char ,int,short,float,double等内置类型,他们可以描述一些事物的某一项属性,但是如果我想描述一本书的某些属性,而不是单单的一种属性,该怎么办呢?这时候就会用到自定义类型——结构体,我们就可以创造出属于我们自己的类型。
1.什么是结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体。也就是说结构体是由一些内置类型构成的,这些类型表示事物的某些属性。
2.结构体类型的声明
2.1结构体的声明
struct book
{
char name[100];//书名
int price;//价格
char author[100];//作者
};//分号不能丢
struct是结构体的标志,book是我们自定义的结构体的名字,{}里面的是成员变量
2.2结构体的创建和初始化
按照顺序初始化
struct book b1 = { "红心照耀中国",100,"埃德加·斯诺" };
指定顺序初始化
struct book b2 = { .author = "卡尔·马克思、弗里德里希·恩格斯",.name = "共产党宣言",.price = 50 };
struct book b1和struct book b2是结构体的创建,后面的{}是对这个结构体进行初始化,struct book相当于之前学习的char和int,b1和b2就是变量名。
以上两种是在声明结构体之后进行的初始化,除此之外还可以在声明结构体的时候进行初始化 ,就像这两种方法这样
struct book
{
char name[100];
int price;
char author[100];
}b1 = { "红心照耀中国",100,"埃德加·斯诺" };
struct book
{
char name[100];
int price;
char author[100];
}b2 = { .author = "卡尔·马克思、弗里德里希·恩格斯",.name = "共产党宣言",.price = 50 };
结构体变量的创建和初始化还可以分开进行
比如这个样子:
#include<stdio.h>
struct book
{
char name[100];
int price;
char author[100];
}x;//创建变量
int main()
{
struct book x = { "红星照耀中国",100,"埃德加·斯诺" };//初始化
return 0;
}
2.3结构成员访问操作符
结构体变量创建出来是为了使用的,那他会不会像int,double等类型的使用方法一样呢,关于结构体的使用,这里鱼哥给大家介绍两个操作符(.)结构体成员直接访问,(->)结构体成员简介访问
2.3.1结构体成员直接访问
#include<stdio.h>
struct book
{
char name[100];
int price;
char author[100];
};
int main()
{
struct book b1 = { "红心照耀中国",100,"埃德加·斯诺" };
printf("%s\n", b1.name);
printf("%d\n", b1.price);
printf("%s\n", b1.author);
}
使用方法:变量名.成员名
2.3.2结构体成员的间接访问
#include<stdio.h>
struct book
{
char name[100];
int price;
char author[100];
}b1 = { "红心照耀中国",100,"埃德加·斯诺" };
int main()
{
struct book *ptr = &b1;
ptr->price = 10;
printf("%d\n", b1.price);
return 0;
}
使用方法:结构体指针->成员名
2.4结构体变量的重命名
typedef struct book
{
char name[100];
int price;
char author[100];
}Book;
使用typedef对struct book进行重命名,Book就相当于原来的struct book,也就是他们两个的效果是一样的
2.5结构体的特殊声明
struct
{
char name[100];
int price;
char author[100];
}x;
上面这种声明属于匿名结构体声明,也就是在结构体声明的时候省略了标签(book)
注意:匿名结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次
2.6结构的自引用
结构体出来能装内置类型,可不可以装结构体呢?
struct book2
{
int x;
int y;
};
struct book
{
char name[100];
int price;
char author[100];
struct book2 x1;
};
这样写是OK的,我们可以算出他所占内存空间的大小
能装别的结构体不算nb,如果他能装自己才叫nb
struct book
{
char name[100];
int price;
char author[100];
struct book x1;
};
那么这样写是正确的吗?如果正确,那么他所占内存空间的大小是多少?
经过分析,这样写结构体会无限嵌套下去,这样是无法计算出结构体的大小,他的大小就会无穷大,所以这样的写法是错误的
那么有没有正确的写法呢?答案是有的
struct book
{
char name[100];
int price;
char author[100];
struct book *next;
};
我们可以这样写,既然结构体不能嵌套结构体,那我结构体装结构体指针,通过结构体指针找到结构体
在结构体自引用使用的过程中,掺杂着typedef对匿名结构体类型的重命名,也是容易出现问题的
typedef struct
{
int x;
Book*next;
}Book;
这样的写法也是不行的,因为Book是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Book来创建成员变量,这是不行的
3.结构体内存对齐
3.1对齐规则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对其到某个数字(对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
--VS中默认的值为8
--Linux中gcc没有默认的对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
光看规则肯定是学不明白的,接下来鱼哥给大家举一下例子帮助大家理解
例1.
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
这个结构体的大小是多少?大家可以自己先算一算。
答案是12,怎么算出来的呢
有人会想,c1一个字节,i四个字节,c2两个字节,应该6个字节才对啊
数字代表偏移量
c1是一个字节,并且要对齐到偏移量为0的地址处
i是四个字节,4小于8,所以对齐数是4,所以i要存储到4的整数倍处
c2是一个字节,对齐数是1,所以c2存到偏移量为8的地址处
最大对齐数是4,所以结构体的总大小是4的倍数,而c1,i,c2占9个字节,所以结构体的大小要大于等于9并且是4的倍数,所以这个结构体的大小是12个字节
例2.
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
这个结构体的大小是8,你会想,这个结构体和例1的成员变量都一样,只是位置不同,凭什么这个的内存就要小一点呢?
c1从偏移量为0的地址处开始存储
c2的对齐数是1,偏移量1是1的倍数,所以1处存c2
i的对齐数是4,所以i从4的位置开始存储
c1,c2,i占8个字节,而最大对齐数是4,8是对齐数的整数倍,所以这个结构体的大小是8个字节
例3.
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
这个结构体的大小是16,有了前面两个例子,鱼哥相信你应该可以算出结果
d从0开始存储,字节大小是8
c的对齐数是1,存储在偏移量为8的位置
i的对齐数是4,存储在偏移量为12的位置
d,c,i占的字节数为16,16又是最大对齐数8的整数倍,所以结构体的大小为16个字节
例4.
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
s3是例3的结构体,这个s4结构体的大小是32
c1从偏移量为0开始
s3的最大对齐数是8,所以s3从偏移量为8的位置开始存储,因为例3已经算出了s3的大小,所以s3存16个字节
d的对齐数是8,24是8的倍数,所以d从24开始存储
c1,s3,d占32个字节,32又是8的整数倍,所以s4的大小是32个字节
3.2为什么存在内存对齐
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中
总的来说:结构体的内存对齐是拿空间换时间
对例1和例2分析,我们发现如果让占用空间小的成员尽量集中在一起可以起到节省空间的效果
3.3修改默认对齐数
VS的编译器的默认对齐数是8,在某些情况下,我们觉得默认对齐数不合适,想要修改,就会用到#pragma这个预处理指令
#include<stdio.h>
#pragma pack(2)
struct S
{
char c1;
int a2;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
这里将默认对齐数改为2,大家可以通过前面的学习自己算一下答案
学会修改也要学会恢复,怎么做呢?看接下来的代码
#include<stdio.h>
#pragma pack(1)
struct S
{
char c1;
int a2;
char c2;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
4.结构体传参
#include<stdio.h>
struct S
{
int arr[100];
int x;
};
struct S s = { {1,2,3,4,5},7 };
//结构体传参
void print1(struct S n)
{
printf("%d\n", n.x);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->x);
}
int main()
{
print1(s);//传结构体
print2(&s);//传地址
return 0;
}
上面这个代码进行了两个结构体的传参,那么哪一种传参更好一点呢?
答案是print2的传参更好一点
原因是:
函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
结论:结构体传参的时候,要传结构体的地址
5.结构体实现位段
5.1位段的声明
struct S
{
int a:2;
int c:3;
int f:24;
};
上述代码就是一个简单的位段声明,是不是感觉和结构体比较像
那么他和结构体有什么区别呢?
1.位段的成员必须是int,unsigned int,signed int (整形家族的),在C99中也可以是其他类型
2.位段的成员名后面是一个冒号加一个数字(这个数字代表比特位数)
5.2位段的内存分配
位段的空间上是按照4个字节或者1个字节的方式来开辟的
#include<stdio.h>
struct S
{
char a : 2;
char b : 4;
char c : 5;
char d : 6;
};
struct S s = { 0 };
int main()
{
s.a = 10;
s.b = 12;
s.c = 13;
s.d = 14;
printf("%d", sizeof(struct S));
return 0;
}
结果为3,来看看数据是怎么在内存中存储的吧
·首先申请一个字节的空间也就是8个比特位,a:两个比特位,将a的值转换位二进制,然后存低位的两个存进去,b是4个比特位,因为申请了8个比特位还剩6个,够用,就接着存,c是5个比特位,不够了,就舍去,重新申请一个字节,存放5个比特位,剩三个,不够d的6个bit位,就有申请一个字节存放d,所以总的申请了3个字节
总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间
5.3位段的注意事项
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员