C++后台开发面试常见问题汇总

搬运自牛客网大神总结


extern关键字

  • extern修饰变量
    是个声明,此变量/函数是在别处定义的,要在此处引用
  • extern修饰函数
    暗示这个函数可能在别的源文件里定义,没有其它作用。
  • extern C的作用?用法?
    告诉编译器在编译函数名时按着C的规则去翻译相应的函数名而不是C++的(C++要支持多态,C没有)

static关键字作用

  1. 修饰局部变量
    被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)
    局部静态变量使得该变量在退出函数后,不会被销毁,因此再次调用该函数时,该变量的值与上次退出函数时值相同。值得注意的是,生命周期并不代表其可以一直被访问,因为变量的访问还受到其作用域的限制。
  2. 修饰全局变量
    全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问。
  3. static修饰函数使得函数只能在包含该函数定义的文件中被调用。
  4. static修饰成员变量
    static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化。因为static是所有对象共享的东西嘛,必须要比对象先存在的。

static在C++中对于静态成员变量和静态成员函数。所有的对象都只维持同一个实例。相当于类的属性

  1. static修饰成员函数
  2. 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员。
  3. 注意事项
  • 出现在类体外的函数不能指定关键字static;
  • 静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
  • 静态成员函数不能访问非静态成员函数和非静态数据成员;
  • 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比,速度上会有少许的增长;
  • 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。

volatile关键字

  1. 访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。
  2. 一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他。

enum枚举变量

某个枚举变量的值默认为前一个变量值加一
第一个枚举变量默认值为0
枚举变量值是可以重复的

const的作用

修饰变量,局部变量,全局变量,成员变量(必须初始值列表)
修饰引用作为函数参数,保护值不会改变,同时也针对常量参数
修饰成员函数,类的成员函数加上const限定可以声明此函数不会更改类对象的内容(并不牢靠)
修饰返回值,表明返回的数据是不可修改的
修饰指针,左定值,右定向,const在*左侧表示所指内容是常量,在*右侧表示指针本身是常量不可变

new和malloc区别

  1. new分配内存按照数据类型进行分配,malloc分配内存按照大小分配;
  2. new不仅分配一段内存,而且会调用构造函数,但是malloc则不会。new的实现原理?但是还需要注意的是,之前看到过一个题说int p = new int与int p = new int()的区别,因为int属于C++内置对象,不会默认初始化,必须显示调用默认构造函数,但是对于自定义对象都会默认调用构造函数初始化。翻阅资料后,在C++11中两者没有区别了,自己测试的结构也都是为0;
  3. new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
  4. new是一个操作符可以重载,malloc是一个库函数;
  5. new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
  6. malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作;
  7. new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。因此对于new,正确的姿势是采用try...catch语法,而malloc则应该判断指针的返回值。为了兼容很多c程序员的习惯,C++也可以采用new nothrow的方法禁止抛出异常而返回NULL;
  8. new和new[]的区别,new[]一次分配所有内存,多次调用构造函数,分别搭配使用delete和delete[],同理,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n;
  9. 如果不够可以继续谈new和malloc的实现,空闲链表,分配方法(首次适配原则,最佳适配原则,最差适配原则,快速适配原则)。delete和free的实现原理,free为什么直到销毁多大的空间?

C++多态性与虚函数

  1. C++多态的实现
    多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
    动态多态实现有几个条件:
    (1) 虚函数;
    (2) 一个基类的指针或引用指向派生类的对象;
    基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
    每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
    虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
  2. 虚函数的作用
  • 用于实现多态
  • 在设计上还具有封装和抽象的作用。比如抽象工厂模式。
  1. 动态绑定是如何实现的
  2. 静态多态和动态多态
    静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。
  3. 虚函数表
    编译器为每一个类维护一个虚函数表,每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。
  4. 纯虚函数
    纯虚函数定义 virtual ~myClass() = 0;
  5. 为什么对于存在虚函数的类中析构函数要定义成虚函数
    为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。
  6. 析构函数能抛出异常吗?
    肯定是不能。 C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
  • 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  • 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
  1. 构造函数可以调用虚函数吗?
    不可以,构造是按继承顺序的,先基类,后派生类,如果构造期间调用了一个子类的虚函数,但是子类还未构造出来,出现未定义行为。析构函数同理。

C++空类默认的成员函数

C++中空类默认会产生以下6个函数:

  • 默认构造函数
  • 复制构造函数
  • 析构函数
  • 赋值运算符重载函数
  • 取地址符重载函数
  • const取地址符重载函数

指针和引用

  1. 指针保存的是所指对象的地址,引用是所指对象的别名,指针需要通过解引用间接访问,而引用是直接访问;
  2. 指针可以改变地址,从而改变所指的对象,而引用必须从一而终;
  3. 引用在定义的时候必须初始化,而指针则不需要;
  4. 指针有指向常量的指针和指针常量,而引用没有常量引用;
  5. 指针更灵活,用的好威力无比,用的不好处处是坑,而引用用起来则安全多了,但是比较死板。

指针与数组

  1. 一个一维int数组的数组名实际上是一个int* const 类型;
  2. 一个二维int数组的数组名实际上是一个int (*const p)[n];
  3. 数组名做参数会退化为指针,除了sizeof

C++中异常的处理方法

关键字有try, catch, throw
使用try{} catch(){}来捕获异常,如果本级没有带适当类型参数的catch块,则不能捕获异常,异常就会向上一级传递,如果一直没有捕获,C++会使用默认的异常处理函数

回调函数

回调函数是通过函数指针调用的函数,把函数的指针(地址)作为参数传递给另一个函数
回调函数与应用程序接口(API)非常接近,都是跨层调用的函数,区别是API是低层给高层的调用,回调函数则相反,是高层提供给低层的调用,必须由高层来安装

内存泄露

所谓内存泄漏是指由于疏忽或错误导致程序未能释放已经不再使用的内存的情况,一般指堆内存的泄露,失去对该段内存的控制,因而造成了内存的浪费,会导致CPU资源耗尽的严重后果

缓冲区溢出

缓冲区是成勋运行的时候机器内存的一个连续块
当向缓冲区内填充数据位数超过了缓冲区自身的容量限制,发生溢出的数据覆盖在合法数据(数据,下一条指令的指针,函数返回地址等),解决办法是检查数据长度

野指针与空悬指针

  • 野指针是指向不可用内存的指针,没有被正确的初始化,指向随机地址
  • 空悬指针:曾经指向有效地址,但原来内存被释放了,现在不再有效

栈空间的最大值

windows下栈的大小是2MB,堆的大小一般小于2GB
Linux默认栈空间大小为8MB,可以通过 ulimit -s 来设置

智能指针的原理和实现

1. 构造函数中计数初始化为1;
2. 拷贝构造函数中计数值加1;
3. 赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
4. 析构函数中引用计数减一;
5. 在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
6. share_prt与weak_ptr的区别?

share_ptr可能出现循环引用,从而导致内存泄露

class A
{
public:
share_ptr p;
};

class B
{
public:
share_ptr p;
}

int main()
{
while(true)
{
share_prt pa(new A()); //pa的引用计数初始化为1
share_prt pb(new B()); //pb的引用计数初始化为1
pa->p = pb; //pb的引用计数变为2
pb->p = pa; //pa的引用计数变为2
}
//假设pa先离开,引用计数减一变为1,不为0因此不会调用class A的析构函数,因此其成员p也不会被析构,pb的引用计数仍然为2;
//同理pb离开的时候,引用计数也不能减到0
return 0;
}

weak_ptr是一种弱引用指针,其存在不会影响引用计数,从而解决循环引用的问题

C++的四种类型转换

  1. const_cast用于将const变量转为非const
  2. static_cast用的最多,对于各种隐式转换,非const转const,void*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
  3. dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
  4. reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
  5. 为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

内存对齐原则

  1. 从0位置开始存储;
  2. 变量存储的起始位置是该变量大小的整数倍;
  3. 结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
  4. 结构体中包含结构体,从结构体中最大元素的整数倍开始存;
  5. 如果加入pragma pack(n) ,取n和变量自身大小较小的一个。

超高频问题

struct Q{
    char c;
    int num1;
    double num2;
};

上述代码中sizeof(Q)为多少? 16
struct的对其系数和以下几个关系有关

  1. 元素大小
  2. 元素顺序
  3. pragma参数

对齐规则

  1. struct中成员在内存中按顺序排列,在没有#pargma pack(n)的情况下,各个成员的对齐系数为自己的长度
  2. 在有#pargma pack(n)的情况下,各个成员的对齐系数为min(自己的长度,n)
  3. struct整体的对齐系数为对齐系数中最大的
  4. 依次排列时要满足地址对对齐系数取模为0

内联函数有什么优点?与宏定义的区别?

  1. 宏定义在预处理阶段进行替换
  2. 内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
  3. 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
  4. 使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) ab,这很危险,正确写法:#define MUL(a, b) ((a)(b))

C++内存管理

  1. C++内存分为几块?
    在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
  • 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改

在此注意自由存储区和堆

  • 自由存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。
  • new所申请的内存区域在C++中称为自由存储区。借由堆实现的自由存储,可以说new所申请的内存区域在堆上。
  • 堆与自由存储区还是有区别的,它们并非等价。
  • 学会迁移,可以说到malloc,从malloc说到操作系统的内存管理,说道内核态和用户态,然后就什么高端内存,slab层,伙伴算法,VMA可以巴拉巴拉了,接着可以迁移到fork()。

STL里的内存管理

STL内存分配分为一级分配器和二级分配器,一级分配器就是采用malloc分配内存,二级分配器采用内存池。

二级分配器设计的非常巧妙,分别给8k,16k,..., 128k等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块10K的内存,那么就找到最小的大于等于10k的块,也就是16K,从16K的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。
如果要分配的内存大于128K则直接调用一级分配器。
为了节省维持链表的开销,采用了一个union结构体,分配器使用union里的next指针来指向下一个节点,而用户则使用union的空指针来表示该节点的地址。

STL里set和map是基于什么实现的?红黑树的特点?

STL的set和map都是基于红黑树实现的

AVL是一种高度平衡的二叉树,所以通常的结果是,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果场景中对插入删除不频繁,只是对查找特别有要求,AVL还是优于红黑的。

红黑树的应用:STL,epoll在内核中的实现

  1. 每个结点或者为黑色或者为红色。
  2. 根结点为黑色。
  3. 每个叶结点(实际上就是NULL指针)都是黑色的。
  4. 如果一个结点是红色的,那么它的两个子节点都是黑色的(也就是说,不能有两个相邻的红色结点)。
  5. 对于每个结点,从该结点到其所有子孙叶结点的路径中所包含的黑色结点数量必须相同。

红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。

如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。

必须在构造函数初始化列表里进行初始化的数据成员有哪些

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
  4. 要注意,对非内置类型成员,肯定还是列表的性能好,因为省略了一次复制的过程

模板特化

模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是如果这个模板有多个类型,那么只限定其中的一部分。
模板特化的目的就是对于某一种变量类型具有不同的实现,因此需要特化版本。例如,在STL里迭代器为了适应原生指针就将原生指针进行特化。

数据结构和算法

手写strcpy

char* strcpy(char* dst, const char* src){
    assert(dst);
    assert(src);
    char* p = dst;
    while((*dst++ = *src++)!='\0') ;
    return p;
}

若考虑重叠,则有:

char* strcpy(char* dst, const char* src){
    assert(dst);
    assert(src);
    int size = strlen(src)+1;
    char* p = dst;
    if(dst > src && dst < src+size)
    {
        dst = dst + size - 1;
        src = dst + size - 1;
        while(size--)
            *dst-- = *src--;
    }
    else
    {
        while(size--)
            *dst++ = *src++;
    }
    return p;
}

手写memcpy函数

void* memcpy(void* dst, const void* src, size_t size){
    if(dst == NULL || src == NULL)
        return NULL;
    void* p = dst;
    char* pdst = (char*)dst;
    char* psrc = (char*)src;
    if(pdst>src && pdst

手写strcat函数

char* strcat(char* dst, const char* src){
    char* p = dst;
    while(*dst != '\0')
        dst++;
    while(*dst++ = *src++ !='\0') ;
    return p;
}

手写strcmp函数

int strcmp(const char* s1, const char* s2){
    while(*s1 == *s2 && *s1!='\0'){
        ++s1;++s2;
    }
    return *s1-*s2;
}

Hash表

  1. 根据关键码值(key value)直接访问的数据结构,实现有拉链法(链表),以及开放地址法解决冲突
  2. 开放地址法常见策略:
  • 线性探测
  • 线性补偿探测法
  • 随机探测
  • 平方探测
  1. STL中hash_map扩容发生什么?
    (1) 创建一个新桶,该桶是原来桶两倍大最接近的质数(判断n是不是质数的方法:用n除2到$sqrt(n)$范围内的数) ;
    (2) 将原来桶里的数通过指针的转换,插入到新桶中(注意STL这里做的很精细,没有直接将数据从旧桶遍历拷贝数据插入到新桶,而是通过指针转换)
    (3) 通过swap函数将新桶和旧桶交换,销毁新桶。

  2. Redis中hash扩容?
    容量扩张是一次完成的,期间要花很长时间一次转移hash表中的所有元素。
    redis中的dict.c中的设计思路是用两个hash表来进行进行扩容和转移的工作,把第一个hash表所有元素的转移分摊为多次转移,而且每次转移的期望时间复杂度为O(1)。这样就不会出现某一次往字典中插入元素要等候很长时间的情况了。

  1. 二叉树结构,二叉搜索树在二叉树的基础上加上了这样的一个性质:对于树中的每一个节点来说,如果有左儿子的话,它的左儿子的值一定小于它本身的值,如果有右儿子的话,它的右儿子的值一定大于它本身的值。
  2. 二叉树的六种遍历(递归,非递归)
  3. 二叉树的层序遍历
  4. 递归是解决二叉树相关问题的神级方法
  5. 树的各种常见算法题

什么是红黑树

  • 节点为红色或黑色
  • 根节点为黑色
  • 每个叶节点(NIL节点,空节点)是黑色的
  • 每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点)
  • 从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点

红黑树与AVL树的区别

  • 红黑树与AVL树都是平衡树,但是AVL是完全平衡的(平衡就是值树中任意节点的左子树和右子树高度差不超过1);
  • 红黑树插入效率更高,因为AVL为了保证其完全平衡,插入和删除的时候在最坏的情况下要旋转logN次,而红黑树插入和删除的旋转次数要比AVL少,牺牲了严格的高度平衡的优越条件使得三次旋转即可平衡。

链表

  • 链表的插入和删除,单向双向链表
  • 链表的问题考虑多个指针和递归
  1. 反向打印链表(递归)
  2. 打印倒数第K个节点(前后指针)
  3. 链表是否有环(快慢指针)

栈和队列

栈(Stack)和队列(Queue)是两种操作受限的线性表。
相同点:
1. 都是线性结构。
2. 插入操作都是限定在表尾进行。
3. 都可以通过顺序结构和链式结构实现。、
4. 插入与删除的时间复杂度都是O(1),在空间复杂度上两者也一样。
5. 多链栈和多链队列的管理模式可以相同。

不同点:

  1. 删除数据元素的位置不同,栈的删除操作在表尾进行,队列的删除操作在表头进行。
  2. 应用场景不同:
    栈的应用场景包括括号问题的求解,表达式的转换和求值,函数调用和递归实现,深度优先搜索遍历等
    队列的应用场景包括计算机系统中各种资源的管理,消息缓冲器的管理和广度优先搜索遍历等。
  3. 顺序栈能够实现多栈空间共享,而顺序队列不能。

海量数据问题

类似问题的解决方法思路:首先哈希将数据分成N个文件,然后对每个文件建立K个元素最小/大堆(根据要求来选择)。最后将文件中剩余的数插入堆中,并维持K个元素的堆。最后将N个堆中的元素合起来分析。可以采用归并的方式来合并。在归并的时候为了提高效率还需要建一个N个元素构成的最大堆,先用N个堆中的最大值填充这个堆,然后就是弹出最大值,指针后移的操作了。当然这种问题在现在的互联网技术中,一般就用map-reduce框架来做了。
大数据排序相同的思路:先哈希(哈希是好处是分布均匀,相同的数在同一个文件中),然后小文件装入内存快排,排序结果输出到文件。最后建堆归并。

海量数据重复问题

使用hash_map或者位图,再利用归并的思想

排序算法

  • 必须至少能快速写出,快排,建堆,和归并
  • 种算法的时间空间复杂度,最好最差平均情况

位运算

  • 左移乘2,右移除2
  • 不用临时变量交换两个整数(多次异或)
  • 消除最后一个1
  • 不用加减乘除完成整数相加

布隆过滤器

由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元素是否在一个集合中。
优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率(但是不会漏报)

网络与TCP/IP

TCP与UDP的区别

  1. IP首部,TCP首部,UDP首部

  2. TCP和UDP区别

    • TCP基于有连接,UDP基于无连接
    • TCP能保证可靠传输,UDP不能保证可靠传输TCP
    • TCP结构复杂,消耗资源多,建立过程较慢较复杂。UDP结构简单,消耗资源少,建立过程较快
    • TCP基于流模式,UDP是数据报模式
    • TCP连接只能是点到点,而UDP可以一对一,一对多或者多对多
    • TCP有确认,重传,拥赛控制机制,UDP在没有建立连接或者对方已经退出的情况下任然会继续发送数据,导致通信流量的浪费。
  3. TCP和UDP应用场景

  • TCP:用于实现可靠传输的情况,文件非常重要,对网络拥堵有较高要求的情况。
  • UDP:用于高速传输和实时性较高的场合(即时通信),广播通信
  1. 实现UDP的可靠传输,如RTP协议,RUDP协议
    检测包的顺序,请求重传,请求者发起或接收者发起

TCP三次握手与四次挥手

建立连接

  1. 客户端发送请求包,告诉服务器:“我想和你通信?”数据包中SYN位置为1,假设其序列号为x,客户端状态变成SYN_SENT;
  2. 服务器端接受到请求包后也发送一个请求包,告诉客户端:“现在可以建立连接”。数据包中SYN位置位1,假设其序列号为y,注意客户端序列号和服务器端序列号并没有关系,他们是由各自的内核按照一定的规则生成的。但是这个应答包的32位应答号,必须是x+1,之所以加1是因为客户端发过来的包SYN位被认为占一个数据。因此,告诉下一包从x+1开始发。发送后,服务器从监听状态变成SYN_RCVD状态。
  3. 客户端发送应答数据包,告诉服务器:“那我们开始发送数据吧”。数据包应答号为y+1。客户端变成ESTABLISHED状态,即可以传输状态。
  4. 服务器端接受到应答数据包后,变成ESTABLISHED状态。
    发送数据

发送数据

  1. 客户端发送一个一个字节的数据,因此序列号为x+1;
  2. 服务端发送一个应答包,应答号为x+2,告诉客户端下次从x+2开始发;

断开连接

  1. 客户端发送请求断开的数据包,告诉服务器:“数据传完了,我要断开了”。发送一个FIN包,序列号x+2。客户端转移到FIN_WAIT_1状态。
  2. 服务器端发送应答包,告诉客户端:“行,我知道了,你断开吧!”。应答号为x+3,服务器进入CLOSE_WAIT状态。客户端收到应答后,转移到FIN_WAIT_2状态。
  3. 服务器发送一个断开数据包,告诉客户端:“既然传完了,那我这边的开关也准备关了”。序列号为y+1,发送完后服务器进入LAST_ACK状态。
  4. 客户端发送一个应答包,告诉服务器:“好的,我知道你要断开了。”应答号为y+2。客户端进入TIME_WAIT状态。

TIME_WAIT又称为2MSL等待状态,MSL是系统中定义的最大报文生存时间,任何TCP报文在网络中生存时间超过这个值就必须被丢弃。
等待MSL的原因是防止最后一个ACK丢失后可以进行重发,如果ACK丢失后,服务器会重发FIN。
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”

TCP相关技术

TCP重发机制

  1. 超时重传(RTO)
    当一个包被发送后,就开启一个定时器,如果定时时间到了,还未收到能确认该发送包的应答包,就重传一份数据。注意收到的应答包可能是该包也可能是后面包的,但是只要能确认该包被收到就行。另外如果,是因为网络延时造成重传,则接受端收到重复数据包后丢弃该包。
  2. 快速重传
    当如果发送端收到一个包的三次应答包后,立即重传,比超时重传更高效。

Nagle算法

  1. 核心思想为任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

  2. Nagle算法简单讲就是,等待服务器应答包到达后,再发送下一个数据包。数据在发送端被缓存,如果缓存到达指定大小就将其发送,或者上一个数据的应答包到达,将缓存区一次性全部发送。

Nagle算法是从发送端角度考虑减少了数据包的个数,时延应答从接收端角度考虑减少了数据包的个数。

TCP流量控制

目的:如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。
所以流量控制是点对点通信的控制,而拥塞控制是对整个网络内流量负载的控制
TCP的流量控制是利用滑动窗口机制实现的,接收方在返回的ACK中会包含自己的接收窗口的大小,以控制发送方的数据发送。


流量控制实例

如上图所示A向B发送数据。在连接建立时,B告诉A接收窗口rwnd(receiver window)= 400,单位字节,因此发送方A的发送窗口不能超过400。

(可以看出,B向A发送的三个报文段都设置了 ACK = 1以保证字段有效,后面的rwnd值就是接收方对发送方的三次流量控制。)

第一次把窗口设置为300 ,第二次100 ,最后一次为 0,即不允许发送方再发送数据的状态。

但是当某个ACK报文丢失了,就会出现A等待B确认,并且B等待A发送数据的死锁状态。为了解决这种问题,TCP引入了持续计时器(Persistence timer),当A收到rwnd=0时,就启用该计时器,时间到了则发送一个1字节的探测报文,询问B是很忙还是上个ACK丢失了,然后B回应自身的接收窗口大小,返回仍为0(A重设持续计时器继续等待)或者会重发rwnd=x。

TCP窗口滑动

窗口是TCP中为了解决应答机制等待时间过长而引入的方法,如果没有窗口,则TCP每发送一次数据就必须等待应答,收到应答后继续发送,如果没有收到则等待一段时间后重发,如果很长时间都无法收到应答则判断为网络断开。而使用窗口后,窗口的大小指无需等待应答可以连续发送多个数据包。
TCP窗口在每个传输方向都有两个窗口,发送端窗口和接收端窗口,又因为TCP是全双工通信,因此有四个窗口。
引入窗口后,TCP的应答包如果部分丢失,无需重传,由后面的应答包保证。TCP为了提高效率,采用延时再确认应答,和选择性确认应答,即收到数据包后不立即发送应答包,而是等待收到下一个或多个包后发一个应答。

TCP的拥塞控制算法和过程

网络中的链路容量、交换结点中的缓存、处理机等等都有着工作的极限,当网络的需求超过它们的工作极限时,就出现了拥塞。拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

慢开始(Slow-Start)和拥塞避免(Congestion Avoidance)结合

慢开始算法是指开始发送数据时,并不清楚网络的负荷情况,会先发送一个1字节的试探报文,当收到确认后,就发送2个字节的报文,继而4个,8个以此指数类推。

需要注意的是,慢开始的“慢”并不是指拥塞窗口的增长速率慢,而是指在TCP开始发送报文时先设置拥塞窗口=1。
拥塞避免算法是让拥塞窗口缓慢地增大,即cwnd加1,而不是如慢开始算法一样加倍。、

C++后台开发面试常见问题汇总_第2张图片
拥塞窗口变化

根据上图的实例进行分析,一开始的慢开始算法的指数增长是很恐怖的,所以为了防止拥塞窗口cwnd增长过快需要设置一个门限ssthresh,这里是16。

  1. 当 cwnd < ssthresh 时,使用上述的慢开始算法。
  2. 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
  3. 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

对超时事件作出反应

在上述对拥塞窗口的描述中,我们只是说在连接开始的时候,以指数级的速率增加,直到第一个丢失事件发生。但实际中TCP对因超时而检测到的丢包事件作出的反应与对因收到3个冗余ACK而检测到的丢包事件做出的反应是不同的。

  • 收到3个冗余ACK后:CongWin减半、窗口再线性增加。
  • 检测超时事件后:CongWin值设置为1MSS、窗口再指数增长、到达一个阈值(Threshold,初始化时被设置为一个很大的值,以使它没有初始效应。每发生一个丢包事件,Threshold就会被设置为当前CongWin值的一半)后,再线性增长。

原因:3个冗余ACK指示网络还具有某些传送报文段的能力;3个冗余ACK以前的超时,则更为 “严重”。

小结:

  • 当CongWin < Threshold时,发送者处于慢启动阶段, CongWin指数增长。
  • 当CongWin > Threshold时,发送者处于拥塞避免阶段, CongWin线性增长。
  • 当出现3个冗余确认时, 阈值Threshold设置为CongWin/2,且CongWin设置为Threshold。
  • 当超时发生时,阈值Threshold设置为CongWin/2,并且CongWin设置为1 MSS.

快重传(Fast Retransmit)和快恢复(Fast Recovery)结合

快重传是指,如果发送端接收到3个以上的重复ACK,不需要等到重传定时器溢出就重新传递,所以叫做快速重传,而快速重传以后,因为走的不是慢启动而是拥塞避免算法,所以这又叫做快速恢复算法。

如果没有快速重传和快速恢复,TCP将会使用定时器来要求传输暂停。在暂停这段时间内,没有新的数据包被发送。所以快速重传和快速恢复旨在快速恢复丢失的数据包。

C++后台开发面试常见问题汇总_第3张图片
快速重传

与快重传配合使用的还有快恢复算法,结合下图的实例来分析,其过程有以下两个要点:

C++后台开发面试常见问题汇总_第4张图片
快恢复
  1. 当发送方在cwnd=24时连续收到三个重复确认,就把慢开始门限ssthresh减半(就是上图中的24修改为12)。
  2. 接下来不执行慢开始算法,而是把cwnd值设置为门限ssthresh减半后的数值(即cwnd不是设置为1而是设置为12),然后开始执行的是拥塞避免算法,使拥塞窗口缓慢地线性增大。

这里为什么替换掉了慢开始算法呢?

这是因为收到重复的ACK不仅仅告诉我们一个分组丢失了,由于接收方只有在收到另一个报文段时才会产生重复的ACK,所以还告诉我们该报文段已经进入了接收方的缓存。也就是说,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。

TCP客户与服务器模型,用到哪些函数

服务端:

  • 创建套接字:int socket(int family,int type,int protocol);返回:非负描述字---成功   -1---失败
  • 绑定套接字:把一个套接字地址(本机IP和端口号)绑定到创建的套接字上
    int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
    返回:0---成功   -1---失败
  • 监听套接字:int listen(int sockfd, int backlog);backlog是已完成队列和未完成队列大小之和
  • 等待来自客户端的连接请求:int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);  返回已连接描述符
  • 数据传输:
    int write(int sockfd, char *buf, int len); 
    int read(int sockfd, char *buf, intlen);  
    send和recv函数:TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的传输控制参数。
  • 关闭套接字:int close(int sockfd);

客户端:

  • 连接服务器:int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
    返回:0---成功   -1---失败

UDP客户与服务器模型,用到哪些函数

UDP与TCP相比要简洁很多,UDP不需要listen,accept和connect过程。

  1. socket函数创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  2. bind函数,绑定服务器地址到套接字上
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  3. sendto函数,发送数据给指定地址
    sendto函数比send函数多出两个参数,一个是目的地址,一个是地址长度。告诉客户端发送给哪个IP地址和哪个端口号。
  4. recvfrom函数,接收数据
    recvfrom函数比recv函数多出两个参数,相当于TCP的accept函数,告诉我们是谁发送了数据过来。

域名解析过程,ARP的机制,RARP的实现

ARP(地址解析协议)

ARP协议是辅助链路层传输的,在已经知道下一站路由器的IP地址后,要将以太网包发送给目的地址,但是以太网需要的是目的mac地址不是IP地址,而通过ARP请求包就可以获得目的IP地址的mac地址。

ARP请求的过程:源主机以广播的形式,发送一个ARP请求包,所有与源主机在直连的主机都会收到一个请求包,如下图所示,请求包询问目的IP地址的mac地址,目的IP地址的主机收到这个请求后,发送一个ARP应答,告诉源主机自己的mac地址。

RARP以与ARP相反的方式工作。RARP发出要反向解析的物理地址并希望返回其对应的IP地址,应答包括由能够提供所需信息的RARP服务器发出的IP地址。

Ping和TraceRoute实现原理

  1. Ping是通过发送ICMP报文回显请求实现。
    ICMP是(Internet Control Message Protocol)Internet控制报文协议,用于在IP主机、路由器之间传递控制消息。
  2. Tracert 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。
  • 首先,tracert送出一个TTL是1的IP 数据包到目的地,当路径上的第一个路由器收到这个数据包时,它将TTL减1。此时,TTL变为0,所以该路由器会将此数据包丢掉,并送回一个「ICMP time exceeded」消息(包括发IP包的源地址,IP包的所有内容及路由器的IP地址),tracert 收到这个消息后,便知道这个路由器存在于这个路径上,接着tracert 再送出另一个TTL是2 的数据包,发现第2 个路由器...... tracert 每次将送出的数据包的TTL 加1来发现另一个路由器,这个重复的动作一直持续到某个数据包 抵达目的地。当数据包到达目的地后,该主机则不会送回ICMP time exceeded消息,一旦到达目的地,由于tracert通过UDP数据包向不常见端口(30000以上)发送数据包,因此会收到「ICMP port unreachable」消息,故可判断到达目的地。
  • tracert 有一个固定的时间等待响应(ICMP TTL到期消息)。如果这个时间过了,它将打印出一系列的*号表明:在这个路径上,这个设备不能在给定的时间内发出ICMP TTL到期消息的响应。然后,Tracert给TTL记数器加1,继续进行。

HTTP

HTTP/HTTPS 1.0、1.1、2.0

HTTP的主要特点

  1. 简单快速:当客户端向服务器端发送请求时,只是简单的填写请求路径和请求方法即可
  2. 灵活:HTTP 协议允许客户端和服务器端传输任意类型任意格式的数据对象
  3. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。(当今多数服务器支持Keep-Alive功能,使用服务器支持长连接,解决无连接的问题)
  4. 无状态:无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即客户端发送HTTP请求后,服务器根据请求,会给我们发送数据,发送完后,不会记录信息。(使用 cookie 机制可以保持 session,解决无状态的问题)

HTTP 1.1 的特点

  1. 默认持久连接节省通信量,只要客户端服务端任意一端没有明确提出断开TCP连接,就一直保持连接,可以发送多次HTTP请求
  2. 管线化,客户端可以同时发出多个HTTP请求,而不用一个个等待响应
  3. 断点续传

http2.0的特点

  • HTTP/2采用二进制格式而非文本格式
  • HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个HTTP连接就可以实现多个请求响应
  • 使用报头压缩,HTTP/2降低了开销
  • HTTP/2让服务器可以将响应主动“推送”到客户端缓存中

GET和POST的区别

本质上

  • GET是向服务器索取数据的请求
  • POST是向服务器提交数据的请求
  • GET是等幂的,POST不是等幂的,等幂就是一次执行和多次执行效果一样!DELETE,PUT,HEAD,也是等幂的,由于网络是不可靠的,安全性和等幂性就特别重要,如果POST两次相同的,会产生两个资源。

表现上

  • GET,服务器端用request.QueryString获取变量的值,POST,服务器端用request.Form获取数据
  • get传输数据是通过URL请求,以field(字段)= value的形式,置于URL后,并用"?"连接,多个请求数据间用"&"连接,如http://127.0.0.1/Test/login.action?name=admin&password=admin,这个过程用户是可见的;post传输数据通过Http的post机制,将字段与对应值封存在请求实体中发送给服务器,这个过程对用户是不可见的;
  • GET安全性低,用户可见,POST安全性高些
  • GET效率高些,只发送一次数据包,POST会发送两次TCP数据包(先header,再data)
  • 数据量大小:URL不存在参数上限,取决于特定的浏览器或服务器限制,POST数据理论上也没有限制

返回状态码

100:请求者应当继续提出请求。服务器返回此代码则意味着,服务器已收到了请求的第一部分,现正在等
待接收其余部分。
101(切换协议)请求者已要求服务器切换协议,服务器已确认并准备进行切换。

200:请求被正常处理
204:请求被受理但没有资源可以返回
206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
301:永久性重定向
302:临时重定向
303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
304:发送附带条件的请求时,条件不满足时返回,与重定向无关
307:临时重定向,与302类似,只是强制要求使用POST方法
400:请求报文语法有误,服务器无法识别
401:请求需要认证
403:请求的对应资源禁止被访问
404:服务器无法找到对应资源
500:服务器内部错误
503:服务器正忙

HTTP 协议头相关

http数据由请求行,首部字段,空行,报文主体四个部分组成
首部字段分为:通用首部字段,请求首部字段,响应首部字段,实体首部字段

https与http的区别?如何实现加密传输?

  • https就是在http与传输层之间加上了一个SSL
  • 对称加密与非对称加密:非对称加密和解密使用的是两个不同的密钥,公钥和私钥,不需要交换密钥

浏览器中输入一个URL发生什么,用到哪些协议?

浏览器中输入URL,首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议

得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理。如果采用https还会先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。

数据库

SQL语言(内外连接,子查询,分组,聚集,嵌套,逻辑)

MySQL索引方法?索引的优化?

InnoDB与MyISAM区别?

事务的ACID

事务的四个隔离级别

查询优化(从索引上优化,从SQL语言上优化)

B-与B+树区别?

MySQL的联合索引(又称多列索引)是什么?生效的条件?

分库分表

Linux

进程与线程

  • 进程与线程区别

    1. 进程是资源分配的基本单位,线程是cpu调度,或者说是程序执行的最小单位。但是并不是说CPU不在以进程为单位进行调度,虽然在某些操作系统中是这样。同一个进程中并行运行多个线程,就是对在同一台计算机上运行多个进程的模拟。
    2. 进程有独立的地址空间,而同一进程中的线程共享该进程的地址空间。比如在linux下面启动一个新的进程,系统必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种非常昂贵的多任务工作方式。而运行一个进程中的线程,它们之间共享大部分数据,使用相同的地址空间,因此启动一个线程,切换一个线程远比进程操作要快,花费也要小得多。
    3. 线程之间的通信比较方便。统一进程下的线程共享数据(比如全局变量,静态变量,打开的文件,子进程)
    4. 多进程比多线程程序要健壮。一个线程死掉整个进程就死掉了,但是在保护模式下,一个进程死掉对另一个进程没有直接影响。
    5. 线程的执行与进程是有区别的。每个独立的线程有有自己的一个程序入口,顺序执行序列和程序的出口,但是线程不能独立执行,必须依附与程序之中,由应用程序提供多个线程的并发控制。
    6. linux中进程具有父子关系,形成进程树,但是线程是平等的没有父子关系
  • 多线程VS多进程?

    1. 多进程程序,一个进程崩溃不会影响其他进程,但是进程之间的切换和通信代价较大;
    2. 多线程程序,一个线程崩溃会导致整个进程死掉,其他线程也不能正常工作,但是线程之前数据共享和通信更加方便。
    3. 进程需要开辟独立的地址空间,多进程对资源的消耗很大,而线程则是“轻量级进程”,对资源的消耗更小,对于大并发的情况,只有线程加上IO复用技术才能适应。

    因此,对于需要频繁交互数据的,频繁的对同一个对象进行不同的处理,选择多线程合适,对于一些并发编程,不需要很多数据交互的采用多进程。

  • 用线程的好处

    1. 一个任务可以分成多个子任务并行执行,他们是对一个对象在操作。
    2. 线程不需要像进程一样维护那么多信息,因此创建和销毁速度更快,拥有同一个地址空间,访问很容易
    3. 任务有CPU密集和IO等待的过程,使用线程可以最大化利用CPU

进程的内存空间布局

进程的内存布局在结构上是有规律的,具体来说对于linux系统上的进程,其内存空间一般可以粗略地分为以下几大段,从高内存到低内存排列:

  1. 内核态内存空间,其大小一般比较固定(可以编译时调整),但 32 位系统和 64 位系统的值不一样。
  2. 用户态的栈,大小不固定,可以用 ulimit -s 进行调整,默认一般为 8M,从高地址向低地址增长。
  3. 共享内存区,包括动态共享库以及mmap内存映射的区域
  4. 堆,由程序员手动分配
  5. 数据段,bss 未初始化 以及 data 已初始化的全局静态变量等
  6. 代码段,二进制文件
C++后台开发面试常见问题汇总_第5张图片
进程的内存空间.png

[图片上传失败...(image-42ade8-1525264842791)]

进程间通信方式

  • 信号量:信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

  • 命名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

  • 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

匿名管道与命名管道的区别:匿名管道只能在具有公共祖先的两个进程间使用

共享文件映射mmap

mmap建立进程空间到文件的映射,在建立的时候并不直接将文件拷贝到物理内存,同样采用缺页终端。mmap映射一个具体的文件可以实现任意进程间共享内存,映射一个匿名文件,可以实现父子进程间共享内存。

常见信号

  • SIGINT 终止进程,通常我们的Ctrl+C就发送的这个消息。
  • SIGKILL 消息编号为9,我们经常用kill -9来杀死进程发送的就是这个消息,程序收到这个消息立即终止,这个消息不能被捕获,封锁或这忽略,所以是杀死进程的终极武器。
  • SIGTERM 是不带参数时kill默认发送的信号,默认是杀死进程。(可以被捕获)
  • SIGSEGV 就是SegmentFault 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据
  • SIGCHLD 一个子进程停止或终止,默认行为忽略
  • SIGALRM 来自alarm函数的定时信号,默认行为 终止

内存管理

虚拟内存

操作系统对内存的管理

几种数据结构:

  • 位图
  • 空闲块链表

内存分配算法:

  1. 首次适配first fit
  2. 下次适配next fit从上次找到的空闲区接着找
  3. 最佳适配best fit查找整个空闲区表,能满足要求的最小空闲区
  4. 最差适配worst fit总是分配最大空闲区

内存池

内存池的作用:

  • 在C和C++语言中,经常需要动态分配内存,我们会用到new,delete,malloc,free。但是当我们分配很多小块的内存时,会造成很多的内存碎片,大大降低了内存的使用效率。为了减少内存碎片的出现,采用了内存池技术。

内存池的实现原理:

  • 内存池的先调用malloc函数申请一大块内存,然后维护一个空闲链表,该链表是一个个小的空闲内存片,每当需要内存时就从空闲链表上拿过来一个小片内存使用。如果空闲链表为空了,就从之前分配的大块内存去取几个插入到空闲链表上。如果分配的大块内存也用光了,就继续用malloc申请一大块。

进程空间和内核空间对内存的管理不同?

linux的slab层

slab是Linux操作系统的一种内存分配机制。其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内存碎片,而且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。

伙伴算法

解决问题:频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页面,由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框可能无法满足请求。

伴算法虽然能够完全避免外部碎片的产生,但这恰恰是以产生内部碎片为代价的。

优点:

  • 较好解决外部碎片问题
  • 当需要分配若干个内存页面时,用于DMA的内存页面必须连续,伙伴算法很好的满足了这个要求
  • 只要请求的块不超过512个页面(2K),内核就尽量分配连续的页面。
  • 针对大内存分配设计。

缺点:

  • 合并的要求太过严格,只能是满足伙伴关系的块才能合并,比如第1块和第2块就不能合并。
  • 碎片问题:一个连续的内存中仅仅一个页面被占用,导致整块内存区都不具备合并的条件
  • 浪费问题:伙伴算法只能分配2的幂次方内存区,当需要8K(2页)时,好说,当需要9K时,那就需要分配16K(4页)的内存空间,但是实际只用到9K空间,多余的7K空间就被浪费掉。
  • 算法的效率问题: 伙伴算法涉及了比较多的计算还有链表和位图的操作,开销还是比较大的,如果每次2n大小的伙伴块就会合并到2(n+1)的链表队列中,那么2n大小链表中的块就会因为合并操作而减少,但系统随后立即有可能又有对该大小块的需求,为此必须再从2(n+1)大小的链表中拆分,这样的合并又立即拆分的过程是无效率的。

所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。

实现原理:

  • 假设要请求一个256(129~256)个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两等分,一般用作满足需求,另一半则插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果这样的块存在,内核就把1024个页框块的256个页框用作请求,然后剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错误信号。

高端内存?

Linux是如何避免内存碎片的

  1. 伙伴算法,用于管理物理内存,避免内存碎片;
  2. 高速缓存Slab层用于管理内核分配内存,避免碎片。

共享内存的实现原理?

共享内存实现分为两种方式一种是采用mmap,另一种是采用XSI机制中的共享内存方法。mmap是内存文件映射,将一个文件映射到进程的地址空间,用户进程的地址空间的管理是通过vm_area_struct结构体进行管理的。mmap通过映射一个相同的文件到两个不同的进程,就能实现这两个进程的通信,采用该方法可以实现任意进程之间的通信。mmap也可以采用匿名映射,不指定映射的文件,但是只能在父子进程间通信。XSI的内存共享实际上也是通过映射文件实现,只是其映射的是一种特殊文件系统下的文件,该文件是不能通过read和write访问的。

同步方法有哪些?

  1. 互斥锁,自旋锁,信号量,读写锁,屏障
  2. 互斥锁与自旋锁的区别:互斥锁得不到资源的时候阻塞,不占用cpu资源。自旋锁得不到资源的时候,不停的查询,而然占用cpu资源。

++i是否是原子操作

明显不是,++i主要有三个步骤,把数据从内存放在寄存器上,在寄存器上进行自增,把数据从寄存器拷贝会内存,每个步骤都可能被中断。

大小端

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。

小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

C++后台开发面试常见问题汇总_第6张图片
大小端

判断大小端

union un
{
int i;
char ch;
};
void fun()
{
union un test;
test.i = 1;
if(ch == 1)
cout << "小端" << endl;
else
cout << "大端" << endl;
}

如何判断操作系统是32位还是64位?

  • Linux指令uname
  • 利用sizeof,判断指针大小,或者其他变量,如long, unsigned long
  • 机器位数不同,表示的数字最大值也不同
  • 对0值取反,判断是不是大于32位下所能表示的最大数

其他

设计模式

  1. 单例模式线程安全写法,参考12. C++写一个线程安全的单例模式
  2. STL里的迭代器使用了迭代器模式
  3. MVC的理解

分布式系统

  • mapreduce
  • 负载均衡
  • CDN

你可能感兴趣的:(C++后台开发面试常见问题汇总)