C语言进阶-自定义类型:结构体、枚举、联合(1)

简介: C语言进阶-自定义类型:结构体、枚举、联合

1.结构体

1.1结构的基本知识

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

我们前面说数组是一组相同类型元素的集合,而结构体中的每个成员可以使不同类型的变量。

1.2结构体的声明

例如,描述一个学生:

struct Stu
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
};//分号不能丢

我们也可以创建结构体变量s1,s2,s3:

法一:

struct Stu
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
}s1,s2,s3;//分号不能丢

法二:

#include<stdio.h>
struct Stu
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
};//分号不能丢
int main()
{
  struct Stu s1, s2, s3;
  return 0;
}

1.3特殊的声明

在声明结构体时,可以不完全的声明,这种也可以叫匿名结构体类型。

如下所示:

struct
{
  int a;
  char c;
  float f;
}s1,s2;

在声明结构体时,结构体类型名被省略了,注意此时要定义一个结构体变量只能使用法一的方法,不能用法二的方法。

下面我们再来创建一个和上面一样的匿名结构体类型,并将它定义为指针,那编译器会不会认为这两个结构体类型是一样的呢?

#include<stdio.h>
struct
{
  int a;
  char c;
  float f;
}x;
struct
{
  int a;
  char c;
  float f;
}*p;
int main()
{
  p = &x;
  return 0;
}

运行:

可以看到,编译器报错了,两种类型不兼容,所以说,虽然我们声明的结构体类型看似相同,但是编译器不认为它们是一种类型。所以一般情况下,匿名结构体只能使用一次。

1.4结构的自引用

在这之前,我们先来了解一些数据结构的知识。

顺序表是顺序存储的,链表是通过节点1找到节点2,然后根据节点2找到节点3.......,那要怎样实现链表呢?

有人说,只要通过这个节点能找到下一个节点就行了,那我们就把自己包含在自己里面就能一直找下去啊,这种做法行吗?

如下面代码所示:

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

显然是不行的,大家考虑一下,如果使用上述方法,sizeof(struct Node)该怎么计算?根本无法计算,因为第一个next里面包含data和next,下一个这个next又包含data和next,这样一直下去,结构体类型的大小根本计算不了,所以这种方法行不通。

其实这里使用结构体指针就能解决了,只要把节点2的地址存放在节点1里面,节点3的地址存放在节点2里面........就能根据地址找到后续的节点。

struct Node
{
  int data;//数据域
  struct Node* next;//指针域
};

这就叫做结构体的自引用。

下面我们再来思考一个问题:

下列对匿名结构体类型进行重命名,然后在结构体内使用它重命名后的类型名,这种方式正确吗?

typedef struct 
{
  int data;
  Node* next;
}Node;
int main()
{
  Node n = { 0 };
  return 0;
}

这种方式是不对的,因为我们在对匿名结构体进行重命名为Node之前,这个结构体类型应该是已经存在的,但是我们还没有命名为Node呢,它在结构体内部就提前使用了Node*,这明显就不对,就像是先有蛋还是先有鸡的问题。

正确的写法应该是这种,不要用匿名结构体类型:

typedef struct Node 
{
  int data;
  struct Node* next;
}Node;
int main()
{
  Node n = { 0 };
  return 0;
}

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

结构体的定义:上文中也讲过

#include<stdio.h>
struct SN
{
  char c;
  int i;
}sn1,sn2;//全局变量
int main()
{
  struct SN sn3, sn4;//局部变量
  return 0;
}

结构体变量的初始化:

#include<stdio.h>
struct SN
{
  char c;
  int i;
}sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量
int main()
{
  //struct SN sn3, sn4;//局部变量
  printf("%c %d", sn2.c, sn2.i);
  return 0;
}

上述代码对sn1,sn2进行初始化及打印。

运行结果:

当然,我们的结构体内部也可以出现结构体变量:

#include<stdio.h>
struct SN
{
  char c;
  int i;
}sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量
struct S
{
  double d;
  struct SN sn;
  int arr[10];
};
int main()
{
  struct S s = { 3.14,{'w',10},{1,2,3} };
  printf("%lf %c %d\n", s.d, s.sn.c, s.sn.i);
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", s.arr[i]);
  }
  return 0;
}

运行结果:

1.6结构体内存对齐

我们现在已经掌握了结构体的基本使用,现在我们来深入讨论一个问题:计算结构体的大小。

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

先来看下面这段代码:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct s1
{
  char c1;
  int i;
  char c2;
};
struct s2
{
  int i;
  char c1;
  char c2;
};
int main()
{
  printf("%zd\n", sizeof(struct s1));
  printf("%zd\n", sizeof(struct s2));
  return 0;
}

大家觉得结果是什么呢?

直接来看吧:

是不是和预想的结果差距很大,那这又是为什么呢?

这就不得不提一下结构体的对齐规则了:

1.第一个成员变量在与结构体变量偏移量为0的地址处。

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

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

    VS中对齐数默认为8

    Linux中没有默认对齐数,对齐数就是成员自身的大小

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

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

了解了结构体对齐规则,我们来分析一下上述代码:

先来看s1,根据第一条规则,第一个成员char c1放在相对于结构体变量偏移量为0的地址处,

第二个成员int i根据第二条规则,要对齐到对齐数的整数倍的地址处,VS默认的对齐数是8,i自身的大小是4,所以对齐数应该是4,所以i应该从地址为4的位置往后占用4个字节,

第三个成员char c2,它的对齐数是1,而任何数都是1的整数倍,所以接着上一个成员地址往下存一个字节,

现在三个成员占用到9个字节的空间,而根据规则3,结构体总大小是最大对齐数的整数倍,c1的对齐数是1,i的对齐数是4,c2的对齐数是1,所以最大对齐数是4,9显然不是4的整数倍,所以要往后浪费3个字节的空间到12,所以最终打印结果是12。

我们来看结构体struct s1在内存中的存储:

同理,结构体struct s2也是参考上述对齐规则:

 

下面我们再来看一个例题:

#include<stdio.h>
struct s3
{
  double d;
  char c;
  int i;
};
int main()
{
  printf("%zd\n", sizeof(struct s3));
  return 0;
}

自己试着做一下

下图是答案:

以上是对对齐规则前3个的使用,下面我们来看看,对齐规则第4个规则该怎么使用:

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

如果我们在结构体struct s4中嵌套一个结构体struct s3,那struct s4的大小是多少?

#include<stdio.h>
struct s3
{
  double d;
  char c;
  int i;
};
struct s4
{
  char c1;
  struct s3 s3;
  double d;
};
int main()
{
  printf("%zd\n", sizeof(struct s4));
  return 0;
}

结果应该是:32

首先,第一个成员c1存储在偏移量为0的地址,而第二个成员s3是个结构体,按照规则4,嵌套的结构体对齐到自己的最大对齐数的整数倍处,上文中struct s3中的最大对齐数是8,那s3就应该从地址8往后占用16个字节,到地址23,24恰好是第三个成员d的对齐数8的整数倍,所以d应该从地址24往后占用8个字节空间,此时结构体一共占用32个字节的空间,32恰好是所有对齐数中最大对齐数8的整数倍,所以结构体最终的大小就是32。

那什么存在内存对齐呢?

1.平台原因(移植原因)

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

2.性能原因:

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

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

例如:我们要存一个char型和一个int 型的数据,(假设是32位机器,一次读取4个字节),如果没有对齐,那我们从char开始读,第一次只能读到int型的3个字节,还得读一次才能凑够4个字节:

如果对齐了,char型在存储时会浪费掉3个字节,然后再存储int型数据,这样我们只需读一次就能得到int型的数据:

总的来说,结构体的对齐是拿空间换取时间的做法。

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

这个可以看看我们之前s1和s2,

struct s1
{
  char c1;
  int i;
  char c2;
};
struct s2
{
  int i;
  char c1;
  char c2;
};

成员相同,写法不同,结构体的大小不同,分别是12和8,由此可以总结出,在设计结构体的时候,要想既满足对齐,又要节省空间,把占用空间小的成员尽量写在一起即可

1.7修改默认对齐数

之前我们见过了#pragma这个预处理命令,这里我们可以用它来修改默认对齐数:

#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
  char c1;
  int i;
  char c2;
};
#pragma pack()//取消修改的默认对齐数,还原为默认
int main()
{
  printf("%zd\n", sizeof(struct s1));
  return 0;
}

修改默认对齐数为1后,结构体成员的存储应该是挨着存储的,那打印结果就是6。

一般我们在设置默认对齐数时,设置为2的次方数。

1.8结构体传参

这个我们在之前的章节中讲过,这里不做细讲,直接看代码:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S
{
  int arr[100];
  int num;
};
//结构体传参
print1(struct S s)
{
  printf("%d\n", s.num);
}
//结构体地址传参
print2(struct S* ps)
{
  printf("%d\n", ps->num);
}
int main()
{
  struct S s = { {1,2,3},100 };
  print1(s);//传结构体
  print2(&s);//传地址
  return 0;
}

上面有两种结构体传参方式,我们选择的那种方式比较好呢?

答案是第二种,结构体地址传参。

原因

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

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

形参是是实参的一份临时拷贝,要是选择结构体传参的话,需要额外开辟一处空间,会造成空间的浪费。

目录
相关文章
|
1天前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
1天前
|
C语言
|
1天前
|
网络协议 编译器 Linux
结构体(C语言)
结构体(C语言)
|
18小时前
|
编译器 C语言
C语言枚举:深入探索下标默认值、自定义值及部分自定义情况
C语言枚举:深入探索下标默认值、自定义值及部分自定义情况
5 0
|
18小时前
|
C语言
C语言中的结构体
C语言中的结构体
4 0
|
2天前
|
编译器 C语言 C++
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
5 0
|
2天前
|
C语言
【C语言基础篇】字符串处理函数(四)strcmp的介绍及模拟实现
【C语言基础篇】字符串处理函数(四)strcmp的介绍及模拟实现
|
2天前
|
C语言
C语言prinf函数
C语言prinf函数
10 4
|
1天前
|
编译器 程序员 Serverless