Cpp高频面经

new和malloc的区别

  • 属性的区别

    new/delete:这两个是C++中的关键字;

    malloc/free:这两个是库函数;

  • 使用上的区别

    malloc:申请空间需要显式填入申请内存的大小;

    new:无需显式填入申请内存的大小,new会根据new的类型分配内存;

  • 返回类型的区别

    new操作符内存分配成功,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,故new是符合类型安全性的操作符。

    malloc内存分配成功返回的是void*指针,需要通过强制类型转换,转换成我们需要的类型。

    所以C++中new比malloc安全可靠。

  • 分配失败的区别

    malloc分配失败会返回NULL,我们可以通过判断返回值是否是NULL得知是否分配成功。

    new分配失败会抛出bad_alloc异常。

  • 扩张内存的区别

    malloc有内存扩张机制(通过realloc实现)。

    new没有扩张内存机制。

https://zhuanlan.zhihu.com/p/338489910

如何避免内存泄漏

  • 明确动态内存使用范围:在程序中使用动态内存时,需要明确该内存的使用范围,确保在不需要使用该内存时能够及时释放内存。
  • 使用RAII技术:RAII(Resource Acquisition Is Initialization)是C++中一种常用的资源管理技术,它利用了C++对象的构造函数和析构函数自动调用的特性,在对象的构造函数中申请资源,在对象的析构函数中释放资源,从而避免资源泄漏问题。
  • 使用智能指针:智能指针可以自动管理动态内存的分配和释放。

线程池的数量一般怎么设置

线程池中线程数量的设置主要考虑两个方面:

  • I/O密集型

  • 如果说任务是耗时I/O型,比如涉及数据库、文件的读写,网络通信等任务,这种任务的特点是不会特别消耗CPU资源,但是我们需要考虑到I/O操作耗时较长。这种情况一般会将线程数设置的比较大,达到了CPU核心数的很多倍。因为如果线程数设置的比较少,会造成CPU计算资源的浪费。

  • CPU密集型

  • 对于CPU密集型任务,线程数不宜设置的过多,因为过多的线程都会去抢占CPU资源,就会产生不必要的上下文切换,反而会造成整体性能的下降

线程数通用计算公示:线程数 = CPU核心数 * (1 + I/O耗时 / CPU耗时)

来了一个新任务,线程池是怎么工作的(本答案有待商榷)

当有一个新任务到来时,线程池会先判断是否有空闲线程,如果有,则将任务分配给空闲线程;

如果此时线程池里没有空线程,则先将任务放任务任务队列,待有空闲线程之后,再从任务队列中取出任务。

完美转发

在我的理解里,完美转发 = std::forward + 万能引用 + 引用折叠。首先引用折叠机制为T&&类型的万能引用中的模板参数T赋予了一个恰到好处的值,而我们用T去指明std::forward的模板参数,从而使得std::forward返回的是正确的类型(这里关于返回的是正确的类型,应该去看std::forward实现的源码,知乎链接如下:https://zhuanlan.zhihu.com/p/369203981)

去掉std::forward会咋样

可能会导致在传递参数的时候丢失类型信息,从而导致编译器无法正确推断模板类型或者在模板类型推断中发生错误。

讲一下C++中的虚继承

基类的析构函数为什么是虚函数?

如果基类的虚构函数不是虚函数,当我们定义一个父类指针指向子类对象时,最后子类的析构函数不会调用,导致内存泄漏。

struct和union的区别(默写)

C++内存布局/程序分段(默写)

了解shared_ptr吗?如果让你手写一个shared_ptr,你会怎么设计

shared_ptr是智能指针里面的共享指针,即多个指针指向同一个内存。每多一个指针指向这片内存,引用计数加1。当对象的引用计数减少为0时,对象会自动析构,对应内存被自动释放。智能指针是模板类,而不是指针。

设计:将shared_ptr定义为一个模板类,包括两个成员:模板类指针和一个指向引用计数的指针。指向引用计数类型的指针应该包括这些成员函数:增加计数、减少计数、返回现有计数。一个私有成员就是用于计数的变量。共享指针模板类的构造函数接受一个模板类型指针,并且需要声明为explicit,表示必须直接初始化。另外就是拷贝构造函数和移动构造函数。拷贝构造需要注意拷贝的对象的指针是否为nullptr,如果不是nullptr,则需要增加引用计数。移动构造函数需要注意将传入的右值引用对象的指针置空,引用计数清零。析构函数有两个判断条件,第一个最后一个指向对象的指针需要为非nullptr并且此时引用计数减一后为0。然后就是重载*、->、bool,还有一个函数用于获得指针get()。动态转换(可考虑)。

介绍一下TCP,TCP粘包如何解决

解决办法:

  • 发送端:使用TCP_NODELAY关闭Nagle算法,但是如果不是时延敏感的应用尽量不要关闭
  • 接收端:没法解决,只能交给应用端解决
  • 应用层:有三种解决办法
    • 只发送固定包长的数据包,但是这个方法基本不用,灵活性太差
    • 指定标识结尾,比如
    • 包头加包体,包头一般是固定长度,并且里面有一个字段可以告知我们接下来的包体有多大

回调函数是什么,回调函数的本质

回调函数允许我们将一个函数(或函数对象)作为参数传递给另一个函数,并在需要的时候由后者调用执行。回调函数定义了在特定事件或条件满足时应该执行的操作。主调函数是接受回调函数作为参数的函数。

进程和线程的区别

  1. 进程
    • 进程拥有独立内存空间和系统资源
    • 进程之间相互独立,一个进程的崩溃通常不会影响其他进程
    • 创建、销毁和切换进程开销比较大
  2. 线程
    • 一个进程可以包含多个线程,所有线程共享相同的地址空间和系统资源
    • 线程之间可以直接读写进程内的共享数据,执行起来更高效
    • 线程的创建、销毁和切换开销比较小

GET和POST的区别

GET用于获取资源,参数通过URL传递,不适合传输敏感信息,幂等,有缓存,传输数据的大小受限于URL的长度。POST用于提交数据,参数通过请求体传递,适合传输敏感信息,不幂等,无缓存,没有数据大小限制。

HTTP和HTTPS

  1. HTTP
    • HTTP是一种应用层协议,用于在Web浏览器和Web服务器之间传输超文本和其他资源
    • HTTP是明文传输的协议,意味着数据在传输过程中是未加密的,容易被窃听和篡改
    • HTTP默认使用80端口号
  2. HTTPS
    • HTTPS是HTTP协议的安全版本,加强了数据传输的安全性和保密性
    • HTTPS使用了SSL/TLS协议进行数据加密和身份认证
    • HTTPS默认使用443端口通信

C++ 11新特性

  • auto关键字

  • 智能指针

  • lambda表达式 参考链接(https://blog.51cto.com/u_15323899/5785594)

    Lambda表达式是C++11引入的一种新特性,它允许在代码中定义匿名函数。虽然在使用上非常简洁和方便,但lambda表达式背后的实现相对复杂。以下是lambda表达式在底层的工作原理:

    1. 转换为类:
      • 当你定义一个lambda表达式,编译器会为你生成一个匿名类(也称为闭包类型)。这个类将会覆盖函数调用操作符,使得该对象可以像函数一样被调用。
      • 如果lambda表达式捕获了外部的局部变量(例如通过值或引用),这些变量将会被添加为该匿名类的成员。
    2. 成员变量:
      • 为了支持捕获,生成的闭包类型可能会包含成员变量。如果使用值捕获,那么这些成员变量将存储捕获的变量的副本;如果使用引用捕获,那么成员变量将存储相应变量的引用。
    3. 函数调用操作符重载:
      • 生成的类会覆盖函数调用操作符operator(). 这个操作符的实现就是lambda表达式的主体。
    4. 构造函数:
      • 该匿名类的构造函数会初始化所有捕获的变量。根据捕获方式(值或引用)来复制或绑定这些变量。
    5. 生成的类是只移动构造的:
      • 这意味着你不能按常规方式复制lambda表达式,但可以移动它。

    下面是一个简单的lambda表达式的例子以及一个可能的简化版本的匿名类表示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Lambda
    auto lambda = [x](int y) { return x + y; };

    // Possible representation by the compiler
    class __anonymous {
    private:
    int x;
    public:
    __anonymous(int _x) : x(_x) {}
    int operator()(int y) const {
    return x + y;
    }
    };

    要注意的是,这只是一个简化的表示。实际编译器生成的代码会比这更复杂,尤其是当涉及到更高级的特性(如泛型、mutable关键字或捕获列表)时。

    最后,虽然从概念上讲,lambda表达式是转换为类的,但这并不意味着性能会受到影响。优化后的编译器通常会内联这些生成的类和函数调用操作符,从而消除由于间接调用导致的任何额外开销。

  • 右值引用和移动语义

lambda表达式的使用场景

lambda表达式提供了一种简洁、方便的方式来创建匿名对象。在一些需要传递简单函数对象的场景下,使用lambda表达式可以避免额外的函数对象类。

vector迭代器失效的原因

vector底层的实现是一个动态数组,vector里面存储的元素都是连续的,一旦比如删除一个元素,后面的所有元素都需要移动。我们可以考虑一种极端情况,删除最后一个元素,此时指向原vector数组的最后一个元素的迭代器就没指向任何元素了,如何此时我们访问这个迭代器所指向的元素,就会导致未定义行为,所以就会判定迭代器失效。其实所有改变vector大小的操作,都会导致vector迭代器失效。

map的底层数据结构

红黑树参考链接:https://www.jianshu.com/p/e136ec79235c

map是有序容器,底层数据结构是红黑树,时间复杂度为log(n)。红黑树的前身可以说是二叉搜索树。但是二叉搜索树最坏的情况下树的高度为n,那么就导致时间复杂度为o(n), 所以便衍生出来了平衡二叉树。其实红黑树的五大特性就是为了保持二叉搜索树的平衡。保证时间复杂度稳定在o(logn)。

unordered_map底层数据结构

unordered_map是无序容器,底层使用哈希表实现的。

TCP握手为什么是三次握手,两次握手为什么不行呢

这里有两个大点,第一个字面意思很好理解,第二点举个例子就通透了,面试时最好全部答上来

  • 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤

  • 防止已失效的连接请求又传送到服务器端,因而产生错误

    只有采用三次握手可以减少服务端的资源浪费。解释如下: 例如,客户端向服务端发送请求同步报文A,因为网络阻塞等原因,服务端没有收到同步报文A,所以没有发送同步确认报文。过了一段时间,客户端没有收到服务端的确认报文,重新向服务端发送请求同步报文B,服务端接收到报文B后,向客户端发送同步确认报文,客户端接收到确认报文后,向服务端发送确认报文,建立连接。数据传输完毕后,连接断开。客户端进入close状态,此时服务端收到之前的报文A,向客户端返回同步确认报文。如果使用两次握手,服务端回应后不确认客户端的状态,连接建立成功。服务端会长时间等待客户端发送数据,连接长期保持,会造成资源浪费。当多个客户端产生这种情况,服务器就会等待多个客户端的响应,连接数量过多,之后的客户端请求,服务器无法响应。造成服务器处于瘫痪状态。 只有使用三次握手,当服务端收到确认报文后,保证当前时刻,客户端可以发送数据时,才能建立有意义的连接。当客户端一段时间不发送数据时,服务器应自动断开连接,来节省自身连接的客户端数量,减少资源的浪费。

HTTP状态码

HTTP状态码是在HTTP协议中用于表示服务器对请求的处理结果的三位数字代码。HTTP状态码的分类如下:

  1. 1xx(信息性状态码):表示服务器已接收请求,需要客户端继续操作。

  2. 2xx(成功状态码):表示服务器成功处理了请求。

  3. 3xx(重定向状态码):表示请求需要进一步的操作,通常用于重定向。

  4. 4xx(客户端错误状态码):表示客户端发出的请求有误。

  5. 5xx(服务器错误状态码):表示服务器在处理请求时发生了错误。

以下是一些常见的HTTP状态码示例:

  1. 200 OK:请求成功,服务器成功处理了请求。

  2. 201 Created:请求成功,服务器已成功创建了资源。

  3. 204 No Content:请求成功,但服务器没有新的信息返回。

  4. 400 Bad Request:请求错误,服务器不理解或无法处理请求。

  5. 401 Unauthorized:请求需要用户认证,未提供有效的认证信息。

  6. 403 Forbidden:请求被服务器拒绝,没有访问权限。

  7. 404 Not Found:请求的资源不存在。

  8. 500 Internal Server Error:服务器内部错误,无法完成请求。

  9. 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效的响应。

  10. 503 Service Unavailable:服务器暂时不可用,通常是由于过载或维护。

这只是HTTP状态码中的一部分,实际中可能还会遇到其他状态码。状态码的具体含义可参考HTTP协议的官方文档或RFC文档。不同的状态码代表了服务器对请求的不同响应结果,客户端根据状态码可以做出相应的处理。

使用智能指针了,普通指针还能用吗?

个人觉得使用了智能指针后不推荐使用普通指针。因为使用了智能指针后,再使用普通指针指向相同对象,并不会增加引用计数,等智能指针引用计数变为0的时候,对象被销毁,此时普通指针就变为了野指针。

什么时候使用智能指针,什么时候使用普通指针 (CHATGPT)

使用智能指针还是普通指针取决于你的代码需求和内存管理需求。以下是一些指导原则:

使用智能指针的情况:

  1. 动态分配内存:如果你需要动态分配内存,尤其是在使用new关键字时,最好使用智能指针来管理这些资源。智能指针会自动在不再需要时释放内存,避免了内存泄漏的问题。

  2. 所有权传递:当多个地方共享一个资源并且需要明确的所有权传递时,智能指针特别有用。例如,使用std::shared_ptr来共享资源,并在最后一个引用释放时自动删除资源。

  3. 异常安全:在使用普通指针的代码中,如果在动态分配资源后发生异常,可能会导致资源泄漏。使用智能指针可以更好地处理异常情况,确保资源在退出作用域时得到正确释放。

  4. 容器存储:如果要在STL容器中存储指针类型,建议使用智能指针。STL容器在元素添加或删除时可能会复制或移动元素,使用智能指针可以正确管理资源的所有权。

使用普通指针的情况:

  1. 接口和传统API:某些接口和传统的C API可能要求使用原始指针,此时你无法使用智能指针。在这种情况下,你应该小心管理内存,确保手动释放资源。

  2. 与C代码交互:当与C语言库或代码进行交互时,普通指针是常用的方式,因为C语言不支持智能指针。

  3. 临时指针:对于一些临时指针,其生命周期很短且不需要动态内存管理时,可以使用普通指针。

  4. 最低开销:在某些性能关键的场景,智能指针可能带来额外的开销,此时使用普通指针可能更合适。但请注意,在性能优化前确保使用智能指针没有影响到代码的正确性和可维护性。

综上所述,智能指针在大多数情况下是推荐的内存管理方式,特别是在现代C++代码中。然而,有些特定的场景可能需要使用普通指针,但在这种情况下你需要格外小心以避免内存泄漏和悬挂指针问题。

Hash_map原理

Hash_map基于Hash_map(哈希表)。

哈希表基本原理:使用一个下标范围较大的数组来储存元素。那我们怎么根据关键字知道它应该放在数组的哪个位置呢?这就通过哈希函数(散列函数)来解决。哈希函数使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应。但是,不能保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这就产生了哈希冲突。因此“直接定址”和“解决冲突”是哈希表的两大特点。

哈希冲突的解决主要有以下四种方法:开放地址法,再哈希法,链地址法和建立公共溢出区。关于这四种方法的解释见知乎链接 https://zhuanlan.zhihu.com/p/29520044

C++多态介绍一下

C++多态主要包括重载、虚函数、模板。重载包括函数重载和运算符重载,编译期,即编译器在编译阶段就会根据函数调用的上下文来决定使用哪一个重载版本。虚函数是在运行期。重载和模板属于静态多态,虚函数属于动态多态。静态多态与动态多态靠编译期与运行期区分。

虚函数、纯虚函数原理,虚表什么时候建立的 https://zhuanlan.zhihu.com/p/37331092

  • 虚函数,在类成员方法的声明(不是定义)语句前加“virtual”,如virtual void func
  • 纯虚函数,在虚函数后加“=0”,如virtual void func = 0
  • 对于虚函数,子类可以(也可以不)重写基类的虚函数,该行为称之为override
  • 对于纯虚函数,子类必须提供纯虚函数的个性化实现

在派生子类中对虚函数和纯虚函数的个性化实现,都体现了多态特性,但区别在于:

  • 子类如果不提供虚函数的实现,将会自动调用基类的缺省虚函数实现
  • 子类如果不提供纯虚函数的实现,将会编译失败

当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数则由指针指向的实际类型决定

实现虚函数表的关键就是虚函数表指针,这个指针指向一张名为虚函数表的表,为表中的数据则为函数指针,存储了虚函数具体实现所对应的位置。另外,当一个类有多个虚函数时,仍然只有一个虚函数指针,而此时的虚函数表里会有多个函数指针,因此,虚函数实现的过程是:通过对象内存中的虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数指针找到对应虚函数的实现区域并进行调用。所以虚函数的调用时由指针所指向内存块的具体类型决定的。

构造函数和析构函数可以是虚函数吗?

答案是:构造函数不能是虚函数,析构函数可以是虚函数且推荐最好设置为虚函数。

首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,也即还没有进行初始化,此时vptr是没有值的,也就无法通过vptr找到作为构造函数和虚函数所在的代码区,所以构造函数只能以普通函数的形式存放在类所指定的代码区中。

而对于析构函数,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,此时我们当然希望可以调用派生类的析构函数对新定义的成员也进行析构。

map为啥用红黑树不用avl树?

  • 平衡调整次数更少
  • 内存使用更少:AVL树需要存储额外的平衡因子信息
  • 更适合于频繁的插入和删除操作

怎么判断map里key值存不存在

c.at(k) 访问关键字为k的元素,如果k不在c中,返回一个out_of_range异常

c.find(k) 如果存在,返回一个迭代器,指向关键字为k的元素;如果不存在,返回尾后迭代器

c.count(k) 不存在返回0,存在返回k关键字的数量

多进程通信方式

管道、命名管道、消息队列、信号量、共享内存、套接字、RPC

管道:一种最简单的进程间通信方式,通常用于父子进程间通信。管道中数据只能朝一个方向流动,即一方读另一方写。管道通过系统调用pipe()创建。

命名管道:一种更通用的进程间通信方式,它可以在无关的进程之间进行通信。不同于管道,命名管道通过文件系统中的路径名来标识。命名管道可以通过系统调用 mkfifo() 创建。

信号量:讲好sem_init(), sem_wait(), sem_post()系统整体运作过程就好了

套接字:服务器监听一个ip+端口,客户端访问连接

TCP和UDP的区别

TCP UDP
可靠性 可靠 不可靠
连接 面向连接 无连接
数据传输方式 字节流 数据报
双工性 全双工 一对一、一对多、多对一、多对多
流量控制 滑动窗口
拥塞控制 慢启动、拥塞避免、快速重传、快速回复
效率
传输速度

TCP/IP协议组

IP协议、TCP协议、UDP协议、ICMP协议、ARP协议、RARP协议

OSI七层

物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

socket编程recv函数

返回0:对方关闭连接

返回相应的接收数据大小

返回错误码:传输出现错误

排序算法

快排思想:选定基准元素,小的放一边,大的放一边,最后分治

HTTP状态码

  1. 1XX - 信息状态码:
    • 100 Continue:服务端已经收到了客户端请求,继续发送剩余部分
  2. 2XX - 成功状态码:
    • 200 OK:请求成功,服务器成功处理了请求
    • 201 Created:请求成功,并创建了资源
    • 204 No Content:请求成功,但没有返回内容
  3. 3XX - 重定向状态码:
    • 301 Moved Permanently:永久重定向,请求的资源被永久移到了新位置
    • 302 Found:临时重定向,请求的资源被临时移到了新位置
    • 304 Not Modified:客户端缓存资源仍然有效,未修改
  4. 4XX - 客户端错误状态码:
    • 400 Bad Request:客户端请求错误,服务器无法理解
    • 401 Unauthorized:请求要求身份验证,客户端未提供有效的身份信息
    • 403 Forbidden:服务器拒绝请求,没有访问权限
    • 404 Not Found:请求的资源不存在
  5. 5XX - 服务器错误状态码:
    • 500 Internet Server Error:服务器内部错误,无法完成请求
    • 502 Bad Gateway:作为网关或代理的服务器从上游服务器收到无效响应
    • 503 Service Unavailable:服务器暂时过载或维护中,无法处理请求
    • 504 Gateway Timeout:作为网关或代理服务器未及时从上游服务器接收响应

DNS解析过程

DNS就是域名解析服务,查询过程依次递增。本地域名解析(操作系统首先会查询本地DNS缓存) —–》本地域名服务器查询 —–》根域名服务器查询 —–》顶级域名服务器查询 —–》权威域名服务器查询 —–》返回结果。DNS查询和应答报文具有相同格式,主要区别在于某些字段的标识。下面讲几个重要的字段。

  • 16位标识:用于标记一对DNS查询和应答,以此区分一个DNS应答是哪一个DNS查询的回应
  • 16位标志
    • QR:查询报文(0)/ 应答报文(1)
    • opcode:标准查询(0)/ 反向查询(1)就是是通过域名获取ip地址还是通过ip地址获取域名

TCP为什么需要四次挥手,三次挥手有什么问题

  1. 客户端最后一个ACK可能会丢失,这样服务端就无法正常进入CLOSED状态。于是B会重传请求释放的报文,而此时如果A已经关闭了,那就收不到B的重传请求,就会导致B无法正常释放。而如果A还在等待时间内,就会收到B的重传,然后进行应答,这样B就可以进入CLOSED状态
  2. 如果三次挥手的话,服务端收到来自客户端的FIN请求后,需要同时回复ACK和发送FIN断开连接请求。但是在TCP连接中是有一个半关闭状态的,也就是服务端其实还是可以继续发送数据的,如果三次挥手,就无法做到服务器再发送数据了。

TCP传输为什么是可靠的

  • 确认与重传:如果一定时间内未收到ACK,则会重传
  • 序列号与顺序性:TCP为每一个数据包分配一个序列号,接收端会进行数据包重组
  • 流量控制
  • 拥塞控制
  • 连接管理:三次握手与四次挥手
  • 超时与重试

线程的状态

新建、就绪、运行、阻塞、等待、终止

死锁产生的条件

死锁发生的必要条件,通常被称为死锁的四个条件,分别是:

  1. 互斥条件(Mutual Exclusion): 指某个资源在一段时间内只能被一个线程或进程占用,其他线程或进程需要等待资源释放才能继续执行。
  2. 请求与保持条件(Hold and Wait): 指线程在保持至少一个资源的同时,还请求其他资源,而这些资源可能被其他线程占用,导致请求阻塞。
  3. 不剥夺条件(No Preemption): 指资源只能由占有它的线程显式释放,其他线程不能强行抢占资源。
  4. 循环等待条件(Circular Wait): 指多个线程形成一个循环,每个线程都在等待下一个线程所持有的资源,导致一个闭环的等待状态。

死锁解决方案

  • 系统重新启动
  • 撤销进程、剥夺资源
  • 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处。

树的知识

https://oi-wiki.org/ds/bplus-tree/

Linux API调用返回值汇总

int pthread_create:成功时返回0,失败时返回错误码

void pthread_exit:不会失败

int pthread_join ( pthread_t thread, void** retval):成功时返回0,失败时返回错误码。错误码:EDEADLK:可能引起死锁,比如两个线程互相针对对方调用pthread_join,或者线程对自身调用pthread_join;EINVAL:目标线程是不可回收的,或者已经有其他线程在回收该目标线程;ESRCH:目标线程不存在。

int sem_init( sem_t* sem, int pshared, unsigned int value )

int sem_destroy( sem_t* sem )

int sem_wait( sem_t* sem)

int sem_trywait( sem_t* sem )

int sem_post( sem_t* sem ):以上五个成功时返回0, 失败时返回-1并设置errno

DNS的解析过程

DNS是实现域名和IP地址相互映射的一个分布式数据库。DNS解析过程主要包括以下步骤

  1. 本地缓存查询:当用户通过浏览器访问某域名时,浏览器会首先在自己的缓存里查询是否有该域名对应的IP地址。
  2. 本地系统查询:查看本计算机系统Host文件DNS缓存是否有对应DNS缓存
  3. 查看路由器缓存
  4. 查询ISP DNS缓存:也称本地域名服务器查询,计算机会向你的ISP(互联网服务提供商)分配的本地域名服务器发出查询请求
  5. 根域名服务器
  6. 顶级域名服务器:根域名服务器并不会直接返回用户IP地址,而是会指向查询请求对应的顶级域名服务器
  7. 权威域名服务器:顶级域名服务器会指向域名的权威域名服务器,这些服务器管理特定的域名的DNS服务。

TCP传输为什么是可靠的

总结下来主要包括三个方面:

  • 检验和
  • 确认应答(ACK和序列号一应一答)
  • 超时重传

NAT协议和跨域(不了解)

GET和POST区别

最明显的一个区别就是GET请求会把参数放在URL(统一资源定位符)中,POST会把参数放在请求体中。

数据大小限制:GET请求把参数放在URL中,GET请求数据大小收到URL长度限制。POST请求则不会

幂等性:GET请求每次相同请求返回的结果都一样,多次重复请求不会对资源产生影响。POST请求则相反

安全性:POST更安全,参数没有暴露在URL上

缓存:GET请求可以被浏览器缓存,POST请求不会被浏览器缓存

写一段死锁的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int a = 0, b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void* another(void* arg){
pthread_mutex_lock(&mutex_a);
printf("in child thread, get mutex a\n");
sleep(5);
a++;
pthread_mutex_lock(&mutex_b);
b += a;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
pthread_exit(NULL);
}

int main(){
pthread_t id;

pthread_mutex_init(&mutex_a, NULL);
pthread_mutex_init(&mutex_b, NULL);
pthread_create(&id, NULL, another, NULL);

pthread_mutex_lock(&mutex_b);
printf("in parent thread, get mutex b\n");
sleep(5);
++b;
pthread_mutex_lock(&mutex_a);
a += b;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);

pthread_join(id, NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);

printf("pro is end\n");

return 0;
}

删除字符串头尾空格代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>

int main(){
std::string s;
std::string tmp;
getline(std::cin, s); //这里很重要哦,如果用cin,“hello world”会被截断成hello

for(int i = 0; i < s.size(); ++i){
if(s[i] == ' '){
continue;
}

s = s.substr(i, s.size() - i);
break;
}

for(int i = s.size() - 1; i >= 0; --i){
if(s[i] == ' '){
continue;
}

s = s.substr(0, i + 1);
break;
}

std::cout << s << std::endl;

return 0;
}

shared_ptr是线程安全的吗?https://cloud.tencent.com/developer/article/1654442

  • 多线程同时读一个shared_ptr对象是安全的
  • 多线程同时对一个shared_ptr对象进行读和写是不安全的,需要加锁

这里给出一个伪代码的简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shared_ptr<Foo> g(new Foo1);   // 线程之间共享的shared_ptr
shared_ptr<Foo> x; // 线程A的局部变量
shared_ptr<Foo> n(new Foo2); // 线程B的局部变量
---------------------------------------------------------
线程A
x = g;
---------------------------------------------------------
线程B
g = n;
---------------------------------------------------------
测试场景:

线程A
智能指针x 读取Foo1,然后还重置Foo1计数。

线程 B:
销毁了Foo1
线程A
重置计数时,foo1已经被销毁。

mysql读写锁怎么实现 mysql 读写锁和互斥锁的区别 https://blog.51cto.com/u_16099299/7031260

  • 请你讲述一下互斥锁机制,以及互斥锁和读写锁的区别
    • 互斥锁(mutex),用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
    • 读写锁(rwlock),分为读锁和写锁。处于读操作的时候,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其他获得写锁失败的线程将会进入睡眠状态,直到写锁释放时被唤醒。注意;写锁会阻塞其他读写锁。当有一个线程获得写锁在写时,读锁也不能被其他线程获取;写者优于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。 读写锁:区分读者和写者,而互斥锁不区分,互斥锁只允许同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
  • Linux中的四种锁机制
    • 互斥锁:在同一时间内只允许一个线程访问对象
    • 读写锁:同一时间内允许多个读者同时读对象,同一时间内只允许一个写者访问对象,且当有一个线程获得写操作的时候,读锁也不能被其他的线程获取,写者优于读者,唤醒时先唤醒写者。
    • 自旋锁:在任何时刻内自能有一个线程访问资源,但是当获取锁操作失败时,不会进入睡眠状态而是会原地自旋,直到锁被释放,这样减少了线程从睡眠状态到被唤醒状态的资源消耗,在加锁时间短暂的情况下使用会提高效率。但是加锁时间过长会非常浪费CPU
    • RCU(read-copy-update):在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改,然后在将老数据update成新数据。在RCU的时候读者几乎不需要同步开销,即不需要获取锁,也不适用原子指令,不会导致竞争因此不用考虑死锁问题了。但是对于写者的同步开销比较大,他需要复制被修改的数据,还必须使用锁机制同步并行其他写者的改操作,在有大量读操作,少量写操作的时候使用。

delete关键字的作用

  1. 删除特殊的成员函数:在C++11中可以使用delete来显示的阻止编译器自动生成某些特殊的成员函数。通过在类的声明中将这些特殊函数标记为delete,可以防止这些函数隐式的生成。

    1
    2
    3
    4
    5
    6
    class MyClass {
    public:
    MyClass() = delete; // 阻止生成默认构造函数
    MyClass(const MyClass&) = delete; // 阻止生成拷贝构造函数
    // ...
    };
  2. 删除特定的函数重载:在函数重载时,可以使用delete关键字来标记某个特定的函数重载,防止特定的重载函数被调用。

    1
    2
    void someFunction(int x);
    void someFunction(double x) = delete; // 阻止调用带有 double 参数的函数

define和const的区别

  • 就起作用的阶段而言:#define是在编译的预处理阶段起作用,而const是在编译、运行时起作用;
  • 就起作用的方式而言:#define只是简单的字符串替换,没有类型检查,而const有类型检查,避免相应的错误;
  • 就存储方式而言:#define只是进行展开,有多少地方使用,就有多少替换。const定义的只读变量在程序运行过程中只有一份备份;

写时拷贝(COW) https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

在linux里,fork()会产生一个子进程,子进程与父进程用的是相同的物理空间,也就是说两者的虚拟空间不同,但对应的物理空间是一个。当父/子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。这里的相应段你能理解不?其实就是一个程序一般分为哪些段,主要可以分为四大段:栈、堆、数据段、代码段;再细分的话可以加一个bss段,也就是用于存放程序中未初始化的全局变量的一块内存区域。

还有一个细节问题就是,fork之后内核会将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,造成不必要的复制,导致效率的下降。可以这么理解,可能子进程执行的代码并不需要写,完全可以使用原共享段,而父进程反而会改变(fork之后),如果父进程先执行,则子进程就要COW。

coredump,gdb怎么定位

整体流程如下:

1
g++ -g source.cpp -o source
1
ulimit -c unlimited
1
./source
1
gdb ./source ./core
1
where(在gdb下输入)

epoll

关于epoll问题在这暂且不多写,只需记住内核事件表,selectpoll都是轮询,事件复杂度:epoll(o1),其他(on)

selectpoll必须遍历内核事件表上所有已注册的文件描述符以找到其中的就绪者;epoll仅遍历就绪的文件描述符。

static的作用

  1. 在函数体内,被声明为静态的变量在这一函数被调用过程中维持其值不变。

    这句话其实是想说如果在函数内部声明了一个静态变量,那么这个变量在函数调用期间不会被重新初始化。它的生命周期是从程序开始到程序结束,但其作用域仍然限制在该函数内。这里给一段示例代码最好理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>

    void count(){
    static int num = 0;
    num++;
    std::cout << "num is: " << num << std::endl;
    }

    int main(){
    count(); // num = 1
    count(); // num = 2 正常情况下,如果是非static,num会被重新置0,但这里并没有体现出来。
    count(); // num = 3
    return 0;
    }
  2. 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。

    这句话就是表面意思,很好理解,这里给一个代码帮助理解

    moduleA.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>

    static int secretValue = 42;

    void printSecretValue(){
    std::cout << secretValue << std::endl;
    }

    void modifySecretValue(int value){
    secretValue = value;
    }

    moudleB.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    extern void printSecretValue();
    extern void modifySecretValue(int value);

    int main(){
    printSecretValue();
    modifySecretValue(100);

    // std::cout << secretValue << std::endl;
    // 记住,这句话是会导致编译错误的,模块B不能直接访问模块A的变量。
    }
  3. 在模块内,一个被声明为静态的函数只可被本模块内的其他函数调用。也就是,这个函数被限制在声明它的模块的本地范围内使用。

    这里的意思其实就和第二点差不多了,也给一个简单示例代码吧

    moduleA.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>

    // 这是一个静态函数,只在moduleA.cpp中可见
    static void secretFunction() {
    std::cout << "Inside secret function of moduleA!" << std::endl;
    }

    // 公开的函数,可以被其他模块调用
    void publicFunction() {
    std::cout << "Inside public function of moduleA." << std::endl;
    secretFunction();
    }

    moduleB.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>

    extern void publicFunction();

    int main() {
    publicFunction(); // 可以调用这个函数

    // 不能调用 secretFunction,因为它在moduleA.cpp中是静态的
    // secretFunction(); // 这一行会导致编译错误

    return 0;
    }

    在上面的示例中,moduleA.cpp定义了一个静态函数secretFunction。在moduleB.cpp中,我们可以调用publicFunction,但当我们尝试调用secretFunction时,会导致编译错误,因为secretFunction只在moduleA.cpp中可见。

有关static的必要补充

  • 修饰成员变量

    用static修饰类的数据成员,使其成为类的全局变量,会被类的所有对象共享,包括派生类对象。所有对象只维持同一个实例。因此类的static成员必须进行类外初始化,而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。因为const修饰的变量无法修改。

  • 修饰成员函数

    用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问static成员变量。注意:正是因为static修饰的成员函数不含this指针,而const修饰的成员函数会添加一个隐藏的参数const this*,所以这两种语义是矛盾的,所以在成员函数上不能同时用他们。

  • this指针理解

    对象就是大房子,成员就是房子里的物件,this是一个指着房子的指针,如果要获取对象的成员也就是大房子内的物件,需要使用this指针加->这个符号。

三种智能指针

智能指针是在栈中的一个类,用于管理堆上分配的内存。传统的C/C++对于堆上内存的开辟释放,需要程序手动管理,而智能指针是一个类,有构造函数和析构函数,在超出作用范围后,程序会自动调用析构函数释放其管理的指针指向的内存,不需要手动释放。

  • shared_ptr 共享智能指针,多个智能指针可以指向同一个对象,对象的资源在最后一个指针销毁时释放,通过引用计数来判断是否是最后一个智能指针。
  • unique_ptr 独占智能指针,同一时刻只有一个智能指针可以指向该对象,如果要安全重用该指针,标准库函数std::move()可以将unique_ptr赋值给另一个unique_ptr。
  • weak_ptr 弱智能指针,不会增加shared_ptr的引用计数,可以避免两个shared_ptr相互引用的死锁问题。weak_ptr只能用shared_ptr或者另一个weak_ptr构造,通过lock()方法weak_ptr可以转化为shared_ptr。

下面给出代码示例

unique_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <memory>
#include <string.h>

class test{
public:
std::string str;
~test(){ std::cout << "test::~test()" << std::endl; }
};

int main(){
std::unique_ptr<test> p1(new test());
p1->str = "test string";
std::cout << "p1->str: " << p1->str << std::endl;
std::unique_ptr<test> p2;
p2 = std::move(p1);
// std::cout << p1->str << std::endl; p1此时已经访问不到资源了
std::cout << "p2->str: " << p2->str << std::endl;
return 0;
}

// 运行结果
// p1->str: test string
// p2->str: test string
// test::~test() 析构函数发生了调用,自动释放资源

shared_ptr造成的死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include<memory>
using namespace std;

class B;
class A{
public:
shared_ptr<B> a;
~A(){
cout << "A::~A()" << endl;
}
};


class B{
public:
shared_ptr<A> b;
~B(){
cout << "B::~B()" << endl;
}
};
/*******前边两个类中相互引用,此时引入weak_ptr解决死锁问题*/

int main()
{
shared_ptr<A> p1(new A());
shared_ptr<B> p2(new B());
p1->a = p2;
p2->b = p1;//这种情况发生死锁,两个shared_ptr计数都不会为0,资源不会被释放
cout << "use_count():" << p1.use_count() << endl;
cout << "use_count():" << p2.use_count() << endl;
return 0;
}

// 具体解释一下死锁的产生主要原因是引用计数无法降为0。
/*
首先,来理解下为什么这种情况会导致资源不能被释放:

1. 当你创建`p1`和`p2`时,它们都有一个引用计数为1。
2. 当执行`p1->a = p2;`,`p2`的引用计数增加到2,因为现在有两个`shared_ptr`对象(`p1->a`和`p2`)指向同一个`B`对象。
3. 当执行`p2->b = p1;`,`p1`的引用计数增加到2,因为现在有两个`shared_ptr`对象(`p2->b`和`p1`)指向同一个`A`对象。

到此,我们得到了一个环状结构:`p1`指向`A`对象,`A`对象内部的`shared_ptr`指向`B`对象,`B`对象内部的`shared_ptr`又指向`A`对象。这就形成了循环引用。

当`main`函数返回时,`p1`和`p2`的析构函数将被调用,但是它们的引用计数都不会降为0,因为循环引用。所以,`A`和`B`对象的析构函数永远不会被调用,这就导致了资源泄漏。

要解决这个问题,可以使用`weak_ptr`来打破循环引用。你可以将其中一个类的`shared_ptr`成员变量替换为`weak_ptr`。这样,`weak_ptr`不会增加引用计数,从而避免了循环引用的问题。当你需要从`weak_ptr`获取一个`shared_ptr`时,可以使用`lock`方法。
*/

weak_prt解决循环引用问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <memory>
using namespace std;

class B;

class A {
public:
shared_ptr<B> a;
~A() {
cout << "A::~A()" << endl;
}
};

class B {
public:
string str = "hello";
weak_ptr<A> b; // 将 shared_ptr 替换为 weak_ptr
~B() {
cout << "B::~B()" << endl;
}
};

int main() {
shared_ptr<A> p1(new A());
shared_ptr<B> p2(new B());

p1->a = p2;
p2->b = p1; // 这里不会增加 p1 的引用计数

cout << "p1 use_count(): " << p1.use_count() << endl; // 输出 1
cout << "p2 use_count(): " << p2.use_count() << endl; // 输出 2

return 0; // 当 main 返回时,p1 和 p2 都会被析构,它们所指向的对象也会被正确地销毁
}

STL内存池机制

  • 第一级配置器

    第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。

  • 第二级配置器

    在STL的第二级配置器中多了一些机制,避免太多小区造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。

    如果要分配的区块大于128bytes,则移交给第一级配置器处理。 如果要分配的区块小于128bytes,则以 内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。

    参考连接:https://blog.csdn.net/a987073381/article/details/52245795

索引选B+树的原因

个人感觉主要就是探讨B+树和B树之间的区别。 参考链接:https://www.cnblogs.com/JCpeng/p/15231338.html

服务端TIME_WAIT过多的危害及解决办法

危害

  • 导致大量socket连接端口被占用无法释放,导致系统停转
  • 处理大量的TIME_WAIT状态的连接会占用更多的CPU时间,导致服务器响应时间增加

解决办法

  • 调整系统内核参数
  • 调整短连接为长连接

set底层数据结构

set底层数据结构是红黑树。关于map和set的一些问题列举如下:

为何map和set的插入删除效率比用其他序列容器高? 因为对于关联容器来说,不需要做内存拷贝和内存移动。因为map和set容器内部所有元素都是以节点的方式来存储,父节点和子节点。因此插入和删除的时候都是指针的指向换来换去,并没有内存移动。

为何每次插入/删除后以前保存的迭代器不会失效 在这里,迭代器就相当于指向节点的指针,内存没有变,指向内存的指针当然不会失效。不同于map/set,vector就会失效,比如需要扩容的时候,需要一块更大的内存,就必须把之前的内存释放,申请更大的内存,复制已有的数据元素到新的内存。

为何map和set不能像vector一样有个reserve函数来预分配数据 不懂!预留本问题

当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何? map和set使用的是二分查找,举例来说就是,16个元素查四次,32个元素查5次,10000个也就是14次,20000也就15次。

mapreduce原理(不懂,预留)

socket哪些操作会产生阻塞

accept():在等待客户端的连接请求时会阻塞。如果没有客户端尝试连接,调用这个函数的进程或者线程会被挂起。

connect():当客户端尝试与远程主机建立连接时,此操作会阻塞,知道连接成功或失败

recv():当数据从网络到达时,如果没有数据可读,函数调用就会被挂起,阻塞

send():当发送缓冲区已满,并且无法接受更多数据发送到网络时,操作阻塞。

申请一块大内存和一块小内存的效率是一样的吗?

不一样,申请一块小内存通常是不需要一级配置器,小块内存通常由内存分配器从预先分配的内存池中分配,这些池被称为bins,这种分配通常非常快,因为它只是涉及到从已存在的内存池中返回一个指针。大的内存通常要调用mmap()

TCP慢启动和拥塞控制

TCP连接建立好之后,CWND(congestion window,拥塞窗口)被设置为初始值IW(initial window),其大小为2~4个SMSS(TCP报文段的最大长度,仅指数据部分),新的linux内核加大了这个值。此时发送端最多能发送IW字节的数据,此后发送端每收到一个确认,CWND就按照如下公式增长: CWND+ = min(N, SMSS) 其中N是此次确认中包含的之前未被确认的字节数。 如果不施加其他手段,慢启动必然使得CWND增长的很快,所以TCP拥塞控制中有另外一个非常重要的变量:慢启动门限。当CWND超过慢启动门限值时,TCP将进入拥塞避免阶段。

发送端判断发生拥塞的依据

  • 传输超时
  • 接收到重复的确认报文段

Linux下的POSIX互斥锁和条件变量

牢记这几个函数:

1
2
3
4
5
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destory(&mutex);
pthread_mutex_lock(&mutex);
pthread_mutex_trylock(&mutex);
pthread_mutex_unlock(&mutex);
1
2
3
4
5
pthread_cond_init(&cond, NULL);
pthread_cond_destory(&cond);
pthread_cond_broadcast(&cond);
pthread_cond_signal(&cond);
pthread_cond_wait(&cond, &mutex);

条件变量要配合互斥锁使用。这里挂一个讲的比较清晰明了的博客链接(https://punmy.cn/2018/06/07/%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F%EF%BC%88Condition%20Variables%EF%BC%89.html),下面是使用互斥锁加条件变量实现的生产者-消费者模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <pthread.h>
#include <stdio.h>

static int value = 0;
static pthread_mutex_t mutex_;
static pthread_cond_t condition_;
static pthread_t notifying_thread;

void setup(){
pthread_mutex_init(&mutex_, NULL);
pthread_cond_init(&condition_, NULL);
}

void destory(){
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&condition_);
}

void* producer(void *args){
pthread_mutex_lock(&mutex_);
while(value != 10){
value++;
}
notifying_thread = pthread_self();
pthread_mutex_unlock(&mutex_);
pthread_cond_signal(&condition_);
return NULL;
}

void waitCondition(){
pthread_mutex_lock(&mutex_);
while(value != 10){
pthread_cond_wait(&condition_, &mutex_);
}
printf("value is 10, the pthread is: %lu\n", (unsigned long)notifying_thread);
pthread_mutex_unlock(&mutex_);
}

void* consumer(void *args){
waitCondition();
return NULL;
}

int main(){
pthread_t a1;
pthread_t a2;

setup();

pthread_create(&a1, NULL, producer, NULL);
pthread_create(&a2, NULL, consumer, NULL);

pthread_join(a1, NULL);
pthread_join(a2, NULL);

destory();

return 0;
}

写一个生产者/消费者模型(采用信号量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <semaphore.h>
#include <queue>
#include <pthread.h>
#include <unistd.h>

const int MAX_BUFFERS = 10;
std::queue<int> buffer;

sem_t empty;
sem_t full;
pthread_mutex_t mutex_;

void *producer(void *arg){
int num;
while(true){
num = rand() % 100;
sem_wait(&empty);
pthread_mutex_lock(&mutex_);
buffer.push(num);
std::cout << "Produced" << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex_);
sem_post(&full);
}
return nullptr;
}

void *comsumer(void *arg){
int num;
while(true){
sem_wait(&full);
pthread_mutex_lock(&mutex_);
num = buffer.front();
buffer.pop();
std::cout << "Comsumed" << std::endl;
sleep(1);
pthread_mutex_unlock(&mutex_);
sem_post(&empty);
}
return nullptr;
}


int main(){
pthread_t proThread, comPthread;

pthread_mutex_init(&mutex_, NULL);

sem_init(&empty, 0, MAX_BUFFERS);
sem_init(&full, 0, 0);

pthread_create(&proThread, NULL, producer, NULL);
pthread_create(&comPthread, NULL, comsumer, NULL);

pthread_join(proThread, NULL);
pthread_join(comPthread, NULL);

pthread_mutex_destroy(&mutex_);

sem_destroy(&empty);
sem_destroy(&full);

return 0;
}

基类的析构函数为什么是虚函数

当基类的析构函数不是虚函数可能会导致派生类对象的析构函数不被调用。从而引发资源泄露或者其他未定义行为。下面给出示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Base{
public:
Base() { std::cout << "Base obj is constructed" << std::endl; }
// ~Base() { std::cout << "Base obj is deconstructed" << std::endl; }
virtual ~Base() { std::cout << "Base obj is deconstructed" << std::endl; }
};

class Derived : public Base{
public:
Derived(){
num_ = new int[10];
std::cout << "Derived is constructed" << std::endl;
}

~Derived(){
std::cout << "Derived is deconstructed" << std::endl;
}
private:
int *num_;
};

int main(){
Base *base = new Derived();
delete base;
return 0;
}

为什么基类不使用虚函数会导致派生类的析构函数不调用(除了写的内容,最好再加上有关虚函数表、指针的内容)

当一个基类没有虚析构函数,并且通过基类指针删除一个派生类对象时,只有基类的析构函数会被调用。这是因为编译器在编译时期决定了调用哪个析构函数,而不是在运行时期,这种机制被称为“静态绑定”。

如果基类的析构函数是虚函数,那么编译器会在运行时决定要调用哪个析构函数,这种机制称为“动态绑定”或“多态”。

拷贝构造函数为什么要用引用

避免拷贝构造函数无限递归下去!那传引用为什么行呢?回忆一下引用定义,传引用人家根本就不会涉及到构造函数这玩意,形参相当于实参的一个别名。

左右值的差别

C++中的虚继承

c++中的虚继承主要是为了解决多继承情况中存在的二义性问题。比如,现在类A为基类,类B和类C继承自类A,类D继承自类B和类C,假设此时A中有一个名为x的变量且B和C都定义了x,如果此时D直接访问x会产生二义性问题。因为不清楚是A->B->D还是A->C->D。此时如果让类B和类C虚继承自A,就使得在派生类中只保留有一份成员变量x,解决了二义性问题。 参考链接: http://c.biancheng.net/view/2280.html https://zhuanlan.zhihu.com/p/41309205(虚继承、虚函数都讲了,十分详细)

C++中的forward函数

左值引用和右值引用的结果都是左值,无法通过引用区分原本变量的左右值,forward可以保持变量的原本的左右值属性,帮助我们区分。

如何判断TCP断开

  1. 发送心跳消息
  2. 利用recv/send函数的返回值
    • recv返回0代表对方关闭了连接
    • recv返回-1并且errno==EAGAIN或者EWOULDBLOCK时,表示没有数据可读,可以稍后再试。
    • send返回非负值:
      • send返回的是实际发送的字节数,这可能会少于你请求发送的字节数(缓冲区已满)。
      • 如果没有可用的缓冲区空间,那么在非阻塞模式下,send可能会返回EAGAINEWOULDBLOCK
    • send返回-1:
      • recv类似,这表示出现了错误。
      • 可以使用perrorstrerror或检查errno来确定具体的错误原因。
      • 常见的错误原因包括EPIPE(对端已关闭,本端仍尝试发送数据导致的”Broken pipe”错误)和ECONNRESET(连接被对端重置)。
  3. 利用tcp自带的keepalive机制

如何查看系统的最大进程数和线程数

1
2
cat /proc/sys/kernel/pid_max    # 最大进程数
cat /proc/sys/kernel/threads-max # 最大线程数

git rebase和git merge的区别(https://joyohub.com/2020/04/06/git-rebase/)

C++ calss 和 struct的区别

class默认访问类型是private,struct默认访问类型是public

指针和引用的区别

  1. 指针是一个变量,它保存了另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。
  2. 指针可以被重新赋值,指向新的对象;引用绑定对象后不能更改。
  3. 指针可以为nullptr;引用必须初始化。

为什么模板类的声明和实现不能分别写在.h文件和.cpp文件中

首先,模板不是传统意义上的代码。它更像是编译器的“代码生成工具”。当你使用一个特定的模板类型(例如std::vector<int>),编译器会为你生成这种类型的实例代码。为了做到这一点,编译器需要能够看到模板的完整定义。注意:这就是模板和普通类的最大区别。普通类在编译阶段并没有(并不依赖于)编译时的类型特化,只需要在链接阶段将所有.cpp文件链接在一起就好了。而如果模板分开写,其他使用模板的.cpp文件在编译时找不到定义,导致链接错误。

动态链接与静态链接

静态链接(.a/.lib):把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。如果多个程序使用相同的库,那么该库的代码在每个程序中都会被复制,这浪费了磁盘和内存空间。但是所有代码都在可执行文件中,没有运行时链接开销。

动态链接(.so/.ddl):动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。多份代码可以共享同一份库代码。

pragma pack编译宏

pragma pack用于指定内存对齐值。

vector底层实现原理

理解vector的实现主要考虑四个东西就好了:三个指针加动态内存申请。

三个指针:

  • first_:指向vector容器对象起始地址的位置
  • last_:指向vector容器对象中当前最后一个元素的末尾字节
  • end_:指向vector容器所占内存空间的最后一个字节

动态内存申请:

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放。

常量指针和指针常量

常量指针:记忆方法(常量的指针),指针指向的对象是一个常量,只能读取指针指向的内容,不能修改指针指向的内容。

指针常量:指针本身是一个常量,不能修改指针指向的地址,可以修改指针指向地址的内容。

C++中一些特别的关键字(default,override、final、volatile)

参考链接:https://blog.csdn.net/u011947630/article/details/103062773

字节算法题:小于N的最大数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include<string>
#include<vector>
#include<iostream>
#include <algorithm>

using namespace std;

int ans;
int value = 14132;
string valueStr = to_string(value);
vector<int> nums = {1,4,9};

bool dfs(int index, bool judge, int temp) {

if(index == valueStr.size()) {
ans = temp;
return true;
}
if(judge) {
return dfs(index + 1, true, temp * 10 + (nums[nums.size() - 1]));
} else {
int val = valueStr[index] - '0';
for(int i = nums.size() - 1; i >= 0; --i) {
if(val == nums[i]) {
if(dfs(index + 1, false, temp * 10 + nums[i])) {
return true;
}
} else if (val > nums[i]) {
if(dfs(index + 1, true, temp * 10 + nums[i])) {
return true;
}
}
}
//最终结果为空
if(index != 0) {
return false;
}
//少了一位,显然小了
return dfs(index + 1, true, temp);
}
}

int main()
{

dfs(0, false, 0);
cout << ans << endl;
return 0;
}

如果同时有大量客户并发建立连接,服务器端有什么机制进行处理

  1. 多进程和多线程
  2. 连接池
  3. 异步I/O
  4. 负载均衡
  5. 限流
  6. 短连接与长连接

HTTP和TCP之间的关系

  1. HTTP是一个应用层协议,TCP是一个传输层协议

  2. HTTP依赖于TCP来进行数据的传输。比如,当你使用浏览器访问一个网页时,背后发生的是HTTP请求和响应的交换,而这些HTTP数据包是通过TCP连接发送的。

  3. HTTP/1.0对于每一个请求-响应都会新建一个TCP连接,使用完之后就关闭

    HTPP/1.1引入了keep-alive机制,允许在单个TCP连接上发送多个HTTP请求和响应

    HTTP/2进一步扩展了这种机制,允许在单个TCP连接上同时多路复用多个HTTP请求和响应

HTTP从请求到得到结果的过程

  1. 域名解析
    • 客户端(通常是浏览器)首先检查URL是否包含域名(例如 www.example.com)。
    • 如果包含,客户端首先会查找其DNS缓存是否已经有该域名的IP地址。
    • 如果没有,客户端会发起一个DNS查询到配置的DNS服务器,以获取对应的IP地址。
  2. 建立TCP连接
    • 使用从DNS查询得到的IP地址,客户端尝试与服务器的80端口(HTTP)或443端口(HTTPS)建立一个TCP连接。
    • 这涉及到TCP三次握手过程。
  3. (HTTPS的情况)SSL/TLS握手
    • 如果是HTTPS请求,一旦TCP连接建立,客户端和服务器会进行SSL/TLS握手来建立加密的通信通道。
  4. 发送HTTP请求
    • 连接建立后,客户端会发送HTTP请求报文。这包括请求行(例如 GET /path HTTP/1.1)、请求头和(对于某些请求如POST)请求体。
  5. 服务器处理请求
    • 服务器接收到请求后,由其HTTP服务软件(如Apache、Nginx等)处理。
    • 服务器可能会根据请求路径查询文件、与数据库交互或调用其他服务,以生成响应。
  6. 服务器发送响应
    • 服务器生成响应后,它将响应报文发送回客户端。响应通常包括状态行(例如 HTTP/1.1 200 OK)、响应头和响应体。
    • 对于动态内容,如由PHP、Python或Node.js等后端语言生成的页面,服务器可能会进行一些额外的处理来生成响应内容。
  7. 客户端处理响应
    • 客户端(如浏览器)接收响应,并基于响应内容采取相应的行动。
    • 如果响应是一个HTML页面,浏览器会开始解析HTML,并可能发起其他请求来获取页面上的资源,如图片、CSS、JavaScript文件等。
  8. 关闭连接
    • 一旦数据交换完成,通常会关闭TCP连接。然而,在HTTP/1.1中,默认使用keep-alive,意味着连接可以被复用,从而减少后续请求的延迟。

https和http的区别是什么,https具体是怎么做的

HTTPS和HTTP唯一的区别就是HTTPS使用TLS/SSL来加密普通的HTTP请求和响应。

关于https具体是如何做的,这里有一个参考博文链接,写的超好(参考链接:https://www.runoob.com/w3cnote/http-vs-https.html)

gdb中,如何查看每个线程相关的信息

在 gdb 中,可以使用 info threads 命令查看当前进程中的所有线程信息。这个命令会列出每个线程的编号、状态(如运行、挂起等)以及当前所在的函数名称。

1
2
3
4
5
6
(gdb) info threads
Id Target Id Frame
2 Thread 0x7ffff7fc8700 (LWP 13627) "main" __libc_start_main (argc=1, argv=0x7fffffffe4c8, env=0x7fffffffe4d8,
auxvec=0x7fffffffe4d8, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe4c8) at ../csu/libc-start.c:310
3 Thread 0x7ffff77c7700 (LWP 13628) "main" foo () at test.c:6
4 Thread 0x7ffff6fc6700 (LWP 13629) "main" bar () at test.c:11

你还可以使用 thread <thread-id> 命令来切换到指定的线程,然后使用其他 gdb 命令来查看线程的信息或调试线程。

例如,你可以输入 thread 3 命令切换到编号为 3 的线程,然后输入 bt 命令来查看线程的调用堆栈:

1
2
3
4
5
6
7
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff77c7700 (LWP 13628))]
#0 foo () at test.c:6
(gdb) bt
#0 foo () at test.c:6
#1 0x00007ffff7bbb830 in start_thread (arg=<optimized out>) at pthread_create.c:486
#2 0x00007ffff78f95fd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

构造函数中能调用虚函数吗,如果调用虚函数,会出现什么问题,写程序验证,并从原理上分析原因

构造函数从语法上调用虚函数没有任何问题,但这样做可能并不会达到预期的结果。因为在构造函数执行期间,对象的虚表仍在被设置,因此调用的虚函数版本可能并不是我们想要的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Base{
public:
Base(){
std::cout << "Base construct\n";
call();
}

virtual void call() const{
std::cout << "Base call\n";
}

virtual ~Base() {}
};

class Derived : public Base{
public:
Derived(){
std::cout << "Derived construct\n";
call();
}

virtual void call() const override{
std::cout << "Derived call\n";
}

virtual ~Derived() {}
};

int main(){
Derived d;
return 0;
}

对于上面这段程序,可能我们期望得到的结果是

1
2
3
4
Base constructor
Derived call
Derived constructor
Derived call

但我们实际得到的结果是

1
2
3
4
Base constructor
Base call
Derived constructor
Derived call

原因是在Base构造函数执行时,Derived部分的对象还没有完全构造出来,因此,此时vtable仍然指向Base类的vtable。因此,当在Base构造函数中调用call虚函数时,他会调用Base版本,而不是Derived版本。

这里延伸一下不要在析构函数中调用虚函数的原因:析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。

迭代器的实现

参考链接:https://www.cnblogs.com/wengle520/p/12492708.html

epoll红黑树的作用

简单点讲就是在内核事件表上快速注册/删除所需连接文件描述符。下面两篇文章基本阐述了这个问题,好好看!

https://cloud.tencent.com/developer/article/1862671

https://zhuanlan.zhihu.com/p/366955699

用户态和内核态分别会做什么,怎么切换的

内核空间主要负责操作系统内核线程以及用户程序系统调用。

用户空间主要负责用户程序的非系统调用。

从用户态切换到内核态主要有三种方式:

  1. 系统调用:系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中
  3. 外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

有一个面试问题:I/O频繁发生内核态和用户态切换,怎么解决?

答案:使用用户进程缓冲区。

用户进程缓冲区

你看一些程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。所以说:用户缓冲区的目的就是是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。除了在进程中设计缓冲区,内核也有自己的缓冲区。

内核缓存区

当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。等到数据已经读取到内核缓冲区时,把内核缓冲区中的数据读取到用户进程中,才会通知进程,当然不同的IO模型,在调度和使用内核缓冲区的方式上有所不同。

参考链接:

https://blog.csdn.net/qq_42052956/article/details/111562280

https://cloud.tencent.com/developer/article/2131401

进程的创建需要系统分配什么资源

根据其他博客里写的底层源码来看,分配给一个进程的东西太多啦,我们挑几个记一下:内存、CPU处理时间、输入输出设备、存储空间。

参考链接:

https://blog.csdn.net/lvyibin890/article/details/82193900

https://juejin.cn/s/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%BB%99%E8%BF%9B%E7%A8%8B%E5%88%86%E9%85%8D%E4%BB%80%E4%B9%88%E8%B5%84%E6%BA%90

HTTPS加密方式:对称加密、非对称加密

对称加密:密钥只有一个,加密解密为同一个密码。利用这种加密方式时必须把密钥也发送给对方,密钥在传输过程中被窃取,也就失去了加密的意义。

非对称加密:密钥成对出现,公钥加密需要私钥解密,私钥加密需要公钥解密。

参考链接:

https://www.runoob.com/w3cnote/http-vs-https.html

https://www.itwork.club/2022/05/27/why-did-https-use-mixed-encryption-algorithm/

线程的三种状态

就绪态、运行态、阻塞态

析构函数里能不能抛异常?为什么?

析构函数从语法上是可以抛出异常的,但是这样做很危险,请尽量不要这要做。原因在《More Effective C++》中提到两个:

(1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

(2)通常异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding),因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。

两个类里有相同的成员,区别在于一个有虚函数,一个没有,哪个内存大?

有虚函数的大,因为有虚函数的类的会包含一个虚函数指针。注意不同操作系统指针大小不一样。

参考链接:https://blog.csdn.net/luolaihua2018/article/details/110736211

数据报和数据流的区别?

可以这么简单理解:

假设现在有100字节数据,面向数据流和面向数据报的连接可以分别怎么样处理呢?

  • 数据流是发送端可以调用10次write,每次10字节。接收端调用一次read。也可以是发送端调用一次write,接收端调用10次read。数据流是没有边界的
  • 数据报则是一次write就需要一次read,是有边界的。

在分布式系统中,如果某个节点宕机了咋办?

做一个数据副本策略,把每一台机器上的数据做几个副本的冗余,放在别的机器上。万一说某一台机器宕机,没事啊,因为其他机器上还有他的副本。

自旋锁

所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。

自旋锁实现参考链接:https://blog.csdn.net/jeffasd/article/details/80661804

线程同步的方式有哪些

互斥锁、信号量、条件变量、自旋锁、读写锁、屏障