C语言的结构体详细介绍

本文详细介绍了C语言中的结构体,包括结构体的声明、成员类型,结构体变量的定义与初始化,结构体成员的访问,以及结构体传参。重点讨论了结构体内存对齐的规则及其背后的平台和性能原因。结构体的自引用和匿名结构体的使用也进行了讲解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.结构体的声明和成员类型

所谓结构,就是一些值的集合,这些值被称为成员变量。结构的每个成员都可以是不同类型的变量,可以是标量、数组、指针,甚至是其他结构体。结构体的声明框架如下:

struct tag//结构体标签名称
{
	member-list; //结构中的成员列表
}variable-list;//变量列表

举个简单的例子,假设我们要描述一个运动员的姓名,编号,年龄,性别还有成绩时,可以这样来声明一个结构体。

struct Stu 
{
	char name[20];//姓名   
	char id[20];//编号
	int age;//年龄    
	char sex[5];//性别    
	int result;//成绩
};//切记此处分号不能丢

当然在一些特殊结构体声明的时候,也可以进行不完全的声明,例如:

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

这两个结构体在声明的时候都忽略了结构体标签(tag),那么 p = &x 合法吗?答案是不合法。由于在结构体声明时,缺少了标签,只有 struct ,那么编译器就会把上面的两个声明当成完全不同的类型。而这种不完全声明的结构体被称为匿名结构体类型。

二.结构体变量的定义和初始化

我们已经知道了结构体的声明方法,那么结构体又是如何定义和初始化的呢?当有了结构体的类型之后,定义变量其实非常简单。

struct Point 
{ 
	int x;   
	int y; 
}p1;//声明类型的同时定义变量p1 
struct Point p2;//定义结构体变量p2

这就是结构体的定义,但是同数组一样,我们也需要对结构体进行一个初始化。

//初始化:定义变量的同时赋初值。 
struct Point p3 = {x, y};
struct Stu  
{    
         char name[15];//名字    
          int age;//年龄 
}; 
struct Stu s = {"zhangsan", 20};//定义变量s并初始化

在定义和初始化结构体时,也可以进行结构体的嵌套:

struct Point 
{
	int x;
	int y;
};
struct Stu
{ 
	int data;    
	struct  Point p;    
	struct stu *pp; 
}s1 = { 10, { 4, 5 }, NULL }; //结构体嵌套初始化

struct Stu s1 = { 20, { 5, 6 }, NULL };//结构体嵌套初始化

三.结构体成员的访问

结构变量的成员是通过点操作符( . )访问的。点操作符接受左右两个操作数,
在这里插入图片描述
在这张图里我们可以看到被定义的结构体 s 有成员 nameage ,当访问 s 的成员时就要用到我们的点操作符了。

struct S s; 
strcpy(s.name, "zhangsan");//使用.访问name成员 
s.age = 20;//使用.访问age成员

切记一点,当我们给字符型的成员进行初始化时,要用到 strcpy 字符拷贝函数,不可以像数字整形一样直接进行赋值。

当我们遇到结构体变量时,用点操作符进行访问。但如果我们只有指向结构体的指针时,又该怎么访问结构体成员呢?

struct Stu 
{ 
	char name[20];    
	int age; 
};
void print(struct Stu* ps)
{
	printf("name = %s,age = %d\n",(*ps).name, (*ps).age);
	//使用结构体指针访问指向对象的成员
	printf("name = %s,age = %d\n", ps->name, ps->age); 
	//使用 -> 操作符指向要访问的成员
} 
int main() 
{
	struct Stu s = {"zhangsan", 20};
	print(&s);//结构体地址传参    
	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); //传地址   
	system("pause");
	return 0; 
} 

我们能够看到函数 print1 的参数为结构体 s, 而 print2 的参数为结构体 s 的地址,那么那种传参方式更科学呢?答案是第二种。

函数传参时的参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,就会导致性能的下降,所以我们在给函数传结构体的参数时,最好是传结构体的地址,让函数用结构体指针类型来接收。

五.结构体的自引用

我们知道结构体中可以有很多种不同类型的成员,当然也可以有该结构体本身类型的成员。

struct Node 
{     
	int data;    
	struct Node* next; 
};

这段代码就是结构体的自引用,同时这也是定义一个节点。那试想如果去掉 * ,例如 struct Node node;,这个语句就是想引用自己的结构体变量,但是当引用自己时,每个结构体内都含有一个一样的结构体,这样就会导致无限个结构体的嵌套,这样也会导致结构体的内存大小无法计算。

六.结构体内存对齐

在了解的结构体的基本构造之后,我们应该思考一个问题,那就是结构体的内存大小是如何计算的?很多人可能会觉得大小就是结构体成员所占内存大小相加,但实则不然。结构体的大小需要根据结构体的对齐规则来进行计算。

结构体对齐规则:

  1. 第一个结构体成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要从对齐数的整数倍的地址处开始存储,存储的大小根据其类型决定。
    对齐数 = 编译器默认对齐数 与 该成员大小的较小值
    (VS中默认值为 8,Linux中默认值为 4
  3. 结构体总大小必须为所有结构体成员中最大对齐数的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体要对齐到自己成员的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

有了结构体的对齐规则,我们来看下面这段代码:

struct S1
{
	char c1;//1   8     1 c1对齐数为1
	int i;//  4   8     4  i对齐数为4
	char c2;//1   8     1 c2对齐数为1
};
int main()
{
	printf("%d\n", sizeof(struct S1));//打印结果为12
	return 0;
}

代码中已经标明了每个成员的对齐数(假设在vs编译器中,默认对齐数为8)。我们通过图片来分析一下,假设这些单元格就是结构体要存放的地址。
在这里插入图片描述
只要我们遵循结构体的对齐规则,按部就班地根据对齐数进行内存计算,结构体的大小还是和容易得出来的。

那么为什么会存在内存对齐呢?有两个原因:

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 为了访问未对齐的内存,处理器需要进行两次内存访问,但是对齐的内存访问仅需要一次访问。

实际上,内存对齐其实就是用存储空间来换取更短的运行时间。所以我们应该在设计结构体的时候,尽可能的让占用空间小的成员集中在一起,这样既能满足内存对齐,又能节省空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值