自定义类型:结构体

目录

  • 1.结构体类型的声明
    • [1.1 结构体回顾](#1.1 结构体回顾)
      • [1.1.1 结构的声明](#1.1.1 结构的声明)
      • [1.1.2 结构体变量的创建和初始化](#1.1.2 结构体变量的创建和初始化)
    • [1.2 结构的特殊申明](#1.2 结构的特殊申明)
      • [1.2.1 匿名结构体变量的使用](#1.2.1 匿名结构体变量的使用)
      • [1.2.2 匿名结构体指针的使用](#1.2.2 匿名结构体指针的使用)
    • [1.3 结构体的自引用](#1.3 结构体的自引用)
  • [2. 结构体内存对齐](#2. 结构体内存对齐)
    • [2.1 对齐规则](#2.1 对齐规则)
      • [2.1.1 练习1](#2.1.1 练习1)
      • [2.2.2 练习2](#2.2.2 练习2)
      • [2.2.3 练习3](#2.2.3 练习3)
      • [2.2.4 练习4](#2.2.4 练习4)
    • [2.2 为什么存在内存对齐?](#2.2 为什么存在内存对齐?)
    • [2.3 修改默认参数](#2.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.结构体类型的声明

前面我们在讲解C语言操作符详解时,讲到了结构体的相关知识点,我们再来复习一下。

1.1 结构体回顾

结构是一些值的集合,这些值就叫做成员变量。结构的每个成员可以是不同类型的变量。

1.1.1 结构的声明

c 复制代码
struct Tag //tag是自定义名字
{
	member-list; //成员列表:1个或者是多个
}variable-list; //变量列表 
//分号不要掉了

描述一个学生:

c 复制代码
struct Student
{
	char name[20];
	int ahe;
	double score;
};

1.1.2 结构体变量的创建和初始化

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

1.2 结构的特殊申明

在声明结构的时候,可以不完全的声明。

c 复制代码
//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}t;

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

在结构体声明的时候,匿名结构体类型 可能会触发警告

  • 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。 编译器会将上面两个结构体的声明当成两个完全不同的类型,所以是非法的。
  • 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。 匿名的结构体,如果没有对结构体进行重命名的,基本上只能使用一次。

注意:每写一次匿名的结构体,都会生成一个新的结构体类型 注意:每写一次匿名的结构体,都会生成一个新的结构体类型 注意:每写一次匿名的结构体,都会生成一个新的结构体类型


1.2.1 匿名结构体变量的使用

c 复制代码
struct
{
	int a;
	float b;
	char c;
}x;

这里,我就定义了结构体变量x,因为结构体没有名字,所以我们只能通过变量x来访问它

c 复制代码
int main()
{
	x.a = 4;
	x.b = 5.5;
	x.c = 'Z';
	return 0;
}

完整示例:

c 复制代码
#include <stdio.h>

struct
{
	int a;
	float b;
	char c;
}x;

int main()
{
	x.a = 4;
	x.b = 5.5;
	x.c = 'Z';
	printf("%d %f %c", x.a, x.b, x.c);
	return 0;
}

画图讲解:


1.2.2 匿名结构体指针的使用

如果想要使用指针结构体,并且让它指向对应的匿名结构体,就要声明在同一块空间中(写在同一次声明中),指针和变量一般一起声明。

c 复制代码
struct
{
    int a;
    char b;
    float c;
} x, *p = &x;

这样我们就可以通过结构体指针变量p来访问x:

c 复制代码
#include <stdio.h>

struct
{
	int a;
	float b;
	char c;
}x , *p = &x;

int main()
{
	x.a = 4;
	x.b = 5.5;
	x.c = 'Z';
	printf("%d %f %c", p->a, p->b, p->c);
	return 0;
}

画图讲解:

所以在开始的时候的代码是有问题的,我们的正确的写法一般有两种办法:

  1. 给结构体起名字 : 1. 给结构体起名字: 1.给结构体起名字:
c 复制代码
#include <stdio.h>

struct Student
{
    int a;
    char b;
    float c;
} x;

struct Student *p = &x;

int main()
{
    return 0;
}
  1. 使用 t y p e d e f 2.使用typedef 2.使用typedef
c 复制代码
#include <stdio.h>

typedef struct
{
    int a;
    char b;
    float c;
} Student;

Student x;
Student *p = &x;

int main()
{
    return 0;
}

Student 是通过 typedef匿名结构体 起的类型别名 , Student 代表这个结构体类型


1.3 结构体的自引用

如果在一个结构体中包含一个类型为结构本身的成员可以吗?

c 复制代码
struct Node
{
	struct Node s1;
	int a;
}s2;

上述代码会造成无穷递归 的问题,我们来画图演示一下:

正确的自引用方式:

c 复制代码
struct Node
{
	struct Node* next;
	int a;
}s2;

在结构体⾃引⽤使⽤的过程中,夹杂了typedef 对匿名结构体类型重命名,也容易引⼊问题,看看下⾯的代码,可⾏吗?

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

答案是不行的,Node 是通过 typedef 起的类型别名,但是,Node 这个别名只有在整个结构体定义结束之后才会生效,也就是说,当编译器读到结构体内部的Node* next时,Node还没有定义完成,编译器还不知道Node是什么类型,所以会报错。

正确的写法是给结构体加上结构体标签名:

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

这样的话,编译器就知道Node是结构体类型了。


2. 结构体内存对齐

在前面,我们掌握了结构体的使用,现在,我们要深入探讨一个问题:计算结构体的大小 ,这就要涉及到结构体内存对齐的问题。

2.1 对齐规则

结构体的对齐规则有如下几点:

  1. 结构体的第1个成员 对⻬到和结构体变量起始位置偏移量为0的地址处。

  2. 从第 2 个成员开始,每个成员都要放在对应 对齐数 的整数倍地址处。
    对齐数 = 默认对齐数成员自身大小较小值

环境 默认对齐数
VS 8
Linux gcc 通常按成员自身大小对齐
  1. 结构体总大小 必须是最大对齐数整数倍 ,其中,最大对齐数是所有结构体成员中对齐数最大的那一个。
  2. 如果出现了结构体嵌套的情况:
    • 嵌套结构体 作为成员时,其起始位置 要对齐到自身最大对齐数整数倍地址处
    • 整个结构体大小 要是所有最大对齐数 - 含嵌套结构体成员的对齐数的整数倍。

2.1.1 练习1

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

画图讲解:


2.2.2 练习2

c 复制代码
#include <stdio.h>

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

int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

画图演示:


2.2.3 练习3

c 复制代码
#include <stdio.h>

struct S3
{
	double d;
	char c;
	int i;
};

int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

画图演示:


2.2.4 练习4

c 复制代码
//结构体嵌套问题
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 为什么存在内存对齐?

内存对齐本质上是:用一定的空间浪费,换取更高的访问效率和更好的平台兼容性。

一、平台原因:提高可移植性 一、平台原因:提高可移植性 一、平台原因:提高可移植性

不同硬件平台对内存访问的要求不同。有些平台可以访问任意地址上的任意数据; 有些平台只能在特定地址读取特定类型的数据。

如果数据没有按照平台要求对齐,可能会导致:

  • 程序运行异常
  • 数据读取错误
  • 跨平台兼容性变差

二、性能原因:提高访问效率 二、性能原因:提高访问效率 二、性能原因:提高访问效率

数据结构通常会尽量按照 自然边界 对齐。

这样 CPU 访问内存时,可以减少访问次数,提高读取效率。

情况 内存访问次数 说明
已对齐 1 次 数据刚好位于合适的内存边界
未对齐 2 次或更多 数据可能跨越多个内存块,需要多次读取

假设处理器一次从内存中读取 8 个字节

如果一个 double 类型数据的地址是 8 的倍数地址:0、8、16、24 ...,那么数据就可以一次性读取完成,如果地址不是8的倍数,例如6,该 double 数据可能会跨越两个 8 8 8 字节内存块,CPU 就需要进行两次内存访问,效率更低。

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

在设计结构体的时候,我们既要满足内存对齐,还要节省空间,我们就可以让占用空间小一点的成员在一起。

c 复制代码
//例如:
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

我们来画图比较一下s1s2两者的不同:


2.3 修改默认参数

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

c 复制代码
#include <stdio.h>
#pragma pack(1) //设置默认对齐数为1

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

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

int main()
{
	printf("%zu\n", sizeof(struct S1));
	return 0;
}

画图演示:


3.结构体传参

c 复制代码
#include <stdio.h>

struct S
{
	int data[1000];
	int num;
};

void print1(struct S p1)
{
	for (int i = 0; i < 4;i++)
		printf("%d ", p1.data[i]);
	printf("\n");
	printf("%d\n", p1.num);
}

void print2(const struct S* p2)
{
	for (int i = 0; i < 4;i++)
		printf("%d ", p2->data[i]);
	printf("\n");
	printf("%d\n", p2->num);
}

int main()
{
	struct S s = { {1,2,3,4},4 };
	print1(s); //传值调用
	print2(&s);//传址调用
	return 0;
}

画图演示:

一般在结构体传参的时候,我们都会选print2函数,原因:

函数传参的时候,参数时需要压栈的,会有时间和空间上的系统开销。

如果传递给一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以导致系统性能下降。

结论:结构体传参的时候,要传结构体的地址。 结论:结构体传参的时候,要传结构体的地址。 结论:结构体传参的时候,要传结构体的地址。


4. 结构体实现位段

4.1 位段是什么?

位段的声明和结构体很相似,有两个不同:

  1. 位段的成员必须是int,unsigned intsigend int,在C99中位段成员的类型也可以选择其他整型家族类型,比如:char.

  2. 位段的成员名后面有一个冒号和数字

比如:

c 复制代码
struct S
{
	int _a : 1;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

S就是一个位段类型。


4.2 位段的内存分配

  1. 位段成员通常可以是:intunsigned intsigned intchar
  2. : 后面的数字表示该成员所占用的是二进制位数(比特位)
  3. 位段不是以完整变量大小存储,而是按来分配空间。常见开辟单位:
位段类型 可能的开辟单位
int 4 字节
char 1 字节
  1. 位段的内存分配特点:
    • 多个位段成员会尽量放在同一个存储单元中。
    • 如果当前存储单元放不下新的位段成员,就会重新开辟新的空间
    • 位段可以节省空间。
  2. 位段是不跨平台的,注重可移植性的程序应尽量避免使用位段

举一个例子:

c 复制代码
//⼀个例⼦
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;  
	return 0;
}

画图演示:


4.3 位段的跨平台问题

序号 知识点 说明 影响
1 位段符号性不确定 int 位段成员是否为有符号数,由编译器决定 跨平台结果可能不同
2 位段最大位数不确定 位段宽度不能超过其底层类型大小;如 16 位机器最大 16,32 位机器最大 32 超出位数可能报错或异常
3 位段内存分配方向不确定 位段成员可能从左向右分配,也可能从右向左分配 内存布局依赖编译器
4 剩余位处理不确定 当前位段剩余空间不足以存放下一个成员时,是否浪费或继续利用不确定 结构体大小可能不同
5 跨平台问题 位段的符号、位宽、分配方向、空间利用方式都存在实现差异 不适合强依赖二进制布局的场景
6 使用优势 位段可达到类似结构体的表达效果,并节省空间 适合状态标志、位级数据存储
7 总结 位段能节省内存,但可移植性较差 使用时需考虑平台和编译器差异

4.4 位段的应用

位段常用于网络协议 中的数据包格式,下图是⽹络协议中,IP数据报的格式。当某些属性只需几个 bit 表示时,使用位段可以节省空间,减小数据包大小,提高网络传输效率。


4.5 位段使用的注意事项

  1. 位段按 bit 存储,多个成员可能共用一个字节
  2. 字节有地址,但字节内部的 bit 没有独立地址
  3. 不能 对位段成员使用 & 取地址,因此不能直接用 scanf 输入,应先输入到普通变量,再赋值给位段成员。
c 复制代码
#include <stdio.h>

struct A
{
	int _a : 3;
	int _b : 6;
	int _c : 10;
	int _d : 30;
};

int main()
{
	struct A s = { 0 };
	scanf("%d %d %d %d", &s._a, &s._b, &s._c, &s._d); //err
	//不能直接取地址

	//true
	int b;
	scanf("%d", &b);
	s._b = b;
	printf("%d\n", s._b);

	return 0;
}


相关推荐
Vect__1 小时前
C++转go的之路:变量声明、iota、函数、切片、init、defer
开发语言·后端·golang
晚烛1 小时前
CANN 自定义算子开发:Ascend C 编程接口与算子实现完整指南
c语言·开发语言·人工智能·python
问心无愧05131 小时前
ctf show web入门 254
java·开发语言·笔记
郝学胜-神的一滴1 小时前
Qt 高级开发 013: 元对象编译器(MOC)
开发语言·c++·qt·程序人生·用户界面
吃好睡好便好9 小时前
用while循环语句求和
开发语言·学习·算法·matlab·信息可视化
TechWayfarer9 小时前
查询IP所在地的3种方案:从API到离线库,风控场景怎么选?
开发语言·网络·python·网络协议·tcp/ip
摇滚侠9 小时前
Java 零基础全套教程,集合框架,笔记 153-163
java·开发语言·笔记
王璐WL10 小时前
【C语言入门级教学】函数的概念2
c语言·数据结构·算法
程序员榴莲10 小时前
Python 单例模式
开发语言·python·单例模式