【得物】C++岗-后端一面,被问麻了,附详细答案

1、要实现一个队列,要求固定长度,push和pop都是O(1),怎么实现?

要实现一个固定长度且具有O(1)时间复杂度的队列,可以使用循环数组(Circular Array)来实现。

以下是一个基于循环数组的示例实现:

#include <iostream>

class FixedSizeQueue {
private:
    int* queue;
    int capacity;
    int front;
    int rear;
    int size;

public:
    FixedSizeQueue(int capacity) : capacity(capacity), front(0), rear(0), size(0) {
        queue = new int[capacity];
    }

    ~FixedSizeQueue() {
        delete[] queue;
    }

    bool isEmpty() const {
        return size == 0;
    }

    bool isFull() const {
        return size == capacity;
    }

    void push(int item) {
        if (isFull()) {
            std::cout << "Queue is full. Unable to push item: " << item << std::endl;
            return;
        }
        queue[rear] = item;
        rear = (rear + 1) % capacity; // 循环更新 rear 的位置
        size++;
    }

    void pop() {
        if (isEmpty()) {
            std::cout << "Queue is empty. Unable to perform pop operation." << std::endl;
            return;
        }
        front = (front + 1) % capacity; // 循环更新 front 的位置
        size--;
    }

   int getFront() const {
       if (isEmpty()) {
           std::cout << "Queue is empty. No front element." << std::endl;
           return -1; // 返回一个默认值
       }
       return queue[front];
   }
};

int main() {
   FixedSizeQueue q(5);

   q.push(10);
   q.push(20);
   q.push(30);

   std::cout << "Front element: " << q.getFront() << std::endl;

   q.pop();

   std::cout << "Front element after pop: " << q.getFront() << std::endl;

   return 0;
}

这个实现中,使用循环数组来存储队列元素。通过 front 和 rear 指针来追踪队列的前端和后端位置。push 操作将元素添加到 rear 的位置,pop 操作将 front 向前移动一个位置。通过利用取模运算,实现循环更新 front 和 rear 的位置,使得在达到数组边界时能够回到起始位置。这种基于循环数组的实现可以满足固定长度、push 和 pop 都是 O(1) 时间复杂度的要求。

2、C++运行出现Core Dump怎么办?

当C++程序出现Core Dump时,表示程序发生了严重的错误导致崩溃。Core Dump是指程序在崩溃时生成的内存转储文件,可以通过分析该文件来定位问题。

以下是一些常见的处理方法:

  1. 检查代码:首先检查代码中是否存在明显的错误或潜在的问题,如空指针解引用、数组越界等。仔细阅读相关代码,并使用调试工具进行跟踪和测试。
  2. 调试器:使用调试器(如GDB)来运行程序并捕获Core Dump文件。通过回溯栈帧、变量值以及其他调试信息,可以更好地理解程序崩溃的原因。
  3. 日志记录:在程序中添加适当的日志记录,以便在出错时能够获取更多信息。将关键信息输出到日志文件中,有助于排查问题。
  4. 内存管理:检查内存管理方面的问题,比如释放已经释放过的内存、内存泄漏等。确保正确地分配和释放内存,并避免悬挂指针等错误。
  5. 使用静态分析工具:借助静态分析工具,对代码进行检查和扫描,以发现潜在的问题和错误。
  6. 更新编译器版本:如果使用较旧版本的编译器,尝试更新到最新版本,以修复已知的编译器错误和问题。

3、GDB调试输出栈的信息用什么命令?

在使用GDB调试时,可以使用命令backtrace(或缩写为bt)来输出当前的函数调用栈信息。这个命令会显示出函数的调用顺序以及每个函数对应的源代码位置。另外,还可以使用frame命令加上帧号来查看特定帧的详细信息。例如,使用frame 1来查看第一帧的信息。

4、GDB调试切换线程用什么命令?

在GDB调试中,要切换线程可以使用以下命令:

  1. thread <线程号>:切换到指定线程。例如,thread 2 将切换到线程号为 2 的线程。
  2. info threads:显示当前程序中所有线程的信息和编号。
  3. thread apply all <命令>:对所有线程执行相同的命令。
  4. thread next 或 thread step: 切换到下一个可执行的线程。
  5. thread select <条件>: 根据条件选择并切换到相应的线程。

注意,在多线程调试时,请确保程序已停止在断点或暂停状态下才能进行切换。

5、C++程序运行起来如果发现内存使用不断在长,怎么确定问题位置?

当C++程序发现内存使用不断增长时,可以采取以下步骤来确定问题的位置:

  1. 使用内存分析工具:使用专门的内存分析工具(如Valgrind、Dr. Memory等)对程序进行检查。这些工具可以帮助您找到内存泄漏或者未释放的内存块。
  2. 仔细检查代码:检查代码中可能存在的错误,例如未正确释放动态分配的内存、循环引用等。确保每次分配内存后都有相应的释放操作。
  3. 使用日志和调试信息:在程序中插入适当的日志输出和调试信息,以便追踪程序执行过程中可能导致内存泄漏或者不断增长的地方。记录堆栈跟踪信息以帮助定位问题。
  4. 运行性能剖析器:使用性能剖析器(如gprof、Intel VTune等)来确定哪些函数或代码段占用了大量内存,并寻找潜在的问题。
  5. 重视第三方库:如果您使用了第三方库,特别是涉及到资源管理(如内存)的库,确保正确地使用和释放这些资源。

6、C++多线程相关的应该注意些什么?

在使用C++进行多线程编程时,有几个方面需要注意:

  1. 线程安全:确保对共享数据的访问是安全的,避免出现竞态条件和数据不一致的问题。可以使用互斥锁、条件变量等机制来实现线程间同步。
  2. 数据共享:合理管理共享数据,避免不必要的数据复制和传递。可以使用引用或指针来传递大型对象或数据结构,减少开销。
  3. 死锁:避免死锁情况的发生,即多个线程相互等待对方释放资源而无法继续执行。通常通过按照固定的顺序获取锁来预防死锁。
  4. 线程创建与销毁开销:频繁地创建和销毁线程会增加系统开销,影响性能。因此,在设计时要考虑好线程池、线程重用等技术手段。
  5. 资源管理:注意及时释放申请的资源,例如内存、文件句柄等,以避免资源泄露和系统负载过高。
  6. 异常处理:合理处理异常情况,确保程序正确稳定地运行。可利用try-catch块捕获异常并进行适当处理或回滚操作。
  7. 性能调优:考虑多线程任务的负载均衡和并发度,避免出现线程间争夺资源导致性能瓶颈。

7、信号量和互斥锁有什么区别?

信号量(Semaphore)和互斥锁(Mutex)是用于多线程/多进程环境下实现同步的机制,但它们有以下几个区别:

  1. 用途不同:互斥锁主要用于保护共享资源,确保在任意时刻只有一个线程/进程可以访问共享资源。信号量既可以用于控制对共享资源的访问,也可以用于控制并发执行的线程/进程数量。
  2. 操作方式不同:互斥锁提供了两个基本操作:lock(加锁)和unlock(解锁)。只有获得了互斥锁的线程/进程才能继续执行关键代码段。信号量提供了三个基本操作:wait(等待),signal(发送信号),以及类似P(proberen)和V(verhogen)的原语。
  3. 资源控制能力不同:互斥锁只允许一个线程/进程同时持有该锁,其他线程/进程必须等待当前持有者释放。而信号量可以指定一定数量的资源,并允许多个线程/进程同时获得这些资源,直到没有剩余资源可用为止。
  4. 使用场景不同:互斥锁适合用于保护临界区、避免数据竞争。信号量适合用于控制并发执行的线程/进程数量,例如限制线程池中同时执行的任务数。

8、死锁是什么引起的?给一个死锁场景?怎么避免?

死锁是指在并发编程中,两个或多个进程(或线程)彼此持有对方所需要的资源,并且由于无法获取到对方所占用的资源而处于阻塞状态,导致程序无法继续执行的情况。

一个经典的死锁场景是银行转账问题。假设有两个账户A和B,同时进行转账操作。如果账户A先获得了锁并等待账户B释放锁才能完成转账,而同时账户B也获得了锁并等待账户A释放锁才能完成转账,这样就会导致双方相互等待对方释放资源,形成死锁。

为避免死锁,可以采取以下几种策略:

  1. 预防死锁:通过合理地分配和管理资源来避免产生死锁,例如实施资源申请顺序、强制抢占等机制。
  2. 避免死锁:使用银行家算法等算法来动态地检测和避免可能引发死锁的操作序列。
  3. 检测与恢复:定期检测系统中是否存在死锁,并通过剥夺某些进程已获得的资源来解除死锁。
  4. 忽略死锁:在某些情况下,可以将死锁视为系统故障,进行系统重启或者人工干预来解决。

选择适当的策略取决于具体场景和需求,以确保并发程序的正确性和可靠性。

9、C++中关键字volatile修饰一个int,在多线程场景下会有安全问题吗?

在C++中,关键字volatile用于告诉编译器,该变量可能会被意外地修改,因此编译器不能进行一些优化操作(比如缓存变量值)。然而,volatile并不能解决多线程并发访问的安全问题。

如果多个线程同时读写一个被volatile修饰的整型变量,仍然存在数据竞争和原子性问题。volatile只能保证对该变量的操作不会被优化掉,并且可以立即从内存中读取最新值,但它并不提供同步机制来确保线程安全。

要在多线程场景下实现安全访问和操作共享资源(如整型变量),需要使用其他同步机制,例如互斥锁、原子操作或线程间通信等。这些机制能够保证数据的正确性和一致性,并避免数据竞争产生的问题。

10、MySQL事务,四大特性?隔离级别?

MySQL的事务具有四个特性,即ACID:

  1. 原子性(Atomicity):事务中的所有操作要么全部成功执行,要么全部回滚。如果在事务执行过程中发生错误,会将已经执行的操作进行回滚,保持数据一致性。
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。在事务执行过程中对数据的修改必须符合预定义的规则,以保证数据的有效性和正确性。
  3. 隔离性(Isolation):每个事务在并发执行时都应该相互隔离,并且不应该互相干扰。这意味着一个事务在提交之前对其他事务是不可见的,防止了读取到未提交或部分提交的数据。
  4. 持久性(Durability):一旦事务提交成功后,其所做的修改就会永久保存到数据库中,并且不能被撤销。即使在系统故障或断电情况下,也能够保证数据不会丢失。

MySQL支持多种隔离级别来控制并发访问时可能出现的问题:

  1. 读未提交(Read Uncommitted):最低级别隔离,允许一个事务读取另一个未提交事务修改的数据。可能导致脏读、不可重复读和幻读的问题。
  2. 读已提交(Read Committed):允许一个事务只能读取已经提交的数据,解决了脏读的问题。但仍可能遇到不可重复读和幻读的问题。
  3. 可重复读(Repeatable Read):保证在同一个事务中多次读取同一记录时,其结果始终一致。通过MVCC(多版本并发控制)来实现,解决了不可重复读的问题。但仍可能遇到幻读的问题。
  4. 串行化(Serializable):最高级别隔离,确保事务之间完全串行执行。通过强制事务等待以防止任何并发问题,可以解决所有并发访问引起的问题。

11、聚簇索引和非聚簇索引有什么区别,回表是什么意思?

聚簇索引和非聚簇索引是数据库中两种常见的索引类型,它们有以下区别:

  1. 聚簇索引(Clustered Index):聚簇索引定义了表中数据行的物理顺序,一个表只能有一个聚簇索引。聚簇索引通常是基于主键创建的,如果没有显式定义主键,则会选择唯一非空列来作为聚簇索引。因此,查询根据聚簇索引可以快速找到具有连续物理位置的相关数据行。
  2. 非聚簇索引(Non-Clustered Index):非聚簇索引与表中的数据行物理顺序无关,它单独维护一个指向实际数据行的指针。一个表可以拥有多个非聚簇索引。非聚簇索引通过在其内部存储了被索引字段和指向实际数据行的指针来提供快速检索。

回表(Bookmark Lookup)指的是在使用非聚簇索引进行查询时,由于需要获取除了非聚簇索引之外的其他列值信息,在找到满足条件的记录后,还需要通过查找对应数据页或者堆中相应位置才能得到完整的记录。这个过程可能导致额外的IO操作和性能开销。

12、口述SQL:有一张表有学生、班级、性别等字段,如何通过一条SQL语句查出各班级分别有多少男生和女生?

你可以使用GROUP BY子句和聚合函数来实现这个需求。下面是一个示例的SQL语句:

SELECT 班级, SUM(CASE WHEN 性别 = '男' THEN 1 ELSE 0 END) AS 男生人数, SUM(CASE WHEN 性别 = '女' THEN 1 ELSE 0 END) AS 女生人数
FROM 学生表
GROUP BY 班级;

上述语句会将学生表按班级进行分组,并使用SUM函数和CASE表达式计算出各班级男生人数和女生人数。

请注意将”学生表”替换为你实际的表名,”班级”、”性别”替换为你实际的字段名。

13、手写一下快排

下面是一个简单的快速排序算法的实现,使用递归方式进行排序:

#include <iostream>
using namespace std;

// 交换两个元素的值
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 将数组划分为左右两部分,并返回基准元素的索引
int partition(int arr[], int low, int high) {
    // 选择最右边的元素作为基准
    int pivot = arr[high];

    // 初始化小于基准和大于基准的分界点
    int i = low - 1;

    for (int j = low; j <= high - 1; j++) {
        // 如果当前元素小于或等于基准,将其放入左侧分区
        if (arr[j] <= pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }

    // 将基准元素放入正确位置
    swap(arr[i + 1], arr[high]);

    return (i + 1);
}

// 快速排序函数
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        // 划分数组并获取基准索引
        int pivotIndex = partition(arr, low, high);

        // 对左侧子数组进行递归排序
        quickSort(arr, low, pivotIndex - 1);

        // 对右侧子数组进行递归排序
        quickSort(arr, pivotIndex + 1, high);
    }
}

// 测试
int main() {
    int arr[] = {9, 5, 7, 2, 4, 1, 8};
    int size = sizeof(arr) / sizeof(arr[0]);

    cout << "原始数组:";
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }

    quickSort(arr, 0, size - 1);

    cout << "\n排序后数组:";
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }

    return 0;
}

这段代码实现了快速排序算法,可以对一个整数数组进行升序排序。

发表评论

后才能评论