C语言之结构体详解

目录

一、结构体的概念

二、结构体的声明

一般声明

特殊声明 

typedef的使用(结构体的别名)

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

结构体的定义

结构体的嵌套 

三、结构体成员的访问与成员变量的修改

 四、结构体传参

五、结构体的自引用 

错误的自引用方式

结构体自引用概念

正确的自引用方式

使用typedef时自引用方式

六、结构体的内存对齐(重点) 

什么是内存对齐?

结构体内存对齐的规则

简单结构体求大小:

嵌套结构体求大小 :

为什么存在内存对齐 ?

七、C语言offsetof宏的使用 

offsetof的介绍

offsetof的参数

offsetof的使用 

8、修改默认对齐数 


一、结构体的概念

结构体是一些值的集合,这些值被称作成员变量,结构的每个成员可以是不同类型的变量,所以结构体常用来描述复杂对象。结构体类型不是又系统定义好的,需要我们自己去定义。C语言只是提供了关键字struct来标识所定义的结构体类型。

二、结构体的声明

一般声明

结构体的声明一般由结构体关键字+结构体名称+成员列表组成:

struct tag       //struct:结构体关键字  tag:结构体标签
{
	member - list;    //成员列表
}variable - list;  //变量列表(可以省略)

描述一个人如下:

struct Pepole
{
char name[20];//名字
int ID;//身份证号
char sex[10];//性别
int age;//年龄
};//注意这个分号不能丢

特殊声明 

 结构体声明的时候,可以不完全声明,即把结构体名称省略掉,这种结构体被称为匿名结构体:

//匿名结构体
struct 
{
    member-list;
}x;

声明一个匿名结构体:


struct 
{
char name[20];//名字
int ID;//身份证号
char sex[10];//性别
int age;//年龄
}stu;

由于匿名结构体没有名字,所以不能在程序的其他位置使用该结构体创建的结构体变量,而只能在结构体声明的同时定义结构体,匿名结构体只能使用一次。

typedef的使用(结构体的别名)

我们如果在定义一个结构体变量的时候,觉得书写过于麻烦,我们可以给他定义一个新的名称,

//typedef的使用
typedef struct student
{
	char name[20];
	int age;
	char id[20];
}stu;

int main()
{
	stu s1 = { "jark", 30, "204071" };
	printf("%s  %d  %s\n", s1.name, s1.age, s1.id);

这样我们就可以用stu来代替struct student,这样我们就方便结构体的创建,但是这样我们无法在结构体后面创建变量,取而代之的是结构体的别名。 

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

结构体的定义

struct student
{
    int age;
    char name[20];
}x1;
struct student x2;
struct student x3={18,"make"};

1、先定义一个学生结构类型,x1表示声明类型的同时定义变量x1。
2、x2表示定义结构体变量x2。
3、x3表示,初始化,定义变量的同时赋初值。

结构体的嵌套 

struct Point
{
 int x;
 int y; }p1;

struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化

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

三、结构体成员的访问与成员变量的修改

我们先看代码:
//坐标类型
struct Point
{
	int x;
	int y;
};
//学生类型
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
};
int main()
{
	//结构体初始化
	struct Point p = { 10,20 };
	struct Stu s = { "张三",20,"男",180 };
	//结构体成员访问 点操作符
	printf("x = % d y = % d\n", p.x, p.y);
	printf("%s %d %s %d\n", s.name, s.age, s.sex, s.hight);
    s.age=18;
    //Stu.name="李四";//err
    strcpy(Stu.name,"李四");//对字符串修改只能用strcpy
	printf("%s %d %s %d\n", s.name, s.age, s.sex, s.hight);
   
	//结构体成员访问 ->操作符
	struct Point* ps = &p;
	struct Stu* pt = &s;
	printf("x = % d y = % d\n", ps->x, ps->y);
	printf("%s %d %s %d\n", pt->name, pt->age, pt->sex, pt->hight);
	return 0;
}

注意:1、访问结构体成员有两种方式,结构体变量.结构体成员名,结构体指针->结构体成员名。

2、对字符串的修改只能用strcpy,切勿使用赋值操作符。

 四、结构体传参

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

一共有两种传参的方式,一个是传结构体,一个是传结构体的地址,不过要优先选择结构体地址进行传参,这是因为 :

函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。如果传递结构体地址,地址占用内存很小,时间空间都会节省,程序效率会更高。

五、结构体的自引用 

错误的自引用方式

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

上面这种结构体声明是错误的,因为struct Node中包含了一个struct Node的 next,它是无限循环的,构成了一个死循环的,我们无法计算该结构体的大小,正确的结构体自引用应该是一个结构体中包含指向该结构体的指针 

结构体自引用概念

结构体自引用是在结构体里面,创建一个指向自身类型结构体的指针。

正确的自引用方式

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

一个结构体中包含了一个指向该结构体的指针,实现了结构体的自引用,同时,由于指针的大小是固定的(4/8个字节),所以该结构体的大小也是可计算的。

使用typedef时自引用方式

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

 以上是错误的,因为next虽然是一个指针,但这里的Node并没有定义。typedef是为结构体创建一个别名NODE,可是类型名的作用域是从语句的结尾开始的,在里面是无法使用的,因为没有定义。

正确的自引用方式:

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

六、结构体的内存对齐(重点) 

什么是内存对齐?

简单来说,结构体内存对齐是指我们创建一个结构体变量时,会向内存申请所需的空间,用来存储结构体成员的内容。我们需要计算结构体的大小时需要运用到该知识点。

结构体内存对齐的规则

1. 第一个成员在与结构体变量偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
               VS 中默认的值为 8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

简单结构体求大小:

struct S1 
{
	char c1;  //变量大小为1,默认对齐数为8 -> 对齐数为1
	int i;    //变量大小为4,默认对齐数为8 -> 对齐数为4
	char c2;  //变量大小为1,默认对齐数为8 -> 对齐数为1
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

运行结果为什么呢:

先看下图进行解析: 

1、我们先求出各个成员的对齐数,char是1,int是4(和VS对齐数8比取最小值),最大对齐数是4。

2、按规则,第一个成员是char类型,对齐到相对于结构体变量起始位置为0的偏移处。

3、第二个成员是int类型占4个字节,但下一个地址偏移处是1,不是它的整数倍,继续往下找,找到4,就从4开始填4个字节,直到7处停止,最后一个是char类型c2,也占一个字节

4、结构体总大小为最大对齐数的整数倍:由于最大对齐数为4,所以总对齐数要为4的倍数,大于9的最小的4的倍数为12,所以整个结构体的大小为12个字节。

嵌套结构体求大小 :

struct S2
{
	char c1;
	int  i;
	char c2;
}s3;
struct S4
{
	char c3;
	struct S2 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct S4));
	return 0;
}

结果:

自己可以尝试画图分析一下,我就不展示了

1、求S4中各个成员的对齐数,char类型是1,因为是嵌套要求它的最大对齐数是4,d是8,所有最大对齐数是8。
2、我们从0开始,char占1字节,直接填。
3、第二个成员是嵌套结构体,要对齐到自己最大对齐数的整数倍处,从开始填12个字节,直到15停止,最后一个是double类型对齐数是8,并占8个字节,16符合条件,从它开始填到24。
4、最后一步是判断填完之后是否是所有最大对齐数的整数倍,24是8的倍数符合条件结束。

为什么存在内存对齐 ?

从上面的例子我们可以看到,结构体内存对齐会浪费一定的内存空间,但是计算机不是要尽可能的做到不浪费资源吗?那为什么还要存在内存对齐呢?关于内存对齐存在的原因,大部分的参考资料是这样说的:

1. 平台原因 ( 移植原因 )
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常
2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿 空间 来换取 时间 的做法
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
列如:
struct S2
{
 char c1;
 char c2;
 int i;
};

这个结构体只占8个空间。

七、C语言offsetof宏的使用 

offsetof的介绍

offsetof是c语言中定义的一个用于求结构体成员在结构体中偏移量的一个宏,其对应的头文件是<stddef.h>,offsetof的使用方法跟函数一样,但是它不是函数。

offsetof的参数

       #include <stddef.h>
       size_t offsetof(type, member);

  这个宏会返回一个结构体成员相对于结构体开头的字节偏移量(**经过结构对其之后**):
   - type 结构体名称
   - 结构体成员名称
   这个宏非常有用,由于结构体对其的问题,整个结构体的大小并不是所有成员大小之和,往往要比他们的和大,(当然我们也可以执行结构体按一个字节进行对其),所以利用这个宏可以很好计算出每个结构体成员相对于结构体开头偏移的字节数。 

offsetof的使用 

#include <stdio.h>
#include <stddef.h>  //offsetof对应头文件
struct S1
{
	char c1;
	int i;
	char c2;
};

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

int main()
{
	printf("%d\t", offsetof(struct S1, c1));
	printf("%d\t", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	printf("%d\t", offsetof(struct S2, c1));
	printf("%d\t", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));

	return 0;
}

我们观察后可以发现:结构体成员在结构体中的偏移量=结构体的地址-结构体的起始地址 

8、修改默认对齐数 

因为结构体字节的对齐方式在不同的编译器中不一样,我们可以使用“#pragma pack(num)”命令来修改VS中的默认对齐数。

代码如下:
 

#include <stdio.h>

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

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

int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}

结果是:

 在struct S2中,我们通过“#pragma pack(num)”命令把VS默认对齐数设置为1,就是相当于不对齐,使得其大小变为6。

### C语言中的结构体详细用法 #### 什么是结构体结构体是一种用户自定义的数据类型,能够将不同类型的数据组合在一起形成一个新的复合数据类型。这种特性使得程序员可以更方便地组织和管理复杂的数据。 #### 定义结构体 结构体可以通过`struct`关键字定义。以下是基本语法: ```c struct 结构体名称 { 数据类型 成员1; 数据类型 成员2; ... }; ``` 例如: ```c struct Point { int x; int y; }; ``` 这里定义了一个名为 `Point` 的结构体,包含两个整型成员变量 `x` 和 `y`[^1]。 #### 初始化结构体 结构体可以在声明时直接初始化,也可以在之后逐一赋值。以下是一个例子: ```c // 声明并初始化结构体 struct Book { char title[50]; char author[50]; char grade; int pages; float price; }; // 方法一:直接初始化 struct Book myBook = {"C Programming", "Nikita", 'A', 300, 500.00}[^3]; // 方法二:逐个赋值 struct Book anotherBook; strcpy(anotherBook.title, "Advanced C"); strcpy(anotherBook.author, "John Doe"); anotherBook.grade = 'B'; anotherBook.pages = 400; anotherBook.price = 600.00; ``` #### 访问结构体成员 通过`.`运算符访问结构体的成员。例如: ```c printf("Title: %s\n", myBook.title); myBook.pages += 50; // 修改页数 ``` 如果使用的是结构体指针,则需要用箭头运算符 (`->`) 来访问其成员。例如: ```c struct Student { char name[50]; int age; }; int main() { struct Student *ptr; ptr = (struct Student *)malloc(sizeof(struct Student)); strcpy(ptr->name, "张三"); ptr->age = 20; printf("Name: %s, Age: %d\n", ptr->name, ptr->age); free(ptr); return 0; } ``` 此代码展示了如何利用动态内存分配创建结构体实例,并通过指针访问其成员[^2]。 #### 使用typedef简化结构体声明 为了减少重复输入`struct`关键字,通常会结合`typedef`一起使用。例如: ```c typedef struct { int id; double score; } Record; Record r1 = {1, 95.5}; ``` 这样可以直接使用`Record`代替完整的`struct`定义[^5]。 #### 动态内存分配与释放 对于较大的结构体或者不确定大小的情况下,推荐采用动态内存分配技术。这不仅节省空间还能提升性能。下面展示了一种常见做法: ```c #include <stdlib.h> #include <string.h> struct Employee { char name[100]; double salary; }; void createEmployee(struct Employee **empPtr) { (*empPtr) = (struct Employee *)malloc(sizeof(struct Employee)); if ((*empPtr)) { strcpy((*empPtr)->name, "Alice"); (*empPtr)->salary = 75000.0; } } int main() { struct Employee *employee = NULL; createEmployee(&employee); if (employee != NULL) { printf("Employee Name: %s\nSalary: %.2f\n", employee->name, employee->salary); free(employee); // 不再需要时记得释放资源 } return 0; } ``` 该片段说明了如何安全有效地申请及清理堆上的存储区域[^4]。 #### 函数间传递结构体 既可以按值传入也能借助地址形式传送整个结构体给其他函数处理。前者适用于小型实体;后者则更适合较大规模的对象因为只复制引用而非实际内容本身从而节约开销。 --- ###
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码小陈的编程之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值