目录
零、前言
——为什么要有自定义类型?
——因为C语言自带的类型有限,不便于描述一些复杂对象。比如描述一个学生,可能需要姓名、性别、年龄、身份证号等等。这些复杂对象不能用单一int、char等描述,需要将他们组合起来,于是我们可以使用结构体这一自定义类型来描述。除了结构体,我们还有枚举和联合体这两种自定义类型。(指针和数组也可以视作自定义类型,但在本博客不做介绍~)
一、结构体——struct
1. 结构体的声明
结构是一些值的集合,这些值成为成员变量,结构的每个成员可以是不同类型的变量。
【语法形式】:
struct tag
{
member-list;
}variable-list;
struct——结构体关键字; tag——标签的名字;member-list——成员列表;variable-list——变量列表。(变量也可以不在此创建,可以声明完以后创建)
例如:
struct Stu
{
char name[20];
int age;
double score;
} s1, s2, s3;
struct Stu ss; //另一种定义变量的方法
也存在匿名结构体类型,即不存在标签的名字,但该种结构体必须在声明的时候创建变量。(补:匿名结构体即使成员一样,但在编译器看来是不同的结构体!)
*可用typedef来进行重命名,来省略struct。
2. 结构体的自引用
——结构中能否包含一个类型为该结构本身的成员?(为了实现链式访问)
struct Node
{
int data;
struct Node n;
};
——不能!不能通过编译。正确的链式访问用指针即可。分成数据域和指针域。正确的如下:
struct Node
{
int data; //数据
struct Node* next; //指针
};
(最后一个指向NULL)
3. 结构体的创建和初始化
struct Stu
{
char name[20];
int age;
double score;
} s1 = {"zhangsan", 17, 88.8};
struct Stu ss = {"lisi", 18, 99.9};
4. 结构体内存对齐【重难点!】
比如我们定义一个结构体变量如下,并想求它的大小:
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
struct s1 s;
printf("%d\n", sizeof(s));
return 0;
}
一般来说,我们一看:char 1个字节,int 4个字节,char 1个字节,一共6个字节就够了。但是,这个程序运行出来结果是12。
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
struct s2 s;
printf("%d\n", sizeof(s));
return 0;
}
我们调整了结构体中元素的顺序,还是求它的大小,这个结果是8。这是为什么呢?那是因为结构体的内存对齐!
【结构体的对齐规则】:
1. 第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对其数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
(VS中默认值为8)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
规则是不是看起来很绕?当然啦!这里可是重难点!我们需要结合一些例子帮助理解~然后再来看这个规则就会清楚一些啦!
我们来试一下吧!
struct s1
{
char c1; //1
int i; //4
char c2; //1
};
int main()
{
struct s1 s;
printf("%d\n", sizeof(s));
return 0;
}
对于这个代码,我们要给结构体变量s来开辟空间:
假设下面的表格就是我们的内存空间(右边一列表示相对起始位置的偏移量):
- 由规则1我们知道s从偏移量为0处开始开辟,c1也就存放在偏移量为0的位置
c1只占一个字节,所以它只需要偏移量为0的那一格就够了。(见下表格)
- 然后我们读到规则2,“其他成员要对齐到某个数字(对齐数)的整数倍的地址处”,其中对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
比如int i,它的大小是4个字节,编译器默认对齐数为8,取较小,所以它的对齐数是4。这意味着,i这个成员必须对齐到4的倍数处的偏移量的位置。在0往下找偏移量为4的倍数,那么我们找到4。于是,i就从偏移量为4的位置开始存放。i占4个字节,所以4~7都是i的。(见下表格)
- 然后我们来放c2,c2的放置也是向上面那样遵守规则。对齐数为自身大小(char的自身大小为1)和默认对齐数(8)的较小值。所以c2的对齐数为1。c2只需要存放在偏移量为1的倍数的位置即可。那么我们就在7以下找1的倍数的偏移量,于是c2就放在的偏移量为8的位置处。(见下表格)
- 现在我们一共放了0~8,共9个字节大小的内存。最后,由我们的规则3:“结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍”得,每个成员的最大对齐数为4,所以我们这个结构体的总大小需要是4的倍数。而我们现在的大小是9,所以离9最近的4的倍数是12,所以我们的总大小是12。于是见下表格。
(从这里开辟s1)c1 | 0 |
(浪费) | 1 |
(浪费) | 2 |
(浪费) | 3 |
i | 4 |
i | 5 |
i | 6 |
i | 7 |
c2 | 8 |
(浪费) | 9 |
(浪费) | 10 |
(浪费)最后的对齐 | 11 |
12 | |
13 | |
14 | |
15 | |
16 | |
17 | |
18 | |
19 | |
20 |
大家听懂了嘛?让我们再举个例子熟悉一下吧!
struct s2
{
char c1; //1
char c2; //1
int i; //4
};
int main()
{
struct s2 s;
printf("%d\n", sizeof(s));
return 0;
}
- 由规则1得,第一个成员c1放在0偏移处,且占一个字节。
- 由规则2得,第二个成员c2大小为1,默认对齐数为8,那么对齐数取较小值,为1。c2存放在偏移量为1的倍数的位置即可,于是c2放在偏移量为1的位置,且占一个字节。
- 由规则2得,第三个成员i大小为4,默认对齐数为8,那么对齐数取较小值,为4。i存放在偏移量为4的倍数处。于是i存放在偏移量为4的位置处,且占4个字节。
- 由规则3得,结构体总大小应该是最大对齐数的整数倍,这里的最大对齐数是1,1,4中的最大值4,所以结构体的总大小应该是4的整数倍。而目前0~7大小为8,是4的整数倍。所以这个结构体的大小就是8。
(从这里开辟s2)c1 | 0 |
c2 | 1 |
(浪费) | 2 |
(浪费) | 3 |
i | 4 |
i | 5 |
i | 6 |
i | 7 |
8 | |
9 | |
10 | |
11 | |
12 | |
13 | |
14 | |
15 |
(补:Linux环境下没有默认对齐数,没有默认对齐数时,自身大小就是对齐数!)
(补:c语言里有个叫做offsetof的宏,是计算结构体成员相对起始位置的偏移量的,大家可以用offsetof来验证)
比如printf("%u\n", offsetof(struct s1, c1)); 你就可以得到0。
下面我们来体验一下规则4。
struct s3
{
double d;
char c;
int i;
}; //我们可以用规则1、2、3算出大小是16
struct s4
{
char c1; //1
struct s3 s; //16
double d; //8
};
现在要求s4的大小。此时出现了嵌套。
- 规则4说:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- c1放0偏移处。老规矩了。
- 然后放s3类型的s。s的大小为16个字节。但是这16个字节是从哪里开始的呢?“嵌套的结构体对齐到自己的最大对齐数的整数倍处”,s3的最大对齐数是8。所以s需要对齐到8的整数倍处。于是s从偏移量为8开始,占16个字节。
- d的存放照常,对齐数是8,24是8的倍数所以放在24处,占8个字节。
- 目前0~31,占了32个字节,“结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍”,所有成员中最大对齐数是8,所以这个结构体的大小应该是8的整数倍。32是8的倍数,所以该结构体大小为32。
(s4从这里开始)c1 | 0 |
(浪 | 1 |
2 | |
3 | |
4 | |
5 | |
6 | |
费) | 7 |
s | 8 |
s | 9 |
s | 10 |
s | 11 |
s | 12 |
s | 13 |
s | 14 |
s | 15 |
s | 16 |
s | 17 |
s | 18 |
s | 19 |
s | 20 |
s | 21 |
s | 22 |
s | 23 |
d | 24 |
d | 25 |
d | 26 |
d | 27 |
d | 28 |
d | 29 |
d | 30 |
d | 31 |
32 | |
33 | |
34 |
——为什么存在结构体对齐?
——1.平台原因:不是所有硬件平台都能访问任意地址上的任意数据的。
2.性能原因:数据结构应该尽量在自然边界上对齐。未对齐的内存,处理器需要两次内存访问,对齐的只需要一次。(也就是用空间换时间)
为了节省空间,可将结构体中较小的成员写在一起。
5. 修改默认对齐数
刚才我们的默认对齐数是8,其实这个是可以改的。
struct s
{
char c1;
double d;
};
int main()
{
struct s ss;
printf("%d\n", sizeof(ss));
return 0;
}
不修改,即默认对齐数为8时,大小为16个字节。现在我们来修改一下:
#pragma pack(4) //改成4
struct s
{
char c1;
double d;
};
#pragma pack() //改回默认值
int main()
{
struct s ss;
printf("%d\n", sizeof(ss));
return 0;
}
此时,我们改成4之后,它的大小(还是依照之前的规则计算得到)就是12个字节。如果改成1就是不对齐。
虽然我们支持修改默认对齐数,但我们最好不要瞎改,一般对齐值都为4、8。与机器字长匹配。所以最好不要修改为别的特殊值。
6. 结构体传参
如果结构体传参传结构体对象的话,(需要压栈)会浪费时间空间。
如果结构体传参传结构体地址的话,效率会更高!
结论:结构体传参尽量传地址!效率更高!而且方便修改!
好啦!C语言自定义类型——【结构体,枚举,联合】(一)的内容就这么多~关于位段、枚举、联合体的内容请见下一篇博客C语言自定义类型——【结构体,枚举,联合】(二)!欢迎大家多多交流!