C++面试题&知识点整理

近期在找工作,面的基本上是C/C++相关岗位,整理了一些网上提到的面试题或者知识点,慢慢补充吧,有错误的地方欢迎指出。 
下面整理归纳了面试中常问到的题目,分为5大类: 
- C++知识点; 
- 操作系统; 
- 多线程编程; 
- 网络; 
- 算法; 
- 其他。


1. C++知识点

1.1 构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

class Line
{
   public:
      void setLength( double len );
      double getLength( void );
      Line();  // 这是构造函数

   private:
      double length;
};

// 成员函数定义,包括构造函数
Line::Line(void)
{
    cout << "Object is being created" << endl;
}

1.2 虚函数的定义

定义 
虚函数是一种在基类定义为virtual的函数,并在一个或多个派生类中再定义的函数。虚函数的特点是,只要定义一个基类的指针,就可以指向派生类的对象。 
虚函数是在基类中使用关键字 virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。 
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

注:无虚函数时,遵循以下规则:C++规定,定义为基类的指针,也能作指向派生类的指针使用,并可以用这个指向派生类对象的指针访问继承来的基类成员;但不能用它访问派生类的成员。
  • 使用虚函数实现运行时的多态性的关键在于:必须通过基类指针访问这些函数。
  • 一旦一个函数定义为虚函数,无论它传下去多少层,一直保持为虚函数。
  • 把虚函数的再定义称为过载(overriding)而不叫重载(overloading)。

纯虚函数 
定义在基类中的一种只给出函数原型,而没有任何与该基类有关的定义的函数。纯虚函数使得任何派生类都必须定义自己的函数版本。否则编译报错。 
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

纯虚函数定义的一般形式:
Virtual type func_name(参数列表)=0;

含有纯虚函数的基类称为抽象基类。抽象基类又一个重要特性:抽象类不能建立对象。但是抽象基类可以有指向自己的指针,以支持运行时的多态性。

1.3 虚函数表

虚函数表的创建和继承 
a. 基类的虚函数表的创建: 
首先在基类声明中找到所有的虚函数,按照其声明顺序,编码0,1,2,3,4……; 
然后按照此声明顺序为基类创建一个虚函数表,其内容就是指向这些虚函数的函数指针,按照虚函数声明的顺序将这些虚函数的地址填入虚函数表中。例如若show放在虚函数声明的第二位,则在虚函数表中也放在第二位。

b. 对于子类的虚函数表: 
首先将基类的虚函数表复制到该子类的虚函数表中。 
若子类重写了基类的虚函数show,则将子类的虚函数表中存放show的函数地址(未重写前存放的是子类的show虚函数的函数地址)更新为重写后函数的函数指针。 
若子类增加了一些虚函数的声明,则将这些虚函数的地址加到该类虚函数表的后面。

通过虚函数表访问对象的方法 
当执行Base->show()时,要观察show在Base基类中声明的是虚函数还是非虚函数。若为虚函数将使用动态联编(使用虚函数表决定如何调用函数),若为非虚函数则使用静态联编(根据调用指针Base的类型来确定调用哪个类的成员函数)。此处假设show为虚函数,首先:由于检查到Base指针类型所指的类Base中show定义为虚函数,因此找到Base所指的对象,访问对象得到该对象所属类的虚函数表地址。其次:查找show在Base类中声明的位置在Base类中所有虚函数声明中的位序。然后到Base所指对象的所属类的虚函数表中访问该位序的函数指针,从而得到要执行的函数。

1.4 为什么要把析构函数定义为虚函数?

new出来的是子类son的对象,采用一个父类father的指针来接收,故在析构的时候,编译器因为只知道这个指针是父类的,所以只将父类部分的内存析构了,而不会去析构子类的内存,就造成了内存泄露。基类析构函数定义为虚拟函数的时候,在子类的对象的首地址开始会有一块基类的虚函数表拷贝,在析构子类对象的时候会删除此虚函数表,此时会调用基类的析构函数,所以此时内存是安全的。

1.5 为什么虚函数比普通函数慢?

因为虚函数要通过查找虚函数表的方法访问。

1.6 为什么构造函数不能是虚函数?

构造函数不可以是虚函数的,这个很显然,毕竟虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果构造函数是虚的,就需要一个虚函数表来调用,但是类还没实例化没有内存空间就没有虚函数表,这根本就是个死循环。

1.7 内联函数、构造函数和静态成员函数可以定义为虚函数么?为什么?

内联函数是编译时展开函数体,所以在此时就需要有实体,而虚函数是运行时才有实体,所以内联函数不可以为虚函数。
静态成员函数是属于类的,不属于任何一个类的对象,可以通过作用域以及类的对象访问,本身就是一个实体,所以不能定义为虚函数。 
如果构造函数定义为虚函数,则需要通过查找虚函数表来进行调用。但是构造函数是虚函数的情况下是找不到的,因为构造函数自己本身也不存在,创建不了实例,没有实例化对象,则类的成员不能被访问。

1.8 基类指针指向派生类时如何知道指向的是哪一个派生类?

派生类对象的内存范围大于基类对象的内存范围。指向派生类的指针如果指向基类,则可能访问不可预知的内存空间,也就是派生类增加的特殊属性或方法地址入口。指向基类的指针如果指向派生类,其访问空间总是在派生类的内存空间的内部,不会越界。

1.9 正确区分重载、重写和隐藏

注意三个概念的适用范围:处在同一个类中的函数才会出现重载。处在父类和子类中的函数才会出现重写和隐藏。 
重载:同一类中,函数名相同,但参数列表不同。 
重写:父子类中,函数名相同,参数列表相同,且有virtual修饰。 
隐藏:父子类中,函数名相同,参数列表相同,但没有virtual修饰;函数名相同,参数列表不同,无论有无virtual修饰都是隐藏。

基类中:  
      (1) virtual void show(); //是虚函数  
      (2) void show(int);     //不是虚函数   
子类中:  
      (3) void show();        //是虚函数  
      (4) void show(int);     //不是虚函数

1,2构成重载,3,4构成重载,1,3构成重写,2,4构成隐藏。另外2,3也会构成隐藏,子类对象无法访问基类的void show(int)成员方法,但是由于子类中4的存在导致了子类对象也可以直接调用void show(int)函数,不过此时调用的函数不在是基类中定义的void show(int)函数2,而是子类中的与3重载的4号函数。

1.10 如何定义一个只能在栈上或者堆上生成对象的类?

详细的解答: 
链接:https://www.nowcoder.com/questionTerminal/0a584aa13f804f3ea72b442a065a7618 
1. 只能在堆上生成对象:将析构函数设置为私有。(最好的方式设置为protect 然后自己定义create和destroy方法 https://www.cnblogs.com/vincently/p/4838283.html) 
原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

2.只能在栈上生成对象:将new 和 delete 重载为私有。 
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。 
将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

1.11 基类的析构函数不是虚函数,会带来什么问题?

派生类的析构函数用不上,会造成资源的泄漏。

1.12 派生类中构造函数与析构函数,调用顺序

构造函数的调用顺序总是如下: 

  1. 基类构造函数: 如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。 
  2. 成员类对象构造函数: 如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。如果有的成员不是类对象,而是基本类型,则初始化顺序按照声明的顺序来确定,而不是在初始化列表中的顺序。 
  3. 派生类构造函数: 析构函数正好和构造函数相反.

1.13 析构函数中抛出异常时概括性总结

  1. C++中析构函数的执行不应该抛出异常;
  2. 假如析构函数中抛出了异常,那么系统将变得非常危险,也许很长时间什么错误也不会发生;但也许系统有时就会莫名奇妙地崩溃而退出了,而且什么迹象也没有;
  3. 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外,即在析构函数内部写出完整的throw…catch()块。

1.14 子类析构时要调用父类的析构函数吗?

析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。

1.15 面向对象有哪些特点?如何体现?

  1. 继承:就是保留父类的属性,开扩新的东西。通过子类可以实现继承,子类继承父类的所有状态和行为,同时添加自身的状态和行为。
  2. 封装:就是类的私有化。将代码及处理数据绑定在一起的一种编程机制,该机制保证程序和数据不受外部干扰。
  3. 多态:是允许将父对象设置成为和一个和多个它的子对象相等的技术。包括重载和重写。重载为编译时多态,重写是运行时多态。 

重载与覆盖: 
Overloading:重载(两个或者多个函数在同一类中,名一样,参数列表不一样)。 
或者说:方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载. 
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现. 
Overriding(重写)=覆盖:在父类有个函数,在子类也又有一个同样名字的函数,而且在子类内中把这个功能做的跟具体化。

1.16 多态

定义:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;
多态实现的两种方式:父类指针指向子类对象 或 将一个基类的引用类型赋值为它的派生类实例。(重要:虚函数 + 指针或用)
构造函数、复制构造函数、析构函数、赋值运算符不能被继承。
在程序编译时多态性体现在函数和运算符的重载上;

1.17 “引用”与多态的关系?

引用是除指针外另一个实现多态的方式。这意味着,一个基类的引用可以指向它的派生类实例。例:

Class A; Class B : Class A{…};
B b; A& ref = b;

1.18 在什么时候需要使用“常引用”? 

如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;

1.19 将“引用”作为函数参数有哪些特点?

  • 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
  • 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
  • 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

1.20 指针和引用的区别?

相同点: 
1. 都是地址的概念; 
2. 指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。

区别: 

  1. 指针是一个实体,而引用仅是个别名; 
  2. 引用使用时无需解引用(*),指针需要解引用; 
  3. 引用只能在定义时被初始化一次,之后不可变;指针可变; 
  4. 引用没有 const,指针有 const; 
  5. 引用不能为空,指针可以为空; 
  6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小; 
  7. 指针和引用的自增(++)运算意义不一样; 
  8. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

参考: http://blog.csdn.net/lyd_253261362/archive/2009/07/06/4323691.aspx

1.21 sizeof(类),如何计算类的大小?

类的大小 
类的sizeof()大小一般是类中的所有成员的sizeof()大小之和,这个就不用多说。确切的说,用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。注意:类只是一个类型定义,它本身是没有大小可言的。 
对象大小= vptr(可能不止一个,这个很难确定,类中定义了一个virtual函数,仍然为占用4个字节) + 所有非静态数据成员大小 + Aligin字节大小(依赖于不同的编译器)。

使用sizeof()计算类大小的一些基本原则: 
(1)类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑; 
(2)类的总大小也遵守类似class字节对齐的,调整规则;(参考5分钟搞定内存字节对齐) 
(3)成员函数都是不会被计算的; 
(4)如果是子类,那么父类中的成员也会被计算; 
(5)虚函数由于要维护虚函数表,所以要占据一个指针大小,也就是4字节。 
总结:一个类中,虚函数、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间。

空类的大小 
《剑指offer》里的分析:空类型的实例中不包含任何信息,本来求sizeof的结果应该是0,但是当我们声明该类型的实例时,必须在内存中占有一定得空间,否则无法使用这些实例。至于占多少内存,由编译器决定。在Visual Studio中,每个空类型的实例占用1字节的空间。 
因为一个空类也要实例化,所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof()为1。

参考:https://blog.csdn.net/jiejinquanil/article/details/51445512

1.22 sizeof(结构体),如何计算结构体的大小?

sizeof,是个运算符不是一个函数,字节数的计算在程序编译时进行,而不是在程序执行的过程中才计算出来。 
注意对齐问题等。c只不过空struct的size为0.

1.23 C对象和C++对象的区别(struct和class)

C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!

a. 默认的继承访问权限。struct是public的,class是private的。  
b. struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。

1.24 结构体和类有什么区别?

C的结构体和C++结构体的区别 
1.1 C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。所以C的结构体是没有构造函数、析构函数、和this指针的。 
1.2 C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。 
1.3 C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。 
以上都是表面的区别,实际区别就是面向过程和面向对象编程思路的区别: 
C的结构体只是把数据变量给包裹起来了,并不涉及算法。 
而C++是把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。 
C语言中是没有类的概念的,但是C语言可以通过结构体内创建函数指针实现面向对象思想。

C++的结构体和C++类的区别 
2.1 C++结构体内部成员变量及成员函数默认的访问级别是public,而c++类的内部成员变量及成员函数的默认访问级别是private。 
2.2 C++结构体的继承默认是public,而c++类的继承默认是private。

1.25 extern“C”有什么作用?原理是什么?

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

c++作为c的超集,支持了函数重载,重载的函数可以通过形参的类型进行区别,调用的时候会根据实参的类型自动匹配调用,其内部实现的原理大概是这样的。

比如有两个函数int add(int a, int b)和double add(double a, double b),它们是重载函数,在编译的时候c++编译器就会生成不同的函数名,对应它们的分别为

_add_int_int(int a, int b) 和 _add_double_double(double a, double b),所以重载后的函数还是可以区分的,而c语言是不支持重载的,所以在c中 int add(int a, int b)生成的函数名还是_add,如果一个模块是用c++语言写的,另一个模块是c语言写的,在函数调用的时候,由于在.obj文件中生成的函数名机制不一样,就会出现调用出错的情况。extern “C”就是为了解决这个问题而设计的。

注意这个的extern “C”关键字是加在声明的变量前,而c调用c++模块时,extern “C”是加在c++模块的函数定义前,这样保证生成的cpp模块中不包含函数重载的类型信息。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。

参考:https://blog.csdn.net/mafia1986/article/details/789261

1.26 const修饰的变量和#define有什么区别?

  • #define在预处理阶段进行简单的替换,const在编译阶段使用 #define不做类型检查,仅仅展开替换,const有数据类型,会执行类型检查
  • #define不分配内存,仅仅展开替换,const会分配内存
  • #define不能调试,const可以调试 #define定义的常量在替换后运行过程中会不断地占用内存,而const定义的常量存储在数据段,只有一份copy,效率更高
  • #definde可以定义一些简单的函数,const不可以

1.27 static有什么作用?如何改变变量的生命周期和作用域?

在C语言中,关键字static有三个明显的作用:

  1. 在函数体内,一个被声明为静态的变量在这一函数被调用过程中维持上一次的值不变,即只初始化一次(该变量存放在静态变量区,而不是栈区)。
  2. 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外访问。(注:模块可以理解为文件)
  3. 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
补充:《C和指针》中说static有两层含义:指明存储属性;改变链接属性。
具体解释:(1)全局变量(包括函数)加上static关键字后,链接属性变为internal,也就是将他们限定在了本作用域内;(2)局部变量加上static关键字后,存储属性变为静态存储,不存储在栈区,下一次将保持上一次的尾值。

除此之外,C++中还有新用法:

    4. 在类中的static成员变量意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;
    5. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量(当然,可以通过传递一个对象来访问其成员)。

1.28 new 和malloc有什么不一样?

1.new 返回指定类型的指针,并且可以自动计算所需要大小; 而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针。    
2.malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。
3.有了malloc/free为什么还要new/delete? 
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。 
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。 
我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
4.既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢? 
这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。 
如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

1.29 delete与 delete []区别

delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”delete与new配套,delete []与new []配套。

MemTest *mTest1=new MemTest[10];
MemTest *mTest2=new MemTest;
Int *pInt1=new int [10];
Int *pInt2=new int;
delete[]pInt1; //-1-
delete[]pInt2; //-2-
delete[]mTest1;//-3-
delete[]mTest2;//-4-

在-4-处报错。

这就说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,delete和delete[]不能互用。delete[]删除一个数组,delete删除一个指针。简单来说,用new分配的内存用delete删除;用new[]分配的内存用delete[]删除。delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。

1.30 STL容器有哪些?

  1. 顺序容器: 是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。顺序容器包括:vector(向量)、list(列表)、deque(队列)。
  2. 关联容器:关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。元素是有序的集合,默认在插入的时候按升序排列。关联容器包括:map(集合)、set(映射)、multimap(多重集合)、multiset(多重映射)。
  3. 容器适配器:本质上,适配器是使一种不同的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用另一种顺序容器去实现,即可以把适配器看作“它保存一个容器,这个容器再保存所有元素”。STL 中包含三种适配器:栈stack 、队列queue 和优先级队列priority_queue

C++面试题&知识点整理_第1张图片

1.31 vector内部数据结构是什?List/Map/Queue

  1. vector(向量):相当于数组,但其大小可以不预先指定,并且自动扩展。它可以像数组一样被操作,由于它的特性我们完全可以将vector 看作动态数组。在创建一个vector 后,它会自动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小可以预先指定也可以由vector 默认指定,这个大小即capacity ()函数的返回值。当存储的数据超过分配的空间时vector 会重新分配一块内存块,但这样的分配是很耗时的,效率非常低。
  2. deque(队列):它不像vector 把所有的对象保存在一块连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端添加或删除元素的开销很小,它不需要重新分配空间。
  3. list(列表):是一个线性链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且可以任意伸缩,这是因为它存储在非连续的内存空间中,并且由指针将有序的元素链接起来。
  4. set, multiset, map, multimap 是一种非线性的树结构,具体的说采用的是一种比较高效的特殊的平衡检索二叉树—— 红黑树结构。

1.32 switch和if分支有什么区别?

  1. 跳转方式不同,分支执行和跳转表或树型结构
  2. 后面跟的值不同,if-else本质是bool值,不管是表达式还是变量还是常量,switch是一个数。
  3. 效率上
  4. switch可读性更好

1.33 堆和栈

从内存角度来说:栈区(stack)由编译器自动分配释放,存放函数的参数值,局部变变量的值等,其操作方式类似于数据结构中的栈,可静态亦可动态分配。 
堆区(heap)一般由程序员分配释放,若程序员不释放,可能造成内存泄漏,程序结束时可能由OS回收。只可动态分配,分配方式类似于链表。 
 从数据结构角度来说:堆可以被看成是一棵树,如:堆排序。 
而栈是一种先进后出的数据结构。

1.34 volitale什么作用?

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样一来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。

1.35 main 函数执行以前,还会执行什么代码?

全局对象的构造函数会在main 函数之前执行。

1.36 描述内存分配方式以及它们的区别?

  1. 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
  2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
  3. 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

1.37 int (*s[10])(int) 表示的是什么?

int (*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。

1.38 将程序跳转到指定内存地址

要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?

*((void (*)( ))0x100000 ) ( );
  首先要将0x100000强制转换成函数指针,即:
  (void (*)())0x100000
  然后再调用它:
  *((void (*)())0x100000)();
  用typedef可以看得更直观些:
  typedef void(*)() voidFuncPtr;
  *((voidFuncPtr)0x100000)();

1.39 C++是不是类型安全的?

不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。 

1.40 C++为什么用模板类,为什么用泛型

通过泛型可以定义类型安全的数据结构(类型安全),而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到更高质量的代码(高性能),因为您可以重用数据处理算法,而无须复制类型特定的代码(可重用)。  

1.41 智能指针

c++ 98的auto_ptr(所属权转移时,可能导致悬挂指针,导致访问NULL指针) 
c++ 11的unique_ptr(所属权,并且有编译器保证正确) 与 shared_ptr(引用计数,销毁时计数为1) 
详细请看博文: C++智能指针简单剖析 http://www.cnblogs.com/lanxuezaipiao/p/4132096.html

1.42 C++ 11的新特性

参考博文:C++11常用新特性快速一览 https://blog.csdn.net/jiange_zh/article/details/79356417 
1. nullptr 
nullptr 出现的目的是为了替代 NULL。 
主要避免重载问题。NULL为0,那么foo(char *) 和 foo(int)函数,当调用foo(NULL)就不知道调用哪个函数了。所以定义了nullptr来区分0和空指针。

     类型推导 
     C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

     区间迭代 
    基于范围的 for 循环 
     C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。 
     最常用的 std::vector 遍历将从原来的样子:

std::vector arr(5, 100);
for(std::vector::iterator i = arr.begin(); i != arr.end(); ++i) {
    std::cout << *i << std::endl;
}

变得非常的简单:

// & 启用了引用
for(auto &i : arr) {    
    std::cout << i << std::endl;
}
  1. 初始化列表
  2. 模板增强
  3. 构造函数
  4. Lambda 表达式
  5. 新增容器
  6. 正则表达式
  7. 语言级线程支持
  8. 右值引用和move语义

2. 操作系统

2.1 计算机加载程序包括哪几个区?

一个由C/C++编译的程序占用的内存分为以下几个部分: 
1. 栈区(stack):—由编译器自动分配释放,存放函数的参数值,局部变量的值等。可静态也可动态分配。其操作方式类似于数据结构中的栈。 
2. 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。动态分配。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 
3. 全局区(静态区):—程序结束后由系统释放,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域;未初始化的全局变量和静态变量在相邻的另一块区域(BSS,Block Started by Symbol),在程序执行之前BSS段会自动清0。 
4. 文字常量区:—程序结束后由系统释放,常量字符串就是放在这里的。 
5. 程序代码区:—存放函数体的二进制代码

2.2 操作系统分页、分段

分页 
用户程序的地址空间被划分为若干固定大小的区域,称为“页”。相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配,由一个页表来维护它们之间的映射关系。

分段 
见 2.1 计算机加载程序包括哪几个区?


3. 多线程编程

3.1. posix thread 互斥锁

创建:动态/静态 
属性: 普通锁、嵌套锁、检错锁、适应锁 
锁操作:pthread_mutex_lock、pthread_mutex_unlock、pthread_mutex_trylock

2.2. linux 线程基本概念和操作

创建

pthread_t id;
pthread_create(&id,NULL,(void *) thread,NULL);

参数:线程id、线程属性、线程运行函数地址、函数参数,返回是否成功

线程等待

int pthread_join __P ((pthread_t __th, void **__thread_return))

参数:第一个参数线程id,第二个参数线程返回值

线程终止

线程有2种方式结束,一种是正常执行到线程函数的结束,正常返回。 
第二种是调用pthread_exit.

void pthread_exit ((void *__retval)) __attribute__ ((__noreturn__))

2.3 死锁及预防与处理

死锁的规范定义如下:如果一个进程在等待只能由该进程停止才能引发的事件,那么该进程就是死锁的。

产生死锁的原因 
- 因为系统资源不足。 
- 进程运行推进的顺序不合适。 
- 资源分配不当等。

产生死锁的四个必要条件 
- 互斥条件:每个资源要么已经分配给了一个进程,要么就是可用的。 
- 占有和等待条件:已经得到了某个资源的进程可以再请求新的资源。 
- 不可抢占条件:已经分配给一个进程的资源不能强制性地被抢占,只能被占有它的进程显式地释放; 
- 环路等待条件:死锁发生时,系统中一定有两个或者两个以上的进程组成的一条环路,该环路中的每个进程都在等待着下一个进程所占有的资源。 
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

处理死锁的四种策略 
- 鸵鸟策略(忽略死锁); 
- 检测死锁并恢复; 
- 仔细对资源进行分配,动态地避免死锁; 
- 通过破坏引起死锁的四个必要条件之一,防止死锁的产生。

死锁避免 
死锁避免的主要算法是基于一个安全状态 的概念。在任何时刻,如果没有死锁发生,并且即使所有进程忽然请求对资源的最大请求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。从安全状态出发,系统能够保证所有进程都能完成,而从不安全状态出发,就没有这样的保证。

银行家算法 :判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求,如果满足请求后系统仍然是安全的,就予以分配。不安全状态不一定引起死锁,因为客户不一定需要其最大贷款额度。

2.4. 高并发

2.5 linux进程通信的方式

https://blog.csdn.net/gatieme/article/details/50908749 
管道(pipe),流管道(s_pipe)和有名管道(FIFO)

信号(signal)

消息队列

共享内存

信号量

套接字(socket)

2.6 线程池

一种线程池的实现方法: https://www.cnblogs.com/yangang92/p/5485868.html

2.7 线程和进程的概念和区别,在Windows和linux上的区别?

概念上: 
(进程)具有一定独立功能的程序关于某个数据集合上的一次运行活动,是应用程序的一个实例,进程是系统进行资源分配和调度的一个独立单位。进程之间无法进行资源共享。 
(线程)是进程的一个实体,是CPU调度和分派的基本单位。基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(程序计数器和虚拟机栈),但是它与同属一个进程的其他的线程共享进程所拥有的全部资源。线程是一个更接近执行体的概念。

两者的区别: 
操作系统资源管理方式:进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响。而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。多进程的程序比多线程的程序健壮,但在进程切换时,耗费资源较大。 
一个程序至少有一个进程,一个进程至少有一个线程。线程的划分都小于进程,使得多线程程序的并发性高。 
线程在执行过程中与进程还是有区别的,每个独立的线程有一个程序运行的入口,顺序执行序列和程序出口。但是线程不能够独立执行,必须依存应用程序中,由应用程序提供多个线程执行控制。 
逻辑角度上看,多线程的意义在于在一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程做多个独立的应用,来实现进程的调度和管理以及资源分配,这就是进程和线程的主要区别。

优缺点: 
线程执行开销小,但不利于资源的管理和保护,而进程相反。进程可以进行跨机器迁移。

windows和linux上的区别: 
Linux只有进程的说法,没有线程的概念。windows的线程相当于linux的进程,windows里面同一个进程各个线程之间是共享数据的,而linux不是。传统从Unix也支持线程的概念,但是一个进程只有一个线程。


4. 网络

4.1 TCP和UDP

tcp是一种面向连接的、可靠的、基于字节流的传输层通信协议。 
udp(用户数据报协议)传输层协议,提供面向操作的简单不可靠的非连接传输层服务,面向报文。

区别: 
- a. tcp是基于连接的,可靠性高;udp是基于无连接的,可靠性较低; 
- b. 由于tcp是连接的通信,需要有三次握手、重新确认等连接过程,会有延时,实时性差;同时过程复杂,也使其易于被攻击;而udp无连接,无建立连接的过程,因而实时性较强,也稍安全; 
- c. 在传输相同大小的数据时,tcp首部开销20字节;udp首部开销只有8个字节,tcp报头比udp复杂,故实际包含的用户数据较少。tcp无丢包,而udp有丢包,故tcp开销大,udp开销较小; 
- d. 每条tcp连接只能是点到点的;udp支持一对一、一对多、多对一、多对多的交互通信。

TCP的三次握手 
- 第一次握手:客户端发送一个tcp的syn标志位置为1的包(连接请求),指明客户打算连接服务器的端口;SYN=1,seq=client_isn 
- 第二次握手:当服务器收到连接请求之后,返回确认包(ack)应答,即将syn和ack标志位同时致为1(授予连接),并为这次连接分配资源;SYN=1,ACK=1,seq = server_isn 
- 第三次握手:客户端收到服务器的授予连接请求之后,再次发送确认包(ack)(syn标志位为0,ack标志位为1),并分配资源,这样tcp就建立连接了SYN=0,ACK=1,seq=client_isn+1

TCP和UDP的数据结构 
a.TCP

struct TCP_HEADER 
{
 short m_sSourPort;              // 源端口号16bit
 short m_sDestPort;              // 目的端口号16bit
 unsigned int m_uiSequNum;         // 序列号32bit
 unsigned int m_uiAcknowledgeNum;  // 确认号32bit
 short m_sHeaderLenAndFlag;        // 前4位:TCP头长度;中6位:保留;后6位:标志位
 short m_sWindowSize;            // 窗口大小16bit
 short m_sCheckSum;              // 检验和16bit
 short m_surgentPointer;           // 紧急数据偏移量16bit
}

b.UDP

struct UDP_HEADER 
{
 short m_sSourPort;              // 源端口号16bit
 short m_sDestPort;              // 目的端口号16bit
 short m_size;                      //长度16bit
 short m_sCheckSum;              // 检验和16bit
}

四次握手:(四次握手是TCP断开连接的过程)客户机发起中断请求报文FIN,服务机收到请求后回复给客户端一个ACK,此时客户端进入等待状态,当服务机确认ACK数据发送完,则向客户端发送FIN报文,客户端收到FIN报文后,给服务端发送ACK,然后进入等待状态,如果一段时间(2MSL)后没有收到回复,则证明服务机已经关闭,所以此时客户机也正常关闭。

4.2 OSI七层是哪七层?IP和TCP分别在哪一层?

物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。 
TCP和UDP在传输层、IP协议在网络层。


5. 算法

5.1 链表-环检测

算法的思想是设定两个指针p, q,其中p每次向前移动一步,q每次向前移动两步。那么如果单链表存在环,则p和q相遇;否则q将首先遇到NULL。

5.2 排序算法-快排

最好 O(nlogn) 最差 O(n^2) 
以first位置元素为key, ,分别从右边找到第一个小于key的值,并更新last值,放到first位置,从左边找到第一个大于key的值,更新first值,放到last位置。 
直到左边都小于key 右边都大于key。再对左右两边进行递归。 
不稳定的排序算法。

void Qsort(int a[], int low, int high)
{
    if(low >= high)
    {
        return;
    }
    int first = low;
    int last = high;
    int key = a[first];/*用字表的第一个记录作为枢轴*/

    while(first < last)
    {
        while(first < last && a[last] >= key)
        {
            --last;
        }

        a[first] = a[last];/*将比第一个小的移到低端*/

        while(first < last && a[first] <= key)
        {
            ++first;
        }

        a[last] = a[first];    
/*将比第一个大的移到高端*/
    }
    a[first] = key;/*枢轴记录到位*/
    Qsort(a, low, first-1);
    Qsort(a, first+1, high);
}

5.3 堆排序

平均性能 O(N*logN) 
不稳定排序方法,辅助空间为O(1)

#include 

void swap(int *a, int *b);
void adjustHeap(int param1,int j, int inNums[]);
void  HeapSort(int nums, int inNums[]);
//大根堆进行调整
void adjustHeap(int param1, int j, int inNums[])
{
    int temp=inNums[param1];
    for (int k=param1*2+1;ktemp)
        {
            inNums[param1]=inNums[k];
            param1=k;
        }
        else
            break;
    }
        //put the value in the final position
    inNums[param1]=temp;
}
//堆排序主要算法
void HeapSort(int nums,int inNums[])
{
    //1.构建大顶堆
    for (int i=nums/2-1;i>=0;i--)
    {
                //put the value in the final position
        adjustHeap(i,nums,inNums);
    }
    //2.调整堆结构+交换堆顶元素与末尾元素
    for (int j=nums-1;j>0;j--)
    {
                //堆顶元素和末尾元素进行交换
        int temp=inNums[0];
        inNums[0]=inNums[j];
        inNums[j]=temp;

        adjustHeap(0,j,inNums);//重新对堆进行调整
    }
}
int main() {
    int data[] = {6,5,8,4,7,9,1,3,2};
    int len = sizeof(data) / sizeof(int);
    HeapSort(len,data);
    return 0;
}

5.4 树相关算法

前中后序遍历
dfs/bfs dfs的解递归
平衡二叉树/红黑树
5.5 AVL树、红黑树、B/B+树和Trie树的比较

https://www.cnblogs.com/lca1826/p/6484469.html

5.6 缓存淘汰算法

先进先出置换算法(FIFO) 
思想:总是淘汰最先进入内存的页面,即选择在内存中驻留时间最长的页面予以淘汰。 
优点:实现简单 
缺点:往往与进程实际运行的规律不相符。有些页面,如存放全局变量、常用函数的页面,在整个进程的运行过程中将会被频繁访问。如果频繁将其换进换出,则会产生“抖动”现象,因此,这种算法在实际中应用很少。 
实现: 利用一个双向链表保存数据,当来了新的数据之后便添加到链表末尾,如果Cache存满数据,则把链表头部数据删除,然后把新的数据添加到链表末尾。在访问数据的时候,如果在Cache中存在该数据的话,则返回对应的value值;否则返回-1。如果想提高访问效率,可以利用hashmap来保存每个key在链表中对应的位置。

最近最久未使用置换算法(LRU-least recently used) 
思想:赋予每个页面一个访问字段,用来记录相应页面自上次被访问以来所经历的时间t,当淘汰一个页面时,应选择所有页面中其t值最大的页面,即内存中最近一段时间内最长时间未被使用的页面予以淘汰。 
实现:那就是利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

LFU 
思想:east Frequently Used-最近最少使用算法。它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。 
实现:为了能够淘汰最少使用的数据,因此LFU算法最简单的一种设计思路就是 利用一个数组存储 数据项,用hashmap存储每个数据项在数组中对应的位置,然后为每个数据项设计一个访问频次,当数据项被命中时,访问频次自增,在淘汰的时候淘汰访问频次最少的数据。这样一来的话,在插入数据和访问数据的时候都能达到O(1)的时间复杂度,在淘汰数据的时候,通过选择算法得到应该淘汰的数据项在数组中的索引,并将该索引位置的内容替换为新来的数据内容即可,这样的话,淘汰数据的操作时间复杂度为O(n)。 
另外还有一种实现思路就是利用 小顶堆+hashmap,小顶堆插入、删除操作都能达到O(logn)时间复杂度,因此效率相比第一种实现方法更加高效。

5.7 哈希表和hash冲突

哈希表的特点:关键字在表中位置和它之间存在一种确定的关系。

哈希函数:一般情况下,需要在关键字与它在表中的存储位置之间建立一个函数关系,以f(key)作为关键字为key的记录在表中的位置,通常称这个函数f(key)为哈希函数。

hash:翻译为“散列”,就是把任意长度的输入,通过散列算法,变成固定长度的输出,该输出就是散列值。 
这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。 
简单的说就是一种将任意长度的消息压缩到莫伊固定长度的消息摘要的函数。

hash冲突:就是根据key即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有人先来了。就是说这个地方要挤一挤啦。这就是所谓的hash冲突啦

1.开放地址法

1.1线性探测法:ThreadLocalMap 
线性再散列法是形式最简单的处理冲突的方法。插入元素时,如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中(会导致相同hash值的元素挨在一起和其他hash值对应的槽被占用)。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续从该槽遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)

1.2线性补偿探测法 
线性补偿探测法的基本思想是:将线性探测的步长从 1 改为 Q ,即将上述算法中的hash = (hash + 1) % m 改为:hash = (hash + Q) % m = hash % m + Q % m,而且要求 Q 与 m是互质的,以便能探测到哈希表中的所有单元。

1.3伪随机探测

1.4平方探测法

2.链地址法 
所有哈希地址相同的记录都链接在同一链表中。

3.再散列(再哈希) 
产生中途是计算另一个哈希函数的地址,直到冲突不在发生为止。

4.建立一个公共溢出区 
把冲突的都放在另一个地方,不在表里面。 
假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。


6. 其他问题

6.1 如何检查内存泄漏?

6.2 从源代码到可执行二进制文件,要经过哪些过程?

6.3 C++中不能重载的运算符:“?:”、“.”、“::”、“sizeof”和”.*”

6.4 数组越界 / 死循环 / 栈溢出 / 内存泄露


参考资料

  1. 阿里、网易和腾讯面试题 C/C++:http://www.cnblogs.com/shinny/p/9228549.html
  2. 菜鸟教程 C++
  3. C++经典面试题(最全,面中率最高) https://www.cnblogs.com/yjd_hycf_space/p/7495640.html
  4. 那些不能遗忘的知识点回顾——C/C++系列(笔试面试高频题)https://www.cnblogs.com/webary/p/4754522.html
  5. 缓存算法(页面置换算法)-FIFO、LFU、LRU http://www.cnblogs.com/dolphin0520/p/3749259.html
  6. 页面置换算法 https://blog.csdn.net/wuxy720/article/details/78941721
  7. Hash冲突的四种解决办法 https://blog.csdn.net/PORSCHE_GT3RS/article/details/79445707
  8. 解决Hash冲突的几种方法:https://blog.csdn.net/u012104435/article/details/47951357

你可能感兴趣的:(C++整理)