自定义类型

C语言内置类型(C语言自身具有的,可以直接使用的类型)有:int,char,short,long,double……

其余的类型称为自定义类型包括:枚举,结构体,联合,数组

结构体

1.1结构体的基础知识

结构体在定义时不能进行赋值。

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

1.2结构体的声明

struct  tag

{

    member-list;//成员列表

}variable-list;//变量列表

1.3特殊结构体声明

struct    //匿名结构体变量的定义,必须在变量列表中定义变量,之后就无法再定义该类型的变量了(因为没有结构体名)

{

    member-list;

}variable-list;

思考:这样是否可以正常运行

struct

{

    int  a;

    int  b;

    double  c;

}sa;

struct

{

    int  a;

    int  b;

    double  c;

}* ps;

int main()

{

    ps = sa;

    return  0;

}

答案:不能,编译器认为这两个匿名结构体类型不是同一类型,所以是非法的。

1.4结构体的自引用

明确结构体的自引用的概念,首先要了解数据结构

数据结构:数据在内存中储存的结构,包括顺序表,链表,栈,队列,二叉树……其中顺序表和链表是线性数据结构。

数据结构中的栈和内存中的栈区是不能划等号的。

其中链表的实现,是通过地址串联起一块块空间的。所以每一块空间中都会保存下一块空间的地址,最后一块空间的地址为NULL。

如何实现链表?

struct  Node

{

    int  a;

    struct  Node  next;

};

可行吗?

如果可行,那么sizeof(Node)是多少?

正确的实现方式:

struct  Node

{

    int  a;

    struct  Node*  next;

};

typedef  struct

{

    int  a;

    Node*  next;

}Node;

可行吗?

不可行,typedef重命结构体,结构体的成员变量必须是清晰的,而Node本身就是通过typedef重定义的,在定义结构体时Node还不存在。所以这样用是非法的。

合理的使用方式:

typedef  struct  Node

{

    int  a;

    struct  Node*  next;

}Node;

但是使用typedef会导致Node在使用时看不出来是结构体,所以有一些书不推荐使用重定义。

具体情况在进入公司后,公司会有一套自己的代码规范,到时候按上面做就行了。

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

struct Point

{

int x;

int y;

}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。

struct Point p3 = {1, 1};

struct Node

{

int data;

struct Point p;

struct Node* next;

}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.6结构体内存对齐(重点)

用途:计算结构体大小

struct  arr

{

    char  a;

    int  b;

    char  c;

};

sizeof(arr);

a占1字节,b占4字节,c占1字节,sizeof计算应该是6字节,但实际上是12字节;要想理解变量在结构体的内存存储方式,首先要了解offsetof,其本质是一个宏(函数size_t  offsetof(structName , memberName),需要引用头文件<stddef.h>)。返回值是一个结构体成员在其起始位置的偏移量。偏移量单位是字节。偏移量即变量所在位置和结构体起始位置相差的字节数。

int  main()

{

    printf("%d\n",offsetof(struct  arr , a));//0

    printf("%d\n",offsetof(struct  arr , b));//4

    printf("%d\n",offsetof(struct  arr , c));//8

    return  0;

}

所以此时内存空间是这样的

至于为什么会这样,就涉及结构体内存对齐的知识点了

结构体对齐规则:

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

2.其他成员变量要对齐到对齐数的整数倍地址处

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

    vs的默认对齐数为8

    linux环境下没有默认对齐数,对齐数就是成员自身大小

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

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

参考对齐规则再看内存空间的图

a是第一个成员,位于偏移量为0的地址处。b为其他成员变量,对齐到对齐数整数倍的地址处,此时对齐数为4,所以b的起始地址为偏移量为4的地址处,b大小为4字节,占据偏移量为4~7处的空间。c为其他变量,对齐数为1,此时地址偏移量为8,符合条件。abc的对齐数分别为1,4,1。最大对齐数为4,结构体总大小为最大对齐数的整数倍,最符合的大小是12字节,即地址偏移量0~11部分为结构体内存空间。所以sizeof计算得12字节。

所以结构体地址的值就是结构体首元素的地址值。

练练看:

struct S2

{

    char c1;

    char c2;

    int i;

};//大小为8字节,最大对齐数为4

struct S3

{

    double d;

    char c;

    int i;

};//大小为16字节,最大对齐数为8

struct S4

{

    char c1;

    struct S3 s3;//如果出现结构体嵌套结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,已知s3最大对齐数为8

    double d;

};//大小为32字节

为什么存在内存对齐?

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

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

2.性能原因

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

访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次。32位ICU读取内存一次读4字节(地址线一次可以传32位),不对齐的情况要想获得某个数据实际需要的读取次数比理论需要的读取次数多。ciiii   要想获得i的数据要读取两次,绿色部分为第一次读取,黑色部分为第二次读取,理论上只需要读取一次就可以得到i的数据。(c为char类型,i为int类型)

总体来说,结构体的内存访问是用空间换时间的做法。

如何节省结构体的空间?

让小的类型尽量集中在一起。release版本不会对结构体进行优化。

1.7修改默认对齐数

#pragma  pack(1)//括号内是修改的默认对齐数

struct  arr

{

    char  a;

    int  b;

    char  c;

};

#pragma  pack()//恢复默认对齐数

//中间结构体的默认对齐数就被修改了,sizeof计算得6

修改的默认对齐数为2的n次方,n为未知量。

1.8结构体传参

结构体传参的方式有两种:传结构体和传结构体地址

由前面的学习可知,传结构体地址比传结构体更合适(传结构体将是一份结构体的拷贝,造成空间浪费,以及在压栈时有时间和空间上的开销)

位段

2.1什么是位段?

位段的声明和结构体是类似的,有两个不同。

1.位段的声明必须是char,int,unsigned  int或signed  int。

2.位段的成员名后面有一个冒号和一个数字。

例如:

struct  A

{

    int  _a : 2;

    int  _b : 5;

    int  _c : 10;

    int  _d : 30;

}

A就是一个位段类型

位段的位指的是二进制位,变量_a后面的冒号加数字中的数字是比特位的个数

sizeof(struct  A)的值为8

2.2位段的内存分配

1.位段的空间是按照需要4个字节(int)或1个字节(char)的方式来开辟的。

2.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应当避免使用位段

struct  A

{

    //开辟4byte,32个比特位

    int  _a : 2;//剩30比特位

    int  _b : 5;//剩25比特位

    int  _c : 10;//剩15比特位

    //不够30,再开辟4byte

    int  _d : 30;

}

至于_d的30比特位是先使用之前剩下的15个比特位,还是直接使用新开辟的32个比特位C语言标准未规定,所以具体情况取决于编译器。这就是位段涉及的不确定因素。

struct S

{

    char a:3;

    char b:4;

    char c:5;

    char d:4;

};

编译器如果使用前面剩下的则大小为2字节,如果不使用前面剩下的大小为3字节

位段的成员似乎可以不为单一变量,如果不使用单一变量,开辟的空间的时机,规则,一次开辟多少都可能发生变化,具体看编译器。

位段的使用只有在条件非常明确的情况下,如只有00,01,10,11四种情况,那么1字节的空间就可以概况,位段是尽可能的减少空间的使用,不是对空间100%完全利用。

struct S

{

    char a:3;

    char b:4;

    char c:5;

    char d:4;

};

struct S s = {0};

s.a = 10;

s.b = 12;

s.c = 3;

s.d = 4;

内存分配情况:

使用字节空间时,具体是从左向右还是从右向左取决于编译器

00000000

开始

01100010

00000011

00000100

最终

假设字节空间的使用方向为从右往左(从低位到高位的使用)。a赋值10,二进制为……01010,但是只有3个比特位允许使用,截断得010,第一个开辟字节内容变成00000010,b赋值12,二进制为……01100,允许使用的比特位为4个截断得1100,第一个开辟的字节内容为01100010。还剩下1个比特位的空余,c需要的空间不够,新开辟一个字节空间00000000。(假设上次开辟的空间余下的比特位不再使用)c赋值3,二进制为……00011,允许使用的比特位为5个,截断得00011,内存空间内容为00000011,同理d的空间不够,开辟一个字节空间,d赋值4,二进制为……0100,截断得0100,内存空间内容为00000100

2.3位段的跨平台问题

1.int位段被当做有符号数还是无符号数是不确定的

2.int位段中最大位(冒号后面的数字)的数目不能确定(16位机器中最大位是16,32位机器中最大位是32,写成27在16位机器上会出问题)(在早期16位机器上int是2字节)

3.位段中的成员在使用内存空间的方式是从左向右,还是从右向左的标准未定义,具体取决于编译器

4.当开辟的一个空间后,当第二个位段成员比较大,无法容纳于第一个位段剩余的比特位时,是舍弃剩余的位,还是利用,是不确定的。

总结:位段是不支持跨平台的。

2.4位段的应用

举例:在微信等平台上发送“呵呵”是如何发送的?首先确定发送人和发送目标,"呵呵"就是要发送的数据,在数据之上,还封装了其他东西,即表中'数据'以上的部分。如果直接使用结构体包装这些信息,包装的数据相对较大,而网络的空间是有限的。在传输的过程中会出现很多空间的浪费。因此使用位段更合理快捷。

枚举——顾名思义就是一一列举

3.1枚举类型的定义和使用

枚举关键字enum

enum  day

{

    //枚举的成员是枚举的可能取值

    Mon,

    Tues,

    Wed,

    Thur,

    Fri,

    Sat,

    Sun

};

enum  sex

{

    mela = 4,//枚举常量的初识值可以通过赋值修改

    female,//修改后下面的值,相对于修改后的值依次增1,比如male被赋值4后,female的值为5,secret的值为6

    secret

};

//枚举常量是有初始值的

注意:枚举的成员是常量,即使是常量最开始也是有一个值的,所以才可以使用赋值操作符,之后就不能改了。

int  main()

{

    enum  day  d = Sun;

    enum  sex  b = secret;

    printf("%d %d %d" , male , female , secret);

    //在默认情况下(没有修改过)结果为0  1  2,枚举体首个常量量从0开始,向下每次递增1。

    return  0;

}

枚举是一个变量类型,成员常量的默认值是0,1,2。是整型,所以枚举的大小为4字节。

在C语言中,如果enum sex  s = 0;没有问题,但在c++中会报错,0是整型,而s是sex类型,类型不匹配。

3.2枚举的优点

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

枚举的优点:

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

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

3.防止命名污染(放在了大括号里)

4.便于调试(#define在调试的时候标识符已经在编译部分的预处理步骤被替换成常量了)

(test.c在经过编译,链接后变成test.exe(可执行程序),调试的是可执行程序部分)

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

联合(共用体)

4.1联合类型的定义

联合也是一种特殊的自定义类型,这种类型的变量也包含一系列成员,特质是这些成员共用同一块空间(所以也叫共用体)。

union  us

{

    int  a;

    char  b;

};

int  main()

{

    union  us  u;

    printf("%d" , sizeof(u));//结果打印4

    printf("%d" , &us);

    printf("%d" , &(us.a));

    printf("%d" , &(us.b));

    //打印结果发现三个地址是一样的

    return  0;

}

因为a,b的空间使用存在重叠现象,所以a和b在同一时间只能使用一次。

4.2联合的特点

联合的特点是共用同一块空间的,这样一个联合变量的大小,至少是最大成员的大小

联合体判断大小端:

int  check_out()

{

    union  us//匿名亦可

    {

        int  i;

        char  j;

    }u;

    u.i = 1;

    return  u.c;

}

4.3联合大小的计算

1.联合的大小至少是最大成员的大小

2.当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍(数组会用到)

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

    vs的默认对齐数为8

    linux环境下没有默认对齐数,对齐数就是成员自身大小

union  us

{

    char  arr[5];

    int  a;

};

sizeof(union  us)计算的值是8。

原因:char是char类型的数组,对齐数是按char来算的,为1,所以最大对齐数是4。对齐到整数倍后的值就是8。结构体的最大对齐数和联合的相同。数组的对齐数是按元素类型来算的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值