目录
结构体
定义:
声明:
结构体的自引用:
结构体变量的定义和初始化:
结构体内存对齐:
结构体传参:
位段(位域):
枚举
声明:
枚举类型的优点:
枚举的使用:
联合体(共用体)
声明:
特点:
联合体大小的计算:
结构体是一些值的集合,这些值称为成员变量。每个成员可以是不同类型的变量。
struct tag
{
member-list;
}variable-list;
其中tag是一个标签名,是自定义的;
member-list是成员列表,可以是多个不同类型的表量;
variable-list是变量列表,是定义的struct tag类型的结构体变量,可以是多个也可以没有。
特别的:当声明结构体时,struct前边出现typedef时,variable-list将不再表示定义结构体变量,而是将struct tag类型的结构体重命名为variable-list
typedef struct str
{
int a;
char b;
double c;
}p;
如上例,p不再是结构体变量,而是将struct str重命名后的名字。
特殊的结构体声明有如下的:
struct
{
member-list;
}variable-list;
与常规结构体声明相比,这种特殊的结构体声明没有标签名,被称为匿名结构体声明。
匿名结构体也可以使用typedef进行重定义操作。
匿名结构体只能在variable-list位置进行定义结构体变量,且即使两个匿名结构体中存储的数据类型完全相同,分别用它们定义的结构体变量,是完全不同的类型。
struct
{
int a;
char b;
double c;
}*p1;
struct
{
int a;
char b;
double c;
}p2;
int main()
{
p1 = &p2;
return 0;
}
上述程序中p1=&p2是非法的,如果这样使用,在程序运行时会出现一些不可预料的问题。
结构体的自引用不能像函数嵌套、递归那样直接引用,如果直接像函数嵌套、递归那样进行结构体自引用的话,结构体的大小将无法算。
正确的结构体自引用方式应该是通过结构体指针进行结构体自引用:
struct str
{
int date;
struct str*p;
};
需要注意如下的结构体自引用是错误的:
typedef struct
{
int date;
node*p;
}node;
因为结构体中自引用的node在后边才出现,解决方法如下:
typedef struct node
{
int date;
struct node*p;
}node;
结构体变量可以再声明时直接定义,也可以在结构体声明之后,需要用的时候再定义。
结构体变量的初始化可以在定时结构体变量时就初始化,也可以需要用的时候再初始化。
struct str
{
int a;
int b;
struct str*p;
}p1={1,2,NULL},p2;
p2={3,4,NULL};
struct str p3={5,6,NULL};
结构体还可以嵌套初始化:
struct str
{
int a;
int b;
};
struct ret
{
int a;
struct str b;
struct ret*c;
}p1={1,{2,3},NULL};
struct ret p2={4,{5,6},NULL};
了解结构体的内存对齐有助于我们理解计算结构体的大小,首先要掌握结构体的对其规则:
1、 结构体的第一个成员永远放在相较于结构体变量起始位置的偏移量为0的位置;
2、从第二个成员开始,往后的每个成员都要对齐到某个数(对齐数)的整数倍的地址处;
对齐数=编译器默认的一个对齐数 与 该成员自身大小的较小值;
3、结构体总体大小为最大对齐数(每个成员都有一个对齐数)的整数倍;
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数) 的整数倍。
根据这个对其规则可以发现,结构体成员变量在内存中不一定是连续存放的。
另外还要知道一个宏 offsetof (type,member) 可以计算结构体或者联合体中成员相较于结构体起始位置的偏移量(以字节为单位),返回值是一个size_t的无符号整形,其中type表示要计算的类型(结构体类型或联合体类型),member是成员名,使用它时要包含头文件
有一段代码:
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
int i;
char c1;
char c2;
};
int main()
{
struct S1 s1 = { 0 };
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", (int)offsetof(struct S1, c1));
printf("%d\n", (int)offsetof(struct S1, i));
printf("%d\n", (int)offsetof(struct S1, c2));
return 0;
}
代码最终输出是什么呢?让我们来分析一下:
代码运行结果:
那对于嵌套结构体,又如何呢?
#include
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s;
double d;
};
int main()
{
struct S4 s = { 0 };
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
printf("%d\n", (int)offsetof(struct S4, c1));
printf("%d\n", (int)offsetof(struct S4, s));
printf("%d\n", (int)offsetof(struct S4, d));
return 0;
}
由于有结构体嵌套,我们先分析struct S3:
再分析struct S4:
代码运行结果:
有些时候编译器默认的对齐数不太合适,可以使用#pragma pack(n)设置编译器默认对齐数为n;使用完之后用#pragma pack()取消人为设置的默认对齐数,还原为编译器默认的对齐数。
结构体传参有 传址 和 传值 两种方式
struct S
{
int data[100];
int num;
};
void ptr1(struct S tmp)
{
tmp.num = 10;
}
void ptr2(struct S* ps)
{
ps->num = 10;
}
int main()
{
struct S s1 = { {1,2,3}, 100 };
struct S s2 = { {1,2,3}, 100 };
ptr1(s1);
ptr2(&s2);
printf("%d\n", s1.num);
printf("%d\n", s2.num);
return 0;
}
相对于 传值 来说,传址 更好一些,因为函数传参时,参数是需要压栈的,会有时间和空间上的系统开销,如果结构体过大,参数压栈的系统开销会很大,进而导致性能下降。
同时 传址 调用 能通过函数改变结构体中的数据,而 传值 调用就做不到。
代码运行结果:
位段的声明与结构体类似,但又有所不同:
1、位段成员必须是int 、unsigned int、signed int、char类型的变量;
2、位段成员名后边有一个冒号和一个数字(数字表示占用几个二进制位)。
比如:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段 ,那么该如何计算位段的大小呢?
首先要知道位段的内存分配:
1、位段开辟内存空间时是以1个字节或者4个字节的方式开辟的;
2、尾端涉及很多不确定因素,不能跨平台。
位段在进行创建空间存储数据时会因为平台的不同,而产生一些差异,例如下面一段代码:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
观察结构体可以发现,总共占用47个bite位,所以理论上sizeof算出来的结果应该是8个字节(两个整型空间),运行代码结果如下:
可以发现,与我们推测的一致,确实是8个字节,但是这其中还是存在一些问题:
1、在一个字节中存储数据时,是从右往左分配空间还是从左往右分配空间?
2、当第一次创建的空间 a(假设空间名字为a) 中剩余空间不够存储下一个位段 date(假设变量为date)时,又创建了一块新空间 b ,那么在存储date时,a空间中的剩余空间是直接舍弃,还是继续利用?
就上述问题,我们来分析一下如下代码:
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;
}
假设在VS2019上数据是从右往左分配空间,且当第一次开辟的空间剩余空间不够存储下一个位段时,直接舍弃:
根据分析,结构体s在内存中存储的应该是62 03 04(小端存储),调试代码,观察内存存储情况:
可以发现内存中的存储情况与推测一致。
但这只是在VS2019上是这样的,在其他平台上是如何存储的需要自己探索。
总之:与结构体相比,位段能够达到同样的效果,但可以很好的节省空间,但同时也一些不确定的问题。
枚举顾名思义就是把可能的取值一行一列举,常见的有日常生活中的周一到周日、男女性别、月份……
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
Male,
Female,
Secret
};
enum Color
{
Red,
Green,
Black,
Bule
};
以上的enum Day、enum Sex、enum Color都是枚举类型,{}中的是枚举类型的可能取值,又叫枚举常量。
枚举常量是有值的,默认从0开始,依次递增1,也可以自己赋初值,例如:
enum Color
{
Red,
Green,
Black=6,
Blue
};
int main()
{
printf("%d %d %d %d\n", Red, Green, Black, Blue);
return 0;
}
代码运行结果:
1、增加代码可读性和可维护性;
2、相比于define定义的标识符,枚举类型有类型检查,更加严谨;
3、便于调试;
4、使用方便,一次可以定义多个常量。
enum Color
{
Red,
Green,
Black=6,
Blue
};
int main()
{
enum Color col = Red;
return 0;
}
注意:只能使用枚举常量对枚举变量赋值。
联合体(共用体)类型定义的变量也包含一系列的成员,特殊的是联合体(共用体)的成员是共用一块空间的。
union UN
{
int i;
char c;
};
联合体成员共用一块空间,所以各成员的地址应该是一样的,联合体的大小应该至少为最大成员的大小。
union UN
{
int i;
char c;
};
int main()
{
union UN un = { 0 };
printf("%p\n", &un);
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%d\n",sizeof(un));
return 0;
}
运行结果:
因为联合体共用一块空间的特性,我们可以用它来判断当前机器是大端存储还是小端存储:
代码如下:
int check()
{
union UN
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
代码分析:
已知我的电脑是小端存储,则代码运行结果为:
与已知结果一致,代码没有问题。
1、联合体大小至少为最大成员的大小;
2、当最大成员大小不是最大对齐数的整数倍时,要对齐到最大对齐数整数倍。
例如如下代码:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
输出应该是多少呢?我们来分析一下:
代码运行结果;