✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/fYaBd
📚专栏简介:在这个专栏中,我将会分享 C++ 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
28. 什么是内存池,如何实现?
内存池(Memory Pool) 是一种内存分配方式。通常我们习惯直接使用 new、malloc 等申请内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等 (一般情况下) 的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块, 若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。
这里简单描述一下《STL 源码剖析》中的内存池实现机制:
allocate 包装 malloc,deallocate 包装 free
一般是一次 20*2 个的申请,先用一半,留着一半,为什么也没个说法,侯捷在 STL 那边书里说好像是 C++ 委员会成员认为 20 是个比较好的数字,既不大也不小。
-
首先客户端会调用 malloc() 配置一定数量的区块(固定大小的内存块,通常为 8 的倍数),假设 40 个 32 bytes 的区块,其中 20 个区块(一半)给程序实际使用,1 个区块交出,另外 19 个处于维护状态。剩余 20 个(一半)留给内存池,此时一共有(
20*32 byte
)。 -
客户端之后有有内存需求,想申请(
20*64 bytes
)的空间,这时内存池只有(20*32 bytes
),就先将(10*64 bytes
)个区块返回,1 个区块交出,另外 9 个处于维护状态,此时内存池空空如也。 -
接下来如果客户端还有内存需求,就必须再调用 malloc() 配置空间,此时新申请的区块数量会增加一个随着配置次数越来越大的附加量,同样一半提供程序使用,另一半留给内存池。申请内存的时候用永远是先看内存池有无剩余,有的话就用上,然后挂在 0-15 号某一条链表上,要不然就重新申请。
-
如果整个堆的空间都不够了,就会在原先已经分配区块中寻找能满足当前需求的区块数量,能满足就返回,不能满足就向客户端报 bad_alloc 异常。
allocator 就是用来分配内存的,最重要的两个函数是 allocate 和 deallocate,就是用来申请内存和回收内存的,外部(一般指容器)调用的时候只需要知道这些就够了。
内部实现,目前的所有编译器都是直接调用的 ::operator new() 和 ::operator delete(),说白了就是和直接使用 new 运算符的效果是一样的,所以老师说它们都没做任何特殊处理。
其实最开始 GC2.9 之前
new 和 operator new 的区别:new 是个运算符,编辑器会调用 operator new(0)。
operator new() 里面有调用 malloc 的操作,那同样的 operator delete() 里面有调用的 free 的操作。
GC2.9 下的 alloc 函数的一个比较好的分配器的实现规则如下:
维护一条 0-15 号的一共 16 条链表,其中 0 号表示 8 bytes ,1 号表示 16 bytes,2 号表示 24 bytes。。。。而15 号表示 16* 8 = 128 bytes。
如果在申请内存时,申请内存的大小并不是 8 的倍数(比如 2、4、7、9、18 这样不是 8 的倍数),那就找刚好能满足内存大小的链表。比如想申请 12 个大小,那就按照 16 来处理,也就是找 1 号链表了;想申请 20 ,距离它最近的就是 24 了,那就找 2 号链表。
只许比所要申请的内容大,不许小!
但是现在 GC4.9 及其之后 也还有 alloc 函数,只不过已经变成 _pool_alloc 这个名字了,名字已经改了,也不再是默认的了。
你需要自己手动去指定它可以自己指定,比如:
cpp
vector<string,__gnu_cxx::pool_alloc<string>> vec;
这样来使用它,等于兜兜转转又回到以前那种对 malloc 和 free 的包装形式了。
29. C**++ 如何让类对象只能在堆(栈)上分配空间?**
在 C++ 中,类的对象建立分为两种,一种是静态建立,如 A a;另一种是动态建立,如 A* ptr = new A;这两种方式是有区别的:
-
**静态建立类对象:**是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
-
**动态建立类对象:**是使用 new 运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行 operator new() 函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
1、只能在堆上分配类对象,就是不能静态建立类对象,即不能直接调用类的构造函数。
容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用 new 运算符来建立对象。然而,前面已经说过,new 运算符的执行过程分为两步,C++ 提供 new 运算符的重载,其实是只允许重载 operator new() 函数,而 operator new() 函数只用于分配内存,无法提供构造功能。因此,这种方法不可以。
当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?
比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上了。
cpp
class A
{
public:
A(){}
void destory(){delete this;}
private:
~A(){}
};
上述方法的缺点:
-
无法解决继承问题。如果 A 作为其它类的基类,则析构函数通常要设为 virtual,然后在子类重写,以实现多态。因此析构函数不能设为 private。还好 C++ 提供了第三种访问控制,protected。将析构函数设为 protected 可以有效解决这个问题,类外无法访问 protected 成员,子类则可以访问。
-
类的使用很不方便,使用 new 建立对象,却使用 destory 函数释放对象,而不是使用 delete。(使用 delete 会报错,因为 delete 对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。
为了统一,可以将构造函数设为 protected,然后提供一个 public 的 static 函数来完成构造,这样不使用 new,而是使用一个函数来构造,使用一个函数来析构。
cpp
class A
{
protected:
A(){}
~A(){}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};
2、只能在栈上分配类对象
只有使用 new 运算符,对象才会建立在堆上,因此,只要禁用 new 运算符就可以实现类对象只能建立在栈上。虽然你不能影响 new operator 的能力(因为那是 C++ 语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将 operator new() 设为私有即可禁止对象被 new 在堆上。
cpp
class A
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
30. 怎么判断内存有没有申请成功呢?
使用 new 操作符来动态申请内存时,如果申请失败,会抛出 std::bad_alloc 异常,因此可以使用 try-catch 语句来捕获这个异常,判断是否申请成功。
使用 malloc 函数来动态申请内存时,如果申请失败,会返回一个空指针(NULL),因此可以通过判断返回的指针是否为 NULL 来判断申请是否成功。