[C语言]--自定义类型: 结构体

目录

前言

一、结构体类型的声明

1.结构的声明

2.结构体变量的创建和初始化

3.结构的特殊声明 

 4.结构的自引用

二、结构体内存对齐

1.对齐规则

2.为什么存在内存对齐?

三、结构体传参 

四、结构体实现位段

1.什么是位段

2.位段的内存分配

3.位段的跨平台问题

 4.位段的应用

5.位段使用的注意事项 

总结


前言

我们已经将C语言中的内置类型学完了,下面我们来学习一下C语言的自定义类型,结构体。


一、结构体类型的声明

1.结构的声明

结构是一些值的集合,这些值称为成员变量。

结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体。

struct tag
{
          member-list;
}variable-list;
  • tag : 称呼 ,表示这个结构体的名字
  • member-list : 成员列表 ,是写成员变量的地方
  • variable-list  : 变量列表 ,是写属于这个结构的全局变量的地方,可写可不写

 eg:

struct Stu
 {
    char name[20];
    int age;
    char sex;   
 }s1,s2,s3;

 这里的s1,s2,s3的作用s4相同

Struct Stu s4;

 这里创建了一个Stu类型的全局变量s4

2.结构体变量的创建和初始化

#include <stdio.h>

struct Stu
{
    char name[20];
    int age;
    char sex;
};

int main() {

    //方式一:根据结构体成员顺序书写
    struct Stu s1 = { "张山",32,"男" };

    //方式二:根据指定顺序书写  
    struct Stu s2 = { .name = "李斯" ,.sex = "男" , .age = 23 };

    //输出方式
    printf("name: %d", s1.age);
    return 0;
}

3.结构的特殊声明 

匿名的结构体类型,即省略tag(称呼)

struct
 {
    char name[20];
    int age;
    char sex;   
 }s1;

 这种声明只能用一次

 4.结构的自引用

这里先介绍两种数据结构,顺序表链表

 顺序表可以用数组来实现,没啥好说的

在链表中,这五个方块都是一个节点,分别存储着数据和下个节点的位置,这样才能在纷乱的位置中依次找出数据

那我们可以这这样做吗?

struct INT
{
    int data;
    struct INT n;
};

 可是这样做是错的,因为sizeof(struct INT)是多少呢?

我们无法计算它的大小,它会无穷的大

正确的自引用方式:

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

一个结构体指针变量,这个变量指向下个节点的地址,并且指针变量可以求出大小 ,没有上述的问题

还有,当结构体自引用时,夹杂了typedef对结构体类型重命名的问题

typedef struct INT
{
    int data;
    INT* n;
}INT;

 这里会出现错误,因为结构体内提前使用INT,无法分辨INT是啥类型

解决办法: 定义结构体不要使用匿名结构体

typedef struct INT
{
    int data;
    struct INT* n;
}INT;

二、结构体内存对齐

1.对齐规则

  1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    1. 对齐数=编译器默认的⼀个对齐数与该成员变量大小的较小值
    2. VS中默认的值为8
    3. Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体总大小为最大对齐数 (结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的) 的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构 体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

举个例子: 

int main() {
	struct S1
	{
		char c1;    //1  8  ->  1
		int i;      //4	 8  ->  4
		char c2;    //1	 8  ->  1
	};
	printf("%zd\n", sizeof(struct S1));
}

 

规则1, 结构体第一个变量c1在结构体变量起始位置偏移量为0处

规则2,根据对齐数,c2的对齐数是4,所以跳过前面,到4处; c3的对齐数是1,到8处

规则3,c1,c2,c3的对齐数分别是1,4,1  最大对齐数是4,所以该结构体总大小为12

2.为什么存在内存对齐?

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

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

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

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

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

当我们要满足对齐,又要节省空间,办法是:

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

当然,结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

#pragma pack(1)   //设置默认对⻬数为1 

#pragma pack()    //取消设置的对⻬数,还原为默认
 

三、结构体传参 

传参分为两种

struct Stu
{
	char name[1000];
	int num;
};

struct Stu s = { "大明", 1000};

//结构体传参
void print1(struct Stu s)
{
	printf("%s\n", s.name);
}

//结构体地址传参
void print2(struct Stu* ps)
{
	printf("%d\n", ps->num);
}

int main()
{
	print1(s);  //传结构体
	print2(&s); //传地址
	return 0;
}

当然print2 比 print1 好 

原因:

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

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

结论:

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

四、结构体实现位段

1.什么是位段

位段和结构体类似,但有两个不同的地方

  1. 位段的成员类型有要求,只能是int、unsigned int 、signed int ,只是在C99中位段的成员类型有其他类型(如char....)
  2. 位段的成员名后面有一个冒号(:)和一个数字(这个数字表示比特位的数量)

例子:

struct A
{
    int a:2;
    int b:5;
    int c:10;
};

这个结构体的大小是4个字节

为什么呢?

2.位段的内存分配

位段的空间是按照成员类型来分配内存的,我们再举个例子

struct A
{
    char a:2;
    char b:4;
    char c:6;
    char d:7;
};

如: 第一个成员类型是char类型,那么会先给1个字节

 这里还要说明一点,位段的储存顺序是不确定的,可能是从左往右,也可能是从右往左,我们假设是从右往左

在这个字节中,我们将a存入,a占两个比特位,接着存入b,b占4个比特位

但当我们要存入c时,剩下的空间不够了(利用还是舍弃不确定,这里舍弃),于是再根据c的类型,生成一个字节

d同理

 

综上所示,这个结构体的大小就是3个字节 

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

3.位段的跨平台问题

  1. int位段被当成有符号还是无符号数是不确定的
  2. 位段中最大的数目不能确定。(16位机器最大16,32位机器最大32。当写成27时,在16位机器就会出现问题)
  3. 位段中成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
  4. 当一个结构体包含两个位段,第二个位段成员比较大时,无法容纳于第一个位段剩余的位时,是舍弃剩余的位段还是利用,这是不确定的

总结:

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

 4.位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

5.位段使用的注意事项 

因为同一个字节中可能会有多个位段成员,而这些成员的起始位置并不是某个字节的起始位置,所以这些位置处是没有地址的

内存中每个字节分配一个地址,但字节内的bit位是没有地址的

因此我们不能对位段的成员使用&操作符


总结

结构体在C语言中并不难,学完后找一些题做做就行啦

上一篇:通信工程学习:什么是SISO单入单出


下一篇:MATLAB绘图基础