后端开发面经系列 -- 小米澎湃OS - C++开发二面

小米澎湃OS - C++开发二面

公众号:阿Q技术站

来源:https://www.nowcoder.com/discuss/610414092750237696

1、linux常用命令举例?

这里小伙伴也给了建议不要举什么cd的,可以说稍微有技术含量的,比如tail 用来查日志,cat 用来查看小文件,ps 用来查看进程,pwd 查看当前目录,grep用来过滤,top 用来查看 cpu 的使用率,chomd 用来修改权限,ash 用来远程登陆。

2、进程,线程区别?

  1. 定义:
    • 进程:是程序的一次执行过程,是操作系统进行资源分配和调度的一个独立单位。
    • 线程:是进程的一个执行单元,是操作系统进行调度的最小单位,它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
  2. 资源拥有:
    • 进程:拥有独立的地址空间、文件描述符、堆栈和数据段等资源。
    • 线程:共享所属进程的地址空间和其他资源,如文件描述符、堆栈、数据段等。
  3. 通信和同步:
    • 进程:进程间通信(IPC)比较复杂,需要借助于操作系统提供的机制,如管道、信号量、消息队列等。
    • 线程:线程间通信(IPC)较为简单,可以直接通过共享内存等方式进行通信。
  4. 创建和销毁开销:
    • 进程:创建和销毁较为复杂,需要分配和回收大量资源。
    • 线程:创建和销毁较为简单,开销较小。
  5. 并发性:
    • 进程:进程之间相互独立,因此可以并发执行。
    • 线程:同一进程内的多个线程共享进程的资源,可以实现并发执行。

3、进程间通信方式?

  1. 管道:
    • 单向通信,分为匿名管道和命名管道。
    • 匿名管道只能用于具有亲缘关系的进程间通信,一般在创建子进程时使用。
    • 命名管道可以用于无亲缘关系的进程间通信,通过文件系统中的特殊文件实现。
  2. 信号:
    • 用于通知进程发生了某种事件。
    • 可以用于进程间的简单通信,但只能传递少量信息。
  3. 消息队列:
    • 允许一个进程向另一个进程发送消息的队列。
    • 消息队列独立于发送和接收进程,可以提供异步通信的能力。
  4. 共享内存:
    • 允许多个进程共享同一段内存空间。
    • 由于是直接访问内存,所以速度快,但需要考虑同步和互斥问题。
  5. 信号量:
    • 用于进程间同步和互斥的一种机制。
    • 可以用来解决进程对共享资源的争用问题,保证多个进程之间的同步。
  6. 套接字:
    • 在网络编程中用于不同主机间的进程通信,也可以用于同一主机内的进程间通信。
    • 提供了一种统一的通信接口,可以实现进程间的数据交换。
  7. 文件锁:
    • 使用文件系统的锁机制来实现进程间的同步。
    • 可以通过对文件进行加锁和解锁来控制进程对文件的访问。

4、源文件到可执行文件的过程?

  1. 预处理:
    • 预处理器(如C预处理器)会处理源文件,执行诸如包含头文件、宏展开、条件编译等操作,生成经过预处理的中间文件。
  2. 编译:
    • 编译器(如GCC、Clang等)将预处理后的文件转换为汇编代码(Assembly Code),汇编代码是一种低级的表示,使用特定的汇编语言描述源代码的操作和数据。
  3. 汇编:
    • 汇编器(Assembler)将汇编代码转换为机器码(Machine Code),即二进制指令,这些指令可以被计算机硬件直接执行。
  4. 链接:
    • 链接器(Linker)将编译后的目标文件与所需的库文件链接在一起,生成最终的可执行文件。
    • 链接器会解析符号引用(Symbol References),将源文件中引用的函数或变量与其定义所在的位置进行关联,最终生成一个完整的可执行文件。
  5. 装载(可选):
    • 如果生成的可执行文件需要在运行时动态加载库文件,操作系统的加载器会负责将这些库文件加载到内存中。
  6. 运行:
    • 最终生成的可执行文件被操作系统加载到内存中,并由CPU执行,实现了源文件的功能。

5、linux内存空间分布(这个应该是要区分一下c++内存分布?)

  1. 内核空间(Kernel Space):
    • 内核空间是操作系统内核运行的内存区域,通常位于高地址部分。
    • 内核空间包含了操作系统内核的代码和数据结构,用于管理系统资源和提供系统调用服务。
  2. 用户空间(User Space):
    • 用户空间是用户程序运行的内存区域,通常位于低地址部分。
    • 用户空间包含了用户程序的代码和数据,以及用户空间的堆、栈等区域。
  3. 堆(Heap):
    • 堆是由malloc、free等函数动态分配和释放的内存区域,位于用户空间。
    • 堆的大小可以动态增长或缩小,受限于操作系统和编译器的限制。
  4. 栈(Stack):
    • 栈是用于函数调用和局部变量存储的内存区域,位于用户空间。
    • 每个线程都有自己的栈,栈的大小通常固定或受限于操作系统和编译器的限制。
  5. 共享库空间(Shared Libraries Space):
    • 共享库空间包含了系统和用户共享的动态链接库(shared libraries)。
    • 这些库在内存中只需加载一次,不同的程序可以共享这些库的代码和数据。
  6. 内存映射区域(Memory-mapped Region):
    • 内存映射区域用于将文件映射到内存,以便直接读写文件而不需要通过系统调用。
    • 这些区域通常用于共享内存、共享库和动态链接等。
  7. 内核空间映射(Kernel Space Mapping):
    • 内核空间映射用于将内核空间的部分内容映射到用户空间,以便用户程序访问内核数据结构和执行系统调用。

6、堆和栈的区别?

  1. 分配方式:
    • 栈(Stack):栈是一种连续的内存区域,由系统自动分配和释放。栈上的内存分配和释放遵循"先进后出"(LIFO)的原则。
    • 堆(Heap):堆是一种动态分配的内存区域,程序员可以根据需要手动申请和释放内存。堆上的内存分配和释放顺序没有特定规律。
  2. 空间大小:
    • 栈:栈的空间通常较小,通常由操作系统预先分配一定大小的栈空间给每个线程使用,超出大小会导致栈溢出。
    • 堆:堆的空间通常较大,可以动态增长或缩小,受限于操作系统和编译器的限制。
  3. 分配效率:
    • 栈:栈上的内存分配和释放速度较快,只需移动栈指针即可。
    • 堆:堆上的内存分配和释放速度较慢,需要进行内存管理和碎片整理。
  4. 生命周期:
    • 栈:栈上的变量生命周期由其所在的函数决定,函数返回时变量会被自动释放。
    • 堆:堆上的变量生命周期可以由程序员控制,需要手动释放内存,否则可能会导致内存泄漏。
  5. 使用方式:
    • 栈:栈通常用于存储局部变量、函数参数和函数返回地址等,适合存储生命周期短暂的数据。
    • 堆:堆通常用于存储动态分配的对象和数据结构,适合存储生命周期较长的数据。

7、开发过程中遇到过内存泄漏的问题,怎么解决的,或者怎么定义内存泄漏?

内存泄漏指的是程序在动态分配内存后,未能及时释放该内存造成的现象。在开发过程中,内存泄漏可能会导致程序占用的内存越来越多,最终耗尽系统的可用内存,导致程序崩溃或系统变慢。

内存泄漏的解决方法:

  1. 检测内存泄漏:使用内存泄漏检测工具(如Valgrind、Dr.Memory等)来检测程序中的内存泄漏问题。这些工具可以帮助定位内存泄漏的位置和原因。
  2. 分析内存泄漏原因:通过检测工具的报告,分析内存泄漏的原因。可能的原因包括未释放动态分配的内存、循环引用导致的内存泄漏等。
  3. 修复内存泄漏:根据分析结果,修复内存泄漏问题。主要包括以下几个方面:
    • 在合适的位置释放动态分配的内存,确保每次分配内存都有对应的释放操作。
    • 避免循环引用,可以使用智能指针等工具来管理对象的生命周期,避免手动管理内存带来的问题。
    • 使用容器类(如std::vector、std::list等)来管理数据,避免手动管理内存造成的错误。
  4. 测试和验证:修复内存泄漏后,进行测试和验证,确保内存泄漏问题已经解决。

8、有没有用过内存泄漏检查工具?

Valgrind是一个强大的开源工具,用于检测内存泄漏、内存错误和线程错误等问题。它可以检测C、C++等语言编写的程序,并提供详细的报告来帮助定位和解决问题。

使用Valgrind进行内存泄漏检查的一般步骤如下:

  1. 编译程序:使用Valgrind提供的工具来编译你的程序,例如使用valgrind --tool=memcheck命令。
  2. 运行程序:使用Valgrind运行你的程序,例如valgrind ./your_program。Valgrind会监控程序的内存使用情况。
  3. 分析报告:Valgrind会生成详细的报告,包括内存泄漏的位置、大小和相关调用堆栈信息。根据报告定位并修复内存泄漏问题。

9、进程同步的方式,信号量和互斥锁的区别?

  1. 信号量:
    • 信号量是一种计数器,用于控制多个进程对共享资源的访问。
    • 信号量可以有两种类型:二进制信号量和计数信号量。二进制信号量的取值只能为0或1,用于互斥访问共享资源;计数信号量的取值可以大于1,用于限制资源的数量。
    • 使用信号量时,进程可以通过 wait 操作来等待资源变为可用,通过 signal 操作来释放资源。
    • 信号量可以用于解决生产者-消费者问题、读者-写者问题等。
  2. 互斥锁:
    • 互斥锁是一种特殊的信号量,只能取值0或1,用于实现对共享资源的互斥访问。
    • 使用互斥锁时,进程可以通过 lock 操作来获取锁,如果锁已被其他进程获取,则进入等待状态;通过 unlock 操作来释放锁。
    • 互斥锁通常用于保护临界区,确保在同一时刻只有一个进程可以访问临界区。
  3. 区别:
    • 信号量可以用于控制资源的数量,而互斥锁只能用于实现互斥访问。
    • 信号量可以用于进程间通信,而互斥锁通常用于线程间同步。
    • 互斥锁的实现通常比信号量更轻量级,性能更高。

10、信号和信号量的区别?

  1. 概念:
    • 信号(Signal)是一种异步通信机制,用于通知进程发生了特定事件,如硬件异常、软件事件等。信号可以在任何时候发送给进程,而进程需要安装信号处理器来处理信号。
    • 信号量(Semaphore)是一种同步机制,用于控制对共享资源的访问。信号量是一个计数器,用于表示可用资源的数量,进程可以通过对信号量执行操作来申请或释放资源。
  2. 用途:
    • 信号常用于处理异步事件,如按下键盘、接收到网络数据等,以及进程间的通知。
    • 信号量常用于实现进程间的同步和互斥,控制对共享资源的访问。
  3. 操作:
    • 信号由操作系统或进程发送,接收信号的进程需要安装信号处理器来处理信号。
    • 信号量是由进程通过系统调用(如 sem_waitsem_post)来操作的,用于控制对共享资源的访问。
  4. 特性:
    • 信号是一次性的,即一旦发送并被接收处理,就不再有效。
    • 信号量是持久的,即其值在操作之间保持不变,直到被显式地改变。
  5. 示例:
    • 信号的例子包括 SIGINT 用于终止进程、SIGSEGV 用于处理段错误等。
    • 信号量的例子包括用于控制共享内存访问的信号量、用于控制进程数量的信号量等。

11、函数指针和指针函数的区别?

  1. 函数指针(Function Pointer):
    • 函数指针是指向函数的指针变量,可以用来间接调用函数。
    • 函数指针的声明形式为 返回类型 (*指针变量名)(参数列表),例如 int (*funcPtr)(int, int) 表示一个返回类型为 int、接受两个 int 参数的函数指针。
    • 函数指针通常用于实现回调函数、动态选择函数等场景,通过在运行时指定函数指针来调用不同的函数。
  2. 指针函数(Pointer to Function):
    • 指针函数是一个返回指针的函数,即函数的返回类型为指针类型。
    • 指针函数的声明形式为 返回类型 (*函数名)(参数列表),例如 int *(*func)(int, int) 表示一个返回类型为 int*、接受两个 int 参数的指针函数。
    • 指针函数通常用于返回动态分配的内存地址或者函数指针。
#include <iostream>
using namespace std;

// 函数指针示例:定义一个函数指针类型
typedef int (*FuncPtr)(int, int);

// 指针函数示例:返回一个指向整数的指针的函数
int* getIntegerPtr(int x) {
    int* ptr = new int;
    *ptr = x;
    return ptr;
}

// 函数指针示例:加法函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 函数指针示例:定义并初始化一个函数指针
    FuncPtr ptr = add;
    cout << "Function pointer result: " << ptr(1, 2) << endl;

    // 指针函数示例:调用指针函数并使用返回值
    int* intPtr = getIntegerPtr(42);
    cout << "Pointer function result: " << *intPtr << endl;
    delete intPtr;

    return 0;
}

12、TCP三次握手和四次挥手?

三次握手

三次握手

  1. 第一次握手(SYN-1):
    • 客户端发送一个带有 SYN 标志的 TCP 报文段给服务器,表示客户端请求建立连接。
    • 客户端选择一个初始序列号(ISN)并将其放入报文段中,进入 SYN_SENT 状态。
  2. 第二次握手(SYN + ACK):
    • 服务器收到客户端发送的 SYN 报文段后,如果同意建立连接,会发送一个带有 SYN 和 ACK 标志的报文段给客户端,表示服务器接受了客户端的请求,并带上自己的 ISN。
    • 服务器进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):
    • 客户端收到服务器发送的 SYN+ACK 报文段后,会发送一个带有 ACK 标志的报文段给服务器,表示客户端确认了服务器的响应。
    • 客户端和服务器都进入 ESTABLISHED 状态,连接建立成功,可以开始进行数据传输。
四次挥手

四次挥手

  1. 第一次挥手(FIN-1):
    • 客户端发送一个 FIN 报文段给服务器,表示客户端已经没有数据要发送了,请求关闭连接。
    • 客户端进入 FIN_WAIT_1 状态,等待服务器的确认。
  2. 第二次挥手(ACK):
    • 服务器收到客户端的 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经接收到了客户端的关闭请求。
    • 服务器进入 CLOSE_WAIT 状态,等待自己的数据发送完毕。
  3. 第三次挥手(FIN-2):
    • 服务器发送一个 FIN 报文段给客户端,表示服务器也没有数据要发送了,请求关闭连接。
    • 服务器进入 LAST_ACK 状态,等待客户端的确认。
  4. 第四次挥手(ACK):
    • 客户端收到服务器的 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经接收到了服务器的关闭请求。
    • 客户端进入 TIME_WAIT 状态,等待可能出现的延迟数据。
    • 服务器收到客户端的 ACK 报文段后,完成关闭,进入 CLOSED 状态。
    • 客户端在 TIME_WAIT 状态结束后,关闭连接,进入 CLOSED 状态。

13、为什么要四次断开,不是三次或者两次?

TCP 的四次挥手是为了确保数据的可靠传输和连接的正确关闭。在两次挥手中,一旦客户端发送了最后的 ACK 确认,表示客户端已经没有数据要发送了,服务器就会立即关闭连接,这时如果客户端还有没有发送完的数据,那么这些数据就会丢失,因为服务器已经关闭了连接。因此,为了确保数据能够完整传输并且保证连接的正确关闭,需要四次挥手来完成整个断开连接的过程。

14、select poll ,epoll的区别?

  1. select
    • select 是最古老的 I/O 多路复用机制,它使用一个 fd_set 结构来存储待检查的文件描述符,并通过 select 函数来检查这些文件描述符的状态变化。
    • select 的最大缺点是效率低下,因为每次调用 select 都需要将所有待检查的文件描述符拷贝到内核空间,导致性能下降,尤其在文件描述符很多时更为明显。
  2. poll
    • poll 是对 select 的改进,使用一个 pollfd 结构数组来存储待检查的文件描述符,并通过 poll 函数来检查这些文件描述符的状态变化。
    • poll 解决了 select 中文件描述符数量限制的问题,但仍然存在效率低下的缺点,因为每次调用 poll 都需要将所有待检查的文件描述符拷贝到内核空间。
  3. epoll
    • epoll 是 Linux 特有的高性能 I/O 多路复用机制,使用一个事件表来存储待检查的文件描述符,并通过 epoll_ctlepoll_wait 函数来操作和等待文件描述符的状态变化。
    • epoll 解决了 selectpoll 的效率问题,它使用了事件通知的机制,只有在文件描述符状态发生变化时才会进行通知,避免了每次调用都要遍历所有文件描述符的问题。
    • epoll 还支持水平触发和边缘触发两种工作模式,水平触发模式下,只要文件描述符处于就绪状态,epoll_wait 就会返回,而边缘触发模式下,只有在文件描述符状态从未就绪变为就绪时,epoll_wait 才会返回。

15、大端字节和小端字节的区别?

  1. 大端字节序(Big Endian)
    • 在大端字节序中,高位字节(Most Significant Byte,MSB)存储在起始地址,低位字节(Least Significant Byte,LSB)存储在结束地址。
    • 例如,对于十六进制数 0x12345678,在大端字节序中,存储顺序为 12 34 56 78
  2. 小端字节序(Little Endian)
    • 在小端字节序中,低位字节(LSB)存储在起始地址,高位字节(MSB)存储在结束地址。
    • 例如,对于十六进制数 0x12345678,在小端字节序中,存储顺序为 78 56 34 12

16、手撕,判断回文数

问题描述

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

  • 例如,121 是回文,而 123 不是。

示例 1:

输入:x = 121
输出:true

示例 2:

输入:x = -121
输出:false
解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:

输入:x = 10
输出:false
解释:从右向左读, 为 01 。因此它不是一个回文数。
思路
  1. 将整数转换为字符串,然后判断字符串是否是回文字符串。
  2. 如果整数为负数,则直接返回false,因为负数不可能是回文数。
  3. 如果整数为正数,则将其转换为字符串,并使用双指针法从字符串的两端向中间比较字符是否相等。
参考代码
C++
#include <iostream>
#include <string>

using namespace std;

class Solution {
public:
    bool isPalindrome(int x) {
        // 如果x为负数,则不可能是回文数
        if (x < 0) {
            return false;
        }
        
        // 将整数转换为字符串
        string str = to_string(x);
        int left = 0, right = str.length() - 1;
        
        // 使用双指针法判断字符串是否是回文字符串
        while (left < right) {
            if (str[left] != str[right]) {
                return false;
            }
            left++;
            right--;
        }
        
        return true;
    }
};

int main() {
    Solution solution;
    cout << solution.isPalindrome(121) << endl; // 输出1,表示true
    cout << solution.isPalindrome(-121) << endl; // 输出0,表示false
    cout << solution.isPalindrome(10) << endl; // 输出0,表示false
    return 0;
}
  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值