c语言结构体超详解!

背景

 针对后面学习数据结构时对结构体概念的模糊,重新对模糊概念进行系统的梳理。

1 结构体声明(创建)

1.1 结构体基础知识

结构体是一些值得集合,这些值称为成员变量,结构体中每个成员变量可以是不同的类型,这也是结构体与数组的最大区别。

1.2 结构体声明

struct tag        // 自定义类型名  相当于int double等等(tag可以省略)
{
   member-list;   //成员列表  可以是不同类型的变量
}variable-list;   //定义(创建)结构体对象(变量)

例如描述一个学生

struct stu
{
	char name[20];     //姓名
	int age;           //年龄
	char sex[3];       //性别
	char id[20];       //学号
};                     //分号一定不能丢!!!!!       

2 结构体对象(变量)的定义与初始化

2.1 不使用typedef创建结构体对象

  1. 如以下代码声明struct point,struct stu类型结构体,定义(创建)结构体对象或者在变量列表定义(创建)struct stu类型结构体对象时对其初始化,并且定义(创建)了struct Stu * p类型的结构体指针以及struct Stu S[20]的结构体数组。
struct point                                       //结构体类型
{
	int x;
	int y;
};

struct Stu
{
	int age;
	char name[10];
	struct point x_y;                               //嵌套的结构体
	struct Stu* next;

}stu = { 10,"张三",{1, 2}, NULL }, * p, S[20];       //声明类型的同时定义了结构体变量stu,结构体指针变量p,结构体数组S
												     //并且对嵌套的结构体stu进行了初始化
int main()
{
	struct Stu s1;                                  //定义结构体变量s1
	struct Stu s2 = { 20,"李四" ,{4, 5}, NULL };    //定义结构体变量的同时,对其初始化
	s1.age = 10;                                    //用.的方式对成员变量逐一赋值
	//s1.name = "张三";                             //不可这样,怎么可以将字符串赋给指针呢?这里可以使用strcpy()进行赋值
	p = &stu; 
	printf("%d\n", s1.age);
	printf("%d\n", stu.age);
	printf("%s\n", stu.name);
	printf("%d\n", p->age);                        //通过指向stu的结构体指针p
	printf("%d %d\n", p->x_y.x, p->x_y.y);        
}
  1. 如以下代码匿名结构体类型(省略了tag),在我们声明一个结构体的时候,可以不完全的声明。这种方式创建结构体对象的时候只能在变量列表创建
struct 
{
	int age;
	char name[10];
}stu1 = {10"张三"},stu2;                      //声明结构体同时,定义了结构体变量stu1,stu2

int main()
{
	stu2.age = 30;
	printf("%d\n", stu1.age);
	printf("%d\n", stu2.age);
}

2.2 使用typedef创建结构体对象

如以下代码typedef在结构体声明中的作用是:
(1)为匿名的结构体类型和结构体指针类型起别名stu1,p1。
(2)为 struct tag与 struct tag*分别起别名为stu2,p2。即stu2与p2实际上就是结构体类型与指针类型。
这样后面使用该结构体类型与结构体指针类型的时候,更加的简洁。
其中 struct tag == stu2
   struct tag * == p2

typedef struct
{
	int age;
	char name[10];
	int num[10];
}stu1, *p1;

typedef struct tag                   //其中tag可以省略,结果一样
{
	int age;
	char name[10];
	int num[10];
}stu2, *p2;

int main()
{
	stu1 S1 = { 10,"李四",{3,4,5,6} };
	p1 h1 = &S1;
	stu2 S2 = { 30,"张三",{4,5,6,7} };  // struct  tag == stu
	p2 h2 = &S2;                         //struct tag*  == p; 一定要注意呀
	printf("%d\n", S1.num[1]);
	printf("%d\n", S2.num[1]);
	printf("%s\n", S1.name);           //name为数组名,表示数组的起始地址
	printf("%s\n", S2.name); 
	printf("%d\n", h1->num[1]);
	printf("%d\n", h2->num[1]);
}

 当我们省略tag时,打印结果一样,但是这样写的话我们后面不能使用struct tag当结构体类型构建对象(因为这个结构体时匿名的,没有名字(tag)),只可用该结构体别名stu构建对象。stu 和 *p在这里的作用就是为这个匿名的结构体类型与结构体指针类型起了一个名字。

2.3 结构体中关于对结构体成员字符数组赋值问题的理解

如以下案例在我们对数组进行初始化的时候我们是对char name[20]初始化,没有直接对name进行初始化,因为如第二个案例中s.name = "张三"这种初始化方式就是错误的方式。因为name为数组名,数组名为数组首元素的地址,我们应该是想将“张三”赋值给name所指向的这块空间中,不能直接赋值,这里采用了strcp()函数对其进行赋值。

***//案例一***
struct stu
{
	char name[20];
	int age;
};

int main()
{
	struct stu s = { "张三",20 }; //直接对其初始化 可以将“张三传给char name[20]”
	printf("%s", s.name);

	return 0;
}
*//案例二*
struct stu
{
	char name[20];
	int age;
};

int main()
{
	struct stu s = { 0 };
	t->age = 20; 
	//t->name[0] = 'z';
	//t->name= "张三"; //这么写是错误的 name是数组名 数组名是数组首元素的地址,怎么可以把“张三放到地址里面去”
					   //应该是把数组放到这块地址所在的空间里面去
					   // t.name[10] = "张三"这样也不行 name[10]是指的name数组中的第十个元素
					   
	//将字符串放入字符数组可以用字符串拷贝函数
	strcpy(t->name, "张三");
	printf("%d %s", t.age, t.name);   //t.name表示其首地址,故可以直接用%s打印
	return 0;
}

3 结构体自引用与互引用

3.1 结构体自引用不使用typedef

错误方式
如下图所示,在结构体中包含一个类型为该结构体本身的成员可否?
如果可以这样定义,那么这个结构体大小为多少呢?

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

 由于struct Node结构体中包含int data和struct Node next,其结构体成员struct Node next中又会包含int data和struct Node next,这样我们可以一直无限循环下去。所以这个结构体的大小是未知的,在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的

正确方式(使用指针)

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

由于指针的大小是确定的(32位机器上指针大小为4,64位机器上指针大小为8),所以不会出现以上无限嵌套的情况

3.2 结构体自引用使用typedef

错误方式

//第一种错误
typedef struct Node
{
	int data;
	Node* next;  //struct Node* next
}Node;

//第二种错误
typedef struct 
{
	int data;
	Node* next;  //怎么也不行,
}Node;           //

 以上两种结构体自引用的时使用的虽然都是指针,但是我们是先声明(创建)结构体struct Node,然后进行typedef。故创建结构体成员next(类型为Node),结构体类型Node是未定义的。,所以我们在结构体自引用的时候使用typedef,不可以在声明结构体时使用别名来构建结构体成员!!!。

正确方式

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

3.2 结构体的互引用

错误方式

typedef struct tag_a{  
    int value;  
    B *bp;              /* 类型B还没有被定义 */  
} A;  
  
typedef struct tag_b{  
    int value;  
    A *ap;  
} B;

错误原因:对于A结构体而言,B结构体还没声明就被使用。同理B结构体也一样。

正确方式
在 struct tag_a结构体中使用 struct tag_b 结构体类型时,需提前使用struct tag_b结构体的不完整声明

 struct tag_b;                 /* 使用结构体的不完整声明(incomplete declaration) */  
 struct tag_a
{  
    struct tag_b *bp;         
    int value;  
}A;  
struct tag_b{  
    struct tag_a *ap;  
    int value;  
}B;  

4 结构体内存对齐(重点)

4.1内存对齐规则

如何计算一个结构体大小,首先要知道结构体对齐规则
结构体对齐规则:

  1. 第一个结构体成员在该结构体变量偏移量为0的地址处。

  2. 其他成员变量大小(结构体第二个成员变量及以后)要对其到==某个数字(对齐数)==的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数与该结构体成员类型大小较小值
        vs中默认对齐数为8
        Linux中没有默认对齐数,对齐数就是自身成员的大小

  3. 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数,取齐数的最大值)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体大小对齐到自己的最大对齐数的整数倍处,被嵌套的结构体对齐数为自身结构体中的最大对齐数。(如结构体S2中嵌套结构体S1,则按照S1的自身的最大对齐数(最大元素类型)和S2中各元素类型取较大者对齐)

结构体大小计算具体案例
案例一

//练习1
struct s1
{          //结构体成员类型大小/默认对齐数/对齐数
 char c1;  //1/8/1    
 int i;    //4/8/4    //对齐到4的倍数
 char c2;  //1/8/1
};
printf("%d\n",sizeof(struct s1));  //12

c1为char类型,大小为1个字节,默认对齐数为8,其大小与1的倍数对齐,故c1位于0偏移处
i为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故i位于4~7偏移处
c2为char类型,大小为1个字节,默认对齐数为8,其大小与1的倍数对齐,故c2位于8偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为12
在这里插入图片描述


案例二

//练习2
struct s1
{          //结构体成员类型大小/默认对齐数/对齐数
 char c1;  //1/8/1    
 char c2;  //1/8/1    //对齐到4的倍数
 int i;    //4/8/4
};
printf("%d\n",sizeof(struct s1));  //8

在这里插入图片描述


案例三

//练习3
struct s1
{                   //结构体成员类型大小/默认对齐数/对齐数
	short c1[10];   //2/8/2
	char c2;        //1/8/1
	int i;          //4/8/4  最大对齐数为4
};
printf("%d\n",sizeof(struct s1));  //28

c1为short类型,大小为2个字节,默认对齐数为8,其大小与2的倍数对齐,故c1位于0~19偏移处
c2为char类型,大小为1个字节,对齐数为1对齐,其大小与1的倍数对齐,故c2位于20偏移处
i为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故故i位于24~27偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为28
在这里插入图片描述


案例四

struct s2
{          //结构体变量大小/默认对齐数/对齐数
	char b1;  //1/8/1    
	char b2;  //1/8/1    //对齐到4的倍数
	int i;    //4/8/4
};//最大对齐数为4 s2大小为8

struct s1
{                   //结构体类型大小/默认对齐数/对齐数
	short c1[10];   //2/8/2      20
	char c2;        //1/8/1      1
	int j;          //4/8/4      4
	struct s2 a;    //对齐数:4   8  //s2中结构体成员中最大对齐数为4,故s2对齐数为4
	                            
	
};

printf("%d", sizeof(struct s1));  //36

c1为short类型,大小为2个字节,默认对齐数为8,其大小与2的倍数对齐。故c1位于0~19偏移处
c2为char类型,大小为1个字节,对齐数为1对齐,其大小与1的倍数对齐。故c2位于20偏移处
j为int类型,大小为4个字节,默认对齐数为8,其大小与4的倍数对齐,故c1位于24~27偏移处
a为一个结构体变量,大小为8,它的对齐数就是该结构体的最大对齐数4,故c1位于28~35偏移处
而结构体的总大小为最大对齐数(4)的整数倍,由图中可知,为36。
在这里插入图片描述

4.2 为什么要内存对齐

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

***总体来说

结构体内存对齐就是拿空间换取时间的做法***

在这里插入图片描述


所有我们在创建结构体的时候,如果既要满足对齐,又要节省空间,我们应该

让占用空间小的成员尽量集中在一起

例如以下两个结构体s1与s2类型的成员一模一样,但是s1和s2所占用的空间大小去不一样,s1大小位12个字节,s2大小位8个字节。

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

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

4.3 修改默认的对齐数

使用预处理指令 #pragma pack(num) ,指定num对齐数的大小可以修改我们的默认对齐数

#include<stdio.h>
#pragma pack(8)   //设置默认对齐数为8
struct s1
{
	char c1;      //1/8   1
	int i;        //4/8   4
	char c2;      //1/8   1
};
#pragma pack()   //取消设置对齐数,还原为默认对齐数

#pragma pack(1)   //设置默认对齐数为1
struct s1
{
	char c1;     //1/1  1
	int i;       //4/1  1
	char c2;     //1/1  1
};
#pragma pack()   //取消设置对齐数,还原为默认对齐数

int main()
{
	printf("%d\n",sizeof(struct s1));  //12
	printf("%d\n",sizeof(struct s2));  //6  //大小为最大对齐数1的整数倍 6
	return 0;
}

5 结构体传参

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

对比上面两种我们应该多用传结构体变量的地址及printf2

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


在这里插入图片描述
在这里插入图片描述

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
引用\[1\]:C语言字节对齐问题详解中提到了C语言中的字节对齐问题。在结构体中,为了提高内存访问的效率,编译器会对结构体进行字节对齐。这意味着结构体的成员在内存中并不是紧凑排列的,而是按照一定的规则进行对齐。具体的对齐规则取决于编译器和编译选项。\[1\] 引用\[2\]:在C语言中,可以使用宏offsetof来获取结构体成员相对于结构体开头的字节偏移量。这个宏非常有用,可以帮助我们计算出每个结构体成员相对于结构体开头的偏移字节数。通过这个宏,我们可以更好地理解结构体的内存布局。\[2\] 引用\[3\]:在C语言中,指针和结构体的组合常常用于处理复杂的数据结构。指针可以指向结构体的成员,通过指针可以方便地对结构体进行操作。指针和结构体的组合可以实现更灵活的数据处理和内存管理。\[3\] 综上所述,C语言中的指针结构体组合可以用于处理复杂的数据结构,而字节对齐问题则是在结构体中为了提高内存访问效率而进行的优化。通过使用宏offsetof,我们可以更好地理解结构体的内存布局。 #### 引用[.reference_title] - *1* *3* [结构体指针,C语言结构体指针详解](https://blog.csdn.net/weixin_34069265/article/details/117110735)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [C语言结构体详解](https://blog.csdn.net/m0_70749276/article/details/127061692)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值