❤️C语言自定义类型的介绍❤️(结构体,枚举,联合体,位段)(上)

简介: 大家好!在C语言中,有个叫“自定义类型”玩意,它究竟是什么呢?其实,就是字面意思,可以自己定义的类型就是自定义类型。具体说就是我们熟知的结构体,枚举,位段,联合体(共用体)。

🌱 1.结构体



🍀🍀1.1结构体概述


🌼🌼🌼1.1.1结构体概念


结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构,它就将不同类型的数据存放在一起,作为一个整体进行处理。


🌼🌼🌼1.1.2 结构体的声明与使用


🌼结构体的声明:

struct Book
{
  char name[20];
  char author[20];
  int price;
};

这个声明描述了一个由两个字符数组和一个int变量组成的结构体。


它将这些变量封装成一个整体,代表了一本书(含有书名,作者名,价格)。


但是注意,它并没有创建一个实际的数据对象,而是描述了一个组成这类对象的元素。


因此,我们有时候也将结构体声明叫做模板,因为它勾勒出数据该如何存储,并没有实例化数据对象。


🌼结构体的使用(一共3种创建方法)


用结构体创建全局变量

struct Book
{
  char name[20];
  char author[20];
  int price;
}b1,b2;
struct Book b1;

这里创建的b1,b2,b3 是完全等价的,都是全局变量。


  1. 用结构体创建局部变量
struct Book
{
  char name[20];
  char author[20];
  int price;
};
int main()
{
  struct Book b4;
  return 0;
}

在main函数中创建的结构体变量就是局部变量


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


🌱1. 初始化:定义变量的同时赋初值。

🌱2. 结构体的初始化要使用大括号。

struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化


🌱3. 结构体嵌套初始化:


在结构体中又包含了一个结构体

struct Point
{
  int x;
  int y;
}p1 = { 1,2 }, p2 = {3,4};
struct Point p3 = { 5,6 };
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
 char name[20];
}n1 = {10, {4,5}, NULL}; 
struct Node n2 = {20, {5, 6}, NULL, "zhangsan"};

🌱4. 对于嵌套结构体的访问

struct Point
{
  int x;
  int y;
}p1 = { 1,2 }, p2 = { 3,4 };
struct Point p3 = { 5,6 };
struct S
{
  int data;
  struct Point p;
  char name[20];
}n1 = { 10, {4,5}, };
struct S n2 = { 20, {5, 6}, "zhangsan" };
int main()
{
  struct Point p4 = {1,2};
  struct S s = { 20,{7,8} };
  printf("%d", s.data);
  printf("%d %d", s.p.x, s.p.y);
  return 0;
}

🌱5. 对于结构体变量中的整型数组如何循环打印:

struct S
{
  int a[10];
}n1 = { {1,2,3} };
int main()
{
  int i = 0;
  struct S s = { {7,8,9} };
  for (i = 0; i < 10; i++)
  {
    printf("%d ", s.a[i]);
  }
  return 0;
}


🌼🌼🌼1.1.3匿名结构体的声明及使用


struct
{
  int a;
  char c;
  double b;
}s1,s2;

匿名结构体可以无结构体的名字,要创建变量的时候只能在大括号的后面直接创建s1,s2


匿名结构体只能使用一次。

struct
{
  int a;
  char c;
  double b;
}s1,s2;//第一个
struct
{
  int a;
  char c;
  double b;
}*ps;//第二个
int main()
{
  ps = &s1;
  return 0;
}

以上创建了两个匿名的结构体类型,但编译器会认为他们是不同的,因此第二个结构体创建的匿名结构体指针无法指向第一个匿名结构体。


非法赋值使编译器报错。

7ea563affca243d891f77a2f0c7605ec.png


🌼🌼🌼1.1.4 结构体的自引用


链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,而链表的实现就需要用到结构体的自引用。

struct Node
{
  int data;
  struct Node* n;
};

上述创建了一个链表,data是数据域,n为指针域。


结构体自引用:能够找到通过地址找到自己同类型的下一个结点。

f97aedb46eb44b239fed1c1942127240.png

🌼🌼练习一:

struct Node
{
 int data;
 struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?

这样不行,会造成死循环,因为无法确定结构体的大小

🌼🌼练习二:

typedef struct
{
 int data;
 Node* next; 
 }Node;
//这样写代码,可行否?

分析如下:定义了一个匿名结构体,并且用typedef将这个匿名结构体重命名为Node,但Node的产生必须要先在内存中创建一个int型,和Node指针大小的内存空间,但是此时还未重命名,所有是错误的


正确写法:

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


🍀🍀 1.2结构体对齐及其大小计算


🌼🌼🌼1.2.1偏移量


定义:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为"有效地址或偏移量"。


先简单举个例子来说:

创建的s1 和s2 结构体变量的内存大小是多少呢:

struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};
int main()
{
  struct S1 s1 = { 'x',100,'y'};
  struct S2 s2 = { 'x','y'100};
  printf("%d", sizeof(struct S1));         //12
  printf("%d", sizeof(struct S2));        //8
  return 0;
}

结果发现两者的内存大小不同,原因是因为涉及了内存对齐。


🌱内存对齐的规则


结构体第一个成员永远放在结构体变量偏移量为0的地址处。

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

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

VS中默认的值为8


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

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


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


🌱为什么存在内存对齐?


平台原因(移植原因):

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

定类型的数据,否则抛出硬件异常。


性能原因:

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

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

问。


总体来说:

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


🌼🌼🌼1.2.2结构体大小计算


🌱对于S1来说:

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

3个变量最大的占4个字节,而VS的默认对齐数是8,因此该结构体的默认对齐数是4

dc0faea2e60845769fd8cf6889d1ad8c.png

🌱对于S1来说:

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

227319b812684ea2b7c7f0c0fa694cf8.png

struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));

68ca71dda16f4d099435f2112c4dc28e.png

因此一共16个字节


🌼🌼🌼1.2.3修改默认对齐数


我们使用#pragma修改默认对齐数

#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\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));

分析:


此时因为默认对齐数被设置成1,最小值,因此所有的对齐数都为1,相当于取消内存对齐,无空间浪费。

689dc23f0db542ac8b36a632751af756.png

🌱百度笔试题:


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

考察: offsetof 宏的实现


<stddef.h> 中定义了个 offsetof(s,m)宏,这个宏用来取得结构体中元素的偏移量很方便,下面是此宏的具体定义:

#define offsetof(s, m) (size_t)&(((s *)0)->m)

offsetof(s, m) 其中,s 是结构体名,m 是它的一个成员。s 和 m 同是宏 offsetof() 的形参,这个宏返回的是结构体 s 的成员 m 在结构体中的偏移地址。


(s *)0 : 这里的用法实际上是欺骗了编译器,使编译器认为 “0” 就是一个指向 s 结构体的指针(地址),即 s 结构体就是位于 0x0 这个地址处。


(s *)0-> m :指向这个结构体的 m 元素。


&((s *)0)->m : 表示 m 元素的地址。这里,如上面所说,因为编译器认为结构体 s 被认为是处于 0x0 地址处,所以 m 的地址自然的就是 m 在 s 中的偏移地址了。


最后将这个偏移值转化为 size_t 类型。

#include<stdio.h>
#include<stddef.h>
#define offsetof(s, m) (size_t)&(((s *)0)->m)
struct S
{
  char c1;
  int a;
  char c2;
};
int main()
{
  printf("%u\n", offsetof(struct S, c1));  //0
  printf("%u\n", offsetof(struct S, a));  //4
  printf("%u\n", offsetof(struct S, c2));  //8
  return 0;
}


🌼🌼🌼1.2.4结构体传参


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; 
}

上面的 print1 和 print2 函数哪个好些?


答案是:首选print2函数。

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。


结论:

结构体传参的时候,要传结构体的地址。


🍀🍀 1.3结构体与位段


🌼🌼🌼1.3.1位段


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


位段的成员必须是 int、unsigned int 或signed int 。

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

位段可以节省空间。

int _a:2;表示_a只需要2个比特位

比如:下方的A就是一个位段类型。那位段A的大小是多少?

struct A 
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};
printf("%d\n", sizeof(struct A));

🌱分析:


位段一次开辟一个整型(4字节==32比特位)

_a + _b + _c一共用了17个比特位,之后的_c不够用了,于是又开辟了4个字节

因此一共8个字节

🌱总结


位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

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

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

🌼🌼🌼1.3.2位段实现结构体


🌱练习:

以下的空间是如何开辟的?

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;

分析:

  1. 由于是char位段,因此是一个字节一个字节开辟的

23123bd9f5714387ad3713a58889c73c.png

e7be9556e8f84e569a2cc7c6a4ee25e5.png

ada1881cc8594c70ac846eb09e9fd42e.png


🌱位段的跨平台问题


int 位段被当成有符号数还是无符号数是不确定的。

位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机

器会出问题。

位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是

舍弃剩余的位还是利用,这是不确定的。


🌱总结:

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


相关文章
|
20天前
|
存储 C语言
如何在 C 语言中实现结构体的深拷贝
在C语言中实现结构体的深拷贝,需要手动分配内存并逐个复制成员变量,确保新结构体与原结构体完全独立,避免浅拷贝导致的数据共享问题。具体方法包括使用 `malloc` 分配内存和 `memcpy` 或手动赋值。
26 10
|
19天前
|
安全 编译器 Linux
【c语言】轻松拿捏自定义类型
本文介绍了C语言中的三种自定义类型:结构体、联合体和枚举类型。结构体可以包含多个不同类型的成员,支持自引用和内存对齐。联合体的所有成员共享同一块内存,适用于判断机器的大小端。枚举类型用于列举固定值,增加代码的可读性和安全性。文中详细讲解了每种类型的声明、特点和使用方法,并提供了示例代码。
17 3
|
19天前
|
存储 大数据 编译器
C语言:结构体对齐规则
C语言中,结构体对齐规则是指编译器为了提高数据访问效率,会根据成员变量的类型对结构体中的成员进行内存对齐。通常遵循编译器默认的对齐方式或使用特定的对齐指令来优化结构体布局,以减少内存浪费并提升性能。
|
24天前
|
编译器 C语言
共用体和结构体在 C 语言中的优先级是怎样的
在C语言中,共用体(union)和结构体(struct)的优先级相同,它们都是用户自定义的数据类型,用于组合不同类型的数据。但是,共用体中的所有成员共享同一段内存,而结构体中的成员各自占用独立的内存空间。
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
32 3
|
4天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
19 6
|
24天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
33 10
|
17天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
23天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7