C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(上)

简介: C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)

本篇将对C语言自定义类型进行讲解

1.结构体(struct)

前面简单讲过结构体,这里将会把前面结构体还没讲完的知识继续补充。复习链接:

1.1 结构的基础知识

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

1.2 结构的声明

 
struct tag  //结构体关键字+标签  合起来是结构体类型
{
    member - list;   //成员变量列表
}variable - list;   //变量列表

例如描述一个学生:

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

1.3 匿名结构体

//匿名结构体类型(一次性使用)

 
struct
{
    int a;
    char b;
    float c;
    double d;
} s;
 
struct
{
    int a;
    char b;
    float c;
    double d;
} *ps;

对于上面的代码如果进行如下操作,是非法的

 
int main()
{
    ps = &s; // error
    return 0;
}

1.4 结构的自引用

介绍:结构体中包含一个类型为该结构体本身的成员,包含同类型的结构体指针(不是包含同类型的结构体变量)

 
struct A
{
    int i;
    char c;
};
 
struct B
{
    char c;
    struct A sa;
    double d;
};

注意事项1:结构体不能自己包含自己,不能包含同类型的结构体变量

 
struct N
{
    int d;
    struct N n; //结构体里不能存在结构体自己类型的成员
};

为了加深理解,先引入一下数据结构的一些知识:

注意事项2:结构体自引用时,不要用匿名结构体:

 
struct  // 如果省略结构体名字
{
    int data;
    struct Node* next; // 这里的 struct Node* 是哪里来的?
};

即使使用 typedef 重新取名为 Node,也是不行的。因为要产生 Node 必须先有结构体类型之后才能重命名 Node,即先 Node* next 定义完成员之后才 typedef 才能对这个类型重命名为 Node。

所以这种方式仍然是不行的:

 
typedef struct
{
    int data;
    Node* next; // 先有鸡还是先有蛋???
} Node;

正确方法:

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

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

有了结构体类型,那如何定义变量,其实很简单。

 
struct S
{
    char c;
    int i;
} s1, s2; // 声明类型的同时创建变量
 
int main()
{
    struct S s3, s4;
 
    return 0;
}

创建变量的同时赋值(初始化)

 
struct S
{
    char c;
    int i;
} s1, s2;
 
int main()
{
    struct S s3 = {'x', 20};
//                  c    i
 
    return 0;
}

结构体包含结构体的初始化方法

 
struct S
{
    char c;
    int i;
} s1, s2;
 
struct B
{
    double d;
    struct S s;
    char c;
};
 
int main()
{
    struct B sb = {3.14, {'w', 100}, 'q'};
    printf("%lf %c %d %c\n", sb.d, sb.s.c, sb.s.i, sb.c);
    
    return 0;
}

1.6 结构体内存对齐

现在已经掌握了结构体的基本使用了。

深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点: 结构体内存对齐

首先得掌握结构体的对齐规则:

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

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

对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的值为8)

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

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整

体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

 
//练习1
#include <stdio.h>
struct S
{
     char c1;
     int i;
     char c2;
};
int main()
{
    struct S s = {0};
    printf("%d\n", sizeof(s));//12
    return 0;
}

d71165441c954d48a56e90802fb82d14.png

 
//练习2
struct S2
{
    char c1;
    char c2;
    int i;
};
printf("%d\n", sizeof(struct S2));//8
 
//练习3
struct S3
{
    double d;//8
    char c;//1
    int i;//4
};
printf("%d\n", sizeof(struct S3));//16
 
//练习4-结构体嵌套问题
#include <stdio.h>
struct S4
{
    double d;
    char c;
    int i;
};
struct S5
{
    char c1;
    struct S4 s4;
    double d;
};
int main()
{
    struct S5 s5 = {0};
    printf("%d\n", sizeof(s5));
    return 0;
}
//上面第4点: 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
//结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
//此题嵌套的结构体对齐到自己的最大对齐数就是8(所以浪费了下面的7个字节)且32是8的倍数

cb442a1d516c479ab65048040b9bddbe.png 为什么存在内存对齐? 大部分的书籍都是这样说的:

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

不是所有的硬件平台都能访问任意地址上的任意数据的;

某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因

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

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

而对齐的内存访问仅需要一次访问。

总体来说:

结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:

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

 
//例如:
struct S1
{
    char c1;
    int i;
    char c2;
};
 
struct S2
{
    char c1;
    char c2;
    int i;
};
//S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
//S1大小是12 而S2大小是8

1.7 修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里再次使用,可以改变默认对齐数。

 
#include <stdio.h>
// 默认对齐数是8
#pragma pack(2) // 把默认对齐数改为2(一般改为2的几次方)
struct S
{
    char c1; //1
    int i; // 4
    char c2; // 1
};
#pragma pack() // 取消
int main()
{
    printf("%d\n", sizeof(struct S)); //修改默认对齐数后12变为8
    return 0;
}

c8a1f24c6c344813ad7aaf98ab972c78.png

所以在结构在对齐方式不合适的时候,可以自己更改默认对齐数。


百度笔试题:

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明

注:这里还没学习宏,可以放在宏讲解完后再自己实现。

考察: offsetof 宏的实现

该宏用于求结构体中一个成员在该结构体中的偏移量。

头文件: stddef.h

使用方法演示:

 
#include <stdio.h>
#include <stddef.h>
struct S
{
    char c1; //1
    int i; // 4
    char c2; // 1
};
int main()
{
    printf("%d\n", offsetof(struct S, c1));//0
    printf("%d\n", offsetof(struct S, i));//4
    printf("%d\n", offsetof(struct S, c2));//8
    return 0;
}

C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(中):https://developer.aliyun.com/article/1513097

目录
相关文章
|
8天前
|
算法 Java 程序员
面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性
【6月更文挑战第15天】面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性。封装可使用结构体封装数据和方法,如模拟矩形对象。继承则通过结构体嵌套实现静态继承。多态可通过函数指针模拟,但C不支持虚函数表,实现复杂。C语言能体现OOP思想,但不如C++、Java等语言原生支持。
27 7
|
9天前
|
编译器 C语言 C++
【C语言基础】:自定义类型(二) -->联合和枚举
【C语言基础】:自定义类型(二) -->联合和枚举
|
9天前
|
编译器 C语言
【C语言基础】:自定义类型(一)--> 结构体-2
【C语言基础】:自定义类型(一)--> 结构体
|
9天前
|
编译器 Linux C语言
【C语言基础】:自定义类型(一)--> 结构体-1
【C语言基础】:自定义类型(一)--> 结构体
|
2天前
|
编译器 C语言
C语言----自定义类型:联合和枚举
C语言----自定义类型:联合和枚举
|
2天前
|
C语言
C语言---自定义类型:结构体(4)
C语言---自定义类型:结构体
|
2天前
|
编译器 C语言
C语言---自定义类型:结构体(3)
C语言---自定义类型:结构体
|
2天前
|
编译器 Linux C语言
C语言---自定义类型:结构体(2)
C语言---自定义类型:结构体
|
2天前
|
C语言
C语言---自定义类型:结构体(1)
C语言---自定义类型:结构体
12 0
|
2天前
|
C语言 C++
C语言----C语言内存函数
C语言----C语言内存函数