C语言学习笔记——自定义数据类型

目录

前言

一、结构体

1.结构体的声明       

2.结构体的定义

3.结构体的自引用

4.结构体的内存对齐

5.结构体调用与传参

二、位段

1.位段的声明与调用

2.位段的内存分配

三、枚举类型 

1.枚举类型的定义

2.枚举类型的优点

 四、联合类型

1.联合类型的定义

2.联合类型的特点

 3.联合体的大小计算

结束语


前言

       C语言为我们提供了整型,浮点型两种基础的数据类型,同时为了方便程序员完成复杂的代码,C语言还提供了三种自定义数据类型——结构体,位段,枚举和联合体。


一、结构体

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

1.结构体的声明       

struct student
{
    char name[20];
    int age;
    double score;
}stu1;

        该段代码即声明一个名为"student"的结构体,该结构体中含有三个元素,分别为char类型的数组name,int类型的age,double类型的score。同时在声明完结构体类型后,直接创建了一个名为"stu1"的全局变量。

        这就是结构体的简单声明,总结下来就是首先在"struct"后加上结构体类型名称,然后在后方大括号中声明该结构体各个元素的类型,最后在大括号后加上分号";"。

        除了上述定义方式外,我们也可以采用下面这种不声明结构体名称的定义方式,称为匿名结构体类型。 注意,下方代码声明了两个看上去一模一样的匿名结构体,但实际上编译器会把它们当成两种不同的类型,因此不能使用“stu1”给“stu2”赋值或进行其他运算。

struct 
{
    char a;
    int b;
    double c;
}stu1;

struct
{
    char a;
    int b;
    double c;
}stu2;

2.结构体的定义

struct student
{
    char name[20];
    int age;
    double score;
}stu1;

struct student stu2;

int main()
{
    struct student stu3 = {"zhangsan",18,95.0};
    struct student* ps = &stu3;
    return 0;
}

        与定义整型一样,定义结构体采用“数据类型+变量名”的格式,需要注意的是,结构体的数据类型为“struct + 结构体名称”,例如上方代码中的结构体“student”的数据类型为“struct student”。同理,定义结构体指针则需在数据类型后加上“*”。

        如需定义全局变量,可模仿该代码直接在分号前声明变量名,也可像定义整型全局变量,浮点型全局变量一样直接定义。

        当然,如果是匿名结构体类型,由于没有结构体名称,所以只能在声明后直接定义全局变量。如需按照一般规则定义变量,则可使用typedef定义类型名称,从而定义变量。

typedef struct 
{
    char a;
    int b;
    double c;
}student;

student stu;

3.结构体的自引用

        我们知道,一个函数可以调用它自身,形成函数递归,而结构体在声明的时候,它所包括的元素可不可以与它的类型相同呢?当然,这就是结构体的自引用。

strcut con
{
    int a;
    struct con next;
};

       上面这种写法显然是不行的,这样声明结构体就类似于函数的死递归。在这里我们应该用结构体指针以避免出现上述情况。

strcut con
{
    int a;
    struct con* next;
};

       结构体自引用的意义是什么呢?通过自引用,结构体形成了一条数据链,通过一个结构体,可以找到下一个结构体的地址从而将数据(例如整型变量a)保存起来。

4.结构体的内存对齐

       我们知道,一个int类型的变量占用4个字节的内存空间,一个char类型的变量占用1个字节,一个double类型的变量占用8个字节。那么下面这个结构体类型的变量占用内存空间大小是13个字节吗?

struct stru
{
    int a;
    char b;
    double c;
};

struct stru s;

        该结构体类型变量占用的内存空间大小为16个字节。结构体变量的大小可不是简单的把所有元素的大小加起来,而是按照一定的对齐规则来计算大小。

 0 1 2 3 4 5 6 7 8 9101112131415
aaaabcccccccc

        该结构体占用内存空间的模型图如上图(空白格为未使用的空间),上方数字代表相对于结构体初始地址偏移x个位置后的地址,下方字母代表该格被对应变量占用。由于变量a位于结构体元素最前方,故a的地址与结构体地址相同,因此a的初始地址偏移量为0。

         我们发现,c的起始位置并不是紧跟在b之后,而是空出了三个字节。这就涉及到结构体的对齐规则,结构体元素的偏移量要等于其对齐数的整数倍。对齐数的值为该元素类型占用内存空间大小默认对齐数之间的较小值(VS编译器的默认对齐数为8,默认对齐数可通过#pragma pack()指令修改)。

         例如,变量b为char类型,大小为1(字节),默认对齐数为8,因此其对齐数为1,偏移量对齐到1的整数倍处(4);变量c为double类型,大小为8(字节),默认对齐数为8,因此其对齐数为8,偏移量对齐到8的整数倍处(8)。

         确定各元素偏移量后,就可以开始计算结构体大小了,这里有一条规则,结构体大小必须是各个元素中的最大对齐数的整数倍。上图中变量a的对齐数为4,变量b的对齐数为1,变量c的对齐数为8,因此最大对齐数为8,则该结构体大小必须为8的整数倍。通过对齐规则可知,该结构体占用16个字节。

struct stru
{
    char a;
    double b;
    int c;
};

struct stru s;
01234567891011121314151617181920212223
abbbbbbbbcccc

       若结构体中元素如上图所示排列,则按照结构体大小的计算规则,该结构体大小为24(字节)。

       另外,若结构体中包含结构体类型的元素,该元素的对齐数为其所包含的元素中的最大对齐数,而不是结构体类型大小与默认对齐数中的较小值。

5.结构体调用与传参

struct stru
{
    int a;
    char b;
    double c;
}s1;

struct stru s2;

int main()
{
    struct stru* ps = &s2;
    scanf("%d %c %lf",&s1.a,&s1.b,&s1.c);    
    s2 = s1;
    printf("%d %c %lf\n",s2.a,s2.b,s2.c);
    printf("%d %c %lf\n",ps->a,ps->b,ps->c);
    return 0;
}

        若需调用整个结构体变量,如图中使用结构体s1给s2赋值,则调用方式与调用整型,浮点型相似。若需单独调用结构体内的某个元素,可使用“变量名+ . +元素名”,如图中第一个print函数后的参数。若需调用结构体指针所指向的结构体变量的元素,则可使用“指针名 + -> + 元素名”。

        关于结构体传参,这里简单一提。在结构体传参时,一般传结构体指针。由于结构体所占用的内存空间一般较大,传参时耗费的时间和空间较多,而指针的大小相对较小,并且指针在使用时更加方便。

二、位段

         跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。位段在跨平台使用时由于部分内容定义不同,容易出现问题,因此在编写跨平台程序时应避免使用位段。这里以VS编译器的位段为例。

1.位段的声明与调用

struct stru
{
    int a:10;
    char b:4;
    char c:2;
};

int main()
{
    struct stru s = {0,0,0};
    s.a = 100;
    s.b = 10;
    s.c = 2;
    return 0;
}

       位段的声明和结构体非常相似,不同的是位段的元素只能为int类型或char类型。另外在声明元素时要在后面加上“ : ”和一个数字。调用则与结构体基本相同,如图。

2.位段的内存分配

        这里我用VS自带的调试器调试并查看了赋值完成后变量s在内存中的保存情况。在位段中,声明元素 : 后的数字即该元素在内存中占用的比特位。如上图中,变量a在内存中占用10个比特位,由于a的类型为int,故需要在内存中开辟4个字节的空间。因此图中内存的第一行的4个字节(显示为“64 00 00 00”,十六进制)的前10个比特位为a所占用的内存空间。转化为二进制表示如下图,红框中为a占用的内存,剩余部分为位段所浪费的内存。

         接下来我们继续看位段中还有两个元素,char类型的b和c。由于它们为同一类型,并且占用的总比特位小于8(1字节,char类型变量的大小)因此将它们保存在同一个字节中(显示为“2a”,十六进制),按顺序从低位向高位保存。转化为二进制如下图,低位两个比特位为b占用,高位四个比特位为c占用,剩下两个未使用。由于这个字节中只剩下两个比特位,若再添加一个占用两个以上比特位的char类型元素,则需放弃这两个比特位,重新开辟一个字节的空间来存放。

三、枚举类型 

1.枚举类型的定义

enum Color
{
    RED,
    BLUE,
    YELLOW = 10,
    GREEN
};

int main()
{
    int a = RED;
    return 0;
}

       如图为枚举类型的定义方式,大括号内部的元素称为枚举常量。枚举常量的使用与#define定义的常量类似,使用常量名即可调用。枚举常量在定义时可进行赋值,如图中“YELLOW = 10”。若未进行赋值,则默认第一个元素的值为0。后一个元素的值为前一个元素的值加一。由此可得,图中RED的值为0,BLUE的值为1,YELLOW的值为10,GREEN的值为11。

2.枚举类型的优点

       枚举常量可以增加代码的可读性,我们可以对比以下两段代码。

int main()
{
    int n = 0;
    scanf("%d",&n);
    switch(n)
    {
        case 0:cal('+');
        break;
        case 1:cal('-');
        break;
        case 2:cal('*');
        break;
        case 3:cal('/');
        break;
    }
    return 0;
}
enum Cal
{
    ADD,
    SUB,
    MULT,
    DIV
}

int main()
{
    int n = 0;
    scanf("%d",&n);
    switch(n)
    {
        case ADD:cal('+');
        break;
        case SUB:cal('-');
        break;
        case MULT:cal('*');
        break;
        case DIV:cal('/');
        break;
    }
    return 0;
}

       这两段代码的效果相同,且均是用来确定cal函数的参数的(假设cal函数存在)。若使用第二段代码则更能直观地看出输入不同值的效果。

       另外,与#define相比,枚举常量便于调试。由于#define定义的常量是在预处理阶段完成,而枚举常量则是在编译阶段完成,因此#define定义的常量无法进行调试。

 四、联合类型

1.联合类型的定义

//联合类型的声明
union Un
{
    char c;
    int i;
};
//联合变量的定义
union Un un;

       联合类型的声明与定义和结构体类型相似,但关键词变成了"union"而不是"struct"。

2.联合类型的特点

       联合体中的所有元素共同占用一块空间,也就是说所有元素的起始地址与结构体的地址在数值上是相同的,如图。

 3.联合体的大小计算

       由于联合体中的元素共同使用同一块空间,因此不需要给每个元素开辟不同空间,只需要开辟一块大于或等于联合体中最大的元素的大小的空间即可。但是,联合体的大小必须对齐到联合体中最大对齐数的整数倍(最大对齐数参考结构体)。例如以下联合体的大小为8(字节)。

union Un
{
    char ch[6];
    int i;
}

结束语

       这就是有关自定义类型的内容了,自定义类型特别是结构体在实际代码的应用中十分广泛,可有效地实现较为复杂的逻辑。希望本篇能帮助大家更好地理解这些自定义类型的知识,也欢迎各位大佬指正本文中的错误。