C++ 面经以及答案

目录

  1. 1. 第一部分、 c++ 基础
    1. 1.1. 1、C和C++的区别
    2. 1.2. 2、C++中指针和引用的区别
    3. 1.3. 3、结构体struct和共同体union(联合)的区别
    4. 1.4. 4、struct 和 class 的区别?
    5. 1.5. 5、#define和const的区别
    6. 1.6. 6、重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别
    7. 1.7. 7、new 和 delete 是如何实现的,与 malloc 和 free有什么异同?
    8. 1.8. 8、delete和delete[]的区别
    9. 1.9. 9、const知道吗?解释一下其作用
    10. 1.10. 10、关键字static的作用
    11. 1.11. 11、堆和栈的区别
    12. 1.12. 12、#include<file.h> #include “file.h” 的区别
    13. 1.13. 13、什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?
    14. 1.14. 14、定义和声明的区别
    15. 1.15. 15、C++文件编译与执行的四个阶段
    16. 1.16. 16、C++的内存管理
    17. 1.17. 17、 C++的四种强制转换
    18. 1.18. 18、extern“C”作用
    19. 1.19. 19、typdef和define区别
    20. 1.20. 20、引用作为函数参数以及返回值的好处
    21. 1.21. 21、什么是野指针
    22. 1.22. 22、C++中内存泄漏的几种情况
    23. 1.23. 23、栈溢出的原因以及解决方法
    24. 1.24. 24、左值和右值
    25. 1.25. 25、左值引用与右值引用
    26. 1.26. 26、头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?
    27. 1.27. 27、指针数组和数组指针的区别
    28. 1.28. 28、C++是不是类型安全的?
    29. 1.29. 29、main函数执行之前,还会执行什么代码?
    30. 1.30. 30、全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
    31. 1.31. 31、关于sizeof小结的。
    32. 1.32. 32、sizeof 和 strlen 的区别?
    33. 1.33. 33、说说内联函数
    34. 1.34. 34、C/C++的存储期
    35. 1.35. 35、流操作符重载为什么返回引用
    36. 1.36. 36、全局变量和局部变量有什么区别?实怎么实现的?操作系统和编译器是怎么知道的?
  2. 2. 第二部分、 c++ 类相关
    1. 2.1. 1、什么是面向对象(OOP)?面向对象的意义?
    2. 2.2. 2、解释下封装、继承和多态?
    3. 2.3. 3、构造函数和析构函数的执行顺序?
    4. 2.4. 4、虚函数是怎么实现的
    5. 2.5. 5、 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?
    6. 2.6. 6、细看拷贝构造函数
    7. 2.7. 7、静态绑定和动态绑定的介绍
    8. 2.8. 8、深拷贝和浅拷贝的区别
    9. 2.9. 9、 什么情况下会调用拷贝构造函数(三种情况)
    10. 2.10. 10、类对象的大小受哪些因素影响?
    11. 2.11. 11、拷贝构造函数和赋值运算符的理解
    12. 2.12. 12、C++空类默认有哪些成员函数?
    13. 2.13. 13、如果虚函数是有效的,那为什么不把所有函数设为虚函数?
    14. 2.14. 14、构造函数和虚构函数可以是内联函数?
    15. 2.15. 15、虚函数声明为inline
    16. 2.16. 16、C++的空类有哪些成员函数
    17. 2.17. 17、C++中的五种构造函数
  3. 3. 第三部分、 c++11/c++14/c++17
    1. 3.1. 1、C++11 中有哪些智能指针?shared_ptr 的引用计数是如何实现的?unique_ptr 的unique 是如何实现的?make_shared 和 make_unique 的作用?智能指针使用注意事项?
    2. 3.2. 2、智能指针weak_ptr 能够破坏环型引用的原理(引用计数的原理)
    3. 3.3. 3、C++ 的闭包
    4. 3.4. 4、lambda 表达式、怎么捕获外部变量
    5. 3.5. 5、C++ 的垃圾回收机制
    6. 3.6. 6、vector的clear()的时间复杂度是多少?
    7. 3.7. 7、C++11为什么引入enum class?
    8. 3.8. 8、std::thread使用lambda做回调,有没有什么注意事项?
    9. 3.9. 9、C++11的thread_local有没有使用过?
    10. 3.10. 10、谈一谈你对zero overhead(零开销原则)的理解
    11. 3.11. 11、了解移动语义和完美转发吗?
    12. 3.12. 12、了解列表初始化吗?
  4. 4. 第四部分、 多线程(多进程) 相关
    1. 4.1. 1、线程与进程的区别
    2. 4.2. 2、什么是线程不安全?
    3. 4.3. 3、造成线程不安全的原因
    4. 4.4. 4、C++线程中的几类锁
    5. 4.5. 5、什么是死锁,解决方式
    6. 4.6. 6、进程之间的通信方式
    7. 4.7. 7、线程之间的通信方式
  5. 5. 第五部分、 STL
    1. 5.1. 1、STL 六大组件
    2. 5.2. 2、stack 中有 pop() 和 top() 方法,为什么不直接用 pop() 实现弹出和取值的功能?
    3. 5.3. 3、 STL库用过吗?常见的STL容器有哪些?算法用过几个?
    4. 5.4. 4、STL中map 、set、multiset、multimap的底层原理(关联式容器)
    5. 5.5. 5、map 、set、multiset、multimap的特点
    6. 5.6. 6、hash_map与map的区别?什么时候用hash_map,什么时候用map?
    7. 5.7. 7、STL中unordered_map和map的区别
    8. 5.8. 8、STL中的vector的实现,是怎么扩容的?
    9. 5.9. 9、C++中vector和list的区别
    10. 5.10. 10、STL内存优化?
    11. 5.11. 11、正确释放vector的内存(clear(), swap(), shrink_to_fit())
    12. 5.12. 12、什么情况下用vector,什么情况下用list,什么情况下用deque
    13. 5.13. 13、priority_queue的底层原理
    14. 5.14. 14、 STL线程不安全的情况
    15. 5.15. 15、vector 中迭代器失效的情况
    16. 5.16. 16、迭代器的几种失效的情况
    17. 5.17. 17、STL 是复制性还是侵入性
    18. 5.18. 18、vector使用注意事项
  6. 6. 第六部分、 网络编程
    1. 6.1. 1、三次握手和四次挥手
    2. 6.2. 2、 TCP 和 UDP 的区别
    3. 6.3. 3、TCP 粘包 、拆包
    4. 6.4. 4、TCP 丢包
    5. 6.5. 5、为什么“握手”是三次,“挥手”却要四次?
    6. 6.6. 6、为什么“握手”是三次,“挥手”却要四次?
    7. 6.7. 7、网络的七层模型,作用、传输单位分别是什么
  7. 7. 第七部分、 设计模式
    1. 7.1. 1、设计模式
    2. 7.2. 2、单例模式的线程安全问题
    3. 7.3. 3、工厂方法模式
  8. 8. 第八部分、 数据结构
    1. 8.1. 1、双链表和单链表的优缺点
    2. 8.2. 2、红黑树比AVL的优势,为何用红黑树
    3. 8.3. 3、红黑树的高度
    4. 8.4. 4、判断链表是不是环形链表
    5. 8.5. 5、简述队列和栈的异同
  9. 9. 整理的原文
  10. 10. 参考阅读
  11. 11. 欢迎指出错误、遗漏的题,尽量做到面试的时候复习一文就够

第一部分 c++ 基础

1C和C++的区别

  1. C是面向过程的语言是一个结构化的语言考虑如何通过一个过程对输入进行处理得到输出C++是面向对象的语言主要特征是封装继承和多态封装隐藏了实现细节使得代码模块化派生类可以继承父类的数据和方法扩展了已经存在的模块实现了代码重用多态则是一个接口多种实现通过派生类重写父类的虚函数实现了接口的重用
  2. C和C++动态管理内存的方法不一样C是使用malloc/free而C++除此之外还有new/delete关键字
  3. C++中有引用C中不存在引用的概念

2C++中指针和引用的区别

  1. 指针有自己的一块空间而引用只是一个别名
  2. 使用 sizeof 看一个指针的大小为4字节32位如果要是64位的话指针为8字节而引用则是被引用对象的大小
  3. 指针可以被初始化为 NULL而引用必须被初始化且必须是一个已有对象的引用
  4. 作为参数传递时指针需要被解引用才可以对对象进行操作而直接对引用的修改都会改变引用所指向的对象
  5. 指针在使用中可以指向其他对象但是引用只能是一个对象的引用不能被改变
  6. 指针可以是多级而引用没有分级
  7. 如果返回动态分配内存的对象或者内存必须使用指针引用可能引起内存泄漏

引用占用内存空间吗 对引用取地址其实是取的引用所对应的内存空间的地址这个现象让人觉得引用好像并非一个实体但是引用是占用内存空间的而且其占用的内存和指针一样因为引用的内部实现就是通过指针来完成的

3结构体struct和共同体union联合的区别

结构体将不同类型的数据组合成一个整体是自定义类型 共同体不同类型的几个变量共同占用一段内存

  1. 结构体中的每个成员都有自己独立的地址它们是同时存在的 共同体中的所有成员占用同一段内存它们不能同时存在
  2. sizeof(struct)是内存对齐后所有成员长度的总和sizeof(union)是内存对齐后最长数据成员的长度

结构体为什么要内存对齐呢

  1. 平台原因移植原因不是所有的硬件平台都能访问任意地址上的任意数据某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常
  2. 硬件原因经过内存对齐之后CPU的内存访问速度大大提升

4struct 和 class 的区别

  1. 首先说一下C中的结构体和C++中的结构体的异同 在这里插入图片描述
  2. C++中 struct 与 class 的区别 内部成员变量及成员函数的默认访问属性struct 默认防控属性是 public 的而 class 默认的访问属性是private的 继承关系中默认访问属性的区别在继承关系struct 默认是 public 的而 class 是 private class这个关键字还可用于定义模板参数就等同于 typename而strcut不用与定义模板参数

5#define和const的区别

  1. #define定义的常量没有类型所给出的是一个立即数const定义的常量有类型名字存放在静态区域
  2. 处理阶段不同#define定义的宏变量在预处理时进行替换可能有多个拷贝const所定义的变量在编译时确定其值只有一个拷贝
  3. #define定义的常量是不可以用指针去指向const定义的常量可以用指针去指向该常量的地址
  4. #define可以定义简单的函数const不可以定义函数

6重载overload覆盖重写override隐藏重定义overwrite这三者之间的区别

  1. overload将语义相近的几个函数用同一个名字表示但是参数列表参数的类型个数顺序不同不同这就是函数重载返回值类型可以不同 特征相同范围同一个类中函数名字相同参数不同virtual关键字可有可无
  2. override派生类覆盖基类的虚函数实现接口的重用返回值类型必须相同 特征不同范围基类和派生类函数名字相同参数相同基类中必须有virtual关键字必须是虚函数
  3. overwrite派生类屏蔽了其同名的基类函数返回值类型可以不同 特征不同范围基类和派生类函数名字相同参数不同或者参数相同且无virtual关键字

7new 和 delete 是如何实现的与 malloc 和 free有什么异同

new操作针对数据类型的处理分为两种情况

  1. 简单数据类型包括基本数据类型和不需要构造函数的类型 简单类型直接调用 operator new 分配内存 可以通过new_handler 来处理 new 失败的情况 new 分配失败的时候不像 malloc 那样返回 NULL它直接抛出异常bad_alloc要判断是否 分配成功应该用异常捕获的机制
  2. 复杂数据类型需要由构造函数初始化对象 new 复杂数据类型的时候先调用operator new然后在分配的内存上调用构造函数

delete也分为两种情况

  1. 简单数据类型包括基本数据类型和不需要析构函数的类型 delete简单数据类型默认只是调用free函数
  2. 复杂数据类型需要由析构函数销毁对象 delete复杂数据类型先调用析构函数再调用operator delete

与 malloc 和 free 的区别

  1. 属性上new / delete 是c++关键字需要编译器支持 malloc/free是库函数需要c的头文件支持
  2. 参数使用new操作符申请内存分配时无须制定内存块的大小编译器会根据类型信息自行计算而mallco则需要显式地指出所需内存的尺寸
  3. 返回类型new操作符内存分配成功时返回的是对象类型的指针类型严格与对象匹配故new是符合类型安全性的操作符而malloc内存成功分配返回的是void *需要通过类型转换将其转换为我们需要的类型
  4. 分配失败时new内存分配失败时抛出bad_alloc异常malloc分配内存失败时返回 NULL
  5. 自定义类型new会先调用operator new函数申请足够的内存通常底层使用malloc实现然后调用类型的构造函数初始化成员变量最后返回自定义类型指针delete先调用析构函数然后调用operator delete函数释放内存通常底层使用free实现 malloc/free是库函数只能动态的申请和释放内存无法强制要求其做自定义类型对象构造和析构工作
  6. 重载C++允许重载 new/delete 操作符而malloc为库函数不允许重载
  7. 内存区域new操作符从自由存储区free store上为对象动态分配内存空间而malloc函数从堆上动态分配内存其中自由存储区为C++基于new操作符的一个抽象概念凡是通过new操作符进行内存申请该内存即为自由存储区而堆是操作系统中的术语是操作系统所维护的一块特殊内存用于程序的内存动态分配C语言使用malloc从堆上分配内存使用free释放已分配的对应内存自由存储区不等于堆如上所述布局new就可以不位于堆中

既然有了malloc/freeC++中为什么还需要new/delete呢 运算符是语言自身的特性有固定的语义编译器知道意味着什么由编译器解释语义生成相应的代码库函数是依赖于库的一定程度上独立于语言的编译器不关心库函数的作用只保证编译调用函数参数和返回值符合语法生成call函数的代码 对于非内部数据类型而言光用malloc/free无法满足动态对象都要求new/delete是运算符编译器保证调用构造和析构函数对对象进行初始化/析构但是库函数malloc/free是库函数不会执行构造/析构

8delete和delete[]的区别

delete只会调用一次析构函数而delete[]会调用每个成员的析构函数 用new分配的内存用delete释放用new[]分配的内存用delete[]释放

9const知道吗解释一下其作用

const修饰类的成员变量表示常量不可能被修改 const修饰类的成员函数表示该函数不会修改类中的数据成员不会调用其他非const的成员函数 const函数只能调用const函数非const函数可以调用const函数

10关键字static的作用

  1. 函数体内 static 修饰的局部变量作用范围为该函数体不同于auto变量其内存只被分配一次因此其值在下次调用的时候维持了上次的值
  2. 模块内static修饰全局变量或全局函数可以被模块内的所有函数访问但是不能被模块外的其他函数访问使用范围限制在声明它的模块内
  3. 类中修饰成员变量表示该变量属于整个类所有对类的所有对象只有一份拷贝
  4. 类中修饰成员函数表示该函数属于整个类所有不接受this指针只能访问类中的static成员变量 注意和const的区别const强调值不能被修改而static强调唯一的拷贝对所有类的对象

11堆和栈的区别

  1. 堆栈空间分配区别操作系统由操作系统自动分配释放 存放函数的参数值局部变量的值等其操作方式类似于数据结构中的栈操作系统 一般由程序员分配释放 若程序员不释放程序结束时可能由OS回收分配方式倒是类似于链表
  2. 堆栈的缓存方式区别 栈是内存中存储值类型的大小为2Mwindowlinux下默认为8M可以更改超出则会报错内存溢出内存中存储的是引用数据类型引用数据类型无法确定大小堆实际上是一个在内存中使用到内存中零散空间的链表结构的存储空间堆的大小由引用类型的大小直接决定引用类型的大小的变化直接影响到堆的变化
  3. 堆栈数据结构上的区别 堆数据结构堆可以被看成是一棵树堆排序数据结构一种先进后出的数据结构

C++内存区域中堆和栈的区别 管理方式不同栈是由编译器自动管理无需我们手工控制对于堆来说释放由程序员完成容易产生内存泄漏 空间大小不同一般来讲在32为系统下面堆内存可达到4G的空间从这个角度来看堆内存几乎是没有什么限制的但是对于栈来讲一般都是有一定空间大小的例如在vc6下面默认的栈大小好像是1M当然也可以自己修改打开工程 project–>setting–>link在category中选中output然后再reserve中设定堆栈的最大值和 commit 能否产生碎片对于堆来讲频繁的new/delete势必会造成内存空间的不连续从而造成大量的碎片使程序效率降低对于栈来讲则不会存在这个问题 生长方向不同对于堆来讲生长方向是向上的也就是向着内存地址增加的方向对于栈来讲它的生长方式是向下的是向着内存地址减小的方向增长 分配方式不同堆都是动态分配的栈静态分配由编译器完成比如局部变量的分配 分配效率不同栈是机器系统提供的数据结构计算机会在底层对栈提供支持分配专门的寄存器存放栈的地址压栈出栈都有专门的指令执行这就决定了栈的效率比较高堆则是c/c++库函数提供的机制很复杂库函数会按照一定的算法进行分配显然堆的效率比栈要低得多 进程内存中的映像主要有代码区动态存储区new/delete的动态数据静态存储区

堆和自由存储区的区别 总的来说堆是C语言和操作系统的术语是操作系统维护的一块动态分配内存自由存储是C++中通过new与delete动态分配和释放对象的抽象概念他们并不是完全一样 从技术上来说heap是C语言和操作系统的术语堆是操作系统所维护的一块特殊内存它提供了动态分配的功能当运行程序调用malloc()时就会从中分配稍后调用free可把内存交还而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念通过new来申请的内存区域可称为自由存储区基本上所有的C++编译器默认使用堆来实现自由存储也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现这时藉由new运算符分配的对象说它在堆上也对说它在自由存储区上也正确 扩展: 堆和栈的区别转过无数次的文章

12#include<file.h> #include “file.h” 的区别

前者是从标准库路径寻找 后者是从当前工作路径

13什么是内存泄漏面对内存泄漏和指针越界你有哪些方法

动态分配内存所开辟的空间在使用完毕后未手动释放导致一直占据该内存即为内存泄漏 方法malloc/free要配套对指针赋值的时候应该注意被赋值的指针是否需要释放使用的时候记得指针的长度防止越界

14定义和声明的区别

为变量分配地址和存储空间的称为定义不分配地址的称为声明一个变量可以在多个地方声明但是只在一个地方定义 加入 extern 修饰的是变量的声明说明此变量将在文件以外或在文件后面部分定义说明很多时候一个变量只是声明不分配内存空间直到具体使用时才初始化分配内存空间如外部变量

15C++文件编译与执行的四个阶段

  1. 预处理根据文件中的预处理指令来修改源文件的内容
  2. 编译编译成汇编代码
  3. 汇编把汇编代码翻译成目标机器指令
  4. 链接链接目标代码生成可执行程序

16C++的内存管理

在C++中内存被分成五个区自由存储区静态存储区常量区 存放函数的参数和局部变量编译器自动分配和释放 new关键字动态分配的内存由程序员手动进行释放否则程序结束后由操作系统自动进行回收 自由存储区new所申请的内存则是在自由存储区上使用delete来释放

  • 堆是操作系统维护的一块内存而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念堆与自由存储区并不等价

全局/静态存储区存放全局变量和静态变量 常量区存放常量不允许被修改

17 C++的四种强制转换

类型转化机制可以分为隐式类型转换和显示类型转化强制类型转换 (new-type) expression new-type (expression) 隐式类型转换比较常见在混合类型表达式中经常发生四种强制类型转换操作符 static_castdynamic_castconst_castreinterpret_cast

  1. static_cast 编译时期的静态类型检查static_cast静态转换相当于C语言中的强制转换但不能实现普通指针数据空指针除外的强制转换一般用于父类和子类指针引用间的相互转换没有运行时类型检查来保证转换的安全性

①用于类层次结构中基类父类和派生类子类之间指针或引用的转换不管是否发生多态父子之间互转时编译器都不会报错

  • 进行上行转换把派生类的指针或引用转换成基类表示是安全的
  • 进行下行转换把基类指针或引用转换成派生类表示由于没有动态类型检查所以是不安全的但是编译器不会报错

②用于基本数据类型之间的转换如把int转换成char把int转换成enum这种转换的安全性也要开发人员来保证 ③把空指针转换成目标类型的空指针 ④把任何指针类型转换成空指针类型 ⑤可以对普通数据的const和non_const进行转换但不能对普通数据取地址后的指针进行const添加和消去 ⑥无继承关系的自定义类型不可转换不支持类间交叉转换 另外static_cast不能转换掉原有类型的constvolatile或者 __unaligned属性(前两种可以使用const_cast 来去除)

  1. dynamic_cast运行时的检查 动态转换的类型和操作数必须是完整类类型或空指针空引用说人话就是说只能用于类间转换支持类间交叉转换不能操作普通数据 主要用于类层次结构中基类父类和派生类子类之间指针或引用的转换 ①进行上行转换把派生类的指针或引用转换成基类表示是安全的允许转换 ②进行下行转换把基类指针或引用转换成派生类表示由于没有动态类型检查所以是不安全的不允许转化编译器会报错 ③发生多态时允许互相转换 ④无继承关系的类之间也可以相互转换类之间的交叉转换 ⑤如果dynamic_cast语句的转换目标是指针类型并且失败了则结果为0如果转换目标是引用类型并且失败了则dynamic_cast运算符将抛出一个std::bad_cast异常
  2. const_cast const_cast用于强制去掉不能被修改的常数特性其去除常量性的对象一般为指针或引用
  3. reinterpret_cast 在C++语言中reinterpret_cast主要有几种强制转换用途将指针或引用转换为一个足够长度的整型将整型转换为指针或引用类型 更详细见 C++的四种强制转换
  4. 为什么不使用C的强制转换 C的强制转换表面上看起来功能强大什么都能转但是转化不够明确不能进行错误检查容易出错

18externC作用

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码加上extern “C”后会指示编译器这部分代码按C语言的进行编译而不是C++的

19typdef和define区别

#define是预处理命令在预处理是执行简单的替换不做正确性的检查 typedef是在编译时处理的它是在自己的作用域内给已经存在的类型一个别名 typedef (int*) pINT; #define pINT2 int* 效果相同实则不同实践中见差别pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b

20引用作为函数参数以及返回值的好处

对比值传递引用传参的好处

  1. 在函数内部可以对此参数进行修改
  2. 提高函数调用和运行的效率所以没有了传值和生成副本的时间和空间消耗 值传递 形参是实参的拷贝改变形参的值并不会影响外部实参的值从被调用函数的角度来说值传递是单向的实参->形参参数的值只能传入不能传出当函数内部需要修改参数并且不希望这个改变影响调用者时采用值传递 指针传递 形参为指向实参地址的指针当对形参的指向操作时就相当于对实参本身进行的操作 引用传递 形参相当于是实参的别名对形参的操作其实就是对实参的操作在引用传递过程中被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间但是这时存放的是由主调函数放进来的实参变量的地址被调函数对形参的任何操作都被处理成间接寻址即通过栈中存放的地址访问主调函数中的实参变量正因为如此被调函数对形参做的任何操作都影响了主调函数中的实参变量 用引用作为返回值最大的好处就是在内存中不产生被返回值的副本

但是有以下的限制

  1. 不能返回局部变量的引用因为函数返回以后局部变量就会被销毁
  2. 不能返回函数内部new分配的内存的引用虽然不存在局部变量的被动销毁问题可对于这种情况返回函数内部new分配内存的引用又面临其它尴尬局面例如被函数返回的引用只是作为一 个临时变量出现而没有被赋予一个实际的变量那么这个引用所指向的空间由new分配就无法释放造成memory leak
  3. 可以返回类成员的引用但是最好是const因为如果其他对象可以获得该属性的非常量的引用那么对该属性的单纯赋值就会破坏业务规则的完整性

21什么是野指针

野指针不是NULL指针是未初始化或者未清零的指针它指向的内存地址不是程序员所期望的可能指向了受限的内存 成因

  1. 指针变量没有被初始化
  2. 指针指向的内存被释放了但是指针没有置NULL
  3. 指针超过了变量了的作用范围比如b[10]指针b+11

22C++中内存泄漏的几种情况

内存泄漏是指动态分配的堆内存由于某种原因程序未释放或无法释放造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果

  1. 类的构造函数和析构函数中new和delete没有配套
  2. 在释放对象数组时没有使用delete[]使用了delete
  3. 没有将基类的析构函数定义为虚函数当基类指针指向子类对象时如果基类的析构函数不是virtual那么子类的析构函数将不会被调用子类的资源没有正确释放因此造成内存泄露
  4. 没有正确的清楚嵌套的对象指针

23栈溢出的原因以及解决方法

栈溢出是指函数中的局部变量造成的溢出函数中形参和函数中的局部变量存放在栈上 栈的大小通常是1M-2M,所以栈溢出包含两种情况一是分配的的大小超过栈的最大值二是分配的大小没有超过最大值但是接收的buf比原buf小

  1. 函数调用层次过深,每调用一次,函数的参数局部变量等信息就压一次栈
  2. 局部变量体积太大

解决办法大致说来也有两种

  1. 增加栈内存的数目如果是不超过栈大小但是分配值小的就增大分配的大小
  2. 使用堆内存具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)

24左值和右值

不是很严谨的来说左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式)右值指的则是只能出现在等号右边的变量(或表达式)举例来说我们定义的变量 a 就是一个左值而malloc返回的就是一个右值或者左值就是在程序中能够寻址的东西右值就是一个具体的真实的值或者对象没法取到它的地址的东西(不完全准确)因此没法对右值进行赋值但是右值并非是不可修改的比如自己定义的class, 可以通过它的成员函数来修改右值 归纳一下就是 可以取地址的有名字的非临时的就是左值 不能取地址的没有名字的临时的通常生命周期就在某个表达式之内的就是右值 但是到了 C++11 之后概念变的略微复杂引入了 lvalue, glvalue, rvalue, xvalue 和 prvalue

25左值引用与右值引用

左值引用就是我们通常所说的引用如下所示左值引用通常可以看作是变量的别名

type-id & cast-expression 

// demo
int a = 10
int &b = a

int &c = 10	// 错误<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>无法对一个立即数做引用

const int &d = 10	// 正确<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span> 常引用引用常数量是ok的<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>其等价于 const int temp = 10; const int &d = temp	

右值引用是 C++11 新增的特性其形式如下所示右值引用用来绑定到右值绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期

  • 右值引用支持移动语义的实现可以减少拷贝提升程序的执行效率
  • 右值引用可以使重载函数变得更加简洁右值引用可以适用 const T& 和 T& 形式的参数
type-id && cast-expression  

// demo
int &&var = 10;	// ok

int a = 10
int &&b = a	// 错误<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span> a 为左值

int &&c = var	// 错误<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>var 为左值

int &&d = move(a)	// ok, 通过move得到左值的右值引用

在汇编层面右值引用做的事情和常引用是相同的即产生临时量来存储常量但是唯一 一点的区别是右值引用可以进行读写操作而常引用只能进行读操作

26头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别

相同点 它们的作用是防止头文件被重复包含 不同点

  1. ifndef 由语言本身提供支持但是 program once 一般由编译器提供支持也就是说有可能出现编译器不支持的情况(主要是比较老的编译器)
  2. 通常运行速度上 ifndef 一般慢于 program once特别是在大型项目上 区别会比较明显所以越来越多的编译器开始支持 program once
  3. ifndef 作用于某一段被包含define 和 endif 之间的代码 而 program once 则是针对包含该语句的文件 这也是为什么 program once 速度更快的原因
  4. 如果用 ifndef 包含某一段宏定义当这个宏名字出现撞车可能会出现这个宏在程序中提示宏未定义的情况在编写大型程序时特别需要注意因为有很多程序员在同时写代码相反由于program once 针对整个文件 因此它不存在宏名字撞车的情况 但是如果某个头文件被多次拷贝program once 无法保证不被多次包含因为program once 是从物理上判断是不是同一个头文件而不是从内容上

27指针数组和数组指针的区别

数组指针是指向数组的指针而指针数组则是指该数组的元素均为指针

  1. 数组指针是指向数组的指针其本质为指针形式如下如 int (*p)[n]p即为指向数组的指针()优先级高首先说明p是一个指针指向一个整型的一维数组这个一维数组的长度是n也可以说是p的步长也就是说执行p+1时p要跨过n个整型数据的长度数组指针是指向数组首元素的地址的指针其本质为指针可以看成是二级指针
类型名 (*数组标识符)[数组长度]
  1. 指针数组在C语言和C++中数组元素全为指针的数组称为指针数组其中一维指针数组的定义形式如下指针数组中每一个元素均为指针其本质为数组如 int _p[n] []优先级高先与p结合成为一个数组再由int_说明这是一个整型指针数组它有n个指针类型的数组元素这里执行p+1时则p指向下一个数组元素这样赋值是错误的p=a因为p是个不可知的表示只存在p[0]p[1]p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址但可以这样 _p=a; 这里_p表示指针数组第一个元素的值a的首地址的值
类型名 *数组标识符[数组长度]

28C++是不是类型安全的

不是两个不同类型的指针之间可以强制转换

29main函数执行之前还会执行什么代码

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

30全局变量和局部变量有什么区别是怎么实现的操作系统和编译器是怎么知道的

生命周期不同 全局变量随主程序创建和创建随主程序销毁而销毁局部变量在局部函数内部甚至局部循环体等内部存在退出就不存在 使用方式不同 通过声明后全局变量程序的各个部分都可以用到局部变量只能在局部使用分配在栈区 内存分配位置不同 全局变量分配在全局数据段并且在程序开始运行的时候被加载局部变量则分配在堆栈里面

31关于sizeof小结的

sizeof计算的是在栈中分配的内存大小

  1. sizeof不计算static变量占得内存
  2. 32位系统的指针的大小是4个字节64位系统的指针是8字节而不用管指针类型
  3. char型占1个字节int占4个字节short int占2个字节 long int占4个字节float占4字节double占8字节string占4字节 一个空类占1个字节单一继承的空类占1个字节虚继承涉及到虚指针所以占4个字节
  4. 数组的长度 若指定了数组长度则不看元素个数总字节数=数组长度*sizeof元素类型 若没有指定长度则按实际元素个数类确定 Ps若是字符数组则应考虑末尾的空字符
  5. 结构体对象的长度 在默认情况下为方便对结构体内元素的访问和管理当结构体内元素长度小于处理器位数的时候便以结构体内最长的数据元素的长度为对齐单位即为其整数倍若结构体内元素长度大于处理器位数则以处理器位数为单位对齐
  6. unsigned影响的只是最高位的意义数据长度不会改变所以sizeofunsigned int=4
  7. 自定义类型的sizeof取值等于它的类型原型取sizeof
  8. 对函数使用sizeof在编译阶段会被函数的返回值的类型代替
  9. sizeof后如果是类型名则必须加括号如果是变量名可以不加括号这是因为sizeof是运算符
  10. 当使用结构类型或者变量时sizeof返回实际的大小当使用静态数组时返回数组的全部大小sizeof不能返回动态数组或者外部数组的尺寸

32sizeof 和 strlen 的区别

  1. sizeof 是一个操作符 strlen 是库函数
  2. sizeof 的参数可以是数据的类型 也可以是变量 而 strlen 只能以结尾为 ‘\0’ 的字符串做参数
  3. 数组做 siezeof 的参数不退化传递给 strlen 就退化为指针了
  4. 编译器在编译时就计算出了 sizeof 的结果 而 strlen 函数必须在运行时才能计算出来 而且 sizeof 计算的是数据类型占内存的大小 而 strlen 计算的是字符串实际的长度

33说说内联函数

函数调用是有时间和空间开销的程序在执行一个函数之前需要做一些准备工作要将实参局部变量返回地址以及若干寄存器都压入栈中然后才能执行函数体中的代码函数体中的代码执行完毕后还要清理现场将之前压入栈中的数据都出栈才能接着执行函数调用位置以后的代码如果函数体代码比较多需要较长的执行时间那么函数调用机制占用的时间可以忽略如果函数只有一两条语句那么大部分的时间都会花费在函数调用机制上这种时间开销就就不容忽视 为了消除函数调用的时空开销C++ 提供一种提高效率的方法即在编译时将函数调用处用函数体替换类似于C语言中的宏展开这种在函数调用处直接嵌入函数体的函数称为内联函数Inline Function又称内嵌函数或者内置函数 详见 C++ inline内联函数详解

34C/C++的存储期

C对象有4种存储期静态存储期线程存储期自动存储期动态分配存储期 详见 C/C++的存储期

35流操作符重载为什么返回引用

在程序中流操作符>>和<<经常连续使用因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用其他的数据类型都无法做到这一点 注意除了在赋值操作符和流操作符之外的其他的一些操作符中如+-*/等却千万不能返回引用因为这四个操作符的对象都是右值因此它们必须构造一个对象作为返回值

36全局变量和局部变量有什么区别实怎么实现的操作系统和编译器是怎么知道的

  1. 生命周期不同 全局变量随主程序创建和创建随主程序销毁而销毁 局部变量在局部函数内部甚至局部循环体等内部存在退出就不存在 内存中分配在全局数据区
  2. 使用方式不同通过声明后全局变量程序的各个部分都可以用到局部变量只能在局部使用分配在栈区

操作系统和编译器通过内存分配的位置来知道的全局变量分配在全局数据段并且在程序开始运行的时候被加载局部变量则分配在堆栈里面

第二部分 c++ 类相关

1什么是面向对象OOP面向对象的意义

Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法思想通过将需求要素转化为对象进行问题处理的一种思想其核心思想是数据抽象封装继承和动态绑定多态 面向对象的意义在于将日常生活中习惯的思维方式引入程序设计中将需求中的概念直观的映射到解决方案中以模块为中心构建可复用的软件系统提高软件产品的可维护性和可扩展性

2解释下封装继承和多态

  1. 封装 封装是实现面向对象程序设计的第一步封装就是将数据或函数等集合在一个个的单元中我们称之为类 封装的意义在于保护或者防止代码数据被我们无意中破坏 从封装的角度看public private 和 protected 属性的特点如下

    • 不管哪种属性内类都是可以访问的
    • public 是一种暴露的手段比如暴露接口类的对象可以访问
    • private 是一种隐藏的手段类的对象不能访问
    • protected 成员
      • 和 public 一样可以被子类继承
      • 和 private 一样不能在类外被直接调用
      • 特例在衍生类中可以通过衍生类对象访问
  2. 继承 继承主要实现重用代码节省开发时间 子类可以继承父类的一些东西

    • a.公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时它们都保持原有的状态基类的私有成员仍然是私有的不能被这个派生类的子类所访问
    • b.私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员并且不能被这个派生类的子类所访问
    • c.保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员并且只能被它的派生类成员函数或友元访问基类的私有成员仍然是私有的 这里特别提一下虚继承虚继承是解决C++多重继承问题其一浪费存储空间第二存在二义性问题的一种手段比如菱形继承典型的应用就是 iostream, 其继承于 istream 和 ostream而 istream 和 ostream 又继承于 ios
  3. 多态 多态是指通过基类的指针或者引用在运行时动态调用实际绑定对象函数的行为与之相对应的编译时绑定函数称为静态绑定多态是设计模式的基础多态是框架的基础 C++静态多态与动态多态

3构造函数和析构函数的执行顺序

构造函数

  1. 首先调用父类的构造函数
  2. 调用成员变量的构造函数
  3. 调用类自身的构造函数

析构函数 对于栈对象或者全局对象调用顺序与构造函数的调用顺序刚好相反也即后构造的先析构对于堆对象析构顺序与delete的顺序相关

4虚函数是怎么实现的

每一个含有虚函数的类都至少有有一个与之对应的虚函数表其中存放着该类所有虚函数对应的函数指针地址 类的示例对象不包含虚函数表只有虚指针 派生类会生成一个兼容基类的虚函数表

5 构造函数为什么一般不定义为虚函数而析构函数一般写成虚函数的原因

  1. 构造函数不能声明为虚函数 1). 因为创建一个对象时需要确定对象的类型而虚函数是在运行时确定其类型的而在构造一个对象时由于对象还未创建成功编译器无法知道对象的实际类型是类本身还是类的派生类等等 2). 虚函数的调用需要虚函数表指针而该指针存放在对象的内存空间中若构造函数声明为虚函数那么由于对象还未创建还没有内存空间更没有虚函数表地址用来调用虚函数即构造函数了
  2. 析构函数最好声明为虚函数 首先析构函数可以为虚函数当析构一个指向派生类的基类指针时最好将基类的析构函数声明为虚函数否则可以存在内存泄露的问题 如果析构函数不被声明成虚函数则编译器实施静态绑定在删除指向派生类的基类指针时只会调用基类的析构函数而不调用派生类析构函数这样就会造成派生类对象析构不完全 子类析构时要调用父类的析构函数吗 析构函数调用的次序时先派生类后基类的和构造函数的执行顺序相反并且析构函数要是virtual的否则如果用父类的指针指向子类对象的时候析构函数静态绑定不会调用子类的析构 不用显式调用会自动调用

6细看拷贝构造函数

对于 class A它的拷贝构造函数如下

A::A(const A &a){}
  1. 为什么必须是当前类的引用呢 循环调用如果拷贝构造函数的参数不是当前类的引用而是当前类的对象那么在调用拷贝构造函数时会将另外一个对象直接传递给形参这本身就是一次拷贝会再次调用拷贝构造函数然后又将一个对象直接传递给了形参将继续调用拷贝构造函数……这个过程会一直持续下去没有尽头陷入死循环

只有当参数是当前类的引用时才不会导致再次调用拷贝构造函数这不仅是逻辑上的要求也是 C++ 语法的要求

  1. 为什么是 const 引用呢 拷贝构造函数的目的是用其它对象的数据来初始化当前对象并没有期望更改其它对象的数据添加 const 限制后这个含义更加明确了

另外一个原因是添加 const 限制后可以将 const 对象和非 const 对象传递给形参了因为非 const 类型可以转换为 const 类型如果没有 const 限制就不能将 const 对象传递给形参因为 const 类型不能直接转换为非 const 类型这就意味着不能使用 const 对象来初始化当前对象了

7静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

  1. 对象的静态类型和动态类型 由于继承导致对象的指针和引用具有两种不同的类型静态类型和动态类型 静态类型对象在声明时采用的类型在编译时确定 动态类型当前对象所指的类型在运行期决定对象的动态类型可变静态类型无法更改
  2. 静态绑定和动态绑定 静态绑定绑定的是对象的静态类型函数依赖于对象的静态类型在编译期确定 动态绑定绑定的是对象的动态类型函数依赖于对象的动态类型在运行期确定 只有虚函数才使用的是动态绑定其他的全部是静态绑定
  3. 引用是否能实现动态绑定为什么引用可以实现 可以因为引用或指针既可以指向基类对象也可以指向派生类对象这一事实是动态绑定的关键用引用或指针调用的虚函数在运行时确定被调用的函数是引用或指针所指的对象的实际类型所定义的

8深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为如果一个类拥有资源当这个类的对象发生复制过程的时候如果资源重新分配了就是深拷贝反之没有重新分配资源就是浅拷贝

9 什么情况下会调用拷贝构造函数三种情况

系统自动生成的构造函数普通构造函数和拷贝构造函数 在没有定义对应的构造函数的时候 生成一个实例化的对象会调用一次普通构造函数而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数 调用拷贝构造函数的情形

  1. 用类的一个对象去初始化另一个对象的时候
  2. 当函数的参数是类的对象时就是值传递的时候如果是引用传递则不会调用
  3. 当函数的返回值是类的对象或者引用的时候

10类对象的大小受哪些因素影响

  1. 类的非静态成员变量大小静态成员不占据类的空间成员函数也不占据类的空间大小
  2. 内存对齐另外分配的空间大小类内的数据也是需要进行内存对齐操作的
  3. 虚函数的话会在类对象插入vptr指针加上指针大小
  4. 当该该类是某类的派生类那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中也会对派生类进行扩展

11拷贝构造函数和赋值运算符的理解

  1. 拷贝构造函数生成新的类对象而赋值运算符不能
  2. 由于拷贝构造函数是直接构造一个新的类对象所以在初始化这个对象之前不用检验源对象 是否和新建对象相同而赋值运算符则需要这个操作另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉

当类中有指针类型的成员变量时一定要重写拷贝构造函数和赋值运算符 不要使用默认的

12C++空类默认有哪些成员函数

默认构造函数析构函数复制构造函数赋值函数

13如果虚函数是有效的那为什么不把所有函数设为虚函数

不行首先虚函数是有代价的由于每个虚函数的对象都要维护一个虚函数表因此在使用虚函数的时候都会产生一定的系统开销这是没有必要的

14构造函数和虚构函数可以是内联函数

构造函数析构函数虚函数可以声明为内联函数这在语法上是正确的 编译器并不真正对声明为inline的构造和析构函数内联因为编译器会在构造和析构函数中添加额外的操作申请/释放内存构造/析构对象调用基类成员对象构造函数等致使构造函数/析构函数并不像看上去的那么精简

15虚函数声明为inline

inline是编译期决定的而虚函数是运行期决定的即在不知道将要调用哪个函数的情况下如何将函数内联

16C++的空类有哪些成员函数

  1. 缺省构造函数
  2. 缺省拷贝构造函数
  3. 缺省析构函数
  4. 缺省赋值运算符
  5. 缺省取址运算符
  6. 缺省取址运算符 const

注意有些书上只是简单的介绍了前四个函数没有提及后面这两个函数但后面这两个函数也是空类的默认函数另外需要注意的是只有当实际使用这些函数的时候编译器才会去定义它们

17C++中的五种构造函数

  1. 默认构造函数
  2. 普通构造函数
  3. 拷贝构造函数
  4. 转换构造函数
  5. 移动构造函数 详见 C++中的五种构造函数

第三部分 c++11/c++14/c++17

1C++11 中有哪些智能指针shared_ptr 的引用计数是如何实现的unique_ptr 的unique 是如何实现的make_shared 和 make_unique 的作用智能指针使用注意事项

C++ 11 中的智能指针有shared_ptr, unique_ptr 和 weak_ptr shared_ptr 的引用计数是存放在堆上的多个 shared_ptr 的对象的引用计数都指向同一个堆地址 unique_ptr 中拷贝构造函数和赋值操作符都声明为delete或private 优先使用 make_shared 和 make_unique 的原因是为了避免内存泄露参考 C++11 中的 Smart Pointershared_ptr/weak_ptr/unique_ptr 总结 智能指针使用注意事项 不使用相同的内置指针值初始化或reset多个智能指针 不delete get()返回的指针 不使用get()初始化或reset另一个智能指针 get()返回的智能指针可能变成dangling pointer 如果智能指针管理的内存不是new出来的需要提供删除器 拓展问题 shared_ptr 是否线程安全 侵入式智能指针 从源码理解智能指针—— shared_ptrweak_ptr 在这里插入图片描述

2智能指针weak_ptr 能够破坏环型引用的原理引用计数的原理

weak_ptr只是提供了对管理对象的一个访问手段weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放它是对对象的一种弱引用不会增加对象的引用计数和shared_ptr之间可以相互转化shared_ptr可以直接赋值给它它可以通过调用lock函数来获得shared_ptr 引用计数原理 shared_ptr的实现是这样的: shared_ptr模板类有一个__shared_count类型的成员_M_refcount来处理引用计数的问题__shared_count也是一个模板类它的内部有一个指向Sp_counted_base_impl类型的指针_M_pi所有引用同一个对象的shared_ptr都共用一个_M_pi指针 当我们谈论shared_ptr的线程安全性时我们在谈论什么

3C++ 的闭包

闭包与函数A调用函数B相比较闭包中函数A调用函数B可以不通过函数A给函数B传递函数参数而使函数B可以访问函数A的上下文环境才可见(函数A可直接访问到)的变量 c++11提供的闭包方式lambdafunctionbind

引用百度上对闭包的定义闭包是指可以包含自由未绑定到特定对象变量的代码块这些变量不是在这个代码块内或者任何全局上下文中定义的而是在定义代码块的环境中定义局部变量闭包 一词来源于以下两者的结合要执行的代码块由于自由变量被包含在代码块中这些自由变量以及它们引用的对象没有被释放和为自由变量提供绑定的计算环境作用域

详见 c++11的闭包(lambdafunctionbind)

4lambda 表达式怎么捕获外部变量

  1. 值捕获: 值捕获和参数传递中的值传递类似被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入因此随后对该变量的修改不会影响影响Lambda表达式中的值
  2. 引用捕获: 使用引用捕获一个外部变量只需要在捕获列表变量前面加上一个引用说明符&
  3. 隐式捕获: 还可以让编译器根据函数体中的代码来推断需要捕获哪些变量这种方式称之为隐式捕获
  4. 混合方式捕获: 即同时使用显式捕获和隐式捕获混合捕获时捕获列表中的第一个元素必须是 = 或 &此符号指定了默认捕获的方式是值捕获或引用捕获

C++11捕获外部变量总结

捕获形式 说明
[] 不捕获任何外部变量
[变量名, …] 默认以值得形式捕获指定的多个外部变量用逗号分隔如果引用捕获需要显示声明使用&说明符
[this] 以值的形式捕获this指针
[=] 以值的形式捕获所有外部变量
[&] 以引用形式捕获所有外部变量
[=, &x] 变量x以引用形式捕获其余变量以传值形式捕获
[&, x] 变量x以值的形式捕获其余变量以引用形式捕获

5C++ 的垃圾回收机制

很多语言都有对内存自动管理即所谓的垃圾回收机制所谓垃圾指的是那些不再使用或者没有任何指针指向的内存空间回收则指的是将这些垃圾收集起来以便再次利用C++采用 unique_ptrshared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收来实现自动释放分配的内存 C++之智能指针C++的垃圾回收机制

6vector的clear()的时间复杂度是多少

C++标准明确指出不管是序列容器⽐如vector还是关联容器⽐如unordered_map)其 clear()成员函数都是线性时间复杂度O(n)的因为只要执⾏了clear()就需要对其存储的元素调⽤析构函数 这个析构操作显然是逐个析构的因⽽时间复杂度是O(n) 当然在实践中也有个例⽐如当vector存储基本数据类型或POD类型⽐如基本数据类型构成的struct的时候由于其元素类型没有析构函数也不需要析构函数加之vector内部连续存储的特性编译器的实现是可以在常量时间完成clear()的 仅限于vector存储基本数据类型和POD类型的时候编译器可能有此优化如果vector存储的 是其他类型的对象或者是其他容器⽐如listmapunordered_map都是没办法做这个优化的 详见 C++和STL中有哪些副作用或者稍不注意会产生性能开销的地方

7C++11为什么引入enum class

  1. C++98 的 enum 是非域化的而 C++11 的 enum class 是域化的限制了枚举成员只在域内可见
  2. enum class 的缺省潜在类型 (underlying type) 是 int 型而 enum 没有缺省潜在类型
  3. enum class 一般总是前置声明而 enum 只有在指定了潜在类型时才可以是前置声明

详见 C++11 之 enum class

8std::thread使用lambda做回调有没有什么注意事项

  1. 使用到外部引用要小心谨慎避免悬空引用 若需要用到的外部局部变量需以值传递的方式捕获而非引用捕获若是外部指针变量则需深拷贝
  2. 谨慎使用或者不用外部指针 如果你用值捕获了个指针你在lambda创建的闭包中持有这个指针的拷贝但你不能阻止lambda外面的代码删除指针指向的内容从而导致你拷贝的指针空悬
  3. 注意引用捕获陷阱引用捕获[&]不要使用外部局部变量
  4. 注意this陷阱lambda里避免有全局变量或静态变量或者比当前类生命周期更长的变量Effective Modern C++ 条款31 对于lambda表达式避免使用默认捕获模式
  5. 避免使用默认捕获模式([=][&],它可能导致你看不出悬空引用问题) 默认值捕获就意外地捕获了this指针而不是你以为的外部变量

例子参考 c++的lambda使用注意事项,可能导致的崩溃问题分析

9C++11的thread_local有没有使用过

有且只有thread_local关键字修饰的变量具有线程周期(thread duration)这些变量(或者说对象在线程开始的时候被生成(allocated)在线程结束的时候被销毁(deallocated)并且每 一个线程都拥有一个独立的变量实例(Each thread has its own instance of the object)thread_local 可以和static 与 extern关键字联合使用这将影响变量的链接属性(to adjust linkage) 那么哪些变量可以被声明为thread_local以下3类都是ok的

  1. 命名空间下的全局变量
  2. 类的static成员变量
  3. 本地变量

详见 C++11中thread_local的使用

10谈一谈你对zero overhead零开销原则的理解

  1. 不需要为没有使用到的语言特性付出代价
  2. 使用某种语言特性不会带来运行时的代价

详见 跟我学c++中级篇——zero overhead abstraction

11了解移动语义和完美转发吗

移动语义可以理解为转移所有权拷贝是对于别人的资源自己重新分配一块内存存储复制过来的资源而对于移动语义类似于转让或者资源窃取的意思对于那块资源转为自己所拥有别人不再拥有也不会再使用通过C++11新增的移动语义可以省去很多拷贝负担如何利用移动语义主要通过移动构造函数

完美转发指可以写一个接受任意实参的函数模板并转发到其它函数目标函数会收到与转发函数完全相同的实参转发函数实参是左值那目标函数实参也是左值转发函数实参是右值那目标函数也是右值 参考 C++11之右值引用移动语义和完美转发(带你了解移动构造函数纯右值将亡值右值引用std::moveforward等新概念)

12了解列表初始化吗

列表初始化可以直接在变量名后面加上初始化列表来进行对象的初始化 参考 C++11之初始化列表

第四部分 多线程多进程 相关

1线程与进程的区别

线程与进程的比较如下

  1. 进程是资源包括内存打开的文件等分配的单位线程是 CPU 调度的单位
  2. 进程拥有一个完整的资源平台而线程只独享必不可少的资源如寄存器和栈
  3. 线程同样具有就绪阻塞执行三种基本状态同样具有状态之间的转换关系
  4. 线程能减少并发执行的时间和空间开销

对于线程相比进程能减少开销体现在

  1. 线程的创建时间比进程快因为进程在创建的过程中还需要资源管理信息比如内存管理信息文件管理信息而线程在创建的过程中不会涉及这些资源管理信息而是共享它们
  2. 线程的终止时间比进程快因为线程释放的资源相比进程少很多
  3. 同一个进程内的线程切换比进程切换快因为线程具有相同的地址空间虚拟内存共享这意味着同一个进程的线程都具有同一个页表那么在切换的时候不需要切换页表而对于进程之间的切换切换的时候要把页表给切换掉而页表的切换过程开销是比较大的
  4. 由于同一进程的各线程间共享内存和文件资源那么在线程之间数据传递的时候就不需要经过内核了这就使得线程之间的数据交互效率更高了 所以线程比进程不管是时间效率还是空间效率都要高

进程之间私有和共享的资源 私有地址空间全局变量寄存器 共享代码段公共数据进程目录进程 ID 线程之间私有和共享的资源 私有线程栈寄存器程序计数器 共享地址空间全局变量静态变量

2什么是线程不安全?

在随机调度之下,线程执行有多种可能,其中某些可能会导致代码出bug就称为线程不安全.

3造成线程不安全的原因

  1. 操作系统的随机调度/抢占式执行(万恶之源)–>无法改变
  2. 多个线程修改同一个变量(一个字都不能少)—>尽量避免
  3. 有些修改操作,不是原子的!(不可拆分的最小单位,就叫原子 即对应一条机器指令)—>通过加锁操作,把指令打包
  4. 内存可见性问题(内存改了,但是在优化的背景下,读不到,看不见) 如:线程1一直在读取硬盘上的资源再判断,在多次读取硬盘并获得相同的结果后编译器会认为这样的做法过于低效,然后就会省略读取的过程,一直判断,若此时有线程2需要这个判断结果(读取到数据后的判断结果)就会出现问题.
  5. 指令重排序(也是编译器,操作系统等的优化,调整了代码的执行顺序)

最常见的就是对象new的问题 分为三步:1.创建内存空间 2.往内存空间上构造对象 3.把这个内存的引用赋值给你要创建的变量 有时候编译器会认为先执行3或者先执行2最高效,就会出现指令顺序倒转的问题,这时另一个线程尝试读取变量的引用就会出现问题

4C++线程中的几类锁

线程之间的锁有 互斥锁条件锁自旋锁读写锁递归锁一般而言锁的功能与性能成反比详见C++11线程中的几种锁如何理解互斥锁条件锁读写锁以及自旋锁

5什么是死锁解决方式

死锁死锁是指两个或两个以上的进程线程在运行过程中因争夺资源而造成的一种僵局若无外力作用这些进程线程都将无法向前推进 详见 死锁四个必要条件及死锁的预防检测避免解除

6进程之间的通信方式

管道PIPE 1有名管道一种半双工的通信方式它允许无亲缘关系进程间的通信 ①优点可以实现任意关系的进程间的通信 ②缺点a长期存于系统中使用不当容易出错b缓冲区有限 2无名管道一种半双工的通信方式只能在具有亲缘关系的进程间使用父子进程 ①优点简单方便 ②缺点a局限于单向通信b只能创建在它的进程以及其有亲缘关系的进程之间c缓冲区有限

信号量Semaphore一个计数器可以用来控制多个线程对共享资源的访问 ①优点可以同步进程 ②缺点信号量有限

信号Signal一种比较复杂的通信方式用于通知接收进程某个事件已经发生

消息队列Message Queue是消息的链表存放在内核中并由消息队列标识符标识 ①优点可以实现任意进程间的通信并通过系统调用函数来实现消息发送和接收之间的同步无需考虑同步问题方便 ②缺点信息的复制需要额外消耗 CPU 的时间不适宜于信息量大或操作频繁的场合

共享内存Shared Memory映射一段能被其他进程所访问的内存这段共享内存由一个进程创建但多个进程都可以访问 ①优点无须复制快捷信息量大 ②缺点a通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的因此进程间的读写操作的同步问题b利用内存缓冲区直接交换信息内存的实体存在于计算机中只能同一个计算机系统中的诸多进程共享不方便网络通信

套接字Socket可用于不同计算机间的进程通信 ①优点 a传输数据为字节级传输数据可自定义数据量小效率高 b传输数据时间短性能高 c适合于客户端和服务器端之间信息实时交互 d可以加密,数据安全性强 ②缺点需对传输的数据进行解析转化成应用级的数据 更多详见 C++基础语法梳理进程与线程知识点详细梳理

7线程之间的通信方式

锁机制 包括互斥锁/量mutex读写锁reader-writer lock自旋锁spin lock条件变量condition

  1. 互斥锁/量mutex提供了以排他方式防止数据结构被并发修改的方法
  2. 读写锁reader-writer lock允许多个线程同时读共享数据而对写操作是互斥的
  3. 自旋锁spin lock与互斥锁类似都是为了保护共享资源互斥锁是当资源被占用申请者进入睡眠状态而自旋锁则循环检测保持者是否已经释放锁
  4. 条件变量condition可以以原子的方式阻塞进程直到某个特定条件为真为止对条件的测试是在互斥锁的保护下进行的条件变量始终与互斥锁一起使用

信号量机制(Semaphore)

  1. 无名线程信号量
  2. 命名线程信号量 信号机制(Signal)类似进程间的信号处理 屏障barrier屏障允许每个线程等待直到所有的合作线程都达到某一点然后从该点继续执行 线程间的通信目的主要是用于线程同步所以线程没有像进程通信中的用于数据交换的通信机制

第五部分 STL

1STL 六大组件

STL 六大组件容器Container算法Algorithm迭代器Iterator仿函数Function object适配器Adaptor和 空间配置器allocator

2stack 中有 pop() 和 top() 方法为什么不直接用 pop() 实现弹出和取值的功能

把pop()和top()的功能放在一个函数中如果 stack 中存放的是较大是内容时比如 vector 类型取值的时候就会发生拷贝如果拷贝失败要弹出的数据将会丢失就很可能出现下述的bug导致原始数据丢失 假设有一个stackvector是一个动态容器当你拷贝一个vector时标准库会从堆上分配很多内存来完成这次拷贝当这个系统处在重度负荷或有严重的资源限制的情况下这种内存分配就会失败所以vector的拷贝构造函数可能会抛出一个std::bad_alloc异常当vector中存有大量元素时这种情况发生的可能性更大当pop()函数返回弹出值时(也就是从栈中将这个值移除)会有一个潜在的问题这个值被返回到调用函数的时候栈才被改变但当拷贝数据的时候调用函数抛出一个异常会怎么样如果事情真的发生了要弹出的数据将会丢失它的确从栈上移出了但是拷贝失败了std::stack的设计人员将这个操作分为两个部分先获取顶部元素(top())然后从栈中移除元素(pop())这样在不能安全的将元素拷贝出去的情况下栈中的这个数据还依旧存在没有丢失当问题是堆空间不足时应用可能会释放一些内存然后再进行尝试 参考 为什么适配器stack中成员函数top()和pop()需要分离实现

3 STL库用过吗常见的STL容器有哪些算法用过几个

STL包括两部分内容容器和算法 容器即存放数据的地方比如array, vector分为两类序列式容器和关联式容器 序列式容器其中的元素不一定有序但是都可以被排序比如vector,list,queue,stackheap, priority-queue, slist 关联式容器内部结构是一个平衡二叉树每个元素都有一个键值和一个实值比如map, set, hashtable, hash_set 算法有排序复制等以及各个容器特定的算法 迭代器是STL的精髓迭代器提供了一种方法使得它能够按照顺序访问某个容器所含的各个元素但无需暴露该容器的内部结构它将容器和算法分开让二者独立设计 Vector是顺序容器是一个动态数组支持随机存取插入删除查找等操作在内存中是一块连续的空间在原有空间不够情况下自动分配空间增加为原来的两倍vector随机存取效率高但是在vector插入元素需要移动的数目多效率低下 注意vector动态增加大小时并不是在原空间之后持续新空间因为无法保证原空间之后尚有可供配置的空间而是以原大小的两倍另外配置一块较大的空间然后将原内容拷贝过来然后才开始在原内容之后构造新元素并释放原空间因此对vector的任何操作一旦引起空间重新配置指向原vector的所有迭代器就都失效了

4STL中map setmultisetmultimap的底层原理关联式容器

map set multiset multimap 的底层实现都是红黑树 红 黑 树 的 特 性

  1. 每个结点或是红色或是黑色
  2. 根结点是黑色
  3. 每个叶结点是黑的
  4. 如果一个结点是红的则它的两个儿子均是黑色
  5. 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点

5map setmultisetmultimap的特点

  1. set和multiset会根据特定的排序准则自动将元素排序set中元素不允许重复multiset可以重复
  2. map和multimap将key和value组成的pair作为元素根据key的排序准则自动将元素排序因为红黑树也是二叉搜索树所以map默认是按key排序的map中元素的key不允许重复multimap可以重复
  3. map和set的增删改查速度为都是logn是比较高效的

6hash_map与map的区别什么时候用hash_map什么时候用map

构造函数hash_map需要hash function和等于函数而map需要比较函数大于或小于 存储结构hash_map以hashtable为底层而map以RB-TREE为底层 总的说来hash_map查找速度比map快而且查找速度基本和数据量大小无关属于常数级别而map的查找速度是logn级别但不一定常数就比log小而且hash_map还有hash function耗时 如果考虑效率特别当元素达到一定数量级时用hash_map 考虑内存或者元素数量较少时用map

7STL中unordered_map和map的区别

map是STL中的一个关联容器提供键值对的数据管理底层通过红黑树来实现实际上是二叉排序树和非严格意义上的二叉平衡树所以在map内部所有的数据都是有序的且map的查询插入删除操作的时间复杂度都是O(logN) unordered_map和map类似都是存储key-value对可以通过key快速索引到value不同的是unordered_map不会根据key进行排序unordered_map底层是一个防冗余的哈希表存储时根据key的hash值判断元素是否相同即unoredered_map内部是无序的

8STL中的vector的实现是怎么扩容的

vector使用的注意点及其原因频繁对vector调用push_back()对性能的影响和原因 vector就是一个动态增长的数组里面有一个指针指向一片连续的空间当空间装不下的时候会申请一片更大的空间将原来的数据拷贝过去并释放原来的旧空间当删除的时候空间并不会被释放只是清空了里面的数据对比array是静态空间一旦配置了就不能改变大小 vector的动态增加大小的时候并不是在原有的空间上持续新的空间无法保证原空间的后面还有可供配置的空间而是以原大小的两倍另外配置一块较大的空间然后将原内容拷贝过来并释放原空间在VS下是1.5倍扩容在GCC下是2倍扩容

9C++中vector和list的区别

vector和数组类似拥有一段连续的内存空间vector申请的是一段连续的内存当插入新的元素内存不够时通常以2倍重新申请更大的一块内存将原来的元素拷贝过去释放旧空间因为内存空间是连续的所以在进行插入和删除操作时会造成内存块的拷贝时间复杂度为o(n) list是由双向链表实现的因此内存空间是不连续的只能通过指针访问数据所以list的随机存取非常没有效率时间复杂度为o(n); 但由于链表的特点能高效地进行插入和删除 vector拥有一段连续的内存空间能很好的支持随机存取因此vector::iterator支持++=<等操作符 list的内存空间可以是不连续它不支持随机访问因此list::iterator则不支持++=<等 vector::iterator和list::iterator都重载了++运算符 总之如果需要高效的随机存取而不在乎插入和删除的效率使用vector; 如果需要大量的插入和删除而不关心随机存取则应使用list

10STL内存优化

STL内存管理使用二级内存配置器

  1. 第一级配置器 第一级配置器以malloc()free()realloc()等C函数执行实际的内存配置释放重新配置等操作并且能在内存需求不被满足的时候调用一个指定的函数一级空间配置器分配的是大于128字节的空间如果分配不成功调用句柄释放一部分内存如果还不能分配成功抛出异常 第一级配置器只是对malloc函数和free函数的简单封装在allocate内调用malloc在deallocate内调用free同时第一级配置器的oom_malloc函数用来处理malloc失败的情况
  2. 第二级配置器 第一级配置器直接调用malloc和free带来了几个问题 内存分配/释放的效率低 当配置大量的小内存块时会导致内存碎片比较严重 配置内存时需要额外的部分空间存储内存块信息所以配置大量的小内存块时还会导致额外内存负担 如果分配的区块小于128bytes则以内存池管理第二级配置器维护了一个自由链表数组每次需要分配内存时直接从相应的链表上取出一个内存节点就完成工作效率很高 自由链表数组自由链表数组其实就是个指针数组数组中的每个指针元素指向一个链表的起始节点数组大小为16即维护了16个链表链表的每个节点就是实际的内存块相同链表上的内存块大小都相同不同链表的内存块大小不同从8一直到128如下所示obj为链表上的节点free_list就是链表数组 内存分配allocate函数内先判断要分配的内存大小若大于128字节直接调用第一级配置器否则根据要分配的内存大小从16个链表中选出一个链表取出该链表的第一个节点若相应的链表为空则调用refill函数填充该链表默认是取出20个数据块 填充链表 refill若allocate函数内要取出节点的链表为空则会调用refill函数填充该链表refill函数内会先调用chunk_alloc函数从内存池分配一大块内存该内存大小默认为20个链表节点大小当内存池的内存也不足时返回的内存块节点数目会不足20个接着refill的工作就是将这一大块内存分成20份相同大小的内存块并将各内存块连接起来形成一个链表 内存池chunk_alloc函数内管理了一块内存池当refill函数要填充链表时就会调用chunk_alloc函数从内存池取出相应的内存 在chunk_alloc函数内首先判断内存池大小是否足够填充一个有20个节点的链表若内存池足够大则直接返回20个内存节点大小的内存块给refill 若内存池大小无法满足20个内存节点的大小但至少满足1个内存节点则直接返回相应的内存节点大小的内存块给refill 若内存池连1个内存节点大小的内存块都无法提供则chunk_alloc函数会将内存池中那一点点的内存大小分配给其他合适的链表然后去调用malloc函数分配的内存大小为所需的两倍若malloc成功则返回相应的内存大小给refill若malloc失败会先搜寻其他链表的可用的内存块添加到内存池然后递归调用chunk_alloc函数来分配内存若其他链表也无内存块可用则只能调用第一级空间配置器

11正确释放vector的内存(clear(), swap(), shrink_to_fit())

vec.clear()清空内容但是不释放内存 vector().swap(vec)清空内容且释放内存想得到一个全新的vector vec.shrink_to_fit()请求容器降低其capacity和size匹配 vec.clear();vec.shrink_to_fit();清空内容且释放内存

12什么情况下用vector什么情况下用list什么情况下用deque

  1. vector可以随机存储元素即可以通过公式直接计算出元素地址而不需要挨个查找但在非尾部插入删除数据时效率很低适合对象简单对象数量变化不大随机访问频繁除非必要我们尽可能选择使用vector而非deque因为deque的迭代器比vector迭代器复杂很多
  2. list不支持随机存储适用于对象大对象数量变化频繁插入和删除频繁比如写多读少的场景
  3. 需要从首尾两端进行插入或删除操作的时候需要选择deque

13priority_queue的底层原理

priority_queue优先队列其底层是用堆来实现的在优先队列中队首元素一定是当前队列中优先级最高的那一个

14 STL线程不安全的情况

  1. 在对同一个容器进行多线程的读写写操作时
  2. 在每次调用容器的成员函数期间都要锁定该容器
  3. 在每个容器返回的迭代器例如通过调用begin或end的生存期之内都要锁定该容器
  4. 在每个在容器上调用的算法执行期间锁定该容器

推荐阅读 C++ STL容器如何解决线程安全的问题

15vector 中迭代器失效的情况

  1. 当插入push_back一个元素后end操作返回的迭代器肯定失效
  2. 当插入(push_back)一个元素后capacity返回值与没有插入元素之前相比有改变则需要重新加载整个容器此时first和end操作返回的迭代器都会失效
  3. 当进行删除操作erasepop_back指向删除点的迭代器全部失效指向删除点后面的元素的迭代器也将全部失效

16迭代器的几种失效的情况

  1. vectordeque该数据结构分配在连续的内存中当删除一个元素后内存中的数据会发生移动以保证数据的紧凑所以删除一个元素后其他数据的地址发生了变化之前获取的迭代器根据原有信息就访问不到正确的数据 解决方法erase返回下一个有效迭代器的值
  2. mapsetmultisetmapmultimap树形数据结构以红黑树或者平衡二叉树组织数据虽然删除了一个元素整棵树也会调整以符合红黑树或者二叉树的规范但是单个节点在内存中的地址没有变化变化的是各节点之间的指向关系删除一个结点不会对其他结点造成影响 erase迭代器只是被删元素的迭代器失效
  3. list链表型数据结构双向链表使用不连续分配的内存删除运算使指向删除位置的迭代器失效不会使其他迭代器失效

17STL 是复制性还是侵入性

STL实现的容器都是非侵入式容器通过模板类可以放入任何类型的结构与浸入式容器的最大不同是C++的非侵入式容器必须存储用户数据的拷贝 参考 STL容器概述

18vector使用注意事项

  1. 在push_back()resize()insert()后有可能引起重新分配内存此时原始的迭代器失效需要重新生成一次迭代器
  2. 为了防止多次扩容造成性能低可以先使用reserve()预留足够的空间最后在使用 shrink_to_fit()收缩空间
  3. vector不能用来存储bool类型的元素存储机理不同bool类型的元素存储到vector中会转化为1个bit不是1个byte可以用deque或者是bitset存储bool类型的
  4. vector的扩容VS是1.5倍GCC是2倍

第六部分 网络编程

1三次握手和四次挥手

参考 详解 TCP 连接的三次握手四次挥手

2 TCP 和 UDP 的区别

  1. TCP面向连接如打电话要先拨号建立连接;UDP是无连接的即发送数据之前不需要建立连接
  2. TCP提供可靠的服务也就是说通过TCP连接传送的数据无差错不丢失不重复且按序到达;UDP尽最大努力交付即不保 证可靠交付
  3. TCP面向字节流实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制因此网络出现拥塞不会使源主机的发送速率降低对实时应用很有用如IP电话实时视频会议等
  4. 每一条TCP连接只能是点到点的;UDP支持一对一一对多多对一和多对多的交互通信
  5. TCP首部开销20字节;UDP的首部开销小只有8个字节
  6. TCP的逻辑通信信道是全双工的可靠信道UDP则是不可靠信道 详见 TCP和UDP的最完整的区别

3TCP 粘包 拆包

如果应用一次请求发送的数据量比较小没达到缓冲区大小TCP则会将多个请求合并为同一个请求进行发送站在业务上来看这就是粘包; 如果应用一次请求发送的数据量比较大超过了缓冲区大小TCP就会将其拆分为多次发送这就是拆包也就是将一个大的包拆分为多个小包进行发送 常见的解决思路如下

  1. 发送端给每个数据包添加包首部
  2. 发送端将每个数据包封装为固定长度
  3. 可以在数据包之间设置边界 详见 一分钟看懂TCP粘包拆包

4TCP 丢包

服务器给客户端发大量数据Send的频率很高那么就有可能在Send时发生错误原因可能是又多种可能是程序处理逻辑问题多线程同步问题缓冲区溢出问题等等如果没有对Send失败做处理重发数据那么客户端收到的数据就会比理论应该收到的少就会造成丢数据丢包的现象 这种现象其实本质上来说不是丢包也不是丢数据只是因为程序处理有错误导致有些数据没有成功地被socket发送出去 常用的解决方法如下 拆包加包头发送组合包如果客户端服务端掉线常采用心跳测试 详见 TCP通信丢包原因总结

5为什么握手是三次挥手却要四次

TCP建立连接时之所以只需要”三次握手”是因为在第二次”握手”过程中服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的SYN是请求连接标志表示服务器端同意建立连接ACK是确认报文表示告诉客户端服务器端收到了它的请求报文 即SYN建立连接报文与ACK确认接收报文是在同一次”握手”当中传输的所以”三次握手”不多也不少正好让双方明确彼此信息互通 TCP释放连接时之所以需要四次挥手,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次”握手”传输的为何建立连接时一起传输释放连接时却要分开传输

  • 建立连接时被动方服务器端结束CLOSED阶段进入握手阶段并不需要任何准备可以直接返回SYN和ACK报文开始建立连接
  • 释放连接时被动方服务器突然收到主动方客户端释放连接的请求时并不能立即释放连接因为还有必要的数据需要处理所以服务器先返回ACK确认收到报文经过CLOSE-WAIT阶段准备好释放连接之后才能返回FIN释放连接报文

所以是三次握手四次挥手

6为什么握手是三次挥手却要四次

TCP建立连接时之所以只需要”三次握手”是因为在第二次”握手”过程中服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的SYN是请求连接标志表示服务器端同意建立连接ACK是确认报文表示告诉客户端服务器端收到了它的请求报文 即SYN建立连接报文与ACK确认接收报文是在同一次”握手”当中传输的所以”三次握手”不多也不少正好让双方明确彼此信息互通

  • TCP释放连接时之所以需要四次挥手,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次”握手”传输的为何建立连接时一起传输释放连接时却要分开传输 建立连接时被动方服务器端结束CLOSED阶段进入握手阶段并不需要任何准备可以直接返回SYN和ACK报文开始建立连接
  • 释放连接时被动方服务器突然收到主动方客户端释放连接的请求时并不能立即释放连接因为还有必要的数据需要处理所以服务器先返回ACK确认收到报文经过CLOSE-WAIT阶段准备好释放连接之后才能返回FIN释放连接报文

所以是三次握手四次挥手

7网络的七层模型作用传输单位分别是什么

整个模型分为七层物理层数据链路层网络层传输层会话层表示层应用层

详见 OSI七层模型及各层作用

第七部分 设计模式

1设计模式

  1. 创建型模式 对象实例化的模式创建型模式用于解耦对象的实例化过程
  • 单例模式某个类智能有一个实例提供一个全局的访问点
  • 工厂模式一个工厂类根据传入的参量决定创建出哪一种产品类的实例
  • 抽象工厂模式创建相关或依赖对象的家族而无需明确指定具体类
  • 建造者模式封装一个复杂对象的创建过程并可以按步骤构造
  • 原型模式通过复制现有的实例来创建新的实例
  1. 结构型模式 把类或对象结合在一起形成一个更大的结构
  • 装饰器模式动态的给对象添加新的功能
  • 代理模式为其它对象提供一个代理以便控制这个对象的访问
  • 桥接模式将抽象部分和它的实现部分分离使它们都可以独立的变化
  • 适配器模式将一个类的方法接口转换成客户希望的另一个接口
  • 组合模式将对象组合成树形结构以表示部分-整体的层次结构
  • 外观模式对外提供一个统一的方法来访问子系统中的一群接口
  • 享元模式通过共享技术来有效的支持大量细粒度的对象
  1. 行为型模式 类和对象如何交互及划分责任和算法
  • 策略模式定义一系列算法把他们封装起来并且使它们可以相互替换
  • 模板模式定义一个算法结构而将一些步骤延迟到子类实现
  • 命令模式将命令请求封装为一个对象使得可以用不同的请求来进行参数化
  • 迭代器模式一种遍历访问聚合对象中各个元素的方法不暴露该对象的内部结构
  • 观察者模式对象间的一对多的依赖关系
  • 仲裁者模式用一个中介对象来封装一系列的对象交互
  • 备忘录模式在不破坏封装的前提下保持对象的内部状态
  • 解释器模式给定一个语言定义它的文法的一种表示并定义一个解释器
  • 状态模式允许一个对象在其对象内部状态改变时改变它的行为
  • 责任链模式将请求的发送者和接收者解耦使的多个对象都有处理这个请求的机会
  • 访问者模式不改变数据结构的前提下增加作用于一组对象元素的新功能

2单例模式的线程安全问题

饿汉模式的单例模式本身就是现场安全的懒汉模式的线程安全见 C++单例模式与线程安全 推荐阅读 c++的单例模式为什么不直接全部使用static而是非要实例化一个对象

3工厂方法模式

简单工厂模式 简单工厂模式属于类的创建型模式,又叫做静态工厂方法模式通过专门定义一个类来负责创建其他类的实例被创建的实例通常都具有共同的父类简单工厂模式可以减少客户程序对类创建过程的依赖 优点

  • 帮助封装: 实现组件封装面向接口编程
  • 解耦合:客户端和具体实现类的解耦合

缺点

  • 可能增加客户端的复杂度
  • 不方便扩展子工厂

工厂模式 工厂方法模式同样属于类的创建型模式又被称为多态工厂模式 工厂方法模式的意义是 定义一个创建产品对象的工厂接口将实际创建工作推迟到子类当中 核心工厂类不再负责产品的创建这样核心类成为一个抽象工厂角色仅负责具体工厂子类 必须实现的接口这样进一步抽象化的好处是使得工厂方法模式可以使系统在不修改具体工厂角色的情况下引进新的产品 优点

  • 需求改变时改动最小
  • 具体的创建实例过程与客户端分离

缺点

  • 新增功能时工程量稍大

抽象工厂模式 抽象工厂模式是所有形态的工厂模式中最为抽象和最一般性的抽象工厂模式可以向客户端提供一个接口使得客户端在不必指定产品的具体类型的情况下能够创建多个产品族的产品对象

抽象工厂方法是针对与一个产品族使得易于交换产品系列只需改变具体的工厂就可以使用不同的产品配置当一个族中的产品对象被设计成一起工作且一个应用只适用同一族的对象例如设计系统生成不同风格的UI界面按钮边框等UI元素在一起使用并且只能同属于一种风格这很容易使用抽象工厂实现 优点

  • 抽象工厂封装了变化封装了对象创建的具体细节
  • 增加新的产品族很方便无须修改已有系统
  • 针对接口进行编程而不是针对具体进行编程

缺点

  • 增加新的产品等级结构需对原系统做较大修改(违背开放封闭)

详见 C++工厂模式简单工厂工厂方法抽象工厂 详见 C++设计模式三种工厂模式详解简单工厂工厂模式抽象工厂

第八部分 数据结构

1双链表和单链表的优缺点

单向链表单向链表包含两个域一个是信息域一个是指针域也就是单向链表的节点被分成两部分一部分是保存或显示关于节点的信息第二部分存储下一个节点的地址而最后一个节点则指向一个空值 优点单向链表增加删除节点简单遍历时候不会死循环双向也不会死循环循环链表忘了进行控制的话很容易进入死循环缺点只能从头到尾遍历只能找到后继无法找到前驱也就是只能前进

双向链表每个节点有2个链接一个是指向前一个节点当此链接为第一个链接时指向的是空值或空列表另一个则指向后一个节点当此链接为最后一个链接时指向的是空值或空列表意思就是说双向链表有2个指针一个是指向前一个节点的指针另一个则指向后一个节点的指针 优点可以找到前驱和后继可进可退缺点增加删除节点复杂

2红黑树比AVL的优势为何用红黑树

红黑树的查询性能略微逊色于AVL树因为他比avl树会稍微不平衡最多一层也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较但是红黑树在插入和删除上完爆avl树 avl树每次插入删除会进行大量的平衡度计算而红黑树为了维持红黑性质所做的红黑变换和旋转的开销相较于avl树为了维持平衡的开销要小得多

3红黑树的高度

从任何一个节点到其最远的后代叶子的节点数不超过到最近的后代叶子的节点数的两倍 每个具有 n 个节点的红黑树的高度 <= 2Log 2 (n+1) 详见: 红黑树详解 详见: 红黑树 (Red-Black Tree) – 介绍

4判断链表是不是环形链表

  1. 基于循环次数判断的暴力方法
  • 如果一个链表不成环那么它就是可以穷尽的我们假定你的链表的节点的个数小于10^4个如果比这个还多的话也许你需要用到树型结构了不然你的速度会比乌龟还慢因此我们可以一直循环循环的时候要么是因为到达了指针的尾部NULL要么是因为循环次数太多超越了节点的个数循环结束时判断循环次数就可以了
  1. 借助Hash表
  • 每次访问节点的时候都判断是否在hash表中如果没有就加入如果存在就说明有环如果没有环一个节点只可能被访问一次,这和我们企图修改链表的数据结构去维护访问次数是等价的只是以一种不影响节点内容的方式去完成
  1. 快慢指针
  • 定义两种类型的指针slow慢指针前进步长为1快指针前进步长为2在第一次前进后要判断前进后是否到达了NULL这样如果在循环的过程中快指针追上了慢指针那么说明成了环

5简述队列和栈的异同

队列和栈都是线性存储结构但是两者的插入和删除数据的操作不同队列是先进先出栈是 后进先出

  • 注意区别栈区和堆区堆区的存取是顺序随意而栈区是后进先出栈由编译器自动分配释放 存放函数的参数值局部变量的值等其操作方式类似于数据结构中的栈堆一般由程序员分配释放 若程序员不释放程序结束时可能由 OS 回收分配方式类似于链表 它与本题中的堆和栈是两回事堆栈只是一种数据结构而堆区和栈区是程序的不同内存存储区域

整理的原文

c++面试常见问题汇总—建议收藏

C++常见面试题总结

C/C++ 最常见50道面试题

C++核心知识点汇总(面试)

精选 30 个 C++ 面试题含解析

如果你是一个C++面试官你会问哪些问题

k6k4中C/C++面试题

参考阅读

十大经典排序你真的都会了吗源码详解

漫画排序算法 大总结

内存都没了还能运行程序

多个线程为了同个资源打起架来了该如何让他们安分

Linux 运维故障排查思路有这篇文章就够了

万字长文剑指offer全题解思路汇总

我的天第一次知道操作系统的算法还能这么迷人

24 张图彻底弄懂九大常见数据结构

万字长文系统梳理C++函数指针

万字长文图解七道超高频位运算面试题

一口气搞懂文件系统就靠这 25 张图了

看了齐姐这篇文章再也不怕面试问树了

看完这篇操作系统和面试官扯皮就没问题了

史上最全的数据库面试题面试必刷

70道C语言与C++常见问答题

c++面试

C++面试题1

万字详解我今年经历的腾讯Linux C++ 笔试/面试题及答案

C++开发面试之——C++11新特性20问

腾讯 C++ 笔试/面试题及答案

C++面试——内存管理堆栈指针50问

C语言 / C++基础面试知识大集合

50 家公司的 C++ 面经总结硬核分享

一文让你搞懂设计模式

这里有60篇硬核文章

超硬核 | 2 万字+20 图带你手撕 STL 容器源码

熬夜整理的万字C/C++总结值得收藏

现代 C++ 教程高速上手 C++ 11/14/17/20

c++后端开发 资料整理学习

欢迎指出错误遗漏的题尽量做到面试的时候复习一文就够

作者红尘氵梦
链接https://www.nowcoder.com/discuss/454697528508870656
来源牛客网