一 , 结构体类型的声明
1.1 结构体回顾
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
1.2 结构的声明
struct tag
{
member - list; //成员列表
} variable - list ; //变量列表
struct Stu
{
char name[20];
int age;
char id[12];
double weight;
};//分号不能省略
1.3 结构体变量的创建和初始化
1.结构体变量的创建有两种方式:全局变量的创建和局部变量的创建
struct Stu
{
char name[20];
int age;
char id[12];
double weight;
}s4,s5,s6;//分号不能省略
int main()
{
struct Stu s1;
struct Stu s2;
struct Stu s3;
return 0;
}
2. 结构体变量的初始化也有两种方式:
- 按照结构体成员的顺序初始化
- 按照指定的顺序初始化
struct Stu
{
char name[20];
int age;
char id[12];
double weight;
}s4,s5,s6;//分号不能省略
int main()
{
//按照结构体成员的顺序初始化
struct Stu s1 = { "zhngsan",20,"20240101",75.2 };
//按照指定的指定初始化
struct Stu s2 = { .age = 25,.id = "20240202",.name = "lisi",.weight = 80.0 };
//结构体成员变量的访问
printf("姓名:%s\n", s1.name);
printf("年龄:%d\n", s1.age);
printf("学号:%s\n", s1.id);
printf("体重:%.1lf\n", s1.weight);
struct Stu s3;
return 0;
}
1.4 结构的特殊声明
在声明结构的时候,可以不完全的声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
上面的两个结构体在声明的时候省略掉了结构体标签(tag)
思考以下代码是否合法?
p = &x;
- 编译器会把上面的两个声明当成不同的两个类型,所以是非法的
- 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次
1.5 结构体变量的重命名
使用 typedef 对变量进行重命名。
在这里先要区分,结构体标签 (tag) 和 结构体名字(name) 通常是用来引用结构体的两个不同的概念:
1.结构体标签(tag): 是在结构体定义的时候给结构体起的一个标识符,用于标记这个结构体的类型;
2.结构体名字(name):通过 typedef 关键字为结构体定义的一个别名,通过typedef 创建结构体别名,这个别名可以用来声明结构体变量,就像使用原始的结构体名字一样;
typedef struct tag
{
int x;
char y;
}tag_name;
这里的 tag_name 是结构体别名,可以用来声明变量,比方:tag_name s1;
总的来说,结构体标签是结构体定义时起的一个标识符,而结构体名字是通过 typedef 别名机制为结构体定义的一个简化名称。同时,结构体标签可以与结构体的别名相同,但是得注意以下的情况,在结构体创建中,别名是还没有定义的,不能再该结构体内去使用别名
//error
struct Node
{
int data;
Node* next;
}Node;
1.5 结构的自引用
思考:结构体中包含一个类型为该结构体本身的成员是否可以?在思考前先了解数据结构是什么:
数据结构 ---- 数据在内存中的存储结构
如果我们需要存储5个数据,既可以给它申请5个连续的空间来存放,即数组,在数据结构中,叫顺序表。也可以在内存中乱序的存放,但是可以通过一条 “ 链 ",把这些数据串连起来,在数据结构中,叫链表。
在链条中,我们把存放数据的这一块空间称 节点 ,这个节点不仅需要存放数据,还需要能找到下一个节点。
struct Node
{
int data;
struct Node n;
};
通过 sizeof(struct Node) 分析,这样的代码是不合理的,因为一个结构体再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大,是不合理的,但是可以通过下一个节点的地址来找到下一个节点,所以可以定义一个指针变量,而不是结构体本身。
struct Node
{
int data;
struct Node* n;
};
二 , 结构体内存对齐
结构体的大小怎样计算?
自定义了一个结构体类型,既然是类型的话,整型int 的字节大小是4,字符型char 是 1 ,变量的创建是向内存申请一块空间,那么创建结构体变量会向内存申请多大空间。
2.1 对齐规则
首先得掌握结构体的对齐规则:
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址数
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
-vs 中默认为8
-Linux中gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
struct s1
{
char c1;
char c2;
int n;
};
struct s2
{
char c1;
int n;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct s1));
printf("%zd\n", sizeof(struct s2));
return 0;
}
如果按照char 的大小是一个字节,int 的大小是四个字节来看,s1,和s2 的大小都是6?
根据结构体的对齐规则来看:
可以借助以下表的思路来理解:
练习题1:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
练习2:
//练习2
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
练习3:(嵌套)
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
2.2 为什么存在内存对齐
大部分的参考资料都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问 ;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总的来说:结构体的内存对齐是拿空间来换取时间的做法。
所以,在设计结构体的时候,既要满足内存对齐,又要节省空间
--> 让占空间小的成员尽量集中在一起(拿前面例题举例)
#include <stdio.h>
struct S1
{
char c1;
char c2;
int n;
};
struct S2
{
char c1;
int n;
char c2;
};
int main()
{
printf("S1 : %zd\n", sizeof(struct S1));
printf("S2 : %zd\n", sizeof(struct S2));
return 0;
}
2.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
- #pragma pack( X) //设置默认对齐数为x
- #pragma pack() //取消设置默认对齐数,还原为0
- 注意这个默认对齐数并不是可以随意改成任意数,一般是2的倍数:1 2 4 8 16
#pragma pack(1) //设置默认对齐数为1
struct S1
{
char c1;
int n;
char c2;
};
#pragma pack() //取消设置的默认对齐数
int main()
{
printf("%zd\n", sizeof(struct S1));
//6
return 0;
}
三 ,结构体传参
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;
}
- 函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
- 结构体传参的时候,要传结构体的地址
四 , 结构体实现位段
结构体如何实现 位段 的能力:
4.1 什么是位段
位段的声明和结构体是类似的,有两个不同
- 位段的成员必须是 int ,unsigned int 或者 signed int ,在C99中位段的成员的类型也可以选择其他类型
- 位段的成员名后边有一个冒号和一个数字
struct S
{
int _a;
int _b;
int _c;
int _d;
};
struct SS
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("S : %zd\n", sizeof(struct S));
printf("SS : %zd\n", sizeof(struct SS));
return 0;
}
4.2 位段的内存分配
-
位段的成员可以是 int , unsigned int , signed int 或者是char 等类型
-
位段的空间上是按照需要以4个字节( int ) 或者1个字节( char )的方式来开辟的
-
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
//该代码位段的大小是?
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
4.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,如果写成27 ,在16位机器上会出问题
- 位段中的成员在内存中从右向左分配还是从左向右分配,标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,时舍弃剩余位,还是利用,这是不确定的
与结构体相比,位段可以达到相同的效果,并且可以更好的节省空间,但是又跨平台的问题存在。
4.4 位段的使用
网络协议中,IP数据报的格式,其中很多的属性只需要几个bit 位就可以描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
4.5 位段使用的注意事项
位段的几个成员共用一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit 位是没有地址的
所以不能对位段的成员使用 &操作符,这样就不能使用 scanf 直接给位段成员输入值,只能是先输入放在一个变量中,然后赋值给位段成员。
struct SS
{
int _a : 2;
int _b : 4;
int _c : 10;
int _d : 30;
};
int main()
{
struct SS s={0 };
scanf("%d", &s._b);
//error
return 0;
}
正确写法:
struct SS
{
int _a : 2;
int _b : 4;
int _c : 10;
int _d : 30;
};
int main()
{
struct SS s={0 };
int b = 0;
scanf("%d", &b);
s._b = b;
printf("%d\n", s._b);
return 0;
}
标签:位段,struct,自定义,int,char,类型,对齐,结构
From: https://blog.csdn.net/khjjjgd/article/details/142332201