四、内存管理

(1)变量的存储位置?程序的内存分配?

在C++中,内存区分为5个:堆、栈、自由存储区、全局/静态存储区、常量存储区。new是在自由存储区开辟内存。
在C中,内存区分为堆、栈、全局/静态存储区、常量存储区。malloc是在堆上开辟内存。
一个由C++ *编译过* 的程序占用的内存分为以下几个部分:
(1)栈区stack:由编译器自动分配释放。存放函数的参数值、局部变量的值。

(2)堆区heap:一般由程序员分配释放,若程序员不释放,程序结束时由操作系统释放。

(3)全局区(静态区static):全局变量和静态变量的存储是放在一起的。//在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,
在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0。

(4)文字常量区:常量字符串放在这里。程序结束时由系统释放

(5)程序代码区:存放函数体的二进制代码。

四、C++内存管理常见面试题_段错误

(2)内存的分配方式有几种?

1. 在栈上分配:在执行函数时,局部变量的内存都可以在栈上分配,函数结束时会自动释放;栈内存的分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限;

2. 从堆上分配:就是那些由new分配的内存块,释放由程序员delete释放,编译器不自动释放。

//3. 从自由存储区分配:如果说堆是操作系统维护的一块内存,那么自由存储区就是C++中通过new和delete动态分配和释放对象的抽象概念。需要注意的是,自由存储区和堆比较像,但不等价。

4. 从常量存储区分配:特殊的存储区,存放的是常量,不可修改;

5. 从全局/静态存储区分配:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0。

(3)堆和栈的区别?

1. 分配和管理方式不同:
堆是动态分配的,其空间的分配和释放都是由程序员控制
栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成,比如局部变量的分配;动态分配由alloca()函数进行分配,由编译器释放;

2. 产生碎片不同:
对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,产生大量碎片,是程序效率降低;
对栈来说,不存在碎片问题,因为栈具有先进后出的特性;

3. 生长方向不同:
堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长;
栈是向着内存地址减小的方向增长的,从内存的高地址向低地址方向增长;

4. 申请大小限制不同:
栈顶和栈底是预设好的,大小固定;
堆是不连续的内存区域,其大小可以灵活调整

(4)说一说操作系统中的内存结构

四、C++内存管理常见面试题_开发语言_02

一个程序本质上都是由BSS段、data段、text段三个组成的。
可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。

1. BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。
BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。

2. 数据段:存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配

3. 代码段:存放程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量

4. text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。
bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。
数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。


可执行程序在运行时又多出两个区域:栈区和堆区。
1. 栈区:由编译器自动释放,存放函数的参数值、局部变量等。
* 每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间。
* 每调用一个函数一个新的栈就会被使用。
* 栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

2. 堆区:用于动态分配内存,位于BSS和栈中间的地址区域,由程序员申请分配和释放。
* 堆是从低地址位向高地址位增长,采用链式存储结构。
* 频繁的malloc/free造成内存空间的不连续,产生碎片。
* 当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。

(5)内存泄漏的场景有哪些?

场景:
1. 在堆中创建对象分配内存,但未显式释放内存
2. 在构造函数中动态分配内存,但未在析构函数中正确释放内存
3. 没有将基类的析构函数定义为虚函数
4. 释放两次相同的内存

判断和定位内存泄漏的方法:
1. 在Linux系统下,可以使用valgrind、mtrace等内存泄漏检测工具。
2. 另一方面,我们在写代码时可以添加内存是申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄漏。
最好用智能指针代替new/delete,就不用这么麻烦。

(6)静态内存分配和动态内存分配的区别?

占用CPU/不占   栈/堆  编译器/程序员  编译时/运行时  安全/内存泄漏  内存大小固定/不固定

1. 静态内存分配是在编译时期完成的,不占用CPU资源,动态内存分配是在运行时期完成的,分配和释放需要占用CPU资源。

2. 静态内存分配是在栈上分配的;动态内存分配是在堆上分配的。

//3. 静态内存分配不需要指针或引用类型的支持;动态内存分配需要。

//4. 静态内存分配是按计算分配的,在编译前确定内存块的大小;动态内存分配是按需要分配的。

5. 静态内存分配是把内存的控制权交给了编译器;动态内存分配是把内存的控制权给了程序员。

6. 静态内存分配的运行效率比动态内存分配高;动态内存分配不当可能造成内存泄漏。

(7)如何构造一个类,使得只能在堆上或只能在栈上分配内存?

只能在堆上分配内存:将析构函数声明为private;
只能在栈上生成对象:将new和delete重载为private。

(8)浅拷贝和深拷贝有什么区别?

区别与联系:
1.二者都是为了实现复制的功能;
2.在不涉及指针,空间分配等资源问题时,深浅拷贝无区别;
3.深拷贝重新申请了空间,改变被拷贝对象时拷贝对象不会变化;

为什么需要深拷贝:因为浅拷贝可能会造成内存泄露
拷贝发生于用被拷贝对象实例化新对象,过程中被拷贝对象的属性值会一一赋值给新对象。

那么当我们的类中存在指针或者动态的内存分配时,使用普通拷贝(浅拷贝,默认拷贝)只会将那块内存的位置告知当前对象,并不会重新为新对象分配内存。当程序运行结束后,两个对象分别析构,此时这同一块内存将被释放两次。释放第二次时,析构对象找不到需要释放的内存(在第一次析构时已经释放),就会导致内存泄漏。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享一块内存;
而深拷贝会创造一个相同的对象,新对象与原对象不共享内存,修改新对象不会影响原对象。

//浅拷贝:简单的赋值拷贝操作
//深拷贝:在堆区重新申请空间,进行拷贝操作
#include<iostream>
using namespace std;

class A {
public:
int* data;
int size;

A(){}; //缺省构造
A(int _size) : size(_size) { //有参构造,初始化构造
cout << "初始化" << endl;
data = new int[size];
}

//在用一个对象初始化时,开辟新的内存,避免指向同一块位置
A(const A& _A) : size(_A.size) { //拷贝构造、复制构造
cout << "deep copy" << endl;
data = new int[size];
for (int i = 0; i < _A.size; i++)
{
data[i] = _A.data[i];
}
}

~A() {
delete [] data;
}
};

int main(){
A a(5), b = a;
cout << "两个对象的数据内存位置:" << endl;
cout << a.data << " " << b.data << endl;
a.data[0] = 10;
cout << "更改一个对象的值,另一个并不会发生改变:" << endl;
cout << a.data[0] << " "<<b.data[0] << endl;

return 0;
}

四、C++内存管理常见面试题_开发语言_03

(9)字节对齐的原则是什么?

四、C++内存管理常见面试题_c++_04

(10)结构体内存对齐问题

​结构体内存对齐问题-帅地玩编程 (iamshuaidi.com)​

结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

c++11以后引入两个关键字 alignas与 [alignof]。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式。

(11)什么是段错误?什么时候发生段错误?

段错误是指程序尝试访问一段不可访问的内存地址。

在类Unix系统中,当出现段错误时,系统发送信号量SIGSEGV给产生段错误的进程;在Windows系统中,系统会发送异常STATUS_ACCESS_VIOLATION给产生段错误的进程。

产生段错误的原因:
程序运行过程中能访问到的内存空间主要有栈和堆。栈存放了函数的本地变量,堆是程序运行过程中能够自由分配和使用的内存空间。产生段错误和栈、堆的访问密切相关。

产生段错误的原因主要有:
1. 解引用空指针
2. 访问不可访问的内存空间(如内核空间)
3. 访问不存在的内存地址
4. 试图写一个只读内存空间(如代码段)
5. 栈溢出(函数递归调用)
6. 使用未初始化的指针(定义时没有初始化或者已经回收)

避免段错误:
1. 定义指针后初始化
2. 数组下标是否越界
3. 在堆上分配空间是否足够(内存限制)
4. 变量处理时格式控制是否合理

1. new/delete、malloc/free

(1)new/delete和malloc/free之间有什么关系?

在C++中,内存区分为5个:堆、栈、自由存储区、全局/静态存储区、常量存储区。new是在自由存储区开辟内存。
在C中,内存区分为堆、栈、全局/静态存储区、常量存储区。malloc是在堆上开辟内存。
int *p1 = new int;//创建一个整形指针,返回一个指向该对象的地址
int *p2 = new int();//同上,并且将指针p2指向的地址的值初始化为0
int *p3 = new int(1024);//同上,并且将指针p3指向的地址的值初始化为1024

delete p1,p2,p3;//释放指针pi(i=1,2,3)指向的int型对象所占的空间

//此时pi尽管没有意义,但依然存放了他所指向对象的地址,
//而pi所指向内存已被释放,不再有效

//释放后,对其置空,清除表明指针不指向任何对象
p1 = NULL;
p2 = NULL;
p3 = NULL;

「注意」:delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该讲该指针指向NULL。
int *p = new int[];//开辟一个数组。令p指向该数组
int *p = new int[length];//开辟一个长度为length的数组,未初始化。
int *p = new int[length]();//开辟一个长度为length的数组,且初始化为0。

//动态开辟的数组释放与一般对象的释放不一样
delete[] p;//释放p所指向的数组
p = NULL;
malloc/free:
两个函数在头文件stdlib.h中
void *malloc(size_t size);
void free(void *pointer);
因为malloc()函数的返回值类型为void *,所以需要在函数前面进行相应的强制类型转换。
int *p = (int *)malloc(100);//开辟大小为100字节的内存
int *p = (int *)malloc(25 * sizeof(int));//开辟25个int大小的内存

//验证内存是否分配成功
if (NULL == p)
printf("Out of memory!\n");

free(p);//释放内存

​new/delete和malloc/free之间有什么关系?-帅地玩编程 (iamshuaidi.com)​

共同点:
1. 都可以在堆上申请动态内存和释放内存。


区别:
1. new/delete是C++的运算符;malloc/free是库函数。//运算符可以重载,库函数不行。

2. new与delete直接带具体类型的指针,malloc和free返回void类型的指针,所以需要在函数前面进行相应的强制类型转换。

3. new会自动执行构造函数,能满足C++语言动态内存分配和初始化工作,delete会自动执行析构函数,能完成C++语言的清理和释放内存的工作。//对于非内部数据结构(诸如类的实例)而言,光用malloc/free无法满足动态内存的要求。由于malloc/free不再编译器控制权限之内,不能自动执行构造函数和析构函数,故无法满足动态创建对象的需求。

四、C++内存管理常见面试题_开发语言_05

(2)delete与delete []有什么区别?

int *a = new int(1);
delete a;

int *b = new int[12];
delete [] b;
对于new分配的单个对象使用delete;
对于new分配的对象数组使用delete [],逐个调用数组中对象的析构函数,从而释放所有内存。

对于标准数据类型,两个都可以用
对于自定义数据类型(类),用delete []
/**
* difference of delete and delete[]
*/
#include <iostream>
using namespace std;

class A{
private:
int m_data;
public:
A() { }
~A() { cout << "delete object." << endl; }
};

int main(){
A *a = new A[10];
//delete []a;
delete a;

return 0;
}

四、C++内存管理常见面试题_动态内存分配_06


四、C++内存管理常见面试题_开发语言_07


四、C++内存管理常见面试题_初始化_08

(3)内存块太小导致malloc和new返回空指针,该怎么处理?

对于malloc来说,需要判断其是否返回空指针,如果是则马上用return语句终止该函数或者exit终止该程序;
对于new来说,默认抛出异常,所以可以使用try...catch...代码块的方式:
try {
int *ptr = new int[10000000];
} catch(bad_alloc &memExp) {
cerr << memExp.what() << endl;
}

(4)在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?

不能。

malloc/free主要为了兼容C,new和delete完全可以取代malloc/free的。malloc/free的操作对象都是必须明确大小的。

而且不能用在动态类上。new和delete会自动进行类型检查,也不需要自己明确内存大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。

当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。
malloc与free是c++、c语言的标准库函数,new、delete是c++的运算符。它们都可用用申请动态内存和释放内存。对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此c++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。