14.1 手撕C/C++
手撕array?
#include <iostream>
#include <stdexcept> // for std::out_of_range
#include <algorithm> // for std::copy
template <typename T, size_t N>
class MyArray {
private:
T data_[N]; // 固定大小的连续内存
public:
// 默认构造函数
MyArray() = default;
// 初始化列表构造(C++11,推荐)
MyArray(std::initializer_list<T> init) {
if (init.size() > N) {
throw std::out_of_range("Too many elements in initializer_list");
}
std::copy(init.begin(), init.end(), data_);
// 如果传入的元素少于N,剩余部分是未初始化的!
}
// 拷贝构造函数
MyArray(const MyArray& other) {
std::copy(other.data_, other.data_ + N, data_);
}
// 拷贝赋值运算符
MyArray& operator=(const MyArray& other) {
if (this != &other) {
std::copy(other.data_, other.data_ + N, data_);
}
return *this;
}
// 移动构造函数(C++11,加分)
MyArray(MyArray&& other) noexcept {
std::copy(other.data_, other.data_ + N, data_);
}
// 移动赋值运算符(C++11,加分)
MyArray& operator=(MyArray&& other) noexcept {
if (this != &other) {
std::copy(other.data_, other.data_ + N, data_);
}
return *this;
}
// 访问元素 [],不检查边界
T& operator[](size_t index) {
return data_[index];
}
const T& operator[](size_t index) const {
return data_[index];
}
// 安全访问,带边界检查
T& at(size_t index) {
if (index >= N) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}
const T& at(size_t index) const {
if (index >= N) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}
// 获取数组大小(编译期常量,但封装成函数)
constexpr size_t size() const {
return N;
}
// 是否为空(固定大小,一般不为空)
constexpr bool empty() const {
return N == 0;
}
// 返回第一个元素
T& front() {
return data_[0];
}
const T& front() const {
return data_[0];
}
// 返回最后一个元素
T& back() {
return data_[N - 1];
}
const T& back() const {
return data_[N - 1];
}
// 迭代器支持(让MyArray支持范围for循环)
T* begin() {
return data_;
}
T* end() {
return data_ + N;
}
const T* begin() const {
return data_;
}
const T* end() const {
return data_ + N;
}
const T* cbegin() const {
return data_;
}
const T* cend() const {
return data_ + N;
}
};
手撕vector?
#include <iostream>
#include <stdexcept> // for std::out_of_range
#include <algorithm> // for std::copy
template <typename T>
class MyVector {
private:
T* data_; // 指向堆上数组的指针
size_t size_; // 当前存储的元素个数
size_t capacity_; // 当前分配的容量
// 扩容函数(私有)
void resize_if_needed() {
if (size_ >= capacity_) {
size_t new_capacity = (capacity_ == 0) ? 1 : capacity_ * 2;
reserve(new_capacity);
}
}
public:
// 默认构造函数
MyVector() : data_(nullptr), size_(0), capacity_(0) {}
// 带初始容量的构造
explicit MyVector(size_t capacity)
: data_(new T[capacity]), size_(0), capacity_(capacity) {}
// 拷贝构造函数
MyVector(const MyVector& other)
: data_(new T[other.capacity_]), size_(other.size_), capacity_(other.capacity_) {
for (size_t i = 0; i < size_; ++i) {
data_[i] = other.data_[i]; // 调用元素的拷贝构造
}
}
// 移动构造函数(C++11,加分项)
MyVector(MyVector&& other) noexcept
: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
// 析构函数
~MyVector() {
delete[] data_;
}
// 拷贝赋值运算符
MyVector& operator=(const MyVector& other) {
if (this != &other) {
// 先释放原有资源
delete[] data_;
// 分配新空间
capacity_ = other.capacity_;
size_ = other.size_;
data_ = new T[capacity_];
// 拷贝元素
for (size_t i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
}
return *this;
}
// 移动赋值运算符(加分项)
MyVector& operator=(MyVector&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
capacity_ = other.capacity_;
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
return *this;
}
// 添加元素到末尾
void push_back(const T& value) {
resize_if_needed();
data_[size_] = value;
++size_;
}
// 添加元素到末尾(移动语义,加分)
void push_back(T&& value) {
resize_if_needed();
data_[size_] = std::move(value);
++size_;
}
// 删除末尾元素
void pop_back() {
if (size_ > 0) {
--size_;
}
}
// 访问元素 [] 不检查边界
T& operator[](size_t index) {
return data_[index];
}
const T& operator[](size_t index) const {
return data_[index];
}
// 访问元素,带边界检查
T& at(size_t index) {
if (index >= size_) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}
const T& at(size_t index) const {
if (index >= size_) {
throw std::out_of_range("Index out of range");
}
return data_[index];
}
// 容量相关接口
size_t size() const { return size_; }
size_t capacity() const { return capacity_; }
bool empty() const { return size_ == 0; }
// 预留空间
void reserve(size_t new_capacity) {
if (new_capacity <= capacity_) return;
T* new_data = new T[new_capacity];
for (size_t i = 0; i < size_; ++i) {
new_data[i] = data_[i]; // 调用拷贝构造
}
delete[] data_;
data_ = new_data;
capacity_ = new_capacity;
}
// 改变大小
void resize(size_t new_size) {
if (new_size > capacity_) {
reserve(new_size);
}
if (new_size > size_) {
// 默认初始化新增的元素(调用T的默认构造)
for (size_t i = size_; i < new_size; ++i) {
data_[i] = T(); // 或者使用 placement new 更精细控制
}
}
size_ = new_size;
}
void resize(size_t new_size, const T& value) {
if (new_size > capacity_) {
reserve(new_size);
}
if (new_size > size_) {
for (size_t i = size_; i < new_size; ++i) {
data_[i] = value;
}
}
size_ = new_size;
}
// 迭代器支持(简化,可选)
T* begin() { return data_; }
T* end() { return data_ + size_; }
const T* begin() const { return data_; }
const T* end() const { return data_ + size_; }
};
手撕string?
#include <cstring> // for strlen, strcpy
#include <utility> // for std::swap (C++11)
class MyString {
public:
// 1. 默认构造函数
MyString() : data_(nullptr), size_(0) {}
// 2. 从 C 风格字符串构造
MyString(const char* str) {
if (str) {
size_ = strlen(str);
data_ = new char[size_ + 1]; // +1 for '\0'
strcpy(data_, str);
} else {
data_ = nullptr;
size_ = 0;
}
}
// 3. 析构函数
~MyString() {
delete[] data_; // 安全,delete nullptr 是合法的
}
// 4. 拷贝构造函数(深拷贝)
MyString(const MyString& other) : data_(nullptr), size_(0) {
if (other.data_) {
size_ = other.size_;
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
}
}
// 5. 拷贝赋值运算符(使用 copy-and-swap 惯用法,更安全、更简洁)
MyString& operator=(MyString other) { // 注意:传值,已经调用了拷贝构造
swap(*this, other); // 交换当前对象与传进来的副本
return *this; // other 析构时会释放旧内存
}
// 移动构造
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
// 6. swap 函数(用于 copy-and-swap)
friend void swap(MyString& a, MyString& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
// (可选加分项)获取 C 风格字符串
const char* c_str() const { return data_ ? data_ : ""; }
// (可选加分项)获取字符串长度
size_t size() const { return size_; }
char& operator[](size_t index) {
// 注意:这里不做越界检查,真实项目中建议检查!
return data_[index];
}
private:
char* data_;
size_t size_;
};
手撕printf?
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h> // for write()
// 辅助函数:输出单个字符(可用 putchar 或 write)
static void my_putchar(char c) {
putchar(c); // 或者使用 write(1, &c, 1);
}
// 辅助函数:整数转字符串(十进制,支持正负,不反向)
// 注意:这里为了简化,直接输出每一位,不返回字符串
static void print_int(int num) {
if (num == 0) {
my_putchar('0');
return;
}
// 处理负数
if (num < 0) {
my_putchar('-');
num = -num;
}
char digits[12]; // 足够存放下 32-bit int
int i = 0;
while (num > 0) {
digits[i++] = '0' + (num % 10);
num /= 10;
}
// 倒序输出
while (i > 0) {
my_putchar(digits[--i]);
}
}
// 辅助函数:输出字符串
static void print_string(const char *str) {
if (!str) {
my_putchar('(');
my_putchar('n');
my_putchar('u');
my_putchar('l');
my_putchar('l');
my_putchar(')');
return;
}
while (*str) {
my_putchar(*str++);
}
}
// 辅助函数:输出字符
static void print_char(char c) {
my_putchar(c);
}
// 主函数:简化版 printf
int my_printf(const char *format, ...) {
va_list args;
va_start(args, format);
int count = 0;
while (*format) {
if (*format == '%') {
++format; // 跳过 %
switch (*format) {
case 'c': {
char c = (char)va_arg(args, int); // char 在可变参数中提升为 int
print_char(c);
++count;
break;
}
case 's': {
char *str = va_arg(args, char*);
print_string(str);
// 粗略计算长度
const char *s = str;
while (s && *s) { ++count; ++s; }
break;
}
case 'd':
case 'i': {
int num = va_arg(args, int);
print_int(num);
// 粗略计算数字位数
if (num == 0) ++count;
else {
int tmp = num < 0 ? -num : num;
while (tmp > 0) { ++count; tmp /= 10; }
if (num < 0) ++count; // '-'
}
break;
}
case '%': {
my_putchar('%');
++count;
break;
}
default: {
// 未知格式符,原样输出(或可报错)
my_putchar('%');
my_putchar(*format);
count += 2;
break;
}
}
} else {
// 普通字符
my_putchar(*format);
++count;
}
++format;
}
va_end(args);
return count; // 返回打印的字符数,模仿标准 printf
}
(追问)C的printf是线程安全的吗?你设计的printf是线程安全的吗?
C的printf原理:当调用 printf 时,首先进行可变参数处理,接着由 glibc的 vfprintf 函数解析格式化字符串,再将数据转换为文本并写入用户态缓冲区。缓冲区满或遇到换行时,write系统调用将数据发送到内核。
并非线程安全。
如何设计一个线程安全的类?
-
使用mutex,但是需要注意死锁问题。
-
使用原子变量、CAS。
-
避免构造/析构期间暴露this指针。
一个C++程序有什么情况会导致宕机?进程崩溃?段错误?
-
空指针解引用
-
野指针 —— 访问已释放的内存
-
访问越界的数组
-
使用未初始化的指针
-
访问已释放的栈对象
-
双重释放
-
访问非法内存地址(比如 0x1、0xDEADBEEF)
-
栈溢出
-
使用非法类型转换或访问虚函数表错误(UB)
-
断言失败(assert() 触发)
-
访问未对齐的内存
-
多线程数据竞争,导致内存破坏
手撕move?
#include <iostream>
#include <type_traits> // 仅用于 std::remove_reference,可选,我们也可以不用
// 简易版 my_move,不依赖 <utility> 或 <type_traits>
template <typename T>
typename std::remove_reference<T>::type&& my_move(T&& arg) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}
// 如果你不想用 type_traits,也可以这样写(更基础的方式):
/*
template <typename T>
T&& my_move(T&& arg) noexcept {
return static_cast<T&&>(arg);
}
*/
手撕forward?
#include <iostream>
#include <type_traits> // for std::remove_reference
// 简易版 my_forward
template <typename T>
T&& my_forward(typename std::remove_reference<T>::type& arg) noexcept {
return static_cast<T&&>(arg);
}
14.2 手撕数据结构
【必问】手撕智能指针?
shared_ptr?
template<typename T>
struct ControlBlock {
T* ptr; // 指向实际对象
std::atomic<size_t> use_count; // 强引用计数(shared_ptr 个数)
std::atomic<size_t> weak_count; // 弱引用计数(weak_ptr 个数)
// 构造函数:有对象时
explicit ControlBlock(T* p)
: ptr(p), use_count(1), weak_count(0) {}
// 析构函数:不负责删除 ptr,由 shared_ptr 在 use_count==0 时处理
~ControlBlock() {
// 注意:我们不在 ControlBlock 析构函数里 delete ptr
// 因为只有当 use_count == 0 时,才应该 delete ptr
// 所以由 shared_ptr 来负责对象的销毁
}
};
#include <atomic>
#include <utility> // for std::swap
template <typename T>
class shared_ptr {
private:
T* ptr; // 可能为 nullptr
ControlBlock<T>* ctrl_block; // 指向控制块
// 辅助:增加 use_count
void add_use_ref() {
if (ctrl_block) {
ctrl_block->use_count.fetch_add(1, std::memory_order_relaxed);
}
}
// 辅助:减少 use_count,如果为0则销毁对象
void release_use() {
if (ctrl_block) {
if (ctrl_block->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr; // use_count == 0,销毁对象
ptr = nullptr;
// 如果也没有 weak_ptr 了,销毁控制块
if (ctrl_block->weak_count.load(std::memory_order_relaxed) == 0) {
delete ctrl_block;
}
ctrl_block = nullptr;
}
}
}
public:
// 默认构造
shared_ptr() : ptr(nullptr), ctrl_block(nullptr) {}
// 从裸指针构造
explicit shared_ptr(T* p)
: ptr(p), ctrl_block(new ControlBlock<T>(p)) {}
// 拷贝构造
shared_ptr(const shared_ptr& other)
: ptr(other.ptr), ctrl_block(other.ctrl_block) {
add_use_ref();
}
// 移动构造
shared_ptr(shared_ptr&& other) noexcept
: ptr(other.ptr), ctrl_block(other.ctrl_block) {
other.ptr = nullptr;
other.ctrl_block = nullptr;
}
// 拷贝赋值
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
release_use();
ptr = other.ptr;
ctrl_block = other.ctrl_block;
add_use_ref();
}
return *this;
}
// 移动赋值
shared_ptr& operator=(shared_ptr&& other) noexcept {
if (this != &other) {
release_use();
ptr = other.ptr;
ctrl_block = other.ctrl_block;
other.ptr = nullptr;
other.ctrl_block = nullptr;
}
return *this;
}
// 析构
~shared_ptr() {
release_use();
}
// 解引用
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
// 获取引用计数
size_t use_count() const {
return ctrl_block ? ctrl_block->use_count.load(std::memory_order_relaxed) : 0;
}
bool unique() const {
return use_count() == 1;
}
// 重置
void reset(T* p = nullptr) {
release_use();
if (p) {
ptr = p;
ctrl_block = new ControlBlock<T>(p);
} else {
ptr = nullptr;
ctrl_block = nullptr;
}
}
void swap(shared_ptr& other) {
std::swap(ptr, other.ptr);
std::swap(ctrl_block, other.ctrl_block);
}
};
你写的这个shared_ptr,sizeof多大?
T* ptr;
ControlBlock<T>* ctrl_block;
就他俩,大小8+8=16
你写的这个shared_ptr,线程安全吗?
引用计数线程安全,所管理的资源不安全。
weak_ptr?
template <typename T>
class weak_ptr {
private:
T* ptr; // 可能是悬空指针
ControlBlock<T>* ctrl_block; // 指向控制块
// 辅助:增加 weak_count
void add_weak_ref() {
if (ctrl_block) {
ctrl_block->weak_count.fetch_add(1, std::memory_order_relaxed);
}
}
// 辅助:减少 weak_count,必要时销毁控制块
void release_weak() {
if (ctrl_block) {
if (ctrl_block->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
// 如果没有 use_count 了,也销毁控制块
if (ctrl_block->use_count.load(std::memory_order_relaxed) == 0) {
delete ctrl_block;
}
ctrl_block = nullptr;
}
}
}
public:
// 默认构造
weak_ptr() : ptr(nullptr), ctrl_block(nullptr) {}
// 从 shared_ptr 构造
weak_ptr(const shared_ptr<T>& sp)
: ptr(sp.get()), ctrl_block(sp.ctrl_block) {
add_weak_ref();
}
// 拷贝构造
weak_ptr(const weak_ptr& other)
: ptr(other.ptr), ctrl_block(other.ctrl_block) {
add_weak_ref();
}
// 析构
~weak_ptr() {
release_weak();
}
// 赋值
weak_ptr& operator=(const weak_ptr& other) {
if (this != &other) {
release_weak();
ptr = other.ptr;
ctrl_block = other.ctrl_block;
add_weak_ref();
}
return *this;
}
// 尝试提升为 shared_ptr
shared_ptr<T> lock() const {
if (ctrl_block && ctrl_block->use_count > 0) {
// 简单起见,这里直接判断 use_count > 0;
// 更严格的实现应该用原子 CAS 来确保线程安全地增加 use_count
size_t old_use = ctrl_block->use_count.load();
do {
if (old_use == 0) return shared_ptr<T>();
} while (!ctrl_block->use_count.compare_exchange_weak(old_use, old_use + 1));
return shared_ptr<T>(ctrl_block->ptr, ctrl_block);
}
return shared_ptr<T>();
}
// 判断是否过期
bool expired() const {
return !ctrl_block || ctrl_block->use_count.load() == 0;
}
size_t use_count() const {
return ctrl_block ? ctrl_block->use_count.load() : 0;
}
// 释放资源
void release_weak() {
if (ctrl_block) {
if (ctrl_block->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
if (ctrl_block->use_count.load() == 0) {
delete ctrl_block;
}
ctrl_block = nullptr;
}
}
}
};
你写的这个weak_ptr,sizeof多大?
8
你写的这个weak_ptr,线程安全吗?
引用计数线程安全,lock安全(CAS锁),所管理的资源不安全。
unique_ptr?
template <typename T>
class unique_ptr {
private:
T* ptr; // 指向管理的对象
public:
// 默认构造函数 => 空指针
unique_ptr() noexcept : ptr(nullptr) {}
// 从裸指针构造(接管所有权)
explicit unique_ptr(T* p) noexcept : ptr(p) {}
// 禁止拷贝(独占!)
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 移交所有权后,原指针置空
}
// 移动赋值
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(); // 先释放当前资源
ptr = other.ptr; // 接管新资源
other.ptr = nullptr;
}
return *this;
}
// 析构函数:释放资源
~unique_ptr() {
reset();
}
// 释放所有权,返回裸指针,并将内部指针置空
T* release() noexcept {
T* p = ptr;
ptr = nullptr;
return p;
}
// 重置指针,先 delete 原对象,再管理新对象
void reset(T* p = nullptr) noexcept {
if (ptr) {
delete ptr;
}
ptr = p;
}
// 获取裸指针(不放弃所有权)
T* get() const noexcept {
return ptr;
}
// 解引用
T& operator*() const noexcept {
return *ptr;
}
// 成员访问
T* operator->() const noexcept {
return ptr;
}
// 检查是否为空
explicit operator bool() const noexcept {
return ptr != nullptr;
}
};
你写的这个unique_ptr,sizeof多大?
4
你写的这个unique_ptr,线程安全吗?
不安全,如果面试官让你安全,你自己加锁。
【必问】手撕LRU Cache?
核心数据结构组合:
-
双向链表(Doubly Linked List)
-
用于维护 key 的访问顺序:链表头部是最近使用的,尾部是最久未使用的。
-
当访问(get)或更新(put)某个 key 时,将其对应的节点移到链表头部。
-
当需要淘汰时,直接删除链表尾部节点。
-
-
哈希表(std::unordered_map)
-
用于实现 O(1) 的 key 查找,存储
key → 链表节点指针的映射。 -
通过哈希表,我们可以快速定位某个 key 对应的链表节点,进而进行移动、删除、更新等操作。
-
#include <unordered_map>
#include <list>
#include <utility>
#include <iostream>
template <typename Key, typename Value>
class LRUCache {
private:
using ListType = std::list<std::pair<Key, Value>>;
using MapType = std::unordered_map<Key, typename ListType::iterator>;
ListType cacheList_; // 双向链表:存储 [key, value],头部是最近使用,尾部是最久未使用
MapType cacheMap_; // 哈希表:key -> 链表中对应节点的迭代器
size_t capacity_; // 缓存容量
public:
// 构造函数,指定容量
explicit LRUCache(size_t capacity) : capacity_(capacity) {}
// 获取 key 对应的 value,如果不存在返回 -1(或者可以改为返回 std::optional<Value>)
Value get(const Key& key) {
auto it = cacheMap_.find(key);
if (it == cacheMap_.end()) {
// key 不存在
return Value(); // 或者 return -1; 或者抛异常,视情况而定
}
// key 存在,将对应节点移到链表头部(表示最近使用)
cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
return it->second->second;
}
// 插入或更新 key-value
void put(const Key& key, const Value& value) {
auto it = cacheMap_.find(key);
if (it != cacheMap_.end()) {
// key 已存在,更新 value,并移动到链表头部
it->second->second = value;
cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
return;
}
if (cacheList_.size() >= capacity_) {
// 缓存已满,删除链表尾部元素(最久未使用)
auto last = cacheList_.back();
cacheMap_.erase(last.first); // 从 map 中删除
cacheList_.pop_back(); // 从链表中删除
}
// 插入新元素到链表头部,并在 map 中记录
cacheList_.emplace_front(key, value);
cacheMap_[key] = cacheList_.begin();
}
// (可选)打印当前缓存内容,用于调试
void print() const {
std::cout << "LRUCache (most recently used -> least recently used):" << std::endl;
for (const auto& item : cacheList_) {
std::cout << "[" << item.first << ":" << item.second << "] ";
}
std::cout << std::endl;
}
};
【必问】手撕链表是否有环?
方法一:哈希表法(容易想到,但空间复杂度高 ✖ 不满足最优)
-
遍历链表的每个节点,用一个哈希集合(如
std::unordered_set)记录已经访问过的节点。 -
每访问一个节点,检查它是否已经在集合中:
-
如果在,说明有环;
-
如果遍历到
nullptr,说明无环。
-
方法二:快慢指针法(Floyd 判圈算法)✔ 推荐,最优解
-
使用两个指针,一快一慢:
-
慢指针(slow):每次走一步(
slow = slow->next); -
快指针(fast):每次走两步(
fast = fast->next->next)。
-
-
如果链表中没有环,快指针最终会走到
nullptr,结束循环; -
如果链表有环,快指针最终会追上慢指针(即 fast == slow),因为它们都在环内,快指针每次比慢指针多走一步,迟早相遇。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if (!head || !head->next) {
return false; // 空链表或只有一个节点且无环
}
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next; // 慢指针走一步
fast = fast->next->next; // 快指针走两步
if (slow == fast) { // 相遇则有环
return true;
}
}
return false; // 快指针遇到 nullptr,说明无环
}
};
(追问)如何确定环的大小(环中节点个数)?
方法:在快慢指针相遇后,保持一个指针不动,另一个指针继续前进并计数,直到再次相遇。
// 假设已经找到 slow == fast,即相遇了
int cycleLength = 0;
do {
slow = slow->next;
cycleLength++;
} while (slow != fast);
std::cout << "环的大小为: " << cycleLength << std::endl;
(追问)确定环的起点位置?
数学原理(重点!面试加分点)
设:
-
链表头到环的入口点的距离为 A;
-
环的入口点到快慢指针第一次相遇点的距离为 B;
-
从相遇点到环的入口点的距离为 C;
-
整个环的长度为 L = B + C。
第一次相遇时:
-
慢指针
slow走了:A + B -
快指针
fast走了:A + B + n × L,其中 n 是快指针在环内绕的圈数(n ≥ 1) -
但因为
fast走的是slow的两倍速度,所以:
2(A + B) = A + B + nL ⇒ A + B = nL ⇒ A = nL - B
关键结论:
从链表头到环入口的距离 A,等于 从相遇点继续走 n 圈减去 B,也就是 从相遇点走 (L - B) = C,再走若干圈回到入口。
但更直观的理解是:
如果我们让一个指针从链表头开始,另一个指针从相遇点开始,两者都每次走一步,它们最终会在环的入口节点相遇!
手撕二叉树的前序、中序、后序遍历?
见数据结构与算法
手撕最小堆?
见数据结构与算法
如何设计一个线程安全的跳表?
为了实现并发安全,需要对修改操作(插入、删除)加锁,但为了提高并发度,不能锁整个跳表,而是采用 细粒度锁(如每层每节点锁、或节点级锁),或者使用 无锁(lock-free)或乐观锁策略。
方案一(常用、实用):使用节点级互斥锁(细粒度锁)
-
每个跳表节点拥有一个
std::mutex; -
查找操作通常不加锁(只读),或使用乐观策略;
-
插入和删除操作需要按顺序(从上到下、从前往后)获取相关节点的锁,防止数据竞争和死锁;
-
采用“锁耦合”技术,逐步加锁与解锁,只锁定必要的节点;
-
这种方案实现较为可控,适合大多数工程场景。
方案二(高级、高性能):无锁(lock-free)跳表
-
使用 CAS(Compare-And-Swap)等原子操作来修改节点的 next 指针;
-
所有指针变更必须通过原子操作完成,通常要配合重试机制;
-
实现非常复杂,需要处理 ABA 问题、内存回收等,一般用于极致性能要求的场景。
内存里有一个map,你如何设计持久化策略,使得忽然宕机时,能够保证数据最少丢失?
以下二者组合:
-
写前日志(类似AOF)
-
每次修改 map 前,先写一条 操作日志 到磁盘(比如
"SET key value"或"DEL key"); -
日志是 追加写(append-only),性能高且安全(一般写到磁盘文件末尾);
-
-
定时快照(类似RDB)
-
定期将内存中的整个 map 序列化(如二进制或文本)并写入磁盘,形成某个时间点的快照。宕机后从最近一次快照恢复。
-
C++如何用栈实现队列?
核心思想:使用两个栈,一个用于输入,一个用于输出
我们定义两个栈:
-
inStack(输入栈):用来处理
push()操作,直接将新元素压入此栈 -
outStack(输出栈):用来处理
pop()和peek()操作
关键点:
-
当你要
pop()或peek()时,如果 outStack 为空,就需要将 inStack 的所有元素依次弹出并压入 outStack,这样最先进入 inStack 的元素就跑到了 outStack 的栈顶,就可以直接操作了。 -
如果 outStack 不为空,直接对 outStack 操作即可。
这样就能保证队列的 FIFO 特性。
#include <iostream>
#include <stack>
using namespace std;
class MyQueue {
private:
stack<int> inStack; // 用于 push 操作
stack<int> outStack; // 用于 pop / peek 操作
// 将 inStack 中的元素全部转移到 outStack 中(只在 outStack 为空时调用)
void transferIfNeeded() {
if (outStack.empty()) {
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
}
public:
MyQueue() {}
// 入队:直接 push 到 inStack
void push(int x) {
inStack.push(x);
}
// 出队:从 outStack 弹出;如果 outStack 为空,则先转移
int pop() {
transferIfNeeded();
int front = outStack.top();
outStack.pop();
return front;
}
// 获取队首元素:同上
int peek() {
transferIfNeeded();
return outStack.top();
}
// 判空:两个栈都为空时才为空
bool empty() {
return inStack.empty() && outStack.empty();
}
};
14.3 手撕算法
【必问】手撕快排?
见数据结构与算法。
手撕堆排序?
#include <iostream>
#include <vector>
// 调整以 i 为根的子树成为最大堆,范围是 [start, end]
void heapify(std::vector<int>& arr, int n, int i) {
int largest = i; // 初始化最大值为根节点
int left = 2 * i + 1; // 左孩子
int right = 2 * i + 2; // 右孩子
// 如果左孩子比根大
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右孩子比当前最大值还大
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大值不是根节点,交换并递归调整
if (largest != i) {
std::swap(arr[i], arr[largest]);
heapify(arr, n, largest); // 递归调整受影响的子树
}
}
// 堆排序主函数
void heapSort(std::vector<int>& arr) {
int n = arr.size();
// Step 1: 构建最大堆(从最后一个非叶子节点开始)
// 最后一个非叶子节点索引 = (n / 2) - 1
for (int i = n / 2 - 1; i >= 0; --i) {
heapify(arr, n, i);
}
// Step 2: 一个个从堆顶取出最大值
for (int i = n - 1; i > 0; --i) {
// 将当前最大值(堆顶,即arr[0])与末尾元素交换
std::swap(arr[0], arr[i]);
// 重新调整剩余元素为最大堆,范围 [0, i-1]
heapify(arr, i, 0);
}
}
2G内存在20个亿个整数中找出出现次数最多的数?
哈希分片:
-
哈希分片:
-
选择一个哈希函数,将每个整数映射到一个固定的桶(bucket)中(比如0到N-1)。
-
将20亿个整数根据哈希值分配到N个不同的临时文件中(比如N=1000,每个文件大约2 million个整数)。
-
这样,相同的整数一定会被分配到同一个文件中(因为哈希值相同)。
-
-
逐个处理分片:
-
对于每个临时文件,将其加载到内存中,使用哈希表统计该文件中每个整数的出现次数。
-
记录该文件中出现次数最多的整数及其次数。
-
最后比较所有分片的结果,找出全局出现次数最多的整数。
-
给出一副4种花色的扑克牌,输出这幅扑克牌可以组成的所有顺子和同花?
数据结构已给定:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <map>
using namespace std;
// 扑克牌结构
struct Card {
int rank; // 1=A, 2-10, 11=J, 12=Q, 13=K
char suit; // S, H, D, C
// 为了输出好看
string rankStr() const {
if (rank == 1) return "A";
if (rank == 11) return "J";
if (rank == 12) return "Q";
if (rank == 13) return "K";
return to_string(rank);
}
string suitStr() const {
switch (suit) {
case 'S': return "S"; case 'H': return "H";
case 'D': return "D"; case 'C': return "C";
default: return "?";
}
}
friend ostream& operator<<(ostream& os, const Card& c) {
os << c.rankStr() << c.suitStr();
return os;
}
};
// 生成一副牌:4花色 * 13点数
vector<Card> generateDeck() {
vector<Card> deck;
char suits[] = { 'S', 'H', 'D', 'C' };
for (char suit : suits) {
for (int rank = 1; rank <= 13; ++rank) {
deck.push_back(Card{ rank, suit });
}
}
return deck;
}
同花(n取k的组合):
// 从n个元素中选择k个的组合生成(索引版)
void generateCombinations(int n, int k, int start, vector<int>& current, vector<vector<int>>& result) {
if (current.size() == k) {
result.push_back(current);
return;
}
for (int i = start; i < n; ++i) {
current.push_back(i);
generateCombinations(n, k, i + 1, current, result);
current.pop_back();
}
}
// 打印某个花色的所有五张牌组合
void printFlushCombinationsForSuit(const vector<Card>& suitCards) {
int n = suitCards.size(); // 13
int k = 5;
vector<int> indices;
vector<vector<int>> combos;
generateCombinations(n, k, 0, indices, combos);
cout << "=== 同花组合(花色有 " << suitCards[0].suitStr() << ",共 " << combos.size() << " 种组合)===" << endl;
for (const auto& combo : combos) {
vector<Card> flushCombo;
for (int idx : combo) {
flushCombo.push_back(suitCards[idx]);
}
for (const Card& c : flushCombo) {
cout << c << " ";
}
cout << endl;
}
cout << "==================================================" << endl << endl;
}
int main() {
vector<Card> deck = generateDeck();
// 按花色分组
map<char, vector<Card>> suitsMap;
for (const Card& card : deck) {
suitsMap[card.suit].push_back(card);
}
// 对每个花色,生成并打印所有5张牌的同花组合
for (const auto& entry : suitsMap) {
char suit = entry.first;
const vector<Card>& suitCards = entry.second;
// 只处理有13张牌的花色(正常情况)
if (suitCards.size() == 13) {
printFlushCombinationsForSuit(suitCards);
}
}
return 0;
}
顺子(先生成顺子,再给每个顺子配花色):
// 从n个元素中选择k个的组合生成(索引版)
void generateRepeatedCombinations(int n, int k, int start, vector<int>& current, vector<vector<int>>& result) {
if (current.size() == k) {
result.push_back(current);
return;
}
for (int i = 0; i < n; ++i) { // 可重复
current.push_back(i);
generateRepeatedCombinations(n, k, i + 1, current, result);
current.pop_back();
}
}
// 打印某个顺子的所有五张牌组合
void printStraightCombinationsForSuit(const vector<Card>& suitCards) {
int n = suitCards.size(); // 4 梅花A 方片A 黑桃A 洪涛A
int k = 5;
vector<int> indices;
vector<vector<int>> combos;
generateRepeatedCombinations(n, k, 0, indices, combos);
cout << "=== 顺子组合,共 " << combos.size() << " 种组合)===" << endl;
for (const auto& combo : combos) { // S S S S S/S S S S D/S S S S H
vector<Card> flushCombo;
int i = 0;
for (int idx : combo) {
flushCombo.emplace_back(suitCards[idx].rank + i, suitCards[idx].suit);
++i;
}
for (const Card& c : flushCombo) {
cout << c << " ";
}
cout << endl;
}
cout << "==================================================" << endl << endl;
}
int main() {
vector<Card> deck = generateDeck();
// 所有的顺子(示意)
//vector<vector<int>> straightRankGroups = {
// {1, 2, 3, 4, 5}, // A-2-3-4-5
// {2, 3, 4, 5, 6},
// {3, 4, 5, 6, 7},
// {4, 5, 6, 7, 8},
// {5, 6, 7, 8, 9},
// {6, 7, 8, 9, 10},
// {7, 8, 9, 10, 11}, // 7-8-9-10-J
// {8, 9, 10, 11, 12}, // 8-9-10-J-Q
// {9, 10, 11, 12, 13} // 9-10-J-Q-K
//};
// 按数字分组
map<int, vector<Card>> suitsMap;
for (const Card& card : deck) {
suitsMap[card.rank].push_back(card);
}
// 对每个数字,生成并打印所有5张牌的顺子组合
for (const auto& entry : suitsMap) {
int start_num_of_straight = entry.first;
const vector<Card>& suitCards = entry.second;
// 只处理起始数字<=9的情况,否则无法组成顺子
if (start_num_of_straight <= 9) {
printStraightCombinationsForSuit(suitCards);
}
}
return 0;
}
40亿个非负整数中算中位数和找出现两次的数?
-
数据规模:40 亿个 unsigned int,大约是 4,000,000,000 个数字。
-
每个数字占 4 字节,如果全部加载进内存,大约需要:
4,000,000,000 × 4 byte ≈ 16 GB
-
一般机器的内存远远不够一次性加载所有数据,所以不能直接排序然后取中间值。
中位数:基于“分桶统计 + 二分查找”(Counting in Buckets)
思路:
-
将整个数字范围 [0, 2³²-1] 分成多个区间(比如 1024 或 65536 个桶 / 段)
-
统计每个区间内有多少个数字(即落在 [0~K], [K+1 ~ M], ... 的数字个数)
-
通过累加统计,找到那个包含第 20 亿个数字的区间
-
然后再单独加载该区间内的数据,进行排序或计数,最终找到中位数
核心思想是“分而治之” + 二分查找定位中位数所在区间
这种方法不需要一次性加载所有数据,也避免了对整个 43 亿范围做精确计数,大大节省了内存,适合面试手撕或实际工程优化
两次数:使用“哈希 + 计数,但分块处理 / 外部排序”
思路:
-
分批读取数据(比如每次读 1 亿个数到内存)
-
对每块数据,用哈希表统计数字出现次数
-
记录出现两次的数,并清理或合并统计信息
-
最终汇总所有块的结果,得到全局出现两次的数
适用于数据量极大但可以分片处理的场景,比如外部数据处理 / 流式处理
怎么在一个整数集合中,快速找到一个数,满足这个数是其他数的整数倍?
如果集合很大,可以:
-
将数字存入一个集合(哈希集合)以便快速查找。
-
对于每个数字
x,尝试找到其所有因数y(除了x本身),然后检查y是否在集合中。-
如何高效找到
x的所有因数?可以遍历 1 到 sqrt(x),检查是否能整除x,然后对应的因数是i和x // i。
-
14.4 手撕操作系统场景
手撕线程池?
见Linux系统编程。
如何判断Linux上两个文件是否完全相同?
-
使用
cmp命令(推荐,高效,只比较内容) -
使用
diff命令(适合文本文件,可查看差异详情) -
使用
md5sum/sha256sum(比较文件的哈希值,内容完全一致时哈希相同)
手撕epoll?
#include <iostream>
#include <unordered_map>
#include <vector>
#include <utility> // for std::pair
#include <thread>
#include <chrono>
#include <random>
#include <mutex>
#include <condition_variable>
// 模拟 Epoll 类
class SimpleEpoll {
public:
using Event = int; // 简化:用 int 表示关注的事件,比如 1<<0 表示读,1<<1 表示写
// 模拟 epoll 实例
struct EpollInstance {
std::unordered_map<int, Event> fd_events; // fd -> 关注的事件
};
private:
int next_epfd_ = 1; // 模拟 epoll_create() 返回的 fd
std::unordered_map<int, EpollInstance> epoll_instances_; // epfd -> EpollInstance
// 模拟“就绪事件”:在实际中是由内核通知的,这里我们用变量模拟
std::unordered_map<int, Event> ready_events_; // fd -> 实际就绪的事件
std::mutex mtx_;
std::condition_variable cv_;
bool ready_ = false;
public:
// 模拟 epoll_create() -> 返回一个 epoll fd
int create() {
int epfd = next_epfd_++;
epoll_instances_[epfd] = EpollInstance{};
std::cout << "[SimEpoll] epoll_create() -> epfd = " << epfd << std::endl;
return epfd;
}
// 模拟 epoll_ctl(epfd, fd, op, events)
// op: 1=ADD, 2=MOD, 3=DEL (简化,实际是 EPOLL_CTL_ADD 等)
void ctl(int epfd, int fd, int op, Event events) {
std::cout << "[SimEpoll] epoll_ctl(epfd=" << epfd << ", fd=" << fd
<< ", op=" << op << ", events=" << events << ")" << std::endl;
if (epoll_instances_.find(epfd) == epoll_instances_.end()) {
std::cerr << "Error: Invalid epfd" << std::endl;
return;
}
auto& instance = epoll_instances_[epfd];
if (op == 1) { // ADD
instance.fd_events[fd] = events;
std::cout << " -> Add fd " << fd << " with events " << events << std::endl;
} else if (op == 2) { // MOD
if (instance.fd_events.count(fd))
instance.fd_events[fd] = events;
std::cout << " -> Modify fd " << fd << " events to " << events << std::endl;
} else if (op == 3) { // DEL
instance.fd_events.erase(fd);
std::cout << " -> Delete fd " << fd << std::endl;
} else {
std::cerr << "Error: Unknown op " << op << std::endl;
}
}
// 模拟:手动触发某个 fd 的事件就绪(用于模拟异步事件到达,比如数据到达 socket)
void trigger_event(int fd, Event revents) {
std::unique_lock<std::mutex> lock(mtx_);
ready_events_[fd] = revents;
ready_ = true;
std::cout << "[SimEpoll] [INTERNAL] Trigger event on fd " << fd
<< ", revents = " << revents << std::endl;
cv_.notify_one(); // 通知等待的线程,有事件就绪了
}
// 模拟 epoll_wait(epfd, events[], maxevents, timeout)
int wait(int epfd, std::vector<std::pair<int, int>>& out_events, int timeout_ms = 1000) {
std::cout << "[SimEpoll] epoll_wait(epfd=" << epfd << ", timeout=" << timeout_ms << "ms)" << std::endl;
if (epoll_instances_.find(epfd) == epoll_instances_.end()) {
std::cerr << "Error: Invalid epfd" << std::endl;
return -1;
}
std::unique_lock<std::mutex> lock(mtx_);
// 等待某个 fd 就绪(模拟异步事件到达)
if (cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this]() { return ready_; })) {
// 模拟返回就绪的 fd 和事件
for (const auto& [fd, revents] : ready_events_) {
// 只返回该 epoll 实例管理的 fd(简化,真实 epoll 会关联)
out_events.emplace_back(fd, revents);
std::cout << " -> Ready: fd=" << fd << ", events=" << revents << std::endl;
}
ready_events_.clear();
ready_ = false;
return out_events.size(); // 返回就绪事件数
} else {
std::cout << " -> Timeout, no events ready." << std::endl;
return 0; // 超时,无事件
}
}
};
手撕生产者消费者模型?
-
1 个或多个生产者线程
-
1 个或多个消费者线程
-
1 个共享队列(std::queue)
-
互斥锁 + 条件变量 控制同步
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <random>
class ProducerConsumer {
private:
std::queue<int> buffer; // 共享缓冲区(队列)
const unsigned int max_size = 5; // 缓冲区最大容量
std::mutex mtx; // 互斥锁,保护共享数据
std::condition_variable not_full; // 条件变量:队列不满
std::condition_variable not_empty; // 条件变量:队列不空
public:
// 生产者函数
void producer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列满了,等待消费者消费
not_full.wait(lock, [this]() { return buffer.size() < max_size; });
// 模拟生产一个数据
int num = rand() % 100;
buffer.push(num);
std::cout << "Producer " << id << " produced: " << num
<< " | Buffer size: " << buffer.size() << std::endl;
lock.unlock(); // 解锁(其实 unique_lock 析构时也会解锁,但显式更好理解)
not_empty.notify_one(); // 通知消费者可以消费了
// 模拟生产耗时
std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 300));
}
}
// 消费者函数
void consumer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列空了,等待生产者生产
not_empty.wait(lock, [this]() { return !buffer.empty(); });
// 消费一个数据
int num = buffer.front();
buffer.pop();
std::cout << "Consumer " << id << " consumed: " << num
<< " | Buffer size: " << buffer.size() << std::endl;
lock.unlock();
not_full.notify_one(); // 通知生产者可以生产了
// 模拟消费耗时
std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 500));
}
}
};
int main() {
ProducerConsumer pc;
// 创建 2 个生产者线程 和 3 个消费者线程(数量可调整)
std::thread producers[2];
std::thread consumers[3];
for (int i = 0; i < 2; ++i) {
producers[i] = std::thread(&ProducerConsumer::producer, &pc, i + 1);
}
for (int i = 0; i < 3; ++i) {
consumers[i] = std::thread(&ProducerConsumer::consumer, &pc, i + 1);
}
// 等待所有生产者完成
for (int i = 0; i < 2; ++i) {
producers[i].join();
}
// 等待所有消费者完成
for (int i = 0; i < 3; ++i) {
consumers[i].join();
}
std::cout << "All producers and consumers finished." << std::endl;
return 0;
}
什么场景用线程?什么场景用协程?
线程适合用在 CPU 密集型、需要真正并行、利用多核 的场景,比如大规模计算、图像处理等。线程是操作系统调度的,可以分配到不同核心上并行执行,但线程的创建和切换开销较大,线程数过多时管理复杂,且需要处理同步与锁的问题。
协程适用于 I/O 密集型、高并发、异步任务调度 的场景,比如网络请求、Web 服务、爬虫等。协程是用户态轻量级任务,创建和切换成本极低,可以轻松支持数十万并发,而且用同步的代码风格写异步逻辑,代码更清晰。但协程一般运行在单线程内,不能直接利用多核,除非配合多线程调度器。
程序CPU占用率较高,如何排查?
-
找到高 CPU 的 进程
-
top 或 htop
-
-
找到进程中高 CPU 的 线程
-
top -H -p <PID>
-
-
采集调用栈,分析热点代码
-
gdb 多线程
-
perf 火焰图
-
-
分析代码逻辑,定位根因
程序崩了,你的第一反应是什么?
-
查看core文件:
-
ulimit -c unlimited命令确保崩溃时core dump核心已转储。 -
调用栈(Stack Trace):通过调试器(
gdb ./XX core.XX.YY)加载core dump或附加到进程,查看崩溃时的函数调用链(命令:bt)。 -
错误类型:常见崩溃原因包括:
-
空指针解引用(
Segmentation fault) -
数组越界(Buffer Overflow)
-
野指针/悬垂指针
-
多线程竞争(如访问已释放内存)
-
堆栈溢出(递归过深或局部变量过大)
-
资源耗尽(如内存不足、文件句柄泄漏)。
-
-
日志:检查程序自身的日志(尤其是崩溃前的最后几条日志)。
-
valgrind查看内存泄漏(以及不止内存泄漏)。
手撕读写锁?
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class ReadWriteLock {
private:
std::mutex mtx_; // 互斥锁,保护内部状态
std::condition_variable read_cond_; // 读者条件变量
std::condition_variable write_cond_;// 写者条件变量
int readers_ = 0; // 当前正在读的线程数
bool writing_ = false; // 是否有写者正在写
int waiting_writers_ = 0; // 正在等待的写者数(用于公平性)
public:
// 加读锁
void lockRead() {
std::unique_lock<std::mutex> lock(mtx_);
// 如果有写者正在写,或者有写者在等待(可选策略:写者优先)
read_cond_.wait(lock, [this]() {
return !writing_ && waiting_writers_ == 0;
});
++readers_;
}
// 解读锁
void unlockRead() {
std::unique_lock<std::mutex> lock(mtx_);
--readers_;
if (readers_ == 0) {
// 唤醒等待的写者
write_cond_.notify_one();
}
}
// 加写锁
void lockWrite() {
std::unique_lock<std::mutex> lock(mtx_);
++waiting_writers_; // 表示有一个写者在等待
// 等待:没有读者,也没有其他写者
write_cond_.wait(lock, [this]() {
return readers_ == 0 && !writing_;
});
--waiting_writers_;
writing_ = true;
}
// 解写锁
void unlockWrite() {
std::unique_lock<std::mutex> lock(mtx_);
writing_ = false;
// 优先唤醒写者还是读者?这里先唤醒写者(可调整策略)
if (waiting_writers_ > 0) {
write_cond_.notify_one();
} else {
// 没有写者在等,唤醒所有读者
read_cond_.notify_all();
}
}
};
14.5 手撕设计模式
【必问】手撕单例模式(懒汉式)?
懒汉式(Lazy Initialization):第一次调用时才创建实例。
class Singleton {
private:
static Singleton* instance; // 静态成员指针
// 私有构造函数,防止外部 new
Singleton() {}
public:
// 获取单例对象
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 删除拷贝构造和赋值操作(C++11 起推荐)
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
(追问)你这玩意线程安全吗?
不安全。加锁。
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
(追问)你加这么多锁,性能高吗?Meyers' Singleton是什么?
线程安全 + 高性能版本(双重检查锁定 DCLP,推荐)Double-Checked Locking Pattern(双重检查锁定)
目的:只在第一次创建时加锁,后续调用无需加锁,提升性能。
但还是太麻烦!
最推荐写法:Meyers' Singleton(局部静态变量,C++11 起线程安全)
🎯 利用 C++11 特性:局部静态变量的初始化是线程安全的!
这是目前 C++ 中最简洁、最安全、最高效、最推荐的单例实现方式,由 Scott Meyers 在《Effective Modern C++》中推荐,也叫 Meyers' Singleton。
class Singleton {
private:
Singleton() {} // 私有构造函数
public:
// 获取单例对象
static Singleton& getInstance() {
static Singleton instance; // C++11保证此处初始化线程安全
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
14.6 手撕网络问题
手撕ping?
Ping 是什么?
Ping 是一个常用的网络工具,用于测试两台主机之间是否可达,以及测量数据包的往返时间(RTT, Round-Trip Time)。
它基于 ICMP 协议(Internet Control Message Protocol,互联网控制消息协议),是 IP 层的辅助协议,不是传输层的 TCP 或 UDP。
Ping 的工作原理?(基于 ICMP Echo 请求/应答)
Ping 的核心是发送一个 ICMP Echo Request(类型 8) 报文到目标主机,然后等待对方返回一个 ICMP Echo Reply(类型 0) 报文。
-
如果收到回复,说明目标主机可达,并且你能得到 RTT(往返时间)。
-
如果收不到,可能目标不可达、网络不通、防火墙拦截等。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <chrono>
using namespace std;
// 计算校验和
unsigned short checksum(void* buf, int len) {
unsigned short* data = (unsigned short*)buf;
unsigned int sum = 0;
for (int i = 0; i < len / 2; ++i)
sum += data[i];
if (len % 2)
sum += *((unsigned char*)data + len / 2);
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
int main() {
const char* target_ip = "8.8.8.8"; // 可换成任何你想 ping 的 IP,比如 "127.0.0.1"
// 1. 创建原始 ICMP 套接字
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sock < 0) {
perror("socket");
cerr << "请使用 root / 管理员权限运行!" << endl;
return 1;
}
// 2. 构造 ICMP Echo Request 报文
char packet[64] = {0};
struct icmphdr* icmp = (struct icmphdr*)packet;
icmp->type = ICMP_ECHO; // 8
icmp->code = 0;
icmp->un.echo.id = htons(getpid() & 0xFFFF); // 可简单用 pid 做标识
icmp->un.echo.sequence = htons(1);
icmp->checksum = 0;
// 填充一些数据
strncpy(packet + sizeof(struct icmphdr), "PING", 4);
// 计算校验和(包括整个 ICMP 报文:头 + 数据)
icmp->checksum = checksum(packet, sizeof(struct icmphdr) + 4);
// 3. 设置目标地址
struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
inet_pton(AF_INET, target_ip, &dest.sin_addr);
// 4. 发送 ICMP 请求
if (sendto(sock, packet, sizeof(struct icmphdr) + 4, 0,
(struct sockaddr*)&dest, sizeof(dest)) <= 0) {
perror("sendto");
return 1;
}
cout << "Sent ICMP Echo Request to " << target_ip << endl;
// 5. 尝试接收 ICMP Echo Reply(简化,不循环、不超时)
char recv_buf[1024];
struct sockaddr_in from;
socklen_t from_len = sizeof(from);
if (recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr*)&from, &from_len) > 0) {
// 跳过 IP 头(通常 20 字节),拿到 ICMP 部分
struct iphdr* ip_header = (struct iphdr*)recv_buf;
int ip_hdr_len = ip_header->ihl * 4;
struct icmphdr* recv_icmp = (struct icmphdr*)(recv_buf + ip_hdr_len);
// 判断是否是 Echo Reply (Type == 0),并且 ID 匹配
if (recv_icmp->type == ICMP_ECHOREPLY &&
ntohs(recv_icmp->un.echo.id) == (getpid() & 0xFFFF)) {
cout << "Received ICMP Echo Reply from "
<< inet_ntoa(from.sin_addr) << endl;
} else {
cout << "Received something, but not our reply." << endl;
}
} else {
perror("recvfrom");
cout << "No reply received (可能目标不可达或防火墙拦截)" << endl;
}
close(sock);
return 0;
}
【必问】一般在Linux系统下遇到网络问题用哪些命令或手段进行排查?
在 Linux 系统下排查网络问题,我们通常会使用一系列常用命令和工具,从不同的层次去定位问题是出在网络连接、路由、防火墙、还是应用层本身。
ping:首先,最基础也最常用的命令是 ping,它可以用来测试目标主机是否可达,以及大致的网络延迟和丢包情况。如果 ping 不通,可能说明网络层就有问题,比如目标不可达、本地网络配置错误,或者中间有防火墙阻断了 ICMP 包。
traceroute:接下来是 traceroute(或 tracepath),它可以显示数据包从本机到目标主机经过的每一跳路由,帮助我们定位网络中断或高延迟的具体位置,尤其适合排查网络路径上的问题。
telnet、nc:如果目标服务是 TCP 类型的,比如 HTTP、MySQL 等,那可以用 telnet 或者更现代的 nc(netcat) 命令去测试目标主机的某个端口是否开放、是否能建立 TCP 连接。比如 telnet IP 端口 或者 nc -zv IP 端口,如果连不上,可能说明服务没起来、端口没监听、防火墙拦了,或者网络策略有问题。
netstat、ss:查看本机网络连接状态,最常用的命令是 netstat 或者更推荐的 ss(socket statistics),比如 ss -tulnp 可以列出当前所有监听的 TCP/UDP 端口以及对应的进程,方便确认服务是否正常监听。而 ss -antp 或 netstat -antp 则可以查看当前的 TCP 连接情况,包括连接状态(比如 ESTABLISHED、TIME_WAIT、CLOSE_WAIT 等),这在排查连接数过多、连接未正常释放等问题时非常有用。
iptables:如果怀疑是本地防火墙或安全组策略导致的问题,可以查看 iptables(或者 nftables,视发行版而定)的规则,比如 iptables -L -n -v,看是否有规则把相关流量给拦截了。在云服务器上还需要检查云平台自带的安全组配置。
对于 DNS 相关的问题,可以用 nslookup、dig 命令去测试域名解析是否正常,如果应用出现连接失败但 IP 是硬编码的,那可能不是 DNS 的问题,但如果用的是域名,解析失败就会导致连接不上。
tcpdump:另外,tcpdump 是一个非常强大的抓包工具,可以用来抓取指定网卡上的网络流量,比如 tcpdump -i eth0 port 80,通过分析抓到的包,我们可以看到请求有没有发出去、对方有没有回应、是不是被拦截、TCP 握手是否正常完成等,它是深入排查网络问题的利器。
ifconfig:最后,如果问题出在系统配置上,比如路由表不对,可以用 route -n 或者 ip route 查看路由信息;如果怀疑是本地网络接口配置问题,可以用 ifconfig 或者 ip addr 查看网卡状态和 IP 地址配置是否正确。
14.7 手撕中间件问题
目前没遇到。

被折叠的 条评论
为什么被折叠?



