C语言结构体

前言

还记得我在总结操作符时,涉及到了结构体,在C语言中类型分为两大类一个是内置类型,一个是自定义类型,常见的内置类型我们不说,我们今天来好好看一看为自定义类型之一的结构体吧

个人主页:小张同学zkf

若有问题 评论区见

感兴趣就关注一下吧

目录

1.什么是结构体

2.结构体类型的声明

3.结构的自引用

[4. 结构体内存对齐](#4. 结构体内存对齐)

5.修改默认对齐数

6.结构体传参

7.结构体实现位段

7.1什么是位段

7.2位段的内存分配

7.3位段的跨平台问题



1.什么是结构体

结构体由一系列成员(member)组成,每个成员可以是不同的数据类型。这些成员通常通过结构体的名称和点运算符来访问,结构体可以被声明为变量、指针或数组,用于存储和操作包含多种数据类型的复杂数据。在编程中,结构体常用于封装相关属性,以便于管理和使用。


2.结构体类型的声明

结构体声明的格式

struct tag
{
member- list ;
}variable- list ;

来我们来举一个例子,假如我们用结构体来描述一个学生,用代码就可以表示

struct Stu
{
char name[ 20 ]; // 名字
int age; // 年龄
char sex[ 5 ]; // 性别
char id[ 20 ]; // 学号
}; // 分号不能丢

接着我们将它初始化

include <stdio.h>

struct Stu
{
char name[ 20 ]; // 名字
int age; // 年龄
char sex[ 5 ]; // 性别
char id[ 20 ]; // 学号
};
int main ()
{
// 按照结构体成员的顺序初始化
struct Stu s = { " 张三 " , 20 , " 男 " , "20230818001" };
printf ( "name: %s\n" , s.name);
printf ( "age : %d\n" , s.age);
printf ( "sex : %s\n" , s.sex);
printf ( "id : %s\n" , s.id);
// 按照指定的顺序初始化
struct Stu s2 = { .age = 18 , .name = "lisi" , .id = "20230818002" , .sex =
" ⼥ " };
printf ( "name: %s\n" , s2.name);
printf ( "age : %d\n" , s2.age);
printf ( "sex : %s\n" , s2.sex);
printf ( "id : %s\n" , s2.id);
return 0 ;
}

以上就是正常的结构体的创建

但有一些特殊情况,让我们来看一下

// 匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[ 20 ], *p;

这种就是匿名结构体类型,你会发现这个结构体类型没有名字,那么可以p = &x吗

编译器会把这两个没有名字的结构体当做两个不同的类型,所以这是非法的
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。


3.结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

比如,定义一个链表(之后的数据结构的博客会总结,这里先知道一下)

struct Node
{
int data;
struct Node next ;
};
上述代码正确吗?如果正确,那 sizeof(struct Node) 是多少?
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
但我们把它换成指针就可以了,指针指向下一个空间的起始位置
struct Node
{
int data;
struct Node * next ;
};

在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看
下面的代码,可行吗?

typedef struct
{
int data;
Node* next;
}Node;
答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决方案如下:定义结构体不要使用匿名结构体了
typedef struct Node
{
int data;
struct Node * next ;
}Node;


4. 结构体内存对齐

我们了解了结构体声明,但是我们如何计算一个结构体的大小那

其实我们需要了解一个规则------对齐规则

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字( 对齐数 )的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
  • VS 中默认的值为 8
  • Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
  1. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

光说结论肯定不好理解,我们来看几道题

struct S1
{
char c1;
int i;
char c2;
};
printf ( "%d\n" , sizeof ( struct S1));

来看一下,这道题char c1变量大小是1吧,它是第一个成员,对齐到偏移量为0的地址处,接下来的成员int i 大小为4,与vs默认的8比较是不是比8小,那这个成员的最大对齐数就是4,对齐到最大对齐数的整数倍处,那就是从偏移量4开始对齐,对齐完4个字节,接下来再来看最后一个成员名,是char类型大小为1与默认的8比较得最大对齐数为1,那就是1的倍数,那就紧接着int对齐完之后再来一个空间就行了,成员都对齐完了,那我们就开始算结构体的总大小,那就是所有最大对齐数中最大的那个的整数倍,第一成员是1,第二个是4,第三个是1,里面最大的是4,所以是4的整数倍,此时空间大小为为9不是4的整数倍,那就是空几个空间到12就是4的整数倍,所以结构体大小为12


再来一道

// 练习 2
struct S2
{
char c1;
char c2;
int i;
};
printf ( "%d\n" , sizeof ( struct S2));

这个和上面就是成员位置变了,c1依旧是在偏移量为0的首地址处,c2最大对齐数是1,那就在偏移量为1的地址处,i最大对齐数是4,那就在偏移量为4的倍数的地址处,所以在偏移量为4的地址处,往后四个字节,到偏移量7结束,此时空间占了8个,正好是1,1,4中最大为4的倍数,那此时结构体总大小就会变化 ,变成了8


我们继续看一个

struct S3
{
double d;
char c;
int i;
};
printf ( "%d\n" , sizeof ( struct S3));

这个第一个成员类型为double,大小为8个字节,与默认的8一样,所以最大对齐数为8,为首成员,所以在偏移量为0的地址处向后申请8个字节,到偏移量为7的空间,再看第二个,char类型大小为1个字节,与默认的8比较,1小所以最大对齐数为1,1的整数倍那直接在偏移量为8中存放就行了, 最后一个int,4比8小,所以最大对齐数为4,在偏移量为四的整数倍处存储,相当于跳到偏移量为12中存储,向后访问四个字节空间。

此时总大小为16,16正好为最大对齐数中最大的值为8的整数倍,所以此时大小就是结构体的总大小为16


如果结构体里套一个结构体,那结果会是什么那

我们来看一下

struct S4
{
char c1;
struct S3 s3 ;
double d;
};
printf ( "%d\n" , sizeof ( struct S4));

还是按上面的思路分析,首成员大小为1,与默认对齐数8比交,1是最大对齐数,因为是首成员,所以从偏移量为0开始,占一个字节,接着这个结构体s3是上面那个练习题的s3,此时我们要知道结构体的对齐数为里面成员的最大对齐数最大的那个数,上面s3我们在求时最大对齐数中最大值为8,所以被用来当作这个结构体s3的对齐数,与默认的8比较相等,所以要在偏移量为8的倍数处存储,这个就在偏移量为8处向后16个字节,到偏移量为23处,第三个成员double为8个字节与默认8比较相等,就在8的倍数处存储,此时就在偏移量24处存储,此时三个成员最大对齐数中的最大值为8,此刻总大小为32正好是8的倍数,所以这个结构提的总大小为32.

那么为什么结构体要存在内存对齐

分为两个原因

  1. 平台原因 (移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
    **总体来说:**结构体的内存对齐是拿空间来换取时间的做法。

5.修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

include <stdio.h>

pragma pack(1) // 设置默认对⻬数为 1

struct S
{
char c1;
int i;
char c2;
};

pragma pack() // 取消设置的对⻬数,还原为默认

int main ()
{
// 输出的结果是什么?
printf ( "%d\n" , sizeof ( struct S));
return 0 ;
}

此时利用 #pragma把默认对齐数8改为默认对齐数1,那第一个成员的最大对齐数为1,第二个成员的最大对齐数为1,第三个成员的最大对齐数为1,所以只需要紧挨着存放就行了,结构体大小为6


6.结构体传参

我们来看一个代码仔细看一下结构体如何传参

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 ;
}

上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。

附加
在字符串那个博客说过
结构体指针变量调用成员的话用'->'操作符,结构体变量调用成员的话用'.'操作符


7.结构体实现位段

7.1什么是位段

记住这个是位段,不是我们游戏中的段位!!!哈哈哈开玩笑

回到正题,位段其实是一种特殊的结构体
位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是 int 、 unsigned int 或 signed int ,在C99中位段成员的类型也可以
    选择其他类型。
  2. 位段的成员名后边有一个冒号和一个数字。后面那个数字就是给你这个数所占的比特位

举一个例子

struct A
{
int _a: 2 ;
int _b: 5 ;
int _c: 10 ;
int _d: 30 ;
};
A就是一个位段类型。

7.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 ;
};
struct S s = { 0 };
s.a = 10 ;
s.b = 12 ;
s.c = 3 ;
s.d = 4 ;
// 空间是如何开辟的?

有图可知,在vs2013中a和b在一个内存空间中当剩余的比特位不够下一个成员进入时,再给它开辟一个空间,让成员在新的空间存放,依次按照这样的方式存放

7.3位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  1. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用剩余的位段,这是不确定的。
    总结:
    跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

位段使用的注意事项:

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。


结束语

结构体知识就总结到这里了,下篇博客我们来看看同为自定义类型家族的联合体和枚举

OK,感谢观看!!!

相关推荐
我是陈泽10 分钟前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
优雅的小武先生21 分钟前
QT中的按钮控件和comboBox控件和spinBox控件无法点击的bug
开发语言·qt·bug
虽千万人 吾往矣27 分钟前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
创作小达人30 分钟前
家政服务|基于springBoot的家政服务平台设计与实现(附项目源码+论文+数据库)
开发语言·python
郭二哈32 分钟前
C++——list
开发语言·c++·list
杨荧34 分钟前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
ZPC821040 分钟前
Python使用matplotlib绘制图形大全(曲线图、条形图、饼图等)
开发语言·python·matplotlib
镜花照无眠42 分钟前
Python爬虫使用实例-mdrama
开发语言·爬虫·python
aaasssdddd961 小时前
python和c
c语言·开发语言·python
星星法术嗲人1 小时前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言