结构体和内存函数

目录

一.结构体

(一)匿名结构体

 (二)结构体内存对齐

1.规则

2.例题

 3.为什么存在内存对齐

 4.修改默认对齐数

 二.内存函数(memcpy和memmove) 

(一)memcpy

1.函数原型

2.模拟实现 

3.特殊情况 

(二)memmove 

1.函数原型

2.模拟实现 


一.结构体

(本篇不是对结构体的详细介绍)

(一)匿名结构体

struct {
	int a;
	int b;
}x;

可以不完全声明结构体,此时的x就是该结构体类型的变量

struct {
	int a;
	int b;
}x;
struct {
	int a;
	int b;
}*y;
int main() {
	y = &x;
	return 0;
}

这里在编译时会显示=两边类型不兼容(但并没有报错和警告,编译器为vs2019),所以可以知道这两个声明是两个不同的类型(不太清楚这个有啥用,但多知道一点没坏处)

 (二)结构体内存对齐

1.规则

  1. 第一个成员在结构体变量偏移量为0的地址处
  2. 其他成员要对齐到对齐数的整数倍的地址处
  3. 对齐数=编译器默认对齐数 和 该成员大小的 最小值
  4. vs中默认的对齐数为8
  5. 结构体总大小为每个成员的最大对齐数的整数倍
  6. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍, 整个结构体的大小是所有成员(包括嵌套的结构体的成员)的最大对齐数的整数倍

2.例题

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

  •  黄色区域为结构体成员占用的内存,蓝色区域是由对齐规则产生的浪费的内存空间
  1. c1从0偏移量开始,占用一个字节
  2. i大小为四个字节,它的对齐数为4(4和8中取最小值),因此要偏移到4的倍数,此时的偏移量为1,不符合,而距离最近的符合条件的数字就是4,因此从偏移量为4的位置占用空间
  3. c2的对齐数是1(1和8中取最小值),此时的偏移量为7,下一个数字是8,8是1的倍数,因此继续占用一个字节
  4. 此时,成员已经占用空间完毕,但总大小为9,9不是4(1和4中取最大值)的倍数,距离9最近的符合的数为12,因此最终结构体的大小为12
struct S1{
 double a;
 char b;
 int c;
};

struct S2{
 char d;
 struct S1 s;
 double e;
};

  •  黄色和粉色区域为s成员占用空间,蓝色是s浪费的空间
  1. a占八个字节,从0偏移量开始
  2. b占一个字节,对齐数为1(1和8取最小值),8是1的倍数
  3. c占四个字节,对齐数为4(4和8取最小值),但9不是4的倍数,距离9最近的4的倍数为12,因此c从12偏移量开始占用空间
  4. s结构体总大小为16,最大对齐数为8(double a)
  5. 黄色和粉色区域为struct s2成员占用的空间,蓝色是struct s2浪费的空间
  6. d占一个字节
  7. s占16个字节,最大对齐数为8,因此从偏移量为8的位置开始占用空间
  8. e占八个字节,对齐数为8,此时偏移量为23,下一偏移量24恰好为8的倍数,因此继续占用
  9. 最后,该结构体总大小为32

 3.为什么存在内存对齐

1. 平台原因 ( 移植原因 )
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
3.总结:内存对齐就是用空间换时间
4.例题:
struct a {
	char a;
	int b;
};

若我们的数据总线为32位,即一次读取数据大小为32bit,即四个字节,那么当不存在内存对齐时,我们需要两次才能读取到b(注意是从边界上读取)

 如果存在内存对齐,只需要一次

 4.修改默认对齐数

#pragma pack(2);
这个预处理指令可以修改默认对齐数
#pragma pack();

这个可以取消设置的默认对齐数,还原为默认


 二.内存函数(memcpy和memmove) 

(一)memcpy

1.函数原型

  •  dest是拷贝的目标地址,src是拷贝的起源地址,count是拷贝的字节数
  • 它会返回目标地址

2.模拟实现 

#include<assert.h>
 
void* work(void* p1, const void* p2, size_t n) {    //const保证起源地址不被更改
	void* ret = p1;     //保存目标地址,以作为函数返回值
	assert(p1 && p2);    //断言,assert 宏的原型定义在 assert.h 中,其作用是如果它的条件返回错误,则终止程序执行,用来增加代码的安全性
	while (n--) {
		*(char*)p1 = *(char*)p2;   //转换为单字节类型进行赋值
		++(char*)p1;    //进行运算时也需要转换类型,因为原类型为void*,无法进行运算
		++(char*)p2;
	}
	return ret;
}
int main() {
	int a[5] = { 1,2,3,4,5 };
	int b[5] = { 0 };
	int* p = work(b, a + 2, 8);
	return 0;
}

(代码结果)

3.特殊情况 

	int a[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = memcpy(a+2 , a , 12);

按照我们上面对memcpy的模拟中,无法实现像该例子的拷贝,因为我们已经改变了要拷贝部分的元素

 但是在vs2019中,似乎已经将memcpy修改成了可以处理重叠内存的问题(但不重要,只要我们自己学会怎么处理就行了)

(二)memmove 

1.函数原型

可以看出来,memmove和memcpy的定义是大差不差的,但它可以实现重叠内存的拷贝(虽然是这么说的,但是感觉现在memcpy和memmove的功能区别不大耶,用啥应该都行,但保险起见,还是用memmove比较好)

2.模拟实现 

  •  如果目的地址在起源地址之后:memcpy的方法不可行,会改变要拷贝部分的元素,因此我们需要换一种方法
  • 如果起源地址在目的地址之后:上面的方法没有问题
  • 新方法:
  •  从要拷贝部分的末尾开始拷贝,将5改为3,再将4改为2,最后将3改为1
  • 因此就不存在改变元素的问题了
  • 所以我们需要处理一下起源地址和目标地址,每次循环时两个地址-1,并且总次数为count
void* work(void* p1, const void* p2, size_t n) {
	void* ret = p1;
 	assert(p1 && p2);
	if (p1 > p2) {       //p1是目标地址,p2是起源地址
		while (n--) {
	*((char*)p1 + n) = *((char*)p2 + n);//第一次循环时n为2,刚好指向拷贝空间的最后一个元素
		}
	}
	else {
		while (n--) {     //方法和之前的一样
			*(char*)p1 = *(char*)p2;
			++(char*)p1;
			++(char*)p2;
		}
	}
	return ret;
}
int main() {
	int a[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = work(a+2 , a , 12);
	return 0;
}

感谢观看~ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值