目录
前言
1、结构体声明
1.1 结构体基本概念
1.2 结构体声明
1.3 特殊的结构体声明
1.3.1 匿名结构体声明
1.4 结构体自引用
1.5 结构体变量的定义和初始化
1.6 结构体内存对齐
1.7 修改默认对齐数
1.8 结构体传参
总结
前言
C语言除了有其内置类型,还有自定义类型。因为在实际运用中,我们可能会遇到一类数据,它的内容是由多种数据组成的,例如:一个学生的个人信息,它就包含了姓名、年龄、学号等等。这些数据并不是统一的类型,因此不能用C语言本身的数组去定义,那如果我们需要将其放在一个变量中时,我们就可以运用到结构体了。结构体的出现就是用来处理这样的内容数据不统一的变量,这给了编程者很大的操作空间,使得编程更加灵活自由。
接下来我们就详细介绍一下结构体吧。
1、结构体声明
1.1 结构体基本概念
结构体是一些值得集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。类比数组,数组的成员变量的类型必须相同,而结构体没有要求。
1.2 结构体声明
举个例子:
这是一个“学生”结构体的定义,它的类型为struct stu,struct是结构体声明的前缀,stu是自己定义的类型名,因为是包含“学生”信息的结构体,因此我们用来stu。定义好类型名之后,我们用一个大括号阔住接下来的内容,并且在大括号后面加上“;”。然后我们来看大括号里面的内容,上面这个例子我大括号里面放了了四个变量,分别用来存放:姓名;年龄;性别;学号。如果你想定义其他的也是可以的,内容是可以自己灵活改变的。这样,我们就算是定义好了一个结构体类型,它可以用来表示学生的基本信息。
注意,上面仅只是建立了一个类型,还没有正真建立结构体变量。相当于我们只是建立了一个类似于:int;float;double这样的东西,后面还需要用具体的变量去承载。类比int,我们定义int类型的变量,还得想一个变量名,比如“p”,那我们定义方式就是:int p=0;那这里p就是一个整型的变量。同理结构体也一样,上面的操作只是相当于写了一个“int”,还需要想一个变量名,假如“p”,定义时像这样:struct stu p={ 0 };这样,才算定义出了一个结构体变量p。
例如:
这是两种声明方法,可以在结构体建立时就声明变量p1,也可先定义类型,后再声明p2。这两种方法的区别是,前者定了全局变量,后者定义的是局部变量。
1.3 特殊的结构体声明
1.3.1 匿名结构体声明
例如:
声明结构体类型时,没有名字。这种类型的结构体只能用一次,而且是马上建立马上用:
像这样,定义一个s1的结构体之后,就再也无法使用这个匿名结构体。
匿名结构体特点:对于不同的匿名结构体,即使其内容一模一样,计算机还是会认为他们不是一个类型,如果用两个内容相同的匿名结构体,一个定义变量,一个定义指针,当指针指向变量时,计算机会报错。
1.4 结构体自引用
结构体内部包含自己。看一个例子:
对于这个结构体,第一个数据是一个整型,第二个数据定义的居然是指向自己的指针类型,这样的话,我们去访问这个结构体时,可以在它内部发现指向自己的指针,这就是结构体的自引用。
想一个问题:我们能不能在结构体里面直接定义一个本身类型的结构体变量呢?我们为什么要定义指针呢?
原因:因为如果我们在一个结构体里面直接定义它本身类型的变量,那我们将无法预估这个结构体的大小,因为每次计算这个结构体第二个参数的大小时,又会进入到一个结构体里面,而那个结构体的第二个参数还是结构体,是一个死循环。所以我们在结构体自引用时,只定义自身类型 的指针,就能避开这个问题。
为什么要这么用?
这个用法很重要,有了这样一个功能,才能实现链表建立。是数据结构很重要的内容。
注意:结构体的自定义是无法通过匿名结构体实现的。
1.5 结构体变量的定义和初始化
这个在前面其实已经讲到一部分,下面看几种定义的例子:
p1和p2定义时未初始化,p3定义时进行了初始化。这三种方式都定义了结构体变量,p3在定义时进行了初始化。
1.6 结构体内存对齐
先看一个例子,思考如下结构体占用几个字节:
#include <stdio.h>
struct S1
{
char c1;
int c2;
char c3;
};
int main()
{
printf("%d", sizeof(struct S1));
return 0;
}
答案:
答案是12。那么这是为什么呢?为什么不是6?这就涉及到我们揭晓来要讲的:结构体内存对齐。
内存中如何开辟结构体空间?
首先了解结构体的对其规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。 |
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 |
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 |
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍 处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数 倍。 |
就拿上面的结构体举例,讲解其在内存中的存放情况:
struct S1
{
char c1;
int c2;
char c3;
};
只有vs有默认对齐数,其他平台是没有默认对齐数的,其他平台对齐数就是数据本身的大小。
为什么存在内存对齐?
1、平台原因
不是多有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定的数据类型,否则抛出硬件异常。
2、性能原因:
数据结构(尤其是栈)应尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次访问;而对齐的内存访问只需要访问一次。
总的来说:
结构体的内存对齐是拿空间换取时间的做法。
在我们大致了解对齐规则后,我们需要了解,结构体定义的时候,位置的不同会影响其空间的大小。让占用空间小的数据尽量放在一起,这样可以减少结构体空间的浪费。
1.7 修改默认对齐数
在vs中默认对齐数是8,但是它可以被修改。
使用#pragma
看例子:
#include <stdio.h>
#pragma pack(4)
struct S
{
int i;
double d;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
用#pragma pack(4)将默认对齐数改成了4,结果从16变成了12。
后面的#pragma pack()是用来恢复默认对齐数。
1.8 结构体传参
直接看例子:
#include <stdio.h>
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* p)
{
for(int i=0;i<10;i++)
printf("%d\n", p->data[i]);
}
int main()
{
print1(s);
print2(&s);
return 0;
}
结构体传参有两种方式,第一种print1是直接将值传送过去(传值调用);第二种print2是传地址(传址调用)。我们认为第二种更好,因为直接传值的话,参数是需要压栈的,会有时间和空间的系统开销,如果传值一个过大的结构体,会导致系统的性能下降。
总结
本篇详细讲解了结构体的定义,希望对你有所帮助。