自定义类型:结构体

1. 结构体类型的声明

1.1 结构体

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

1.1.1 结构的声明

struct tag
{
member-list;
}variable-list;

struct 是结构体类型的关键字

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

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

#include <stdio.h>
struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
};

int main()
{
    //按照结构体成员的顺序初始化
    struct Stu s = { "张三", 20, "男", "20230818001" };
    printf("name: %s\n", s.name);
    printf("age : %d\n", s.age);
    printf("sex : %s\n", s.sex);
    printf("id : %s\n", s.id);
   
    //按照指定的顺序初始化
    struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女
    printf("name: %s\n", s2.name);
    printf("age : %d\n", s2.age);
    printf("sex : %s\n", s2.sex);
    printf("id : %s\n", s2.id);
    return 0;
}

1.2 结构的特殊声明

在声明结构的时候,可以不完全的声明。采用匿名结构体类型

例:

struct
{
    int a;
    char b;
    float c;
} s = { 'x', 100, 3.14 };

int main()
{
    printf(... ...

    ... ...
}

s 可以初始化,也可以不初始化。 
匿名的结构体类型,如果没有对结构体类型重命名的话,只能使用一次。一般只有在只考虑使用一次的情况下使用。

struct
{
    int a;
    char b;
    float c;
} s = { 'x', 100, 3.14 };


struct
{
    int a;
    char b;
    float c;
}a[20], *p;

int main()
{
    printf(... ...

    ... ...
}

如果这时候我们再加一个匿名结构体类型,编译器会报警报,因为编译器不会分辨两个类型,所以把上面的两个声明当成完全不同的两个类型,也是非法的

 

如果我们非得说让匿名结构体类型有名字,可以吗?当然可以:

typedef struct
{
    int a;
    char b;
    float c;
} S;

int main()
{
    S s;

    ... ...
}

我们可以在struct 前加 typedef。往后可以随时用S了,话虽如此,但是既然我们想让他匿名,但是又加上名字,何苦呢?

1.3 结构的自引用

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

我们以链表为例:
 

什么是链表?

它能让各个节点相互有序链接,每个节点携带一个数值,并找到下一个节点。

比如,定义一个链表的节点:

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

cf256b1910cb457dbd37c4a7e472286a.png

 仔细看上面的代码,是否正确?

表面上是一个节点里包含下一个节点,实际上这并不合理。因为一个结构体中再包含一个同类型的结构体变量,就相当于,我自己包含我自己,这样结构体变量的大小就会无穷的大,是不合理的。

正确示范:

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

b01977944ed84f44b56e34e01a4a4179.png

我们稍加改动,用指针指定位置就能精准找到下一个节点。

2. 结构体内存对齐

2.1 对齐规则

首先得掌握结构体的对齐规则:
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
        对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值

        - VS 中默认的值为 8
        - Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的
整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

例:

struct S1
{
    char c1;//1
    int i;//4
    char c2;//1
};
printf("%d\n", sizeof(struct S1));

bdea0a43ab214d8c8ad81d956c8ee9e4.png

假设 S1从起始点对齐开始。

第一个偏移量(c1)在0的位置,并且只占一个字节:

de82136ad8aa420eafe1d5648cd9e405.png

如果后面还有变量成员,就要对齐到某个数字(对齐数)的整数倍的地址处,i的对齐数是4(4/8相比 4 最小),整数倍,即4的倍数。故从4(4的倍数)开始,并且只占4个字节:

fd3d7573559f48ea83db48dd3d562c95.png

c2只占一个字节,对齐数是1(1/8相比1最小),下一个是8,是1的倍数,就可以放进去了:

eb418710441f4592ac65d668f4ef4a22.png

结构体总大小是最大对齐数的整数倍,三个成员最大对齐数是4,总大小要是4的倍数,往后到12,是4的倍数,故占12个字节。

11ea6502b5d44204b03971dda5ed19a2.png同时我们也看到了,浪费了6个字节(目前不做追究)。

再举个例子:

struct S1
{
    char c1;//1
    int i;//4
    char c2;//1
};


struct S2
{
char c1;
struct S1 s1;
double d;
};
printf("%d\n", sizeof(struct S2));

ec506186b0334f96b6d1dbcecae75ca6.png老样子,假设从“0”对齐点开始。

第一个偏移量(c1)在0的位置,并且只占一个字节:

0a60c44b4ef04a338c1f61051842bc3c.png

嵌套了S1结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处(i 的 4),所以在4的倍数处存放,占12个字节:

6af04d16c35f40628a166e5186e6d77a.png

接下来是 double 类型,对齐数是8(和默认同值),占8个字节:

0eccb7204f76455ea254977933e62a19.png

结构体总大小是最大对齐数的整数倍,三个成员最大对齐数是8,总大小要是8的倍数,往后到24,是8的倍数,故占24个字节。

bfb29e25c5c04bbfadae3f56f6a52235.png浪费3空间。

2.2 为什么存在内存对齐?

2.2.1 平台原因 (移植原因)

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

2.2.2性能原因

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

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

 

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


struct S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};

int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
}

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

24153b41c15f4f83852d1909a2e95797.png

2.3 修改默认对齐数

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

#include <stdio.h>

#pragma pack(1)
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()

int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}

#pragma pack(1) 是设置默认对齐数为1,当然,我们可以根据需要改编默认对齐数。

3. 结构体传参

struct S
{
	int arr[1000];
	int n;
	double d;
};

void print1(struct S tmp)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("%d ", tmp.n);
	printf("%lf\n", tmp.d);
}

void print2(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("%d ", ps->n);
	printf("%lf ", ps->d);
}
//ps ->指向的是s,s里有 arr ,n , d,通过循环找到并打印arr[i],其他同理打印。 

int main()
{
	struct S s = { {1,2,3,4,5}, 100, 3.14 };
	//print1(s);
	print2(&s);

	return 0;
}

上面的print1 和print2 函数哪个好些?
答案是:

首选print2函数。

原因:

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

print1,s是值传递,arr(4000),n(4),d(8),占多少空间,s向tmp就会传多少空间,占用4012空间,即占空间,又耗时间。

print2直接指针找到地址。

结论:

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

4. 结构体实现位段

4.1 什么是位段

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有一个冒号和一个数字。

位段(二进制位)式结构:

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

int a:2; 为例,int 里占4个字节(32比特位),假设a里就一个数字1,表示它只需要两个比特位,浪费的30个怎么办,就不要了,我直接取2个比特位占用会节省更多空间。

我们看看它占了多少内存:

3de63b8af52c4dfea64b80a359b8a629.png

4.2 位段的内存分配

1. 位段的成员可以是int unsigned int signed int 或者是char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

struct S
{
    int a:3;
    int b:4;
    int c:5;
    int d:4;
};

int main()
{
struct S s = {0};
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;

    return 0;

}

从a开始进行分配,先拿出来 int  的8个比特位:

       926fb5bb203c43e8b0d95d5ce61f1fff.png这时我们该思考是从右向左还是从左向右?

vs是从右向左:

        cb67344216cd45108a5504145c310c2b.png

b要用4个:

     60d0a40dc1834518ad750f8bba28d3fe.png

接下来,c要占用5个,但剩下了1个,怎么办?是留还是废?

vs是浪费掉,我们再从依次序另取8个比特位,并占用上:

     f28d5f10a47c4adebf43e92159077324.png

继续同理放b:

   c47c4173e63f47c0a84bb0c3ebefbf31.png

s 初始化都是0;

a 要存10(00001010),但是只有3个比特位,怎么办?没办法,只能留下3的(右向左):

      fdfc1fa1d9dc4fc6ab832a9818697bb9.png

同理:

  a0398f0d63494c62a902571bbfc6341b.png

为什么开头要是0?因为s初始化就都是0,和占用空间无关;放完后:

 9dc2e6fcf22a4edca11ea7b3d8312562.png

如果我们每4位(一个整形)以二进制转换成数字会是 6 2 0 3 0 4,在内存里就是以这样的形式存储(16进制):

43d0f3b03e534e57bccfabaa723fcce2.jpg

4.3 位段的跨平台问题

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

总结:

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

4.4 位段的应用

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

98893deed46e4ef182316f18f522408e.png

源IP地址就好比寄快递的发件人,目的IP地址好比寄快递的收件人,要让人知道从哪里来,到哪里去。

格式里我们发现版本号,服务类型等占比特位不同,如果随意给类型必然导致空间浪费,但是我们又看一行从头到尾位语段分配又恰是32个比特位,从上到下又是20字节(5个整形),非常巧妙哦。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值