❤️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位机

器会出问题。

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

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

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


🌱总结:

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


相关文章
|
13天前
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
78 14
|
17天前
|
存储 编译器 C语言
【C语言】结构体详解 -《探索C语言的 “小宇宙” 》
结构体通过`struct`关键字定义。定义结构体时,需要指定结构体的名称以及结构体内部的成员变量。
76 10
|
22天前
|
存储 数据建模 程序员
C 语言结构体 —— 数据封装的利器
C语言结构体是一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个整体。它支持数据封装,便于管理和传递复杂数据,是程序设计中的重要工具。
|
28天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
98 13
|
C语言
《C语言及程序设计》实践项目——枚举应用
返回:贺老师课程教学链接 【项目1-对称点】 设计函数,可以按指定的方式,输出一个平面点的对称点 下面给出枚举类型定义和main函数(测试函数),请写出output函数的实现。 #include&lt;stdio.h&gt; enum SymmetricStyle {axisx, axisy, point};//分别表示按x轴, y轴, 原点对称三种方式 void
1094 0
|
17天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
39 10
|
17天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
41 9
|
17天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
31 8
|
17天前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
40 6
|
17天前
|
存储 C语言
【C语言】输入/输出函数详解
在C语言中,输入/输出操作是通过标准库函数来实现的。这些函数分为两类:标准输入输出函数和文件输入输出函数。
101 6