6、C++高频面试真题|51-60
C++ 面试真题(51-60)
51. 🌟 谈一谈你对面向对象的理解
回答重点
四大核心特性(3 + 1):
- 封装:将数据(属性)和操作数据的方法(函数)绑定到类中,通过访问控制(
public
/private
/protected
)隐藏实现细节。可提高安全性(防止外部直接修改数据),简化接口使用。 - **继承:**通过派生类(子类)复用基类(父类)的代码,来支持层次化设计。
- **多态:**同一接口在不同上下文中表现出不同行为,分为编译时多态(函数重载、模板)和运行时多态(虚函数)。运行时多态主要是通过虚函数表和虚函数指针,实现动态绑定。
- **抽象:**仅暴露必要接口,隐藏复杂实现细节。主要通过纯虚函数定义抽象类(接口)。
扩展知识
菱形继承:C++ 支持一个类继承多个基类,但需注意菱形继承问题(通过虚继承解决):
class A {}; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; // 虚继承避免 A 的重复拷贝
虚函数与多态成本:虚函数通过虚函数表实现,会带来额外内存和间接调用开销,但提供了灵活的运行时多态。

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) |
constexpr
53. 🌟详细介绍C++中的回答重点
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)constexpr
与 const
的区别:
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),它在对象创建时自动初始化,指向该类的虚表。不同类型的对象,其虚表指针会指向不同的虚表。例如,上述示例中,Base
和 Derived
对象的虚表指针分别指向它们各自的虚表。
实际如图所示:
**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
模式,即在对象的生命周期内自动管理资源的获取和释放。
它为什么可以做到独占?
可以直接看这段源码:
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_ptr
。weak_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_count
和 weak_count
)。当所有的 shared_ptr
对象销毁时,资源被释放,但控制块直到所有 weak_ptr
也销毁时才会被释放。
4)成员函数:
lock()
:将weak_ptr
转换为shared_ptr
,若资源已被释放则返回一个空的shared_ptr
。expired()
:检查weak_ptr
指向的资源是否已被释放。
**5)线程安全:**控制块是线程安全的,因此 shared_ptr
和 weak_ptr
本身是线程安全的。多个线程可以同时使用 shared_ptr
和 weak_ptr
而不需要额外的同步机制。
特性 | 栈内存 (Stack) | 堆内存 (Heap) |
管理方式 | 编译器自动分配/释放,生命周期与作用域绑定 | 程序员手动管理(或通过智能指针),生命周期动态控制 |
存储位置 | 函数调用栈帧内 | 进程的全局堆区 |
std::string 应用 | - 对象本身始终在栈上 - 短字符串优化(SSO)时,数据直接嵌入对象(栈存储) | - 长字符串时,对象内的指针指向堆内存 - 数据动态分配,容量可扩展 |
典型场景 | `std::string s = "Hi";` (SSO 生效) | `std::string s = "Very long string...";` (SSO 不适用) |
访问速度 | 快(CPU 缓存友好) | 稍慢(需间接寻址) |
线程安全 | 栈变量线程私有(安全) | 需同步机制(多线程修改同一堆数据会竞争) |
58. STL 容器的六个组件是什么?
主要是:容器,算法,迭代器,仿函数, 适配器,空间配置器。

容器
用于存储和管理数据,提供多种数据结构(如动态数组、链表、树、哈希表等)。
分类:
std::vector<int> vec = {1, 2, 3}; _// 动态数组_
std::map<std::string, int> m = {{"Alice", 25}}; _// 键值对_
算法
作用:对容器中的数据进行通用操作(如排序、查找、遍历、修改等),通过迭代器与容器解耦。
分类:
- 非修改序列算法:
find
、count
、for_each
。 - 修改序列算法:
copy
、replace
、remove
。 - 排序与搜索算法:
sort
、binary_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()
的类),用于定制算法的操作逻辑。它比普通函数更灵活,可携带状态(通过成员变量)。
分类:
- 算术仿函数:
plus
、minus
、modulus
(定义在<functional>
中)。 - 关系仿函数:
less
、greater
、equal_to
。 - 逻辑仿函数:
logical_and
、logical_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
(调用new
和delete
)。 - 自定义场景:需要优化内存碎片或性能时,可替换为自定义分配器。
std::vector<int, MyAllocator<int>> vec; _// 使用自定义分配器_
六大组件的关系
- 容器通过空间配置器管理内存。
- 算法通过迭代器访问容器数据。
- 仿函数和适配器增强算法和容器的灵活性。
- 组件间高度解耦,用户可独立扩展某一部分(如自定义容器或分配器)
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_open
、mq_send
、mq_receive
、mq_close
、mq_unlink
进行操作。
共享内存(Shared Memory) 共享内存是最快的进程间通信方式,因为数据不需要从一个缓冲区拷贝到另一个缓冲区。特点是:
- 多个进程可以同时访问同一个内存段。
- 在 Linux 上,使用
shmget
,shmat
,shmdt
,shmctl
等系统调用进行操作。 - 需要同步机制(如信号量)来防止竞争条件。
**信号(Signals) **信号是一种最古老的进程间通信方式。信号是一种比较简单的通知机制:
- 用于通知进程发生了某个事件。
- 由于信号携带的信息量很少,通常用于简单的通知和控制。
**套接字(Sockets) **套接字不仅支持进程间通信,而且可以用于网络通信。分为:
- 本地域套接字(UNIX Domain Sockets):用于同一主机上的进程间通信。
- 网络套接字(TCP/UDP):用于不同主机间的进程通信,应用广泛但相对速度较慢。
文件(Files) 尽管效率不是很高,但使用文件进行进程间通信比较简单:
- 通过文件写入和读取来传递信息。
- 避免文件竞争通常使用文件锁(如
flock
)。
2)不同平台建议选择不同的IPC方式

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

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