目录
[1.1 一般结构体声明](#1.1 一般结构体声明)
[1.2 结构的特殊声明(匿名)](#1.2 结构的特殊声明(匿名))
一、结构体类型的声明
1.1 一般结构体声明
结构体声明的语法:
cpp
struct tag
{
member - list; //成员名
}variable - list; //变量名
member - list是成员名
variable - list是创建的变量名
比如我们想创建一个描述学生的结构体:
cpp
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
};
1.2 结构的特殊声明(匿名)
cpp
// 匿名结构体:无类型名,直接定义变量 x
struct
{
int a;
char b;
float c;
} x;
// 匿名结构体:定义数组 a[20]、指针 p
struct
{
int a;
char b;
float c;
} a[20], * p;
我们没有给这两个结构体结构体起名字,所以这两个结构体被称之为匿名结构体
匿名结构体的特点:
每个匿名结构体都是不同的类型,就像上述代码,虽然两个匿名结构体中存的内容一样,但它们其实属于两个不同的匿名结构体类型,如果此时我们让*p=&x,系统会报错,说前后类型不一
匿名的结构体类型,无法单独创建变量,只能在声明的时候就将变量创建好
匿名的结构体类型,如果没有对结构体类型重命名的话,只能使用一次,使用一次的意思是,只能在定义结构体的那一行,一次性创造好变量名,之后再想换行单独创建变量,是做不到的,但是后续赋值、访问、修改、打印都是正常使用的,都没问题,只是记住!!!只能一次性创造好变量名!
1.3结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
比如定义一个链表的节点:
cpp
struct Node
{
int data;
struct Node next;
};
我们写成上面这种代码的形式可行吗?sizeof(struct Node)能计算出该结构体的大小吗?
上面这种结构体中嵌套结构体的形式是不可行的,因为我们没法计算出结构体struct Node的大小,计算第一次时是计算 data(4字节)+ struct Node(未知大小),struct Node的大小未知,所以我们又会计算struct Node的大小,就这样会一直循环往复,根本无法计算出正确大小
所以我们正确自引用的写法应该是:
cpp
typedef struct Node
{
int data;
struct Node* next;
} Node;
在结构体中嵌套一个指针结构体变量,这个指针变量的大小是4或8,所以就能正确计算出我们struct Node结构体的大小,就可以正常使用
总结:结构体自引用 = 结构体里放一个【指向自己的指针】
二、结构体变量的创建和初始化
1.结构体变量的创建可以在声明的时候就创建好;也可以单独创建
2.结构体的初始化,如果在结构体声明的时候就创建了变量,那么可以顺带初始化,可以用到.结构体成员操作符
3.结构体的初始化也可以在单独创建结构体变量后初始化,可以用到.结构体成员操作符
. 可以访问成员,也能配合变量初始化
-> 只能访问成员,绝对不能用来初始化
我们来把这创建变量和初始化的分别的两种情况都看一下:
(1)情况1:结构体变量的创建
在结构体声明的时候就创建好变量:
cpp
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
}s1, s2, * s3; //全局变量
我们创建了struct Stu类型的变量s1,s2和struct Stu*类型的s3,它们都是全局变量
(2)情况2:结构体变量的创建
变量名也可以单独创建:
cpp
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
};
struct Stu s1; //全局变量s1
int main()
{
struct Stu s2; //局部变量s2
struct Stu* s3; //局部变量s3
return 0;
}
struct Stu s1在主函数外面创建,是全局变量;struct Stu s2和struct Stu* s3在{}内,是局部变量
(3)情况3:结构体变量的初始化
在结构体声明的时候就已创建好变量的初始化:
cpp
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
}s1 = { .age = 22,.name = "love" }, s2 = { "kkeeper",20 }, * s3 = NULL;
s1使用.成员操作符去创建变量,可以不按照顺序,但s2初始化为{ "kkeeper",20 },没有用.成员操作符,就只能按照顺序进行初始化,结构体指针变量s3初始化为NULL空指针
(4)情况4:结构体变量的初始化
在结构体变量单独创建时初始化:
cpp
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
};
struct Stu s1 = { .age = 20, .name="XiaoMing"}; //全局变量s1
int main()
{
struct Stu s2 = { .age = 20, .name = "DaMing" }; //局部变量s2
struct Stu* s3=&s1; //局部变量s3
return 0;
}
三、结构成员访问操作符
结构体成员访问操作符有两个: . 和 ->
我们来看看具体使用方法:
. 结构体变量.结构体成员名
-> 结构体指针->结构体成员名
cpp
#include <stdio.h>
struct Stu
{
char name[20]; // 名字
int age; // 年龄
};
struct Stu s1 = { .age = 20, .name="XiaoMing"};
int main()
{
struct Stu* s3=&s1;
printf("%d %s\n", s1.age, s1.name); // .成员访问操作符
printf("%d %s\n", s3->age, s3->name); // ->成员访问操作符
return 0;
}
四、结构体内存对齐
结构体的大小是怎么在内存中开辟的呢?这需要我们了解结构体内存对齐的知识
4.1对齐规则
结构体内存对齐规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为 0 的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
- VS 中默认的值为 8。
- Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小。
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
上述的规则,开辟的内存,都是在对齐好后才开始开辟内存的
4.2代码练习:判断结构体大小
(1)练习1
cpp
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
VS中默认对齐数为8,char c1变量大小为1,所以对齐数取较小的,为1,所以char c1开辟一个字节的大小,从偏移量为0处开始开辟一个字节的大小,开辟完后偏移量来到1;接着int i变量大小为4,比默认对齐数小,所以对齐数取4,我们要先将内存的偏移量达到4后,才会开辟int i的内存空间,开辟四个字节,此时偏移量到8;最后char c2,偏移量为1,现在偏移量是8,是1的倍数,所以我们直接紧接着给char c2开辟属于它的空间,一个字节,此时偏移量来到9;因为我们知道,最终的结构体的整体大小一定是所以对齐数中最大对齐数的正数倍,而偏移量为9,就是是只开辟了9个字节的空间,是对齐数1的倍数,但不是对齐数4的倍数,所以我们又会开辟3个字节的空间,让偏移量来到12,此时这个结构体大小就为12
运行截图:

但是因为为了达到我们的内存对齐,我们会浪费掉一些空间,上述的结构体中,在创建完char c1后,我们开辟了三个字节的空间,用于内存对齐,这三个空间是不使用的,接着我们开辟了四个字节的空间给int i,接着又开辟一个字节给char c2,此时偏移量来到9,最后又要开辟三个字节的空间,来保证我们的内存对齐,所以我们就浪费了六个字节的空间去内存对齐
(2)练习2
cpp
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
c1对齐数取1,c2对齐数取1,i对齐数取4,首先很简单c1,c2开辟了两个字节的空间,偏移量来到2,接着在创建int i的空间时,我们首先要开辟两个字节的空间,让偏移量来到4,接着才给我们的int i开辟四个字节的空间,此时偏移量为8,结构体总大小为8个字节,是最大对齐数4的倍数,所以结果为8
运行截图:

(3)练习3
cpp
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
double对齐数取8,char对齐数取1,int对齐数取4,初始时,创建八个字节的空间给double d,偏移量来到8,8是char对齐数1的倍数,所以直接给char c开辟一个字节的空间,此时偏移量来到9,9不是int对齐数4的倍数,所以我们要开辟3个空间,让对齐数来到12后,再去开辟4个空间给int i,此时总共开辟了16个字节的空间,浪费3个字节的空间
运行截图:

(4)练习4:结构体嵌套问题
cpp
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
结构体struct S4中,char c1的对齐数为1,struct S3 s3结构体变量的对齐数为该结构体中的最大对齐数,是double类型的对齐数,为8,double d的对齐数是8。所以我们char c1首先先开辟一个字节内存空间,偏移量为1,因为struct S3的偏移量为8,所以我们又开辟了七个字节的空间,用于和struct S3对齐数对齐,偏移量来到8,然后开辟我们struct S3 s3的空间,练习3中我们知道,struct S3的结构体大小是16,所以此处我们开辟了16个字节的内存大小,偏移量来到24,24是8的倍数,所以我们直接给double d开辟8个字节的空间,此时总开辟的空间就为1+7+16+8=32
4.3为什么存在内存对齐
首先内存对齐的规则只发生于结构体/联合体中
为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 因为CPU 不是一个字节一个字节读内存的,而是按块读取(一次读 4/8 字节)。
- 如果数据对齐存放 ,CPU 一次就能读完。
- 如果数据没对齐 ,CPU 要读两次 → 拼接数据 → 丢弃多余,速度慢很多
比如结构体中创建了一个char i 和 int j 变量:如果不存在内存对齐,那么就开辟六个字节的空间就足够了,此时CPU一次读取4个字节的空间话,读第一次,读掉了char i,但 int j 只读取了它的三个字节,我们还需要读取一次,才能把 int j 的最后一个字节读取到,所以单单 int j 这个数据,我们就分了两次去读取,读取效率较慢;如果存在内存对齐,那么就需要开辟八个字节的空间,char i占1个字节后,开辟3个字节的空间浪费掉用于对齐,然后给四个字节给int j,这样CPU在读取的时候,第一次就能把char i 和 开辟的用于对齐的3个字节的空间读取掉,然后再读int i,读 int i 时,就能一次性读取四个字节,刚好就是int i的四个字节,这样int i就不用分两次去读取了,一次就能将这个数据读取成功,这样读取效率更高
总的来说:结构体的内存对齐就是用空间 换取时间
如果我们在设计结构体的时候,既要满足内存对齐,又要节省空间,我们该怎么做?
比如:
cpp
struct S1 //结构体大小为12
{
char c1;
int i;
char c2;
};
struct S2 //结构体大小为8
{
char c1;
char c2;
int i;
};
结构体struct S1 和 struct S2 中所存的数据类型一模一样,但创建的结构体大小却不一样,这是经典的内存对齐的问题,这是因为在S1中,我们浪费了6个字节的空间,但在S2中,只浪费了2个字节的空间,所以当我们把占用内存空间较小的成员集中在一起时,因为内存已经对齐好了,我们不需要给同样类型的成员再浪费空间进行内存对齐,这也就有助于节省我们的空间
总结:将占用内存空间小的成员集中在一起
4.4修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
- #pragma pack(num) 设置默认对齐数
- #pragma pack() 取消设置的对齐数,还原为默认
用法如下:
cpp
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
struct S1时,我们通过 #pragma pack(1) 将默认对齐数设置为 1,那么struct S1根据我们内存对齐的规则,就会开辟6个字节的空间给这个结构体;在struct S2时,我们先通过 #pragma pack() 将我们修改的对齐数,还原为默认对齐数,在VS中为8,根据内存对齐的规则,就会开辟12个字节的空间给这个结构体
运行截图:

五、结构体传参
结构体传参分为两种:传结构体变量 和 传结构体变量的地址
分别用结构体变量 和 结构体指针变量 去接收
下面我们来看一串代码演示:
cpp
#include <stdio.h>
struct S
{
int data[10];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
// 结构体传参(值传递)
void print1(struct S s)
{
printf("%d\n", s.num);
}
// 结构体地址传参(指针传递)
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
//主函数
int main()
{
print1(s); // 传结构体
print2(&s); // 传地址
return 0;
}
运行截图:

我们将结构体变量 (s) 传给了printf1 函数,将结构体变量的地址 (&s) 传给了printf2 函数,再分别在这两个函数中调用我们的 . 和 -> 成员访问操作符去打印了我们的num数据,如上图截图所示,但其中我们要注意一个知识点,传结构体变量时 ,其实是传值调用,形参会单独开辟一份空间,并将传入的结构体复制一遍在这个空间,才是如果在函数内部修改这个形参的结构体数据,是不会改变实参的结构体数据的;传结构体变量的地址时,是传址调用,我们会通过地址找到我们实参的结构体,不会单独创立一个新的空间,此时如果在函数内部修改我们结构体的数据,改变的就是我们实参的结构体数据
我们来用代码观察一下以上说的 传值调用 和 传参调用 的区别:
cpp
#include <stdio.h>
struct S
{
int data[10];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
// 结构体传参(值传递)
void print1(struct S s)
{
s.num = 100;
}
// 结构体地址传参(指针传递)
void print2(struct S* ps)
{
ps->num = 100;
}
int main()
{
print1(s); // 传结构体
printf("%d\n", s.num);
print2(&s); // 传地址
printf("%d\n", s.num);
return 0;
}
运行截图:

由截图可以看到,传值调用确实不会改变实参,传址调用会改变实参
对比两种传参方式,我们更建议用传址调用,因为传值调用会重新开辟一块新空间将原结构体复制进去,在栈区中压栈入栈出栈都要时间,这样既耗费了空间也耗费了时间
结论:结构体传参的时候,要传结构体的地址
六、结构体实现位段
6.1什么是位段
位端的声明和结构体高度类似,它是一种特殊的结构体,位段等于在结构体里,给每位成员按bit位分配空间,而不是按字节分配,目的是节省内存空间, 位段的成员必须是整数类型 :int、unsigned int、``signed int 类型,在C99标准之后的编译器中,也可以是 char、short 等整型 家族的类型**,但** 不能是 float、double、指针 等
下面我们来看看位段的具体写法:
cpp
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
位段要在结构体变量之后加一个冒号和一个数字,如上述代码所示,A就是一个位段类型,_a就是位端的成员,简称一个位段,(:2)表示_a这个变量只占两个bit位的空间
6.2位段的内存分配
位段内存分配的规则:
1.位段类型的位段成员一般都是同一种类型
2.位段的内存分配是位段成员类型来决定的,都是int 类型的数据,就会以4个字节为单位开辟空间;都是short 类型的数据,就会以2个字节为单位开辟空间;都是char 类型的数据,就会以1个字节为单位来开辟空间
3.用完当前开辟的单位的空间,如果不够使用,才会再开辟下一个相同单位大小的空间
我们来看看具体的代码,以VS编译器中为例:思考下面这串代码是如何分配内存的?
cpp
#include <stdio.h>
struct S
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
struct S s = { 0 };
int main()
{
s._a = 10;
s._b = 12;
s._c = 3;
s._d = 4;
printf("%zd\n", sizeof(struct S));
return 0;
}
在VS中,我们是这样来给位段类型的数据开辟内存的:首先因为struct S这个位段类型中的数据都是char类型,所以我们是以一个字节为单位开辟内存,如果开辟的空间不够使用,我们才会再开辟下一个相同单位大小的空间,而且在VS中,我们都是从这个字节空间的低位开始存储的,在第一个字节中,一个字节为8个bit位,首先使用了3个bit位给_a,再使用了4个bit位给_b,剩下一个bit位,不够char c:5存放,所以我们又开辟了一个字节的空间,8个bit位,使用5个bit位给char _c,剩下3个bit位不够_d:4存放,所以我们又开辟了一个字节的空间,8个bit位,使用了4个bit位给char _d,所以我们总共就开辟了三个字节的空间,省了一个字节的空间大小
运行截图:

6.3位段使用的注意事项
在上述6.2的代码中,我们要注意一个位段使用的注意事项:由于我们是从字节的低位开始存放数据,比如我们在放_a 和 _b在第一个字节当中,我们占用的7个bit位是从低位开始占用的,也就是第1个bit位,并没有被占用,而是被浪费了,但这个字节的地址,就是指向的第一个bit位,内存中每个字节分配一个地址,而个字节内部的bit位是没有地址的,所以我们没法指向一个字节中的第二个bit位,由此我们可以知道,我们无法在内存中精确具体访问到我们的_a 和 _b数据,也就是不能对位段的成员进行取地址(&)操作,无法使用scanf库函数进行输入数据的操作,但是我们能通过赋值的操作,给位段的成员进行赋值,编译器会帮我们进行位运算,找到对应的 bit 位,修改数据
总结:位段成员不能进行取地址(&)操作,只能进行读取和赋值操作
6.4位段的跨平台问题
位段不适用于可移植性代码的书写,不具备可移植性,不能用于跨平台、可移植的程序开发
原因如下四点:
1.不同平台下的int类型或者char类型数据,是有符号数还是无符号数是不确定的,不同编译器解释不同
2.int 类型占4个字节,但在有些平台下,只占2个字节的大小,位段成员能开辟的最大bit数不一样,如果在int为4个字节大小的平台中,给一个位段成员开辟了30个bit位的空间,那么在移植到int为2个字节的平台中,就会发生错误
3.位段的成员空间,是从开辟的字节的低位开始分配,还是高位开始分配,不同平台规则不同
4.开辟完一块空间给一个位段成员时,如果有剩但不够存放下一个位段成员,这个剩余空间我们是浪费,还是给下一个位段成员,这是不确定的,C语言并未规定,由编译器自行实现
6.5位段的使用
数据报:其中很多的属性只需要几个bit位就能描述,此时使用位段就能达到想要的效果

感谢大家的观看,新人求互三,关注我必回关!下章见!