1.数据存储
1.编译器会根据数据类型的不同,分配大小不同的内存。(就相对于看字节数吧)
2.整型数据:有三种表示⽅法,即原码、反码和补码。
原码:直接将⼆进制按照正负数的形式翻译成⼆进制就可以。
反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码+1就得到补码。
正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。
原因:在计算机系统中,数值⼀律⽤补码来表示和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法 和减法也可以统⼀处理( CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
3.浮点数据类型:这个比较麻烦,就简单介绍一下
对于32位的浮点数:最高的1位是符号位,接着的8位是指数,剩下的23位为有效数字。
对于64位的浮点数:最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字。
2.大小端(重点*****)
1.⼤端(存储)模式,是指数据的低位保存在内存的⾼地址中,⽽数据的⾼位保存在内存的低地址中。
2.⼩端(存储)模式,是指数据的低位保存在内存的低地址中,⽽数据的⾼位保存在内存的⾼地址中。
3.为什么会有⼤⼩端模式之分呢?
- 这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit。但是在C语⾔中除了8bit的char之外,还有16bit的short型, 32bit的long型(要看具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如果将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存储模式。例如⼀个16bit的short型x,在内存中的地址为0x0010, x的值为0x1122,那么0x11为⾼字节, 0x22为低字节。对于⼤端模式,就将0x11放在低地址中,即0x0010中, 0x22放在⾼地址中,即0x0011中。⼩端模式,刚好相反。我们常⽤的X86结构是⼩端模式。很多的ARM, DSP都为⼩端模式。有些ARM处理器还可以由硬件来选择是⼤端模式还是⼩端模式。PowerPC、KEIL C51与网络字节序都是采用的大端模式。
4.使用程序判断大小端
- 使用联合判断:因为联合底层是公用同一块内存的,也就是说这个联合c所占用的内存就是4个字节,b取低位如果是1就是小端,如果不是1就是大端。画个图理解理解(其中联合是从低地址开始放数据的)
-
int Judgement() { union { int a; char b; }c; c.a = 1; return (c.b == 1); //小端返回TRUE,大端返回FALSE }
-
使用指针判断:原理非常相似上面:就是char*类型的指针只能接受&i,需要对i进行强转数据会丢失一部分
-
void Judgement(void) { int i = 1; unsigned char* p; p = (unsigned char *)&i; if (*p) { printf("little_end"); } else { printf("big_end"); } }
5.涉及接受一下C++中内存地址分配:
- 内存地址是从高地址到低地址进行分配的。
- 函数参数列表的存放方式是,先对最右边的形参分配地址,后对最左边的形参分配地址。
- Little-endian模式的CPU对操作数的存放方式是从低字节到高字节的。
- Big-endian模式的CPU对操作数的存放方式是从高字节到低字节的。
- 联合体union的存放顺序是所有成员都从低地址开始存放。
- 一个变量的地址是由它所占内存空间中的最低位地址表示的。
- 堆栈的分配方式是从高内存地址向低内存地址分配的。
3.C/C++程序内存的分配 (重点*****)
⼀个由C/C++编译的程序占⽤的内存分为以下⼏个部分:
1.栈区---由编译器⾃动分配释放,存放为运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。其操作⽅式类似于数据结构中的栈,而且栈是向下增长的。
2.内存映射段---是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
3.堆区---⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅式类似于链表。堆是可以上增长的。
4.数据段---存放全局变量、静态数据、常量。程序结束后由系统释放。
5.代码端---可执行的代码/只读常量。
4.动态内存函数的介绍:(这里就用讲一组常用C语言常用的malloc和free,C++的new和delete)
C语⾔提供了⼀个动态内存开辟的函数:
void* malloc (size_t size);
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回⼀个指向开辟好空间的指针。
- 如果开辟失败,则返回⼀个NULL指针,因此malloc的返回值⼀定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者
⾃⼰来决定。
- 如果参数 size 为0, malloc的⾏为是标准是未定义的,取决于编译器。
C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数⽤来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做 。
C++语言提供的new和delete(其实底层new还是用malloc申请空间只是需要调用构造函数,delete要调用析构函数):
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。在这里就实现一下
new函数的实现:
void *_CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
void *p;
while ((p = malloc(size)) == 0) {
if (_callnewh(size) == 0) {
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return (p);
}
delete函数的实现:
//该函数最终是通过free来释放空间的
void operator delete(void *pUserData) {
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK);
_TRY
pHead = pHdr(pUserData);
}
*****这里有个重要的知识就是设计一个类,该类只能在堆上创建对象
在C++中,创建类的对象有两种方法,一种是静态建立,A a; 另一种是动态建立,调用new 操作符。
静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
1.只能在堆上创建对象的类:
那就是动态建立类的对象,使用new操作符来完成。
所以可以这样做,将该类的构造函数和析构函数权限设为protected,(可以让该类可以被继承),然后定义两个static 函数来调用new ,delete 来创建和销毁对象。
class A
{
protected:
A(){}
~A(){}
public:
static A* Create()
{
return new A();
}
static void Destroy(A* p)
{
delete p;
p = NULL;
}
};
2.将析构函数声明为私有的:(在堆上创建)
对象建立在栈上面时,是由编译器分配空间的,调用构造函数来构造栈对象,当对象使用完之后,编译器会调用析构函数来释放栈对象所占的空间,编译器管理了对象的整个生命周期,编译器为对象分配空间的时候,只要是非静态的函数都会检查,包括析构函数,但是此时析构函数不可访问,编译器无法调用类的析构函数来释放内存,那么编译器将无法在栈上为对象分配内存。
class a
{
public :
a(){}
void destory()
{
delete this;
}
private:
~a(){};
};
方法二两个缺点:(1)无法解决继承问题,因为通常情况之下a作为基类,一般析构函数要设为vitual,然后子类重写,已实现多态,因此析构函数不能设为private,不过c++还有protected访问控制方式,将析构函数设置为protected,这样子类可以访问,但是类外无法访问。
(2)使用不方便,不统一,因为你使用了new创造了对象,但是不能使用delete释放对象,必须使用destory函数,这种方式比较怪异,所以我们也可以将构造函数设置为protected,同时提供另一public static create()函数来进行替代new。这样 create()创建对象在堆上, destory()释放内存。
3.只能在栈上创建对象的类
class B
{
private:
void * operator new(size_t size){}
void operator delete(void *ptr){}
public:
B(){}
~B(){}
};
malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
1. malloc和free是函数,new和delete是操作符
2. malloc申请的空间不会初始化,new可以初始化
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
7. new/delete比malloc和free的效率稍微低点,因为new/delete的底层封装了malloc/free
5.堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存泄露
2、空间大小不同;
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的(但是可以在编译器上设置空间大小打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。注意:Reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。)
3、能否产生碎片不同;
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出。
4、生长方向不同;
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
5、分配方式不同;
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。
6、分配效率不同;
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
总结:看了好多博客和自己看书总结的笔记,浓缩的,感觉差不多讨论全了,有不足的地方希望大佬多多评论