C++工程师(后端)面试题(C++基础知识部分)

       本篇博客记录近段时间博主为准备秋招面试收集的C++基础知识面试题。问题后都附上了我自己总结的答案,这个答案可能是书上找的,可能是我自己理解的(所以有错误的地方希望大家能指出来),可能是看了好些博客总结的,可能有的是别人博客里的原话(侵删,请联系我)。
       本篇博客会持续更新,后面面试面得越多博客里收集的问题就越多啦。如果内容太多的话也许后面会给博客搞个目录什么的。


1、C++为什么要提出引用?(引用和指针的区别?)

       引用变量在功能上等于一个常量指针(到底是常量指针还是指针常量?有的书上写的常量指针,有的博客里写的指针常量,那就不纠结这个问题吧。总之这里指的是顶层const),即一旦指向某一个单元就不能在指向别处。
(1)我们在用指针的使用经常犯的错误是什么?
       ①操作野指针;
       ②不知不觉改变了指针的值,而后还以为该指针正常。
       如果我们要正确的使用指针,我们不得不人为地保证这两个条件。而引用的提出就是解决这个问题。
       引用区别于指针的特性是:
       ① 必须初始化(保证不是野指针);
       ② 一个引用永远指向他初始化的那个对象(保证指针值不变)。
所以引用的提出就是:让人为地保证这两个条件变成让编译器保证。这样可以减少错误的产生。

2)程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。

3)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。


2、使用const而不是#define来定义常量?

  • ①const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而后者仅仅进行字符替换,没有类型安全检查,并且在字符替换过程中会产生一些意料不到的错误。
  • ②const方法可以很方便地用于复合类型。
    如const int a[3]={1,2,3};
  • ③const标识符遵循变量的作用域原则,可以创建作用域为全局、名称空间、数据块的常量。


3、C++左值和右值的区别?

  • 左值:lvalue(locator value)代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
  • 右值:rvalue通过排他性来定义,每个表达式不是lvalue就是rvalue。因此从上面的lvalue的定义,rvalue是在不在内存中占有确定位置的表达式。


4、全局变量?

特点:

  • ① 作用域:全局可见。
           全局变量(外部变量)是在函数外部定义的,它的作用域为从变量的定义处开始,到本程序文件的末尾。
           注:通常把超出一个函数的作用域称为全局作用域,其他几种(如块作用域)不超出一个函数的作用域称为局部作用域。
  • ② 存储空间:静态存储区
           系统会在编译时将全局变量分配在静态存储区,在程序执行期间,对应的存储空间不会释放,一直到程序结束才会释放。
           注:一个程序在内存中占用的存储空间可以分为3个部分:程序区(存放可执行程序的代码)、静态存储区(存放静态变量)、动态存储区(存放动态变量)。
  • ③ 优先度:全局变量优先度低于局部变量
    当全局变量和局部变量重名时,会屏蔽全局变量,局部优先。
           

优点:使用全局变量程序运行时速度会快一点,因为内存不需要再分配。
缺点:使用全局变量会占用更多的内存,因为其生命期长。

       
全局变量作用域的扩展和限制

  • ① 扩展:使用extern关键字可以对全局变量的作用域进行扩展。
            前面提到,全局变量的作用域为从变量的定义处开始,到本程序文件的末尾。若想在本文件全局变量定义之前引用该全局变量,可以在引用之前用extern关键字对该变量进行说明,有了此说明,就可以从说明之处起,合法地引用该变量。
            若想在一个文件(设为a.cpp)中引用另一个文件(设为b.cpp)中已定义的全局变量,可以在a.cpp中extern关键字对该全局变量进行说明,在编译和连接时,系统就会知道该全局变量已经在其他文件(b.cpp)中定义过了。
           注:在编译时遇到extern,系统会现在本文件中查找全局变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时在其他文件中查找全局变量的定义,如果找到,就将作用域扩展到本文件;如果还找不到,按出错处理。

  • ② 限制:使用static关键字可以限制全局变量的作用域。(又称之为隐藏)
           全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern对全局变量进行声明,就可以使用全局变量。
           如果希望全局变量仅限本文件引用,而不能被其他文件引用,可以在定义全局变量时在前面加一个static关键字。
           

全局变量和全局静态变量的区别:

       首先,并不是说全局变量在定义时加了static关键字才是静态存储,不加static就是动态存储,不是的。不管加不加static,全局变量都是存储在静态存储区的,都是在编译时分配存储空间的,两者只是作用域不同,全局变量默认具有外部链接性,作用域是整个工程,全局静态变量的作用域仅限本文件,不能在其他文件中引用。

       

5、static关键字?

特点:用来控制存储方式和可见性

  • ① 存储空间:静态存储区(控制变量的存储方式)
           静态变量存储在静态存储区(存储在静态存储区的变量,如果不显式地对其进行初始化,系统会将其初始化为0),在程序执行期间,对应的存储空间不会释放,一直到程序结束才会释放。
           static控制变量的存储方式体现在局部变量上。局部变量存储在动态存储区,在局部变量定义前加上static,该局部变量就变成了局部静态变量,局部静态变量存储在静态存储区,即使函数调用结束,它占用的存储空间也不会释放,但其他函数不能引用该局部静态变量。当下一次调用该函数时,不会再次对该局部静态变量进行初始化,它的值是上一次函数调用结束时的值。
           对全局变量而言,存储方式没有什么改变,因为全局变量和全局静态变量都存储在静态存储区。
  • ② 作用域:(控制变量、函数的可见性)
           static控制变量的可见性体现在全局静态变量和静态函数上。
           全局变量默认具有外部链接性,作用域是整个工程。使用static关键字可以限制全局变量的作用域,全局静态变量的作用域仅限本文件,它对在其他文件不可见,也就是说不能在其他文件中引用该全局静态变量,但其他文件中可以定义和它名字相同的变量,不会发生冲突。
           在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数的不同在于,它只能在声明它的文件当中可见,不能被其它文件使用,其它文件中可以定义相同名字的函数,不会发生冲突。
           局部静态变量的作用域与局部变量的作用域相同,其作用域都是从定义开始到函数或程序块结束为止。

       
类中的static关键字:

       在类中声明static变量或者函数时,初始化时使用作用域运算符(::)来标明它所属类,静态成员是类的成员(所有对象中共享的成员),而不是某一个对象的成员。

  • ① 静态数据成员
           在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。
           静态数据成员和普通数据成员一样遵从public,protected,private访问规则。
    对于非静态数据成员,每个对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只会被分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
           因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它。
           同全局变量相比,使用静态数据成员有两个优势:
           静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
           可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

  • ② 静态成员函数
           与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的,如函数fun()实际上是this->fun()。但是与普通函数相比,静态成员函数由于不与任何的对象相联系,因此它不具有this指针。
           非静态成员函数可以任意地访问静态成员函数和静态数据成员;
           静态成员函数不能访问非静态成员函数和非静态数据成员;静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
           

什么时候用static?

       需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

为什么要引入static?

       函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题:如果想将函数中此变量的值保存至下一次调用时,如何实现?最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

       

6、C++和C的区别?

设计思想上:
C++是面向对象的语言,而C是面向过程的结构化编程语言,C++在C的基础上增加了类。

语法上:
① C++具有封装、继承和多态三种特性。
② C++相比C,增加了许多类型安全的功能,比如强制类型转换。
③ C++支持范式编程,比如模板类、函数模板等。
④ 在C++中,引用是一个经常使用的概念。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
⑤ 在C++语言中,仍然支持malloc()和free()来分配和释放内存,同时增加了new和delete来管理内存。
⑥ C++支持函数重载,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。

       

7、智能指针(smart pointer)?

       智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
       C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr,其中后三个是c++11支持,并且第一个已经被11弃用。
       
为什么要使用智能指针?
       动态内存的使用中很容易出现问题。比如,申请的空间忘记释放,造成内存泄漏;或者在后面还会使用到该指针的情况下释放了内存,在这种情况下就引用了非法内存的指针。
       用智能指针可以很大程度上的避免这些问题,因为智能指针就是一个类(而且还是像vector这样的模板类,当我们创建一个智能指针时,还必须提供额外的信息——指针可以指向的类型),当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
       
shared_ptr:
       shared_ptr允许多个指针指向同一个对象。当指向某一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过析构函数来完成销毁工作的。
       
unique_ptr:
       unique_ptr“独占”所指对象。两个unique_ptr不能指向一个对象,即unique_ptr不共享它所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库(STL)算法。只能移动unique_ptr,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个unique_ptr,并且原始unique_ptr不再拥有此资源。
       
weak_ptr:
       weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator* 和 operator-> ,因此取名为 weak,表明其是功能较弱的智能指针。它的最大作用在于协助 shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。

摒弃auto_ptr的原因?
       一句话总结就是:避免因潜在的内存问题导致程序崩溃。unique_ptr比auto_ptr更加安全,因为auto_ptr有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr则禁止了拷贝语义,但提供了移动语义,即可以使用 std::move()进行控制权限的转移。

       

8、堆和栈?

数据结构中的堆和栈?
(1)栈就像装数据的桶或箱子
我们先从大家比较熟悉的栈说起吧,它是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。
(2)堆像一棵倒过来的树(好像大根堆小根堆就是这种)
而堆就不同了,堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
       

内存分配中的栈和堆?
       函数的调用过程由栈来实现,函数返回地址、实参和局部变量都采用栈的方式存放。堆区用于分配程序员申请的内存空间。
(1) 栈
       栈由操作系统自动分配释放,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
       其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到低(向上生长),所以后定义的变量地址低于先定义的变量。栈中存储的数据的生命周期随着函数的执行完成而结束。
(2) 堆
       堆由程序员分配释放,若程序员不释放,程序结束时由OS回收,分配方式倒是类似于链表。
       堆的内存地址生长方向与栈相反,由低到高(向下生长),但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。
       

堆(heap)和栈(stack)的区别?
(1) 申请方式和回收方式不同
栈是系统自动分配空间的,例如我们定义一个 char a;系统会自动在栈上为其开辟空间。而堆则是程序员根据需要自己申请的空间,例如malloc(10);开辟十个字节的空间。由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
(2) 申请后系统的响应
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:堆上申请空间的效率比栈要低得多。
首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

       

9、程序的内存模型分为那几个区域?

4个区域:静态存储区、堆区、栈区、程序区
(1)静态存储区:存放全局变量、静态变量、常量。
(2)堆区:由程序员根据需要自己申请空间,需要自己回收这些空间,如果没有回收,程序结束时操作系统会回收这些空间。也用来保存类和对象。
(3)栈区:存放函数的参数、返回值、局部变量等。
(4)代码区:存放代码。

也有人说C++五个区域:静态存储区、堆区、栈区、程序区、自由存储区。
其他四个区域功能依旧,C++里增加了new关键字,new申请的内存是在自由存储区上。我并没有搞懂自由存储区和堆区的区别。

       

9、malloc/free和new/delete的区别?

C++工程师(后端)面试题(C++基础知识部分)

       

10、那你知道如果用free去清理new出来的内存会产生什么问题吗?

       new实际过程中做了两步操作,第一步是分配内存空间,第二步是调用类的构造函数;delete也同样是两步,第一步是调用类的析构函数,第二步才是释放内存;而malloc()和free()仅仅是分配内存与释放内存操作。
       如果通过new分配的内存,再用free去释放,就会少一步调用析构函数的过程。同时,在构造函数里面申请的内存因为没有调用析构函数,所以该内存并没有释放,所以如果再输出该内存的值,那么应该还是原来设置的值。

       

11、vector实现?

       vector内部使用动态数组的方式实现的。
       初始化时,vector会申请比当前要容纳元素所需的内存更大一块的内存,预留出空闲空间,用于之后存放新添加的元素,这样就不需要每添加一个新元素就重新分配整个容器的内存空间。当不断向数组内添加元素时,如果数组的内存不够了,就要重新申请一块内存,一般是当前大小的两倍,然后把原数组的内容拷贝过去。
       vector和普通数组一样都支持随机访问,但当在数组中头部或者中间添加或删除元素时,都要移动数组元素,如果频繁地进行这样的操作,会导致vector性能降低。
       vector的析构函数会先销毁所有已存在的元素,然后释放所有内存。
       vector的一个特点:内存空间只会增长,不会减小。

       

12、std::sort原理?

对要排序的元素数目有一个阈值,如果大于该阈值则是用快速排序,如果小于阈值则用插入排序。

       

13、深拷贝和浅拷贝?

(1) 浅拷贝
       浅拷贝只是增加了一个指针指向已存在的内存地址,实际内存中并没有重新开一块内存复制原来地址内的内容。比如变量的引用就是浅拷贝,这个变量只是多了一个别名,内存中并没有额外用一块内存重新保存这个变量,我改变引用的值,原来变量的值也会跟着改变。
(2)深拷贝
       深拷贝是申请了一个新的内存保存复制的内容,而且增加了一个指针并且使这个指针指向这块新的内存,相当于有两块内存保存了一样的内容。我改变这块新内存中的内容,原来那块内存中的内容不会改变。

       

14、C++多态实现的机制?(重载和重写的区别?)

       多态就是多种形态,C++的多态分为静态多态与动态多态。
静态多态就是重载,编译器根据函数实参的类型判断出要调用哪个函数。比如函数重载和函数模板。
       动态多态依靠的是虚函数表和动态绑定机制,因为是在运行时根据对象的类型在虚函数表中寻找调用函数的地址来调用相应的函数,所以称为动态多态。
       
(1)什么是多态?
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的类型来调用相应的函数。如果对象的类型是派生类,就调用派生类的函数;如果对象是基类,就调用基类的函数。
       
(2) 虚函数表?
声明了虚函数的类会隐式创建一个虚指针指向虚函数表,而虚函数表保存了虚函数的地址(虚函数表中只有虚函数,非虚函数在另一个表中),当我们调用虚函数时,实际上是通过虚指针去查找虚函数表中对应虚函数的地址。

       

15、为什么list不能使标准库算法sort()?

       list的内存空间不是连续的,标准库算法中的操作需要随机访问迭代器。因此forward_list也不可以。

       

16、解决哈希冲突的方法有哪些?

(1)开放地址法
       当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
(2)再哈希法
       当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。
(3) 建立一个公共溢出区
除了原本的存储空间外,另外开辟一个存储空间用以存储发生冲突的记录。

       

17、容器适配器?

       C++提供了三种容器适配器(container adapter):栈stack、队列queue、优先队列priority_queue。stack和queue基于deque实现,priority_queue基于vector实现。
       假设我需要一个栈结构,可以用deque来模拟,只在一端进行元素插入和弹出,另一端不动,但我不能防止别人在deque的另一端进行操作,因此deque并不能严格地满足我的要求。我对它进行封装,作一些限制,使它留出的接口只能在一端进行插入和删除,这样就实现了stack。实际上stack也是使用的deque,只是对deque进行了封装改变了对外的接口而已。因此,stack、queue、priority_queue这样的类一般称为容器适配器,它们只是基本容器类型(vector,dequeue,list)的适配。