c++基础总结
创建:2023-10-27 18:00
更新:2025-05-20 11:25

C++了解

  1. C++和C的区别

    1. C++是面向对象编程,C是过程编程
    2. C++兼容C语言
    3. 其他的本质上都是C++对C的面向对象包装。包括对象生命周期管理,内存管理,操作符重载,继承抽象等
  2. C++中struct和class的区别

    1. struct 继承默认 public , class 继承默认 private
    2. struct 成员默认 public, class 成员默认 public
    3. class这个关键字还用于定义模板参数,就像typename。但关建字struct不用于定义模板参数
  3. main 函数执行以前,还会执行什么代码
    main 函数执行之前主要是初始化相关资源

    1. 设置栈指针
    2. 初始化static静态变量和全局变量,即data段内容
    3. 全部变量赋值,.bbs段内容
    4. 运行全局函数构造器
    5. 设置main函数需要的参数
    6. 运行main函数
  4. 写个函数在main函数执行前先运行

    __attribute((constructor))void before()
    {
        printf("before main\n");
    }
    
  5. double能够表示的最大值,以及能够表示的最大的安全值

    double各个位置的运用:

    • 符号位:1 位(0 表示正数,1 表示负数)
    • 指数位:11 位(偏移量 1023)
    • 尾数位:52 位

    最大值(最大有限正数):(2 - 2^(-52)) × 2^1023 ≈ 1.7976931348623157 × 10^308

    浮点数在表示整数时存在精度限制。安全整数是指能被精确表示且不会因舍入误差导致精度丢失的整数范围。

    double 的最大安全整数为:2^53 - 1 = 9,007,199,254,740,991。double 的尾数部分有 52 位,加上隐含的最高位 1,实际可表示的二进制位数为 53 位

  6. main之后运行

    全局对象的析构函数会在main函数之后执行;
    可以用_onexit 注册一个函数,它会在main 之后执行;

    如果需要加入一段在main退出后执行的代码,可以使用atexit()函数,注册一个函数。
    语法:

    #include <stdlib.h> 
    #include <stdio.h>
    
    int atexit(void (*)(void));
    void fn1( void ), fn2( void ), fn3( void );
    
    void _tmain(int argc, TCHAR *argv[])
    {
      atexit(fn1);
      atexit( fn2 );
      printf( "This is executed first." );
    }
    void fn1()
    {
      printf( " This is" );
    }
    void fn2()
    {
      printf( " executed next." );
      system("pause");
    }
    

    结果:

    This is executed first. 
    This is executed next.
    
  7. extern "C"

    1. 让编译器将代码按照c语言的方式进行编译
    2. 主要是c++支持函数重载等等特性,c语言不支持
    3. 同一个函数名称,实际链接过程中的名字会不一样
  8. 整数、指针、布尔变量、浮点数值如何与0比较大小?

    1. 整数直接比较
    2. 浮点数需要注意,需要转化为 if (x >= -0.00001 && x <= 0.00001)
  9. GDB调试、条件断点

    • gdb exefile corefile
    • gdb exefile <pid>
    • set args val1 val2
    • r 运行
    • c 继续
    • s 单步跟踪进入
    • n 执行一行
    • b 代码行 设置断点
    • bt 列出调用栈
    • f <栈帧> 到指定栈帧
    • p 变量 打印变量
    • info threads 查看线程情况
    • thread ID 切换到对应线程
  10. 一个C++源文件从文本到可执行文件经历的过程?

    1. 预处理

      • 宏定义替换
      • 条件编译指令处理
      • include处理
      • 删除注释,添加文件行号标识等等
    2. 编译产生汇编文件 .s文件

      • 词法语法分析后产生汇编代码文件
    3. 汇编产生目标文件 .o文件

      • 汇编器将汇编代码转换成机器码
    4. 链接产生可执行文件 .out .exe

      • 地址空间分配,符号决议,重定位等
  11. include头文件的顺序以及双引号""和尖括号<>的区别?

    1. #include头文件其实在编译预处理阶段会将整个文件内容导入进来,是一个递归处理,include顺序就是导入后的替换位置
    2. <>搜索路径是系统路径, ""先搜索用户路径,然后搜索c++安装路径,最后系统路径
    3. 正常使用:标准去,系统库用<>, 其他的用 ""就好
  12. RTTI

    1. Run-Time Type Identification 运行时类型识别
    2. 在C++层面主要体现在dynamic_cast和typeid
    3. 虚函数表的-1位置存放了指向typeinfo的指针。对于存在虚函数的类型,typeid和dynamiccast都会去查询type_info
  13. 字节对齐是什么?为什么要进行字节对齐?什么因素会影响字节对齐呢?可以让字节以1对齐么?

    1. 便于CPU快速访问,如果不对齐,可能需要访问多次拼接才能获取到一个值

    2. 合理设计struct结构,关闭字节对齐,可以节约空间,适用于网络传输等

    3. 代码强制转换的时候需要考虑字节对齐的隐患

    4. 跨平台通信:由于不同平台对齐方式可能不同,如此一来,同样的结构在不同的平台其大小可能不同,在无意识的情况下,互相发送的数据可能出现错乱,甚至引发严重的问题。因此,为了不同处理器之间能够正确的处理消息,我们有两种可选的处理方法。

      • 1字节对齐: 能够保证跨平台的结构大小一致,同时还节省了空间,但不幸的是,降低了效率。

        #pragma pack(1) /*1字节对齐*/
        struct test
        {
            int a;
            char b;
            int c;
            short d;
        };
        #pragma pack()/*还原默认对齐*/
        
      • 自己对结构进行字节填充: 访问效率高,但并不节省空间,同时扩展性不是很好,例如,当字节对齐有变化时,需要填充的字节数可能就会发生变化。

        struct test
        {
            int a;
            char b;
            char reserve[3];
            int c;
            short d;
            char reserve1[2];
        };
        
  14. 运行时多态和编译时多态

    1. 运行时多态,主要是继承
    2. 编译时多态,主要是通过模板具体化,函数重载的方式实现
  15. 字节序、字节序如何转化

    1. 组成多字节的字的字节排列顺序, 该整数的最低有效字节(类似于最低有效位)排在最高有效字节前面,则成为"小端序";反之成为"大端序"
    2. 网络字节顺序 NBO(Network Byte Order):按照从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。TCP/IP中规定好的一种数据表示格式,与具体的 CPU 类型、操作系统等无关。
    3. 主机字节顺序(HBO:Host Byte Order):不同机器 HBO 不相同,与 CPU 有关。计算机存储数据有两种字节优先顺序:Big Endian 和 Little Endian。Internet 以 Big Endian 顺序在网络上传输,所以对于在内部是以 Little Endian 方式存储数据的机器,在网络通信时就需要进行转换。
    4. 转换函数
      • htons() : unsigned short 从主机序转换到网络序
      • htonl(): unsigned long 从主机序转换到网络序
      • ntohs(): unsigned short 从网络序转换到主机序
      • ntohl(): unsigned long 从网络序转换到主机序
  16. int的数据范围

    • -0x7fffffff ~ 0x7fffffff , -2^31 ~ 2^31-1
    • 正数在计算机中表示为原码,最高位为符号位 2147483647的原码为0111 1111 1111 1111 1111 1111 1111 1111
    • 负数在计算机中表示为补码,最高位为符号位 -2147483647的原码为1111 1111 1111 1111 1111 1111 1111 1111,补码为1000 0000 0000 0000 0000 0000 0000 0001 (原码是我们正常理解的表示,计算机只能进行加法运算,所以负数用补码存储)
    • 因为0只需要一个,所以把-0拿来当做一个最小的数-2147483648。 -2147483648的补码表示为1000 0000 0000 0000 0000 0000 0000 0000,在32位没有原码。
  17. &和&&的区别?

    • & 是位运算
    • && 是逻辑运算,且是短路运算,前边的是 false 就不会再执行后边的代码了
  18. 设计模式分类

    • 创建型: 单例,原型,工厂,建造者
      • 单例:全局一个实例
      • 原型: 克隆
      • 工厂: 通过接口实现不同类工厂,不同类产品。对工厂而言,生产函数相同。对产品而言,功能函数相同。
      • 建造者:Builder模式,通过函数链设置不同参数,最终产生产品
    • 结构型: 代理,适配,桥接,组合,装饰,外观,享元
      • 代理:对对象的行为在做一层封装,进行额外处理。java有本身提供的通过接口实现的静态代理,也可以通过cglib生成子类字节码实现动态代理
      • 适配:Adapter 就是通过继承实现或者替换需要实现函数功能
      • 桥接: 传入其他接口实现
      • 组合:将功能调用同时传递给多个实现。例如可以用到 addListener() 在事件触发的时候会传递给所有的监听器
      • 外观:和组合类似,但是组合更动态,外观模式则是固定的
      • 装饰:对目标进行装饰,与桥接相反
      • 享元:对象池
    • 行为型: 模板, 策略,命令,职责链,状态,观察者,中介者,迭代器,访问者,备忘录,解释器
      • 模板:抽象类的抽象方法
      • 策略:变更属性,便是换了一个策略去执行函数
      • 命令:实现命令接口,调用执行者进行执行
      • 责任链:Handler 一般http处理过程就是通过这个来进行的
      • 状态:改变状态后,对象 handle调用不同状态的实际执行
      • 观察者:其实就是监听器
      • 中介者:类似组合,不知道有啥区别,只是意义不同?
      • 迭代器:最常用的,Iterator.next()函数
      • 访问者:Visitor。语法树解析常用的方式就是访问者主动遍历,和监听器
      • 备忘录模式:快照方式记录对象数据
      • 解释器:构建语法树然后进行解释,不常用
  19. 面向对象的特征有哪些方面

    • 封装:对象属性,方法。以及可见性
    • 继承:属性方法的继承复用
    • 多态:通过父类或者接口调用实现,在调用的时候动态指向
  20. 面向对象的设计原则

    • 单一原则:简单来讲就是实体的意义要明确,功能要明确。比较模糊的点
    • 开闭原则:对扩展开放,对修改关闭。就是尽量不修改原来的类,可以建新类或者继承等方式。
    • 里氏替换:就是子类必须能履行父类的功能
    • 接口隔离:接口尽可能小
    • 依赖反转:高层模块不依赖低层。如果需要则另外建接口
  21. const修饰的变量和#define有什么区别?

    1. #define 宏定义,在预编译的时候进行替换

    2. const修饰变量:会占据实际内存, const修饰的变量是告知编译器该变量不可修改,编译器会根据需要进行编译优化,替换可以替换的值。该变量虽然也可以通过系列操作进行修改,但是不建议,因为有编译器优化的参与,可能出现意外的结果

    3. const修饰函数返回值:

      • A:const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
      • B: const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
      • C: const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
    4. const修饰类成员函数: 防止成员函数修改成员变量,被mutable修饰的成员变量不受影响

      #include<iostream>
      using namespace std;
      class Test
      {
      public:
          Test(int _m,int _t):_cm(_m),_ct(_t){}
          void Kf()const
          {
              ++_cm; // 错误
              ++_ct; // 正确
          }
      private:
          int _cm;
          mutable int _ct;
      };
      
      int main(void)
      {
          Test t(8,7);
          return 0;
      }
      
  22. 如果同时定义了两个函数,一个带const,一个不带,会有问题吗?

    1. 对于两个成员函数没有问题
    2. 如果函数名相同,在相同的作用域内,其参数类型、参数个数,参数顺序不同等能构成函数重载。有趣的是如果同时在类中,对于函数名相同参数列表也相同的成员函数的const函数和非const函数能够构成重载。
    3. 它们被调用的时机为:如果定义的对象是常对象,则调用的是const成员函数,如果定义的对象是非常对象,则调用重载的非const成员函数。
  23. static有什么作用?如何改变变量的生命周期和作用域?

    1. 全局(静态)存储区:分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。

    2. 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。

    3. static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。

    4. static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。

    5. 局部静态变量在C++11后也是线程安全的, 且只在调用的时候初始化一次,此后不再初始化

    6. 类或者结构体中的 static 成员变量不会影响sizeof大小,且不会影响响字节对齐。只是用了个命名空间而已。

  24. volitale什么作用?

    1. 告诉编译器对访问该变量的代码就不再进行优化, 每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值
    2. 没有其他作用,不保证原子性
  25. thread_local

    1. 每个线程都会进行一次初始化
    2. 声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,它具有static变量一样的初始化特征和生命周期,虽然它并没有被声明为static
    3. 线程结束时释放
    4. 线程单独拥有的static变量,每个线程拥有各自的对应变量。可以用于线程session, 线程访问安全等
  26. cast 转换

    1. const_cast 常量指针被转化成非常量
    2. static_cast 和C语言风格强制转换的效果基本一样
    3. dynamic_cast 面向对象的多态性和程序运行时的状态,也与编译器的属性设置有关
    4. reinterpret_cast 无关类型转换
  27. constexpr

    1. c++17 支持,可以在编译阶段,根据模板参数的值编译相应的段落。

      vector<int> *ptr1 = new vector<int>{1, 2, 3};
      vector<int> *ptr2 = new vector<int>{4, 5, 6};
      
      template<bool flag>
      inline void fun(vector<int> *tmp)
      {
          if constexpr (flag)
              swap(tmp, ptr1);
          else
              swap(tmp, ptr2);        
      }
      
      vector<int> *a, *b;
      fun<true>(a);
      fun<false>(b);
      
  28. 以下四行代码的区别是什么? const char * arr = "123"; char * brr = "123"; const char crr[] = "123"; char drr[] = "123";

    • 指针指向的是 data 数据段的指针
    • 数组是栈上的数据空间
  29. 函数中 char arr[20], char *p = new char[20], char *p = new char[20]() 的区别?初始化和未初始化的情况?放在内存的那个位置?

    • char arr[20] 内存分配在栈上,没有初始化
    • char *p = new char[20] 内存分配在堆,没有初始化
    • char *p = new char[20]() 内存分配在堆,初始化为0. new 对象带括号的才会有自动初始化,否则没有初始化,对class也一样
  30. C++内存分配有哪几种方式? 画出C++内存布局图?

    1. 内存分配:全局数据区data区,栈内存分配,堆内存分配。堆内存分配可以使用c语言的方式,需要手动free. 使用new 需要手动delete.

    2. 内存布局

      1. 代码区(text段)
      2. data段:初始化的全局变量和静态变量在一块区域data段, 主要存储常量字符串,以及一些内嵌资源数据
      3. bss段:未初始化的全局变量和未初始化的静态变量在相邻的另一块区域bss段
      4. 栈区:函数执行过程中的局部变量存储读取区域
      5. 堆区:自由申请释放的内存区域

      注意:

      • text 和 data 段都在可执行文件中,由系统从可执行文件中加载;
      • 而 bss 段不在可执行文件中,由系统初始化。
    3. 不存在virutal函数的类内存与c的struct区别不大,函数会变为全局函数

    4. 存在virtual函数的类,会多出虚函数表,存在虚函数的类,具体内存结构键虚函数篇章

  31. 一个类,里面有static,virtual,之类的,这个类的内存分布

  32. C++里是怎么定义常量的?常量存放在内存的哪个位置?

    1. 宏定义方式定义常量,没有存储位置,预编译时替换
    2. const 变量。存储在全部变量bss段或者函数栈上仅仅表示常量不可变
  33. C语言是怎么进行函数调用的?

    1. 将当前函数的程序指针和即将调用的函数的参数压入栈中。至于具体的存储策略,网络,各个系统平台还不太一样
    2. 然后程序指针跳转到对应的函数段上执行代码
  34. C语言参数压栈顺序?

    1. 从右往左入栈
    2. 不定长参数无法确认地址,并且函数参数的个数也不确定,C/C++中规定了函数参数的压栈顺序是从右至左
  35. C++函数栈空间的最大值

    1. ubuntu 下默认8M
    2. ulimit -a 查看
    3. ulimit -s [大小] 修改,单位KB
  36. C++如何处理返回值?

    1. 就是调用函数提前占位申请
    2. 然后复制或者移动过去

内存管理

  1. C++中拷贝赋值函数的形参能否进行值传递?

    1. 不能,不然就递归炸栈了
  2. new/delete与malloc/free的区别是什么

    1. new/delete时c++提供的,由c++自动计算需要申请的内存空间大小,可以自动调用构造函数进行初始化,delete的时候会调用析构函数
    2. malloc/free是系统库,需要自己计算申请大小,然后手动释放自身以及关联内存资源
  3. 什么是memory leak,也就是内存泄漏, 如何判断内存泄漏,C++如何处理内存泄漏?

    1. 内存泄漏就是申请内存使用完之后没有进行释放
    2. 判断内存泄漏:程序内存过大不符合预期,程序内存持续增长,通过工具检测程序关闭的时候哪些内存数据没有释放
    3. 处理:
      1. 如果对代码熟悉,可以通过全局查找申请的位置,排查释放位置,添加日志进行排查,能快简洁快速查找出对应的问题
      2. 如果对代码不熟悉,则需要通过工具进行排查, Valgrind
      3. 其实c++写代码已经很少能遇到内存泄漏,更常遇到的是野指针访问,这个就通过core文件进行分析即可
  4. 什么时候会发生段错误

    1. 空指针访问
    2. 野指针访问
    3. 越界
  5. 静态变量什么时候初始化

    1. 全局静态变量,在程序加载完后就会执行对应的初始化代码
    2. 代码块中的静态变量,在第一次执行到的时候进行初始化,且c++11后是线程安全的
  6. 堆溢出,和栈溢出?解释下堆和栈的区别?

    • 栈的大小是有限制的,递归过多,局部数组过大,越界访问等就会导致栈溢出
    • 堆溢出就是消耗完计算机剩余内存资源,不断申请内存不进行释放
  7. 将数组定义在函数内部和外部有什么区别?分配的内存在哪里?

    • 数组定义到外部,存放在bbs段,程序初始化后固定分配
    • 数组定义到函数内部,存储在函数栈上
  8. malloc和 new的区别,失败会返回什么

    1. new 是 c++ 提供的功能,可以重写,申请内存后可以直接初始化。malloc是c语言要求的基础函数,用于内存申请,由系统实现
    2. new 成功返回队形,malloc成功返回 void* 指针
    3. new 内存分配失败的时候会抛出异常,malloc分配失败的时候会返沪 NULL
  9. free()函数入参是一个void*指针,它是如何知道被指向的大小的?

    • 这个看系统的具体实现,如果是我,我会在指针前边4个字节存储大小
  10. malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

    • malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。
    • 对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
    • 而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
    • brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
    • mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。
    • https://blog.csdn.net/qq_41754573/article/details/104439527
  11. 指针和引用的区别?

    1. 引用其实就是指针的一种书写优化
    2. 区别就是书写上的区别
  12. 野指针是什么?

    1. 指针指向的内存已经被释放,但是仍被访问的时候,就是野指针了
    2. 或者是访问到一个不可访问的内存区域
  13. "引用"与多态的关系?

    int main(int argc, char const* argv[]) {
        Parent* p = new Child(1);
        p->hello(); // 调用子类方法
    Parent a = Child(2); // Child会存在释放
    a.hello(); // 调用的是父类方法
    
    Child c = Child(3);
    Parent&amp; b = c;
    b.hello(); // 调用子类方法
    
    delete p;
    return 0;
    
    }
  14. 在什么时候需要使用"常引用"?

    1. 要求值不被修改
    2. 使用引用而不是拷贝值
  15. 智能指针

    1. shared_ptr

      1. 引用计数,计数为0则删除
      2. auto pointer = std::make_shared<int>(10); 初始化
      3. get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通过use_count() 来查看一个对象的引用计数
    2. unique_ptr

      1. 独占指针
      2. 不可在被拷贝
      3. 可以通过 std::move 转移所有权
    3. weak_ptr

      1. std::weakptr并不是一种独立的智能指针,而是std::sharedptr的一种扩充。一种弱引用,弱引用不会阻止释放
      2. std::weakptr一般是由std::sharedptr创建的,之后两者就指涉到相同的控制块,但std::weak_ptr并不会影响所指涉对象的引用计数。
      3. 用来打破std::shared_ptr引起的环路
    4. auto_ptr 废弃

  16. 智能指针有没有内存泄露的情况, 智能指针的内存泄漏如何解决

    1. 只能指针中存在循环引用
    2. 使用std::week_ptr弱引用打破循环
  17. 右值引用

    1. 有地址的变量就是左值,没有地址的字面值、临时值就是右值。
    2. 右值引用作为函数参数只能传入右值
  18. 右值引用有办法指向左值吗?
    有办法,std::move: int a = 5; int &&ref_a_right = std::move(a);; std::move移动不了什么,唯一的功能是把左值强制转化为右值, 转移所有权

  19. std::move

    1. 在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能
    2. 减少拷贝的方式就是转移所有权
    3. 移动构造函数
      cpp class Array { public: Array(Array&& temp_array) { data_ = temp_array.data_; size_ = temp_array.size_; // 为防止temp_array析构时delete data,提前置空其data_ temp_array.data_ = nullptr; // 原来的放弃数据所有权 } public: int *data_; int size_; };
  20. C++是如何实现多态的

    1. 不带虚函数的多态实现靠编译器直接确定数据位置偏移
    2. 带虚函数的,会有一个虚函数表
  21. 为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数

    1. 非虚函数的调用其实和c语言的函数调用没啥区别,都是固定调用。第一个参数会添加this指针而已。
    2. 虚函数是通过查虚函数表调用最终的绑定的函数指针
    3. 派生类的析构函数在执行完后,会自动执行基类的析构函数,这个是编译器强制规定的
    4. 同样的在创建子类对象时,先调用父类默认的构造函数(编译器自动生成),再调用子类的构造函数。 如果子类的构造函数没有显式地调用父类的构造,则将会调用父类的无参构造函数。

    知道上边几个规则后,就可以理解:
    如果不是虚函数会导致父类类型指针无法清除调用到子类的析构,如下边的例子

    class Parent{
    public:
        ~Parent(){
            println("release parent");
        }
    };
    
    class Child: public Parent{
    public:
        ~Child(){
            println("release child");
        }
    };
    
    Child* c = new Child();
    delete c; // 先release child, 然后 release parent
    
    Parent* p = new Child();
    delete p; // 只 release parent
    

    虚析构函数的情况:

    class Parent{
    public:
        virtual ~Parent(){
            println("release parent");
        }
    };
    
    class Child: public Parent{
    public:
        virtual ~Child(){
            println("release child");
        }
    };
    
    Parent* p = new Child();
    delete p; // 都是 release child, 然后 release parent
    
  22. 为什么构造函数不能是虚函数
    构造函数需要用来创建对象,在实例化时需要明确知道构造函数是那个

  23. 函数指针

    1. 函数的执行其实就是读取指定位置一段机器码,按照指令顺序执行
    2. 函数指针就是指向这串代码的地址位置。
    3. 通过函数指针调用函数与普通函数调用区别不大
    4. 缺点就是无法进行 编译器优化(内联)和处理器优化(分支预测),性能上可能会有一丢丢劣势。但是基本可以忽略
  24. C++中析构函数的作用

    1. 析构函数允许类自动完成类似清理工作
    2. 一般都是回收一些分配出去的内存,或者通知有依赖关系的对象进行某些特殊处理
  25. 重载和覆盖

    1. 重载是同一个类里,函数名相同,参数列表不同的函数是重载关系。当然对于c++, 函数有无被const修饰也同样可以构成重载
    2. 覆盖是子类用相同的方法,对父类的函数进行覆盖的非虚函数。如果是虚函数,则是重写。
  26. 虚函数表具体是怎样实现运行时多态的?

    1. 虚函数会存到一个虚函数表中
    2. 重写的虚函数指针会替换基类函数在表中占据的函数位置的值
    3. 调用的时候就是去查找虚函数表对应的位置的函数的实际函数指针进行调用
  27. 什么是虚函数表,为什么需要虚函数表, 存在虚函数表的对象的内存结构

    1. 虚函数表其实就是函数指针表,用于实现多态

    2. 单继承的虚函数表结构

      class A {
      public:
          virtual void a() {
          }
          virtual void b() {
          }
      };
      
      class B : public A {
      public:
          virtual void a() {
          }
          virtual void c() {
          }
          virtual void d(){
          }
      };
      
      class C: public B{
      public:
          virtual void a(){
          }
          virtual void c(){
          }
      };
      
      int main(int argc, char const* argv[]) {
          C* p = new C();
          delete p;
          return 0;
      }
      

      gdb查看内存结构如下

      (gdb) p *p
      $1 = {<B> = {<A> = {_vptr.A = 0x4e9c80 <vtable for C+16>}, <No data fields>}, <No data fields>}
      
      (gdb) p /a *(void**)0x4e9c80@7
      $2 = {0x41cb00 <C::a()>, 0x41ca70 <A::b()>, 0x41cb10 <C::c()>, 0x41cac0 <B::d()>, 0x0, 
      0x4e5610 <_ZTIN10__cxxabiv115__forced_unwindE>, 0x0}
      
    3. 多继承下虚表的结构

      class A {
      public:
          int a1;
          virtual void a() {
          }
          virtual void b() {
          }
      };
      
      class B {
      public:
          int b1;
          virtual void a() {
          }
          virtual void c() {
          }
          virtual void d(){
          }
      };
      
      class C: public A, public B{
      public:
          int a1;
          int c1;
          virtual void a(){
          }
          virtual void c(){
          }
      };
      
      int main(int argc, char const* argv[]) {
          C* c = new C();
          A* a = c;
          B* b = c;
          delete c;
          return 0;
      }
      

      gdb查看虚表信息

      (gdb) p *c
      $1 = {<A> = {_vptr.A = 0x4e9ca0 <vtable for C+16>, a1 = 0}, <B> = {_vptr.B = 0x4e9cc8 <vtable for C+56>, b1 = 0}, a1 = 0,     
      c1 = 0}
      
      (gdb) p *a
      $2 = {_vptr.A = 0x4e9ca0 <vtable for C+16>, a1 = 0}
      
      (gdb) p *b
      $3 = {_vptr.B = 0x4e9cc8 <vtable for C+56>, b1 = 0}
      
      (gdb) p /a *(void**)0x4e9ca0@10
      $4 = {0x41cb40 <C::a()>, 0x41cac0 <A::b()>, 0x41cb50 <C::c()>, 0xfffffffffffffff0, 0x4e55e0 <_ZTI1C>,
      0x4d8100 <_ZThn16_N1C1aEv>, 0x4d8110 <_ZThn16_N1C1cEv>, 0x41cb10 <B::d()>, 0x0,
      0x4e5620 <_ZTIN10__cxxabiv115__forced_unwindE>}
      
      (gdb) p /a *(void**)0x4e9cc8@10
      $5 = {0x4d8100 <_ZThn16_N1C1aEv>, 0x4d8110 <_ZThn16_N1C1cEv>, 0x41cb10 <B::d()>, 0x0,
      0x4e5620 <_ZTIN10__cxxabiv115__forced_unwindE>, 0x0, 0x0, 0x4da220 <__cxa_pure_virtual>, 0x0, 0x0}
      
      (gdb) p _ZThn16_N1C1aEv
      $7 = {<text variable, no debug info>} 0x4d8100 <non-virtual thunk to C::a()>
      
      (gdb) p _ZThn16_N1C1cEv
      $8 = {<text variable, no debug info>} 0x4d8110 <non-virtual thunk to C::c()>
      
    4. 菱形继承

      class A {
      public:
          int a1;
          virtual void a() {
          }
          virtual void b() {
          }
      };
      
      class B : public A {
      public:
          int b1;
          virtual void a() {
          }
          virtual void c() {
          }
          virtual void d() {
          }
      };
      
      class C : public A {
      public:
          int a1;
          int c1;
          virtual void a() {
          }
          virtual void c() {
          }
      };
      
      class D : public B, public C {
      public:
          int a1;
          int c1;
          virtual void d() {
          }
      };
      
      int main(int argc, char const* argv[]) {
          D* d = new D();
          // A* a = d;    // 报编译错误
          A* a1 = (B*)d;
          A* a2 = (C*)d;
          B* b = d;
          C* c = d;
          delete d;
          return 0;
      }
      
      (gdb) p *d
      $1 = {<B> = {<A> = {_vptr.A = 0x4e9d10 <vtable for D+16>, a1 = 0}, b1 = 0}, <C> = {<A> = {
          _vptr.A = 0x4e9d40 <vtable for D+64>, a1 = 0}, a1 = 0, c1 = 0}, a1 = 0, c1 = 0}
      
      (gdb) p *a1
      $2 = {_vptr.A = 0x4e9d10 <vtable for D+16>, a1 = 0}
      
      (gdb) p *a2
      $3 = {_vptr.A = 0x4e9d40 <vtable for D+64>, a1 = 0}
      
      (gdb) p *b 
      $4 = {<A> = {_vptr.A = 0x4e9d10 <vtable for D+16>, a1 = 0}, b1 = 0}
      
      (gdb) p *c
      $5 = {<A> = {_vptr.A = 0x4e9d40 <vtable for D+64>, a1 = 0}, a1 = 0, c1 = 0}
      
  28. 继承的底层原理
    看上边的例子,就可以基本理解继承的原理,以及虚表的原理

  29. 介绍下内联函数

    1. 如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
    2. 有点像是一种特殊的宏定义,但是由编译器自行确定是否替换。
  30. 虚函数可以是内联的吗

    1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
    2. 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联
    3. 基类指针或引用来调用虚函数,它都不会内联函数。使用类的对象(不是指针或引用)来调用时,可以当做是内联,因为编译器在编译时确切知道对象是哪个类的。
    // 此处的虚函数who(),是通过类的具体对象来调用的,编译期间就能确定了,所以它可以是内联的。
    Base b;
    b.who();
    
    // 此处的虚函数是通过指针调用的,需要在运行时期间才能确定,所以不能为内联。
    Base* ptr = new Derived();
    
  31. 菱形继承的数据存储结构

    class A{
    public:
        int a1;
        int a2;
    };
    
    class B: public A{
    public:
        int b1;
    };
    
    class C: public A{
    public:
        int c1;
    };
    
    class D: public B, public C{
    public:
        int d1;
    };
    
    int main(int argc, char const* argv[]) {
        D d;
        return 0;
    }
    
    (gdb) p d
    $1 = {<B> = {<A> = {a1 = 8, a2 = 0}, b1 = 52}, <C> = {<A> = {a1 = 0, a2 = 11801856}, c1 = 0}, d1 = 16}
    
    (gdb) p sizeof(d) 
    $2 = 28
    

    相当于A类的数据会有两份

  32. 为什么GCC下中的虚函数表存在两个虚析构函数
    https://www.zhihu.com/question/29257977 : 一个叫complete object destructor, 另一个叫deleting destructor,区别在于前者只执行析构函数不执行delete(),后面的在析构之后执行deleting操作。

  33. 拷贝,赋值函数

    class data {
    private:
        char *m_str;
    
    public:
        // 构造函数
        data(const char *str) {
            this->m_str = new char[strlen(str)];
        }
        // 拷贝
        data(const data &str) {
            this->m_str = new char[strlen(str.m_str)];
            cout << "copy" << endl;
        }
        // 赋值
        data &operator=(const data &str) {
            cout << "=" << endl;
            delete m_str;
            this->m_str = new char[strlen(str.m_str)];
            return *this;
        }
        // 析构
        ~data() {
            cout << "delete" << endl;
            delete this->m_str;
        }
    };
    
    data test() {
        cout << "== start test ==" << endl;
        data a = data("a");
        data b = data("b");
        cout << "==1==" << endl;
        data c = a; // copy
        cout << "==2==" << endl;
        b = a; // =
        cout << "== end test ==" << endl;
        return b; // RVO优化, 不会调用拷贝构造函数, 将RVO优化关闭,可以对g++增加选项-fno-elide-constructors
    }
    

    输出

    == start test==
    ==1==
    copy
    ==2==
    =
    == end test==
    delete
    delete
    == end main ==
    delete
    

进程和线程

  1. 单核机器上写多线程程序,是否需要考虑加锁,为什么?

    1. 在单核机器上写多线程程序,仍然需要线程锁
    2. 线程锁通常用来实现线程的同步和通信。在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
  2. 线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的

    1. 程在切换的过程中需要保存当前线程Id、线程状态、堆栈、寄存器状态等信息
    2. SP:堆栈指针,指向当前栈的栈顶地址
    3. PC:程序计数器,存储下一条将要执行的指令
    4. EAX:累加寄存器,用于加法乘法的缺省寄存器
    5. TODO 重新去了解一下寄存器相关内容
  3. 多线程和多进程的不同

    1. 多线程占相比于多进程占用内存少、CPU利用率高,创建销毁,切换都比较简单,速度很快
    2. 进程相比于多线程共享数据复杂,需要将进程间通信

    场景

    1. 对线程适合业务耦合大的, 多进程适合业务耦合小的
    2. 多进程适合多核,多机分布,多线程适合多核分布
  4. A是B的父进程,A挂了,B的父进程是谁?

    1. 进程A是进程B的父进程,进程A挂了(exit() 退出了),而它的子进程还在运行,那么这些子进程就会成为孤儿进程,被进程号为 1 的 init 进程收养(领养),并由 init 进程对他们完成状态收集过程,防止这些子进程变成僵尸进程,资源得不到释放。
    2. 僵尸进程:指完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于"终止状态"的进程。
  5. 进程状态转换图,动态就绪,静态就绪,动态阻塞,静态阻塞

  6. 项目中单进程模型,怎样做到的高并发

    1. IO多路复用
    2. 内存操作,基于缓存
  7. linux进程通信的方式

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

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

    3. 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

    4. 信号量( semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

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

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

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

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

  8. 进程通信方式,哪种最快
    共享内存是最快的 IPC 方式

  9. 多线程,线程同步的几种方式

    • 互斥锁: 提供了以排他方式防止数据结构被并发修改的方法。
    • 读写锁: 允许多个线程同时读共享数据,而对写操作是互斥的。
    • 条件变量: 可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
    • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
    • 信号机制(Signal): 类似进程间的信号处理
  10. 互斥锁(mutex)机制,以及互斥锁和读写锁的区别

    • 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
    • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 写锁会阻塞其它读写锁
    • 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
    • RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操
  11. 产生死锁的原因
    死锁是指两个或以上进程因竞争临界资源而造成的一种僵局,即一个进程等待一个已经被占用且永不释放的资源。 若无外力作用,这些进程都无法向前推进。 产生死锁的根本原因是系统能够提供的资源个数比要求该资源的进程数要少。

  12. 死锁发生的条件以及如何解决死锁

    1. 条件
      • 互斥:一个资源每次只能被一个线程使用。
      • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
      • 不剥夺:进程已获得的资源,在未使用完之前,不能强行剥夺。
      • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。
    2. 解决
      • 死锁预防:
        1. 预先静态分配法:破坏了“不可剥夺条件”
        2. 资源有序分配法:破坏了“环路条件”
      • 死锁避免:设法破坏4个必要条件之一,严格防止死锁的发生
        1.银行家算法:若发现分配资源后进入不安全状态,则不予分配;若扔处于安全状态,则实施分配。
      • 死锁检测:
        1.允许死锁产生,定时地运行一个死锁检测程序,判断系统是否发生死锁。/ 超时检测
      • 死锁解除:
        1.资源剥夺法
        1.撤销进程法
  13. 数据库中是否会出现死锁?

    1. 数据库访问中同样可能出现死锁
    2. 不能完全避免死锁,但可以使死锁的数量减至最少
    3. 预防:
      • 按同一顺序访问对象
      • 使用低隔离级别
      • 避免事务中的用户交互
      • 使用乐观锁进行控制。乐观锁大多是基于数据版本(Version)记录机制实现。
  14. 如果数据库中的确发生了死锁,应该怎么解决?

    1. MySQL 提供了一个系统参数 innodbprintall_deadlocks 专门用于记录死锁日志,当发生死锁时,死锁日志会记录到 MySQL 的错误日志文件中
    2. 拿到了死锁日志
    3. 日志中列出了死锁发生的时间,以及导致死锁的事务信息(只显示两个事务,如果由多个事务导致的死锁也只显示两个),并显示出每个事务正在执行的 SQL 语句、等待的锁以及持有的锁信息等
    4. 分析是那个服务导致死锁
    5. kill掉对应服务
  15. c++ 线程锁

    1. 互斥锁 std::mutex

      #include <mutex>
      
      std::mutex mtx;
      
      mtx.lock();
      mtx.unlock();
      
      std::lock_guard<std::mutex> guard(mtx);
      
      std::lock_guard<std::mutex> guard(mtx);
      

      std::lock_guard其实就是简单的RAII封装

      std::uniquelock的功能相比std::lockguard来说,就强大多了,是std::lockguard的功能超集, 封装了各种加锁操作,阻塞的,非阻塞的,还可以结合条件变量一起使用,基本上对锁的各种操作都封装了,当然了,功能丰富是有代价的,那就是性能和内存开销都比std::lockguard大得多,所以,需要有选择地使用。

      std::uniquelock也会在析构的时候自动解锁,所以说,是std::lockguard的功能超集。

    2. 条件锁/条件变量

      std::mutex mut;
      std::condition_variable data_cond;
      
      std::unique_lock<std::mutex> lk(mut);
      // data_cond.wait(lk,[]{return !data_queue.empty();});
      data_cond.wait(lk);
      ...
      lk.unlock();
      
      std::unique_lock<std::mutex> lk(mut);
      data_cond.notify_one();
      lk.unlock();
      
    3. 自旋锁

      //使用std::atomic_flag的自旋锁互斥实现
      class spinlock_mutex {
          std::atomic_flag flag = ATOMIC_FLAG_INIT;
      public:
          void lock() {
              while(flag.test_and_set(std::memory_order_acquire));
          }
          void unlock() {
              flag.clear(std::memory_order_release);
          }
      }
      
    4. 读写锁

      std::shared_mutex share_mtx;
      
      std::shared_lock<std::shared_mutex> lock(share_mtx); // 读数据时这样加锁
      
      std::unique_lock<std::shared_mutex> lock(share_mtx); // 改数据时这样加锁
      
    5. 递归锁 recursive_mutex (可重入锁)

      std::recursivemutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursivemutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

      #include <mutex>
      
      class X {
          std::recursive_mutex m;
          std::string shared;
      public:
          void fun1() {
              std::lock_guard<std::recursive_mutex> lk(m);
              shared = "fun1";
              std::cout << "in fun1, shared variable is now " << shared << '\n';
          }
          void fun2() {
              std::lock_guard<std::recursive_mutex> lk(m);
              shared = "fun2";
              std::cout << "in fun2, shared variable is now " << shared << '\n';
              fun1(); // 递归锁在此处变得有用
              std::cout << "back in fun2, shared variable is " << shared << '\n';
          };
      

      在java中锁通过 AbstractQueueSynchronizer, 提供的一套用于实现基于 FIFO 等待队列的阻塞锁和和相关同步器的同步框架实现。其基于 CAS(compare and swap 比较并交换,实现的一种乐观锁) 。当一个线程拿到锁之后,整个线程都拥有该锁,所以java都是可重入锁。

  16. 线程池的优点:省时、省资源、更好管理线程

    1. 减少资源的消耗:较少线程创建和销毁造成的消耗
    2. 提高响应速度:当任务到达的时候,就能够立刻执行,减少了线程创建和销毁的时间
    3. 能够更好地管理线程
  17. 孤儿进程、僵尸进程

    1. 孤儿进程: 如果父进程先退出,子进程还没退出那么子进程将被 托孤给init进程,这是子进程的父进程就是init进程(1号进程). init进程没有父进程.
    2. 僵尸进程: 进程终止后进入僵死状态(zombie),等待告知父进程自己终止,后才能完全消失.但是如果一个进程已经终止了,但是其父进程还没有获取其状态,那么这个进程就称之为僵尸进程.僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息.一旦父进程得到想要的信息,僵尸进程就会结束.
  18. 进程间通讯

    • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
    • 有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
    • 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
    • 信号量( semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    • 消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    • 信号 ( sinal ) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
    • 共享内存( sharedmemory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
    • 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  19. 线程通讯

    • 互斥锁: 提供了以排他方式防止数据结构被并发修改的方法。
    • 读写锁: 允许多个线程同时读共享数据,而对写操作是互斥的。
    • 条件变量: 可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
    • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
    • 信号机制(Signal): 类似进程间的信号处理
  20. 产生死锁的四个必要条件

    1. 互斥条件: 一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所使用。此时如果有其他进程请求该资源,则请求进程只能等待。
    2. 请求与保持条件: 进程中已经保持了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时请求进程被阻塞,但对自己已经获得资源保持不放。
    3. 不可剥夺条件: 进程未使用完的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。
    4. 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系。在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请。

    注意:这四个条件是死锁的必然条件,只要系统发生死锁,这些条件必然成立。只要有上述条件有一条不满足,就不会发生死锁。

  21. 死锁的预防

    我们可以通过破坏产生死锁的四个必要条件来预防死锁,由于资源互斥是固有特性无法改变的。

    1. 破坏“请求与保持”条件

      方法一:静态分配,每个进程在开始执行时就申请他所需要的全部资源。
      方法二:动态分配,每个进程在申请所需要的资源时他本身不占用系统资源。

    2. 破坏“不可剥夺”条件: 一个进程不可获得其所需要的全部资源便处于等待状态,等待期间他占用的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。

    3. 破坏“循环等待”条件: 采用资源有序分配的基本思想。将系统中的资源顺序进行编号,将紧缺的、稀少的资源采用较大的编号,申请资源时必须按照编号的顺序执行,一个进程只有较小编号的进程才能申请较大编号的进程

  22. 递归锁 recursive_mutex

    recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。

    recursive_mutex 提供排他性递归所有权语义:

    • 调用方线程在从它成功调用 lock 或 trylock 开始的时期里占有 recursivemutex 。此时期间,线程可以进行对 lock 或 try_lock 的附加调用。所有权的时期在线程调用 unlock 匹配次数时结束。
    • 线程占有 recursivemutex 时,若其他所有线程试图要求 recursivemutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。
    • 可锁定 recursivemutex 次数的最大值是未指定的,但抵达该数后,对 lock 的调用将抛出 std::systemerror 而对 try_lock 的调用将返回 false 。

    若 recursivemutex 在仍为某线程占有时被销毁,则程序行为未定义。 recursivemutex 类满足互斥 (Mutex) 和标准布局类型 (StandardLayoutType) 的所有要求。

STL

  1. STL有什么基本组成:六大组件

    1. 容器:vector statck queue map set 等
    2. 算法:find sort copy reverse等
    3. 迭代器
    4. 仿函数: 在实现阶段,通常编译器都会把lambda函数转化为一个仿函数对象
    5. 适配器:adapter
    6. 空间配置器:allocator
  2. STL迭代器删除元素

    1. 对于序列式容器 vector deque list 删除当前 iterator 会使后边的都失效,同样插入也会导致后边的失效。对于vector, 如果删除或者插入元素导致空间重新分配,则所有的迭代器都会失效
    2. 对于关联容器 map set 删除当前知会使当前失效,插入也不会有影响
  3. STL里resize和reserve的区别

    1. reserve 是预留空间
    2. resize 是改变大小,并会创建对象占据位置
  4. STL的allocator

    1. allocator 就是内存管理器
    2. 可以自定义实现,传给容器 vector<int, DIYAllocator<int>> list;
    3. 为了能自主掌控内存申请与释放,因为默认的内存管理器会有cache导致内存不会及时释放,可能会出现进程一直处于高内存状态
  5. STL中迭代器的作用,有指针为何还要迭代器

    1. 保证遍历方式一样
    2. for 语法糖的实现
  6. vector和list的区别,应用,越详细越好

    1. vector 内部使用数组来实现,空间不够则2倍增加, 然后复制过去, 注意如果发生空间重新分配,则原迭代器全部失效
    2. list 底层是双向链表,对于插入删除更加快捷,随机访问性能较差
    3. 需要的列表,更多的是读取,则选择 vector. 如果长度已知则选择vector. 不定长且更偏向于增加删除则选择list
  7. STL中map与unordered_map

    1. map
      • 有序容器底层通过红黑树实现
    2. unordered_map
      • 不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用“链地址法”(又称“开链法”)解决数据存储位置发生冲突的哈希表
      • 对于空的 umap 容器,初始状态下会分配 8 个桶,并且默认最大负载因子为 1.0。超过负载时就会发生扩容,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。
  8. map和set有什么区别,分别又是怎么实现的?

    • map是存储键值对
    • set 是无序列表,值唯一
    • map和 set 底层都是 红黑树实现
  9. hash表的实现,包括STL中的哈希桶长度常数

    • 对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数法等。
    • 对于处理哈希冲突来说,最常用的处理冲突的方法有开放定址法、再哈希法、链地址法、建立公共溢出区等方法。SGL版本使用链地址法,使用一个链表保持相同散列值的元素。虽然链地址法并不要求哈希桶长度必须为质数,但SGI STL仍然以质数来设计哈希桶长度,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
  10. 哈希表的桶个数为什么是质数,合数有何不妥?

    • 减少hash冲突
    • 和数取余冲突概率大。例如存储 2 4 6 8 10. 如果为6则hash结果是 2 4 0 2 4. 如果是7 则为 2 4 6 1 3
  11. hash表如何rehash,以及怎么处理其中保存的资源

    • hash表负载达到设定的值,负载因子的时候就会进行rehash
    • 开辟一个原来桶数组的两倍空间,称为新桶数组,然后把原来的桶数组中元素全部重新哈希到新的桶数组中。
  12. 解决hash冲突的方法

    • 开放定址法: 再探散列
    • 再哈希法: 设计多个哈希函数,冲突了就换一个函数
    • 链地址法
    • 建立公共溢出区
  13. C++ STL 的内存优化

    1. 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
    2. 如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。
      • a. 如果链表不为空,返回第一个node,链表头改为第二个node。
      • b. 如果链表为空,使用blockAlloc请求分配node。
        • 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。
        • 如果内存池只有一个node的空间,直接返回给用户。
        • 若果如果连一个node都没有,再次向操作系统请求分配内存。
          • 分配成功,再次进行b过程。
          • 分配失败,循环各个自由链表,寻找空间。
    3. 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。小于128字节按照其大小找到合适的自由链表,并将其插入。
  14. 为什么有 list 还有 stack queue 等容器的提供

    1. 限制接口,功能明确,实现上复杂度降低
    2. list不支持随机访问,可在两端进行push、pop; deque双端队列,随机访问方便,即支持[], 可在两端进行push、pop
  15. map vector删除数据

    // vector 不能使用这种非方式
    for(auto it=mymap.begin(); it!=mymap.end();) {
        if (it->first == target) {
            mymap.erase(it++); //here is the key
        } else {
            it++;
        }
    }
    

    或者

    // vector 可以使用这种非方式
    for(auto it=mymap.begin(); it!=mymap.end();) {
        if (it->first == target) {
            it = mymap.erase(it);
        } else {
            it++;
        }
    }
    
  16. STL中排序算法sort的实现是什么

    • STL中的sort(),在数据量大时,采用快排quicksort,分段递归;一旦分段后的数量小于某个门限值,改用插入排序Insertion sort,避免quicksort深度递归带来的过大的额外负担,如果递归层次过深,还会改用heapsort(堆排序)。
    • 深度递归的危害: 可能造成栈溢出。因为每次递归的函数调用都会在栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层次太多时,可能会超出栈容量(栈容量似乎是8M,可设置)。

OSI七层

  1. OSI七层知道吗,简单介绍一下
    1. 物理层
    2. 链路层:交换机,MAC地址
    3. 网络层:IP ARP
    4. 运输层:TCP UDP
    5. 会话层:SMTP, DNS
    6. 表示层:Telnet
    7. 应用层:HTTP FTP等

TCP

  1. tcp 为什么要三次握手,两次不行吗?为什么?。 为什么要四次挥手来进行关闭?详细介绍小TCP协议

    • 三次握手是为了建立可靠的数据传输通道,四次挥手则是为了保证等数据完成的被接收完再关闭连接
    • 三次握手
      • 客户端 SYN -> 服务端
      • 服务端 SYN ACK -> 客户端
      • 客户端 ACK -> 服务端
    • 四次挥手
      • 主动关闭方 FIN -> 被动关闭方
      • 被动关闭方 ACK -> 主动关闭方
      • 被动关闭方 FIN -> 主动关闭方
      • 主动关闭方 ACK -> 主动关闭方
  2. TCP 如果解决粘包问题

    • 固定包长
    • 添加包头,包头包含长度信息
    • 指定字符串作为结束,一般使用换行符
  3. TCP闪断

    • 断网时有数据传输:断网时如果有数据发送,由于收不到 ACK,所以会重试,但并不会无限重试下去,达到一定的重发次数之后,如果仍然没有任何确认应答返回,就会判断为网络或者对端主机发生了异常,强制关闭连接。此时的关闭是直接关闭,而没有挥手

    • 断网时没有数据传输: 还得看 TCP 连接的 KeepAlive 是否打开。

      • client 开启 KeepAlive 连接 server 后,什么数据都不发送,把server 的网断掉,可以看到 KeepAlive 心跳包,一段时间后连接被置为 CLOSED 状态
      • 关闭 KeepAlive 后,如果没有数据传输,连接永远不会断开
    • 断电/断网后 server 重启再恢复:如果 server 重启后,client 还是不发数据,那这条连接看起来还是可用的,因为他们根本不知道对方是个什么情况,但如果此时 client 发送一点数据给 server,会发现 server 会发送一个 RST 给client,然后 client 就断开连接了

    • 通信双方A/B非直连链路断:断开时间很短暂,小于TCP连接超时时间: TCP连接丝毫不受影响

    • 本地物理连接断开,比如网线拔出,会导致本机IP释放,则协议栈会释放所有连接

  4. TCP 如何保证可靠传输

    1. ARQ协议
    2. 滑动窗口和流量控制
    3. 拥塞控制

    详细说:

    1. 应用数据被分割成 TCP 认为最适合发送的数据块。
    2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
    3. 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如1. 果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
      TCP 的接收端会丢弃重复的数据。
    4. 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
    5. 拥塞控制: 当网络拥塞时,减少数据的发送。
    6. ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
    7. 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
  5. ARQ协议

    自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。

    1. 停止等待ARQ协议

      • 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;
      • 在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;

      优点: 简单
      缺点: 信道利用率低,等待时间长

      1) 无差错情况:
      发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

      2) 出现差错情况(超时重传):
      停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。

      3) 确认丢失和确认迟到

      • 确认丢失 :确认消息在传输过程丢失。当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:1. 丢弃这个重复的M1消息,不向上层交付。 2. 向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。
      • 确认迟到 :确认消息在传输过程中迟到。A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:1. A收到重复的确认后,直接丢弃。2. B收到重复的M1后,也直接丢弃重复的M1。
    2. 连续ARQ协议

      连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

      优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
      缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

  6. 滑动窗口和流量控制

    TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

  7. 拥塞控制

    在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

    为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

    TCP的拥塞控制采用了四种算法,即 慢开始拥塞避免快重传快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

    • 慢开始: 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。
    • 拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.
    • 快重传与快恢复:
      在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。  当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。
  8. TCP: SYN ACK FIN RST PSH URG 详解

    TCP的三次握手是怎么进行的了:发送端发送一个SYN=1,ACK=0标志的数据包给接收端,请求进行连接, 这是第一次握手;接收端收到请求并且允许连接的话,就会发送一个SYN=1,ACK=1标志的数据包给发送端,告诉它,可以通讯了,并且让发送端发送一个 确认数据包,这是第二次握手;最后,发送端发送一个SYN=0,ACK=1的数据包给接收端,告诉它连接已被确认,这就是第三次握手。之后,一个TCP连 接建立,开始通讯。

    1. SYN:同步标志

      同步序列编号(Synchronize Sequence Numbers)栏有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户 端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字 节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。

    2. ACK:确认标志

      确认编号(Acknowledgement Number)栏有效。大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号(w+1,Figure-1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。

    3. RST:复位标志

      复位标志有效。用于复位相应的TCP连接。

    4. URG:紧急标志

      紧急(The urgent pointer) 标志有效。紧急标志置位,

    5. PSH:推标志

      该标志置位时,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。

    6. FIN:结束标志

      带有该标志置位的数据包用来结束一个TCP回话,但对应端口仍处于开放状态,准备接收后续数据。

  9. TCP报文到达确认(ACK)机制

    原文: http://blog.csdn.net/wjtxt/article/details/6606022

    TCP数据包中的序列号(Sequence Number)不是以报文段来进行编号的,而是将连接生存周期内传输的所有数据当作一个字节流,序列号就是整个字节 流中每个字节的编号。一个TCP数据包中包含多个字节流的数据(即数据段),而且每个TCP数据包中的数据大小不一定相同。在建立TCP连接的三次握手 过程中,通信双方各自已确定了初始的序号x和y,TCP每次传送的报文段中的序号字段值表示所要传送本报文中的第一个字节的序号。

    TCP的报文到达确认(ACK),是对接收到的数据的最高序列号的确认,并向发送端返回一个下次接收时期望的TCP数据包的序列号(Ack Number)。例如, 主机A发送的当前数据序号是400,数据长度是100,则接收端收到后会返回一个确认号是501的确认号给主机A。

    TCP提供的确认机制,可以在通信过程中可以不对每一个TCP数据包发出单独的确认包(Delayed ACK机制),而是在传送数据时,顺便把确认信息传出, 这样可以大大提高网络的利用率和传输效率。同时,TCP的确认机制,也可以一次确认多个数据报,例如,接收方收到了201,301,401的数据报,则只 需要对401的数据包进行确认即可,对401的数据包的确认也意味着401之前的所有数据包都已经确认,这样也可以提高系统的效率。

    若发送方在规定时间内没有收到接收方的确认信息,就要将未被确认的数据包重新发送。接收方如果收到一个有差错的报文,则丢弃此报文,并不向发送方 发送确认信息。因此,TCP报文的重传机制是由设置的超时定时器来决定的,在定时的时间内没有收到确认信息,则进行重传。这个定时的时间值的设定非 常重要,太大会使包重传的延时比较大,太小则可能没有来得及收到对方的确认包发送方就再次重传,会使网络陷入无休止的重传过程中。接收方如果收到 了重复的报文,将会丢弃重复的报文,但是必须发回确认信息,否则对方会再次发送。

    TCP协议应当保证数据报按序到达接收方。如果接收方收到的数据报文没有错误,只是未按序号,这种现象如何处理呢?TCP协议本身没有规定,而是由TCP 协议的实现者自己去确定。通常有两种方法进行处理:一是对没有按序号到达的报文直接丢弃,二是将未按序号到达的数据包先放于缓冲区内,等待它前面 的序号包到达后,再将它交给应用进程。后一种方法将会提高系统的效率。例如,发送方连续发送了每个报文中100个字节的TCP数据报,其序号分别是1, 101,201,…,701。假如其它7个数据报都收到了,而201这个数据报没有收到,则接收端应当对1和101这两个数据报进行确认,并将数据递交给相关的应用 进程,301至701这5个数据报则应当放于缓冲区,等到201这个数据报到达后,然后按序将201至701这些数据报递交给相关应用进程,并对701数据报进行 确认,确保了应用进程级的TCP数据的按序到达。

  10. ICMP (网间控制消息协议Internet Control Message Protocol)

    如同名字一样, ICMP用来在主机/路由器之间传递控制信息的协议。 ICMP包可以包含诊断信息(ping, traceroute - 注意目前unix系统中的traceroute用UDP包而不是ICMP),错误信息(网络/主机/端口 不可达 network/host/port unreachable), 信息(时间戳timestamp, 地址掩码address mask request, etc.),或控制信息 (source quench, redirect, etc.) 。

  11. 网络相关知识概念了解

    1. 运输层提供应用进程之间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节(如网络拓补,所采用的路由选择协议等),它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。
    2. 网络层为主机提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。
    3. 运输层的两个重要协议是用户数据报协议UDP和传输控制协议TCP。按照OSI的术语,两个对等运输实体在通信时传送的数据单位叫做运输协议数据单元TPDU(Transport Protocol Data Unit)。但在TCP/IP体系中,则根据所使用的协议是TCP或UDP,分别称之为TCP报文段或UDP用户数据报。
    4. UDP在传送数据之前不需要先建立连接,远地主机在收到UDP报文后,不需要给出任何确认。虽然UDP不提供可靠交付,但在某些情况下UDP确是一种最有效的工作方式。 TCP提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP不提供广播或多播服务。由于TCP要提供可靠的,面向连接的传输服务,这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。
    5. 硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层各种协议进程与运输实体进行层间交互的一种地址。UDP和TCP的首部格式中都有源端口和目的端口这两个重要字段。当运输层收到IP层交上来的运输层报文时,就能够 根据其首部中的目的端口号把数据交付应用层的目的应用层。(两个进程之间进行通信不光要知道对方IP地址而且要知道对方的端口号(为了找到对方计算机中的应用进程))
    6. 运输层用一个16位端口号标志一个端口。端口号只有本地意义,它只是为了标志计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。协议端口号简称端口。虽然通信的终点是应用进程,但只要把所发送的报文交到目的主机的某个合适端口,剩下的工作(最后交付目的进程)就由TCP和UDP来完成。
    7. 运输层的端口号分为服务器端使用的端口号(0~1023指派给熟知端口,1024~49151是登记端口号)和客户端暂时使用的端口号(49152~65535)
    8. UDP的主要特点是①无连接②尽最大努力交付③面向报文④无拥塞控制⑤支持一对一,一对多,多对一和多对多的交互通信⑥首部开销小(只有四个字段:源端口,目的端口,长度和检验和)
    9. TCP的主要特点是①面向连接②每一条TCP连接只能是一对一的③提供可靠交付④提供全双工通信⑤面向字节流
    10. TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP地址:端口号)来表示。每一条TCP连接唯一被通信两端的两个端点所确定。
    11. 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
    12. 为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。
    13. 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续ARQ协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。
    14. TCP报文段的前20个字节是固定的,后面有4n字节是根据需要增加的选项。因此,TCP首部的最小长度是20字节。
    15. TCP使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口的前沿通常是不断向前移动的。一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。
    16. 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
    17. 为了进行拥塞控制,TCP发送方要维持一个拥塞窗口cwnd的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。
    18. TCP的拥塞控制采用了四种算法,即慢开始,拥塞避免,快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理AQM),以减少网络拥塞的发生。
    19. 运输连接的三个阶段,即:连接建立,数据传送和连接释放。
    20. 主动发起TCP连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫做服务器。TCP连接采用三报文握手机制。服务器要确认用户的连接请求,然后客户要对服务器的确认进行确认。
    21. TCP的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了TCP连接

EPOLL

  1. select、poll、epoll之间的区别

    1. select poll 都是轮询,复杂度O(n)。epoll是事件触发复杂度O(1)
    2. select 可监听的fd列表使用数组存储有大小限制,,可以修改。poll因为是链表,所以没有大小限制。
    3. epoll 内部是红黑树+双向链表实现,然后有内核监听到fd变化进行事件通知
  2. epoll原理

    1. epoll 每个event存储在红黑树中,同时有双向链表指针的存储
    2. 双线链表指针是为了获取event list时能快速复制到数组中。使用双向链表而不是单向是方便删除
  3. epoll的两种模式

    1. LT(level triggered,水平触发模式)是默认的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
    2. ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。
  4. epoll为什么要有EPOLLET触发模式?

    1. 如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epollwait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epollwait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符
  5. epoll的优点

    1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
    2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
    3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

HTTP

  1. http 返回码知道哪些,简单介绍一下

    • 1** 信息,服务器收到请求,需要请求者继续执行操作
    • 2** 成功,操作被成功接收并处理:200 OK
    • 3** 重定向,需要进一步的操作以完成请求: 301客户端跳转,302服务器内部跳转
    • 4** 客户端错误,请求包含语法错误或无法完成请求:403 Forbidden,404 Not Found
    • 5** 服务器错误,服务器在处理请求的过程中发生了错误:500 Internal Server Error,502 Bad Gateway,503 Service Unavailable
  2. forward 和 redirect 的区别

    • redirect(重定向):服务端发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL.
    • forward(转发):服务端完成内部跳转,客户端不受影响
  3. Http header中用于控制缓存的字段

    • Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT
    • Cache-Control 告诉所有的缓存机制是否可以缓存及哪种类型 Cache-Control: no-cache 【no-cache, must-revalidate, max-age=600(文件被用户访问后的存活时间)】
    • Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT
    • ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d”

其他

  1. ipv4与ipv6的区别

    • IPv4的地址位数为32位,也就是最多有2的32次方的电脑可以联到Internet上。
    • IPv6采用128位地址长度,几乎可以不受限制地提供地址
    • IPv6还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用
    • IPv6与IPv4相比具有的优点
      1. 更大的地址空间
      2. 更小的路由表:IPv6的地址分配一开始就遵循聚类(Aggregation)的原则,这使得路由器能在路由表中用一条记录(Entry)表示一片子网,大大减小了路由器中路由表的长度,提高了路由器转发数据包的速度。
      3. 增强的组播支持以及对流的支持
      4. 加入了对自动配置的支持:这是对DHCP协议的改进和扩展,使得网络(尤其是局域网)的管理更加方便和快捷
      5. 更高的安全性: 在使用IPv6网络中用户可以对网络层的数据进行加密并对IP报文进行校验
  2. 局域网可用IP网段:

    • A类:10段,后三位自由分配,也就是 10.0.0.0 - 10.255.255.255;

    • B类:172.16段,后两位自由分配,也就是 172.16.0.0 - 172.31.255.255;

    • C类:192.168段,后两位自由分配,也就是 192.168.0.0 - 192.168.255.255;

    • 特殊的

    • 127.0.0.1为本地回路测试地址

    • 255.255.255.255代表广播地址

    • 0.0.0.0代表任何网络

GCC、GDB

  1. gcc编译相关

    # 预处理
    gcc -E hello.c -o hello.i
    
    # 编译
    gcc -S hello.i -o hello.s
    
    # 生成目标代码 *.o ;有两种方式:使用 gcc 直接从源代码生成目标代码 gcc -c *.s -o *.o 以及使用汇编器从汇编代码生成目标代码 as *.s -o *.o
    gcc -c hello.s -o hello.o
    
    # 或者
    as hello.s -o hello.o
    # 也可以直接使用as *.s, 将执行汇编、链接过程生成可执行文件a.out, 可以像上面使用-o 选项指定输出文件的格式。
    
    # 生成可执行文件;可以生成的可执行文件格式有: a.out/*/,当然可能还有其它格式。
    gcc hello.o -o hello        #生成可执行文件 hello
    
    #生成静态库
    ar -crv libhello.a hello.o
    
     gcc -MM main.cpp #获取文件导入的文件
    
     #结果
     main.o: main.cpp main.h base.h
    
  2. gdb调试

    bash:
    ps -aux // 获取进程信息
    gdb attach <PID> // 进入进程
    
    gdb: 
    info inferiors    // 查看进程
    info threads      // 查看线程
    info b            // 查看断点
    bt                // 查看线程栈结构
    thread <n>        // 切换到线程
    break 行号/函数名  // 设置断点