阿龙的学习笔记---转载及精炼总结github仓库:cpp-backend-reference

—> 转载自https://github.com/chankeh/cpp-backend-reference

突击复习常见题,要提前批了,之前的忘了不少,知识广度先再拓展一下。文章中的一些详细讲解的链接可以深度学习。

后端开发面试知识点大纲:


语言类(C++):

关键字作用解释:

  • volatile作用

    • Volatile关键词的第一个特性:易变性。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取

    • Volatile关键词的第二个特性:“不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行

    • Volatile关键词的第三个特性:”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。 (C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的; C/C++ Volatile变量间的操作,是不会被编译器交换顺序的)

    • C/C++ Volatile关键词深度剖析

  • static

    • 控制变量的存储方式和可见性。

      (1)修饰局部变量

      一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。

      (2)修饰全局变量

      对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见

      (3)修饰函数

      用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域, 变为本源文件可见

      (4)C++中的static

      如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有它们在存储空间中都只存在一个副本。可以通过类和对象去调用。

  • const的含义及实现机制

    const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。

    (1) const修饰基本数据类型

    1. const修饰一般常量及数组

      基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。

    2. const修饰指针变量*及引用变量&

      如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;

      如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

    (2) const应用到函数中,

    1. 作为参数的const修饰符

      调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。
      [注意]:参数const通常用于参数为指针或引用的情况;

    2. 作为函数返回值的const修饰符

      声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。

    (3) const在类中的用法

    1. 不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化列表中进行
    2. 类中的成员函数:int A::fun4() const; 其意义上是不能修改所在类的的任何变量

    (4) const修饰类对象,定义常量对象

    1. 常量对象只能调用常量函数,别的成员函数都不能调用。

    http://www.cnblogs.com/wintergrass/archive/2011/04/15/2015020.html

  • extern

    • 在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

      • 注意: extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。
    • 在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

  • 宏定义和展开、内联函数区别,

    • 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全

    • 宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。

    • 宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token的替换和连接等操作,在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性。

    • 对于问题:有了函数要它们何用?答案是:一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回)

    • 内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样

    • 内联函数必须是和函数体申明在一起,才有效。

    • 宏定义和内联函数区别


库函数实现(原文中无,待补充):

  • malloc,
  • strcpy,
  • strcmp的实现,常用库函数实现,
  • 哪些库函数属于高危函数

STL原理及实现:

  • STL提供六大组件,彼此可以组合套用:

    1. 容器(Containers):各种数据结构,如:序列式容器vector、list、deque、关联式容器set、map、multiset、multimap。用来存放数据。从实现的角度来看,STL容器是一种class template。

    2. 算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last)

    3. 迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

    4. 仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。

    5. 配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。

    6. 配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

    • 这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数)

    • STL六大组件

  • 序列式容器及原理:

    • vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
    • list链表
    • deque-双端队列:分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址。真正的数据在数组中存放着。deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。当map也不够时,重新分配内存当作新的map,把原来map中的内容copy的新map中。所以使用deque的复杂度要大于vector,尽量使用vector。
    • stack-堆栈:基于deque。
    • queue-队列:基于deque。
    • heap - 完全二叉树:使用最大堆排序,以数组(vector)的形式存放。
    • priority_queue-优先队列:基于heap。
    • slist - 双向链表
  • 关联式容器

    • set, map, multiset, multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。
    • unordered_map是c++11引入的哈希表结构,底层使用hashtable+buket的实现原理,hashtable可以看作是一个数组 或者vector之类的连续内存存储结构(可以通过下标来快速定位时间复杂度为O(1))处理hash冲突的方法就是在相同hash值的元素位置下面挂buket(桶),当数据量在8以内使用链表来实现桶,当数据量大于8 则自动转换为红黑树结构 也就是有序map的实现结构。
  • list和vector有什么区别?

    • vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率差,使用vector。
    • list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除
    • 而不关心随即存取,则应使用list。

虚函数:

  • 虚函数的作用和实现原理,什么是虚函数,有什么作用?
    • C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。

    • 虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。

    • 结论:

      1. 每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl。 vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数。
      2. 同时该类的每个对象都会包含一个vptr去指向该vtbl。
      3. 如果子类覆盖了父类的虚函数,将被放到了子类的虚表中原来父类虚函数的位置。
      4. (存疑) 在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中。

  • 衍生问题:为什么 C++里访问虚函数比访问普通函数慢?
    • 结论:单继承时性能差不多,多继承的时候会慢

    • 调用性能方面

      从前面虚函数的调用过程可知。当调用虚函数时过程如下(引自More Effective C++):

      通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。
      找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。
      调用第二步找到的的指针所指向的函数。
      在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承的情况下,由于会根据多个父类生成多个vptr,在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈。 虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。

    • 占用空间方面

      • 在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表vtbl的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的vtbl,如果子类只覆盖父类的一两个虚函数接口,子类vtbl的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些GUI库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致UI库的占用内存明显变大。 由于虚函数指针vptr的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。
    • C++虚函数浅析


  • 纯虚函数,为什么需要纯虚函数?
    • 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

      virtual void funtion1()=0
      
    • 原因:

      1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
      2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
    • 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

    • 虚函数和纯虚函数的区别


  • 为什么需要虚析构函数,什么时候不需要?父类的析构函数为什么要定义为虚函数
    • 一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
    • 当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

  • 内联函数、构造函数、静态成员函数可以是虚函数吗?
    • inline, static, constructor三种函数都不能带有virtual关键字。
      • inline是编译时展开,必须有实体;因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。
      • static属于class自己的,也必须有实体;静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制。
      • 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。(构造函数中可以调用虚函数吗?)

  • 总结一下关于虚函数
    1. 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。
    2. 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
    3. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
    4. 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。
    5. 纯虚函数通常没有定义体,但也完全可以拥有。
    6. 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
    7. 非纯的虚函数必须有定义体,不然是一个错误。
    8. 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。

  • override和overload的区别,
    • override(重写/覆盖)
    1. 方法名、参数、返回值相同。
    2. 子类方法不能缩小父类方法的访问权限。
    3. 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
    4. 存在于父类和子类之间。
    5. 方法被定义为final不能被重写。
    • overload(重载)
    1. 参数类型、个数、顺序至少有一个不相同。
    2. 不能重载只有返回值不同的方法名。
    3. 存在于父类和子类、同类中。
    • 注意:子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常。
  • 资料: 虚析构函数(√)、纯虚析构函数(√)、虚构造函数(X)


  • 为什么需要虚继承?虚继承实现原理解析,
    • 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
    • 如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。
    • 虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
    • C++虚拟继承

设计模式:

  • C++单例模式写法:
    • 静态化并不是单例 (Singleton) 模式:

      • 第一, 静态成员变量初始化顺序不依赖构造函数, 得看编译器心情的, 没法保证初始化顺序 (极端情况: 有 a b 两个成员对象, b 需要把 a 作为初始化参数传入, 你的类就 必须 得要有构造函数, 并确保初始化顺序).
      • 第二, 最严重的问题, 失去了面对对象的重要特性 – “多态”, 静态成员方法不可能是 virtual 的. Log 类的子类没法享受 “多态” 带来的便利.
    • 饿汉模式: 是指单例实例在程序运行时被立即执行初始化:

      class Log {
      public:
        static Log* Instance() {
          return &m_pInstance;
        }
      
        virtual void Write(char const *logline);
        virtual bool SaveTo(char const *filename);
      
      private:
        Log();              // ctor is hidden
        Log(Log const&);    // copy ctor is hidden
      
        static Log m_pInstance;
        static std::list m_data;
      };
      
      // in log.cpp we have to add
      Log Log::m_pInstance;
      
      • 这种模式的问题也很明显, 类现在是多态的, 但静态成员变量初始化顺序还是没保证.
    • 懒汉模式 (堆栈-粗糙版)

      • 单例实例只在第一次被使用时进行初始化:
      class Log {
      
      public:
        static Log* Instance() {
          if (!m_pInstance)
            m_pInstance = new Log;
          return m_pInstance;
        }
      
        virtual void Write(char const *logline);
        virtual bool SaveTo(char const *filename);
      
      private:
        Log();        // ctor is hidden
        Log(Log const&);    // copy ctor is hidden
      
        static Log* m_pInstance;
        static std::list m_data;
      };
      
      // in log.cpp we have to add
      Log* Log::m_pInstance = NULL;
      
      • Instance() 只在第一次被调用时为 m_pInstance 分配内存并初始化. 嗯, 看上去所有的问题都解决了, 初始化顺序有保证, 多态也没问题. 程序退出时, 析构函数没被执行. 这在某些设计不可靠的系统上会导致资源泄漏, 比如文件句柄, socket 连接, 内存等等.对于这个问题, 比较土的解决方法是, 给每个 Singleton 类添加一个 destructor() 方法:
    • 懒汉模式 (局部静态变量-最佳版):

      • 它也被称为 Meyers Singleton [Meyers]:

        class Log {
        public:
          static Log& Instance() {
            static Log theLog;
            return theLog;
          }
        
          virtual void Write(char const *logline);
          virtual bool SaveTo(char const *filename);
        
        private:
          Log();          // ctor is hidden
          Log(Log const&);      // copy ctor is hidden
          Log& operator=(Log const&);  // assign op is hidden
        
          static std::list m_data;
        };
        
      • 在 Instance() 函数内定义局部静态变量的好处是, theLog 的构造函数只会在第一次调用Instance() 时被初始化, 达到了和 “堆栈版” 相同的动态初始化效果, 保证了成员变量和 Singleton 本身的初始化顺序.

      • 它还有一个潜在的安全措施, Instance() 返回的是对局部静态变量的引用, 如果返回的是指针, Instance() 的调用者很可能会误认为他要检查指针的有效性, 并负责销毁. 构造函数和拷贝构造函数也私有化了, 这样类的使用者不能自行实例化.

      • 另外, 多个不同的 Singleton 实例的析构顺序与构造顺序相反.

    • 考虑多线程+自动析构:(补充)C++中的单例模式

      class Singleton
      {
      private:
          Singleton(){}
          ~Singleton(){}
          static Singleton *pInstance;
      
          class Garbo{                           //它的唯一工作就是在析构函数中删除Singleton的实例
          public:
              ~Garbo(){
                  if (pInstance != NULL){
                      Lock();
                      if (pInstance != NULL){
                          delete pInstance;
                          pInstance = NULL;
                          cout << "Delete instance!" << endl;
                      }
                      Unlock();
                  }
              }
          };
          static Garbo garbo;                  //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数
      
      public:
      	static Singleton *GetInstance()        // 对GetInstance稍加修改,这个设计模板便可以适用于可变多实例情况,如一个类允许最多五个实例。
      	{
      	    if (pInstance == NULL)            //判断是否第一次调用
      	    {
      	        Lock();
      	        if (pInstance == NULL)        // 此处进行了两次m_Instance == NULL的判断,是借鉴了Java的单例模式实现时,
      	                                      // 使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,
      	                                      // 而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。
      	        {
      	            pInstance = new Singleton();
      	            cout << "Create instance" << endl;
      	        }
      	        Unlock();
      	    }
      	    return pInstance;
      	}
      };
      Singleton *Singleton::pInstance = NULL;
      Singleton::Garbo Singleton::garbo;
      

  • 用C++设计一个不能被继承的类。
    • 构造函数或析构函数为私有函数,所以该类是无法被继承的,

  • 如何定义一个只能在堆上定义对象的类? 栈上呢?
    • 只能在堆内存上实例化的类:将析构函数定义为private,在栈上不能自动调用析构函数,只能手动调用。也可以将构造函数定义为private,但这样需要手动写一个函数实现对象的构造。

    • 只能在栈内存上实例化的类:将函数operator new和operator delete定义为private,这样使用new操作符创建对象时候,无法调用operator new,delete销毁对象也无法调用operator delete。

    • 设计一个只能在堆上或栈上实例化的类


  • 写string类的构造,析构,拷贝函数

    String 类的原型如下

    class String
    {
    public:
             String(const char *str=NULL); //构造函数
             String(const String &other); //拷贝构造函数
             ~String(void); //析构函数
             String& operator=(const String &other); //等号操作符重载
             ShowString();
    
    private:
             char *m_data; //指针
    };
    
    String::~String()
    {
       delete [] m_data; //析构函数,释放地址空间
    }
    String::String(const char *str)
    {
       if (str==NULL)//当初始化串不存在的时候,为m_data申请一个空间存放'\0';
        {
           m_data=new char[1];
           *m_data='\0';
        }
       else//当初始化串存在的时候,为m_data申请同样大小的空间存放该串;
        {
           int length=strlen(str);
           m_data=new char[length+1];
           strcpy(m_data,str);
        }
    }
    
    String::String(const String &other)//拷贝构造函数,功能与构造函数类似。
    {
       int length=strlen(other.m_data);
       m_data=new [length+1];
       strcpy(m_data,other.m_data);
    }
    String& String::operator =(const String &other) 
    {
       if (this==&other)//当地址相同时,直接返回;
           return *this; 
    
       delete [] m_data;//当地址不相同时,删除原来申请的空间,重新开始构造;
       int length=sizeof(other.m_data);
       m_data=new [length+1];
       strcpy(m_data,other.m_data);
       return *this; 
    }
    
    String::ShowString()//由于m_data是私有成员,对象只能通过public成员函数来访问;
    {
         cout<<this->m_data<<endl;
    }
    
    main()
    {
    	String AD;
    	char * p="ABCDE";
    	String B(p);
    	AD.ShowString();
    	AD=B;
    	AD.ShowString();
    }
    

  • 多重类构造和析构的顺序
    • 先调用基类的构造函数,在调用派生类的构造函数

    • 先构造的后析构,后构造的先析构


内存分配:

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

  • c++运行时各类型内存分配(堆,栈,静态区,数据段,BSS,ELF),BSS段。

  • sizeof求大小(字节对齐原则)

  • C++四种强制类型转换,

指针

  • 防止指针的越界使用,
    1. 防止数组越界
    2. 防止向一块内存中拷贝过多的内容
    3. 防止使用空指针
    4. 防止改变const修改的指针
    5. 防止改变指向静态存储区的内容
    6. 防止两次释放一个指针
    7. 防止使用野指针.

  • 指针的移动问题,
    • 指针P ++具体移动的字节数等于指针指向的变量类型大小.

  • Const,volatile修饰指针的含义,

  • 指针与地址的区别?
    • 区别:
      1. 指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在.
      2. 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址.

  • 指针和引用的区别(一般都会问到)
    • 相同点:

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

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

  • 迭代器与普通指针有什么区别

  • 智能指针的原理,
    • 智能指针:实际指行为类似于指针的类对象 ,它的一种通用实现方法是采用引用计数的方法。
    1. 智能指针将一个计数器与类指向的对象相关联,引用计数跟踪共有多少个类对象共享同一指针。
    2. 每次创建类的新对象时,初始化指针并将引用计数置为1;
    3. 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;
    4. 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;这是因为左侧的指针指向了右侧指针所指向的对象,因此右指针所指向的对象的引用计数+1;
    5. 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
    6. 实现智能指针有两种经典策略:一是引入辅助类,二是使用句柄类。这里主要讲一下引入辅助类的方法

数据结构算法:

  • 链表、树、哈希表、有效避免hash结果值的碰撞????
  • 排序算法性能比较

操作系统:


  • linux的内存管理机制,内存寻址方式,什么叫虚拟内存,内存调页算法,任务调度算法、
    • Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制

    • 内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。


  • 进程和线程、进程间及线程通信方式、共享内存的使用实现原理: 之前写过。

  • 死锁必要条件及避免算法、
    • 必要条件:
    1. 资源不能共享,只能由一个进程使用。
    2. 请求与保持(Hold andwait):已经得到资源的进程可以再次申请新的资源。
    3. 不可剥夺(Nopre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
    4. 循环等待:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源
    • 处理死锁的策略:
    1. 忽略该问题。
    2. 检测死锁并且恢复。
    3. 仔细地对资源进行动态分配,以避免死锁。
    4. 通过破除死锁四个必要条件之一,来防止死锁产生。)

  • 动态链接和静态链接的区别、
    • 动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外的可执行模块中,在运行时再装入;
    • 静态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了。

  • c程序辨别系统是16位or32位,、
    	//法一:int k=~0;
    	
    	if((unsigned int)k >63356) cout<<"at least 32bits"<<endl;
    	else cout<<"16 bits"<<endl;
    	
    	//法二://32为系统
    	
    	int i=65536;
    	cout<<i<<endl;
    	int j=65535;
    	cout<<j<<endl;
    

  • 大端or小端字节序
    1. Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
    2. Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    • 大端模式:
      低地址 -----------------> 高地址
      0x12 | 0x34 | 0x56 | 0x78
    • 小端模式:
      低地址 ------------------> 高地址
      0x78 | 0x56 | 0x34 | 0x12
      BOOL IsBigEndian()  
      {  
          int a = 0x1234;  
          char b =  *(char *)&a;  //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分  
          if( b == 0x12)  
          {  
              return TRUE;  
          }  
          return FALSE;  
      }
      
    • 联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写:
      BOOL IsBigEndian()  
      {  
          union NUM  
          {  
              int a;  
              char b;  
          }num;  
          num.a = 0x1234;  
          if( num.b == 0x12 )  
          {  
              return TRUE;  
          }  
          return FALSE;  
      }
      

  • 常见的信号、系统如何将一个信号通知到进程、
    • 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。

    • 进程之间可以互相通过系统调用kill发送软中断信号。
      SIGHUP 1 A 终端挂起或者控制进程终止
      SIGINT 2 A 键盘中断(如break键被按下)
      SIGQUIT 3 C 键盘的退出键被按下
      SIGILL 4 C 非法指令
      SIGABRT 6 C 由abort(3)发出的退出指令
      SIGFPE 8 C 浮点异常
      SIGKILL 9 AEF Kill信号
      SIGSEGV 11 C 无效的内存引用
      SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道

    • 进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,


  • linux系统的各类同步机制、linux系统的各类异步机制。

  • 如何实现守护进程
    • 守护进程最重要的特性是后台运行。
    1. 在后台运行。

      为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

      if(pid=fork())
      exit(0); //是父进程,结束父进程,子进程继续

    2. 脱离控制终端,登录会话和进程组

      有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:

       setsid();
      

      说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

    3. 禁止进程重新打开控制终端

      现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

       if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
      
    4. 关闭打开的文件描述符

      进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

       for(i=0;i 关闭打开的文件描述符close(i);>
      
    5. 改变当前工作目录

      进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir("/")

    6. 重设文件创建掩模

      进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

    7. 处理SIGCHLD信号

      处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

       signal(SIGCHLD,SIG_IGN);
      

      这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。


  • 标准库函数和系统调用的区别
    • 系统调用

      • 系统调用提供的函数如open, close, read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为 size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如fd=open(/"/dev/video/", O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。
      • 系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。
      • 系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性
      • 系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。
    • 库函数调用

      • 标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose,fflush, fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_t fwrite(const void *buffer,size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen(/"~/proj/filename/",/“w/”)。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。
      • 库函数调用通常用于应用程序中对一般文件的访问。
      • 库函数调用是系统无关的,因此可移植性好。
      • 由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作。

  • 五种I/O 模式
    1. 阻塞I/O (Linux下的I/O操作默认是阻塞I/O,即open和socket创建的I/O都是阻塞I/O)
    2. 非阻塞 I/O (可以通过fcntl或者open时使用O_NONBLOCK参数,将fd设置为非阻塞的I/O)
    3. I/O 多路复用 (I/O多路复用,通常需要非阻塞I/O配合使用)
    4. 信号驱动 I/O (SIGIO)
    5. 异步 I/O
    • 对于sock编程来说:

      • 第一步: 一般来说是等待数据从网络上传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;
      • 第二步: 是从内核中把数据拷贝到程序的数据区中。
    • 阻塞I/O模式: 进程处于阻塞模式时,让出CPU,进入休眠状态

      • 阻塞 I/O 模式是最普遍使用的 I/O 模式。是Linux系统下缺省的IO模式。一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为Linux系统默认的IO模式是阻塞模式)
    • 非阻塞模式I/O: 非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源。 我们开始对 recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。 应用程序不停的 polling 内核来检查是否 I/O操作已经就绪。这将是一个极浪费 CPU资源的操作。这种模式使用中不是很普遍。

    • I/O多路复用:针对批量IP操作时,使用I/O多路复用,非常有好。

      • 对于单个I/O操作,和阻塞模式相比较,select()和poll()或epoll并没有什么高级的地方。

      • 多路复用的高级之处在于:它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

      • IO 多路技术一般在下面这些情况中被使用:

      1. 当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。
      2. 当程序需要同时进行多个套接字的操作的时候。
      3. 如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
      4. 如果一个服务器程序同时使用 TCP 和 UDP 协议。
      5. 如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd就是这样的)。
    • 异步IO模式有:

      1. 信号驱动I/O模式
      2. 异步I/O模式
      • 信号驱动I/O模式, 我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。
      • 异步I/O模式: 当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。
      • 异步 I/O 和 信号驱动I/O的区别是:
        1. 信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。

        2. 异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。


  • select,poll,epoll
    • 常用模型的缺点

      • PPC/TPC 模型: 两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。

      • select 模型

      1. 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,www.linuxidc.com 由FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …
      2. 效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了??!!
      3. 内核 / 用户空间内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。
      • poll 模型

        • 基本上效率和select 是相同的,select 缺点的 2 和 3 它都没有改掉。
      • Epoll 的提升

        1. poll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 察看。
        2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。
        3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。
    • Epoll 为什么高效

      • Epoll 的高效和其数据结构的设计是密不可分的,这个下面就会提到。

        • 首先回忆一下select 模型,当有I/O 事件到来时,select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;代码像下面这样:
        • Epoll 不仅会告诉应用程序有I/0事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。
      • Epoll 关键数据结构

        • 前面提到Epoll 速度快和其数据结构密不可分,其关键数据结构就是:

            struct epoll_event {
                __uint32_tevents;      // Epoll events
                epoll_data_tdata;      // User data variable
            
            };
            typedef union epoll_data {
                void *ptr;
                int fd;
                __uint32_t u32;
                __uint64_t u64;
            } epoll_data_t;
          
        • 可见epoll_data 是一个 union 结构体 , 借助于它应用程序可以保存很多类型的信息 :fd 、指针等等。有了它,应用程序就可以直接定位目标了。

    • select和epoll的区别(必问)

    • epoll哪些触发模式,有啥区别?(必须非常详尽的解释水平触发和边缘触发的区别,以及边缘触发在编程中要做哪些更多的确认)

      • epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

      • epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

      • 另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

    • 惊群现象

      • 多个进程/线程在等待同一资源是,每当资源可用,所有的进程/线程都来竞争资源,造成的后果:

        1. 系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
        2. 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
      • 最常见的例子就是对于socket描述符的accept操作,当多个用户进程/线程监听在同一个端口上时,由于实际只可能accept一次,因此就会产生惊群现象,当然前面已经说过了,这个问题是一个古老的问题,新的操作系统内核已经解决了这一问题。

      • 对于一些已知的惊群问题,内核开发者增加了一个“互斥等待”选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:

      1. 当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
      2. 当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。也就是说,对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

  • 块设备和字符设备有什么区别
    1. 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。
    2. 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。
    3. 两种设备本身并没用严格的区分,主要是字符设备和块设备驱动程序提供的访问接口(file I/O API)是不一样的。本文主要就数据接口、访问接口和设备注册方法对两种设备进行比较。

  • 用户态和内核态的区别
    1. 虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序.
    2. 当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,

  • linux文件系统:inode,inode存储了哪些东西,目录名,文件名存在哪里
    • inode包含文件的元信息,具体来说有以下内容:

      	文件的字节数
      	文件拥有者的User ID
      	文件的Group ID
      	文件的读、写、执行权限
      	文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
      	链接数,即有多少文件名指向这个inode
      	文件数据block的位置
      
    • 每个inode都有一个号码,操作系统用inode号码来识别不同的文件。

    • 用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:

      1. 首先,系统找到这个文件名对应的inode号码;
      2. 通过inode号码,获取inode信息;
      3. 最后,根据inode信息,找到文件数据所在的block,读出数据。
    • 硬链接:一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码

      • 这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。
    • 软连接

      • 文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。
      • 这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:“No such file or directory”。这是软链接与硬链接最大的不同文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。
    • 详解: 软链接和硬链接


网络:


  • TCP和UDP区别、
    • 关键点: TCP是一种面向连接的可靠的字节流服务
      1. 面向链接:TCP面向链接,面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须通过三次握手先建立一个TCP连接。在一个TCP中仅有两方彼此通信,多播和广播不能用于TCP。UDP是不可靠的传输,传输前不需要建立链接,可以应用多播和广播实现一对多的通信。
      2. 可靠性:TCP提供端到端的流量控制,对收到的数据进行确认,采用超时重发,对失序的数据进行重新排序等机制保证数据通信的可靠性。而UDP是一种不可靠的服务,接收方可能不能收到发送方的数据报。
      3. TCP是一种流模式的协议,UDP是一种数据报模式的协议。进程的每个输出操作都正好产生一个UDP数据报,并组装成一份待发送的IP数据报。TCP应用程序产生的全体数据与真正发送的单个IP数据报可能没有什么联系。TCP会有粘包和半包的现象。
      4. 效率上:速度上,一般TCP速度慢,传输过程中需要对数据进行确认,超时重发,还要对数据进行排序。UDP没有这些机制所以速度快。数据比例,TCP头至少20个字节,UDP头8个字节,相对效率高。组装效率上:TCP头至少20个字节,UDP头8个字节,系统组装上TCP相对慢。
      5. 用途上:用于TCP可靠性,http,ftp使用。而由于UDP速度快,视频,在线游戏多用UDP,保证实时性

  • TCP和UDP头部字节定义,

  • TCP和UDP三次握手和四次挥手状态及消息类型,

  • time_wait状态产生原因,keepalive,
    • TIME_WAIT:主动发起关闭的一方,表示收到了对方的FIN报文,并发送出了ACK报文。 TIME_WAIT状态下的TCP连接会等待2*MSL(Max Segment Lifetime,最大分段生存期,RFC 1122建议是2分钟),然后即可回到CLOSED 可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

    • 主要有两个原因

      1. 防止上一次连接中的包,迷路后重新出现,影响新连接。(经过2MSL,上一次连接中所有的重复包都会消失)
      2. 可靠的关闭TCP连接。 在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发 fin,如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED。
    • time_wait太多解决

      • 所以目前看来最好的办法是让每个TIME_WAIT早点过期。
      • 重用端口。
    • 对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。

      • HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个request/response,一个主要原因就是发现了这个问题。还有一个方法减缓TIME_WAIT压力就是把系统的2*MSL时间减少,因为240秒的时间实在是忒长了点,一般认为不要少于60,不然可能会有麻烦。

  • 什么是滑动窗口,超时重传。

  • 列举你所知道的tcp选项。???

  • connect会阻塞,怎么解决?(必考必问)
    • 最通常的方法最有效的是加定时器;也可以采用非阻塞模式。

  • keep alive是什么东东?如何使用?
    • 在TCP中有一个Keep-alive的机制可以检测死连接,原理很简单,TCP会在空闲了一定时间后发送数据给对方:
      1. 如果主机可达,对方就会响应ACK应答,就认为是存活的。
      2. 如果可达,但应用程序退出,对方就发RST应答,发送TCP撤消连接。
      3. 如果可达,但应用程序崩溃,对方就发FIN消息。
      4. 如果对方主机不响应ack, rst,继续发送直到超时,就撤消连接。这个时间就是默认的二个小时。

  • UDP中使用connect的好处:

  • 长连接和短连接,

  • DNS和HTTP协议,HTTP请求方式,

  • cookie,session,localstroage,

  • 一致性哈希负载均衡,

  • 描述在浏览器中敲入一个网址并按下回车后所发生的事情,

  • PING命令
    • 用到ICMP协议, ping命令所利用的原理是这样的: 网络上的机器都有唯一确定的IP地址,我们给目标IP地址发送一个数据包,对方就要返回一个同样大小的数据包,根据返回的数据包我们可以确定目标主机的存在,可以初步判断目标主机的操作系统等。


数据库:

  • 数据库中索引的理解?索引和主键区别?

  • 聚簇索引和非聚簇索引:
    • 聚集索引:索引和数据在一起。该索引中键值的逻辑顺序决定了表中相应行的物理顺序。聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个。

    • 聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。

    • Innodb中的主键是聚簇索引。适用于: 区分度高的列;范围值查询的列;被连续访问的列;

      • 不适用于:
        • 频繁更改的列 。这将导致整行移动(因为 SQL Server 必须按物理顺序保留行中的数据值)。这一点要特别注意,因为在大数据量事务处理系统中数据是易失的。
        • 宽键 。来自聚集索引的键值由所有非聚集索引作为查找键使用,因此存储在每个非聚集索引的叶条目内。
    • Innodb中的非主键索引是非聚集索引:数据存储在一个地方,索引存储在另一个地方,索引带有指针指向数据的存储位置。

    • 可以创建不唯一的聚簇索引吗(主键可以不是唯一索引吗)

      • 可以!如果未使用 UNIQUE 属性创建聚集索引,数据库引擎将向表自动添加一个四字节 uniqueifier 列。必要时,数据库引擎 将向行自动添加一个 uniqueifier 值,使每个键唯一。此列和列值供内部使用,用户不能查看或访问。
    • 在数据库中通过什么描述聚集索引与非聚集索引的?

      • 索引是通过二叉树的形式进行描述的,我们可以这样区分聚集与非聚集索引的区别:聚集索引的叶节点就是最终的数据节点,而非聚集索引的叶节仍然是索引节点,但它有一个指向最终数据的指针。
        第五:在主键是创建聚集索引的表在数据插入上为什么比主键上创建非聚集索引表速度要慢?
        有了上面第四点的认识,我们分析这个问题就有把握了,在有主键的表中插入数据行,由于有主键唯一性的约束,所以需要保证插入的数据没有重复。我们来比较下主键为聚集索引和非聚集索引的查找情况:聚集索引由于索引叶节点就是数据页,所以如果想检查主键的唯一性,需要遍历所有数据节点才行,但非聚集索引不同,由于非聚集索引上已经包含了主键值,所以查找主键唯一性,只需要遍历所有的索引页就行,这比遍历所有数据行减少了不少IO消耗。这就是为什么主键上创建非聚集索引比主键上创建聚集索引在插入数据时要快的真正原因。

  • 索引数据结构:
    • B+树
    • 非常全面的分析:MySQL索引背后的数据结构及算法原理

  • 索引的优点和缺点,
    • 建立索引的优点

      1. 大大加快数据的检索速度;
      2. 创建唯一性索引,保证数据库表中每一行数据的唯一性;
      3. 加速表和表之间的连接;
      4. 在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间。
    • 索引的缺点

      1. 索引需要占物理空间。
      2. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。

  • 关系型数据库和非关系数据库NoSQL的特点,
    • 非关系型数据库提出另一种理念,例如,以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这 样就不会局限于固定的结构,可以减少一些时间和空间的开销。使用这种方式,用户可以根据需要去添加自己需要的字段,这样,为了获取用户的不同信息,不需要 像关系型数据库中,要对多表进行关联查询。仅需要根据id取出相应的value就可以完成查询。

    • 但非关系型数据库由于很少的约束,他也不能够提供像SQL 所提供的where这种对于字段属性值情况的查询。并且难以体现设计的完整性。他只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,SQL数 据库显的更为合适。

    • 关系型数据库的最大特点就是事务的一致性:传统的关系型数据库读写操作都是事务的,具有ACID的特点,这个特性使得关系型数据库可以用于几乎所有对一致性有要求的系统中,如典型的银行系统。但是影响效率。

    • 关系数据库的另一个特点就是其具有固定的表结构,因此,其扩展性极差

    • 非关系型数据库分类:

      1. 面向高性能并发读写的key-value数据库:

        • key-value数据库的主要特点即使具有极高的并发读写性能,Redis, Tokyo Cabinet, Flare就是这类的代表
      2. 面向海量数据访问的面向文档数据库:

        • 这类数据库的特点是,可以在海量的数据中快速的查询数据,典型代表为MongoDB以及CouchDB
      3. 面向可扩展性的分布式数据库:

        • 这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化
    • 关系型数据库和非关系型数据库


  • 乐观锁与悲观锁的区别,
    • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。

    • 乐观锁:假设不会发生并发冲突,直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改才能保证完整性。这说明在乐观锁环境中,会增加并发用户读取对象的次数。

    • 乐观锁与悲观锁的区别


  • 数据库范式
    • 1NF的定义为:符合1NF的关系中的每个属性都不可再分,1NF是所有关系型数据库的最基本要求,
    • 2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖。
    • 3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖。也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。
    • BCNF范式在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。
    • 解释一下关系数据库的第一第二第三范式?

  • 数据库日志类型作用
    1. 错误日志(Error Log)
    2. 二进制日志(Binary Log & Binary Log Index)
    3. 通用查询日志(query log)
    4. 慢查询日志(slow query log)
    5. Innodb 的在线 redo 日志(innodb redo log)
    6. 更新日志(update log)

  • innodb和myisam的区别
    • innodb,聚集索引,支持外键和事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant)),不支持全文索引,不支持计数,统计的时候会遍历,
    • myisam,非聚集索引,不支持外键事务,支持全文索引,支持计数,查询效果较好

  • union和join
    • JOIN用于按照条件联接两个表,主要有四种:

      1. INNER JOIN:内部联接两个表中的记录,仅当至少有一个同属于两表的行符合联接条件时,内联接才返回行。我理解的是只要记录不符合ON条件,就不会显示在结果集内。
      2. LEFT JOIN / LEFT OUTER JOIN:外部联接两个表中的记录,并包含左表中的全部记录。如果**左表的某记录在右表中没有匹配记录,则在相关联的结果集中右表的所有选择列表列均为空值。**理解为即使不符合ON条件,左表中的记录也全部显示出来,且结果集中该类记录的右表字段为空值。
      3. RIGHT JOIN / RIGHT OUTER JOIN:外部联接两个表中的记录,并包含右表中的全部记录。简单说就是和LEFT JOIN反过来
      4. FULL JOIN / FULL OUTER JOIN: 完整外部联接返回左表和右表中的所有行。就是LEFT JOIN和RIGHT JOIN和合并,左右两表的数据都全部显示。
    • UNION运算符

      • 将两个或更多查询的结果集组合为单个结果集,该结果集包含联合查询中的所有查询的全部行。UNION的结果集列名与UNION运算符中第一个Select语句的结果集的列名相同。另一个Select语句的结果集列名将被忽略。

  • 慢查询日志:
    • 慢查询具体详解https://blog.csdn.net/qq_40884473/article/details/89455740
    • 如何利用慢查询调试??

海量数据处理:

  • bitmap 位图

  • Map-Reduce原理:
    • MapReduce 是一种比较牛批的大数据的分布式计算框架,不过现在也有新的更好的了。
    • 其实也是一种处理思路:
      1. MapReduce将输入的数据进行逻辑切片,一片对应一个Map任务
      2. Map以并行的方式处理切片
      3. 框架对Map输出进行排序,然后发给Reduce
      4. MapReduce的输入输出数据处于同一个文件系统(HDFS)
      5. 框架负责任务调度、任务监控、失败任务的重新执行
      6. 框架会对键和值进行序列化,因此键和值需要实现writable接口,框架会对键排序,因此必须实现writableComparable接口。
    • MapReduce原理

  • BloomFilter原理、布隆过滤器
    • 它实际上是一个很长的二进制向量和一系列随机映射函数(Hash函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率删除困难。Bloom Filter广泛的应用于各种需要查询的场合中,如Orocle的数据库,Google的BitTable也用了此技术。

    • Bloom Filter特点:

      1. 不存在漏报(False Negative),即某个元素在某个集合中,肯定能报出来。
      2. 可能存在误报(False Positive),即某个元素不在某个集合中,可能也被爆出来。
      3. 确定某个元素是否在某个集合中的代价和总的元素数目无关。

  • Trie树: 也叫前缀树、字典树、单词搜索树
    • 阿龙的学习笔记---转载及精炼总结github仓库:cpp-backend-reference_第1张图片
    • 可以看出,前缀是共用的,所以也叫前缀树。一般还在在末尾处标志。
    • 空间换时间,利用字符串的公共前缀来减少无谓的字符串比较以达到提高查询效率的目的。

  • B+树原理,

  • LSM树原理,

工具:

  • 编译工具GCC,
  • 调试工具GDB,
  • 性能优化工具Perf
  • 内存泄露检查工具Valgrind,
  • makefile编写
  • 其他工具: netstat,ps,top,df,fdisk,lsof,ifconfig,uname,kill,tcpdump,ipcs,grep

其他:安全,加密方式(DES,SHA)

  • 软件开发理论的问题! 软件开发者面试百问

相关书籍


  • 语言类:
    C: C程序设计语言(K&R)->C和指针->C专家编程->C陷阱与缺陷->你必须知道的495个C语言问题
    C++: C++ primer -> effective C++->深度探索C++对象模型 ->stl源码分析->C++必知必会
    java:java编程思想->java并发编程->深入理解Java虚拟机:JVM高级特性与最佳实践
    

  • 算法和数据结构:
    算法导论->数据结构与算法分析(维斯)->编程之美->剑指offer
    

  • 操作系统:
    深入理解计算机操作系统->编译原理(龙书)
    鸟哥的linux私房菜->linux内核设计与实现->深入理解linux内核
    linux shell脚本攻略(短小精悍)
    

  • 网络编程:
    TCP/IP协议详解v1->
    unix高级环境编程->
    unix网络编程(卷1&2->
    unix编程艺术(进阶)
    

  • 视野:
    大型网站技术架构:核心原理与案例分析,
    深入理解nginx:模块开发与架构解析,
    大规模分布式存储系统 : 原理解析与架构实战	
    

  • 其他:
    程序员自我修养,
    重构,
    编写可读代码的艺术,
    headfirst设计模式
    

相关网络资源

Coolshell:						http://coolshell.cn/
Matrix67大牛的博客:				http://www.matrix67.com/blog/。
July的CSDN博客:					http://blog.csdn.net/v_JULY_v。
何海涛博客:						http://zhedahht.blog.163.com/。
笔试面试经典问题与解答:			http://hawstein.com/posts/ctci-solutions-contents.html
LeetCode:						http://leetcode.com/
这里有不少笔试题集锦:			http://blog.csdn.net/hackbuteer1
程序员编程艺术:面试和算法心得		http://taop.marchtea.com/

你可能感兴趣的:(阿龙的学习笔记,#,C++,#,后台开发)