前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C语言进阶-自定义类型:结构体/位段/枚举/联合

C语言进阶-自定义类型:结构体/位段/枚举/联合

作者头像
用户9645905
发布2022-11-30 08:14:57
6930
发布2022-11-30 08:14:57
举报
文章被收录于专栏:Linux学习~

目录

前言

结构体struct

结构体的声明

结构的自引用

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

空结构体大小

结构体内存对齐

修改默认对齐数

宏offsetof

结构体传参

柔性数组

struct与class的区别

位段

什么是位段

位段的内存分配

位段的跨平台问题

枚举enum

枚举类型的定义

枚举的优点

enum 与 #define 的区别

联合union

联合类型的定义

联合的特点

union和大小端

联合大小的计算


前言


本章主要讲解重点:

  • 深入掌握结构体,枚举,联合的使用和特点,以及学会明白位段

结构体struct


  • 定义:

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

结构体的声明

  • 普通声明:
代码语言:javascript
复制
struct tag
{
 member-list;
}variable-list;
  • 特殊的声明:不完全的声明(匿名结构体类型)
  • 示例:
代码语言:javascript
复制
struct
{
 int a;
 char b;
 float c;
}x;
struct
{
 int a;
 char b;
 float c;
}a[20], *p;
  • 注意:

对于匿名结构在声明的时候省略掉了结构体标签(tag),也就是只能在声明的时候进行操作(声明外再次使用无法进行调用(没有名称))

  • 示例:
代码语言:javascript
复制
//在上面代码的基础上,下面的代码为err
p = &x;
//编译器会把上面的两个声明当成完全不同的两个类型

结构的自引用

在链表中我们需要用到的就是结构的自引用

  • 示例:
代码语言:javascript
复制
//创建链表节点
struct Node
{
 int data;
 struct Node* next;
};
  • 易错点:
代码语言:javascript
复制
typedef struct
{
 int data;
 Node* next;
}Node;
//只有在重命名后才能使用重命名名
//正确写法:
typedef struct Node
{
 int data;
 struct Node* next;
}Node;

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

  • 示例1:单类型结构体
代码语言:javascript
复制
struct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
  • 示例2:多类型结构体
代码语言:javascript
复制
struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
  • 示例3:嵌套型结构体
代码语言:javascript
复制
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

空结构体大小

  • 示例:
代码语言:javascript
复制
struct student
{
}stu;
 
int main (void)
{
	printf ("sizeof (stu) = %d\n", sizeof (stu));
	return 0;
}
代码语言:javascript
复制
输出结果:
在C中, sizeof (stu) = 0
在C++中, sizeof (stu) = 1
  • 结论:

对于空结构体不同编译器理解不同,所以大小不一(可能0或者1(作为占位符))

结构体内存对齐

  • 定义:

struct中的各成员变量的存储地址有一套对齐的机制(让CPU能够更舒服地访问变量)

  • 总体来说:

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

  • 原因:

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
  • 对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数为编译器默认的一个对齐数与该成员大小的较小值。(VS中默认的值为8)
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,如果不满足,在最后一个成员后面填充
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
  • 示例1:
代码语言:javascript
复制
struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));
//输出结果:12
  • 解释:

  1. 第一个成员c1在与结构体变量偏移量为0的地址处
  2. 对于c2它的对齐数为4(int大小为4,小于平台默认值8),该变量要对齐到偏移量为4的倍数处,即从偏移量为4的位置开始存放
  3. 对于c3(char类型的对齐数为1,正数都为1的倍数),从偏移量为9的位置开始放
  4. 该结构体的总大小须为最大对齐数(每个成员变量都有一个对齐数)(这里也就是4)的整数倍,故为12(已经占用了9个字节)
  • 示例2:
代码语言:javascript
复制
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));
//输出结果:8
  • 解释:

  1. 第一个成员c1在与结构体变量偏移量为0的地址处
  2. c2放在偏移量为1的地址处
  3. i放在偏移量为4的地址处(对齐到偏移量为对齐数4的倍数处)
  • 示例3:
代码语言:javascript
复制
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
//输出结果:16
  • 解释:

  1. 第一个成员d在与结构体变量偏移量为0的地址处
  2. c放在偏移量为8的地址处
  3. i放在偏移量为12的地址处(对齐到偏移量为对齐数4的倍数处)
  • 示例4:
代码语言:javascript
复制
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));
//输出结果:48
  • 解释:

  1. 第一个成员c1在与结构体变量偏移量为0的地址处
  2. s3放在偏移量为8的地址处(s3最大对齐数为8)
  3. d放在偏移量为24的地址处(对齐到偏移量为对齐数8的倍数处)
  4. 总大小为成员变量最大对齐数的倍数(也就是16的倍数)即大小为32
  • 结论:

尽量让占用空间小的成员尽量集中在一起(既满足对齐,又节省空间)(如示例1与示例2)

修改默认对齐数

使用#pragma 这个预处理指令来改变我们的默认对齐数

  • 示例:
代码语言:javascript
复制
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
 char c1;
 int i;
 char c2;  
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\t", sizeof(struct S1));
    printf("%d\t", sizeof(struct S2));
    return 0;
}
//输出结果:12    6
  • 结论:

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

宏offsetof

  • 作用:

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

代码语言:javascript
复制
#include<stdio.h>
#include<stddef.h>
struct s
{
	char c;
	int i;
	double d;
};
int main()
{
	// offsetof其实是一个宏,用来表示成员相对于结构体的偏移量
    //而且offsetof的参数传的是一个类型,更加说了offsetof是一个宏
	printf("%d\n", offsetof(struct s, c));// 0
	printf("%d\n", offsetof(struct s, i));// 4
	printf("%d\n", offsetof(struct s, d));// 8
	return 0;       
}

结构体传参

  • 示例:
代码语言:javascript
复制
struct S
{
 int data[1000];
 int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
//创建临时结构体来接收(消耗空间)
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//结构体地址传参
//指针接收
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s);  //传结构体
 print2(&s); //传地址
 return 0;
}
  • 结论:

其实效果都一样,两者都可以选择,但是推荐结构体传址

  • 原因:

  1. 函数传参的时候,参数是需要压栈的,压栈会占用空间
  2. 如果传递一个结构体对象的时候,结构体过大的话,那么参数压栈的的系统开销比较大,会导致性能下降

柔性数组

  • 定义:

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员

  • 使用:

  1. sizeof 返回的这种结构大小不包括柔性数组的内存
  2. 用malloc()函数进行内存动态分配,分配的内存应该大于结构的大小,以适应柔性数组的预期大小
  3. 用malloc函数分配了内存,肯定就需要用free函数来释放内存
  • 示例:
代码语言:javascript
复制
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
typedef struct data
{
    int len;    //一般用来表示字符数组的字符个数 
    char name[];//空间大小为0
}S; 
int main(void)
{
    S s;
    printf("sizeof(s)=%d\n",sizeof(s));//输出为4,即是int类型大小
    int len = 10; //申请空间 
    struct data *p =(struct data*)malloc(sizeof(s)+sizeof(char)*len);
    //判断是否申请成功&请空处理 
    p->len = len;
    strcpy(p->name,"xxxxxx"); //字符串赋值需要用strcpy
    printf("%s\n",p->name); 
    //释放指针p
    free(p);
    return 0;
}
//输出结果:xxxxxx

struct与class的区别

在C++里struct关键字与class关键字一般可以通用

  • 只有一个很小的区别:

struct的成员默认情况下属性是public的,而class成员却是private的

位段


什么是位段

位段的声明和结构是类似的

  • 有两个不同:

  1. 位段的成员必须是 int、unsigned int 或signed int
  2. 位段的成员名后边有一个冒号和一个数字
  • 示例:
代码语言:javascript
复制
struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

位段的内存分配

  • 说明:

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型 
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
  • 示例:
代码语言:javascript
复制
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;
  • 对于上述空间是如何开辟的:

注:在vs平台下,其他平台不确定(可以自己进行测试)

  1. 首先对于char类型会开辟一个字节空间(8bite)
  2. 在这一个空间里先从高地址开始使用(从右边开始)
  3. 当存入的数据大于相应的空间(冒号后的数字表示属于该变量的空间大小,单位为bite)可以表示的数据大小时,会发生截断(从右边开始)
  4. 对于一个字节里还剩的空间,如果后面的位段空间能够放入时,将存入这一个字节里还剩的空间里
  5. 不够时将另开辟一个对应类型的空间来存放

位段的跨平台问题

  • 存在问题:

  1. int 位段被当成有符号数还是无符号数是不确定的
  2. 位段中最大位的数目不能确定(例如:16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
  • 结论:

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

枚举enum


  • 定义:

枚举型是一个集合(可以一一列举的类似元素),其元素(枚举成员)是一些命名的整型常量(元素之间用逗号隔开)

枚举类型的定义

  • 示例:
代码语言:javascript
复制
enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
};
enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};
  • 注意:
  1. 以上定义的 enum Day , enum Sex , enum Color 都是枚举类型
  2. {}中的内容是枚举类型的可能取值,也叫枚举常量
  3. 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1(可以人为设定枚举成员的值,从而自定义某个范围内的整数)
  • 示例:
代码语言:javascript
复制
enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};

枚举的优点

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

  • 示例:程序中为某些整数定义一个别名

预处理指令#define:

代码语言:javascript
复制
#define MON  1
#define TUE  2
#define WED  3
#define THU  4
#define FRI  5
#define SAT  6
#define SUN  7

枚举类型能完成同样的工作(更加简洁便捷):

代码语言:javascript
复制
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
  • 总结枚举的优点:
  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨
  3. 防止了命名污染(封装)
  4.  便于调试
  5. 使用方便,一次可以定义多个常量

enum 与 #define 的区别

  • 作用的时期:

#define是在预处理阶段直接进行替换,并且不进行类型检查, 枚举则是在程序运行之后才起作用

  • 储存位置:

#define定义的类型存储在代码段 枚举常量存储在数据段的静态存储区里

  • 赋值类型:

#define可以赋值多种类型数据 枚举变量的大小只能为整型数据(例如:0、1、2…)(enum当我们不主动对它进行赋值时,第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1,#define则不会)

  • 使用定义:

#define宏一次只能定义一个 枚举可以一次定义大量相关的常量

  • 调试:

一般在编译器里,可以调试枚举常量,但是不能调试宏常量

  • 定义类型:

枚举量具有类型,宏没有类型;枚举常量属于常量,宏定义不是常量

联合union


  • 定义:

联合也称为共用体,很明显意思是多个变量共用一个空间,所以不能同一时间使用多个变量

联合类型的定义

  • 示例:
代码语言:javascript
复制
//联合类型的声明
union Un
{
 char c;
 int i;
};
//联合变量的定义
union Un un;

联合的特点

  • 联合变量的大小:

因为联合的成员是共用同一块内存空间的,所以联合变量至少是最大成员的大小(联合至少得有能力保存最大的那个成员)

  • 示例:
代码语言:javascript
复制
//在上述代码的基础上 计算联合变量的大小
printf("%d\n", sizeof(un));
//输出结果:4
  • 联合成员的空间使用:

任何成员变量都是从低地址开始使用

  • 示例:
代码语言:javascript
复制
union Un
{
 int i;
 char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
//相同 都是从联合变量的低地址开始使用
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//0x11223355

union和大小端

  • 经典面试题:

判断当前计算机的大小端存储

代码语言:javascript
复制
#include<stdio.h>
union var{
        char c[4];
        int i;
};
int main(){
        union var data;
        data.c[0] = 0x04;//因为是char类型,值对应ascii
        data.c[1] = 0x03;//16进制便于直接与内存中的值对比
        data.c[2] = 0x02;
        data.c[3] = 0x01;
//数组先使用低地址再使用高地址,内存内容依次为:04,03,02,11(共四字节)
//而把四个字节作为一个整体,对于小端来说:低地址放在低权位
//读取出来则是:0x01020304
//反之则是大端存储模式
        printf("%x\n",data.i);//共用空间
}

联合大小的计算

  • 规则:
  1. 联合的大小至少是最大成员的大小
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
  • 示例:
代码语言:javascript
复制
union Un1
{
 char c[5];
 int i;
};
union Un2
{
 short c[7];
 int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
//输出结果:8
printf("%d\n", sizeof(union Un2));
//输出结果:16
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-09-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 结构体struct
    • 结构体的声明
      • 结构的自引用
        • 结构体变量的定义和初始化
          • 空结构体大小
            • 结构体内存对齐
              • 修改默认对齐数
                • 宏offsetof
                  • 结构体传参
                    • 柔性数组
                      • struct与class的区别
                      • 位段
                        • 什么是位段
                          • 位段的内存分配
                            • 位段的跨平台问题
                            • 枚举enum
                              • 枚举类型的定义
                                • 枚举的优点
                                  • enum 与 #define 的区别
                                  • 联合union
                                    • 联合类型的定义
                                      • 联合的特点
                                        • union和大小端
                                          • 联合大小的计算
                                          领券
                                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档