【C语言-自定义类型】还能这样整?

前言

拜读了陈皓大佬的《C语言结构体里的成员数组和指针》,受益匪浅


1.结构体

“有容乃大”,给啥都往里装

(1)声明

基本结构

struct tag
{
	member_list;
}variable_list;
  • tag - 类型名:可以用 struct tag 创造一个此类型的变量
  • member_list - 成员列表:表示 tag 类型里的成员们
  • variable_list - 变量列表:表示用此类型创造的变量们

看看例子:

struct fruit
{
	char name[15];
	float weight;
	float price;
};

(2)定义与初始化

定义

  1. 直接在变量列表的位置创建
struct fruit
{
	char name[15];
	float weight;
	float price;
}apple, melon;
  1. 用类型创建
int main()
{
	struct fruit banana;
	return 0;
}

初始化

一坨结构体用一对花括号初始化

  1. 在变量列表处初始化
struct fruit
{
	char name[15];
	float weight;
	float price;
}apple = { "apple", 1.5, 10 }, melon = { "melon", 4, 40 };
  1. 用类型创建变量并初始化
int main()
{
	struct fruit pineapple = { "pineapple", 1, 30 };
	return 0;
}
  1. 嵌套结构体的初始化
struct f1
{
	char buyer[20];
};
struct fruit
{
	char name[15];
	float weight;
	float price;
	struct f1;
}apple = { "apple", 1.5, 10 }, melon = { "melon", 4, 40 };


int main()
{
	//struct fruit pineapple = { "pineapple", 1, 30 };
	struct fruit mango = { "mango", 0.5, 10, {"bacon"} };
	return 0;
}

(3)用处

描述复杂对象

一个芒果,单纯的用char/int 来描述,达不到我们想要的描述效果

(4)内存对齐

为什么要对齐?

  1. 有些机器只能访问到特定地址处的数据,不能访问任意地址处的数据
  2. 访问未对齐的内存,处理器需要做两次访问;而对齐的内存只需要一次
    所以我们说数据结构应该尽可能地向自然边界对齐

内存对齐其实就是一种 空间换时间 的做法

对齐规则

  1. 结构体的首个成员在结构体偏移量为0的地址处
  2. 每个成员要对齐到 偏移量为对齐数的整数倍 的地址处
  3. 结构体的总大小是最大对齐数的整数倍
  4. 如果嵌套了结构体,总大小就是包括嵌套的结构体在内的所有对齐数中最大的对齐数的整数倍
  • 对齐数:系统默认对齐数(vs上是8,可修改)和成员大小的较小值

例子

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

在这里插入图片描述

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

在这里插入图片描述
欸?例2和例1明明成员一样,例2的总大小却更小
原来

让大小较小的成员尽量集中在一起可以节省空间

  1. 结构体嵌套
struct ss
{
	char c1;
	int i1;
};
struct SS
{
	char c2;
	int i2;
	struct ss s1;
};
int main()
{
	struct SS s2;
	printf("%d\n", sizeof(s2));
	return 0;
}

在这里插入图片描述

(5)结构体实现位段

位段

位段是什么?和结构体很类似,但是有几点要注意:

  1. 位段的成员必须是 整型家族的
  2. 位段在成员后有一个冒号和一个数字,来表示此成员所占的大小(比特位)
  3. 位段的开辟是按char / int 开辟的,也就是一次开辟1/4 字节(看成员)

位段本身就是很不稳定的玩意,所以我们基本不会把int 和 char成员混在一起

位段有什么用?

一定程度上节省空间

例子
struct A
 {
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

在这里插入图片描述

位段的内存分配
struct S 
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};

在这里插入图片描述
其实这里从右向左还是从左向右是标准未定义的,不过vs2019是右到左

位段的缺点

跨平台问题

  1. int的位段被当成有符号或是无符号是不确定的
  2. 位段中最大为的数目不能确定(对于16位、32位机器,27在32位可行;在16位不行)
  3. 位段的成员在内存中从左到右分配还是从右向左是未定义的
  4. 当一个位段成员剩余的比特位不够容纳下一个位段成员时,剩下的位舍弃或是利用,不确定
位段的应用

在这里插入图片描述

(6)一段很有趣的结构体代码

这一段代码来自于“左耳朵耗子”大佬的博客,我叫它“ 不是bug的‘bug’ ”

这个程序在运行起来之后会在第18行crash

  • 为什么打印 00000004 ?
  • 为什么不是在第16行,16行的判断条件用了个空指针呢!
#include <stdio.h>
struct str
{
    int len;
    char s[0];
};

struct foo
{
    struct str* a;
};

int main(int argc, char** argv) 
{
    struct foo f = { 0 };
    if (f.a->s) 
    {
        printf("%p\n", f.a->s);
    }
    return 0;
}
:
00000004

解释一下这段代码:
在这里插入图片描述
访问 空结构体指针 的成员,结果打印了 00000004

接下来,我们好好剖析一下结构体到底是怎么回事

结构体和其成员的本质

首先我们要了解:任何变量的本质其实都是地址,是内存地址的抽象名字,因为机器只认地址,变量在编译的时候会被转成地址

struct test
{
	int i;
	char* p;
};

int main()
{
	struct test t;
	return 0;
}

引用陈皓大佬的代码,gdb进来:

// t实例中的p就是一个野指针
(gdb) p t
$1 = {i = 0, c = 0 '\000', d = 0 '\000', p = 0x4003e0 "1\355I\211\..."}

// 输出t的地址
(gdb) p &t
$2 = (struct test *) 0x7fffffffe5f0

//输出(t.i)的地址
(gdb) p &(t.i)
$3 = (char **) 0x7fffffffe5f0

//输出(t.p)的地址
(gdb) p &(t.p)
$4 = (char **) 0x7fffffffe5f4

可以看到:

  • t 这个结构体的地址是 0x7fffffffe5f0

  • t.i 这个结构体成员的地址是 0x7fffffffe5f0

  • t.p 这个结构体成员的地址是 0x7fffffffe5f4

也就是:

  • t.i = (&t) + 0x0
  • t.p = (&p) + 0x4

得出:

  • 结构体的成员访问就是 加偏移量(相对地址)

*偏移量通过内存对齐得到

再看一段可以体现此本质的代码

struct test
{
	int i;
	short c;
	char* p;
};

int main()
{
	struct test* t = NULL;
	return 0;
}

引用陈皓大佬的代码,gdb进来:

(gdb) p pt
$1 = (struct test *) 0x0
(gdb) p pt->i
Cannot access memory at address 0x0
(gdb) p pt->c
Cannot access memory at address 0x4
(gdb) p pt->p
Cannot access memory at address 0x8

解惑1

可以看到,即使 t 是 NULL,还是通过偏移量(相对地址)访问,也相当于访问 pt 的内址

现在就能够理解为什么访问空结构体指针的成员能打印个 00000004 出来了吧

成员数组和成员指针

区别:

通过汇编来看 “ 不是bug的’bug’ ”

  • 对于char s[0]来说,汇编代码用了lea指令,lea 0x04(%rax), %rdx
  • 对于char*s来说,汇编代码用了mov指令,mov 0x04(%rax), %rdx
lea  0x04(%rax),   %rdx //对于 char s[0]
mov  0x04(%rax),   %rdx//对于 char* s

lea(loading effective address): 加载有效地址(把地址放进去,不访问其中的数据)
mov:把地址里的数据放进去(访问地址中的数据)

有个很有意思的比喻,lea 好比到银行门口张望,但是不抢银行,也不犯法;mov 好比在警察眼皮子底下抢银行

总结:

  • 访问成员数组只是拿到 相对地址 ——没访问
  • 访问成员指针则是拿到 相对地址里的数据 ——访问了

解惑2

到此,咱们也能解释为什么不是在第16行报错
:我们可以拿到 00 00 00 04 这个地址,但绝对不能访问其中的数据

(7)柔性数组

柔性数组:结构中最后一个未知大小的数组

声明

struct S
{
	char c;
	int i[];
};

或是

struct S
{
	char c;
	int i[0];
};

理解

柔性数组在结构中到底是什么存在呢?

引陈皓大佬

其实它连内存空间都不占,可以把它看作一个占位的标识,等我们通过这个标识,给其开辟的空间,它才从标识变成了有长度的数组

使用

声明

struct S
{
	char c;
	int i[0];
};

int main()
{
	struct S* p = (struct S*)
		malloc(sizeof(struct S) + 100 * sizeof(int));
	if (NULL == p)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 100; i++)
	{
		p->i[i] = i;
	}
	for (i = 0; i < 100; i++)
	{
		printf("%d ", p->i[i]);
	}
	return 0;
}

既然柔性数组在没开辟空间的时候是 标识,那我们为他开辟空间的时候就要算上结构体内其他的成员啦

所以有了 malloc(sizeof(struct S) + 10 * sizeof(int))

读到这可能很多人有疑问:我拿个指针不一样可以代替这破柔性数组吗?可以,一起看看

struct S
{
	char c;
	int* pi;
};

int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S));
	p->pi = (int*)malloc(100 * sizeof(int));
	if (NULL == p)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 100; i++)
	{
		p->pi[i] = i;
	}
	for (i = 0; i < 100; i++)
	{
		printf("%d ", p->pi[i]);
	}

	free(p->pi);
	p->pi = NULL;
	free(p);
	p = NULL;
	return 0;
}

两次开辟,两次释放,也能完成柔性数组的工作

但是柔性数组就没有它的优点吗?和这种指针实现比较,有两个优点:

  • 一次开辟,空间是连续的,没有内存碎片;一次释放,方便
  • 访问速度更高一点,可是基于我们研究过的结构体成员数组和成员指针,都知道到底还是要偏移访问,也快不了太多
    但是不管如何,分配了空间的柔性数组,它原地就是数据,而指针还要再解引用,总归快点

2.枚举

枚举:一一列举

enum day
{
	//枚举类型day 包含的枚举常量
};

定义与初始化

定义

给出枚举常量

初始化

默认初始化:从第一个到最后一个枚举常量的值为 0—n-1

每个枚举常量的值都是前一个+1

enum day
{
	//枚举类型day 包含的枚举常量
	Mon,//0
	Tues,//1
	Wed = 5,//5
	Thur,//6
	Fri,//7
	Sat,//8
	Sun//9
};

用处

  1. 增加代码可读性
int main()
{
	int input = 0;
	scanf("%d", &input);
	switch (input)
	{
	case 0://不直观
	case Mon://直观
		printf("Mon\n");
		break;

	case 1://不直观
	case Tues://直观
		printf("Tues\n");
	}(input == 8);
	return 0;
}
  1. 增加了“类型检查”,比#define定义的常量更严谨
  2. 用起来效率高(不指执行效率),可以一次定义多个常量
  3. 防止命名污染(把相近的常量封装起来了)
enum day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
----------------
#define Mon 1
#define Tues 2
#define Wed 3
#define Thur 4
#define Fri 5
#define Sat 6
#define Sun 7

易错

  • 对枚举常量的赋值

常量也能赋值吗?二次赋值,对于枚举常量和枚举常量是可以的

enum color
{
	Red,//0
	Green,//1
	Blue//2
};

int main()
{
	enum color clr = Red;
	clr = Green;//(1)
	clr = 2;//(2)
	return 0;
}

(1)没问题:同类型的枚举常量可以互相赋值
(2)错误:常量2 不是和 clr 同类型的枚举常量,不能赋值(C语言是强类型的语言)


3.联合体(共用体)

联合体的成员共用一块内存空间(都保存在同一块内存)

声明

union un
{
	char a;
	char b;
};

定义

union un
{
	char a;
	char b;
};

实例

用联合体来判断机器大小端:

union un
{
	int i;
	char c;
};

int main()
{
	union un u;
	u.i = 0x11223344;
	u.c = 0x00;
	printf("%x\n", u.i);
	return 0;
}11223300

在这里插入图片描述

轻松判断:小端

联合体是把双刃剑

  • 共用一块内存空间,代表有超多不同的访问途径(就像 p[i] 和 *(p+i) 都没两样),对于char 成员,通过int来访问就出事

我们在使用的时候要通过正确的途径,保证不同时使用联合体成员

用处

节省空间


本期分享就到这啦,不足之处望请斧正

培根的blog,和你共同进步!

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值