深入了解C语言中的结构体类型与内存对齐

引言:

在C语言中,结构体是一种自定义的数据类型它允许我们将不同类型的数据组合在一起,形成一个新的数据类型。结构体的使用为我们解决了一些复杂数据的表示和处理问题,不仅限于单单的整型或者字符。本文将深入探讨结构体类型、结构体变量的创建和初始化,并详细介绍结构体中的内存对齐规则。

1.结构体类型

结构体类型是由不同类型的数据成员组成的集合,其中每个数据成员可以是任意类型的数据,包括基本数据类型、数组、指针、其他结构体等。结构体类型的定义使用关键字"struct"。

1.1 结构体的声明

形式如下:

c 复制代码
struct tag
{
	member-list; // 成员列表
};

举例:描述一个学生

c 复制代码
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}; // 注意分号不要遗忘

1.2 结构体的创建和初始化

  • 结构体可以在声明的同时创建
c 复制代码
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}stu; // 这样就在声明的同时创建了一个struct student类型的变量stu
  • 结构体也可以声明完后再创建
c 复制代码
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
};

int main()
{
	struct student stu; // 这样也是创建了一个struct student类型的变量stu
	return 0;
}
  • 结构体的初始化
c 复制代码
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.3 访问结构体成员

  • 结构体指针变量成员的访问:
c 复制代码
struct student
{
	char name[20]; // 名字 
	int age; // 年龄 
	char sex[5]; // 性别
	char id[20]; // 学号
};

int main()
{
	struct student* stu = (struct student*)malloc(sizeof(struct student));
	stu->age = 18;
	printf("age : %d\n", stu->age); // 只有指针能用箭头访问成员
	(*stu).age = 20;
	printf("age : %d\n", (*stu).age); // 必须要加(),因为.的优先级高于*
	return 0;
}
  • 输出结果:

    只有指针能用箭头->访问成员。

  • 非指针的结构体成员的访问
c 复制代码
struct student
{
	char name[20]; // 名字 
	int age; // 年龄 
	char sex[5]; // 性别
	char id[20]; // 学号
};

int main()
{
	struct student stu;
	stu.age = 18;
	printf("age : %d\n", stu.age);
	return 0;
}
  • 输出结果:

    非指针的结构体只能通过.访问成员。

1.4 小技巧

当我们需要多次使用结构体时,我们可以通过重命名结构体变量来减少代码量

  • 可以在声明结构体类型的时候重命名:
c 复制代码
typedef struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
}student;

int main()
{
	student stu; // 这样创建的变量是和struct student一个类型的,但是可以一定程度减少代码量
	return 0;
}
  • 也可以声明后重命名
c 复制代码
struct student
{
	char name[20]; // 名字 
 	int age; // 年龄 
 	char sex[5]; // 性别
 	char id[20]; // 学号
};

typedef struct student student; // 将struct student重命名为student

int main()
{
	student stu; // 这样创建的变量是和struct student一个类型的,但是可以一定程度减少代码量
	return 0;
}

1.5 结构的特殊声明

  • 在声明结构的时候,可以不完全的声明。
c 复制代码
//匿名结构体类型 
struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], *p; 

上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。

那么此时有一个问题,如下的代码合法吗?

c 复制代码
p = &x;
  • 警告:
    • 编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
    • 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。

1.6 结构体的自引用

在链表和二叉树中常常用到这样一种结构:

c 复制代码
typedef struct Node
{
	struct Node* next; // 结构体指针可以指向下一个节点
	int data;
}Node;

当我们对结构体指针next赋值时,我们就可以通过next找到它指向的下一个结构体变量节点

  • 注意:
    • 以下形式是非法的:
c 复制代码
typedef struct Node
{
	Node* next; // 此时struct Node 还没有重命名为Node ,所以不能使用Node自引用
	int data;
}Node;

2.结构体内存对齐

我们已经掌握了结构体的基本使⽤了。

现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩

这也是⼀个特别热⻔的考点: 结构体内存对⻬

2.1 对齐规则

⾸先得掌握结构体的对⻬规则:

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

2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

  • 对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
  • VS中对齐数默认的值为 8
  • Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

2.1.1 内存对齐例题

下面例题均在VS环境下

题目1:

c 复制代码
struct S1 // 计算该结构体大小
{
	char c1;
	int i;
	char c2;
};

解析:



题目2:

c 复制代码
struct S2 // 计算该结构体大小
{
	char c1;
	char c2;
	int i;
};

解析:



题目3:

c 复制代码
struct S3 // 计算该结构体大小
{
	double d;
	char c;
	int i;
};

解析:



题目4:

c 复制代码
struct S4 // 计算该结构体大小
{
	char c1;
	struct S3 s3; // 题目3中的结构体
	double d;
};

解析:



2.2 为什么存在内存对⻬?

大部分的参考资料都是这样说的:

  1. 平台原因(移植原因):

    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因:

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

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

  • 所以我们在设计结构体的时候,尽量让占⽤空间⼩的成员尽量集中在⼀起,这样既可以满⾜对⻬,⼜可以节省空间。

例如:

c 复制代码
struct S1
{
	char c1;
	int i;
	char c2;
}; // 12 byte

struct S2
{
	char c1;
	char c2;
	int i;
}; // 8 byte

S1 和 S2 类型的成员⼀模⼀样,但是 S2 占的空间比 S1 要小。


2.3 修改默认对⻬数

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。

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

c 复制代码
#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;
}
  • 输出结果:

    如果不修改默认对齐数,则结果为 12 。

3.结构体传参

c 复制代码
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 函数。

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

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

结语:

通过本文的介绍,我们了解到了C语言中结构体类型的定义和使用方法,以及结构体变量的创建和初始化。同时,我们还详细讲解了结构体中存在的内存对齐规则,帮助我们更好地理解结构体在内存中的存储方式。结构体在C语言中的应用非常广泛,掌握了结构体的基本用法及内存对齐规则,有助于我们写出更加高效、灵活的程序代码。

相关推荐
左灯右行的爱情2 分钟前
缓存并发更新的挑战
jvm·数据库·redis·后端·缓存
浩宇软件开发6 分钟前
Android开发,实现一个简约又好看的登录页
android·java·android studio·android开发
brzhang6 分钟前
告别『上线裸奔』!一文带你配齐生产级 Web 应用的 10 大核心组件
前端·后端·架构
shepherd1117 分钟前
Kafka生产环境实战经验深度总结,让你少走弯路
后端·面试·kafka
南客先生13 分钟前
多级缓存架构设计与实践经验
java·面试·多级缓存·缓存架构
anqi2715 分钟前
如何在 IntelliJ IDEA 中编写 Speak 程序
java·大数据·开发语言·spark·intellij-idea
XuX0320 分钟前
MATLAB小试牛刀系列(1)
开发语言·matlab
袋鱼不重20 分钟前
Cursor 最简易上手体验:谷歌浏览器插件开发3s搞定!
前端·后端·cursor
m0_7401546721 分钟前
maven相关概念深入介绍
java·maven
嘻嘻哈哈开森22 分钟前
Agent 系统技术分享
后端