面试题
Posted weiweng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题相关的知识,希望对你有一定的参考价值。
以下内容均摘抄自他人博客,正确性有待考察,请以质疑的态度阅读学习,若有错误请留言指正
stl用过哪些容器?
- Vector:动态数组
- List:双向链表
- Deque:与vector类似,但支持双端操作。
- Set:关联容器-集合,底层红黑树实现。
- Map:关联容器-键值对,底层红黑树实现。
- Stack:栈,默认由deque封装得到。
- Queue:队列,默认由deque封装得到。
- C++11新加入的有:array、forward_list、unordered_set、unordered_map(这两个使用hash表来存储)
stl内存分配器的设计与实现
内存分配器(Memory Allocator)负责内存管理,实现动态内存的分配和释放。内存分配器分为两级。第一级分配器直接调用C函数分配内存,第二级分配器则采用内存池来管理内存。如果申请的内存块足够大,那么启动第一级分配器,否则启动第二级分配器。这种设计的优点是可以快速分配和释放小块内存,同时避免内存碎片;缺点是内存池的生命周期比较长,并且很难显式释放。
第一级分配器只是简单的调用函数malloc()、realloc()和free()。为了保证内存按照指定字节数对齐,则需要调用函数_aligned_malloc()、_aligned_realloc()和_aligned_free(),因此实际分配的内存块可能大于申请内存的大小。
第二级分配器需要维护16个空闲块链表和一个内存池。每个链表中的空闲块的大小都是固定的,默认对齐字节数为8,则各个链码空闲块大小依次为n、2n、3n、4n、5n、6n、7n、8n、9n、10n、11n、12n、13n、14n、15n、16n。内存池由两个指针来描述,free_start记录起始地址,free_end记录结束地址。另外两个变量heap_size和used_size分别纪录堆大小和已用内存大小。
内存池管理的内存块大小只有固定的16个规格, 当所需内存块大于16n时,则使用第一级分配器进行内存分配。否则,按照以下步骤进行内存分配:
- 申请内存的大小上调至8的倍数,根据此大小查找对应的空闲链表;如果空闲链表中有可用的内存块,则直接返回此空闲块,并从空闲链表中删除该块,否则继续下面的步骤;
- 计算内存池中所剩空间的大小,如果足以分配20个内存块,则从中取出20个内存块,调整内存池起始地址,返回第一个内存块,并将剩余的19个块并入空闲链表,否则继续下面的步骤;
- 如果剩余空间足以分配至少1个内存块,则从中取出尽可能多的内存块,调整内存池起始地址,返回第一个内存块,并将剩余的内存块并入空闲链表,否则继续下面的步骤;
- 如果内存池中还有一些内存,则将剩余空间并入其对应大小的空闲链表中;向系统申请一个较大的内存块,如果申请成功,返回第一个内存块,调整内存池起始地址,否则继续下面的步骤;
- 山穷水尽,只能遍历后面更大的空闲链表,如果存在更大的空闲内存块,则从空闲链表中删除该块,返回该块首地址,并将剩余的部分内存交给内存池管理,否则最后尝试使用第一级配置器。如果还是失败则报错。
内存池向系统申请的内存空间,在使用过程中会被划分为更小的内存块,而这些小内存块的使用和归还几乎是随机的。如果试图对这些小内存块进行合并和释放,其高昂的代价会大幅降低内存池的性能。但在内存池的已用内存大小为0时,释放内存是安全的。内存分配器维护一个指针链表,用于内存空间的统一释放。
解决Hash冲突的几种方法
- 开放地址法:线性探测、线性补偿探测法、伪随机探测
- 拉链法:冲突的地方建立链表保存冲突数据。
- 再散列:使用其他哈希函数计算地址,直到无冲突为止。
- 建立公共溢出区:再建立一个存储向量来保存冲突的数据。
虚指针工作原理,解释了一下虚函数列表
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。
点击链接跳转
构造函数、析构函数能不能是虚函数?
构造函数不能声明为虚函数的原因是:
- 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
- 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。 - 析构函数设为虚函数的作用:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
虚函数可不可以是内联函数
内联函数不能为虚函数,原因在于虚表机制需要一个真正的函数地址,而内联函数展开以后,就不是一个函数,而是一段简单的代码(多数C++对象模型使用虚表实现多态,对此标准提供支持),可能有些内联函数会无法内联展开,而编译成为函数。
vector怎么释放内存
采用 Vector存储一些数据,但是发现在执行 clear() 之后内存并没有释放。
方法:vector 的 clear 不影响 capacity , 应该 swap 一个空的 vector。交换后临时变量注销,则vector的内存分配得到释放。
赋值运算符重载 注意点
什么样的对象才能作为STL容器的元素
元素需要具备构造、析构、赋值、拷贝等函数,关联容器还要求元素可以比较大小。
引申:指针作为容器的元素时,额外的内存管理问题直接以普通指针作为容器的元素时。我们知道,指针就是一个地址值,因此以指针为元素的容器存放的就是一些内存地址,而不是真正的数据。但是,容器只负责指针元素一级的内存问题,即它负责指针元素本身的内存分配和释放,而不会负责指针指向对象的内存管理事务,因为那是程序员的责任。
点击链接跳转
const 有什么用途
- 定义只读变量,即常量
- 修饰函数的参数和函数的返回值
- 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值
指针和引用的区别
- 引用是变量的一个别名,内部实现是只读指针
- 引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
- 引用不能为NULL,指针可以为NULL
- 引用变量内存单元保存的是被引用变量的地址
- “sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
- 引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
- 引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址
C++中有了malloc / free , 为什么还需要 new / delete
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
- 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。 对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
- 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
编写类String 的构造函数,析构函数,拷贝构造函数和赋值函数
#include <iostream>
class String
{
public:
String(const char *str=NULL);//普通构造函数
String(const String &str);//拷贝构造函数
String & operator =(const String &str);//赋值函数
~String();//析构函数
protected:
private:
char* m_data;//用于保存字符串
};
//普通构造函数
String::String(const char *str){
if (str==NULL){
m_data=new char[1]; //对空字符串自动申请存放结束标志'