#C语言——学习攻略:自定义类型路线--结构体--结构体类型,结构体变量的创建和初始化,结构体内存对齐,结构体传参,结构体实现位段

🌟菜鸟主页:@晨非辰的主页

👀学习专栏:《C语言学习》

💪学习阶段:C语言方向初学者

⏳名言欣赏:"人理解迭代,神理解递归。"


目录

[1. 结构体类型](#1. 结构体类型)

[1.1 旧知识回顾](#1.1 旧知识回顾)

[1.1.1 结构体声明](#1.1.1 结构体声明)

[1.1.2 结构体的创建和初始化](#1.1.2 结构体的创建和初始化)

[1.2 结构体的特殊声明](#1.2 结构体的特殊声明)

[1.3 结构体的自引用](#1.3 结构体的自引用)

[2. 结构体内存对齐(热门考点)](#2. 结构体内存对齐(热门考点))

[2.1 对齐规则](#2.1 对齐规则)

习题1

习题2

习题3

习题4--嵌套结构体大小

[2.2 为什么存在内存对齐](#2.2 为什么存在内存对齐)

[2.3 修改默认对齐数](#2.3 修改默认对齐数)

[3. 结构体传参](#3. 结构体传参)

[4. 结构体实现位段](#4. 结构体实现位段)

[4.1 什么是位段](#4.1 什么是位段)

[4.2 位段的内存分配](#4.2 位段的内存分配)

[4.3 位段的跨平台问题](#4.3 位段的跨平台问题)

[4.4 位段的应用](#4.4 位段的应用)

[4.5 位段使用注意事项](#4.5 位段使用注意事项)


1. 结构体类型

1.1 旧知识回顾

-- 在前面操作符部分有过初步的了解: 结构体是一些值的集合,这些值称为成员变量。结构体的每个成员都可以是不同类型的变量;比如:数组、指针、其他结构题、体等等。

--博客跳转链接:结构成员访问操作符

1.1.1 结构体声明

cpp 复制代码
struct tag        struct表明是结构体
{
	member-list;    一个或多个成员变量
}variable - list    变量列表, 可有可无,在声明变量类型是可同时定义的变量,且为全局变量
  

--比如,当我们想描述一个学生时,就要包括;姓名、成绩、年龄、学号等等,这时候单一的内置类型就显得力不从心; 哎~~,结构体就上场了:

cpp 复制代码
struct Stu
{
	char name[20];  名字拼音
	int age;        年龄
	char id[20];    学号
	float score;    成绩
	//......
};    分号绝对不能丢

1.1.2 结构体的创建和初始化

cpp 复制代码
struct Stu
{
	char name[20];
	int age;
	char sex[5];
	char id[20];
};

int main()
{
	//初始化--按成员顺序
	struct Stu s1 = { "liming", 18, "man", "2023319829" };
	//进行访问-
	printf("name:%s\n", s1.name);
	printf("age:%d\n", s1.age);
	printf("sex:%s\n", s1.sex);
	printf("id:%s\n", s1.id);

	printf("\n");
	//初始化--按指定顺序
	struct Stu s2 = { .age = 20, .sex = "man", .name = "zhangsan", .id = "2023393839" };
	
	printf("name:%s\n", s1.name);
	printf("age:%d\n", s1.age);
	printf("sex:%s\n", s1.sex);
	printf("id:%s\n", s1.id);
	return 0;
}

1.2 结构体的特殊声明

--在声明时,结构体也存在着不完全声明:

--匿名结构体:

cpp 复制代码
struct
{
	int a;
	char b;
	float c;
}x;//全局变量

struct
{
	int a;
	char b;
	float c;
}* p;

--可以发现,上面的结构体省略了标签-tag;

--那如果在上面代码的基础,那下面的合理吗?

cpp 复制代码
p = &x;

警告:

--虽然上面两个结构体成员相同,但是编译器会将上面两个声明当作不同的类型(类似两个同名但不同地址的房屋),会导致类型不兼容错误;

--匿名结构体(无标签)只能通过原始定义使用(同时声明结构体、变量),无法在其他地方引用相同的类型。匿名结构体无法复用-改进:

--使用结构体标签、用 typedef 创建类型别名;

1.3 结构体的自引用

--在对结构体进行定义后,那是否可以将结构体本身当作结构体的一个成员呢?

--比如定义一个链表的结点:

cpp 复制代码
struct Node
{
	int data;
	struct Node next;
};

--这样其实是错误的!!

--因为当结构体里面在包含一个同类型的结构体,会导致结构体内存无限大,显然是错误的。

--但是可以这样操作(可以包含指向同类型的指针):

cpp 复制代码
struct Node
{
	int data;
	struct Node* next;
};

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

cpp 复制代码
typedef struct
{
   int data;
   Node* next;
}Node;

--这样也是错误的!!因为Node是重命名来的,在结构体内部提前使用重命名的结果是不可行的。所以最好不要使用匿名结构体!!


2. 结构体内存对齐(热门考点)

--了解了基础知识后,下面来谈一谈它的内存如何计算??

2.1 对齐规则

结构体对齐规则:

  • 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处;
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;

-- 对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。

**--**VS 中默认的值为 8 、 Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小;

  • 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍;
  • 如果嵌套了结构体,嵌套者对齐到自己成员最大对齐数的整数倍,总的结构体大小由第3条进行判断(包括嵌套者的成员);

--这样干巴得理解还是有点模糊,别急,下面几道例题来救一下!

习题1

cpp 复制代码
struct s1
{
	char c1;
	int i;
	char c2;
};

int main()
{
	printf("%zd\n", sizeof(struct s1));	12
	return 0;
}

图解演示------

习题2

cpp 复制代码
struct S2
{
	char c1;
	char c2;
	int i;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S2));//8
}

图解演示------

习题3

cpp 复制代码
struct S3
{
	double d;
	char c;
	int i;
};
 
 
int main()
{
	printf("%zu\n", sizeof(struct S3));//16
}

图解演示------

习题4--嵌套结构体大小

cpp 复制代码
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S4));  32
}

图解演示------

2.2 为什么存在内存对齐

  • **平台原因 (移植原因):**不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;

  • **性能原因:**数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中;

--总体来说:结构体的内存对齐是拿空间来换取时间的做法。

--那该如何做到是设计结构体是,满足对齐和节省空间呢,对此,我们可以让占用空间小的成员尽量集中在一起。(减少空间浪费)

cpp 复制代码
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
 
int main()
{
	printf("%zu\n", sizeof(struct S1));  12
	printf("%zu\n", sizeof(struct S2));  8
}

--看上面的代码,成员一样,但是排列顺序不同,明显看到S2更小一点。

2.3 修改默认对齐数

--#pragma 预处理指令,可以改变默认对齐数,但是一般设置为2的次方数。

cpp 复制代码
#pragma pack(2)//设置默认对⻬数为2
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认,下次再到别的结构体中就是默认的了
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S));//8
	return 0;
}

3. 结构体传参

cpp 复制代码
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. 结构体实现位段

4.1 什么是位段

--位段的声明和结构十分类似,但有者两个不同:

  • 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型;
  • 位段的成员名后边有⼀个冒号和一个数字;

--比如:

cpp 复制代码
struct A
{
     int _a:  2 ;
     int _b:  5 ;
     int _c:  10 ;
     int _d:  30 ;
};

补充------

--一般习惯在位段成员加上'-' ;

--冒号后面的数字表示:这个成员要占用的比特位的数量;

--A就是⼀个位段类型。 那位段A所占内存的大小是多少呢?

4.2 位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型'
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的;
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。;
cpp 复制代码
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;
	//空间是如何开辟的?
}

4.3 位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的;
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32),写成27,在16位机器会出问题;
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义;
  4. 当⼀个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的;

总结------

--跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在;

4.4 位段的应用

--图片为网络协议中,IP数据报的格式,可以看到其中大多属性只需要几个bit位就能描述,这里使用位段能够实现想要的结果,也节省了空间;这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

4.5 位段使用注意事项

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

cpp 复制代码
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;
}

旧知识回顾------

#C语言------学习攻略:数据在内存中的存储--整数在内存中的存储,大小端字节序和字节序判断,浮点数在内存中的存储

#C语言------学习攻略:探索内存函数--memcpy、memmove的使用和模拟实现,memset、memcmp函数的使用

结语:本篇文章到此结束,呈现了自定义--结构体的内容,内涵丰富,大家要多次回顾, **如果这篇文章对你的学习有帮助的话,欢迎一起讨论学习,**你这么帅、这么美给个三连吧~~~

相关推荐
ue星空25 分钟前
UE地裂制作学习
学习
Hemy0840 分钟前
QT_QUICK_BACKEND 环境变量详解(AI生成)
开发语言·qt
wdfk_prog1 小时前
[Linux]学习笔记系列 -- [arm][lib]
linux·运维·arm开发·笔记·学习
源远流长jerry1 小时前
OpenHarmony概述与使用
c语言·c++·鸿蒙系统
艾莉丝努力练剑1 小时前
深入详解C语言的循环结构:while循环、do-while循环、for循环,结合实例,讲透C语言的循环结构
c语言·开发语言·c++·学习
赵英英俊3 小时前
Python day43
开发语言·python
Include everything3 小时前
Rust学习笔记(一)|Rust初体验 猜数游戏
笔记·学习·rust
勇往直前plus3 小时前
一文学习nacos和openFeign
java·学习·微服务·openfeign
Warren983 小时前
公司项目用户密码加密方案推荐(兼顾安全、可靠与通用性)
java·开发语言·前端·javascript·vue.js·python·安全