结构体
什么是结构体呢?它是一种值的集合,不同于数组的是,结构体的成员可以是不同类型的。
下面我们将从六个方面来介绍结构体
1.结构体类型的声明
例如我们描述一个学生,包含他的年龄,姓名,性别:
而这里的分号是不可省略的****这里的Stu代表的是结构体这种类型
struct
{
int age;
char name[20];
char sex[10];
}s1,s2;
这里s1,s2代表的是结构体变量。
此时变量是全局变量
并且初始化也可以不按照结构体内部的顺序:
struct
{
int age;
char name[20];
char sex[10];
}s1 = {.age=20,.name="zhangsan",.sex="nan"};
也可以在主函数里初始化:
(此时变量就是局部变量)
struct Stu
{
int age;
char name[20];
char sex[10];
};
int main()
{
struct Stu s1 = { 20,"zhangsan","nan" };
return 0;
}
如果你觉的在主函数里初始化的时候要写一大串太冗杂的话,也可以用typedef,把结构体定义成一个新的名字:
typedef struct Stu
{
char name[20];
char sex[10];
}Stu;
然后在后面的定义变量的时候就可以按如下方式:
typedef struct Stu
{
char name[20];
char sex[10];
}Stu;
int main()
{
Stu people = {"zhangsan","nan"};
return 0;
}
另外还有一种特殊的声明方式,叫做匿名结构体,如下:
struct
{
char name[20];
int age;
};
那我们把两个相同的结构体用不同变量创建,那他们的地址是相同的码,我们来看:
所以我们知道了编译器会把他们当成两种不同的类型,所以我们最多使用一次匿名结构体类型。
2.结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
我们想引用结构体自身的方式来获取下一个节点:
比如struct Node { int data; struct Node next; };
但这是不行的,因为如果我们用sizeof来计算结构体的大小,那struct Node next又包含了data和下一个节点,这样无穷无尽就没法计算了。
所以我们用指针来自引用:
struct Node
{
int data;
struct Node* next;
};
那我们这样写可以不可呢:
typedef struct
{
int data;
Node* next;
}Node;
也是不行的,因为在进入结构体的时候还没有创建Node变量,而结构体自引用的时候用Node*
改进一下:
typedef struct Node
{
int data;
struct Node* next;
}Node;
3.结构体内存对齐
那结构体在内存中是怎么样存放的呢?我们来计算一下下面几个例子中的结构体大小:
1.
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
我们看到了前三个例子中结构体的大小分别为:12,8,16
那大小是用什么规则来计算的呢?
下面我们就来介绍一下结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 (VS中默认的值为8 ;Linux中没有默认对齐数,对齐数就是成员自身的大小)
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们就用一个图来解释:
这个图就能很好解释第一个例子:方框右边黑色数字代表了偏移量,从0开始,左边第一列是我们要放置的三个数据,第二列分别是每个数据的大小,第三列是vs中默认的对齐数的大小’8’,第三列是自身大小和默认对齐数大小比较后的较小值,即对齐数,解释完后我们来看看实现的原理;规则是第一个成员要放在偏移量0处,所以我们把char c1放在第一格,而int i 通过规则2我们知道它要放在对齐数的整数倍的位置,所以4的倍数最近的4,所以把int i放在了偏移量为4的地方,而它占用了4个字节,而最后的char c2对齐数是1,所以直接放到8的地方,通过规则3,整个结构体的大小必须要是最大对齐数的整数倍,而这里最大对齐数是4,所以距离存放完数据的最近的整数倍就是12,所以这三个数据就占用了0~11这12个偏移量位置的大小,所以用sizeof算出来是12个字节,而图中灰色填充的就是每个数据占用的地方,而蓝色填充的地方就是满足这个规则而浪费的内存空间。
不知道通过这个图大家能不能更好的理解结构体是怎么计算的大小了,其他的例2,例3都是相同的方法,大家也可以自己去画图理解一下。
下面我们跟着来看例4,例4引用了另一个结构体变量,那么它的计算方式又是怎么样的呢?
而上面的规则4也已经告诉我们了: 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
嵌套的结构体是S3,而它当中的最大对齐数是double的‘8’,这里就不画图了,char c1还是在偏移量0的位置,S3就从8开始,里面的double占用8,9,10,11,12,13,14,15,char c占用16,浪费了17,18,19,int i占用20,21,22,23,double d占用24,25,26,27,28,29,30,31,整个结构体大小就是0~31共32个字节。
详细解析了规则之后那我们就在想为什么存在内存对齐呢?
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总而言之:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:我们就引出了修改默认对齐数这一概念:
#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;
}
把对齐数设置为1就相当于没有浪费空间了,所以大小是1+4+1=6.
4.结构体传参
这里简单提一下结构体的传参,函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
比如下面这个例子,第二个就好于第一个:
struct S
{
int data[100];
int a;
};
struct S s = {{1,3,4}, 100};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.a);
}
//结构体地址传参
void print2(struct S* p)
{
printf("%d\n", ps->a);
}
int main()
{
print1(s);
print2(&s);
return 0;
}
5.结构体实现位段(位段的填充&可移植性)
**
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int,unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
**
例如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
printf("%d\n", sizeof(struct A));
为什么大小是8个字节呢?接下来我们就要介绍位段的内存分配了。
-
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(s));
return 0;
}
这个代码中结构体中的成员是被赋值成什么了呢?
我们调试打开内存可以看到:
结构体中存放的是62 03 04,那这几个数又是怎么来的呢?
这个图就能很好解释了,先把成员要赋予的值转化成二进制,因为是char类型,所以先开辟1个字节的大小,我们假定从右往左放,因为a给了3个比特位,b给了4个,对应的放进去,因为现在就只剩下一个位了,所以继续开辟一个字节,依次这样放置了c,d。
但是位段也存在一些跨平台问题:
-
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
枚举
枚举顾名思义就是一一列举。
把所有可能的取值一一列举。
如下面的例子:enum Day { Mon, Tues, Wed, Thur, Fri, Sat, Sun };
我们把一周所有的天数枚举出来,
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
例如
enum Color
{
RED=1,
GREEN=2,
BLUE=4
};
> 枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 便于调试
- 使用方便,一次可以定义多个常量
联合体
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
union Un
{
char c;
int i;
};
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。
我们可以看到在这里两个成员的地址是相同的,那到底是怎么存放的呢?
这里我们先调试到把11223344赋值给了i,然后下一步看i会变成什么
我们赋值完c以后,i也变了,所以我们能看出枚举类型是公用的内存
之前我们可以用强制转化类型的方式来判断编译器的大小端存储,现在我们也可以用联合体类型来判断了:
下面我们简单介绍一下联合体类型的计算:也要遵循以下规则
联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
我们来看个例子:
解释以下short[7]它一共14个字节,每个元素2个字节,默认对齐数是8,所以对齐数为2,而int对齐数是4,所以为4的整数倍并且大于14,就为16.
以上就是关于C语言自定义类型的介绍,如有错误,欢迎指正,谢谢大家!