Skip to content

小米C++软开一二面面经

来源:https://www.nowcoder.com/feed/main/detail/006d084584424a3da3c1a0edfa0aaeff

一面

1、static有什么应用场景?

  1. 静态变量(静态成员变量):在类中声明的静态变量是该类的所有对象共享的。这些变量通常用于表示类的状态或跟踪某个类的实例的数量。静态变量可以通过类名或类的对象访问。
C
class MyClass {
public:
    static int count;  // 静态成员变量
    MyClass() {
        count++;
    }
};

int MyClass::count = 0;  // 静态成员变量的初始化

int main() {
    MyClass obj1;
    MyClass obj2;
    std::cout << MyClass::count << std::endl;  // 2
    return 0;
}
  1. 静态函数(静态成员函数): 静态函数是与类关联的函数,而不是与类的对象关联。它们通常用于执行与类相关的操作,而不需要访问类的实例。静态函数可以通过类名或类的对象调用,但不能访问非静态成员变量或函数。
C
class MathUtility {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

int result = MathUtility::add(5, 3);  // 使用静态函数
  1. 静态局部变量: 静态局部变量在函数内部声明,但只初始化一次,然后在函数多次调用时保留其值。它们通常用于保存函数的状态信息。
C
int myFunction() {
    static int counter = 0;  // 静态局部变量
    counter++;
    return counter;
}
  1. 静态类: 在 C++ 中,可以将类声明为静态,这将导致类的成员变量和成员函数也成为静态的,这意味着它们不需要类的实例来访问。静态类通常用于提供全局的、不依赖于特定对象的服务。
C
class UtilityClass {
public:
    static void doSomething() {
        // 实现某种功能
    }
};

int main() {
    UtilityClass::doSomething();  // 使用静态类的函数
    return 0;
}

2、extern解决了什么问题?

通常与变量和函数一起使用,用于解决链接器(linker)中的问题。

  1. 当你在多个源文件中定义了一个全局变量,如果你希望这些变量在不同的文件中共享,那么你需要使用 extern 来进行声明。这告诉编译器该变量在其他地方定义,这是一个外部链接变量。在链接时,链接器将查找这些变量的实际定义并链接它们在一起。

比方说,在一个源文件中定义一个全局变量:

C
// File1.cpp
int globalVar = 42;

然后,在另一个源文件中,你可以使用 extern 来声明并访问这个变量:

C
// File2.cpp
extern int globalVar;
void someFunction() {
    int value = globalVar; // 使用全局变量
}

这种方式允许你在多个文件中使用相同的全局变量,而不会导致链接错误。

  1. 外部链接函数:同样,你可以使用extern声明函数,以便在不同的源文件中调用它。这通常用于解决跨文件调用函数的问题。

示例:

C
// File1.cpp
int add(int a, int b) {
    return a + b;
}

然后,在另一个源文件中,你可以使用 extern 来声明并调用这个函数:

C
// File2.cpp
extern int add(int a, int b);
void someOtherFunction() {
    int result = add(5, 3); // 调用另一个文件中的函数
}

extern 告诉编译器该函数在其他地方定义,以便在链接时能够找到它的定义。

那顺便再讲一下“extern c”的作用?

通常用于解决 C++ 与 C 语言的混合编程问题。当你将 C++ 代码与 C 代码混合编译时,可能会遇到一些问题,因为 C++ 与 C 在函数名重载、异常处理等方面有不同的特性。extern "C" 用于告诉 C++ 编译器要按照 C 语言的方式来处理某段代码,从而确保 C++ 与 C 代码可以正确地协同工作。

它的用法主要包括这几个点:

  1. C++ 调用 C 函数: 当你需要在 C++ 代码中调用一个在 C 代码中定义的函数时,你可以使用 extern "C" 来告诉 C++ 编译器使用 C 的函数名规则,以避免 C++ 函数名的名称修饰。这是因为 C++ 支持函数重载和名称修饰,而 C 不支持。
C
extern "C" {
    int myCFunction(int param);
}

int main() {
    int result = myCFunction(42); // 在 C++ 代码中调用 C 函数
    return 0;
}
  1. C 调用 C++ 函数: 当你需要在 C 代码中调用一个在 C++ 代码中定义的函数时,也需要使用 extern "C",但这通常在头文件中声明。这样 C 编译器知道如何正确地链接 C++ 代码中的函数。

在 C++ 头文件中声明:

C
#ifdef __cplusplus
extern "C" {
#endif

void myCppFunction(int param);

#ifdef __cplusplus
}
#endif

在 C 文件中包含该头文件即可:

C
#include "myCppHeader.h"
// 然后可以调用 myCppFunction
  1. 链接 C 和 C++ 代码: 在链接阶段,使用 extern "C" 可确保链接器正确链接 C 和 C++ 代码。这是非常重要的,因为不同的语言编译出的二进制代码可能有不同的调用约定和命名规则。

3、heap和stack分别有什么作用?

  1. Stack(栈):
  • Stack 是一块自动管理内存的区域,其中存放着函数的局部变量和函数调用的上下文信息。

  • 由于是自动管理的,因此分配和释放内存非常迅速。

  • Stack 大小通常有限制,因为它是有限的物理内存区域,通常在几兆字节到几十兆字节之间。

  • 栈上的数据的生命周期与函数的生命周期相关,当函数退出时,栈上的数据就被销毁。

  • 栈遵循后进先出的原则,最后进栈的数据最先被弹出。

  1. Heap(堆):

    • Heap 是一块动态内存区域,需要手动管理内存的分配和释放,通常使用 newdeletemallocfree 等操作来进行内存管理。

    • 由于需要手动管理内存,分配和释放内存比栈慢。

    • 堆的大小通常比栈大得多,受可用物理内存的限制,理论上可以很大。

    • 堆上的数据的生命周期通常比栈上的长,可以在函数调用之间保持数据的状态。

    • 堆中的数据的分配和释放顺序不受限制,程序员需要自行管理内存,确保不会出现内存泄漏或悬挂指针。

栈用于存储函数的局部变量,它非常适合那些生命周期短、有限大小且无需手动管理的数据。堆用于存储生命周期较长、较大且需要手动管理的数据,但需要谨慎处理以避免内存泄漏和悬挂指针等问题。

4、为什么需要虚拟地址?

  1. 虚拟地址允许不同的进程或程序共享同一台计算机的物理内存,但每个进程都有自己的虚拟地址空间。这提供了内存隔离,确保一个进程无法访问另一个进程的内存,从而保护了各个进程的数据和代码的完整性。
  2. 虚拟地址空间简化了内存管理。程序员可以专注于分配和释放虚拟内存而不必担心物理内存的细节。虚拟内存管理器负责将虚拟地址映射到物理内存上,当物理内存有限时,可以使用页面置换算法来将最常用的数据加载到物理内存中。
  3. 虚拟地址提供了内存扩展的能力。当进程需要更多内存时,操作系统可以动态地将更多的虚拟地址分配给它,而无需更改程序的代码。这使得程序在不同的环境下能够运行,而无需重新编写。
  4. 虚拟地址空间允许操作系统将用户空间和内核空间分开,这有助于维护内核的安全性和稳定性。用户程序无法直接访问内核的地址空间,从而减少了操作系统受到的恶意或误操作的影响。
  5. 虚拟内存允许多个进程共享相同的虚拟地址空间,减少了物理内存的浪费。此外,虚拟内存可以通过页共享技术,使多个进程共享相同的物理内存页,而无需复制相同的数据。

5、进程通信方式?

  1. 管道(Pipes): 管道是一种半双工通信方式,主要用于具有亲缘关系的进程之间的通信。管道是一种线性数据流,数据只能单向流动。它有两种类型:
    • 无名管道(Anonymous Pipes): 通常用于父子进程之间的通信。创建管道使用 pipe() 系统调用。
    • 命名管道(Named Pipes): 用于无关联的进程之间的通信,它们以文件系统中的命名管道文件形式存在。
  2. 消息队列(Message Queues): 消息队列允许进程通过消息进行异步通信。消息队列允许多个进程通过将消息发送到队列,然后其他进程从队列中接收消息来进行通信。消息队列通常有操作系统提供的 API 来管理消息的发送和接收。
  3. 共享内存(Shared Memory): 共享内存是一种高效的通信方式,允许多个进程共享相同的物理内存区域。这使得数据在进程之间的传输非常快速,因为它们可以直接读写相同的内存。然而,共享内存需要进行同步以避免数据竞争。
  4. 信号(Signals): 信号是异步通信的一种方式,用于通知进程某些事件的发生,如错误或异常。每个信号都有一个数字标识符,当事件发生时,进程可以注册信号处理程序来处理信号。
  5. 套接字(Sockets): 套接字是一种用于网络通信的通用通信机制,但也可以在同一台计算机上的不同进程之间使用。套接字提供了面向流和面向数据报的通信方式,允许进程通过网络套接字进行通信。
  6. 文件(File): 进程可以通过读写文件来实现通信。一个进程可以将数据写入文件,而另一个进程则可以读取该文件的内容。这种方式不够高效,但是可以应用在不同进程之间的通信需求较少的情况下。
  7. 信号量(Semaphores): 信号量是一种用于控制多个进程对共享资源的访问的同步机制。信号量可以用于避免竞争条件,确保一次只有一个进程可以访问共享资源。
  8. 共享文件映射(Memory-Mapped Files): 共享文件映射允许进程将文件映射到它们的地址空间中,以便多个进程可以访问相同的文件数据。这在共享大量数据时非常有用。

6、socket可以和同一主机进程通讯吗?

可以用于同一主机进程之间的通信,这通常被称为"本地套接字"或"Unix 套接字"。

在 Unix/Linux 系统中,本地套接字是一种专用的套接字,通常使用文件系统路径来标识。

给大家介绍一些使用本地套接字进行同一主机进程间通信的常见方式:

  1. AF_UNIX 套接字类型: 本地套接字使用套接字域为 AF_UNIX (或 AF_LOCAL),而不是 AF_INET,以指示它们是本地套接字。它们通常采用 Unix 文件路径来表示套接字地址。
  2. 套接字通信模式: 本地套接字可以使用流式套接字(SOCK_STREAM)或数据报套接字(SOCK_DGRAM)模式,类似于网络套接字。流式套接字提供面向连接的、可靠的、双向的通信,而数据报套接字提供无连接的通信。
  3. 通信过程: 一个进程可以通过创建一个本地套接字,并使用 bind() 绑定到一个路径,然后使用 connect() 连接到该路径,或者使用 listen() 开始监听来自其他进程的连接。进程可以使用 send()recv()write()read() 等系统调用在套接字上进行通信。
  4. 文件路径: 本地套接字的文件路径通常位于文件系统中,可以在文件系统中使用。这使得进程可以通过文件路径来找到套接字地址,而不需要像网络套接字那样使用 IP 地址和端口号。

二面

1、手撕--三数之和

思路:

  1. 对给定的整数数组 nums 进行排序。排序是为了确保相同的元素都相邻,这有助于避免重复的三元组。
  2. 遍历排序后的数组 nums,将当前元素作为潜在的三元组中的第一个元素。
  3. 对于每个第一个元素 nums[i],使用左右两个指针(leftright)在剩余部分中查找另外两个元素。初始化 left 指向 i+1right 指向数组末尾。
  4. 在内部循环中,计算当前三元组的和 sum = nums[i] + nums[left] + nums[right]
    1. 如果 sum 等于零,说明找到了一个满足条件的三元组,将 [nums[i], nums[left], nums[right]] 添加到结果中。
    2. 如果 sum 大于零,将 right 指针向左移动以减小和。
    3. 如果 sum 小于零,将 left 指针向右移动以增加和。
  5. 为了避免得到重复的三元组,需要在内部循环中处理重复元素。如果当前元素与前一个元素相同,可以跳过它,以避免重复结果。
  6. 在外部循环中,如果下一个元素与当前元素相同,也需要跳过,以避免得到重复的结果。

参考代码:

C
#include <vector>
#include <algorithm>

std::vector<std::vector<int>> threeSum(std::vector<int>& nums) {
    std::vector<std::vector<int>> result;
    int n = nums.size();

    // Sort the array
    std::sort(nums.begin(), nums.end());

    for (int i = 0; i < n - 2; ++i) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;  // Skip duplicates

        int left = i + 1, right = n - 1;

        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                result.push_back({nums[i], nums[left], nums[right]});
                while (left < right && nums[left] == nums[left + 1]) left++;  // Skip duplicates
                while (left < right && nums[right] == nums[right - 1]) right--;  // Skip duplicates
                left++;
                right--;
            } else if (sum < 0) {
                left++;
            } else {
                right--;
            }
        }
    }

    return result;
}

int main() {
    std::vector<int> nums = {-1, 0, 1, 2, -1, -4};
    std::vector<std::vector<int>> result = threeSum(nums);

    std::cout << "Triplets that sum to zero:" << std::endl;
    for (const std::vector<int>& triplet : result) {
        for (int num : triplet) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

2、常用线程同步方式?

  1. 互斥锁(Mutex):

    • 互斥锁是最常见的线程同步机制,用于防止多个线程同时访问临界区(共享资源)。

    • 通过使用 std::mutex 或类似的库提供的互斥锁,可以实现互斥访问共享数据。

    • C
      std::mutex mtx;    
      // ...
      mtx.lock();
      // 访问共享数据
      mtx.unlock();
  2. 条件变量(Condition Variables):

    • 条件变量用于等待某个条件变为真,然后唤醒等待的线程。

    • 通常与互斥锁一起使用,以实现等待-通知模式。

    • C
      std::mutex mtx;
      std::condition_variable cv;
      bool condition = false;
      
      // 线程1
      mtx.lock();
      while (!condition) {
          cv.wait(mtx);
      }
      // 执行线程1的操作
      mtx.unlock();
      
      // 线程2
      mtx.lock();
      condition = true;
      cv.notify_all();
      mtx.unlock();
  3. 信号量(Semaphore):

    • 信号量是一个计数器,用于控制同时访问共享资源的线程数量。

    • 通常有两种类型:二进制信号量和计数信号量。

    • C
      std::mutex mtx;
      std::condition_variable cv;
      int count = 0;
      
      // 线程1
      mtx.lock();
      while (count == 0) {
          cv.wait(mtx);
      }
      count--;
      mtx.unlock();
      
      // 线程2
      mtx.lock();
      count++;
      cv.notify_all();
      mtx.unlock();
  4. 读写锁(Read-Write Lock):

    • 读写锁允许多个线程同时读取共享数据,但只允许一个线程写入数据。

    • 适用于读操作远多于写操作的情况。

    • C
      std::shared_mutex rwLock;
      
      // 读线程
      rwLock.lock_shared();
      // 读取共享数据
      rwLock.unlock_shared();
      
      // 写线程
      rwLock.lock();
      // 写入共享数据
      rwLock.unlock();
  5. 自旋锁(Spinlock):

    • 自旋锁是一种不会让线程进入睡眠状态的锁,而是一直忙等待锁的释放。

    • 适用于短暂的临界区,因为它可能会消耗大量的 CPU 时间。

    • C
      std::atomic_flag spinLock = ATOMIC_FLAG_INIT;
      // ...
      while (spinLock.test_and_set(std::memory_order_acquire)) {
          // 忙等待
      }
      // 访问共享数据
      spinLock.clear(std::memory_order_release);

3、虚拟内存和物理内存的关系?

  1. 虚拟内存:
    • 虚拟内存是操作系统为每个进程提供的一种抽象概念,它为每个进程分配一块连续的地址空间,通常是4GB(32位系统)或更大(64位系统)。
    • 进程中的程序可以认为是运行在自己的独立虚拟地址空间中,这使得每个进程都有相同的地址范围,简化了程序的编写。
    • 虚拟内存的主要目的是提供了一个抽象层,将程序员从底层物理内存的管理中解脱出来,同时允许更多的进程共享物理内存。
  2. 物理内存:
    • 物理内存是计算机实际的硬件内存,通常由RAM(随机访问存储器)组成。它是计算机用于存储数据和程序的实际硬件资源。
    • 物理内存的大小是硬件限制的,通常比虚拟内存要小,因此虚拟内存允许操作系统有效地管理物理内存的使用。
  3. 虚拟内存与物理内存的关系:
    • 当进程在虚拟内存中分配内存或执行指令时,操作系统负责将虚拟地址转换为物理地址。这个过程通常被称为地址映射。
    • 操作系统使用页表(Page Table)或类似的数据结构来维护虚拟地址到物理地址的映射关系。
    • 虚拟内存允许多个进程共享相同的物理内存,同时通过分页和分段等技术,允许操作系统将数据从物理内存中交换到磁盘上以释放内存资源。
    • 当进程访问虚拟内存中的数据时,操作系统会根据页表将对应的物理内存数据加载到RAM中,或者在需要时从磁盘加载。
    • 虚拟内存还提供了内存隔离和保护,一个进程不能直接访问其他进程的虚拟内存,从而增加了系统的稳定性和安全性。

4、零拷贝?

"零拷贝" 是一种优化技术,旨在最小化数据在内存中的复制操作,从而提高数据传输的效率。这对于高性能计算和网络应用程序非常重要。

  1. 什么是零拷贝?

零拷贝是指在数据传输过程中避免不必要的数据拷贝,即将数据从一个地方移动到另一个地方而不是复制数据。这通常涉及在不涉及数据复制的情况下,直接将数据从一个地方传输到另一个地方。

  1. 零拷贝的应用场景?

    • 文件传输:从磁盘读取文件并将其发送到网络或其他媒体,或者将数据从网络接收并写入磁盘。

    • 网络传输:将数据从一个套接字传输到另一个套接字,例如,通过Socket传输数据。

    • 内存映射:将文件映射到内存,以便在不涉及数据复制的情况下进行访问。

  2. 零拷贝的工作原理

零拷贝的核心思想是通过操作系统或硬件提供的特性,将数据直接传输到目标位置,而不需要在中间进行数据复制。

  • DMA: 许多现代计算机和网络设备支持DMA,它允许外部设备(如网络适配器或磁盘控制器)直接访问系统内存。这样,数据可以直接从设备读取到内存,或者从内存发送到设备,而无需通过CPU进行数据拷贝。
  • 内存映射: 部分操作系统允许文件内容映射到进程的地址空间,这意味着文件的内容可以直接从磁盘读取到进程的内存,而不需要额外的数据拷贝。这在文件操作和数据库管理中很常见。
  • 零拷贝网络传输: 高性能网络库(如DPDK、RDMA等)允许网络数据包在不涉及数据拷贝的情况下直接从网络适配器传输到内存或另一个网络适配器。
  1. 零拷贝的优点

    • 提高性能:避免了不必要的数据拷贝,减少了CPU和内存带宽的开销,从而提高了数据传输的效率。

    • 减少内存占用:不进行中间数据拷贝可以减少内存占用。

    • 减少CPU使用率:避免了数据拷贝所需的CPU时间,降低了CPU的负载。

5、TCP、UDP、HTTP的头部常用字段和占用字节?

  1. TCP(传输控制协议)头部字段:

    • 源端口(16位): 标识发送端的端口号。

    • 目标端口(16位): 标识接收端的端口号。

    • 序列号(32位): 用于保证数据的有序传输,以及处理数据的重复问题。

    • 确认号(32位): 指示已经成功接收的字节数,用于确认收到的数据。

    • 数据偏移(4位): 指示TCP头部的长度,以便正确解析后续的数据。

    • 保留位(6位): 保留字段,用于将来的扩展。

    • 控制位(6位): 包括 SYN、FIN、RST、PSH、ACK 和 URG 标志,用于控制连接和数据传输。

    • 窗口大小(16位): 指示发送端的接收窗口大小,用于流量控制。

    • 校验和(16位): 用于校验头部和数据的完整性,以检测传输中的错误。

    • 紧急指针(16位): 仅在 URG 控制位被设置时有效,指示紧急数据的结束位置。

    • 选项和填充: 可变长度,用于支持各种TCP选项,如最大报文段长度(MSS)、窗口缩放等。

TCP头部长度在通常情况下为20字节,但根据选项的使用可能会更长。

  1. UDP(用户数据报协议)头部字段:

    • 源端口(16位): 标识发送端的端口号。

    • 目标端口(16位): 标识接收端的端口号。

    • 长度(16位): UDP数据报文的长度,包括头部和数据。

    • 校验和(16位): 用于校验UDP数据的完整性,但它不是强制性的。

UDP头部固定为8字节。

HTTP(超文本传输协议)头部字段:

HTTP头部字段可以根据HTTP版本和请求/响应的类型而变化,但下面是一些常见的HTTP头部字段:

  • Host(不定长度): 标识目标服务器的主机名。
  • User-Agent(不定长度): 标识发送请求的用户代理(通常是浏览器或应用程序)。
  • Accept(不定长度): 指定客户端可以接受的媒体类型。
  • Content-Type(不定长度): 指定实体主体的媒体类型。
  • Content-Length(不定长度): 指定实体主体的长度。
  • Authorization(不定长度): 包含身份验证凭证,用于访问受保护资源。
  • Cookie(不定长度): 包含在之前的响应中设置的HTTP Cookie。
  • Set-Cookie(不定长度): 用于在响应中设置HTTP Cookie。
  • Location(不定长度): 用于重定向响应。

HTTP头部字段的长度取决于字段的数量和值的长度,它们是可变长度的。

6、TCP流量控制和拥塞控制?

流量控制:

  1. TCP使用滑动窗口机制来进行流量控制。接收方会告知发送方它可以接收多少字节的数据,这被称为窗口大小。发送方会根据窗口大小来发送数据,确保不会超出接收方的容量。
  2. 接收方通过TCP头部中的窗口字段通告窗口大小,以告知发送方可以接收多少字节的数据。这个窗口可以动态调整,以反映接收方的处理速度和可用内存。
  3. 每次接收到一部分数据后,接收方可以通告一个新的窗口大小,使发送方可以发送更多的数据。这个机制允许发送方根据接收方的处理速度动态调整发送速率。
  4. 如果接收方的窗口大小为0,表示接收方已经饱和,发送方必须等待,直到接收方通告了更大的窗口大小。

拥塞控制:

  1. 拥塞控制使用拥塞窗口来限制发送方的发送速率。拥塞窗口的大小会动态调整,以避免过度拥塞。
  2. 网络拥塞时,路由器和交换机可能会发送拥塞信号,或者丢弃过多的数据包。这些信号通常被解释为网络已经拥塞。
  3. TCP发送方使用慢启动和拥塞避免算法来逐渐增加发送速率,并在出现拥塞信号时快速减小速率。这些算法通过动态调整拥塞窗口大小来实现。
  4. 如果发送方检测到丢失的数据包,它可以通过快速重传和快速恢复来减少拥塞窗口的大小,并尝试更快地恢复发送速率。
  5. 当发送方接收到拥塞信号,它会立即减小拥塞窗口,然后逐渐增加,以找到适当的发送速率。

7、多继承?C++中一般如何解决?

多继承允许一个派生类从多个基类继承特性和成员。例如,一个类可以同时继承自一个基类A和一个基类B。这样,派生类将拥有基类A和基类B的成员和特性。

  1. 菱形继承

当一个类从两个不同的路径继承同一个基类时,可能会导致二义性。这种情况通常称为"菱形继承",它可以通过虚继承来解决。

  1. 二义性

当派生类继承自多个基类,且这些基类中有相同的成员或函数名称,可能会导致编译器不知道应该使用哪个版本,从而引发二义性。

解决多继承问题的方法:

  • 虚继承: 如果一个派生类通过多个路径继承同一个基类,可以使用虚继承来解决菱形继承问题。虚继承通过让中间的基类成为虚基类,确保只有一个实例被派生类继承。
c++
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
  • 重命名冲突成员: 如果派生类继承了多个基类,其中有相同名称的成员,可以在派生类中重命名这些成员,以消除二义性。
c++
class Base1 {
public:
    void commonFunc() { /* ... */ }
};

class Base2 {
public:
    void commonFunc() { /* ... */ }
};

class Derived : public Base1, public Base2 {
public:
    void derivedFunc() { /* ... */ }
    void commonFunc1() { Base1::commonFunc(); }
    void commonFunc2() { Base2::commonFunc(); }
};
  • 使用虚函数: 如果有相同名称的函数,你可以使用虚函数来实现多态,从而避免二义性问题。
c++
class Base1 {
public:
    virtual void commonFunc() { /* ... */ }
};

class Base2 {
public:
    virtual void commonFunc() { /* ... */ }
};

class Derived : public Base1, public Base2 {
public:
    void derivedFunc() { /* ... */ }
};

8、C++四种强制类型转换?

  1. static_cast:

    • static_cast 用于执行基本的类型转换,如将一个整数转换为浮点数,或者将指针转换为不同类型的指针。

    • 这种转换在编译时进行,通常是安全的,但需要程序员确保类型转换是有意义的,否则可能导致不安全的操作。

c++
double x = 3.14;
int y = static_cast<int>(x);
  1. dynamic_cast:

    • dynamic_cast 用于在继承体系中进行安全的向下转型(即从基类指针或引用转换为派生类指针或引用)。

    • 这种转换只适用于具有多态性(使用虚函数)的类,它在运行时检查是否可以执行类型转换。

c++
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
    // 转型成功
} else {
    // 转型失败
}
  1. const_cast:

    • const_cast 主要用于去除变量的 const 修饰符,以便对其进行修改。这种转换用于转换掉常量性。

    • 使用 const_cast 转换可能导致未定义的行为,除非被转换的变量本来就是非常量。

c++
const int x = 5;
int* y = const_cast<int*>(&x); // 去除常量性
  1. reinterpret_cast:

    • reinterpret_cast 用于执行较低级别的类型转换,如将指针转换为整数或将整数转换为指针。这种转换通常很危险,只有在特殊情况下才应使用。

    • reinterpret_cast 进行的转换完全取决于编译器和底层硬件,因此不具有可移植性。

c++
int x = 42;
int* xPtr = &x;
uintptr_t intValue = reinterpret_cast<uintptr_t>(xPtr);

9、列举一下C++11新特性---移动语义、lambda函数?

  1. 移动语义:

    • 移动语义是 C++11 引入的一个关键特性,旨在提高资源管理的效率,特别是在处理动态分配的内存和大型数据结构时。

    • 它引入了右值引用(Rvalue References)和移动构造函数(Move Constructors)来支持资源的高效转移而不是拷贝。

    • 使用右值引用,可以将一个对象的资源有效地“移动”到另一个对象,而不必复制数据。这对于容器和智能指针等数据结构特别有用。

c++
std::vector<int> source;
std::vector<int> destination = std::move(source); // 使用 std::move 移动资源
  1. Lambda 函数:

    • Lambda 函数是一种匿名函数,允许在代码中定义短小的、临时的函数。它们通常与标准库算法和容器结合使用,以提供灵活的功能。

    • Lambda 函数通过 [capture](parameters) -> return_type { body } 的语法进行定义。capture 允许你捕获外部变量,parameters 是函数参数,return_type 是返回类型,body 是函数体。

    • Lambda 函数可以直接在函数调用中使用,也可以作为参数传递给算法函数。

c++
std::vector<int> numbers = {1, 2, 3, 4, 5};
int total = 0;
// 使用 Lambda 函数计算总和
std::for_each(numbers.begin(), numbers.end(), [&total](int x) { total += x; });

其它的大家自行了解,前边文章有说过。

10、C++完美转发?

C++ 中的完美转发是一项在函数模板中的技术,用于在不丢失函数参数信息的情况下将参数传递给其他函数。

xxxxxxxxxx class Base {public:    // 构造函数不能声明为虚函数    Base() {        std::cout << "Base Constructor" << std::endl;   }​    // 虚析构函数    virtual ~Base() {        std::cout << "Base Destructor" << std::endl;   }};​class Derived : public Base {public:    // 构造函数    Derived() {        std::cout << "Derived Constructor" << std::endl;   }​    // 虚析构函数    virtual ~Derived() {        std::cout << "Derived Destructor" << std::endl;   }};​int main() {    Base* obj = new Derived(); // 通过基类指针指向派生类对象​    delete obj; // 通过基类指针删除对象,应该调用派生类的析构函数​    return 0;}C++

  1. 右值引用和 std::forward: 完美转发使用右值引用(Rvalue Reference)来捕获传递给函数的参数。同时,为了将参数传递给其他函数,它使用 std::forward 来保持参数的值类别。
c++
template <typename T>
void wrapper(T&& arg) {
    other_function(std::forward<T>(arg));
}
  1. 模板参数和转发: 完美转发通常结合模板参数来实现。函数模板接受通用引用(Universal Reference),即 T&&,这允许函数接受左值和右值。
c++
template <typename T>
void forwarder(T&& arg) {
    target_function(std::forward<T>(arg));
}

11、进程调度的常用算法?

  1. 先来先服务调度(First-Come, First-Served,FCFS):

    • 简单且易于实现。
    • 不会产生饥饿问题(Starvation)。
    • 缺点是平均等待时间可能较长,特别是在存在长任务的情况下。
  2. 短作业优先调度(Shortest Job First,SJF):

    • 尝试选择运行时间最短的进程,以最小化等待时间。
    • 可能产生饥饿问题,因为长任务可能永远无法运行。
  3. 优先级调度(Priority Scheduling):

    • 每个进程分配一个优先级,调度器选择优先级最高的进程运行。
    • 可能导致高优先级进程饥饿。
  4. 轮转调度(Round Robin):

    • 每个进程被分配一定的时间片,然后排队等待下一个时间片。
    • 避免了饥饿问题,但可能导致高优先级进程等待时间过长。
  5. 多级反馈队列调度(Multi-Level Feedback Queue,MLFQ):

    • 将进程按优先级分成多个队列,不断提高或降低它们的优先级。
    • 综合了其他算法的优点,适用于多种情况。
  6. 最高响应比优先调度(Highest Response Ratio Next,HRRN):

    • 优先选择与等待时间比例最高的进程。
    • 具有短作业优先算法的好处,但不容易受到长作业的干扰。
  7. 最短剩余时间优先调度(Shortest Remaining Time Next,SRTN):

    是 SJF 的抢占版本,会在更短的任务到来时中断正在运行的任务。

详细一点的可以翻翻我之前的总结,总结还是挺详细的,今天在这里就不过多赘述。