目录
基本介绍
结构体是一些数据的集合,这些数据被称为成员,而每个成员可以是不同类型的变量。
而我们可以用结构体自定义一个类型来使用。
例如:
#include<stdio.h>
struct STU
{
char name[20];//char数组类型成员
int age;//int类型成员
struct STU Deskmate;//结构体类型成员
};
1.结构体类型的声明
#include<stdio.h>
struct STU
{
char name[20];
int age;
char sex[5];
};//👈分号不能少
定义一个结构体,描述好结构体内的成员类型,就是结构体的声明。
2. 结构体变量的创建和初始化
2.1结构体正常声明
创建结构体变量有两种方式:
第一种:在结构体的声明后面直接创建变量。
示例:
#include<stdio.h>
struct STU
{
char name[20];
int age;
char sex[5];
}stu = { "Lihua",18,"boy" };//👈创建一个结构体变量并赋予初始值。
其中,变量名为stu,三个成员:name,age,sex的值分别被赋为"Lihua",18,"boy"。
如果要创建多个变量,在变量后加个逗号再创建即可。
第二种:在函数内创建结构体变量并初始化。
示例:
#include<stdio.h>
struct STU
{
char name[20];
int age;
char sex[5];
};
int main()
{
struct STU stu = { "Lihua",18,"boy" };//👈创建一个结构体变量并赋予初始值。
return 0;
}
注意:在函数内创建结构体变量时,struct STU代表类型名(struct是在说明类型为结构体),
stu代表变量名。
2.2结构体的匿名声明
struct
{
char name[20];
int age;
char sex[5];
}stu = { "Lihua",18,"boy" };//👈只能在这里创建变量
结构体的匿名声明就是把结构体的类型名省去,正常声明结构体的类型名为struct STU,而匿名声明则将STU省去。
如此一来,结构体的类型名就不完整了,于是就不能正常在函数内创建变量(因为类型名不完整)
只能在结构体声明的最后创建变量,因此这类结构体通常只能用一次(创建一次变量)。
再来看一个例子:
#include<stdio.h>
struct
{
char name[20];
int age;
char sex[5];
}stu = { "Lihua",18,"boy" };//创建变量stu
struct
{
char name[20];
int age;
char sex[5];
}*p;//创建指针p
int main()
{
p = &stu;//指针P指向stu
return 0;
}
上面我创建了两个成员完全一样的结构体,用这两个结构体分别创建了一个变量stu与指针p,
并让指针P指向变量stu。问题来了,这么做合法吗?
警告:
结构体的类型名不完整,即使两个结构体的成员完全相同,编译器也会将两个结构体当作不同的类型。因此是非法的。
对于匿名结构体,如果没有对结构体进行重命名的话,基本只能使用一次。
2.3结构体的自引用
#include<stdio.h>
struct Node
{
int Data;
struct Node* next;//创建结构体指针变量
};
在结构体内创建自己的结构体变量,我们称之为结构体的自引用。
插播一条题外话,每次创建一个结构体变量时都要写struct+类型名+变量名,你会不会觉得这样写很麻烦?有没有一种更简便的写法呢?
有!只要使用typedef对结构体进行重命名即可。
#include<stdio.h>
typedef struct Node
{
int Data;
struct Node* next;
}N;//←对结构体重命名
//或者这样也行
//struct Node
//{
// int Data;
// struct Node* next;
//};//←对结构体重命名
//typedef struct Node N;
int main()
{
N node = { 0 };
return 0;
}
在结构体声明的开头写上typedef进行重命名,并在声明的结尾处写上结构体的新名字(N),就能将结构体类型名从struct Node重命名成N,这样创建结构体变量时就不会那么麻烦。
但是问题又来了,在结构体内进行结构体的自引用时能不能用重命名后的类型名呢?
答案是:不行。
因为声明结构体是先确定好结构体内的成员再对其进行重命名,如果在结构体内使用了重命名后的类型名,编译器会无法识别重命名后的类型名。
3. 结构成员访问操作符
既然我们能定义一个结构体,也能创建结构体变量,那么要如何访问结构体的成员呢?
要访问结构体成员,有两种方式。
3.1 直接访问结构体成员
#include<stdio.h>
struct
{
char name[20];
int age;
char sex[5];
}stu = { "Lihua",18,"boy" };
int main()
{
printf("%s %d %s", stu.name, stu.age, stu.sex);
return 0;
}
要直接访问结构体成员,可以使用 .操作符(点操作符),使用格式为:变量名.成员名。
3.2 通过指针访问结构体成员
除了直接访问结构体成员,还可以通过创建结构体指针,再用结构体指针来访问结构体成员。
例如:
#include<stdio.h>
struct STU
{
char name[20];
int age;
char sex[5];
}stu = { "Lihua",18,"boy" },*p=&stu;
int main()
{
printf("%s %d %s", p->name, p->age, p->sex);
return 0;
}
创建一个同类型的结构体指针,并指向结构体stu,再使用操作符->就可以访问stu的成员了。
使用格式:指针名->成员名
4. 结构体内存对齐
在谈论这个内容之前,先看一个例子:
struct test
{
int i;//4个字节
char c1;//1个字节
char c2;//1个字节
};
int main()
{
printf("%zd\n", sizeof(struct test));
return 0;
}
猜猜结构体test占用内存大小为多少?
按理来说,int型占4个字节,两个char型各占一个字节,因此结构体应该占用6个字节。
运行看看结果正不正确。
按理来讲咱算出来的是6,为什么实际占用内存是8呢?
这就涉及到一个知识点——结构体内存对齐。
4.1对齐规则
⾸先得掌握结构体的对⻬规则:1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。- VS 中默认的值为 8- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小3. 结构体总大小为最⼤对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
用上面的例子来解释一下对齐规则:
牛刀小试:
struct test
{
char c1;//1个字节
int i;//4个字节
char c2;//1个字节
};
int main()
{
printf("%zd\n", sizeof(struct test));
return 0;
}
还是上面那个例子,但是将第二个成员int i与第一个成员char c1对调位置,该结构体的内存占用
会有变化吗?如果有,应该如何变化?
将上面这个代码按照刚刚的讲的对齐规则来分析即可得出答案!
据我分析,该结构体的大小应为 12 。
运行程序看看计算结果是否正确。
正是如此!
4.2嵌套结构体
如果结构体内嵌套了结构体呢,又该如何计算其大小?
让咱来测试一下便可知晓!
#include<stdio.h>
struct test_in
{
double c1;//8个字节
int i;//4个字节
char c2;//1个字节
};
struct test
{
char c1;//1个字节
int i;//4个字节
struct test_in t;//占用 ?个字节
};
int main()
{
printf("%zd\n", sizeof(struct test));
return 0;
}
看看运行结果:
以下是分析过程:
按照内存对齐规则,先算出结构体struct test_in的大小(过程为左图):
#include<stdio.h>
struct test_in
{
double c1;//8个字节
int i;//4个字节
char c2;//1个字节
};
int main()
{
printf("%zd\n", sizeof(struct test_in));
return 0;
}
不难算出struct test_in的大小为16。在计算过程中,咱发现其成员的最大对齐数为8(double的)
根据对齐规则的第④条规则:嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处
可以知道:成员struct test_in的对齐数为8(double)。
因此,在结构体struct test中存储时,它需要存到偏移量为8的整数倍处(即图中下标为8的格子)
而整个结构体的大小为16,则直接存入16字节,此时结构体struct test的大小为8+16=24
根据对齐规则第④条:结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
可知:结构体struct test的最大对齐数为8(double),因此该结构体的大小必须为8的整数倍
而此时struct test的大小为24,符合对齐规则。因此可以得出结构体的大小为24。
4.3为什么存在内存对齐?
大部分的参考资料都是这样说的:1. 平台原因 (移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2. 性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那么,在设计结构体时,要如何做到既满足对齐规则又能尽量节省空间呢?
答:让占用空间小的成员尽量集中在一起。
例如:
struct test
{
int i;//4个字节
char c1;//1个字节
char c2;//1个字节
};
int main()
{
printf("%zd\n", sizeof(struct test));
return 0;
}
struct test
{
char c1;//1个字节
int i;//4个字节
char c2;//1个字节
};
int main()
{
printf("%zd\n", sizeof(struct test));
return 0;
}
就拿上面用的例子来说,第一段代码结构体大小为8字节,第二段代码结构体大小为12字节。
可以发现:即使成员类型完全相同,但是将占用空间小的成员放在一起的结构体大小会更小。
4.4修改默认对齐数
上文的对齐规则中有提到:
VS 中默认的值为 8- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
既然VS有默认对齐数,那么有没有办法能将VS的默认对齐数修改呢?
有!
#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
请看代码:
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S {
char c1;//1个字节
int i;//4个字节
char c2;//1个字节
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
将默认对齐数(8)修改为 1 后,三个成员char c1、int i、char c2 的实际对齐数将变为1
任何偏移量(下标)都为1的倍数,因此这三个成员就会紧凑地排在一起,就占用6个字节
用这个方法,结构体在对齐方式不合适的时候,我们可以自己更改默认对⻬数。
5. 结构体传参
先来看一段代码:
#include<stdio.h>
struct test
{
int data[1000];
int num;
};
void print1(struct test t)
{
printf("%d\n", t.num);
}
void print2(struct test* pt)
{
printf("%d\n", pt->num);
}
int main()
{
struct test T = { {0},10 };
print1(T);
print2(&T);
return 0;
}
这两个print函数都能打印出成员num的值,哪一个效果更好呢?
先看函数print1,形参为结构体变量。再看print2,形参为结构体指针。
根据学过的知识,我们知道:函数在传参时会创建一个临时变量(形参),并将实参的值拷贝到
该临时变量(形参)中。
也就是说:函数print1被调用时,会新创建一个结构体变量t,并将实参(T)的值赋给形参 t 。
而print2被调用时,会新创建一个结构体指针pt,并将实参(T的地址)赋给形参pt。
从占用内存的角度来看,print1会占用一个结构体的大小(sizeof(struct test)),而print2则只占用
了一个结构体指针的大小(所有指针的大小都统一 4/8个字节)。
综上所述,print2的效果更佳。
结论:结构体传参的时候,要传结构体的地址。
6. 结构体实现位段
6.1什么是位段
我们知道:char占一个字节,int占4个字节。
当我们在int中存储一个比较小的数据时,就会有内存浪费的现象。
例如:int a=5;
5的二进制为: 00000000 00000000 00000000 00000101
可以看到,虽然int占用4个字节,但是实际上存储有效数据的空间只有前三个比特位,
剩下的29个比特位将被浪费。
那么有没有一种方法,可以只给int型变量分配3个比特位的空间,以此来避免内存浪费呢?
有!那就是位段。
6.2位段的声明
位段的声明和结构是类似的,有两个不同:1. 位段的成员必须是 int、unsigned int 或signed int 、char,在C99中位段成员的类型也可以选择其他类型。2. 位段的成员名后边有⼀个冒号和⼀个数字。
例如:
struct A {
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型
我们打印一下,看看位段A所占内存的大小。
思考:结构体A中含有4个int型成员,按理说应当占用16个字节,实际却只占用了8个字节,这是
什么原因?
这就与位段的内存分配有关了。
6.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;
}
注意:位段的存储规则在每个编译器中都不同,在VS中是这样存,也许换到其他编译器就不同了
因此,位段的存储不支持跨平台!
根据上图的分析,该结构体一共使用了三个字节来存储各个成员的数据,
且数据为(从低字节到高字节)62 03 04,运行看看是否正确。
运行结果显示:确实占用了三个字节,且数据(低地址到高地址)也的确是62 03 04。
现在再回过头看看之前说的那个例子,是不是就迎刃而解了?
struct A {
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
6.4位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。2. 位段中最大位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
6.5位段使用的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
#include<stdio.h>
struct A
{int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
完