[深入浅出C语言]浅析自定义类型(篇一)

83 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

前言

        C语言中除了诸如int、char等等的内置类型,还有自定义类型,主要是这几种:结构体、联合、位段和枚举。

        本文就来分享一波作者对自定义类型的学习心得与见解。本篇属于第一篇,主要介绍结构体,后续还有,可以期待一下。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

结构体

什么是结构体

        结构是一些值的集合,这些值称为成员变量。

        结构是一种复合类型,结构的每个成员可以是不同类型的变量。其实可以和数组比较一下,数组是相同类型的元素的集合,而结构体是各种类型(可以不同)元素的集合。

结构的声明

        结构中的成员可以是任何一种C类型,也可以是结构。

struct tag
{
    member-list;
}variable-list;//分号不能丢

比如:

struct any
{
    int a;
    char o[10];
    int*p;
};

        这样就声明了一种结构类型名为struct any,其中包含三个成员。 

struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
}student; 

        可以直接再结构声明后面加上变量名,在声明结构的同时也创建了该结构类型的变量。在这里,struct Stu才是完整的类型名。

        注意:在声明结构的时候只是在创建一种结构数据类型,并没有定义变量。

        结构声明内的成员不能是自己。

struct book
{
    int a;
    struct book poo;
};//无限套娃了,如果要是可以的话占用内存会无穷大

        结构体的自引用是依靠指针实现的。

struct node
{
    int date;
    struct node *next;//指向下一个相同结构的变量
};

        注意别漏了下面花括号后的分号。

        这样写代码,可行吗?

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

        不行。不过这里还有一个typedef将该匿名结构体重命名成了Node,但是不能在这个结构体内部使用这一名字来声明成员,因为typedef重命名是在后面起作用的,而Node* next在重命名的前面,所以是不行的。

解决方案:

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

特殊的声明

        在声明结构的时候,可以不完全的声明。

//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;

struct
{
int a;
char b;
float c;
}a[20], *p;

        上面的两个结构在声明的时候省略掉了结构体标签,这样一来就是一次性声明了,属于匿名结构体,也就是说没有标识,下次还能再用这一类型吗?不能,没有名字怎么使用?

        那么问题来了,在上面代码的基础上,下面的代码合法吗?

p = &x;

        编译器会把上面的两个声明当成完全不同的两个类型。

        所以是非法的。

结构变量的定义

        结构声明只是告诉了编译器我要定义的结构的布局,需要定义变量后才会分配内存空间。

        既可以在声明结构时创建变量(如n),又可以在结构声明以后再创建变量(如roy)。

struct any
{
    int a;
    char o[10];
    int*p;
}n;

struct any roy;
int a;//struct anyint都是数据类型

结构体初始化与内部成员访问

        类同数组,结构体的初始化也可以用花括号括起来,不同变量间用逗号隔开。

        结构体通过 . 访问内部成员,如k.value。实质上与数组用下标访问数组元素是差不多的。

       两个结构体类型中的内部成员名字相同也没关系,因为每个成员属于的是不同的结构体类型。

int a[] = {2, 1, 4};

struct e
{
    char o[];

    int r;
};

struct k
{
    char o[];
    struct e ad; 
    int r;
};

int main()
{
    struct k wu = {"hello", {"world", 456}, 123};//嵌套初始化
    printf("%d",wu.ad.r);
}

        还可以这样写。

struct k wu = {.o = "hello", 
               .ad = {.o = "world",
                      .r = 5}
               .r = 6                 }

结构指针

        结构体的指针,声明方式例如struct guy * him;

        和数组不同的是,结构数组名并不是结构数组首元素的指针,需要在前面加上&,如him = &bay[0]。

        用指针访问成员有两种方法:

        1.使用->,比如,him = &bay[0],那么him->a即为bay[0].a,由指针指向对应结构的某成员。

        2.使用*,比如,him = &bay[0],那么*him == bay[0],则有bay[0].a == (*him).a,注意必须要用()因为。.运算符比*运算符优先级要高。 

结构体内存对齐

         感觉起来内存好像应该是这样算的:

struct e
{
    char o[6];//6

    int r;//4
};

struct k
{
    char o[6];//6
    struct e ad;//10 
    int r;//4
};

int main()
{
    struct k wu = {"hello", {"world", 456}, 123};//20
    printf("%d",sizeof(wu));
    return 0;
}

        可结果却是24!

        为什么呢?

        因为结构体存储遵循一些原则。

对齐规则

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

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

每个成员的对齐数 = 编译器默认的一个对齐数与该成员内存大小的比较值(取较小值)。

(VS中默认为8,Linux没有默认对齐数概念)

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

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

例1

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

        注意计算各成员的对齐数,除了第一个成员以外,其他成员要注意对齐数倍数的偏移量,最后算结构体总大小的时候别忘了对应为最大对齐数的整数倍。

        下面各成员对应不同颜色,灰色的代表未被利用的空间。 

image.png

例2

struct e
{
    char o[6]; //6
    int r; //4
};

        要注意的是,对于数组,其对齐数为数组元素类型的对齐数,比如char o[6]的对齐数就是1。

        下面各成员对应不同颜色,灰色的代表未被利用的空间。  

image.png

例3

struct e
{
    char o[6]; 
    int r; 
};

struct k
{
    char o[6]; 
    struct e ad;
    int r;
};

struct k wu;

        需要注意的是嵌套结构体的内存对齐,应当把内层结构体看成一个整体,对齐数就是它的成员中的最大对齐数, 然后从对应地址处开始对齐,其内部成员的对齐按照相对偏移量进行。

如图:               

image.png

        我们可以用一个宏offsetof来测试一下:

        原型:

offsetof( structName, memberName)

        第一个参数是结构体类型名,第二个参数是结构体成员名,用于计算该成员在对应的结构体中的偏移量大小。

        用它来测一测例3:

#include<stddef.h>
#include<stdio.h>
struct e
{
    char o[6];
    int r;
};

struct k
{
    char o[6];
    struct e ad;
    int r;
};

struct k wu;

int main()
{
    printf("%d\n", offsetof(struct k, o));
    printf("%d\n", offsetof(struct k, ad));
    printf("%d\n", offsetof(struct k, r));

    return 0;
}

        发现都能对的上。

image.png

为什么存在内存对齐

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。

2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。

总而言之,结构体的内存对齐是拿空间来换取时间的做法。  

例子:

image.png

         有内存对齐时(图中上部分),读取成员t只需要访问一次,没有内存对齐时(图中下部分),读取成员t需要读两次。

        那能不能 “站着把钱给挣了” ——设计结构体既满足对齐原则,又节省了空间?

     

  那我们就让占用空间小的成员尽量集中在一起。

struct s1
{
    char i;
    char p;
    int u;
};

struct s2
{
    char i;
    int u;
    char p;

};

        对比一下,明显地s1占用内存更少,假设再定义多两个char类型成员,像s1那样全部放在Int前面,就能减少结构体对齐产生的内存浪费。

image.png

        还有什么办法没?

      修改默认对齐数

#pragma pack(2)//设置默认对齐数为2,一般考虑设置偶数
struct s3
{
    char a;
    int b;
    char c;
};

#prama pack()//取消用户设置,变回默认值8

        如果认为对齐方式不合理,可以通过上面方法自己设置对齐方式。


以上就是本文的全部内容了,感谢观看,你的支持就是对我最大的鼓励~

v2-1a2bf23551e8c0438c155f93aab4b495_720w.jpg