C++面试篇之八股文系列

问题1:进程和线程的区别

进程和线程是现代操作系统中实现并发执行和资源共享的不同层次的抽象概念,它们之间的主要区别如下:

  1. 资源分配

    • 进程:是操作系统进行资源分配和保护的基本单位,每个进程都有其独立的地址空间,包含了代码、数据、堆和栈等。这意味着不同的进程之间各自拥有独立的内存区域,它们之间不能直接访问彼此的内存,若需通信,则需要通过IPC(进程间通信)机制。
    • 线程:是进程内部执行流程的独立控制流,它是系统调度和执行的最小单位。同一进程内的所有线程共享相同的地址空间,包括全局变量、文件描述符等资源。这意味着线程间的通信更为直接且高效,可以直接读写同一块内存。
  2. 创建和切换成本

    • 创建一个新进程需要分配独立的地址空间和其他相关资源,开销较大。
    • 创建一个新线程只需为其分配少量的线程控制块(TCB)和栈空间,相对而言开销较小。线程间的切换比进程间的切换更快,因为不需要切换整个地址空间。
  3. 并发执行

    • 进程可以在系统中并发执行,彼此独立。
    • 同一进程内的多个线程也可以并发执行,利用多核处理器或多处理器系统的优势,实现并行计算。
  4. 控制和协作

    • 进程间的同步和通信通常比较复杂,需要借助管道、消息队列、共享内存、信号量等方式。
    • 同一进程内的线程可以方便地共享数据和同步执行,但同时也需要更为精细的同步机制(如互斥锁、条件变量)以避免竞态条件和死锁等问题。

总结来说,进程提供了资源隔离和保护,而线程则提供了在进程内部的并发执行和资源共享。在设计和实现多任务系统时,选择合适的进程与线程模型有助于提高系统的并发性能和资源利用率。

问题2:c++如何实现多线程并发

#include <iostream>
2#include <thread>
3#include <vector>
4
5// 定义一个将要在单独线程中运行的函数
6void thread_function(int id) {
7    std::cout << "Hello from thread " << id << std::endl;
8}
9
10int main() {
11    // 创建一个包含两个线程的对象数组
12    std::vector<std::thread> threads;
13
14    // 创建并启动两个线程,每个线程执行thread_function函数
15    for (int i = 0; i < 2; ++i) {
16        threads.emplace_back(thread_function, i + 1); // 使用移动构造函数创建线程
17    }
18
19    // 确保所有线程完成执行
20    for (auto& t : threads) {
21        t.join(); // 主线程调用join等待每个线程结束
22    }
23
24    std::cout << "All threads finished." << std::endl;
25
26    return 0;
27}

在这个例子中:

  • std::thread 类是用来创建和管理线程的。
  • thread_function 是你要在线程中执行的任务,它接受一个整数参数作为线程标识。
  • threads.emplace_back() 方法用于创建一个新的线程并将它添加到 threads 向量中。这里传递给 emplace_back 的第一个参数是线程所执行的函数对象,后面跟着该函数所需的参数。
  • t.join() 方法让主线程等待对应线程 t 执行完毕。如果不调用 join,线程将在其对应的 std::thread 对象析构时自动终止(如果线程尚未完成),这可能导致数据不一致或资源泄漏问题。

此外,在并发编程中,还需要注意线程安全和同步问题,例如使用互斥锁 (std::mutex) 和条件变量 (std::condition_variable) 来保护共享资源,防止数据竞争和死锁的发生。

问题3:如何解决懒汉式单例的线程安全:

当你需要实现一个线程安全的单例类,并且希望在首次调用 getInstance() 方法时才初始化单例对象,这时可以在getInstance()函数内部创建一个局部静态互斥量。当多个线程同时进入getInstance()时,互斥量可以确保只有一个线程完成初始化操作。

1class Singleton {
2private:
3    Singleton() {}
4    Singleton(const Singleton&) = delete;
5    Singleton& operator=(const Singleton&) = delete;
6
7    static std::mutex mtx; // 注意:在C++11之前,局部静态互斥量无法用于解决这个问题
8    static Singleton* instance;
9public:
10    static Singleton& getInstance() {
11        static std::mutex localMtx; // 局部静态互斥量
12        std::lock_guard<std::mutex> lock(localMtx);
13        if (!instance) {
14            instance = new Singleton();
15        }
16        return *instance;
17    }
18};

:自C++11开始,局部静态变量的初始化已经被标准化为线程安全的过程,上述示例中对局部静态互斥量的使用是为了演示旧版本C++可能遇到的情况。在现代C++中,可以直接使用静态局部变量(如 static Singleton* instance{nullptr};),其初始化过程已经隐含了线程安全性。

问题4:如何通过原子操作实现懒汉单例:

在C++11及更高版本中,可以利用C++标准库提供的原子操作(std::atomic)和局部静态变量的线程安全初始化特性,来实现线程安全的懒汉式单例模式,无需显式使用互斥量。以下是一个例子:

#include <mutex>
#include <memory>

class Singleton {
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // C++11及以上版本,利用std::once_flag和std::call_once实现线程安全的初始化
    static std::unique_ptr<Singleton> instance;
    static std::once_flag initFlag;

public:
    static Singleton& getInstance() {
        std::call_once(initFlag, [] {
            instance = std::make_unique<Singleton>();
        });
        return *instance;
    }
};

// 初始化为 nullptr,将在首次调用getInstance()时由std::call_once初始化
std::unique_ptr<Singleton> Singleton::instance(nullptr);
std::once_flag Singleton::initFlag;

// 使用方式
Singleton& s = Singleton::getInstance();

在这个实现中,std::call_once 和 std::once_flag 能够确保 Singleton 类的实例只被初始化一次,而且初始化过程是线程安全的。无论多少个线程同时调用 getInstance() 函数,只会有一个线程执行初始化操作,其余线程等待初始化完成后再返回实例引用。

如果你使用的是C++11之前的版本,那么可以借助全局互斥量实现线程安全,不过这并不是最优解,因为会导致每次获取单例时都要加锁,性能上不如 double-checked locking 或者上述基于 std::call_once 的方案。

问题5:信号量解决的场景和实例

std::Semaphore 是C++20标准库中新加入的一种同步机制,它扩展了线程同步原语,用于控制同时访问某一资源的线程数量。信号量与互斥量的主要区别在于,互斥量允许多个线程对资源的串行访问,而信号量可以控制并发访问资源的线程数量上限。

std::Semaphore 主要用于以下场景:

  1. 资源池管理:比如有固定数量的数据库连接、文件句柄或其他类型的资源,信号量可以确保同时激活的资源不超过预设的数量。

    1#include <semaphore>
    2#include <vector>
    3#include <thread>
    4
    5std::vector<int> resources; // 假设这是我们需要管理的资源集合
    6std::Semaphore resourceSemaphore(5); // 最多允许5个线程同时访问资源
    7
    8void workerThread() {
    9    resourceSemaphore.acquire(); // 获取信号量,如果资源已满,则阻塞等待
    10    // ... 使用资源 ...
    11    resources.push_back(...); // 假设添加一个资源
    12    // ... 完成资源使用 ...
    13    resources.pop_back(); // 假设移除一个资源
    14    resourceSemaphore.release(); // 释放信号量,允许其他线程访问
    15}
    16
    17int main() {
    18    for (int i = 0; i < 10; ++i) {
    19        std::thread t(workerThread);
    20        t.detach();
    21    }
    22
    23    // ... 等待所有线程完成 ...
    24}
  2. 生产者-消费者模型:在缓冲区有限制大小的队列系统中,信号量可以控制生产者往队列中放入元素的速度和消费者从队列中取出元素的速度,从而避免队列溢出或空指针异常等问题。

  3. 任务分发:当有大量任务需要处理,但又不想一次性全部启动,可以使用信号量限制同时运行的任务数量。

由于C++20之前的C++标准库并未内置std::Semaphore,在实际开发中,如果需要信号量功能,往往会借助操作系统API(如POSIX semaphores)或是第三方库来实现。在C++20及更新的标准中,你可以直接使用std::Semaphore来进行此类线程同步操作。

问题6:线程数极限问题

一般2000个就资源枯竭,一个进程控制开线程数低于500个,最好是低于200个;

问题7:unique_lock和lock_guard的区别

std::lock_guard

  • std::lock_guard 是一种更简洁的互斥量管理类,构造时立即锁定互斥量,析构时自动解锁互斥量。
  • 不支持手动锁定和解锁,也不支持调度锁定和try_lock等功能,因此在简单、确定的同步场景下更为方便和安全。
  • 使用std::lock_guard可以确保即使发生异常,互斥量也能得到正确释放,因为它遵循RAII原则。
  • std::mutex mtx;
    2void foo() {
    3    std::lock_guard<std::mutex> guard(mtx);
    4    // 在此处的代码块中,互斥量mtx被锁定
    5    // 即使这段代码抛出异常,互斥量也会在guard离开作用域时自动解锁
    6}

std::unique_lock

  • std::unique_lock 提供了更多控制互斥量的方法,比如可以手动锁定和解锁互斥量,甚至可以尝试锁定互斥量(non-blocking)。
  • 可以随时决定何时锁定互斥量,通过调用lock()unlock()try_lock()等方法。
  • 支持调度锁定(deferred locking),也就是说,构造时可以选择不立即锁定互斥量,这对于某些复杂的同步逻辑非常有用。
  • std::unique_lock还支持升级锁和降级锁的功能,可以与std::shared_mutex配合使用,实现从独占锁到共享锁的转换,或者反过来
  • std::mutex mtx;
    std::unique_lock<std::mutex> ulock(mtx, std::defer_lock); // 不立即锁定
    if (ulock.try_lock()) { // 尝试非阻塞锁定
        // 在此处的代码块中,互斥量mtx已被锁定
    } else {
        // 未能锁定互斥量,可以执行其他操作
    }
    ulock.unlock(); // 手动解锁互斥量

问题8:std:mutex与std::shared_mutex的区别:

std::shared_mutex是C++17引入的一种同步原语,用于提升在多线程环境下对共享资源的访问效率。与传统的互斥锁(如std::mutex)不同,std::shared_mutex允许多个线程以只读模式共享对资源的访问,但写入操作必须独占资源,防止同时有其他线程对共享资源进行读取或写入。这允许了更高的并行性,特别适用于读操作远多于写操作的场景。以下是提升读并发,写独占,通过std::shared_mutex实现独享锁和共享锁的转换的例子:

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_mutex mutex;
int shared_data = 0; // 假定这是需要被多线程共享的数据

// 写数据函数,使用独占锁
void WriteData(int value) {
    std::unique_lock<std::shared_mutex> lock(mutex); // 获取独占锁
    shared_data = value;
    std::cout << "Data written: " << value << std::endl;
}

// 读数据函数,使用共享锁
void ReadData() {
    std::shared_lock<std::shared_mutex> lock(mutex); // 获取共享锁
    std::cout << "Data read: " << shared_data << std::endl;
}

int main() {
    std::vector<std::thread> threads;

    // 创建一个写线程
    threads.emplace_back(WriteData, 100);

    // 创建多个读线程
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(ReadData);
    }

    // 等待所有线程完成操作
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

问题9:http的状态码:

  1. 1xx 系列:信息性状态码,表示服务器已经接收到请求并正在处理,但需要进一步的操作才能完成请求。常见的有100(继续)和101(切换协议)。123
  1. 2xx 系列:成功状态码,表示请求已经成功被服务器接收、理解和处理。常见的有200(成功)、201(已创建)、202(已接受)、204(无内容)和206(部分内容)。12345
  1. 3xx 系列:重定向状态码,表示客户端需要进行进一步的操作才能完成请求。常见的有301(永久移动)、302(临时移动)、304(未修改)和307(临时重定向)。1235
  1. 4xx 系列:客户端错误状态码,表示客户端发起的请求有错误或无法完成。常见的有400(错误请求)、401(未授权)、403(禁止)和404(未找到)。135
  1. 5xx 系列:服务器错误状态码,表示服务器在尝试处理请求时发生内部错误。常见的有500(服务器内部错误)和503(服务不可用)。

问题10:unordered_map和map的区别:

1.实现方式:map:红黑树 VS unordered_map:哈希表(crc32,MD5,hash)

2.优缺点:map:有序 && 费内存(保存父节点和孩子结点)unordered_map:无序 && 时间复杂度O(1)

问题11:如何解决哈希冲突:

  1. 开放定址法(Open Addressing):当发生冲突时,通过一个小的算法计算新的哈希地址,并在新的地址插入数据。如线性探测、二次探测、再哈希等。

  2. 链地址法(Separate Chaining):将所有哈希值相同的元素构成一个链表存储在哈希表的同一个位置。

  3. 再哈希法(Rehashing):使用另一个哈希函数来处理原哈希函数的冲突。

  4. 公共溢出区法(External Overflow):使用一个公共溢出区域存储所有的哈希冲突。

  5. 建立公共溢出区(Consistent Hashing):适用于分布式缓存等场景,保证数据分布均匀。

问题12:linux常用命令:

  • 压缩文件:tar -czvf xx.tar.gz xx

  • 解压文件:tar -xzvf xx.tar.gz

问题13:从c++文件到生成exe文件经过哪些步骤?

预处理,编译,汇编,链接

问题14:插入排序的原理:

int a[] = {4, 0, 2, 3, 1}, i, j, t;
for(i = 1;i < 5;i++) {
    t = a[i];
    j = i - 1;
    while(j >= 0 && t > a[j]){
        a[j + 1] = a[j];
        --j;
    }
    a[j + 1] = t;
}

问题15:常量指针两种等价定义:

const int *p && int const *p

问题16:递归的时间复杂度和空间复杂度

时间复杂度:O(N)  

空间复杂度:O(N)

问题17:数组存储方式:

行优先

问题18:重载,覆盖,隐藏的区别:

重载:同一个类,函数名相同但参数不同,virtual可有可无

覆盖:不同类(基类与派生类),名字相同参数相同,基类virtual必须有

隐藏:不同类,(函数名相同&&参数不同) ||(函数名相同&&参数不同&&基类无virtual)

问题19:sizeof和strlen的区别:

sizeof计算的是变量整体占用的内存大小,包括了字符串末尾的结束符“\0”

strlen是遇到结束符则停止,所以 strlen出来的结果是不包含\0的。

问题20:fopen的参数:

r :打开只读文件,该文件必须存在。

w :打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。

a :以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。

rb :打开一个二进制文件,只允许读取操作

wb :打开(创建)一个二进制文件,只允许写入操作

ab :打开一个二进制文件,在文件后追加操作

r+ :具有读写属性,从文件头开始写,保留原文件中没有被覆盖的内容;

w+ :具有读写属性,写的时候如果文件存在,会被清空,从头开始写。

a+ :以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。

rb+ :打开一个二进制文件,允许读写数据

wb+ :打开(创建)一个二进制文件,允许读写数据

ab+ :打开一个二进制文件,允许读或在文件末追加数据

问题21:vector的push_back和emplace_back的区别

std::vector::emplace_back 函数接受可变数量的参数,并使用这些参数构造一个新元素,然后将其插入到 std::vector 的末尾,这个函数的优点是可以避免额外的拷贝或移动操作,从而提高性能。

我们可以发现 emplace_back 的输入参数类型是 万能引用,入参通过 完美转发 给内部 ::new 进行对象构造,并将其追加到数组对应的位置。

测试例程里 datas.emplace_back("ee");,它插入对象元素,并没有触发拷贝构造和移动构造。因为 emplace_back 接口传递的是字符串常量,而真正的对象构造是在内部实现的:::new ((void*)__p) _Up(std::forward<_Args>(__args)...); ,在插入对象元素的整个过程中,并未产生须要拷贝和移动的 临时对象

问题22如何让函数可以接受左值或者右值传参?

C++11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值,但是注意了:只有发生类型推导的时候,T&&才表示万能引用(如模板函数传参就会经过类型推导的过程);否则,表示右值引用

template<typename T>

void func(T&& param) {

cout << param << endl;

}

问题23:引用折叠规则

C++中不允许对引用再进行引用,对于上述情况的处理,引用折叠只有两条规则:

一个 rvalue reference to an rvalue reference 会变成 (“折叠为”) 一个 rvalue reference.

所有其他种类的"引用的引用" (i.e., 组合当中含有lvalue reference) 都会折叠为 lvalue reference.

情况1:

/*参数左值,实参左值,实际左值*/
template<typename T>

void func(T& param) {

cout << param << endl;

}

int main(){

int num = 2021;

int& val = num;

func(val);

}

情况2:

/*参数左值,实参右值,实际左值*/
template<typename T>

void func(T& param) {

cout << param << endl;

}

int main(){

int&& val = 2021;

func(val);

}

情况3:

/*参数:(右值)推导类型万能引用 实参左值,实际左值*/
template<typename T>

void func(T&& param) {

cout << param << endl;

}

int main(){

int num = 2021;

int& val = num;

func(val);

}

情况4:

/*参数:右值 推导类型万能引用 实参右值 实际右值*/
template<typename T>

void func(T&& param) {

cout << param << endl;

}

int main(){

func(4);

}

问题24:如果保证传递变量类型不变

使用完美转发std::forword,可以保证参数传递类型不变,即传递右值则保持右值类型进行传递

template<typename T>

void warp(T&& param) {

func(std::forward<T>(param));

}

问题25:左值引用和右值引用是什么值

都是左值,右值引用通过指向右值,可以改变右值,但是本身是一个左值

问题26:如果让左值变成右值

使用移动语义std::move(int& val),将左值转换为右值

问题27:类有哪些是默认生成,哪些不会默认生成, 规则是什么?

一定有默认:构造函数,析构函数,拷贝构造函数,拷贝赋值函数

不一定有默认:移动构造函数,移动赋值函数

如果有定义拷贝构造函数,则不会生产移动构造和移动赋值;

如果显式声明了移动构造函数或移动赋值运算符,则拷贝构造函数和拷贝赋值运算符将被 隐式删除(因此程开发人员必须在需要时实现拷贝构造函数和拷贝赋值运算符)

问题28:交换对象里面有指针成员,如何优化swap()函数?

template<class T>
void swap(T &a, T &b) {
    T temp = a; // 调用拷贝构造函数
    a = b; // 调用operator=
    b = temp; // 调用operator=
}

通过move语义将对象转换为右值,进行交换,避免一次复制拷贝和两次赋值运算符重载

template<class T>
void swap(T &a, T &b) {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

问题29:下列函数的函数运行结果?

int main()
{
  int a = 1;
  int &&b = std::move(a);

  std::cout << "a = " << a << std::endl;
  std::cout << "b = " << b << std::endl;

  return 0;
}

a = 1  b = 1 因为基础数据类型没有实现移动语义,故会调用拷贝,a的值任存在,没有被移动

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值