【C/C++】内存对齐

简介:          在了解内存对齐方式前,先介绍计算机的存储方式:Big Endian与Little Endian:  Big Endian   即数据的高位在低地址,地位在高地址,并且把最高字节的地址作为变量的首地址 Little Endian 即数据的高位在高地址,数据的低位在低地址,并且把最低字节的地址作为变量首地址。

         在了解内存对齐方式前,先介绍计算机的存储方式:Big Endian与Little Endian:

  •  Big Endian   即数据的高位在低地址,地位在高地址,并且把最高字节的地址作为变量的首地址
  •  Little Endian 即数据的高位在高地址,数据的低位在低地址,并且把最低字节的地址作为变量首地址。
        现实中,某些基于RISC(精简指令集)的cpu比如SPARC、PowerPC等,采用Big Endian,而Intel系列cpu采用Little Endian。如果想要知道自己的电脑是什么存储格式只需要输入以下代码: 

#include<iostream>

using namespace std;

void main()
{
	 char ch[]={0x12,0x34,0x56,0x78};
	 int* p=(int*)ch;
	 cout<<hex<<*p<<endl;//如果是78563412,说明是 Little Endian,如果是12345678,则是Big Endian
}
          自然对齐:如果一个变量的内存地址正好位于它字节长度的整数倍,它就被称做自然对齐

       对于标准数据类型,它的地址只要是它的长度的整数倍,而非标准数据类型按下面的原则对齐:
  数组 :按照基本数据类型对齐,只要第一个对齐后面的自然也就对齐。 
  联合 :按其包含的长度最大的数据类型对齐。 
  结构体: 结构体中每个数据类型都要对齐。        

字节对齐的好处:
   字节对齐的根本原因在于CPU访问数据的效率问题。学过微机原理的都知道规则字和非规则字,8086cpu访问规则字只要一个周期,而访问非规则字需要两个周期。在这里原理也是一样的,只不过这里是32位的操作系统,最多一次访问4字节,而8086是16位的,一次最多访问2字节。假设上面整型变量的地址是自然对齐,比如为0x00000000,则CPU如果取它的值的话需要访问一次内存,一次直接取从0x00000000-0x00000003的一个int型,如果变量在0x00000001,则第一次访问0x00000001的char型,第二次取从0x00000002-0x00000003的short型,第三次是0x00000004的char型,然后组合得到所要的数据,如果变量在0x00000002地址上的话则要访问两次内存,第一次为short,第二次为short,然后组合得到整型数据。如果变量在0x00000003地址上的话,则与在
 0x00000001类似。
 


  我们通过下面的例子来说明自然对齐:

#include<iostream>

using namespace std;

void main()
{
	int a=0x0abcde11;//a b c 的地址依次减小
	int b=0x012345678;
	double c=0x0f23456789abcdef1;
    char d=0x0fa;

   	char *ptr=(char*)&a;
    printf("a b每个字节的内容:\n");
	printf("  地址  :内容\n");
	for(int i=0;i<8;i++)
		printf("%x  :%x\n",ptr+3-i,*(ptr+3-i));//说明整数是按 little-endian存储的


    printf("\na b c d的首地址和地址与字节长度的余值:\n");
	printf("a: %x :%d\n",&a,long(&a)%sizeof(a));//从这里可以看成变量的内存地址按变量顺序递减的 
	printf("b: %x :%d\n",&b,long(&b)%sizeof(b));//各个变量并不一定存放在连续的内存单元
	printf("c: %x :%d\n",&c,long(&c)%sizeof(c));
	printf("d: %x :%d\n",&d,long(&d)%sizeof(d));
}
上面程序在我电脑的运行结果为:


由上面的结果可以知道:

  • 地址随变量顺序而减小(你可以通过改变变量定义顺序来测试);
  • 我的电脑采用的是Little Endian;
  • 各个变量并不一定存放在连续的内存单元(由c d的地址可知)

对于数组,无论是静态数组还是动态数组都是连续存储的,可以用下面程序来查看:

#include<iostream>

using namespace std;

void main()
{
    int array[5]={0};
	for(int i=0;i<5;i++)
	cout<<&array[i]<<endl;//输出静态数组的每个元素的地址
    cout<<endl;

	int *pt=new int[5];
	for( i=0;i<5;i++)
	cout<<hex<<(pt+i)<<endl;//输出动态数组的每个元素的地址
    cout<<endl;

	delete []pt;//注意要释放内存
    
}

        上面我们讨论了基本数据类型的内存存储,下面我们来看看类的存储结构:

首先我们看看下面这个类:

class person1
	{
		bool m_isMan;
		float m_height;
		bool m_isFat;
		double m_weight;
		unsigned char m_books;
	};
	cout<<sizeof(person1)<<endl;//32=4+4+8+8+8

这里person类的长度为32,其内存单元示意图如下:

在这里是按8字节边界来对齐的

上述变量已经都自然对齐了,为什么person对象最后还要填充7字节?

因为当你定义person类型的数组时,如果不填充7字节,则除了第一个元素外其它的元素就可能不是自然对齐了。


下面通过使用编译指令来定义对齐方式:

#pragma pack(push,4)// 按4字节边界对齐
	class person2
	{
		bool m_isMan;
		float m_height;
		bool m_isFat;
		double m_weight;
		unsigned char m_books;
	};
	cout<<sizeof(person2)<<endl;//24=4+4+4+8+4
#pragma pack(pop)	
这里person类的长度为24,其内存单元示意图如下:

显然,在这里m_weight的地址不一定能被8整除,即不一定是自然对齐的。

从上面可以知道,内存的大小和存取的效率随编译方式和变量定义有关,最好的方法是:按照字节大小从大到小依次定义变量成员,并尽可能采用小的成员对齐方式。

  • 从小到大定义变量:
    //按照从小到大字节长度来定义变量
    	class person4
    	{
    		bool m_isMan;
    		bool m_isFat;
    		unsigned char m_books;
    		float m_height;
    		double m_weight;
    	};
    	cout<<sizeof(person4)<<endl;//16=1+1+1+1字节的填充+4+8
    这里person类的长度为16,其内存单元示意图如下:


  • 从大到小定义变量:
    //按照从大到小字节长度来定义变量
    	class person3
    	{
    		double m_weight;
    		float m_height;
    		unsigned char m_books;
    		bool m_isMan;
    		bool m_isFat;
    	};
    	cout<<sizeof(person3)<<endl;//16=8+4+1+1+1+1字节的填充
    这里person类的长度为16,其内存单元示意图如下:

          从上面可以看出两者所占内存一样,但是稳定度不同,从小到大的方式的对齐方式而发生有的成员变量不会自然对齐。如下所示

         

 #pragma pack(push,1)// 按4字节边界对齐
	class person5
	{
		bool m_isMan;
		bool m_isFat;
		unsigned char m_books;
 		float m_height;
  		double m_weight;
	};
	cout<<sizeof(person5)<<endl;//15=1+1+1+4+8
#pragma pack(pop)

这里person类的长度为15,其内存单元示意图如下:          

    

在上面的程序中,double的偏移量为1+1+1+4=7,很有可能不会自然对齐,所以最好采用从大到小的方式来定义成员变量。



                




目录
相关文章
|
3月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
128 26
|
8月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
4月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
61 1
|
12月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
206 3
|
10月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
407 68
|
7月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
8月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
346 0
|
9月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
338 0
|
11月前
|
存储 缓存 C语言
【c++】动态内存管理
本文介绍了C++中动态内存管理的新方式——`new`和`delete`操作符,详细探讨了它们的使用方法及与C语言中`malloc`/`free`的区别。文章首先回顾了C语言中的动态内存管理,接着通过代码实例展示了`new`和`delete`的基本用法,包括对内置类型和自定义类型的动态内存分配与释放。此外,文章还深入解析了`operator new`和`operator delete`的底层实现,以及定位new表达式的应用,最后总结了`malloc`/`free`与`new`/`delete`的主要差异。
182 3
|
11月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
552 4