
目录一、结构体类型的声明1.1 一般结构体声明1.2 结构的特殊声明匿名1.3结构体的自引用二、结构体变量的创建和初始化1情况1结构体变量的创建2情况2结构体变量的创建3情况3结构体变量的初始化4情况4结构体变量的初始化三、结构成员访问操作符四、结构体内存对齐4.1对齐规则4.2代码练习判断结构体大小1练习12练习23练习34练习4结构体嵌套问题4.3为什么存在内存对齐4.4修改默认对齐数五、结构体传参六、结构体实现位段6.1什么是位段6.2位段的内存分配6.3位段使用的注意事项6.4位段的跨平台问题6.5位段的使用一、结构体类型的声明1.1 一般结构体声明结构体声明的语法struct tag { member - list; //成员名 }variable - list; //变量名tag 是结构体的名字member - list是成员名variable - list是创建的变量名比如我们想创建一个描述学生的结构体struct Stu { char name[20]; // 名字 int age; // 年龄 char sex[5]; // 性别 char id[20]; // 学号 };1.2 结构的特殊声明匿名// 匿名结构体无类型名直接定义变量 x struct { int a; char b; float c; } x; // 匿名结构体定义数组 a[20]、指针 p struct { int a; char b; float c; } a[20], * p;我们没有给这两个结构体结构体起名字所以这两个结构体被称之为匿名结构体匿名结构体的特点每个匿名结构体都是不同的类型就像上述代码虽然两个匿名结构体中存的内容一样但它们其实属于两个不同的匿名结构体类型如果此时我们让*px系统会报错说前后类型不一匿名的结构体类型无法单独创建变量只能在声明的时候就将变量创建好匿名的结构体类型如果没有对结构体类型重命名的话只能使用一次使用一次的意思是只能在定义结构体的那一行一次性创造好变量名之后再想换行单独创建变量是做不到的但是后续赋值、访问、修改、打印都是正常使用的都没问题只是记住只能一次性创造好变量名1.3结构体的自引用在结构中包含一个类型为该结构本身的成员是否可以呢比如定义一个链表的节点struct Node { int data; struct Node next; };我们写成上面这种代码的形式可行吗sizeof(struct Node)能计算出该结构体的大小吗上面这种结构体中嵌套结构体的形式是不可行的因为我们没法计算出结构体struct Node的大小计算第一次时是计算 data4字节 struct Node未知大小struct Node的大小未知所以我们又会计算struct Node的大小就这样会一直循环往复根本无法计算出正确大小所以我们正确自引用的写法应该是typedef struct Node { int data; struct Node* next; } Node;在结构体中嵌套一个指针结构体变量这个指针变量的大小是4或8所以就能正确计算出我们struct Node结构体的大小就可以正常使用总结结构体自引用 结构体里放一个【指向自己的指针】二、结构体变量的创建和初始化1.结构体变量的创建可以在声明的时候就创建好也可以单独创建2.结构体的初始化如果在结构体声明的时候就创建了变量那么可以顺带初始化可以用到.结构体成员操作符3.结构体的初始化也可以在单独创建结构体变量后初始化可以用到.结构体成员操作符.可以访问成员也能配合变量初始化-只能访问成员绝对不能用来初始化我们来把这创建变量和初始化的分别的两种情况都看一下1情况1结构体变量的创建在结构体声明的时候就创建好变量#include stdio.h struct Stu { char name[20]; // 名字 int age; // 年龄 }s1, s2, * s3; //全局变量我们创建了struct Stu类型的变量s1s2和struct Stu*类型的s3它们都是全局变量2情况2结构体变量的创建变量名也可以单独创建#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结构体变量的初始化在结构体声明的时候就已创建好变量的初始化#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结构体变量的初始化在结构体变量单独创建时初始化#include stdio.h struct Stu { char name[20]; // 名字 int age; // 年龄 }; struct Stu s1 { .age 20, .nameXiaoMing}; //全局变量s1 int main() { struct Stu s2 { .age 20, .name DaMing }; //局部变量s2 struct Stu* s3s1; //局部变量s3 return 0; }三、结构成员访问操作符结构体成员访问操作符有两个: . 和 -我们来看看具体使用方法. 结构体变量.结构体成员名- 结构体指针-结构体成员名#include stdio.h struct Stu { char name[20]; // 名字 int age; // 年龄 }; struct Stu s1 { .age 20, .nameXiaoMing}; int main() { struct Stu* s3s1; 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练习1struct S1 { char c1; int i; char c2; }; printf(%d\n, sizeof(struct S1));VS中默认对齐数为8char 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练习2struct S2 { char c1; char c2; int i; }; printf(%d\n, sizeof(struct S2));c1对齐数取1c2对齐数取1i对齐数取4首先很简单c1c2开辟了两个字节的空间偏移量来到2接着在创建int i的空间时我们首先要开辟两个字节的空间让偏移量来到4接着才给我们的int i开辟四个字节的空间此时偏移量为8结构体总大小为8个字节是最大对齐数4的倍数所以结果为8运行截图3练习3struct S3 { double d; char c; int i; }; printf(%d\n, sizeof(struct S3));double对齐数取8char对齐数取1int对齐数取4初始时创建八个字节的空间给double d偏移量来到88是char对齐数1的倍数所以直接给char c开辟一个字节的空间此时偏移量来到99不是int对齐数4的倍数所以我们要开辟3个空间让对齐数来到12后再去开辟4个空间给int i此时总共开辟了16个字节的空间浪费3个字节的空间运行截图4练习4结构体嵌套问题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的对齐数为1struct S3 s3结构体变量的对齐数为该结构体中的最大对齐数是double类型的对齐数为8double 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个字节的空间此时总开辟的空间就为17168324.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就不用分两次去读取了一次就能将这个数据读取成功这样读取效率更高总的来说结构体的内存对齐就是用空间换取时间如果我们在设计结构体的时候既要满足内存对齐又要节省空间我们该怎么做比如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() 取消设置的对齐数还原为默认用法如下#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个字节的空间给这个结构体运行截图五、结构体传参结构体传参分为两种传结构体变量和传结构体变量的地址分别用结构体变量和结构体指针变量去接收下面我们来看一串代码演示#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数据如上图截图所示但其中我们要注意一个知识点传结构体变量时其实是传值调用形参会单独开辟一份空间并将传入的结构体复制一遍在这个空间才是如果在函数内部修改这个形参的结构体数据是不会改变实参的结构体数据的传结构体变量的地址时是传址调用我们会通过地址找到我们实参的结构体不会单独创立一个新的空间此时如果在函数内部修改我们结构体的数据改变的就是我们实参的结构体数据我们来用代码观察一下以上说的传值调用和传参调用的区别#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、指针 等下面我们来看看位段的具体写法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编译器中为例思考下面这串代码是如何分配内存的#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 c5存放所以我们又开辟了一个字节的空间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位就能描述此时使用位段就能达到想要的效果感谢大家的观看新人求互三关注我必回关下章见