《C++面试宝典》V1.0 冲刺大厂~持续更新(2)

        分享面试总结,涉及C++、算法、数据结构、操作系统、计算机网络、Linux、数据库、设计模式 等,后面持续更新~
        内容多为收集、整理总结,视频、书籍学习所得,如有错误请指出,万分感谢!!!

C++篇 — √2

21. C语言的编译链接过程?
源代码-->预处理-->编译-->优化-->汇编-->链接–>可执行文件

  1. 预处理
    读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号等。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i预处理后的c文件,.ii预处理后的C++文件
  2. 编译阶段
    编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码.s文件
  3. 汇编过程
    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件
  4. 链接阶段
    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

扩展:静态链接和动态链接?请自查
静态链接:一个目标文件集合;
动态链接:函数代码放到被称作动态链接库或共享对象的某个目标文件中,可执行文件执行时,动态链接库的全部内容映射到运行时相应进程的虚地址空间,动态链接程序根据记录找到对应代码。

22. 左值右值?

  1. 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
  2. C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,PureRvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
  3. 左值引用就是对一个左值进行引用的类型,右值引用就是对一个右值进行引用的类型。事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
  4. 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

23. 移动构造函数?

  1. 我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
  2. 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
  3. 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

STL部分

建议看侯捷老师的STL源码剖析(大赞),理解更深刻。
24. vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素?

  1. vector数据结构
    vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。
  2. list数据结构
    list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。非连续存储结构:list是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向下一个元素的节点(next)。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。
    区别:
    vector的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易操作。list的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除操作等都比较方便,改变指针的指向即可。list是单向的,vector是双向的。vector中的迭代器在使用后就失效了,而list的迭代器在使用之后还可以继续使用。
  3. int mySize = vec.size();vec.at(mySize -2);
    list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历。

25. vector的实现,删除其中的元素,迭代器如何变化?为什么是两倍扩容?释放空间?
        size()函数返回的是已用空间大小,capacity()返回的是总空间大小,capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。
        由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。
         resize()成员函数只改变元素的数目,不改变vector的容量。

  1. 空的vector对象,size()和capacity()都为0;

  2. 当空间大小不足时,新分配的空间大小为原空间大小的2倍;

  3. 使用reserve()预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率;

  4. 当reserve()分配的空间比原空间小时,是不会引起重新分配的;

  5. resize()函数只改变容器的元素数目,未改变容器大小;

  6. 用reserve(size_type)只是扩大capacity值,这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象;

  7. 不同的编译器,vector有不同的扩容大小。在vs下是1.5倍,在GCC下是2倍;

  8. 空间和时间的权衡。简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多;

  9. 使用k=2增长因子的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对内存不友好。最好把增长因子设为(1,2)之间 ;

  10. 对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。

如何释放空间:
        由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。
        如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。
操作如下:
vector(Vec).swap(Vec);
将Vec的内存空洞清除;
vector().swap(Vec);
清空Vec的内存;

26. 容器内部删除一个元素?

  1. 顺序容器
    erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;
    It = c.erase(it);
  2. 关联容器
    erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;
    c.erase(it++);

27. STL迭代器如何实现?

  1. 迭代器是一种抽象的设计理念,通过迭代器可以在不了解容器内部原理的情况下遍历容器,除此之外,STL中迭代器一个最重要的作用就是作为容器与STL算法的粘合剂。
  2. 迭代器的作用就是提供一个遍历容器内部所有元素的接口,因此迭代器内部必须保存一个与容器相关联的指针,然后重载各种运算操作来遍历,其中最重要的是*运算符与->运算符,以及++、–等可能需要重载的运算符重载。这和C++中的智能指针很像,智能指针也是将一个指针封装,然后通过引用计数或是其他方法完成自动释放内存的功能。
  3. 最常用的迭代器的相应型别有五种:value type、difference type、pointer、reference、iterator catagoly。

28. set与hash_set的区别?
4. set底层是红黑树RB-Tree实现,hash_set底层是哈希表hash_table实现的;
5. RB-Tree有自动排序功能,而hash_table不具有自动排序功能;
6. set和hash_set元素的键值就是实值;
7. hash_table有一些无法处理的型别。
扩展:自查红黑树原理等资料

29. hashmap与map的区别?
区别主要看查找速度、数据量、内存使用等方面回答。

  1. 底层实现不同,Hashmap底层是hash函数,查找比map快,map底层是红黑树log(n);
  2. map具有自动排序的功能,hash_map不具有自动排序的功能;
  3. hashtable有一些无法处理的型别。

30.map、set是怎么实现的?

  1. 他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除;
  2. 在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是map,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value
  3. 因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

扩展:红黑树是怎么能够同时实现这两种容器? 为什么使用红黑树?

31. 如何在共享内存上使用stl标准库?

  1. 想像一下把STL容器,例如map, vector, list等等,放入共享内存中,IPC一旦有了这些强大的通用数据结构做辅助,无疑进程间通信的能力一下子强大了很多。我们没必要再为共享内存设计其他额外的数据结构,另外,STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装,默认情况下有它们自己的内存管理方案。当一个元素被插入到一个STL列表(list)中时,列表容器自动为其分配内存,保存数据。考虑到要将STL容器放到共享内存中,而容器却自己在堆上分配内存。一个最笨拙的办法是在堆上构造STL容器,然后把容器复制到共享内存,并且确保所有容器的内部分配的内存指向共享内存中的相应区域,这基本是个不可能完成的任务。

  2. 假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

32. map插入方式?

  1. 用insert函数插入pair数据,
    mapStudent.insert(pair<int, string>(1, “student_one”));
  2. 用insert函数插入value_type数据
    mapStudent.insert(map<int, string>::value_type (1, “student_one”));
  3. 在insert函数中使用make_pair()函数
    mapStudent.insert(make_pair(1, “student_one”));
  4. 用数组方式插入数据
    mapStudent[1] = “student_one”;

33. STL中unordered_map(hash_map)和map的区别,hash_map如何解决冲突以及扩容?

  1. unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序。
  2. 存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
  3. 所以使用时map的key需要定义operator<,而unordered_map需要定义hash_value函数并且重载operator==,但是很多系统内置的数据类型都自带这些。
  4. 那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。
  5. 如果需要内部元素自动排序,使用map,不需要排序使用unordered_map。
  6. unordered_map的底层实现是hash_table。
  7. hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决。
  8. 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。
  9. 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

34. vector越界访问下标,map越界访问下标?vector删除元素时会不会释放空间?

  1. 通过下标访问vector中的元素时不会做边界检查,即便下标越界。也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。
  2. map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,就将一个具有该key和value的某人值插入这个map。
  3. erase()函数,只能删除内容,不能改变容量大小; erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。

35. STL中list与queue之间的区别?

  1. list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;
  2. list插入操作和结合才做都不会造成原有的list迭代器失效;
  3. list不仅是一个双向链表,而且还是一个双向环状链表,所以它只需要一个指针
  4. list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插入操作之后都仍然有效;
  5. deque是一种双向开口的连续线性空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作;可以在头尾两端分别做元素的插入和删除操作;
  6. deque和vector最大的差异,一在于deque允许常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,deque没有所谓的空间保留功能。

36. STL中的allocator,deallocator?

  1. 第一级配置器直接使用malloc()、free()和relloc(),第二级配置器视情况采用不同的策略:当配置区块超过128bytes时,视之为足够大,便调用第一级配置器;当配置器区块小于128bytes时,为了降低额外负担,使用复杂的内存池整理方式,而不再用一级配置器;
  2. 第二级配置器主动将任何小额区块的内存需求量上调至8的倍数,并维护16个free-list,各自管理大小为8,16,24,~128bytes的小额区块;
  3. 空间配置函数allocate(),首先判断区块大小,大于128就直接调用第一级配置器,小于128时就检查对应的free-list。如果free-list之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小调整至8的倍数,然后调用refill(),为free-list重新分配空间;
  4. 空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。

37. STL中hash_map扩容发生什么?

  1. hash table表格内的元素称为桶(bucket),而由桶所链接的元素称为节点(node),其中存入桶元素的容器为stl本身很重要的一种序列式容器——vector容器。之所以选择vector为存放桶元素的基础容器,主要是因为vector容器本身具有动态扩容能力,无需人工干预。
  2. 向前操作:首先尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易完成前进操作,如果目前正巧是list的尾端,就跳至下一个bucket身上,那正是指向下一个list的头部节点。

38.STL容器介绍?
1.vector 底层数据结构为数组,支持快速随机访问。
2.list 底层数据结构为双向链表,支持快速增删。
3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析。支持首尾(中间不能)快速增删,也支持随机访问deque是一个双端队列(double-ended queue),也是在堆中保存内容。
4.stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因考虑到容量大小有限制,扩容耗时。
5.queue 底层一般用list或deque实现,封闭头部即可,不用vector的原因考虑到容量大小有限制,扩容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)。
6.priority_queue(优先队列) 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
7.set 底层数据结构为红黑树,有序,不重复。
8.multiset 底层数据结构为红黑树,有序,可重复。
9.map 底层数据结构为红黑树,有序,不重复。
10.multimap 底层数据结构为红黑树,有序,可重复。
11.hash_set 底层数据结构为hash表,无序,不重复。
12.hash_multiset 底层数据结构为hash表,无序,可重复。
13.hash_map 底层数据结构为hash表,无序,不重复。
14.hash_multimap 底层数据结构为hash表,无序,可重复。

39. vector的增加删除都是怎么实现的?为什么是1.5倍?

  1. 新增元素:vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
  2. 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了;
  3. 初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
  4. 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容
  5. 向量容器vector的成员函数pop_back()可以删除最后一个元素.
  6. 而函数erase()可以删除由一个iterator指出的元素,也可以删除一个指定范围的元素。
  7. 还可以采用通用算法remove()来删除vector容器中的元素.
  8. 不同的是:采用remove一般情况下不会改变容器的大小,而pop_back()与erase()等成员函数会改变容器的大小。

扩容倍数:
对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2二倍的方式扩容,或者以1.5倍的方式扩容。

40. c和c++的区别?

  1. 第一点就应该想到C是面向过程的语言,而C++是面向对象OOP的语言,一般简历上第一条都是熟悉C/C++基本语法,切勿写精通,了解C++面向对象思想,那么,请问什么是面向对象?C++三大特性(详细介绍)
  2. C和C++动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字;(关于malooc/free与new/delete的不同又可以说一大堆)
  3. 接下来就不得不谈到C中的struct和C++的类,C++的类是C所没有的,但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;
  4. C++支持函数重载,而C不支持函数重载,而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是_fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数;
  5. C++中有引用,而C没有;这样就不得不提一下引用和指针的区别;
  6. 当然还有C++全部变量的默认链接属性是外链接,而C是内连接
  7. C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&,解引用的操作的话,是存放在符号表的,不开辟内存);
  8. 当然还有局部变量的声明规则不同,C++特有输入输出流之类的,很多就不再一一列出来了。

未完待续~

猜你喜欢

转载自blog.csdn.net/qq_21197471/article/details/114479201
今日推荐