目录
1. 结构体类型的声明
1.1 结构体类型的简单介绍和声明
1.1.1 结构的声明
1.1.2 特殊的声明
1.1.3 结构的自引用
2. 结构体变量的创建和初始化
3. 结构成员访问操作符
4. 结构体内存对⻬
4.1 对齐规则
4.2 练习
4.2.1 练习1
4.2.2 练习2
4.3 为什么存在内存对⻬?
4.4 修改默认对齐数
5. 结构体传参
6. 结构体实现位段
6.1 什么是位段
6.2 位段的内存分配
6.3 位段的跨平台问题
6.4 位段使⽤的注意事项
1. 结构体类型的声明
1.1 结构体类型的简单介绍和声明
首先,结构体分为内置类型和自定义类型2种,我们今天介绍的就是自定义类型的结构体。
当内置类型不能满足需求的时候,我们就用自定义类型的结构体,例如:我们要描述一本书,不能只通过一个变量来描述,需要知道书的书名、作者、价格、类型等等
1.1.1 结构的声明
而这就需要自定义类型的结构体来实现,结构体里面包含了各种类型的数据,用来描述一个复杂对象的各种属性。
struct book
{
char name1[20];//作者名
char name2[20];//书名
int price;//价格
//......
};
1.1.2 特殊的声明
我们有时候会看见这种匿名结构体类型
请看接下来的代码:
struct
{
char a;
int c;
float d;
}s = { 0 };
struct
{
char a;
int c;
float d;
}*ps;
int main()
{
ps = &s;
return 0;
}
//在上⾯代码的基础上,下⾯的代码合法吗?
ps= &s;
警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
1.1.3 结构的自引用
大家觉得下面的代码可以运行成功吗?
struct Node
{
int data;//存储数据--4个字节
struct Node next;//下一个节点的地址--->指针类型变量4or8个字节
};
int main()
{
struct Node n;
return 0;
}
在给出答案之前,我们先来了解一下数据结构
我们发现不可以,这是为什么呢?
我们可以假想一下:房子里面要放下一个大小一样的房子和另外的东西,这是不可能的
方法:将一个数据结构分成2部分,一半存数据,一半存放下一个节点的地址,这样就可以存储结构体指针了,也就可以结构自引用了
struct Node
{
int data;//存储数据--4个字节
struct Node* next;//下一个节点的地址--->指针类型变量4or8个字节
};
int main()
{
struct Node n;
return 0;
}
这样问题就解决了
2. 结构体变量的创建和初始化
初始化很简单,请看代码
struct Stu
{
char name[20];
int age;
float score;
}s1,s2;//全局变量
struct Stu
{
char name[20];
int age;
float score;
};
int main
{
struct Stu s1,s2,s3;//局部变量
return 0;
}
3. 结构成员访问操作符
结构体访问成员有2种方法:
1.结构体变量.成员变量名
2.结构体指针—>成员变量名
请看代码:
struct Stu
{
char name[20];
int age;
float score;
}s1 = { "zhangsan",22,35.0f };
int main()
{
struct Stu s2 ={"lisi",19,88.5f };
struct Stu s3 = { "wangwu",39,76.0f };//按照顺序
struct Stu s4 = { .age = 33,.name = "xiaofang",.score = 38.5f };//不按顺序
printf("%s %d %lf\n", s1.name, s1.age, s1.score);
printf("%s %d %lf\n", s2.name, s2.age, s2.score);
struct Stu* ps = &s4;
printf("%s %d %lf\n", ps->name, ps->age, ps->score);
return 0;
}
4. 结构体内存对⻬
我们先看下面的代码,你觉得输出的是什么?
int main()
{
struct S1
{
char c1;//1个字节
int I;//4个字节
char c2;//1个字节
};
printf("%d\n", sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
return 0;
}
结果和你想的一样吗?
为什么成员一样,顺序不一样,存放的数据类型和变量也一样,但是结果不一样呢?
为什么开辟的空间不一样?s1和s2到底是怎么分配内存空间的?这里就涉及到了内存对齐!!!
4.1 对齐规则
1. 结构体的第⼀个成员对⻬到相对结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
- VS中默认的值为8
- Linux中没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数(不包括默认对齐数),所有对⻬数中最⼤的)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
那我们来看看刚刚的s1和s2是怎么存储的
4.2 练习
4.2.1 练习1
struct S3
{
double d;//8个字节
char c;//1个字节
int I;//4个字节
};
printf("%d\n", sizeof(struct S3));
4.2.2 练习2
struct S3
{
double d;//8个字节
char c;//1个字节
int I;//4个字节
};
printf("%d\n", sizeof(struct S3));
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
4.3 为什么存在内存对⻬?
简单来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。
4.4 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对⻬数,还原为默认
但是,我们一般设置成2的次方数,如果乱修改的话,可能适得其反
5. 结构体传参
结构体传参有2种方式,分别是传值调用和传址调用
struct S
{
int data[1000];
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;
}
综合来看,还是传址调用比较好
6. 结构体实现位段
6.1 什么是位段
位段是基于结构体的,是可以节省空间的
位指的是二进制位
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。
使用举例:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
struct B
{
int _a;
int _b;
int _c;
int _d;
};
int main( )
{
printf("%d\n",sizeof(struct A));
printf("%d\n",sizeof(struct B));
return 0;
}
//空间是如何开辟的?
后面的数字指的是变量所占的空间大小, 例如:2指的就是变量a占两个二进制位,
以此类推,总共要占到42个二进制位, 我们初步推测,struct A大概需要6个字节
但是通过运行我们发现,实际上需要占到8个字节,但是还是比第二种方法所占内存小
我们发现和猜测的不一样,这就得讲讲位段的内存空间是怎么分配的了
6.2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
我们以接下来的代码来分析一下:
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;
printf("%d\n",sizeof(struct S));
return 0;
}
先根据位段大小,开辟空间;再根据存储数据的二进制位读取相应空间位数,存放至内存空间(统一假设从右向左储存,剩余空位不够下一个使用就空着不使用),开辟新的内存空间
下面,我们看看完整代码的内存存储是怎么样的
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;
printf("%d\n",sizeof(struct S));
return 0;
}
为了方便观察,我们在内存中以16进制的形式查看
但是,这是为什么呢?
6.3 位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
6.4 位段使⽤的注意事项
内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
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;
}
本次的分享到这里就结束了!!!
PS:小江目前只是个新手小白。欢迎大家在评论区讨论哦!有问题也可以讨论的!
如果对你有帮助的话,记得点赞👍+收藏⭐️+关注➕