C++基础面试总结(二)

C++基础面试总结(二)

1.c++内存管理

在C++中内存结构分为:堆区、栈区、自由存储区、全局/静态存储区、常量存储区、代码区。

  • 堆:堆区是由new分配的内存块,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减,且向上增长。一般由程序员手动分配释放,如果没手动释放,在程序结束时可由OS释放。

  • 栈:存储函数的参数、局部变量、返回值。函数执行结束后这些内存会自动被释放,不由程序员控制,效率高,但是大小有限。

  • 自由存储区:malloc/free操作的内存,与堆相似。

  • 全局/静态存储区:这块内存是在程序编译的时候就已经分配好的,在程序整个运行期间都存在。例如全局变量,静态变量。(在C中,全局变量分为已初始化和未初始化,C++中无此区分)

  • 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。

  • 代码区:存储程序的机器代码和程序指令。

//例:
void fun(){
	int* p=new int[5]; //new表示我们从堆上申请的内存,而p是局部变量,那么p是存储在栈上的。所以,此句话的意思是:在栈中存放了一个指向一块堆内存的的指针p。
}

补充1.堆栈的区别

1.管理方式:栈由编译器自动管理,无需我们手工控制;堆的释放工作由程序员控制,容易产生内存泄漏。
2. 空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间(扩展虚拟内存),但对于栈来说,一般都是有一定空间大小,例如,在VC6默认的栈空间大小是1M。当然,我们可以在编译器中修改栈的大小,但可能增加内存的开销和启动时间。
3. 碎片问题:对于堆,频繁的操作势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈,不会存在这个问题,因为栈是先进后出的队列,不可能有一个内存块从栈中间弹出,在它弹出之前,在其上面的后进的栈内容已经被弹出。
4. 生长方向:对于堆来讲,生长方向是向上的,即向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,即内存地址减小的方向增长。
5. 分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数(扩展malloc、calloc、realloc函数)进行分配,但栈的动态分配和堆不同,栈的动态分配是由编译器进行释放。
6. 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持(分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行),所以栈的效率比较高。堆则是C/C++函数库提供的,库函数会按照一定的算法在堆内存中搜索可用的足够大小空间,如果没有(可能原因是内存碎片太多),操作系统会去增加程序数据段的内存空间。

补充2.C的内存结构

总体上与C++的类似,分为:堆区、栈区、.bss(未初始化数据段)、.data(已初始化数据段)、代码段
.bss区:存入的是未初始化的变量和静态变量
.data区:初始化的全局变量、静态变量、常量
在这里插入图片描述

2. malloc/free和new/delete区别

  1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  2. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
  3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void ,需要通过强制类型转换将void指针转换成我们需要的类型。
  4. new内存分配失败时,会抛出bac_alloc异常,malloc分配内存失败时返回NULL。

补充1 .new/delete运算符的原理

new过程

1.内存分配。调用相应的 operator new(size_t) 函数(通常底层用malloc实现),动态分配内存。如果 operator new(size_t) 不能成功获得内存,则调用 new_handler()函数用于处理new失败问题。如果没有设new_handler() 函数或者 new_handler() 未能分配足够内存,则抛出 std::bad_alloc 异常。
2. 构造函数。在分配到的动态内存块上 初始化相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。

delete过程

delete先调用析构函数,然后调用operator delete函数释放内存(通常底层用free实现)。

补充2.malloc的内存分配机制

过程

首先扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存。如果这一内存块的尺寸正好与要求相当,就将它返回给调用者,如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中。

malloc函数原型

     void* malloc(unsigned int size)

功能

在堆上动态开辟内存空间.

特点

1、返回值为void*,(void* 类型可以强制转换为任何其它类型的指针,但反过来就不行了);
2、需要具体指定要分配空间的大小size,且size类型为无符号整型;
3、它允许申请0个长度的内存(这点很有意思吧);
4、其申请到的空间逻辑上是连续的,物理上是离散的(链表形式管理)。
5、申请失败的时候,返回的是NULL

补充3.free释放空间的时候是怎么知道要free的空间的大小呢?

真正的内存管理如申请/释放等,并不是由malloc或者free等库函数来负责的,而是交由操作系统去完成,它们只是维护一个空闲的链表式的内存块,理解这一点是问题的关键。
例如:要申请sizeof(int)*100大小的内存空间,虽然返回的是内存大小是400,但实际上,操作系统分配时候,会多出一块用于存储内存大小的类似链表head头节点的东东(上cookie),这个节点存储的是空间的首地址及分配内存的大小。当用户调用free函数的时候,其实它也不知道要释放内存的大小,它只需改变head头结点(上cookie)里的内存的大小就可以了,具体内存空间的释放由操作系统去完成。

3. 内存泄漏

1.new/malloc的内存使用完没有进行释放,会造成的堆内存的泄漏
2.双指针释放同一个指针,野指针
3.补充:栈溢出:递归调用层数过多。

4.深浅拷贝

深拷贝指拷贝时对象资源重新分配,两个对象的资源内存不同,释放一个对象资源不会影响另一个。
浅拷贝指拷贝时对象资源没有重新分配,两个对象均指向同一内存空间,释放一个对象的资源,另一个对象的资源也没了,造成野指针

补充1:野指针

说明

野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存。

成因

1)指针变量没有被初始化;
2)指针指向的内存被释放了,但是指针没有置NULL ,free和delete只是把指针指向的内窜释放掉了,并没有把指针本身干掉,此时指针指向的是垃圾内存;
3)指针超过了变量了的作用范围,比如b[10],指针b+11.

解决(养成良好的编码习惯)

1)当没有指针指向时,要置为NULL
2)给指针分配空间,检查是否分配成功,再初始化。
3)使用时,注意不要越界
4)使用完之后,free。free之后再次置NULL

5.智能指针(手撕)

内存泄漏

内存泄漏是指由于疏忽或错误造成了程序未能释放不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制;

智能指针

介绍:智能指针就是一个类,其作用是管理一个指针,利用类的析构和构造函数(进出作用域自动杯编译器调用)的机制,解决资源自动回收的问题。
C++里面的四个智能指针: auto_ptr(被11弃用), unique_ptr , shared_ptr, weak_ptr

//例:自己实现一个简易的智能指针:
class smartPtr{
	private:
		int* p;
	public:
		smartPtr(){}
		smartPtr(int* a){
			p=a;
		}
		//重载->和*符号,使其看起来像一个指针
		int* operator->(){
			return p;
		}
		int& operator*(){
			return *p;
		}
		~smartPtr(){
			delect p;
		}
}

auto_ptr(C++11已经弃用)

采用所有权模式。缺点是:存在潜在的内存崩溃问题.

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr <string> p2;p2 = p1; 
//auto_ptr不会报错.此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。

unique_ptr(替换auto_ptr):

在auto_ptr的基础上,禁止使用拷贝构造和重载=运算符。增加了一个move操作,允许将原来的剪切给新的指针。unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露

unique_ptr<string> p3 (new string ("auto"));
unique_ptr<string> p4;
p4 = p3;//此时会报错!!
//编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

shared_ptr: 带引用计数的智能指针

  • shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
  • shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。
  • 除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
//例:
shared_ptr<int> sptr1(new int(3));
shared_ptr<int> sptr2(new int(4));
shared_ptr<int> sptr3=sptr2; //此时引用计数加一

weak_ptr(补充shared_ptr):

  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。
  • weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,
  • 只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
//例:
	shared_ptr<int> sptr(new int(3));
	shared_ptr<int> sptr2=sptr;
	
	weak_ptr<int> wptr=sptr; //weak_prr必须从一个shared_ptr产生;
	
	if(!wptr.expired()){ //判断wptr指针是否与一个shared_ptr关联
		shared_ptr<int> sptr3=wptr.lock(); //将wptr指针升值为shared_ptr
	}

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值