0. 结构体的内存分配
当声明某种类型的结构变量时,结构成员被分配连续(相邻)的内存位置。
struct student{char name[20];int roll;char gender;int marks[5];} stu1;
此处,内存将分配给name[20]
、roll
、gender
和marks[5]
。st1
这意味着or的大小struct student
将是其成员大小的总和。不是吗?让我们检查。
void main()
{printf("Sum of the size of members = %I64d bytes\n", sizeof(stu1.name) + sizeof(stu1.roll) + sizeof(stu1.gender) + sizeof(stu1.marks));printf("Using sizeof() operator = %I64d bytes\n",sizeof(stu1));
}/* Output */
Sum of the size of members = 45 bytes
Using sizeof() operator = 48 bytes
使用sizeof()
运算符给出的3
字节数多于成员大小的总和。为什么?这3个字节在内存中在哪里?我们先回答第二个问题。我们可以打印成员的地址来查找这些3
字节的地址。
void main()
{printf("Address of member name = %d\n", &stu1.name);printf("Address of member roll = %d\n", &stu1.roll);printf("Address of member gender = %d\n", &stu1.gender);printf("Address of member marks = %d\n", &stu1.marks);
}/* Output */
Address of member name = 4225408
Address of member roll = 4225428
Address of member gender = 4225432
Address of member marks = 4225436
我们观察到该数组marks[5]
不是从 分配的,而是4225433
从 分配的4224536
。但为什么?
数据对齐
在研究数据对齐之前,了解处理器如何从内存读取数据非常重要。
处理器在一个周期内读取一个字。对于32 位处理器,该字为4 字节;对于64 位处理器,该字为 8 字节。周期数越少,CPU 的性能越好。
实现这一目标的一种方法是对齐数据。对齐意味着任何大小的基本数据类型的变量t
将始终(默认情况下)具有 的倍数的地址t
。这本质上是数据对齐。这种情况每次都会发生。
某些数据类型的对齐地址
数据类型 | 大小(以字节为单位) | 地址 |
---|---|---|
char | 1 | 1的倍数 |
short | 2 | 2的倍数 |
int ,float | 4 | 4的倍数 |
double , long , * (指针) | 8 | 8的倍数 |
long double | 16 | 16的倍数 |
结构填充
可能需要在结构的成员之间插入一些额外的字节以对齐数据。这些额外的字节称为填充。
在上面的示例中,3
字节充当填充。如果没有它们,marks[0] 类型int
(地址为 4 的倍数)的基地址将为4225433
(不是 4 的倍数)。
您现在大概可以明白为什么不能直接比较结构了。
结构成员对齐
为了解释这一点,我们将举另一个例子(你会明白为什么)。
struct example{int i1;double d1;char c1;} example1;void main()
{printf("size = %I64d bytes\n",sizeof(example1));
}
输出会是什么?让我们应用我们所知道的。
i1
是4个字节。其后将填充 4 个字节,因为 的地址应能被 8 整除。对于和d1
,其后将分别填充 8 和 1 个字节。因此,输出应为 4 + 4 + 8 + 1 = 17 字节。d1
c1
/* Output */
size = 24 bytes
什么?又错了!如何?通过数组struct example
,我们可以更好的理解。我们还将打印 的成员的地址example2[0]
。
void main()
{struct example example2[2];printf("Address of example2[0].i1 = %d\n", &example2[0].i1);printf("Address of example2[0].d1 = %d\n", &example2[0].d1);printf("Address of example2[0].c1 = %d\n", &example2[0].c1);}/* Output */
Address of example2[0].i1 = 4225408
Address of example2[0].d1 = 4225416
Address of example2[0].c1 = 4225424
假设 的大小example2[0]
是 17 字节。这意味着 的地址example2[1].i1
将为4225425
。这是不可能的,因为 的地址int
应该是 4 的倍数。从逻辑上讲, 的可能地址example2[1].i1
似乎是4225428
,4 的倍数。
这也是错误的。你发现了吗?now的地址example2[1].d1
将是 (28 + 4 ( i1
) + 3 ( padding)),4225436
它不是 8 的倍数。
为了避免这种不对齐,编译器为每个结构引入了对齐。这是通过在最后一个成员之后添加额外的字节来完成的,称为结构成员对齐。
在本节开头讨论的示例中,它不是必需的(因此是另一个示例)。
一个简单的记住方法是通过这个规则 -结构地址和结构长度必须是 的倍数t_max
。这里,t_max
是结构中成员所占用的最大大小。
对于struct example
,8 字节是 的最大大小d1
。因此,结构末尾有 7 个字节的填充,使其大小为 24 个字节。
这两点将帮助您找到任何结构的大小 -
-
任何数据类型都将其值存储在其大小倍数的地址中。
-
任何结构的大小都是成员所占用的最大字节数的倍数。
尽管我们可以降低 CPU 周期,但仍会浪费大量内存。将填充量减少到可能的最小值的一种方法是按成员变量大小的递减顺序声明成员变量。
如果我们遵循这一点struct example
,结构的大小就会减少到 16 个字节。填充从 7 个字节减少到 3 个字节。
struct example{double d1; int i1;char c1;} example3;void main()
{printf("size = %I64d bytes\n",sizeof(example3));
}/* Output */
size = 16 bytes
结构填料
包装与填充相反。它防止编译器填充并删除未分配的内存。对于 Windows,我们使用该#pragma pack
指令,它指定结构成员的打包对齐方式。
#pragma pack(1)struct example{double d1; int i1;char c1;} example4;void main()
{printf("size = %I64d bytes\n",sizeof(example4));
}/* Output */
size = 13 bytes
这可确保成员在 1 字节边界上对齐。换句话说,任何数据类型的地址都必须是 1 字节或其大小(以较小者为准)的倍数。
1. 指针
指针作为成员
结构也可以将指针作为成员。
struct student{char *name;int *roll;char gender;int marks[5];};void main()
{ int alexRoll = 44;struct student stu1 = { "Alex", &alexRoll, 'M', { 76, 78, 56, 98, 92 }};
}
使用.
(点运算符),我们可以再次访问成员。由于roll
现在有 的地址alexRoll
,我们将不得不取消引用stu1.roll
来获取值(而不是stu1.(*roll)
)。
printf("Name: %s\n", stu1.name);printf("Roll: %d\n", *(stu1.roll));printf("Gender: %c\n", stu1.gender);for( int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n", i, stu1.marks[i]);/* Output */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 78
Marks in 2th subject: 56
Marks in 3th subject: 98
Marks in 4th subject: 92
结构体指针
与整数指针、数组指针和函数指针一样,我们也有结构体指针或结构体指针。
struct student {char name[20];int roll;char gender;int marks[5];
};struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};struct student *ptrStu1 = &stu1;
在这里,我们声明了一个ptrStu1
类型为 的指针struct student
。stu1
我们已将的地址分配给ptrStu1
。
ptrStu1
存储 的基地址stu1
,它是结构体第一个成员的基地址。增加 1 将使地址增加sizeof(stu1)
字节。
printf("Address of structure = %d\n", ptrStu1);
printf("Adress of member `name` = %d\n", &stu1.name);
printf("Increment by 1 results in %d\n", ptrStu1 + 1);/* Output */
Address of structure = 6421968
Adress of member 'name' = 6421968
Increment by 1 results in 6422016
我们可以通过两种方式访问stu1
using的成员。ptrStu1
使用 *
(间接运算符)或使用->
(中缀或箭头运算符)。
对于*
,我们将继续使用.
(点运算符),而对于 ,->
我们将不需要点运算符。
printf("Name w.o using ptrStu1 : %s\n", stu1.name);
printf("Name using ptrStu1 and * : %s\n", (*ptrStu1).name);
printf("Name using ptrStu1 and -> : %s\n", ptrStu1->name);/* Output */
Name without using ptrStu1: Alex
Name using ptrStu1 and *: Alex
Name using ptrStu1 and ->: Alex
同样,我们也可以访问和修改其他成员。请注意,使用时括号是必需的,*
因为点运算符 ( .
) 的优先级高于*
。
结构数组
我们可以创建一个类型数组struct student
并使用指针来访问元素及其成员。
struct student stu[10];/* Pointer to the first element (structure) of the array */
struct student *ptrStu_type1 = stu;/* Pointer to an array of 10 struct student */
struct student (*ptrStu_type2)[10] = &stu;
请注意,ptrStu_type1
是 一个指向stu[0]
while的指针,而ptrStu_type2
是一个指向整个数组 10 的指针struct student
。加 1 将ptrStu_type1
指向stu[1]
。
我们可以使用ptrStu_type1
循环来遍历元素及其成员。
for( int i = 0; i < 10; i++)
printf("%s, %d\n", ( ptrStu_type1 + i)->name, ( ptrStu_type1 + i)->roll);
2. 功能
作为成员发挥作用
函数不能是结构的成员。但是,使用函数指针,我们可以使用(点运算符)调用函数.
。但是,不建议这样做。
struct example{int i;void (*ptrMessage)(int i);};void message(int);void message(int i)
{printf("Hello, I'm a member of a structure. This structure also has an integer with value %d", i);
}void main()
{struct example eg1 = {6, message};eg1.ptrMessage(eg1.i);
}
我们在内部声明了两个成员,一个int
egeri
和一个函数指针。函数指针指向一个接受eger 并返回 的函数。ptrMessage
struct example
int
void
message
就是这样一个函数。我们eg1
用6
和初始化message
。然后我们使用和 pass.
来调用该函数。ptrMessage
eg1.i
结构作为函数参数
与变量一样,我们可以将单个结构成员作为参数传递。
#include <stdio.h>struct student {char name[20];int roll;char gender;int marks[5];
};void display(char a[], int b, char c, int marks[])
{printf("Name: %s\n", a);printf("Roll: %d\n", b);printf("Gender: %c\n", c);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,marks[i]);
}
void main()
{struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};display(stu1.name, stu1.roll, stu1.gender, stu1.marks);
}/* Output */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 98
Marks in 2th subject: 68
Marks in 3th subject: 87
Marks in 4th subject: 93
请注意,该结构是在最顶部的struct student
外部声明的。main()
这是为了确保它在全球范围内可用并且display()
可以使用。
如果该结构体在内部定义main()
,其范围将被限制为main()
.
当结构成员数量很大时,传递结构成员的效率不高。然后结构变量可以传递给函数。
void display(struct student a)
{printf("Name: %s\n", a.name);printf("Roll: %d\n", a.roll);printf("Gender: %c\n", a.gender);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,a.marks[i]);
}
void main()
{struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};display(stu1);
}
如果结构的大小很大,那么传递它的副本不会非常有效。我们可以将结构指针传递给函数。在这种情况下,结构的地址作为实际参数传递。
void display(struct student *p)
{printf("Name: %s\n", p->name);printf("Roll: %d\n", p->roll);printf("Gender: %c\n", p->gender);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,p->marks[i]);
}
void main()
{struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};struct student *ptrStu1 = &stu1;display(ptrStu1);
}
将结构数组传递给函数类似于将任何类型的数组传递给函数。数组的名称,即结构体数组的基地址,被传递给函数。
void display(struct student *p)
{ for( int j = 0; j < 10; j++){printf("Name: %s\n", (p+j)->name);printf("Roll: %d\n", (p+j)->roll);printf("Gender: %c\n", (p+j)->gender);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,(p+j)->marks[i]);}
}void main()
{struct student stu1[10];display(stu1);
}
结构作为函数返回
我们可以返回一个结构变量,就像任何其他变量一样。
#include <stdio.h>struct student {char name[20];int roll;char gender;int marks[5];
};struct student increaseBy5(struct student p)
{for( int i =0; i < 5; i++)if(p.marks[i] + 5 <= 100){p.marks[i]+=5;}return p;
}void main()
{struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}};stu1 = increaseBy5(stu1);printf("Name: %s\n", stu1.name);printf("Roll: %d\n", stu1.roll);printf("Gender: %c\n", stu1.gender);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,stu1.marks[i]);
}/* Output */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 81
Marks in 1th subject: 98
Marks in 2th subject: 73
Marks in 3th subject: 92
Marks in 4th subject: 98
该函数increaseBy5()
对加分后分数小于或等于 100 的科目加 5 分。注意,返回类型是 类型的结构体变量struct student
。
返回结构成员时,返回类型必须是该成员的类型。
结构体指针也可以由函数返回。
#include <stdio.h>
#include <stdlib.h>struct rectangle {int length;int breadth;
};struct rectangle* function(int length, int breadth)
{struct rectangle *p = (struct rectangle *)malloc(sizeof(struct rectangle));p->length = length;p->breadth = breadth;return p;
}void main()
{struct rectangle *rectangle1 = function(5,4);printf("Length of rectangle = %d units\n", rectangle1->length);printf("Breadth of rectangle = %d units\n", rectangle1->breadth);printf("Area of rectangle = %d square units\n", rectangle1->length * rectangle1->breadth);
}/* Output */
Length of rectangle = 5 units
Breadth of rectangle = 4 units
Area of rectangle = 20 square units
struct rectangle
请注意,我们已经使用动态分配了 size 的内存malloc()
。由于它返回一个void指针,我们必须将其类型转换为struct rectangle
指针。
3. 自指结构
我们讨论过指针也可以是结构的成员。如果指针是结构体指针怎么办?结构体指针可以与结构体类型相同,也可以不同。
自引用结构是那些具有与其成员相同类型的结构指针的结构。
struct student {char name[20];int roll;char gender;int marks[5];struct student *next;
};
这是一个自引用结构,其中next
是struct student
类型结构指针。我们现在将创建两个结构变量stu1
并stu2
用值初始化它们。然后我们将把 的地址存储stu2
在 的next
成员中stu1
。
void main()
{struct student stu1 = {"Alex", 43, 'M', {76, 98, 68, 87, 93}, NULL};struct student stu2 = { "Max", 33, 'M', {87, 84, 82, 96, 78}, NULL};stu1.next = &stu2;
}
stu2
我们现在可以访问使用stu1
和的成员next
。
void main()
{printf("Name: %s\n", stu1.next->name);printf("Roll: %d\n", stu1.next->roll);printf("Gender: %c\n", stu1.next->gender);for(int i = 0; i < 5; i++)printf("Marks in %dth subject: %d\n",i,stu1.next->marks[i]);
}/* Output */
Name: Max
Roll: 33
Gender: M
Marks in 0th subject: 87
Marks in 1th subject: 84
Marks in 2th subject: 82
Marks in 3th subject: 96
Marks in 4th subject: 78
假设我们想要在 后添加一个不同的结构体变量stu1
,即在和之间插入另一个结构体变量stu1
stu2
。这很容易做到。
void main()
{struct student stu3 = { "Gasly", 23, 'M', {83, 64, 88, 79, 91}, NULL};st1.next = &stu3;stu3.next = &stu2;
}
现在stu1.next
存储 的地址stu3
。并stu3.next
有地址stu2
。我们现在可以使用访问所有三个结构stu1
。
printf("Roll Of %s: %d\n", stu1.next->name, stu1.next->roll);printf("Gender Of %s: %c\n", stu1.next->next->name, stu1.next->next->gender);/* Output */
Roll Of Gasly: 23
Gender Of Max: M
stu1
请注意我们如何使用结构指针在,stu3
和之间形成链接stu2
。我们在这里讨论的内容构成了链表的起点。
自引用结构在创建数据结构(例如链表、堆栈、队列、图等)时非常有用。