结构体
基础知识:
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明:
struct tag
{
member-list;
}variable-list;
例如描述一个学生:
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
};//注意这里的分号不能丢
像一般的基本类型如int float,声明只是告诉编译器有这个变量,还没有申请空间。并且这里我们要知道,这六行代代码只是声明了一个类型,就像int float 一样。
声明了类型之后,就可以用它来创建变量了。我们看看它是如何创建变量的:
1.可以这样创建在结构体类型的后面直接加上变量的名字
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
}stu;
可以看到,这是在结构体后面直接加上了变量的名字,那么这里的这个变量就是一个全局变量,也就是说在全局范围内谁都可以使用,一般不推荐使用。
2.通过类型+变量形式创建,创建形式如int float类型:
struct Stu
{
char name[20];//结构体成员变量
int age;//结构体成员变量
char sex[5];//结构体成员变量
};
struct Stu stu;//通过类型加变量的形式
创建变量也叫实例化对象。我们知道定义变量后,系统就会给该变量分配一定的内存。
特殊的声明:
**即不完全声明。**看下面这两个代码:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20],*p;
可以看到,这两个结构体声明都不像我们刚刚说的结构体声明那样,好像少了点东西,这种结构体类型叫匿名结构体类型,也就是省略了名字,但这种方法是不被推荐的。
那么问题来了?
//在上面代码的基础上,下面的代码合法吗? p = &x;
警告: 编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的, 匿名结构体类型只能在结构体类型的后面直接创建变量,并且此变量只能用一次。
结构体内有成员变量name、age、sex,我们又该如何去访问他们呢?之前的操作符与运算符章节我们了解到有 . 和 -> 运算符,使用结构体变量名+.+成员变量名,就成功的访问了结构体的成员变量。当结构体访问指向变量的成员,我们得到的是一个指向结构体的指针,这时候访问结构体成员变量就需要使用结构体变量名+ -> +成员变量名来进行访问。
接下来是具体的代码:
struct Stu stu;//创建一个结构体变量
strcpy(stu.name,"zhangsan");//初始化成员变量name
s.age = 20;//初始化成员变量age
struct Stu
{
char name[20];
int age;//结构体成员变量
char sex[5];//结构体成员变量
};
再为大家介绍
结构体的自引用
我们可以在结构体内包含一个类型为该结构体本身的成员,于是就有这样的代码:
struct Node
{
int data;
struct Node next;
};
但这样是行不通的,这样写的代码,sizeof(struct Node)是无限大的,因为结构体会一直不停的调用自己。正确的写法应该是,可以将这个在结构体内部的结构体指向自己:
struct Node
{
int data;
struct Node* next;
};
再补几片:
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
typedef struct Node
{
int data;
struct Node* next;
}Node;
//正确解法
struct A
{
int a;
struct B* pb;
}
struct B
{
int b;
struct A* pa;
}//可行否?
struct B;
struct A
{
int a;
struct B* pb;
}
struct B
{
int b;
struct A* pa;
}//正确解法
内存对齐
我们知道,只要是变量,都会有大小,也都可以计算出来。那么结构体也不例外,也有大小并且可以进行计算,以下是它的计算规则:
-
第一个成员在于结构体变量偏移量为0的地址处。
-
其他成员变量要对其到某个数字即对齐数的整数倍的地址处。对齐数=编译器默认的一个对齐数与该成员大小的较小值。
-
结构体总大小为最大对齐数即每个成员变量对齐数中的最大值的整数倍。
-
如果有嵌套结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍。结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐数:
VS中默认的值为8
Linux中的默认值为4
例子:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
为什么存在内存对齐?
大部分的参考资料都是如是说的:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址 处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器 需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为8
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;
}
结论:
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
位段
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
位段的声明和结构体是类似的,有两个不同:
- 位段的成员必须是int,unsigned int 或signed int。
- 位段的成员名后面有一个冒号和一个数字。
同样的,我们来看一个例子
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};
A就是一个位段类型。
那位段A的大小是多少?
printf("%d\n",sizeof(struct A));
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
我们下面看一下上面那个例子具体是怎样计算位段类型A的大小的:
- 首先,可以看到每个成员的类型都是int,也就是该位段的空间是由4个字节的int方式来开辟的。
- 再看每一个成员,第一个成员a,冒号后面是2,所以占2个bit位,接着成员b,占5个bit位,然后成员c,占10个bit位,可以算出现在一个int(32个bit位)被占了(2+5+10)个bit位,接下来的成员d需要30个bit位来存放,可以看到第一个int已经不足以放这个成员了,所以现在只能将这30个bit位全部放在新开辟的int中。
位段空间的开辟方法:
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
- 位段中的成员在内存中从左向右分配还是从右向左分配还没有定义。
- 当一个结构体包含两个位段,第二个位段成员较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。是一个被命名的整常数的集合。那么通俗点讲就是一一列举。
枚举是一种类型。
直接看代码,直观易懂:
enmu Day
{
Mon,//0
Tue,//1
Wed,//2
Thu,//3
Fri,//4
Sat,//5
Sun,//6
};
这样就定义了一个枚举类型,里面的Mon,Tue,等都是枚举类型的可能取值,也叫枚举常量,像注释里面的,这些常量是有值的,默认从0开始,以后依次+1。也可以在定义的时候赋初值,但不能在定义的外面赋初值。
例如:
enmu Day
{
Mon = 1,//1
Tue = 2,//2
Wed,//3
Thu,//4
Fri,//5
Sat,//6
Sun,//7
};
枚举的优点:
- 枚举可以增加代码的可读性和可维护性。
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染。即一个变量的多次使用。
- 便于调试,使用方便。
联合(共用体)
联合类型的定义:
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 比如:
/*联合类型的声明*/
union Un
{
char c;
int i;
};
/*联合变量的定义*/
union Un un;//定义之后就会开辟相应的内存空间
/*计算联合变量的大小*/
printf("%d\n",sizeof(un));
联合的特点:
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有 能力保存最大的那个成员)。
联合的计算:
- 联合的大小至少是最大成员的大小。
- 当最大成员不是最大对齐数的整数倍时,就要对起到最大对齐数的整数倍。
例子:
union Un1
{
char c[5];
int i;
};
最大成员的大小时5,但是不是最大对齐数4的整数倍,所以要对齐到最大对齐数的整数倍上,所以Un1的大小为8。
到现在,我们已经全部了解了结构体、枚举和联合类型,那么下面我们来看看联合和结构体的巧妙结合使用:
//将long类型的IP地址转换为点分十进制的表示形式
#include<stdio.h>
#include<stdlib.h>
/*定义联合类型ip_addr*/
union ip_addr
{
unsigned long addr;//长整型
/*联合类型内定义结构体类型,结构体类型内的4个char连续存放,和long型变量addr共用一块空间*/
struct
{
unsigned char c1;
unsigned char c2;
unsigned char c3;
unsigned char c4;
};
};
int main()
{
union ip_addr ip;
ip.addr = 177222989;
printf("%d.%d.%d.%d\n", ip.c4, ip.c3, ip.c2, ip.c1);//因为是小端存储,所以要从c4到c1依次存储
system("pause");
return 0;
}
运行结果如下: