1.new/delete和malloc/free之间的关系?
相同点:都可以用于动态申请内存和释放内存;
不同点:
1.操作对象不同;
malloc/free是C++/C语言的标准库函数,new/delete是C++的运算符。
C++是面向对象的语言,对象在创建的同时要自动执行构造函数,对象消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限范
围之内,不能把执行构造函数和析构函数的任务强加给malloc/free。
2.用法上也有所不同:
Malloc/Free:
a.用malloc申请一块长度为length的整数类型的内存,程序如下:
int *p = (int*)malloc(sizeof(int)* length);我们应该把注意力集中在两个要素上:“类型转换”和sizeof。
Ⅰ.malloc函数的返回值类型是void*,所以在调用malloc时要显示地进行类型转换,将void*转换成所需要的指针类型;
Ⅱ.malloc函数本身并不识别申请的内存是什么类型,它只关心内存的总字节数。
b.void free(void* memblock);
为什么free函数不像malloc那样复杂呢?这是因为指针p的类型以及所指内存的容量事先都是知道的,free(p)能正确地释放内存。如果p是NULL指针,那么free对p无
论操作多少次都不会出现问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
----------------------------------------
New/Delete:
运算符new使用起来要比函数malloc简单得多(之所以简单是因为new内置了sizeof、类型转换和类型安全检查功能),例如:
int* p1 = (int*)malloc(sizeof(int)*length);
int* p2 = new int[length]; //length代表数组里有10个元素,new返回的是数组元素的首地址;
如果用new 创建对象数组,那么只能使用对象的无参数构造函数:Obj *objects = new Obj[100]; // 创建100 个动态对象
在用delete释放对象数组时,留意不能丢了[]:
delete []objects; // 正确的用法
delete objects; // 错误的用法
Conclusion:
1.new自动计算需要分配的空间,而malloc需要手工计算字节数;
2.new类型是安全的,而malloc不安全,比如:
a.int* p = new float[2]; //编译时指出错误
b.int* p = malloc(2 * sizeof(float)); //编译时无法指出错误
3.new操作符对应于malloc,但new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上,而malloc无能为力;
4.new调用构造函数,delete调用析构函数;
5.malloc/free需要库文件的支持,而new/delete则不需要;
1.本质区别:
malloc/free是C/C++语言的标准库函数,而new/delete是C++的运算符;
对于用户自定义的对象而言,用malloc/free无法满足动态管理对象的要求。对象在创建时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/
free是库函数而不是运算符,不在编译器的控制权限内,不能把执行构造函数和析构函数的任务强加于malloc/free。因此C++需要一个能完成动态内存分配和初始化工作的
运算符new,以及一个能完成清理与释放内存工作的运算符delete。
2.联系:
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?
因为C++程序经常要调用C函数,而C程序理论上只能用malloc/free管理内存。如果用free释放"new创建的对象",那么该对象因无法执行析构函数而可能导致程序出
错。如果用delete释放"malloc申请的动态内存",理论上程序不会出错,但是该程序可读性将会很差!
所以new/delete、malloc/free必须配对使用。
2.delete和delete[]有什么区别?
1.对于简单的类型来说,使用new分配后,不管是数组形式还是非数组形式,两种均可以释放内存。
2.对于自定义类型来说,就需要对单个对象使用delete,对于数组对象使用delete[],逐个调用数组中对象的析构函数,从而释放动态内存。
3.一般来说我们还是对应起来吧,new--->delete new[]--->delete[];
3.内存块太小导致malloc和new返回空指针,该怎么处理?
1.对于malloc来说,需要判断其是否返回空指针,如果是则马上用return语句终止该函数或者exit终止该程序;
2.对于new来说,一旦一个程序用光了它所有可用的内存,new表达式就会失败,默认情况下,如果new不能分配所要求的内存空间,就会抛出一个类型为bad_alloc的异常,我
们可以用try......catch的代码块接收,或者用nothrow声明,这样分配失败就会返回一个空指针;
int *p2 = new (nothrow) int; //如果分配失败,就会返回空指针
4.内存泄漏的场景有哪些?
1.先来理解几个内存的概念?
a.静态内存:用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量(全局变量),static对象和全局对象则是在整个程序结束时才销毁。
b.栈内存:用来保存定义在函数内的非static对象,分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在定义的程序块运行时才进行,程序退出,栈
对象会自动销毁。
c.动态内存:除了静态内存和栈内存,程序还有一块内存池,这部分也就是成为堆,在使用堆空间时就需要使用动态内存分配。
2.何为内存泄漏?
a.那些分配了资源,而又没有定义析构函数来释放这些资源的类;
b.在资源分配和资源释放之间发生了异常;
总的来说就是分配在堆区的内存由于某种原因未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重的后果。
3.内存泄漏的情况总结:
a.malloc/free未成对出现;
b.new/delete、new[]/delete[]未成对出现,释放数组时一定要使用delete[];
c.在构造函数中动态分配内存,但未在析构函数中释放内存;
d.深浅拷贝问题,当类中有指针类型的数据成员时,我们需要重载拷贝构造函数和重载赋值运算符,避免指针的重复释放,造成内存泄漏;
e.在有动态多态的情况下,我们需要将基类的析构函数定义为虚函数,否则会调用基类的析构函数,仅会释放子类中属于父类的成员,将会出现资源释放不干净的问题,
会造成内存泄漏;
4.如何定位和检测内存泄漏?
1.首先检查所有的new和delete是否成对出现;
2.我自己用过的就是给VS安装一个Visual Leak Detector内存泄漏插件,如果有内存泄漏的情况会生成memory_leak_report.txt报告;
5.内存分配的方式有哪些?
1.在栈上分配:函数内部的变量(局部变量)的内存在栈上分配,函数结束时会自动释放;
2.在全局/静态存储区分配:全局变量和static修饰的局部变量和全局变量均存放在此处;
3.在堆上分配:由new分配的内存存放在栈区,需要用delete手动释放;
4.从常量存储区分配:特殊的存储区,存放的是常量,不可修改;
5.代码区:主要用来存放二进制代码;
6.堆和栈有什么区别?
1.分配和管理方式不同:
a.堆是动态分配的,其空间的分配和释放都由程序员控制;
b.栈是由编译器自动管理的;
2.产生碎片不同:
a.对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,产生大量碎片,使程序效率降低;
b.对栈来说,不存在碎片问题,因为栈具有先进后出的特性;
3.生长方向不同:
a.堆是向着内存地址增加的方向增长的,从内存的低地址向高地址的方向增长;
b.栈是向着内存地址减小的方向增长的,从内存的高地址向低地址的方向增长
4.申请大小限制不同:
a.栈顶和栈底是预设好的,大小固定;
b.堆是不连续的区域,其大小可以灵活调整;
7 静态内存分配和动态内存分配有什么区别?
a.静态内存:用来保存局部static对象,类static数据成员以及定义在任何函数之外的变量(全局变量),static对象和全局对象则是在整个程序结束时才销毁;
b.动态内存:用来存放new分配的对象,需要程序员手动释放;
1.静态内存分配是在编译期间完成的,不占用CPU资源;动态内存分配是在运行期间完成的,分配和释放需要占用CPU资源;
2.静态内存分配是在栈上进行分配的;动态内存分配是在堆上进行分配的;
3.静态内存分配是按计划分配的,在编译前确定内存块的大小;动态内存分配是按需要分配的;
4.静态内存分配是把内存的控制权交给了编译器;动态内存分配是把内存的控制权给了程序员;
5.静态内存分配的运行效率比动态内存分配高,动态内存分配不当可能造成内存泄漏;
8.如何让类对象只在栈(堆)上分配空间?
在C++中,类对象的建立分为两种:静态建立(A a)和动态建立(A* ptr = new A)。
1.静态建立对象:是由编译器为对象在栈空间中分配内存。使用这种方法,直接调用类的构造函数,当使用完了后,编译器自动调用析构函数对这片栈内存进行释放。
2.动态建立对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步:a.编译器执行operator new()函数,在堆空间中搜索合适的内存并进行分配;b.调
用构造函数构造对象,初始化这片内存空间。属于是间接调用类的构造函数,当对象调用完毕后delete也会调用析构函数对堆上的内存进行释放。
------------------如何限制类对象只能在堆或者栈上建立呢?-----------------
AA.先来看看只能在堆上分配类对象吧!
只能在堆上分配对象,就是不能建立静态类对象,即不能直接调用类的构造函数。
a.最容易想到的是将构造函数设置为私有,在构造函数私有以后,无法在类的 外部调用构造函数来构造对象,只能使用new运算符来建立对象。然而,我们已经知道,
new运算符分为两步。C++提供new运算符的重载,其实是只允许重载opetator new()函数,而operator new()函数只分配内存,无法提供构造功能。因此,这种方法是不可
以的。
b.当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造对象,当对象使用完毕后,编译器会调用析构函数来释放栈对象所占的内存空间,编译
器管理了对象的整个生命周期。如果编译器无法调用析构函数,将会怎么样呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以:编译器在为类对
象分配栈空间时,先会检查类的所有析构函数的访问性,如果类的析构函数是私有的,则编译器不会在栈空间上为类的对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。看个例子吧:
class A {
public:
A(){}
void destory(){delete this;}
private:
~A(){}
};
试着用A a来建立对象,编译报错,提示析构函数无法访问。这样就只能通过new关键字符来建立对象,构造函数是共有的,可以直接调用。但是类中必须提供一个
destory()函数,来进行内存空间的释放。类对象使用完毕后,必须调用destory()函数。
但是呢?将析构函数设置为私有的缺陷:
Ⅰ.无法解决继承问题。如果类A作为其它类的基类,我们通常要将析构函数设置为virtual,然后在子类重写,以实现多态,目的是为了释放子类中的所有资源,否
则会释放不干净。还好C++提供了protected,将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。
Ⅱ. 类的使用很不方便,使用new建立对象,却使用destory释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,
而析构函数类外不可以访问),这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使
用一个函数来构造,使用一个函数来析构。
class A {
protected:
A(){}
~A(){}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};
这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。
BB.再来看看只能在栈上分配类对象吧!
只有使用new运算符,对象才会在堆上建立,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。因此,可以将operator new()设为私有即可禁止对象被new在
堆上。
class A {
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
Conclusion:思考:在栈上和堆上建立对象的相同点和不同点?
相同点:无论是A a还是A* a = new A();都会由编译器在类外部调用类的构造函数来创建对象,同样在释放内存空间时也是会由编译器调用类的析构函数来释放。
不同点:
a.1:栈对象创建过程中,分配内存然后调用构造函数;对象释放过程中,释放内存然后析构函数的调用都是编译器自动完成的。
a.2:而堆对象的创建过程中需要先由编译器显式的调用operator new()函数进行内存分配然后调用构造函数,堆的释放过程中需要先由编译器显式的调用
operator delete()函数,然后编译器自动调用析构函数。
b.1:为了只能在栈上创建对象,那么我们就可以将operator new()函数和operator delete()函数进行重载,这两个函数不做任何事情不分配内存,并将他们设
置为private,那么编译器就无法调用这两个函数。也就不能再堆上创建对象了,此时的构造函数和析构函数还是可以被编译器自由的调用创建栈对象。
b.2:为了只能在堆上创建对象,那么我们就必须让编译器不能自动调用构造函数和析构函数(虽然编译器能够自动分配内存但是没有调用构造函数,栈对象就没办
法创建完成)。所以将构造函数和析构函数都设置为protected,这样编译器就不能调用了,但是创建堆对象也要由编译器调用构造函数和析构函数怎么办呢?得益于堆对象
的创建过程需要调用类中定义的函数,那么我们可以定义两个函数create和destroy函数分别在内部调用构造函数和析构函数,这样就可以由编译器在堆上创建和释放对
象。
9.浅拷贝和深拷贝有什么区别?
1.浅拷贝只复制指向某个对象的指针,而不复值对象本身,新旧对象还是共享同一块内存;
2.深拷贝会创造一个相同的对象,新对象与旧对象不共享内存,修改新对象不会影响旧对象的值;
值得注意的是:当类中有指针时,我们需要重载拷贝构造函数和赋值运算符,否则就是浅拷贝了,浅拷贝会带来内存重复释放的问题。
10.什么是字节对齐?为什么需要字节对齐?字节对齐的原则是什么?
1.什么是字节对齐?
字节对齐是字节按照一定规则在空间上排列。
现代计算机中内存空间的基础单元是字节(byte),从理论层面上讲,对于任何数据类型的变量的访问,都可以从任何地址开始。但是物理层面实现时,访问特定类型变量
的时候经常需要在特定的内存地址访问,一般是以2/4/8的倍数的字节块来访问内存。
2.那为什么要进行字节对齐呢?
一句话来说,以牺牲空间的方式来减小时间的消耗。甚至有些系统,如果变量的位置没有对齐的话就会访问报错。
3.1对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
a.char:偏移量必须为sizeof(char)即1的倍速;
b.short:偏移量必须为sizeof(short)即2的倍数;
c.int:偏移量必须为sizeof(int)即4的倍数;
d.float:偏移量必须为sizeof(float)即4的倍数;
e.double:偏移量必须为sizeof(double)即8的倍数;
f.long long:偏移量必须为sizeof(long long)即8的倍数;
g.long double:偏移量必须为sizeof(long double)即8的倍数;
h.如果类中有虚函数,那么在类对象中会有一个指向虚函数表的指针,32位操作系统下,指针为4,在64位操作系统下,指针为8;
3.字节对齐的原则:
先来看一个这样的例子:sizeof(MyStruct)等于多少呢?也许你会这样求sizeof(double)+sizeof(char)+sizeof(int)=8+4+1=13;但是我们在VS上运行的时
候我们会发现sizeof(MyStruct)=16
struct MyStruct {
double dda1;
char dda;
int type
};
为上面的结构分配空间的时候,VS根据成员变量出现的顺序和对齐方式:
Ⅰ.先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;
Ⅱ.接下来为dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该
成员变量占用sizeof(char)=1个字节;
Ⅲ.接下来未type分配对象,此时可以分配的地址相对于结构的起始地址的偏移量是9,不是sizeof(int)=4的倍数,因此VS自动填充3个字节(这3个字节什么都不存放),
此时地址的偏移量就为12了,再为type分配4个字节就是16个字节了。
所以整个结构的大小为:sizeof(MyStruct)=8+1+ 3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。
再举个例子:
struct MyStruct {
char dda;
double dda1;
int type
};
1******* 11111111 1111按理来说应该是20个字节,但是字节对齐仍有一个原则:结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以需
要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
所以正确结果为1******* 11111111 1111**** = 24个字节;
------------------------------------------------------------------------
VS对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。
VS中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:
A.如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式;(n过大则不起作用)
B.如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式;(n小点的话就要遵守n的规则)
C.结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数倍数;否则必须为n的倍数(两者相比取小);
#pragma pack(push) //保存对齐状态
#pragma pack(4) //设定为4字节对齐
struct test {
char m1; //1<4
double m4; //8<4
int m3; //4<=4
};
#pragma pack(pop) //恢复对齐状态
由于n<4,所以偏移量为4的倍数即可,不用满足默认的对齐方式;所以1*** 1111 1111 1111 共16个字节即可;
#pragma pack(8)
struct S1{
char a; //1<8
long b; //4<8
};
由于n>4,所以使用默认的字节方式即可,所以1*** 1111,分配8(8也是4的倍数)字节即可;
--------------------------------------涉及到结构体成员对齐
#pragma pack(8)
struct S2 {
char c; //1
struct S1 d; //8
long long e; //8
};
成员对齐有一个重要条件:即每个成员分别对齐,每个成员按照自己的方式对齐。也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。对齐
规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8)中较小的一个对齐。并且结构的长度必须为所用过的所有对齐参数的整数倍,不
够就补空字节。
S2中:
char c按照1字节对齐;
struct S1 d是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的对齐方式就是max(sizeof(类型)),那么d就是按照sizeof(long)=4对齐的;
long long e按照默认8字节对齐;
1*** 1*** 1111**** 11111111
Note:
1.按几字节对齐,就是偏移量为几的倍数;
2.对齐后的长度必须是成员中的最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐;
11.malloc是如何申请内存的?
1.malloc()并不是系统调用,而是C库里的函数,用于动态分配内存;
2.malloc申请内存的时候,会有两种方式向操作系统申请堆内存:
a.通过brk()系统调用从堆分配内存;
b.通过mmap()系统调用在文件映射区分配内存;
a.这种方式很简单,就是通过brk()函数将堆顶指针向高地址移动,获得新的内存空间;
b.通过mmap()系统调用中私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区"偷"了一块内存;
3.那什么时候场景下malloc()会通过brk()分配内存?什么场景下通过mmap()分配内存?
malloc()源码里默认定义了一个阈值:如果用户分配的内存小于128KB,则通过brk()申请内存;如果用户申请的内存大于128KB,则通过mmap()申请内存;
4.malloc()分配的是物理内存吗?
不是的,malloc()分配的是虚拟内存,如果分配后的虚拟内存没有被访问的话,是不会映射到物理内存中,这样就不会占用物理内存。
5.free释放的内存会归还给操作系统吗?
a.malloc通过brk()方式申请的内存,free释放内存的时候,并不会把内存归还给操作系统,而是缓存在malloc的内存池中,待下次使用;
b.malloc通过mmap()方式申请的内存,free释放的时候,会把内存归还给操作系统,内存得到真正的释放;