【进阶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函数。

原因:


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

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


结论:

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

相关文章
|
10天前
|
存储 C语言
C语言程序设计核心详解 第九章 结构体与链表概要详解
本文档详细介绍了C语言中的结构体与链表。首先,讲解了结构体的定义、初始化及使用方法,并演示了如何通过不同方式定义结构体变量。接着,介绍了指向结构体的指针及其应用,包括结构体变量和结构体数组的指针操作。随后,概述了链表的概念与定义,解释了链表的基本操作如动态分配、插入和删除。最后,简述了共用体类型及其变量定义与引用方法。通过本文档,读者可以全面了解结构体与链表的基础知识及实际应用技巧。
|
16天前
|
C语言
c语言中的结构体
本文档详细介绍了C语言中结构体的使用方法,包括结构体的基本定义、变量声明与赋值、数组与指针的应用,以及结构体嵌套、与`typedef`结合使用等内容。通过示例代码展示了如何操作结构体成员,并解释了内存对齐的概念。
|
22天前
|
C语言
C语言结构体赋值的四种方式
本文总结了C语言结构体的四种赋值方式,并通过示例代码和编译运行结果展示了每种方式的特点和效果。
34 6
|
1月前
|
编译器 程序员 Linux
【C语言篇】结构体和位段详细介绍
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
|
1月前
|
存储 编译器 C语言
【C语言篇】自定义类型:联合体和枚举详细介绍
像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。
|
1月前
|
存储 C语言
C语言------结构体和共用体
这篇文章是关于C语言中结构体和共用体的实训,通过示例代码演示了结构体的定义、赋值、使用,以及如何使用结构体变量进行数据的组织和操作,包括输入、排序、求平均分和查找学生信息等功能。
C语言------结构体和共用体
|
7天前
|
存储 Serverless C语言
【C语言基础考研向】11 gets函数与puts函数及str系列字符串操作函数
本文介绍了C语言中的`gets`和`puts`函数,`gets`用于从标准输入读取字符串直至换行符,并自动添加字符串结束标志`\0`。`puts`则用于向标准输出打印字符串并自动换行。此外,文章还详细讲解了`str`系列字符串操作函数,包括统计字符串长度的`strlen`、复制字符串的`strcpy`、比较字符串的`strcmp`以及拼接字符串的`strcat`。通过示例代码展示了这些函数的具体应用及注意事项。
|
10天前
|
存储 C语言
C语言程序设计核心详解 第十章:位运算和c语言文件操作详解_文件操作函数
本文详细介绍了C语言中的位运算和文件操作。位运算包括按位与、或、异或、取反、左移和右移等六种运算符及其复合赋值运算符,每种运算符的功能和应用场景都有具体说明。文件操作部分则涵盖了文件的概念、分类、文件类型指针、文件的打开与关闭、读写操作及当前读写位置的调整等内容,提供了丰富的示例帮助理解。通过对本文的学习,读者可以全面掌握C语言中的位运算和文件处理技术。
|
10天前
|
存储 C语言
C语言程序设计核心详解 第七章 函数和预编译命令
本章介绍C语言中的函数定义与使用,以及预编译命令。主要内容包括函数的定义格式、调用方式和示例分析。C程序结构分为`main()`单框架或多子函数框架。函数不能嵌套定义但可互相调用。变量具有类型、作用范围和存储类别三种属性,其中作用范围分为局部和全局。预编译命令包括文件包含和宏定义,宏定义分为无参和带参两种形式。此外,还介绍了变量的存储类别及其特点。通过实例详细解析了函数调用过程及宏定义的应用。
|
16天前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。