C++面试速通宝典——13

208. class里面定义int a,如果不实现构造函数,实例化这个类,a的值是?

‌‌‌‌  答:a的值是未定义的(在C++标准中成为"未初始化")。

解释

‌‌‌‌  在C++中,如果一个类中定义了一个成员变量(如 int a),但没有提供构造函数来初始化这个变量,那么在实例化该类时,成员变量 a 的值是未定义的。未定义的意思是它的值可能是任何东西,因为它取决于内存中该位置之前存储的内容。

例如:

c 复制代码
class MyClass {
public:
    int a;
};

int main() {
    MyClass obj;
    std::cout << obj.a << std::endl; // a 的值未定义
    return 0;
}

‌‌‌‌  在上述代码中,obj.a 的值是未初始化的,因此它可能是任何值。

‌‌‌‌  为了避免未定义的行为,通常建议在类中提供构造函数来初始化成员变量。例如:

c 复制代码
class MyClass {
public:
    int a;
    MyClass() : a(0) {} // 构造函数将 a 初始化为 0
};

int main() {
    MyClass obj;
    std::cout << obj.a << std::endl; // 现在 a 的值是 0
    return 0;
}

‌‌‌‌  通过提供一个构造函数,可以确保成员变量有一个已知的初始值,从而避免未定义行为带来的潜在问题。

209. unique_ptr可以作为函数返回值么?

‌‌‌‌  可以。

‌‌‌‌  当函数返回一个unique_ptr时,他会利用C++移动语义将所有权从函数内部转移给调用方

解释

‌‌‌‌  unique_ptr 可以作为函数的返回值,并且当函数返回一个 unique_ptr 时,它会利用 C++ 的移动语义将所有权从函数内部转移给调用方。我们可以通过几个步骤来理解这段话:

  1. unique_ptr 的基本概念

    • unique_ptr 是 C++11 引入的智能指针类型,位于 <memory> 头文件中。
    • 它是独占所有权的智能指针,这意味着同一时间只能有一个 unique_ptr 拥有某个对象的所有权。
  2. 移动语义

    • 移动语义是 C++11 引入的特性,允许资源的所有权从一个对象转移到另一个对象,而不是复制资源。
    • 移动语义通过移动构造函数和移动赋值运算符实现,用 std::move 函数来显式调用。
  3. 返回 unique_ptr 的函数示例

    • 当函数返回一个 unique_ptr 时,编译器会自动利用移动语义将所有权从函数内部转移给调用方,而不是复制 unique_ptr
    • 这避免了所有权的复制,并且保证 unique_ptr 的独占所有权特性。

以下是一个示例:

c 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
};

std::unique_ptr<MyClass> createMyClass() {
    // 创建一个 unique_ptr 并返回
    return std::make_unique<MyClass>();
}

int main() {
    // 调用函数,接收返回的 unique_ptr
    std::unique_ptr<MyClass> ptr = createMyClass();
    // 使用 ptr 访问 MyClass 的成员
    return 0;
}

在这个例子中:

  1. createMyClass 函数创建了一个 unique_ptr 并返回它。
  2. main 函数中,ptr 接收 createMyClass 返回的 unique_ptr
  3. createMyClass 返回时,unique_ptr 的所有权从函数内部转移给 main 函数中的 ptr,而不是复制 unique_ptr

‌‌‌‌  总结来说,当函数返回一个 unique_ptr 时,利用移动语义将所有权从函数内部转移给调用方,确保了资源的独占所有权和高效的资源管理。

210. 快手直播流媒体是走长连接网关推送的嘛?

‌‌‌‌  通常会通过长连接来推送流媒体内容 ,这是为了确保数据传输的实时性和可靠性

211. HTTP3.0 对比HTTP 2.0 的优势

  1. 基于QUIC协议:HTTP/3使用QUIC(快速UDP互联网协议)代替TCP,使得连接建立更快,减少了握手的时间。
  2. 提高了传输效率:QUIC支持多路复用,但与HTTP/2不同,它避免了TCP的"队头阻塞"问题,使得即使部分数据丢失也不会影响其他数据的传输。
  3. 更好的错误恢复:QUIC在包级别实现了前向纠错和快速重传机制,减少了因为丢包导致的延迟。
  4. 内置TLS加密:QUIC默认内置了TLS1.3,提高了数据传输的安全性
  5. 更好的数据迁移:支持连接ID,即使用户的IP地址变化,也能无缝继续通信。

212. HTTP2.0对比HTTP1.1的优势

  1. 多路复用:HTTP/2通过在一个TCP上连接同时发送多个请求和接收多个响应消息,来消除不必要的延迟并提高页面加载速度。
  2. 头部压缩:HTTP/2引入了头部压缩机制,减小了数据包的大小并提高了传输效率。
  3. 服务器推送:HTTP/2可以让服务器把客户端需要的资源主动"推送"给客户端,减少了往返的数据交换次数。
  4. 流优先级:在HTTP/2中,客户端可以设置资源的优先级,使得重要的资源可以优先加载,从而提高了用户体验。
  5. 二进制协议:HTTP/2是二进制协议,不再是文本协议,二进制协议解析更高效,更少错误。

213. 讲一下进程、线程、协程的区别

  1. 进程 :是操作系统分配资源的最小单位具备一定的独立功能 ,能运行在处理器上每个进程有自己的独立内存空间
  2. 线程 :是程序执行的最小单位 ,线程是进程内部的一个实体 ,是比进程更小的能独立运行的基本单位 ,一个进程中可以有多个线程 ,这些线程共享进程的资源
  3. 协程 :是一种用户态的轻量级线程 ,也就是协程的切换不需要操作系统参与,由系统用户自行调度,因此效率更高 。协程可以理解为"协作的线程"。它比线程更小,因为它只使用了很少的内存大小。

214. 一个进程调用malloc最大能分配到多少内存?

‌‌‌‌  在32位系统中,最大理论限制约为2GB到4GB

‌‌‌‌  在64位系统中,这个限制远大于物理内存 ,但实际上受制于系统的物理内存和交换空间

215. 物理机malloc(10G)会发生什么?

‌‌‌‌  如果物理机的可用内存加上交换空间 小于10GB,malloc(10G)会失效 ,并返回一个空指针 ,因为没有足够的空间来分配这么大的内存块。

‌‌‌‌  如果可用内存加上交换空间足以满足10GB的要求,malloc将成功分配内存

216. vector线程安全吗?不安全在哪?

‌‌‌‌  Vector在C++STL中是不线程安全的

‌‌‌‌  不安全的原因主要在于他的操作(如增加、删除元素等)在多线程环境下没有同步,可能会导致竞态条件。

217. 多线程下使用vector一定要加锁嘛?

‌‌‌‌  为了保证在多线程环境下对Vector的操作安全,业务代码需要进行手动的锁控制。

解释

‌‌‌‌  在多线程环境中使用 std::vector 时,是否需要加锁取决于如何访问和修改 vector。这里有几个关键点需要考虑:

  1. 只读访问

    • 如果所有线程都只是读取 vector,没有任何线程对其进行修改,那么不需要加锁。这种情况下,vector 的数据是共享的常量数据,线程之间不会互相干扰。
  2. 写操作

    • 如果有任何一个线程对 vector 进行写操作(包括插入、删除、修改元素等),那么就需要加锁。因为 vector 在写操作时可能会改变其内部状态(例如重新分配内存),这会导致数据竞争和未定义行为。
  3. 混合操作

    • 如果一些线程在读取 vector,而另一些线程在写入 vector,那么也需要加锁。加锁可以防止在读取过程中发生修改,导致数据不一致或崩溃。

总结来说,为了确保线程安全,通常需要在访问 vector 时进行加锁:

  • 只读访问:不需要加锁。
  • 写操作:需要加锁。
  • 混合操作:需要加锁。

示例代码

以下是一个简单的示例,展示了如何在多线程环境中使用 std::mutexvector 进行加锁保护:

c 复制代码
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>

std::vector<int> vec;
std::mutex mtx;

void addToVector(int val) {
    std::lock_guard<std::mutex> lock(mtx);
    vec.push_back(val);
}

void printVector() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int v : vec) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::thread t1(addToVector, 1);
    std::thread t2(addToVector, 2);
    std::thread t3(printVector);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个示例中:

  1. std::mutex mtx 用于保护 vec 的访问。
  2. std::lock_guard<std::mutex> lock(mtx) 确保在作用域结束时自动释放锁。
  3. addToVectorprintVector 函数都使用锁来保护对 vec 的访问,以确保线程安全。

‌‌‌‌  这种加锁机制可以防止数据竞争,并确保在多线程环境中安全地访问和修改 vector

我再详细的解释一下这段代码。

c 复制代码
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
  • #include <iostream>:引入标准输入输出流库,用于打印输出。
  • #include <vector>:引入 std::vector 容器。
  • #include <thread>:引入多线程库,用于创建和管理线程。
  • #include <mutex>:引入互斥量库,用于线程同步,确保对共享资源的访问是安全的。
c 复制代码
std::vector<int> vec;
std::mutex mtx;
  • std::vector<int> vec:声明一个全局的整数向量 vec,所有线程共享。
  • std::mutex mtx:声明一个全局的互斥量 mtx,用于保护对 vec 的访问。
c 复制代码
void addToVector(int val) {
    std::lock_guard<std::mutex> lock(mtx);
    vec.push_back(val);
}
  • void addToVector(int val):定义一个函数 addToVector,接受一个整数参数 val
  • std::lock_guard<std::mutex> lock(mtx):创建一个 std::lock_guard 对象 lock,并传递互斥量 mtx。这会立即尝试获取锁,如果成功,在 lock 的生命周期内持有锁,并在 lock 离开作用域时自动释放锁。
  • vec.push_back(val):将 val 添加到向量 vec 的末尾。由于加锁保护,这个操作是线程安全的。
c 复制代码
void printVector() {
    std::lock_guard<std::mutex> lock(mtx);
    for (int v : vec) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}
  • void printVector():定义一个函数 printVector,没有参数。
  • std::lock_guard<std::mutex> lock(mtx):同样创建一个 std::lock_guard 对象 lock,并获取互斥量 mtx 的锁。
  • for (int v : vec):范围循环,遍历 vec 中的每个元素 v
  • std::cout << v << " ":将每个元素打印到标准输出,并以空格分隔。
  • std::cout << std::endl:在输出结束后换行。
c 复制代码
int main() {
    std::thread t1(addToVector, 1);
    std::thread t2(addToVector, 2);
    std::thread t3(printVector);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}
  • int main():程序的主入口。
  • std::thread t1(addToVector, 1):创建一个线程 t1,运行函数 addToVector,并传递参数 1。此时,addToVector(1) 将在线程 t1 中运行。
  • std::thread t2(addToVector, 2):创建另一个线程 t2,运行函数 addToVector,并传递参数 2
  • std::thread t3(printVector):创建第三个线程 t3,运行函数 printVector
  • t1.join():等待线程 t1 完成。
  • t2.join():等待线程 t2 完成。
  • t3.join():等待线程 t3 完成。

‌‌‌‌  通过 join(),确保主线程在继续执行前等待所有子线程完成,确保所有操作按预期顺序执行。整体来看,这段代码展示了如何在多线程环境下安全地对共享数据结构(std::vector)进行读写操作。

218. 两个线程同时对vector下相同索引的元素修改会发生什么?

‌‌‌‌  两个线程若同时对Vector的相同索引元素进行修改,将会导致未定义行为结果可能会是线程中的一个或两个的修改发生,或者导致数据损坏

219. C++内存序介绍一下

  1. memory_order_relax : 放宽内存顺序,不要求操作之间的顺序
  2. memory_order_consume : 较为弱的顺序要求,仅在特定的平台上有效。
  3. memory_order_acquire : 阻止操作重排序到原子操作前。
  4. memory_order_release : 阻止操作重排序到原子操作后。
  5. memory_order_acq_rel : 同时应用acquire和release。
  6. memory_order_seq_cst : 顺序一致,所有线程看到的操作顺序相同。

解释

memory_order_relaxed:放宽内存顺序,不要求操作之间的顺序

  • 特点:不施加任何同步或排序约束,只保证原子操作的原子性。
  • 用途:用于不需要跨线程同步的计数器或统计场景,性能最佳。

memory_order_consume:较为弱的顺序要求,仅在特定平台上有效

  • 特点 :确保数据依赖(data dependency)的可见性,但比 memory_order_acquire 更弱。在大多数平台上,memory_order_consume 等同于 memory_order_acquire,只有少数平台(如某些ARM架构)对其有特殊优化。
  • 用途:较少使用,主要用于那些高度依赖数据依赖性传递的平台。

memory_order_acquire:阻止操作重排序到原子操作之前

  • 特点:确保在原子操作之前的所有加载和存储都不会重排序到原子操作之后。通常用于加载操作。
  • 用途:用于获取锁或其他同步机制,确保在此之前的操作在所有线程中都可见。

memory_order_release:阻止操作重排序到原子操作之后

  • 特点:确保在原子操作之后的所有加载和存储都不会重排序到原子操作之前。通常用于存储操作。
  • 用途:用于释放锁或其他同步机制,确保在此之后的操作在所有线程中都可见。

memory_order_acq_rel:同时应用 acquire 和 release

  • 特点 :结合了 memory_order_acquirememory_order_release 的效果,确保在原子操作之前的所有加载和存储都不会重排序到原子操作之后,同时确保在原子操作之后的所有加载和存储也不会重排序到原子操作之前。
  • 用途:用于需要在同一个操作中既获取又释放同步的场景,比如读-修改-写操作。

memory_order_seq_cst:顺序一致,所有线程看到的操作顺序相同

  • 特点 :提供最强的内存序保证,所有线程都看到相同的操作顺序。所有 memory_order_seq_cst 操作都在全局顺序中执行,确保了最强的一致性。
  • 用途:用于需要强一致性保证的场景,确保所有线程都能以相同的顺序看到原子操作。

示例:

c 复制代码
#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> data(0);
std::atomic<bool> ready(false);

void producer() {
    data.store(42, std::memory_order_relaxed);
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire));
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();

    return 0;
}
  • producer 线程首先将数据存储为 42,然后设置 ready 为 true。
  • consumer 线程在 ready 被设置为 true 之前,保持轮询。
  • std::memory_order_releasestd::memory_order_acquire 确保了正确的同步,确保 consumer 看到 data 的正确值。

总之,内存序在多线程编程中是非常重要的,它们提供了不同级别的同步和排序保证,以便在不同的场景下选择合适的内存序来确保数据一致性和性能。

这里我再解释一下这段代码:

c 复制代码
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> data(0);
std::atomic<bool> ready(false);
  • #include <iostream>:用于标准输入输出。
  • #include <atomic>:用于原子操作和原子变量。
  • #include <thread>:用于多线程编程。
  • std::atomic<int> data(0):声明一个原子整数变量 data,并初始化为 0。原子变量保证对该变量的所有操作都是原子的。
  • std::atomic<bool> ready(false):声明一个原子布尔变量 ready,并初始化为 false。

定义生产者线程函数:

c 复制代码
void producer() {
    data.store(42, std::memory_order_relaxed);
    ready.store(true, std::memory_order_release);
}
  • data.store(42, std::memory_order_relaxed):将 data 设置为 42。使用 memory_order_relaxed 表示这个存储操作没有任何同步或排序约束,只保证原子性。
  • ready.store(true, std::memory_order_release):将 ready 设置为 true。使用 memory_order_release 确保在此操作之前的所有操作(即 data.store)不会重排序到这个操作之后。这确保了其他线程在看到 ready 为 true 时,也能看到 data 的正确值。

定义消费者线程函数:

c 复制代码
void consumer() {
    while (!ready.load(std::memory_order_acquire));
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl;
}
  • while (!ready.load(std::memory_order_acquire)):循环等待,直到 ready 为 true。使用 memory_order_acquire 确保在这个操作之后的所有操作(即 data.load)不会重排序到这个操作之前。这确保了在读取 data 之前,consumer 一定能看到 ready 为 true。
  • std::cout << "Data: " << data.load(std::memory_order_relaxed) << std::endl:打印 data 的值。使用 memory_order_relaxed 表示这个加载操作没有任何同步或排序约束,只保证原子性。

main 函数中创建和启动线程:

c 复制代码
int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();

    return 0;
}
  • std::thread t1(producer):创建一个线程 t1,执行 producer 函数。
  • std::thread t2(consumer):创建另一个线程 t2,执行 consumer 函数。
  • t1.join():等待线程 t1 结束。确保 main 线程在继续执行前等待 t1 完成。
  • t2.join():等待线程 t2 结束。确保 main 线程在继续执行前等待 t2 完成。

内存序的作用

  • memory_order_relaxed :在 producer 中用于 data.store 和在 consumer 中用于 data.load。这种内存序不施加任何同步或排序约束,只保证操作的原子性。它适用于不涉及跨线程同步的简单操作。
  • memory_order_release :在 producer 中用于 ready.store。这种内存序确保在此操作之前的所有操作不会重排序到此操作之后。这意味着当 ready 被设置为 true 时,data 已经被正确地设置为 42。
  • memory_order_acquire :在 consumer 中用于 ready.load。这种内存序确保在此操作之后的所有操作不会重排序到此操作之前。这意味着当 ready 为 true 时,consumer 一定能看到 data 的正确值。

总结

  • producer 线程首先将 data 设置为 42,然后设置 ready 为 true。
  • consumer 线程循环等待,直到看到 ready 为 true,之后打印 data 的值。
  • 使用 memory_order_releasememory_order_acquire 确保了线程之间的同步,使得 consumer 在读取 data 时看到正确的值。
相关推荐
小俊俊的博客12 分钟前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
_WndProc27 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_29 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++
m0_7482400229 分钟前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome
qq_4335545436 分钟前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++
努力学习编程的伍大侠40 分钟前
基础排序算法
数据结构·c++·算法
ThisIsClark44 分钟前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
程序猿进阶2 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
闻缺陷则喜何志丹2 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径