【C语言】结构体,位段与枚举

在C语言中,有许多数据类型,如整型,浮点型,字符型等,结构体,枚举也是一种类型,他们包含在自定义类型中,结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访问。关于位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为位段,利用位段能够用较少的位数存储数据。枚举则可以列举未来的可能取值,下面具体介绍一下。

结构体

结构体的基本构成

结构体声明
struct tag
{
    member-list;
}variable-list;

举例

定义学生类型
struct Stu
{
    //成员变量
    char name[20];
    int age;
    float weight;
} s4, s5, s6;//这是全局变量
 struct Stu s7;//全局变量
int main()
{
    //局部变量
    struct Stu s1;
    struct Stu s2;
    struct Stu s3;
    return 0; 
}

在上面代码中,我们声明了一个结构体类型,名字为Stu,(定义末尾要注意分号)在这个结构体中,有三个成员变量,同时,我们还定义了一个结构体变量,在结构体定义中,可以允许使用者在结构体末尾定义属于这个结构体的变量,当然,也可以在结构体外,例如main函数中,或main函数外,要注意的是,在不同的位置定义变量,他的适用范围也是不同的

除了这种结构体定义外,还有一种特殊的结构体类型声明

匿名结构体

struct
{
    char c;
    int a;
    double d;
}s1;

对于匿名结构体来说,编译器会把他当成一种不同的类型,举例来说

struct
{
    char c;
    int a;
    double d;
}s1;
struct
{
    char c;
    int a;
    double d;
}*ps;
int main()
{
    ps = &s1;//err
    return 0;
}

在上面代码中,我们把一个结构体变量定义为了指针变量,让他去指向另一个结构体的地址,显然这是不行的,因为两个匿名结构体的类型在编译器看来完全不同,因此不能完成该代码,同时,匿名结构体不允许使用者在结构体外创建该结构体变量,只能在该结构体末尾创建。

结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

struct Node
{
    int data;
    struct Node n;
};//err
//不能在自己的类型中包含一个自己类型的变量
//只能包含一个自己类型的指针
int main()
{
    printf("%d\n",sizeof(struct Node));
    return 0;
}

显然,使用这种方式进行自引用是不行的,想要达到目的,只能包含一个自己类型的指针

struct Node
{
    int data;
    struct Node n;
};//err

struct Node
{
    int data;
    struct Node* next;
};

int main()
{
    struct Node n1;
    struct Node n2;
    n1.next = &n2;
    return 0;
}

同时,下面的代码也说明了结构体在引用时的一些问题

typedef struct
{
    int data;
    char c;
} S;//用了typedef后S是类型名而不是变量

typedef struct
{
    int data;
    Node* next;
}Node;//err
//先后顺序有问题
正确用法
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

结构体变量的定义和初始化

有了结构体类型,我们就可以开始定义变量,定义变量的以及初始化方法很简单

struct S
{
    int a;
    char c;
}s1;

struct B
{
    float f;
    struct S s;
};
int main()
{
    int arr[10] = { 1,2,3 };
    int a = 0;
    struct S s2 = { 100,'q' };
    struct S s3 = { .c = 'r',.a = 200 };// .+变量名可以指定初始化
    struct B sb = { 3.14f,{200,'w'} };
    return 0;
}

在初始化中,我们可以根据定义变量的先后进行初始化,也可以用.+变量名对指定的变量进行初始化,同时,我们也可以进行结构体嵌套初始化

结构体内存对齐

对于内存对齐,具有一定的规则:

结构体的第一个成员永远都放在0偏移处

从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍数,这个对齐数是:成员自身大小和默认对齐数的较小值

vs环境下默认对齐数是8

gcc环境下没有默认对齐数,没有的情况下,对齐数就是成员自身大小

当成员全部存放进去后,结构体的总大小必须是,所有成员的对齐数中,最大对齐数的整数倍

如果不够,则浪费空间对齐

如果有嵌套结构体的情况,即结构体里面有一个结构体,那么嵌套的结构体对齐到自身成员的最大对齐数的整数倍数处

结构体有多少字节就放多少,大小必须是最大对齐数的整数倍,最大对齐数包含嵌套的结构体成员中的对齐数

了解了这些规则后,我们再来通过代码计算内存对齐

struct S1
{
    int a;
    char c;
};
struct S2
{
    char c1;
    int a;
    char c2;
};
struct S3
{
    char c1;
    int a;
    char c2;
    char c3;
};
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
int main()
{
    printf("%d\n", sizeof(struct S1));//8
    printf("%d\n", sizeof(struct S2));//12
    printf("%d\n", sizeof(struct S3));//12
    printf("%d\n", sizeof(struct S4));
    return 0;
}

比如整型有4个字节,从0开始,默认对齐数8,4较小所以对齐数为4,对齐到4的倍数

那么为什么存在内存对齐?

原因有以下

1. 平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特

定类型的数据,否则抛出硬件异常

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访

也就是说,结构体的内存对齐是拿空间来换取时间的做法

因此,我们应该让占用空间小的成员尽量集中在一起,这样所浪费的空间也会减少

例如

struct S1
{
    char c1;
    int i;
    char c2;
};
struct S2
{
    char c1;
    char c2;
    int i;
};

通过计算可以发现,两个结构体所定义的变量相同,但是S2所占的空间却要比S1少一些

修改默认对齐数

修改默认对齐数可以通过#pragma pack(1)指令修改,通过修改括号内的数字就可以修改默认对齐数,同理,当括号内没有数字时,就会取消修改,还原默认,所以,当使用者觉得对齐方式不合适时,就可以使用这行代码修改默认对齐数

#pragma pack(1)
struct S
{
    char c1;//1字节 默认1 较小取1
    int i;//4 1 1
    char c2;//1 1 1
};
#pragma pack()

int main()
{
    printf("%d\n", sizeof(struct S));
    return 0;
}

结构体传参

struct S
{
    int data[1000];
    int num;
};
struct S s = { {1,2,3,4},1000 };
void print(const struct S* ps)//加const增加安全性
{
    printf("%d\n", ps->num);
}
int main()
{
    print(&s);
    return 0;
}

结构体传参尽量传结构体地址,为什么不传结构体本身进行传值呢?结构体在传参的时候,参数会被系统压缩,例如在main函数中调用时,会在main函数外创建一个空间进行存放,会进行压栈,也就是说当结构体越大所进行的压栈的浪费的资源越大,会损耗时间空间,因此尽量传结构体地址

位段

位段介绍

位段的声明和结构是类似的,位段的成员是整型家族,即int、unsigned int ,signed int,char

位段 - 二进制位
节省空间
struct a
{
    int _a : 2;//位段成员
    int _b : 5;
    int _c : 10;
};

int main()
{
    struct a sa = { 0 };
    printf("%d\n", sizeof(sa));
    return 0;
}

成员后面的数字指的是比特位,为什么成员的大小设置要变成这样呢?我们知道,位段的位是二进制位,将数据转化为二进制后,表示为00 01 10 11等数字,a是整型,有四个字节32个比特位,但是他只用来表示00 01 10 11的数字,也就是说他只需要两个比特位就能表示存储数据,如此一来就会浪费30个比特位,因此将他设置成2个比特位便足以使用,达到了更加节省空间的目的

位段的内存分配

位段不跨平台
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;
    return 0;
}

枚举

枚举类型的定义

枚举未来的可能取值,默认从0开始,递增1,如果修改后面的值,第一个仍然是0,后面的从改后数字递增1

enum Sex
{
    //枚举未来的可能取值
    //枚举常量,可改
    MALE=5,
    FEMALE,
    SECRET
};

int main()
{
    enum Sex s = MALE;
    return 0;
}

枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?

枚举的优点:

1. 增加代码的可读性和可维护性

2. 和#define定义的标识符比较枚举有类型检查,更加严谨。

3. 防止了命名污染(封装)

4. 便于调试

5. 使用方便,一次可以定义多个常量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值