目录
一、结构体
(一)结构体的声明:
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
上面这种声明是一种比较常见的声明,还有一些特殊的声明,比如:
匿名结构体类型:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
int main()
{
p = &x;
return 0;
}
我们知道,同一个结构体能够相互的赋值,可以看到 *p 和 x 的结构成员一样,但是能够 p=&x 吗?
答案是不可以的,就算 *p 和 x 两个结构体成员一模一样,但实际编译器会把他们当成不同类型。所以 p 不能存放 x 的地址。
匿名结构体类型只能使用一次,以后在不能用了。只能在创建的时候定义结构体变量。
(二)结构体的自引用
结构体自引用就是,在结构体成员中包含指向结构体自身类型的结构体指针
例如:
struct Node
{
int data;
struct Node* next;
};
切记,结构体自引用,成员定义只能是指针,如果结构体内成员定义为struct Node* next; 则会报错,因为a定义中又有a,无限循环,系统无法确定该结构体的长度,会判定定义非法。
如果想要使用typedef,代码如下:
typedef struct Node
{
int data;
struct Node* next;
}Node;
我们一定要注意,typedef struct Node 中的 Node 一定不能省略 ,因为在结构体内部定义结构体变量时,它会去找这个结构体,但该结构体还未声明完成,所以无法被引用和定义。就会产生先有鸡或者先有蛋的这样的问题
(三)结构体内存对齐
我们知道,变量有自己的大小,数组也有自己的大小,那么结构体有没有自己的大小呢?
回答是肯定的,结构体也有自己的大小,但是结构体的大小是简单的将每个成员大小加起来的吗?
下面我们看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S1
{
int a;
char b;
char c;
};
struct S2
{
char b;
int a;
char c;
};
int main()
{
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S2));
return 0;
}
看了这段代码,我们可能以为S1大小=S2大小等于6个字节
可事实是S1的大小是8个字节,S2的大小是12个字节:
原来计算结构体大小是遵循结构体内存对齐规则:
因此,我们就可以分析S1、S2的结构体大小,分析如下图:
存在内存对齐的原因:
(四)修改默认对齐数
虽然对齐数是编译器默认的,我们也是可以修改默认对齐数的 ,
使用#pragma预处理指令改变默认对齐数
#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.直接传结构体 2.传结构体地址
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* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
函数传参的过程中参数是需要压栈,会有时间和空间上的系统的开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大所以会导致性能的降低。
因此,
在结构体传参的时候,我们要传结构体的地址
二、位段
什么是位段:
位段就是通过结构体来实现的以位(bit)为单位数据储存结构,并且允许程序员对位进行修改。因此,我们就可以看的出来,位段是一种节省空间的用法。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
A就是一个位段类型
那A的大小是多少呢?
用过运行我们知道是8个字节。
想要知道为什么是八个字节,我们要学习一下位段的内存方式:
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
我们假设内存是跳着用的,那struct S就是占3个字节,如果是接着上一个空间使用的,那struct S 只会占用2个字节,因此我们可以通过运行结果知道struct S 是如何在内存中存放的
因此,我们通过调试,可以判断,当剩余的空间不足是,会浪费剩下的空间,重新开辟新的空间加以使用。
位段的跨平台问题
总结:
三、枚举
什么是枚举:
顾名思义,就是一 一 列举,
比如在我们的生活中,
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这些实例就可以用枚举
代码如下:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
例如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
四、联合(共用体)
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 比如:
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
return 0;
}
我们可以知道这个union Un的大小是4个字节。
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为 联合至少得有能力保存最大的那个成员)。
联合的大小计算
联合的大小至少是最大成员的大小。 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。