【进阶C语言】自定义类型:结构体,枚举,联合(一)

简介: 【进阶C语言】自定义类型:结构体,枚举,联合(一)

前言


作者简介:热爱跑步的恒川,正在学习C/C++、Java、Python等。

本文收录于C语言进阶系列,本专栏主要内容为数据的存储、指针的进阶、字符串和内存函数的介绍、自定义类型结构、动态内存管理、文件操作等,持续更新!

相关专栏Python,Java等正在发展,拭目以待!


1. 结构体


1.1 结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。注意:之前学过的数组里的每个成员是相同类型的变量。


1.2 结构的声明

struct tag//tag结构体的名字
{
     member-list;
}variable-list;


例如描述一个学生:

struct Stu
{
    //成员变量
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
}; //分号不能丢
//定义学生类型
struct Stu
{
  //成员变量
  char name[20];
  int age;
  float weight;
} s4, s5, s6;//s4、5、6都是学生//全局变量
int main()
{
  //int num = 0;
  //我们用类型创建变量,s1是第一个学生,s2是第二个学生,s3是第三个学生
  struct Stu s1;//局部变量
  struct Stu s2;
  struct Stu s3;
  return 0;
}


1.3 特殊的声明

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

比如:

//匿名结构体类型
struct 
{
  char c;
  int a;
  double d;
}s1;
struct
{
  char c;
  int a;
  double d;
}* ps;
int main()
{
  //ps = &s1;//err
  return 0;
}


上面的两个结构在声明的时候省略掉了结构体标签(stu)。

那么问题来了?

//在上面代码的基础上,下面的代码合法吗?
ps = &s1;


警告:

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

所以是非法的。


1.4 结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

我想将这样一个链表连接起来,那要怎么样连接呢?

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


答案是当然不可行的


正确的自引用方式:

struct Node
{
  int data;//4
  struct Node* next;//4/8
};
int main()
{
  struct Node n1;
  struct Node n2;
  n1.next =  &n2;
  return 0;
}


当一个结构体可以找到另外一个跟自己同类型的结构体的时候就可以用这种方法,自己的类型包含一个自己的变量是绝对不行的,而应该是自己类型包含一个自己类型的指针才是可行的


拓展一个新的知识点

typedef struct
{
  int data;
  char c;
} S;


typedef可以对一个匿名结构体重命名定义一个新的名字,这个S不是一个变量名而是一个类型名


在思考一个问题

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


这样行不行?


答案是不可以,结构体是要有一个先后顺序才行,先进行大括号里面的内容,后看大括号外面的,这里的Node还没有重命名就使用时错误的,如果你硬想这样使用,那应该这样写

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


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

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

变量的定义:

struct S
{
  int a;
  char c;
}s1;//全局变量
struct S s3;//全局变量
int main()
{
  struct S s2;//局部变量
  return 0;
}


这样写行不行?


当然是可行的


初始化:定义变量的同时赋初值

struct S
{
  int a;
  char c;
}s1;
struct B
{
  float f;
  struct S s;
};
int main()
{
  //int arr[10] = {1,2,3};
  //int a = 0;
  struct S s2 = {100, 'q'};
  struct S s3 = {.c = 'r', .a = 2000};//用.操作可以指定我的顺序,其他都是默认顺序
  struct B sb = { 3.14f, {200, 'w'}};//结构体里面含有结构体要再用一个大括号
  printf("%f,%d,%c\n", sb.f, sb.s.a, sb.s.c);
  return 0;
}


含有指针的的结构体初始化

struct S
{
  char name[100];
  int* ptr;
};
int main()
{
  int a = 100;
  struct S s = {"abcdef", NULL};
  return 0;
}


1.6 结构体内存对齐

我们已经掌握了结构体的基本使用了。

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

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

直接上代码

struct S1
{
  int a;
  char c;
};
struct S2
{
  char c1;
  int a;
  char c2;
};
struct S3
{
  char c1;
  int a;
  char c2;
  char c3;
}; 
int main()
{
  //探讨到底是下面三个答案的哪个
  //5 6 7
  //8 8 8
  //8 12 12
  printf("%d\n", sizeof(struct S1));
  printf("%d\n", sizeof(struct S2));
  printf("%d\n", sizeof(struct S3));
  return 0;
}


答案却是 8 12 12,这是为什么呢?


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


结构体的第一个成员永远都放在0偏移处

从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍处

这个对齐数是:成员自身大小和默认对齐数的较小值

备注:

VS环境下 没有默认对齐数,没有默认对齐数时,对齐数就是成员自身的大小

当成员全部存放进去后

结构体的总大小必须是,所以成员的对齐数中最大对齐数的整数倍

如果不够,则浪费空间对齐。

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

当换一下位置时:

如果不相信可以求一下他们的偏移量

求偏移量需要用到一个offsetof宏

#include <stddef.h>
struct S
{
  char c;
  int a;
};
int main()
{
  struct S s = {0};
  printf("%d\n", offsetof(struct S, c));//0
  printf("%d\n", offsetof(struct S, a));//4
  return 0;
}


当了解这个规则后,我们再练习一个

struct S2
{
  char c1;
  char c2;
  int i;
};
int main()
{
  printf("%d\n", sizeof(struct S2));
  return 0;
}


图片讲解:

再练习一个

struct S3
{
  double d;
  char c;
  int i;
};


再练习一个结构体嵌套的问题

struct S3
{
  double d;
  char c;
  int i;
};
struct S4
{
  char c1;
  struct S3 s3;
  double d;
};
int main()
{
  printf("%d\n", sizeof(struct S4));
  return 0;
}


图片讲解:

为什么存在内存对齐?

不存在内存对齐的样子:

存在内存对齐的样子:

大部分的参考资料都是如是说的:


平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因:

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

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


总体来说:


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


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


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

//例如:
struct S1
{
  char c1;
  int i;
  char c2;
};
struct S2
{
  char c1;
  char c2;
  int i;
};


S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。


1.7 修改默认对齐数

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

#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()
{
  //输出的结果是什么?//结果为6
  printf("%d\n", sizeof(struct S1));
  printf("%d\n", sizeof(struct S2));
  return 0;
}


1.8 结构体传参

直接上代码:

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函数。

原因:


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

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


结论:

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

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