C语言自定义类型——【结构体,枚举,联合】(一)

目录

零、前言

一、结构体——struct

1. 结构体的声明

2. 结构体的自引用

3. 结构体的创建和初始化

4. 结构体内存对齐【重难点!】

5. 修改默认对齐数

6. 结构体传参


零、前言

——为什么要有自定义类型?

——因为C语言自带的类型有限,不便于描述一些复杂对象。比如描述一个学生,可能需要姓名、性别、年龄、身份证号等等。这些复杂对象不能用单一int、char等描述,需要将他们组合起来,于是我们可以使用结构体这一自定义类型来描述。除了结构体,我们还有枚举和联合体这两种自定义类型。(指针和数组也可以视作自定义类型,但在本博客不做介绍~)


一、结构体——struct

1. 结构体的声明

结构是一些值的集合,这些值成为成员变量,结构的每个成员可以是不同类型的变量。

【语法形式】:

struct tag
{
    member-list;
}variable-list;

struct——结构体关键字; tag——标签的名字;member-list——成员列表;variable-list——变量列表。(变量也可以不在此创建,可以声明完以后创建)

例如:

struct Stu
{
    char name[20];
    int age;
    double score;
} s1, s2, s3;
struct Stu ss;  //另一种定义变量的方法

也存在匿名结构体类型,即不存在标签的名字,但该种结构体必须在声明的时候创建变量。(补:匿名结构体即使成员一样,但在编译器看来是不同的结构体!)

*可用typedef来进行重命名,来省略struct。


2. 结构体的自引用

——结构中能否包含一个类型为该结构本身的成员?(为了实现链式访问)

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

——不能!不能通过编译。正确的链式访问用指针即可。分成数据域和指针域。正确的如下:

struct Node
{
    int data;  //数据
    struct Node* next;  //指针
};

(最后一个指向NULL) 


3. 结构体的创建和初始化

struct Stu
{
    char name[20];
    int age;
    double score;
} s1 = {"zhangsan", 17, 88.8};
struct Stu ss = {"lisi", 18, 99.9};

4. 结构体内存对齐【重难点!】

比如我们定义一个结构体变量如下,并想求它的大小:

struct s1
{
    char c1;
    int i;
    char c2;
};

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

一般来说,我们一看:char 1个字节,int 4个字节,char 1个字节,一共6个字节就够了。但是,这个程序运行出来结果是12。

struct s2
{
    char c1;
    char c2;
    int i;
};

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

我们调整了结构体中元素的顺序,还是求它的大小,这个结果是8。这是为什么呢?那是因为结构体的内存对齐!

【结构体的对齐规则】:

1. 第一个成员在与结构体变量偏移量为0的地址处

2.其他成员变量要对齐到某个数字(对其数)的整数倍的地址处。

       对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值

        (VS中默认值为8)

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

规则是不是看起来很绕?当然啦!这里可是重难点!我们需要结合一些例子帮助理解~然后再来看这个规则就会清楚一些啦!

我们来试一下吧!

struct s1
{
    char c1;  //1
    int i;   //4
    char c2;  //1
};

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

对于这个代码,我们要给结构体变量s来开辟空间:

假设下面的表格就是我们的内存空间(右边一列表示相对起始位置的偏移量):

  • 由规则1我们知道s从偏移量为0处开始开辟,c1也就存放在偏移量为0的位置

        c1只占一个字节,所以它只需要偏移量为0的那一格就够了。(见下表格)

  • 然后我们读到规则2,“其他成员要对齐到某个数字(对齐数)的整数倍的地址处”,其中对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

        比如int i,它的大小是4个字节,编译器默认对齐数为8,取较小,所以它的对齐数是4。这意味着,i这个成员必须对齐到4的倍数处的偏移量的位置。在0往下找偏移量为4的倍数,那么我们找到4。于是,i就从偏移量为4的位置开始存放。i占4个字节,所以4~7都是i的。(见下表格)

  • 然后我们来放c2,c2的放置也是向上面那样遵守规则。对齐数为自身大小(char的自身大小为1)和默认对齐数(8)的较小值。所以c2的对齐数为1。c2只需要存放在偏移量为1的倍数的位置即可。那么我们就在7以下找1的倍数的偏移量,于是c2就放在的偏移量为8的位置处。(见下表格)
  • 现在我们一共放了0~8,共9个字节大小的内存。最后,由我们的规则3:“结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍”得,每个成员的最大对齐数为4,所以我们这个结构体的总大小需要是4的倍数。而我们现在的大小是9,所以离9最近的4的倍数是12,所以我们的总大小是12。于是见下表格。
(从这里开辟s1)c10
(浪费)1
(浪费)2
(浪费)3
                i4
                i5
                i6
                i7
                c28
(浪费)9
(浪费)10
(浪费)最后的对齐11
 12
13
14
15
16
17
18
19
20

大家听懂了嘛?让我们再举个例子熟悉一下吧!

struct s2
{
    char c1;  //1
    char c2;  //1
    int i;  //4
};

int main()
{
    struct s2 s;
    printf("%d\n", sizeof(s));
    return 0;
}
  • 由规则1得,第一个成员c1放在0偏移处,且占一个字节。
  • 由规则2得,第二个成员c2大小为1,默认对齐数为8,那么对齐数取较小值,为1。c2存放在偏移量为1的倍数的位置即可,于是c2放在偏移量为1的位置,且占一个字节。
  • 由规则2得,第三个成员i大小为4,默认对齐数为8,那么对齐数取较小值,为4。i存放在偏移量为4的倍数处。于是i存放在偏移量为4的位置处,且占4个字节。
  • 由规则3得,结构体总大小应该是最大对齐数的整数倍,这里的最大对齐数是1,1,4中的最大值4,所以结构体的总大小应该是4的整数倍。而目前0~7大小为8,是4的整数倍。所以这个结构体的大小就是8
(从这里开辟s2)c10
                c21
(浪费)2
(浪费)3
                i4
                i5
                i6
                i7
8
9
10
11
12
13
14
15

(补:Linux环境下没有默认对齐数,没有默认对齐数时,自身大小就是对齐数!)

(补:c语言里有个叫做offsetof的宏,是计算结构体成员相对起始位置的偏移量的,大家可以用offsetof来验证)

比如printf("%u\n", offsetof(struct s1, c1)); 你就可以得到0。 

下面我们来体验一下规则4。

struct s3
{
    double d;
    char c;
    int i;
};  //我们可以用规则1、2、3算出大小是16

struct s4
{
    char c1;  //1
    struct s3 s;  //16
    double d;  //8
};

现在要求s4的大小。此时出现了嵌套。

  • 规则4说:如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  • c1放0偏移处。老规矩了。
  • 然后放s3类型的s。s的大小为16个字节。但是这16个字节是从哪里开始的呢?“嵌套的结构体对齐到自己的最大对齐数的整数倍处”,s3的最大对齐数是8。所以s需要对齐到8的整数倍处。于是s从偏移量为8开始,占16个字节。
  • d的存放照常,对齐数是8,24是8的倍数所以放在24处,占8个字节。
  • 目前0~31,占了32个字节,“结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍”,所有成员中最大对齐数是8,所以这个结构体的大小应该是8的整数倍。32是8的倍数,所以该结构体大小为32
(s4从这里开始)c10
(浪1
2
3
4
5
6
费)7
                s8
                s9
                s10
                s11
                s12
                s13
                s14
                s15
                s16
                s17
                s18
                s19
                s20
                s21
                s22
                s23
                d24
                d25
                d26
                d27
                d28
                d29
                d30
                d31
32
33
34

——为什么存在结构体对齐?

——1.平台原因:不是所有硬件平台都能访问任意地址上的任意数据的。

        2.性能原因:数据结构应该尽量在自然边界上对齐。未对齐的内存,处理器需要两次内存访问,对齐的只需要一次。(也就是用空间换时间)

为了节省空间,可将结构体中较小的成员写在一起。


5. 修改默认对齐数

刚才我们的默认对齐数是8,其实这个是可以改的。

struct s
{
    char c1;
    double d;
};

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

不修改,即默认对齐数为8时,大小为16个字节。现在我们来修改一下:

#pragma pack(4)  //改成4
struct s
{
    char c1;
    double d;
};
#pragma pack()  //改回默认值
int main()
{
    struct s ss;
    printf("%d\n", sizeof(ss));
    return 0;
}

此时,我们改成4之后,它的大小(还是依照之前的规则计算得到)就是12个字节。如果改成1就是不对齐。

虽然我们支持修改默认对齐数,但我们最好不要瞎改,一般对齐值都为4、8。与机器字长匹配。所以最好不要修改为别的特殊值。


6. 结构体传参

如果结构体传参传结构体对象的话,(需要压栈)会浪费时间空间。

如果结构体传参传结构体地址的话,效率会更高!

结论:结构体传参尽量传地址!效率更高!而且方便修改!


好啦!C语言自定义类型——【结构体,枚举,联合】(一)的内容就这么多~关于位段、枚举、联合体的内容请见下一篇博客C语言自定义类型——【结构体,枚举,联合】(二)!欢迎大家多多交流!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值