6、C++高频面试真题|51-60

厨子大约 23 分钟编程语言原创技术面试题解析C++面试程序员求职程序厨

C++ 面试真题(51-60)

51. 🌟 谈一谈你对面向对象的理解

回答重点

四大核心特性(3 + 1):

  1. 封装:将数据(属性)和操作数据的方法(函数)绑定到类中,通过访问控制(public/private/protected)隐藏实现细节。可提高安全性(防止外部直接修改数据),简化接口使用。
  2. **继承:**通过派生类(子类)复用基类(父类)的代码,来支持层次化设计。
  3. **多态:**同一接口在不同上下文中表现出不同行为,分为编译时多态(函数重载、模板)和运行时多态(虚函数)。运行时多态主要是通过虚函数表和虚函数指针,实现动态绑定。
  4. **抽象:**仅暴露必要接口,隐藏复杂实现细节。主要通过纯虚函数定义抽象类(接口)。

扩展知识

  • 菱形继承:C++ 支持一个类继承多个基类,但需注意菱形继承问题(通过虚继承解决):

    class A {};
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {}; // 虚继承避免 A 的重复拷贝
    
  • 虚函数与多态成本:虚函数通过虚函数表实现,会带来额外内存和间接调用开销,但提供了灵活的运行时多态。

image-20250511162010139
image-20250511162010139

52. 🌟介绍下C++常用的容器以及特点?

如下表:

容器
特点
时间复杂度
适用场景
vector
动态数组,随机访问快
尾部操作 O(1),中间 O(n)
需要随机访问,尾部操作多
list
双向链表,插入/删除快
插入/删除 O(1),访问 O(n)
频繁中间插入/删除
deque
双端队列,两端操作高效
头尾操作 O(1)
需要两端操作和随机访问
set/map
红黑树实现,有序
操作 O(log n)
需要有序且快速查找
unordered_set/map
哈希表实现,无序,查找快
平均 O(1),最坏 O(n)
需要快速查找,不关心顺序
stack/queue
适配器,限制操作
依赖底层容器
需要特定数据结构(LIFO/FIFO)

53. 🌟详细介绍C++中的constexpr

回答重点

constexpr是C++11引入的关键字,用于声明常量表达式,在编译时就能确定值的表达式,两个用途:

  • 编译时计算:编译时就可以计算出表达式的值,提高运行时性能

  • 常量表达式上下文:可用于哪些需要常量的表达式,比如数组大小、模板参数等。

扩展知识

1)constexpr变量:声明时必须初始化,且初始值必须是常量表达式。

constexpr int size = 10; // size 是编译时常量
int arr[size];

2)constexpr 函数:在编译期求值的函数,它的参数必须是常量表达式。

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int val = factorial(5); // val 在编译时计算为 120

3)constexprconst 的区别:

  • const:运行时不可修改,但不保证编译时求值
  • constexpr:编译时求值
对比项
`constexpr`
`const`
求值时机
编译时
运行时(可能编译时,但不强制)
初始化要求
必须用常量表达式初始化
可用运行时表达式初始化
函数支持
可修饰函数(编译时执行)
仅修饰变量/成员函数(不改变求值时机)
优化潜力
更高(编译期计算消除运行时开销)
无额外优化
适用版本
C++11+
所有版本

54. 🌟 请介绍 C++ 多态的实现原理?

回答重点

整体流程如下:

回答多态的实现原理,主要可以围绕在 虚函数虚函数表虚函数表指针 方向上。

多态通过虚函数实现。通过虚函数,子类可以重写父类的方法,当通过基类指针或引用调用时,会根据对象的实际类型调用对应的函数实现。

而这更深层次的原理,是通过虚表(vtable)和虚表指针(vptr)机制实现的。虚表是一个函数指针数组,包含了该类所有虚函数的地址,而虚表指针存储在对象实例中,指向属于该对象的虚表。

扩展知识

可以从以下几个方面,更全面的了解多态:

**1)虚函数和重写:**在基类中使用关键字 virtual 声明虚函数后,在子类中可以重写这个函数。

class Base {
public:
 virtual void show() {
     std::cout << "Base show" << std::endl;
 }
};

class Derived : public Base {
public:
 void show() override {
     std::cout << "Derived show" << std::endl;
 }
};

**2)虚表(vtable):**每个包含虚函数的类都会有一个虚表(vtable),这个虚表在编译时生成。它包含了该类所有虚函数的指针。对于每个类(而不是每个对象),编译器会创建一个唯一的虚表。

**3)虚表指针(vptr):**每个包含虚函数的对象实例会有一个隐藏的虚表指针(vptr),它在对象创建时自动初始化,指向该类的虚表。不同类型的对象,其虚表指针会指向不同的虚表。例如,上述示例中,BaseDerived 对象的虚表指针分别指向它们各自的虚表。

实际如图所示:

**4)多态的调用机制:**当通过基类指针或引用调用虚函数时,程序会通过该指针或引用找到对应的对象,然后通过虚表指针找到正确的虚表中的函数地址,最终调用适当的函数实现,这样程序能够在运行时决定调用哪一个函数实现。

5)实际示例:

void demonstratePolymorphism(Base &obj) {
 obj.show();  // 依赖于实际对象的类型
}

int main() {
 Base b;
 Derived d;
 demonstratePolymorphism(b);  // 输出 "Base show"
 demonstratePolymorphism(d);  // 输出 "Derived show"

 return 0;
}

6)注意事项:

  • 使用多态会有一定的内存和性能开销,因为每个类需要维护虚表,每个对象也需要存储虚表指针。
  • 虚函数调用通常比普通函数调用更慢一点,因为多了一次指针间接寻址。

55. 🌟 请介绍 C++ 中 unique_ptr 的原理?

回答重点

unique_ptr 是 C++11 引入的智能指针,它利用 RAII 模式,自动管理动态分配的资源。主要特点是它的所有权是独占的,也就是说,在任意时刻,某块内存只能由一个 unique_ptr 实例拥有,这样可以确保资源不会被多次释放。

基本原理如下:

1)unique_ptr 一旦被创建,它会负责管理内存并在适当的时候释放资源。

2)unique_ptr 不允许复制,但可以通过 std::move 进行所有权转移,从而避免双重释放问题。

3)unique_ptr 采用 RAII 模式,即在对象的生命周期内自动管理资源的获取和释放。

它为什么可以做到独占?

可以直接看这段源码open in new window

unique_ptr(const unique_ptr&) = delete;

template<typename _Up, typename _Up_Deleter> 
unique_ptr(const unique_ptr<_Up, _Up_Deleter>&) = delete;

unique_ptr& operator=(const unique_ptr&) = delete;

template<typename _Up, typename _Up_Deleter> 
unique_ptr& operator=(const unique_ptr<_Up, _Up_Deleter>&) = delete;

因为它禁用了拷贝构造函数。

扩展知识

进一步探讨 unique_ptr 的使用和内部机制。

1)创建 unique_ptr

std::unique_ptr<int> ptr1(new int(10));
auto ptr2 = std::make_unique<int>(20);

**2)所有权转移:**使用 std::move 转移 unique_ptr 的所有权。

std::unique_ptr<int> ptr3 = std::move(ptr1);

**3)不可复制:**尝试复制 unique_ptr 会导致编译错误。

// std::unique_ptr<int> ptr4 = ptr3; // 编译错误

**4)析构:**当 unique_ptr 离开作用域时,其析构函数会自动调用 delete,释放内存。

void example() {std::unique_ptr<int> ptr(new int(30));// 离开作用域时, ptr 会自动释放内存
}

5)自定义删除器:可以为 unique_ptr 指定一个自定义删除器。

std::unique_ptr<FILE, decltype(&fclose)> file_ptr(fopen("file.txt", "r"), &fclose);

6)优化性能:通过使用 std::make_unique 减少内存分配和构造步骤,提高性能和安全性。

auto arr = std::make_unique<int[]>(5);

因为这些特性, unique_ptr 非常适合用来管理动态资源,防止常见的内存泄漏和其他资源管理问题。

而且,使用 unique_ptr 还可以使代码更加清晰简洁,减少手动资源管理的负担。

建议在 C++ 开发中,尽量避免使用裸指针,考虑使用智能指针完全替代裸指针。

对比项
`unique_ptr`
`shared_ptr`
裸指针
所有权
独占(不可复制)
共享(引用计数)
无明确所有权
开销
无额外内存开销(仅存指针)
含引用计数和原子操作开销
无开销
安全性
自动释放,防内存泄漏
自动释放,循环引用需注意
需手动管理,易泄漏
拷贝语义
仅移动(`std::move`)
允许拷贝
可随意拷贝
推荐场景
明确独占所有权的资源
需共享所有权的资源
不推荐使用

56. 🌟 请介绍 C++ 中 shared_ptr 的原理?shared_ptr 线程安全吗?

回答重点

1)原理:shared_ptr 原理的回答重点在于 引用计数,它在底层实现上,通过维护一个引用计数,来管理内存对象的生命周期。

当新构造一个对象时,引用计数初始化为 1,拷贝对象时,引用计数加 1,对象作用域结束析构时,引用计数减 1,当最后一个对象被销毁时,引用计数会减为 0,所持有的资源会被释放。

2)线程安全性:shared_ptr 保证多个线程能够安全地增加或减少其引用计数。但是,如果多个线程同时读写同一个 shared_ptr 或者操作其管理的对象,那么需要额外进行同步机制,比如使用互斥锁(mutex)来保护这些操作。

也就是说,shared_ptr 的引用计数是线程安全的,但是它管理的对象是否线程安全,不归 shared_ptr 来管,取决于相关的对象是否有做同步处理。

扩展知识

1)引用计数机制:shared_ptr 内部通常会包含两部分指针:一是指向实际管理的资源,二是指向一个控制块(control block)。控制块中包含引用计数,当 shared_ptr 被拷贝时,引用计数增加;当 shared_ptr 被销毁时,引用计数减少;当引用计数变为零时,资源才会被释放。

**2)线程安全:**具体来说,shared_ptr 的引用计数操作是线程安全的。这是因为标准库对引用计数的增减操作进行了原子化处理。但是在其他场景下,比如多个线程同时修改 shared_ptr 对象自身,依然需要使用锁来保护。

**3)循环引用问题:**需要注意的是,shared_ptr 可能会导致循环引用(circular reference)的情况。比如 A 持有 B 的 shared_ptr,B 又持有 A 的 shared_ptr,这会导致引用计数永远不会为零,资源无法正确释放。解决这种问题,可以引入 weak_ptrweak_ptr 只会弱引用资源,不会影响引用计数。

**4)性能开销:**由于每次创建或销毁 shared_ptr 都会涉及到控制块的分配和释放操作,所以在一些性能敏感的场景(比如频繁创建和销毁对象)下,需要估算好这些操作的开销,在智能指针的选择上面,可优先选择 unique_ptr,次之选择 shared_ptr

5)手写一个 shared_ptr

#include <iostream>
include <mutex>

// 引用计数控制块
template <typename T>
class ControlBlock {
public:
    ControlBlock(T* ptr) : ptr_(ptr), ref_count_(1), weak_count_(0) {}
    
    void add_ref() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++ref_count_;
    }
    
    void release_ref() {
        bool should_delete = false;
        {
            std::lock_guard<std::mutex> lock(mutex_);
            if (--ref_count_ == 0) {
                should_delete = true;
                if (weak_count_ == 0) {
                    delete ptr_;
                    ptr_ = nullptr;
                }
            }
        }
        if (should_delete && weak_count_ == 0) {
            delete this;
        }
    }
    
    void add_weak_ref() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++weak_count_;
    }
    
    void release_weak_ref() {
        bool should_delete = false;
        {
            std::lock_guard<std::mutex> lock(mutex_);
            if (--weak_count_ == 0 && ref_count_ == 0) {
                should_delete = true;
            }
        }
        if (should_delete) {
            delete this;
        }
    }
    
    T* get() const { return ptr_; }
    int use_count() const { return ref_count_; }
    
private:
    T* ptr_;
    int ref_count_;
    int weak_count_;
    mutable std::mutex mutex_;
};

// shared_ptr 主类
template <typename T>
class SharedPtr {
public:
    // 构造函数
    SharedPtr() : ctrl_block_(nullptr) {}
    
    explicit SharedPtr(T* ptr) : ctrl_block_(new ControlBlock<T>(ptr)) {}
    
    // 拷贝构造函数
    SharedPtr(const SharedPtr& other) : ctrl_block_(other.ctrl_block_) {
        if (ctrl_block_) {
            ctrl_block_->add_ref();
        }
    }
    
    // 移动构造函数
    SharedPtr(SharedPtr&& other) noexcept : ctrl_block_(other.ctrl_block_) {
        other.ctrl_block_ = nullptr;
    }
    
    // 析构函数
    ~SharedPtr() {
        if (ctrl_block_) {
            ctrl_block_->release_ref();
        }
    }
    
    // 赋值运算符
    SharedPtr& operator=(const SharedPtr& other) {
        if (this != &other) {
            if (ctrl_block_) {
                ctrl_block_->release_ref();
            }
            ctrl_block_ = other.ctrl_block_;
            if (ctrl_block_) {
                ctrl_block_->add_ref();
            }
        }
        return *this;
    }
    
    SharedPtr& operator=(SharedPtr&& other) noexcept {
        if (this != &other) {
            if (ctrl_block_) {
                ctrl_block_->release_ref();
            }
            ctrl_block_ = other.ctrl_block_;
            other.ctrl_block_ = nullptr;
        }
        return *this;
    }
    
    // 访问指针
    T* get() const { return ctrl_block_ ? ctrl_block_->get() : nullptr; }
    T& operator*() const { return *get(); }
    T* operator->() const { return get(); }
    
    // 引用计数
    int use_count() const { return ctrl_block_ ? ctrl_block_->use_count() : 0; }
    
    // 重置指针
    void reset(T* ptr = nullptr) {
        if (ctrl_block_) {
            ctrl_block_->release_ref();
        }
        ctrl_block_ = ptr ? new ControlBlock<T>(ptr) : nullptr;
    }
    
    // 交换
    void swap(SharedPtr& other) {
        std::swap(ctrl_block_, other.ctrl_block_);
    }
    
    // 转换为bool
    explicit operator bool() const { return get() != nullptr; }
    
private:
    ControlBlock<T>* ctrl_block_;
    
    template <typename U>
    friend class WeakPtr;
};

// weak_ptr 实现
template <typename T>
class WeakPtr {
public:
    WeakPtr() : ctrl_block_(nullptr) {}
    
    WeakPtr(const SharedPtr<T>& shared) : ctrl_block_(shared.ctrl_block_) {
        if (ctrl_block_) {
            ctrl_block_->add_weak_ref();
        }
    }
    
    WeakPtr(const WeakPtr& other) : ctrl_block_(other.ctrl_block_) {
        if (ctrl_block_) {
            ctrl_block_->add_weak_ref();
        }
    }
    
    ~WeakPtr() {
        if (ctrl_block_) {
            ctrl_block_->release_weak_ref();
        }
    }
    
    WeakPtr& operator=(const WeakPtr& other) {
        if (this != &other) {
            if (ctrl_block_) {
                ctrl_block_->release_weak_ref();
            }
            ctrl_block_ = other.ctrl_block_;
            if (ctrl_block_) {
                ctrl_block_->add_weak_ref();
            }
        }
        return *this;
    }
    
    SharedPtr<T> lock() const {
        SharedPtr<T> shared;
        if (ctrl_block_ && ctrl_block_->use_count() > 0) {
            shared.ctrl_block_ = ctrl_block_;
            ctrl_block_->add_ref();
        }
        return shared;
    }
    
    int use_count() const { return ctrl_block_ ? ctrl_block_->use_count() : 0; }
    
private:
    ControlBlock<T>* ctrl_block_;
};

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
    void foo() { std::cout << "MyClass::foo()\n"; }
};

int main() {
    // 创建shared_ptr
    SharedPtr<MyClass> ptr1(new MyClass());
    
    // 拷贝构造
    SharedPtr<MyClass> ptr2 = ptr1;
    std::cout << "Use count: " << ptr1.use_count() << std::endl;  // 2
    
    // 通过weak_ptr观察
    WeakPtr<MyClass> weak = ptr1;
    std::cout << "Use count via weak: " << weak.use_count() << std::endl;  // 2
    
    // 重置ptr1
    ptr1.reset();
    std::cout << "Use count after reset: " << ptr2.use_count() << std::endl;  // 1
    
    // 从weak_ptr获取shared_ptr
    if (auto shared = weak.lock()) {
        shared->foo();
    } else {
        std::cout << "Object already destroyed\n";
    }
    
    // ptr2离开作用域,对象被销毁
    return 0;
}

57. 请介绍 C++ 中 weak_ptr 的原理?

回答重点

std::weak_ptr 是 C++11 引入的新特性,它主要搭配 std::shared_ptr 一起使用,它用来监视 std::shared_ptr 的生命周期,不会影响内部的引用计数,主要用于打破 std::shared_ptr 之间的循环引用问题。例如在对象 A 和对象 B 相互持有对方的 shared_ptr 时,会造成无法释放内存的情况,weak_ptr 可以帮助解决这个问题。

它提供了一种观察资源但不拥有资源的手段,我们可以通过它来检查资源是否依然存在,并且在需要时将其转变为一个 shared_ptr 进行使用。

扩展知识

1)循环引用:下面是 shared_ptr 循环引用的示例:

#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;

struct A {
   std::shared_ptr<B> bptr;
   ~A() {
       cout << "A delete" << endl;
   }
   void Print() {
       cout << "A" << endl;
   }
};

struct B {
   std::shared_ptr<A> aptr; // 这里产生了循环引用
   ~B() {
       cout << "B delete" << endl;
   }
   void PrintA() {
       aptr->Print();
   }
};

int main() {
   auto aaptr = std::make_shared<A>();
   auto bbptr = std::make_shared<B>();
   aaptr->bptr = bbptr;
   bbptr->aptr = aaptr;
   bbptr->PrintA();
   return 0;
}

输出:
A

从上面示例可以看到,A 持有了 B,B 又持有了 A,导致两者的引用计数都无法减为 0,两者对象都没办法析构,出现了内存泄漏。

2)解决循环引用:

使用 weak_ptr 就可以解决上面的循环引用问题,看示例代码:

#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;

struct A {
   std::shared_ptr<B> bptr;
   ~A() {cout << "A delete" << endl;
   }void Print() {cout << "A" << endl;
   }
};

struct B {std::weak_ptr<A> aptr; // 这里改成weak_ptr
   ~B() {
       cout << "B delete" << endl;
   }
   void PrintA() {
       if (!aptr.expired()) { // 监视shared_ptr的生命周期
       auto ptr = aptr.lock();
           ptr->Print();
      }
   }
};

int main() {
   auto aaptr = std::make_shared<A>();
   auto bbptr = std::make_shared<B>();
   aaptr->bptr = bbptr;
   bbptr->aptr = aaptr;
   bbptr->PrintA();
   return 0;
}

输出:
A
A delete
B delete

因为 weak_ptr 不持有引用计数,不管理资源,所以这里不会出现循环引用问题,引用计数会减为 0,两者对象都会正常析构。

3)基本原理:weak_ptr 基于 shared_ptr 实现。weak_ptr 本身不管理资源,而是与 shared_ptr 共享内部控制块(control block)。这个控制块包含了实际资源指针、引用计数(shared_countweak_count)。当所有的 shared_ptr 对象销毁时,资源被释放,但控制块直到所有 weak_ptr 也销毁时才会被释放。

4)成员函数:

  • lock():将 weak_ptr 转换为 shared_ptr,若资源已被释放则返回一个空的 shared_ptr
  • expired():检查 weak_ptr 指向的资源是否已被释放。

**5)线程安全:**控制块是线程安全的,因此 shared_ptrweak_ptr 本身是线程安全的。多个线程可以同时使用 shared_ptrweak_ptr 而不需要额外的同步机制。

特性
栈内存 (Stack)
堆内存 (Heap)
管理方式
编译器自动分配/释放,生命周期与作用域绑定
程序员手动管理(或通过智能指针),生命周期动态控制
存储位置
函数调用栈帧内
进程的全局堆区
std::string 应用
- 对象本身始终在栈上
- 短字符串优化(SSO)时,数据直接嵌入对象(栈存储)
- 长字符串时,对象内的指针指向堆内存
- 数据动态分配,容量可扩展

典型场景
`std::string s = "Hi";` (SSO 生效)
`std::string s = "Very long string...";` (SSO 不适用)
访问速度
快(CPU 缓存友好)
稍慢(需间接寻址)
线程安全
栈变量线程私有(安全)
需同步机制(多线程修改同一堆数据会竞争)

58. STL 容器的六个组件是什么?

主要是:容器,算法,迭代器,仿函数, 适配器,空间配置器。

image-20250511162929250
image-20250511162929250

容器

用于存储和管理数据,提供多种数据结构(如动态数组、链表、树、哈希表等)。

分类

std::vector<int> vec = {1, 2, 3};  _// 动态数组_
std::map<std::string, int> m = {{"Alice", 25}};  _// 键值对_

算法

作用:对容器中的数据进行通用操作(如排序、查找、遍历、修改等),通过迭代器与容器解耦。

分类

  • 非修改序列算法findcountfor_each
  • 修改序列算法copyreplaceremove
  • 排序与搜索算法sortbinary_search
  • 数值算法accumulate(求和)、inner_product(点积)。
std::sort(vec.begin(), vec.end());  _// 排序_
auto it = std::find(vec.begin(), vec.end(), 2);  _// 查找元素_

迭代器

提供访问容器元素的统一接口,充当容器与算法之间的桥梁(类似指针的抽象)。

for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";  _// 通过迭代器访问元素_
}

仿函数

行为类似函数的对象(重载了 operator() 的类),用于定制算法的操作逻辑。它比普通函数更灵活,可携带状态(通过成员变量)。

分类

  • 算术仿函数plusminusmodulus(定义在 <functional> 中)。
  • 关系仿函数lessgreaterequal_to
  • 逻辑仿函数logical_andlogical_not
std::sort(vec.begin(), vec.end(), std::greater<int>());  _// 降序排序_

适配器

  • 作用:对现有组件进行封装,改变其接口或行为,提供更特定的功能。

  • 常见适配器

    • 容器适配器stack(基于 deque/list)、queue(基于 deque)、priority_queue(基于 vector)。
    • 迭代器适配器reverse_iterator(反向遍历)、insert_iterator(插入元素)。
    • 函数适配器bind(参数绑定)、not1(逻辑取反)。
std::stack<int> s;  _// 默认基于 deque_
s.push(10);         _// 适配器隐藏了底层容器的细节_

空间配置器

  • 作用:管理容器的内存分配与释放,实现内存分配的灵活控制(如内存池优化)。
  • 默认行为:STL 容器默认使用 std::allocator(调用 newdelete)。
  • 自定义场景:需要优化内存碎片或性能时,可替换为自定义分配器。
std::vector<int, MyAllocator<int>> vec;  _// 使用自定义分配器_

六大组件的关系

  1. 容器通过空间配置器管理内存。
  2. 算法通过迭代器访问容器数据。
  3. 仿函数适配器增强算法和容器的灵活性。
  4. 组件间高度解耦,用户可独立扩展某一部分(如自定义容器或分配器)

59. 🌟C++ 有哪些进程间通信的方式?

回答重点

C++ 支持多种进程间通信(IPC)方式:

1)管道(Pipes)

2)消息队列(Message Queues)

3)共享内存(Shared Memory)

4)信号(Signals)

5)套接字(Sockets)

6)文件(Files)

扩展知识

这些 IPC 通信方式各有优缺点,

1)不同场景选择不同方式

管道(Pipes)

  • **匿名管道:**通常用于具有父子关系的进程间通信,它们是单向的,也就是数据只能沿一个方向流动,如果需要双向通信,需要使用两个匿名管道。
  • **命名管道(Named Pipe):**命名管道通过在文件系统中创建一个特殊文件来实现。它可以用于没有亲缘关系的进程间通信,并且是双向的。

消息队列(Message Queues) 进程以消息的形式进行通信。消息队列具有以下特点:

  • 消息队列中的消息具有特定的标识,可以优先级排序。
  • 不需同步,消息独立存在,不会覆盖。
  • Posix 中可以使用 mq_openmq_sendmq_receivemq_closemq_unlink 进行操作。

共享内存(Shared Memory) 共享内存是最快的进程间通信方式,因为数据不需要从一个缓冲区拷贝到另一个缓冲区。特点是:

  • 多个进程可以同时访问同一个内存段。
  • 在 Linux 上,使用 shmget, shmat, shmdt, shmctl 等系统调用进行操作。
  • 需要同步机制(如信号量)来防止竞争条件。

**信号(Signals) **信号是一种最古老的进程间通信方式。信号是一种比较简单的通知机制:

  • 用于通知进程发生了某个事件。
  • 由于信号携带的信息量很少,通常用于简单的通知和控制。

**套接字(Sockets) **套接字不仅支持进程间通信,而且可以用于网络通信。分为:

  • 本地域套接字(UNIX Domain Sockets):用于同一主机上的进程间通信。
  • 网络套接字(TCP/UDP):用于不同主机间的进程通信,应用广泛但相对速度较慢。

文件(Files) 尽管效率不是很高,但使用文件进行进程间通信比较简单:

  • 通过文件写入和读取来传递信息。
  • 避免文件竞争通常使用文件锁(如 flock)。

2)不同平台建议选择不同的IPC方式

image-20250511162804764
image-20250511162804764
跨平台建议
平台
推荐 IPC 方式
理由
Linux
共享内存 + UNIX 套接字
高性能;原生支持完善
Windows
命名管道
API 稳定;与系统集成度高
跨平台
ZeroMQ(zmq_ipc)
封装底层差异;提供统一的高性能接口
关键对比维度
  1. 速度:共享内存 > 管道/套接字 > 消息队列 > 文件
  2. 复杂度:共享内存(需同步) > 套接字 > 消息队列 > 管道 > 文件
  3. 数据量:共享内存(大块数据) > 消息队列(结构化数据) > 管道/套接字(流式数据)

60. 什么场景下使用锁?什么场景下使用原子变量?

回答重点

锁(lock)和原子变量(atomic)都可以用作同步机制,它们有各自的适用场景:

1)使用锁的场景:

  • 当需要保护长时间访问的临界区时,比如复杂的操作或逻辑(如链表、树等复杂数据结构的操作)。
  • 当多个共享资源需要同步访问时,锁可以一次性锁定多个资源,确保整体一致性。
  • 在涉及到复杂的操作时,比如需要一次性更新多个共享变量。

2)使用原子变量的场景:

  • 当操作可以在一个原子步骤内完成时,比如简单的整数增减、标志位切换。
  • 当性能非常关键,且锁的开销和上下文切换的成本过高时。原子操作通常比使用锁更轻量级。
  • 用于实现非阻塞算法时,因为原子变量不会导致线程挂起而等待锁释放。

**建议:**优先使用原子变量,如果发现使用原子变量不能满足同步机制的需求,那就使用锁。

扩展知识

这里进一步探讨锁和原子操作:

1)锁的类型:

  • **互斥锁(Mutex):**最常见的普通锁,用于保护一个共享资源。
  • 读写锁(Read-Write Lock):允许多个读者并行访问,但写者访问需要独占。
  • **自旋锁(Spinlock):**线程在等待时会不断轮询锁状态,而不是挂起,非常适合短时间持有锁的场景。

2)原子操作:

  • 可以使用 std::atomic 库提供的原子类型,如 std::atomic<int>, std::atomic<bool>atomic 是个模板类,你还可以使用 double 等类型填充。
  • 这些操作通常由硬件直接支持,比如 x86 架构的"Lock"前缀指令,确保读取-修改-写入一个不可分割的操作。

3)实际应用示例:

  • **使用锁的例子:**假设我们有一个共享的 std::map,需要在线程间进行插入和删除操作,我们可以使用 std::mutex 来保护这个 map
std::mutex mtx;
std::map<int, std::string> sharedMap;

void insertIntoMap(int key, const std::string& value) {
    std::lock_guard<std::mutex> lock(mtx);
    sharedMap[key] = value;
}
  • **使用原子变量的例子:**假设我们有一个计数器,只需要每次增加或减少 1,使用 std::atomic 更高效。
std::atomic<int> counter(0);

void incrementCounter() {
    counter++;
}

void decrementCounter() {
    counter--;
}
image-20250511162822155
image-20250511162822155
选择建议
优先选择原子变量
优先选择锁
- 操作可单指令完成(如标志位
- 性能敏感场景(如高频计数器)
- 操作涉及多个变量
- 需要执行复杂逻辑(如遍历链表)