C++
多态的类型与实现
C++中有两种主要的多态形式:静态多态和动态多态。
1. 静态多态(编译时多态)
静态多态在编译时确定调用的具体函数,主要通过以下方式实现:
- 函数重载
- 模板
- 运算符重载
特点:
- 编译时确定调用关系
- 无运行时开销
- 灵活性较低
示例(函数模板):
template<typename T>
void print(T value) {
cout << value << endl;
}
int main() {
print(10); // 调用print<int>
print(3.14); // 调用print<double>
print("hello"); // 调用print<const char*>
}
2. 动态多态(运行时多态)
动态多态在运行时确定调用的具体函数,主要通过以下方式实现:
- 虚函数
- 继承
特点:
- 运行时确定调用关系
- 有运行时开销(虚表查找)
- 灵活性高
示例(虚函数):
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
class Square : public Shape {
public:
void draw() override {
cout << "Drawing a square" << endl;
}
};
int main() {
Shape* shapes[] = {new Circle(), new Square()};
for (auto shape : shapes) {
shape->draw(); // 运行时决定调用哪个draw()
}
}
对比总结
| 特性 | 静态多态 | 动态多态 | |————|—————-|—————-| | 确定时机 | 编译时 | 运行时 | | 实现方式 | 模板/重载 | 虚函数/继承 | | 性能 | 无额外开销 | 有虚表查找开销 | | 灵活性 | 较低 | 较高 |
C++类型转换
C++提供了四种显式类型转换操作符,比C风格的类型转换更安全、更明确:
1. static_cast
- 用途:基本类型转换、父子类指针/引用转换(上行安全,下行不安全)
- 特点:编译时检查,无运行时开销
- 示例:
```cpp
double d = 3.14;
int i = static_cast
(d); // 基本类型转换
Base* base = new Derived(); Derived* derived = static_cast<Derived*>(base); // 下行转换
#### 2. dynamic_cast
- **用途**:安全的父子类指针/引用转换(主要用于下行转换)
- **特点**:运行时检查,需要RTTI支持,有性能开销
- **示例**:
```cpp
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 安全下行转换
if (derived) { /* 转换成功 */ }
3. const_cast
- 用途:添加或移除const/volatile限定符
- 特点:不改变底层数据,仅改变编译器解释
- 示例:
const int ci = 10; int* mod = const_cast<int*>(&ci); // 移除const *mod = 20; // 未定义行为,原对象是const
4. reinterpret_cast
- 用途:低级别的类型重新解释(如指针与整数间转换)
- 特点:最危险,不进行任何检查
- 示例:
int i = 42; void* p = reinterpret_cast<void*>(&i); // 指针类型转换 int64_t addr = reinterpret_cast<int64_t>(p); // 指针转整数
对比总结
| 转换类型 | 主要用途 | 安全性 | 运行时检查 | |—————|———————————-|————-|———–| | static_cast | 基本类型转换、类层次转换 | 中等 | 无 | | dynamic_cast | 安全的类层次下行转换 | 高 | 有 | | const_cast | const/volatile限定符操作 | 低 | 无 | | reinterpret_cast | 低级别类型重新解释 | 非常低 | 无 |
虚函数表和虚函数表指针的创建
虚函数表实际是一个虚函数地址的数组,其指向为具体的代码区的位置。
- 背景:用来实现多态机制
- 生成:编译器编译的时候生成,由virtual关键字修饰。
- 存放位置:可执行程序状态下,存放在磁盘;运行状态,存放在内存中。
虚函数默认参数的特殊行为
当虚函数有默认参数且被重载时,默认参数的值由调用时的静态类型决定,而不是运行时实际对象的类型。这是因为默认参数是编译时确定的,而虚函数调用是运行时动态绑定的。
class Base {
public:
virtual void foo(int x = 10) {
cout << "Base::foo(" << x << ")" << endl;
}
};
class Derived : public Base {
public:
void foo(int x = 20) override {
cout << "Derived::foo(" << x << ")" << endl;
}
};
int main() {
Base* b = new Derived();
b->foo(); // 输出:Derived::foo(10)
delete b;
return 0;
}
关键点:
- 虽然实际调用的是Derived::foo(),但使用的默认参数是Base::foo()的10
- 默认参数值在编译时根据指针类型(Base*)确定
- 虚函数调用在运行时根据实际对象类型(Derived)确定
最佳实践:
- 避免在虚函数中使用默认参数
- 如果必须使用,确保派生类和基类的默认参数一致
- 可以使用非虚函数提供默认参数,调用虚函数实现:
class Base {
public:
void foo(int x = 10) { do_foo(x); }
protected:
virtual void do_foo(int x) { /*...*/ }
};
- 虚拟内存分区:栈区(栈区之上时内核空间)、文件映射区、堆区、数据区(静态存储区)和代码区。 可执行程序中不同部分存储的内容如下:
| 可执行程序中(磁盘) | 作用 | 对应虚拟内存分区(内存) |
|---|---|---|
| .bss | 未初始化的或初始化为0的全局、静态变量 | 静态存储区 |
| .data | 初始化的全局、静态变量 | 静态存储区 |
| .rodata | 只读数据段和虚函数表 | 代码区 |
| .text | 具体代码表 | 代码区 |
- 虚函数表指针的创建时机:
- 类对象构造的时候,把类的虚函数表地址赋值给vptr。
- 没有构造函数,编译器会生成默认的构造函数(有第一条的作用)。
- 继承的情况下(例如B继承A),先调用基类的构造函数,把A的虚函数表的地址赋值给vptr。再调用子类构造函数,把B的虚函数表的地址赋值给vptr。
虚函数表和虚函数表指针的关系
每个类只有一个虚函数表,类的不同对象通常来说虚函数表指针vptr(每个类实例有一个)是不一样的(自身地址不一样,指向的内容一样,为深拷贝。浅拷贝会导致一个置null后另一个类实例找不到虚函数表)。使用的时候需要显式地定义拷贝构造函数或重载赋值运算符。
默认拷贝构造函数何时生成
背景 不提供的话,位拷贝,全盘复制。
- 危害1:堆上资源也会复制
- 危害2:文件句柄,socket也复制 持有相同的对象资源,句柄,一者释放,另一者就拿不到数据。
触发默认构造函数时机
- A b;
- A a(b);
- A a=b;
- 函数传参,形参为类对象
- 函数返回值为类对象(有返回值优化),无优化会多次调用。(C++11之后还会看是否有移动构造,然后看类有没有默认构造,再没有,报错)
生成时间 编译时生成。
- 类成员变量是一个类,该成员类有默认构造函数。
- 类继承自基类,基类有默认构造函数。
- 类成员有虚函数。
- 类继承自基类,基类有虚函数。
进程和线程的区别
本质:进程是资源费配的基本单位,线程是CPU调度的基本单位。
- 并发性:切换效率。上下文切换(如redis to mysql),运行环境(寄存器,程序计数器,用户空间信息与内核空间,pcb)。如果同一个进程的线程切换,效率高,不用动用户空间的数据。
- 内存:线程不具备独立分配虚拟内存的空间。进程有独立的地址空间。但线程有栈,pc,本地存储等独立空间。
- 所属关系:线程依附于进程,进程可以有多个线程。
- 健壮性:进程与进程之间隔离,独立的。线程共享一个进程,互相影响。进程健壮性高于线程。
线程同步方法
多线程编程中需要同步机制来保证数据一致性和线程安全。C++提供了多种同步原语:
1. 互斥锁(mutex)
- 用途:保护共享数据,防止多线程同时访问
- 特点:阻塞等待,简单易用
- 示例: ```cpp std::mutex mtx; int shared_data = 0;
void thread_func() { mtx.lock(); shared_data++; // 临界区 mtx.unlock(); }
#### 2. 条件变量(condition_variable)
- **用途**:线程间通知/等待机制
- **特点**:需配合互斥锁使用
- **示例**:
```cpp
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
void producer() {
std::unique_lock<std::mutex> lock(mtx);
ready = true;
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
}
3. 原子操作(atomic)
- 用途:无需锁的线程安全操作
- 特点:无锁,高性能
- 示例:
```cpp
std::atomic
counter(0);
void increment() { counter.fetch_add(1); // 原子操作 }
#### 4. 读写锁(shared_mutex)
- **用途**:读多写少场景
- **特点**:多个读线程可同时访问
- **示例**:
```cpp
std::shared_mutex smtx;
void reader() {
std::shared_lock lock(smtx); // 共享锁
// 读取数据
}
void writer() {
std::unique_lock lock(smtx); // 独占锁
// 修改数据
}
5. 自旋锁(spinlock)
- 用途:短期等待的临界区保护
- 特点:忙等待,不释放CPU
- 实现:通常基于原子操作实现
6. 屏障(barrier)
- 用途:同步多个线程的执行点
- 特点:等待所有线程到达屏障点
- C++20:
std::barrier
对比总结
| 同步方法 | 主要用途 | 性能特点 | 适用场景 | |—————-|————————–|————–|—————-| | mutex | 通用数据保护 | 中等 | 大多数场景 | | condition_variable | 线程间通知 | 较高开销 | 生产者-消费者 | | atomic | 简单原子操作 | 最高 | 计数器等简单操作 | | shared_mutex | 读多写少 | 读高性能 | 频繁读偶尔写 | | spinlock | 极短临界区 | 忙等待 | 内核开发等 | | barrier | 多线程同步点 | 中等 | 分阶段任务 |
系统调用的整个流程
- 什么是系统调用 用户态进入内核态的一种形式。用户态需要操作到内核态的资源。 用户程序->函数库->系统调用->内核
- 中断 系统调用是软中断(0x80)。中断号、中断向量表、中断处理程序。
- 调用流程 触发中断->切换堆栈->执行中断程序->从中断处理程序返回(iret软中断返回)
// 示例:通过`syscall`指令或软中断触发
int main() {
write(1, "Hello", 5); // 调用write系统调用
};
x86架构示例(通过0x80中断)
mov eax, 4 ; 系统调用号(sys_write)
mov ebx, 1 ; 文件描述符
mov ecx, msg ; 缓冲区地址
mov edx, len ; 数据长度
// 系统调用表示例(Linux内核)
void *sys_call_table[] = {
[0] = sys_restart_syscall,
[1] = sys_exit,
[4] = sys_write, // 对应示例中的write调用
//...
};
// sys_write内核函数简化逻辑
asmlinkage long sys_write(
unsigned int fd,
const char __user *buf,
size_t count
) {
struct file *file = fget(fd);
return vfs_write(file, buf, count, &file->f_pos);
}
// 通过iret指令恢复现场
ENTRY(ret_from_sys_call)
RESTORE_ALL
iret
- 如果使用阻塞的io且io未就绪,将线程或者进程切换,运行态->阻塞态
动态库问题排查指南
1. 常见动态库问题
- 加载失败:找不到动态库文件
- 符号未定义:动态库版本不匹配或链接错误
- ABI不兼容:编译器版本或编译选项不一致
- 内存泄漏:资源未正确释放
2. 动态库加载过程
- 查找动态库路径(按以下顺序):
- LD_PRELOAD环境变量指定路径
- RPATH/RUNPATH指定的路径
- LD_LIBRARY_PATH环境变量
- /etc/ld.so.cache缓存
- 默认路径(/lib, /usr/lib等)
- 加载动态库到内存
- 解析符号和重定位
3. 排查工具和命令
- ldd:查看程序依赖的动态库
ldd /path/to/program - readelf:查看动态库信息
readelf -d /path/to/library.so - nm:查看动态库符号表
nm -D /path/to/library.so - strace:跟踪系统调用
strace -e openat /path/to/program - LD_DEBUG:调试动态链接器
LD_DEBUG=libs /path/to/program
4. 典型问题解决方案
问题1:动态库未找到
error while loading shared libraries: libxxx.so: cannot open shared object file
解决方案:
- 检查动态库是否存在
- 设置LD_LIBRARY_PATH环境变量
- 使用patchelf修改RPATH
patchelf --set-rpath '$ORIGIN/lib' /path/to/program
问题2:符号未定义
undefined symbol: SomeFunction
解决方案:
- 检查动态库是否包含该符号
nm -D /path/to/library.so | grep SomeFunction - 检查链接顺序是否正确
- 检查编译器版本和选项是否一致
问题3:ABI不兼容
Segmentation fault (core dumped)
解决方案:
- 确保所有动态库使用相同编译器版本编译
- 检查-std和-abi版本是否一致
- 使用统一的编译选项
5. 预防措施
- 使用版本号管理动态库
libfoo.so.1.2.3 - 使用-Wl,–as-needed减少不必要的依赖
- 定期检查动态库依赖关系
- 使用静态分析工具检查ABI兼容性
6. 内存问题排查
- 使用valgrind检测内存泄漏
valgrind --leak-check=full /path/to/program - 使用mtrace跟踪内存分配
#include <mcheck.h> int main() { mtrace(); // 你的代码 muntrace(); }
网络相关概念
MTU和MSS
- MTU(Maximum Transmission Unit): 最大传输单元,指网络接口层能够传输的最大数据包大小,单位为字节。以太网默认MTU为1500字节。
- MSS(Maximum Segment Size): 最大分段大小,指TCP报文段中数据部分的最大长度,由MTU减去IP和TCP头部长度(通常40字节)得到,默认1460字节。
- 关系:MSS = MTU - IP头(20字节) - TCP头(20字节)
- 作用:MTU/MSS用于控制数据包大小,避免IP分片,提高传输效率。
TCP粘包问题
- 定义:TCP是字节流协议,接收方无法区分消息边界,多个应用层数据包可能被粘在一起接收。
- 原因:
- Nagle算法将多个小包合并发送
- 接收方缓冲区中数据堆积
- 解决方案:
- 固定长度消息
- 特殊分隔符(如\n)
- 消息头包含长度字段(常用)
TCP可靠传输机制
- 序列号和确认应答:
- 每个字节都有唯一序列号
- 接收方发送ACK确认收到的数据
- 选择性确认(SACK)可提高效率
- 超时重传:
- 发送数据后启动定时器
- 超时未收到ACK则重传
- 动态计算RTO(重传超时时间)
- 流量控制:
- 滑动窗口机制
- 接收方通过窗口大小通告可用缓冲区
- 零窗口探测防止死锁
- 拥塞控制:
- 慢启动:指数增长窗口
- 拥塞避免:线性增长窗口
- 快速重传:收到3个重复ACK立即重传
- 快速恢复:避免过度降低发送速率
// 简化的TCP发送端伪代码
void tcp_send() {
while(has_data_to_send) {
send_packet(next_seq);
start_timer(next_seq);
next_seq += packet_size;
if(received_ack) {
adjust_window_size();
stop_timer(acked_seq);
} else if(timeout) {
retransmit_packet();
adjust_congestion_window();
}
}
}
线程池手撕
#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <future>
class ThreadPool {
public:
explicit ThreadPool(size_t threads = std::thread::hardware_concurrency()) {
for(size_t i = 0; i < threads; ++i)
workers.emplace_back([this] {
for(;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this]{ return stop || !tasks.empty(); });
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using return_type = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task->get_future();
{
std::unique_lock<std::mutex> lock(queue_mutex);
if(stop)
throw std::runtime_error("enqueue on stopped ThreadPool");
tasks.emplace([task](){ (*task)(); });
}
condition.notify_one();
return res;
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker : workers)
worker.join();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop = false;
};
移动语义
C++11引入了移动语义,可以减少拷贝构造函数的调用,提高效率。 对象移动时,会将对象内的资源转移到另一个对象,而不是拷贝。避免资源的重新分配。
- 移动构造函数:将右值对象内的资源转移到左值对象内。
- 移动赋值运算符:将右值对象内的资源转移到左值对象内。
class A {
public:
A(A&& other) noexcept {
std::cout << "move constructor" << std::endl;
// 资源转移
data = other.data;
other.data = nullptr;
}
A& operator=(A&& other) noexcept {
std::cout << "move assignment operator" << std::endl;
if(this!= &other) {
// 资源转移
data = other.data;
other.data = nullptr;
}
return *this;
}
A(const A& other) {
std::cout << "copy constructor" << std::endl;
// 拷贝构造
data = new int(other.data);
}
A& operator=(const A& other) {
std::cout << "copy assignment operator" << std::endl;
if(this!= &other) {
// 拷贝赋值
delete data;
data = new int(other.data);
}
return *this;
}
~A() {
std::cout << "destructor" << std::endl;
delete data;
}
int* data;
};
int main() {
A a1(10);
A a2(std::move(a1)); // 移动构造
A a3 = std::move(a2); // 移动赋值
return 0;
}
输出:
move constructor
move constructor
move assignment operator
destructor
destructor
移动语义在stl中的应用
- std::move:将左值转换为右值。
- std::forward:将参数类型转换为右值引用。
- std::move_if_noexcept:判断类型是否支持移动语义。
- std::swap:交换两个对象内的资源。
完美转发
在模板中,将模板参数完美地转发到内部函数(内部调用)参数中去。转发时保证被转发的参数的值属性不变。
template<typename F, typename... Args>
auto invoke(F&& f, Args&&... args) -> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
- 借用万能引用,模板参数T或者auto,即需要类型推导,来接受左右属性。
- 应用折叠规则,模板参数T&&,即右值引用。T为左值或者左值引用,转为左值引用。T为右值或者右值引用,转为右值引用。(例如int& &&->int&,int&& &&->int&&,int &&->int &)
- 无论是左值引用还是右值引用,都是左值,具名。左值和右值是指对象是否拥有资源的这个本质(或者叫属性),而不是对象的值。
- std::forward
(v):T为左值引用,v将转化为T类型的左值。T为右值引用,v将转化为T类型的右值。去除了引用,转化为T类型的对象。
智能指针
- 指针管理困境:
- 资源释放问题:指针失效,资源泄露。
- 资源管理问题:资源泄露,资源管理复杂。(重复释放,释放时机不确定,释放顺序不确定)
- 如何解决:智能指针。RAII(Resource Acquisition Is Initialization)思想。根据对象的生命周期进行资源的控制。
- shared_ptr实现原理:
- 引用计数:每个对象维护一个引用计数,当引用计数为0时,释放资源。
- 自动释放:智能指针管理的资源,在离开作用域时自动释放。
- 异常安全:智能指针保证异常安全,即使在构造函数或析构函数中抛出异常,也能保证资源的正确释放。
- 常见智能指针:
- unique_ptr:独占所有权,只能有一个指针指向对象。
- shared_ptr:共享所有权,多个指针指向对象。
- weak_ptr:弱引用,指向shared_ptr对象,但不影响引用计数。
单例模式C++11之后的最佳解决————Meyers’ Singleton
对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。
在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。
而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。
Meyers Singleton的实现:
class Singleton
{
private:
Singleton() { };
~Singleton() { };
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
};
模板工厂模式
- 该模式用来封装和管理类的创建,终极目的是为了解耦,实现创建者和调用者的分离。
template <class ProductClass, typename... ArgType>
void* objCreateFunc(ArgType... arg)
{
return new ProductClass(arg...);
}
#ifndef ReflectRegister
# define ReflectRegister(ProductClass, ...) \
static int __type##ProductClass = \
ReflectFactory::registerObjCreateFunc(#ProductClass, (void*)&objCreateFunc<ProductClass, ##__VA_ARGS__>);
#endif
class ReflectFactory
{
public:
template <class BaseClass, typename... ArgType>
static BaseClass* createObj(const std::string& className, ArgType... arg)
{
typedef BaseClass* (*_CreateFactory)(ArgType...);
auto& _funcMap = getObjCreateFuncMap();
auto iFind = _funcMap.find(className);
if (iFind == _funcMap.end())
return nullptr;
else
return reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);
}
static int registerObjCreateFunc(const std::string& className, void* func)
{
getObjCreateFuncMap()[className] = func;
return 0;
}
private:
static std::unordered_map<std::string, void*>& getObjCreateFuncMap()
{
static std::unordered_map<std::string, void*> s_classRefectionMap;
return s_classRefectionMap;
}
};
- 这里reinterpret_cast<_CreateFactory>(_funcMap[className])(arg...);转换的时void* objCreateFunc(ArgType... arg)这个函数指针,将它的返回值变成了父类指针,但是具体执行的时候return new ProductClass(arg...);还是创建了子类对象,实现了多态!
环形缓冲区
环形缓冲区是一种数据结构,它可以实现在一段连续的内存中存储和读取数据,但是只能以一种循环的方式来访问。
环形缓冲区的实现原理:
- 环形缓冲区由一个缓冲区数组和两个指针组成。
- 缓冲区数组中存储的数据是环形的,即缓冲区数组的首尾相连。
- 两个指针:
- read_ptr:指向当前读入缓冲区的位置。
- write_ptr:指向下一个待写入缓冲区的位置。
- 读操作:
- 读操作从缓冲区中读取数据,首先从read_ptr指向的位置开始读取,直到缓冲区的末尾,然后从缓冲区的开头开始读取,直到read_ptr指向的位置。
- 读操作完成后,read_ptr指向下一个待读取的位置。
- 写操作:
- 写操作向缓冲区中写入数据,首先将数据写入write_ptr指向的位置,然后将write_ptr指向下一个位置。
- 如果写操作导致缓冲区的末尾被覆盖,则将缓冲区的开头到末尾的数据拷贝到缓冲区的开头,然后将write_ptr指向缓冲区的开头。
- 写操作完成后,write_ptr指向下一个待写入的位置。
环形缓冲区的优点:
- 环形缓冲区可以实现数据的循环读写,即使缓冲区的容量不足,也可以循环使用。
- 环形缓冲区可以实现数据的异步读写,即使读写操作发生在不同的线程中,也可以保证数据的完整性。
- 环形缓冲区可以实现数据的缓冲,即使数据生产的速度远远大于消费的速度,也可以缓冲数据。
手撕ringbuffer
#include <iostream>
#include <mutex>
#include <condition_variable>
template<typename T>
class RingBuffer {
public:
RingBuffer(size_t size) : m_size(size), m_buffer(new T[size]), m_read_pos(0), m_write_pos(0) {}
~RingBuffer() { delete[] m_buffer; }
bool push(const T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isFull()) {
return false;
}
m_buffer[m_write_pos] = data;
m_write_pos = (m_write_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool pop(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = m_buffer[m_read_pos];
m_read_pos = (m_read_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool peek(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = m_buffer[m_read_pos];
return true;
}
bool isFull() const {
return (m_write_pos + 1) % m_size == m_read_pos;
}
bool isEmpty() const {
return m_read_pos == m_write_pos;
}
private:
size_t m_size;
T* m_buffer;
size_t m_read_pos;
size_t m_write_pos;
std::mutex m_mutex;
std::condition_variable m_cv;
};
内存对齐的ringbuffer
#include <iostream>
#include <mutex>
#include <condition_variable>
template<typename T, size_t ALIGNMENT = 64>
class AlignedRingBuffer {
public:
AlignedRingBuffer(size_t size) : m_size(size), m_buffer(new char[size * sizeof(T) + ALIGNMENT]), m_read_pos(0), m_write_pos(0) {
m_buffer = (T*)(((size_t)m_buffer + ALIGNMENT - 1) & ~(ALIGNMENT - 1));
}
~AlignedRingBuffer() { delete[] m_buffer; }
bool push(const T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isFull()) {
return false;
}
*(T*)(m_buffer + m_write_pos) = data;
m_write_pos = (m_write_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool pop(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = *(T*)(m_buffer + m_read_pos);
m_read_pos = (m_read_pos + 1) % m_size;
m_cv.notify_one();
return true;
}
bool peek(T& data) {
std::unique_lock<std::mutex> lock(m_mutex);
if (isEmpty()) {
return false;
}
data = *(T*)(m_buffer + m_read_pos);
return true;
}
bool isFull() const {
return (m_write_pos + 1) % m_size == m_read_pos;
}
bool isEmpty() const {
return m_read_pos == m_write_pos;
}
private:
size_t m_size;
char* m_buffer;
size_t m_read_pos;
size_t m_write_pos;
std::mutex m_mutex;
std::condition_variable m_cv;
};
缓存一致性
缓存一致性(Cache Coherency)是指多个CPU或多个处理器共享同一块内存,当其中一个处理器对内存进行修改时,其他处理器需要知道这个修改,并将其缓存中的数据更新。缓存一致性保证了共享内存的正确性,是计算机系统的重要组成部分。
缓存一致性协议:MESI协议,基于总线嗅探,实现了十五的串行化。
- M(Modified):缓存行中的数据被修改,但还没有被写回主存。
- E(Exclusive):缓存行中的数据被独占使用,其他处理器不能访问。
- S(Shared):缓存行中的数据可以被多个处理器共享。
- I(Invalid):缓存行中的数据无效,需要从主存中重新加载。
MESI协议的特点:
- 缓存行的大小一般为64字节。
- 缓存行的状态变化必须通过总线嗅探来通知其他处理器。
- 缓存行的状态变化必须遵循先行发生原则。
- 缓存行的状态变化必须遵循容许延迟的原则。
- 缓存行的状态变化必须遵循写回策略。
- 缓存行的状态变化必须遵循写直达的原则。
- 缓存行的状态变化必须遵循写回拒绝的原则。
- 缓存行的状态变化必须遵循失效拒绝的原则。
CPU 在读写数据的时候,都是在 CPU Cache 读写数据的,原因是 Cache 离 CPU 很近,读写性能相比内存高出很多。对于 Cache 里没有缓存 CPU 所需要读取的数据的这种情况,CPU 则会从内存读取数据,并将数据缓存到 Cache 里面,最后 CPU 再从 Cache 读取数据。
而对于数据的写入,CPU 都会先写入到 Cache 里面,然后再在找个合适的时机写入到内存,那就有「写直达」和「写回」这两种策略来保证 Cache 与内存的数据一致性:
- 写直达,只要有数据写入,都会直接把数据写入到内存里面,这种方式简单直观,但是性能就会受限于内存的访问速度。
- 写回,当缓存行中的数据被修改,并通知其他 CPU 后,缓存行的状态就会变成「M」,这时缓存行中的数据就被标记为「脏」,然后 CPU 就开始将缓存行中的数据写入到内存中,写入完成后,缓存行的状态就会变成「S」,其他 CPU 就可以从内存中读取数据了。写回,对于已经缓存在 Cache 的数据的写入,只需要更新其数据就可以,不用写入到内存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到内存里,这种方式在缓存命中率高的情况,性能会更好。 当今 CPU 都是多核的,每个核心都有各自独立的 L1/L2 Cache,只有 L3 Cache 是多个核心之间共享的。所以,我们要确保多核缓存是一致性的,否则会出现错误的结果。
要想实现缓存一致性,关键是要满足 2 点:
- 第一点是写传播,也就是当某个 CPU 核心发生写入操作时,需要把该事件广播通知给其他核心。
- 第二点是事物的串行化,这个很重要,只有保证了这个,次啊能保障我们的数据是真正一致的,我们的程序在各个不同的核心上运行的结果也是一致的。
基于总线嗅探机制的 MESI 协议,就满足上面了这两点,因此它是保障缓存一致性的协议。 MESI 协议,是已修改、独占、共享、已实现这四个状态的英文缩写的组合。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。
定时器
定时器是操作系统提供的一种机制,用于在指定的时间点后执行某个任务。
#include <sys/epoll.h>
#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>
using namespace std;
struct TimerNodeBase
{
time_t expire;
int64_t id;
};
// TimerNode 继承 TimerNodeBase
struct TimerNode:public TimerNodeBase
{
// C++ 11特性,使用函数对象。降低拷贝消耗,提高效率
using Callback = std::function<void(const TimerNode &node)>;
Callback func;
// 构造函数,只构造一次
TimerNode(int64_t id,time_t expire,Callback func):func(func){
this->id = id;
this->expire = expire;
}
};
// 基类引用,多态特性
bool operator<(const TimerNodeBase &lhd, const TimerNodeBase &rhd)
{
if (lhd.expire < rhd.expire)
return true;
else if (lhd.expire > rhd.expire)
return false;
return lhd.id < rhd.id;
}
class Timer
{
public:
static time_t GetTick()
{
/* C++ 11时间库chrono */
//表示一个具体时间
auto sc = chrono::time_point_cast<chrono::milliseconds>(chrono::steady_clock::now());
auto tmp = chrono::duration_cast<chrono::milliseconds>(sc.time_since_epoch());
return tmp.count();
}
TimerNodeBase AddTimer(time_t msec, TimerNode::Callback func)
{
time_t expire = GetTick() + msec;
//避免拷贝、移动构造
auto ele = timermap.emplace(GenID(), expire, func);
return static_cast<TimerNodeBase>(*ele.first);
}
bool DelTimer(TimerNodeBase &node)
{
// C++ 14新特性,不在需要传一个key对象,传递一个key的等价值
auto iter = timermap.find(node);
if (iter != timermap.end())
{
timermap.erase(iter);
return true;
}
return false;
}
bool CheckTimer()
{
auto iter = timermap.begin();
if (iter != timermap.end() && iter->expire <= GetTick())
{
iter->func(*iter);
timermap.erase(iter);
return true;
}
return false;
}
time_t TimeToSleep()
{
auto iter = timermap.begin();
if (iter == timermap.end())
return -1;//没有定时任务,设置epoll一直阻塞。
time_t diss = iter->expire - GetTick();
return diss > 0 ? diss : 0;
}
private:
static int64_t GenID()
{
return gid++;
}
static int64_t gid;
set<TimerNode, std::less<>> timermap;
};
int64_t Timer::gid = 0;
#define EPOLL_EV_LENFTH 1024
int main()
{
int epfd = epoll_create(1);
unique_ptr<Timer> timer = make_unique<Timer>();
int i = 0;
timer->AddTimer(1000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->AddTimer(1000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->AddTimer(3000, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
cout << Timer::GetTick() << "node id:" << Home | NODE.ID << " revoked times:" << ++i << endl;
});
timer->DelTimer(node);
cout << "now time:" << Timer::GetTick() << endl;
epoll_event evs[EPOLL_EV_LENFTH] = { 0 };
while (1)
{
int nready = epoll_wait(epfd, evs, EPOLL_EV_LENFTH, timer->TimeToSleep());
for (int i = 0; i < nready; i++)
{
/*处理IO事件*/
}
// timer检测和处理
while (timer->CheckTimer());
}
return 0;
}
IO多路复用
IO多路复用是指通过一种机制,一个进程可以监视多个文件句柄fd,一旦某个文件句柄状态改变(比如可读,可写,网络数据到达等),能够通知程序进行相应的读写操作。
select/poll/epoll都是IO多路复用的机制,本质上都是同步I/O模型,都需要在读写事件就绪后自己负责读写,也就是说都需要自己处理读写事件。
select的缺点:
- 单个进程能够监视的文件描述符数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核解决。
- 对于大量连接的服务器,需要维护大量的描述符,内存消耗大。
- 效率不高,每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时是比较大的。
select与poll的区别在于:
- poll使用了pollfd结构体,而不是bitmap,减少了内存消耗,且select的bitmap直接在fd置位不可重用,需要每次重置它。poll选择修改pollfd的revents字段(之后可以置0,就可以重用)。
- 大小没有了1024的限制,可以处理更多的连接。
epoll的优点:
- 没有最大描述符限制,能打开的FD上限远大于1024。
- 效率很高,只有活跃的连接才会调用回调函数,不会浪费CPU资源。
- 内存使用效率高,不会随着FD数目的增加而线性增长。
- 采用事件驱动模型,只需要注册一次事件,之后只要该事件发生,内核会通知进程。
- epfd用户态和内核态共享。置位->重排
简易内存池
#include <cstdlib>
// 每个内存块的前4/8字节用于存储链表指针
struct MemoryBlock {
MemoryBlock* next;
};
class FixedMemoryPool {
private:
MemoryBlock* free_list; // 空闲链表指针
size_t block_size; // 实际供用户使用的块大小
void* pool_begin; // 初始内存池首地址
void* pool_end; // 初始内存池结束地址
public:
// 初始化预分配内存块
FixedMemoryPool(size_t user_block_size, int num_blocks) {
block_size = user_block_size;
// 总内存 = 块数量 * (用户需求大小 + 指针大小)
size_t total_size = num_blocks * (block_size + sizeof(MemoryBlock*));
pool_begin = malloc(total_size);
pool_end = (char*)pool_begin + total_size;
// 初始化空闲链表
free_list = static_cast<MemoryBlock*>(pool_begin);
MemoryBlock* current = free_list;
for(int i=0; i<num_blocks-1; ++i) {
current->next = reinterpret_cast<MemoryBlock*>(
(char*)current + block_size + sizeof(MemoryBlock*)
);
current = current->next;
}
current->next = nullptr;
}
// 分配内存
void* alloc() {
if(!free_list) return nullptr; // 可扩展为自动扩容
MemoryBlock* block = free_list;
free_list = free_list->next;
return (void*)(block + 1); // 返回指针后移一个指针大小的位置
}
// 回收内存
void dealloc(void* ptr) {
if(!ptr) return;
// 指针回退得到完整的MemoryBlock结构
MemoryBlock* block = static_cast<MemoryBlock*>(ptr) - 1;
block->next = free_list;
free_list = block;
}
~FixedMemoryPool() {
free(pool_begin);
}
};
头文件循环包含问题及解决方案
1. 循环引用问题的本质原因
循环引用问题的根源在于C++的编译模型和头文件包含机制:
- 编译单元模型:
- 每个.cpp文件作为独立编译单元
- 编译器需要看到所有使用的类型/函数的完整定义
- 头文件通过文本替换方式包含
- 包含展开过程:
// A.h #include "B.h" // 展开B.h的内容 // B.h #include "A.h" // 展开A.h的内容 // 形成无限递归包含 - 具体发生过程示例:
```text
编译A.cpp时:
- 展开A.h
- 遇到#include “B.h”
- 展开B.h
- 遇到#include “A.h”
- 再次展开A.h… → 无限递归 ```
- 编译器视角:
- 需要为每个类生成完整的内存布局
- 循环引用导致无法确定类大小
- 典型错误:”incomplete type”错误
2. 头文件循环包含示例
典型循环包含场景:
// A.h
#include "B.h"
class A {
B* b; // 需要知道B的大小
};
// B.h
#include "A.h"
class B {
A* a; // 需要知道A的大小
};
2. 导致的问题
- 编译错误:编译器会陷入无限循环
- 符号重定义:同一个类/函数被多次定义
- 增加编译时间
3. 解决方案
3.1 使用前置声明(Forward Declaration)
前置声明能防止循环包含的原理:
- 前置声明只告诉编译器类的存在,不引入类的完整定义
- 编译器只需要知道类名即可处理指针/引用
- 避免了头文件的相互包含导致的无限递归
技术细节:
- 编译器处理指针/引用时只需要知道类型大小
- 指针大小固定(32位4字节/64位8字节)
- 不需要类的完整定义即可确定指针大小
示例:
// A.h
class B; // 前置声明 - 仅声明B类存在
class A {
public:
void setB(B* b); // 使用指针 - 不需要B的完整定义
B* createB(); // 工厂方法
private:
B* m_b; // 指针成员
};
// A.cpp
#include "B.h" // 只在实现文件中包含B.h
B* A::createB() {
return new B(); // 此时需要B的完整定义
}
3.2 使用接口隔离原则
接口隔离原则(ISP)应用:
- 将大接口拆分为多个小接口
- 客户端只依赖需要的接口
- 通过纯虚基类定义抽象接口
示例:
// 不好的设计:庞大接口
class IMonster {
public:
virtual void attack() = 0;
virtual void move() = 0;
virtual void speak() = 0;
};
// 好的设计:接口隔离
class IAttacker {
public:
virtual void attack() = 0;
};
class IMover {
public:
virtual void move() = 0;
};
class ISpeaker {
public:
virtual void speak() = 0;
};
// 客户端只依赖需要的接口
class Player {
public:
void fight(IAttacker* enemy) {
enemy->attack();
}
};
3.3 使用Pimpl惯用法
Pimpl(Pointer to Implementation)模式:
- 将实现细节隐藏到Impl类中
- 头文件只包含接口和前置声明
- 实现放到cpp文件中
示例:
// A.h - 头文件
class A {
public:
A();
~A();
void publicMethod();
private:
class Impl; // 前置声明实现类
Impl* pimpl; // 实现指针
};
// A.cpp - 实现文件
#include "B.h" // 可以安全包含其他头文件
class A::Impl {
public:
void privateMethod() { /*...*/ }
B b; // 可以包含其他类的实例
};
A::A() : pimpl(new Impl()) {}
A::~A() { delete pimpl; }
void A::publicMethod() { pimpl->privateMethod(); }
优势:
- 彻底消除头文件依赖
- 提高编译速度
- 保持ABI兼容性
- 实现可自由修改不影响接口
3.4 使用#pragma once
虽然不能解决循环包含,但可以防止重复包含:
#pragma once
// 头文件内容
4. 最佳实践
- 尽量使用前置声明代替包含头文件
- 遵循单一职责原则,减少头文件依赖
- 使用接口隔离原则
- 使用Pimpl等设计模式降低耦合
- 保持头文件简洁,只包含必要的声明
5. 检测工具
- 编译器警告:开启-Wall -Wextra
- 静态分析工具:clang-tidy, cppcheck
- 依赖关系可视化工具:Doxygen, Include What You Use(IWYU)
设计原则SOLID
- 单一职责原则 一个类应该只有一个因其他变化的原因,一个类应该只负责一个职责或者功能。
- 开闭原则 软件实体应该是可扩展的,但是不可修改的。我们应该在不修改现有代码的情况下添加新的功能。对扩展开放,对修改关闭。
- 里氏替换原则 子类型必须能够替换掉它们的基类型。
- 接口隔离原则 客户端不应该被强迫依赖它们不适用的接口。
- 依赖倒置原则 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。底层细节实现可以根据情况进行替换,高层调用的是抽象。
组合数计算与组合生成
1. 组合数概念
组合数C(n,k)表示从n个不同元素中取出k个元素的组合数量,计算公式为: [ C(n,k) = \frac{n!}{k!(n-k)!} ]
2. 生成所有组合的算法
递归回溯法
#include <vector>
#include <iostream>
void backtrack(std::vector<std::vector<int>>& result,
std::vector<int>& current,
const std::vector<int>& nums,
int start, int k) {
if (current.size() == k) {
result.push_back(current);
return;
}
for (int i = start; i < nums.size(); ++i) {
current.push_back(nums[i]);
backtrack(result, current, nums, i + 1, k);
current.pop_back();
}
}
std::vector<std::vector<int>> combine(const std::vector<int>& nums, int k) {
std::vector<std::vector<int>> result;
std::vector<int> current;
backtrack(result, current, nums, 0, k);
return result;
}
位运算法(适用于n <= 64)
std::vector<std::vector<int>> combineBits(const std::vector<int>& nums, int k) {
std::vector<std::vector<int>> result;
int n = nums.size();
for (int mask = 0; mask < (1 << n); ++mask) {
if (__builtin_popcount(mask) != k) continue;
std::vector<int> combination;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
combination.push_back(nums[i]);
}
}
result.push_back(combination);
}
return result;
}
3. 使用示例
int main() {
std::vector<int> nums = {1, 2, 3, 4};
int k = 2;
auto combinations = combine(nums, k);
std::cout << "从" << nums.size() << "个元素中取" << k << "个的组合数: "
<< combinations.size() << "\n";
std::cout << "所有组合:\n";
for (const auto& comb : combinations) {
for (int num : comb) {
std::cout << num << " ";
}
std::cout << "\n";
}
return 0;
}
4. 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |————–|—————-|—————-|———————-| | 递归回溯法 | O(C(n,k)) | O(k) | 通用,适合任意n和k | | 位运算法 | O(n*2^n) | O(k) | n较小(n<=64)时效率高 |
内存对其原则
TCP滑动窗口实现细节
滑动窗口工作原理
- 发送方维护一个发送窗口,包含:
- 已发送待确认的数据
- 可以发送但未发送的数据
- 接收方维护一个接收窗口,通告剩余缓冲区大小
- 窗口滑动条件:
- 收到新的ACK确认
- 接收方窗口更新
// 简化的滑动窗口实现
class TcpSlidingWindow {
private:
uint32_t send_base; // 发送窗口起始序号
uint32_t next_seq; // 下一个要发送的序号
uint32_t window_size; // 当前窗口大小
std::map<uint32_t, Packet> sent_packets; // 已发送未确认的数据包
public:
void on_ack_received(uint32_t ack_num) {
// 移动发送窗口
while (send_base < ack_num) {
sent_packets.erase(send_base);
send_base++;
}
}
void on_window_update(uint32_t new_window) {
window_size = new_window;
}
bool can_send() const {
return (next_seq - send_base) < window_size;
}
};
拥塞控制算法实现
1. 慢启动算法
- 窗口大小从1个MSS开始
- 每收到一个ACK,窗口大小增加1个MSS
- 指数增长直到达到阈值(ssthresh)
2. 拥塞避免算法
- 窗口大小线性增长
- 每RTT时间窗口增加1个MSS
- 公式:cwnd = cwnd + 1/cwnd
3. 快速重传算法
- 收到3个重复ACK立即重传丢失报文
- 无需等待超时
4. 快速恢复算法
- 重传后不将窗口降为1
- 将窗口设置为ssthresh + 3*MSS
- 进入拥塞避免阶段
class TcpCongestionControl {
private:
uint32_t cwnd; // 拥塞窗口大小
uint32_t ssthresh; // 慢启动阈值
uint32_t dup_ack_count; // 重复ACK计数器
public:
void on_packet_loss() {
ssthresh = cwnd / 2;
cwnd = 1; // 慢启动
dup_ack_count = 0;
}
void on_dup_ack() {
dup_ack_count++;
if (dup_ack_count == 3) {
// 快速重传
retransmit_packet();
ssthresh = cwnd / 2;
cwnd = ssthresh + 3; // 快速恢复
}
}
void on_new_ack() {
if (cwnd < ssthresh) {
// 慢启动阶段
cwnd += 1; // 指数增长
} else {
// 拥塞避免阶段
cwnd += 1.0 / cwnd; // 线性增长
}
dup_ack_count = 0;
}
};
(1) 结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(3) 结构体的总大小为 有效对齐值 的整数倍,如有需要编译器会在最末一个成员之后加上填充字节
TCP与UDP协议对比
1. 基本概念
TCP (传输控制协议)
- 面向连接的协议
- 提供可靠的、有序的数据传输
- 具有流量控制和拥塞控制机制
- 基于字节流的传输方式,分段,大于MSS的包会被拆分
UDP (用户数据报协议)
- 无连接的协议
- 提供不可靠的数据传输服务
- 传输速度快但无法保证数据到达
- 基于数据报的传输方式,不分段,不保证包的顺序
2. 主要区别
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手建立连接) | 无连接 |
| 可靠性 | 可靠传输(确认、重传机制) | 不可靠传输 |
| 数据顺序 | 保证数据按序到达 | 不保证顺序 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(多种算法) | 无 |
| 传输效率 | 相对较低 | 相对较高 |
| 头部开销 | 20-60字节 | 8字节 |
| 双工性 | 全双工 | 可以是全双工 |
3. 应用场景
TCP典型应用
- Web浏览(HTTP/HTTPS)
- 文件传输(FTP)
- 电子邮件(SMTP/POP3/IMAP)
- 远程登录(SSH/Telnet)
UDP典型应用
- 视频/音频流媒体
- 实时在线游戏
- DNS查询
- VoIP(如Skype)
- 广播/多播应用
4. 选择建议
- 需要可靠数据传输时选择TCP
- 需要低延迟、可容忍少量数据丢失时选择UDP
- 实时性要求高的应用通常选择UDP
- 需要确保数据完整性的应用选择TCP
5. 总结
TCP和UDP各有优缺点,没有绝对的优劣之分,关键是根据应用场景的需求选择合适的协议。现代网络应用中,很多场景会同时使用两种协议,发挥各自的优势。
6. 在UDP上实现可靠传输
虽然UDP本身不提供可靠性保证,但可以在应用层实现类似TCP的可靠传输机制。以下是常见的实现方案:
6.1 基本实现思路
- 序列号机制:为每个数据包分配唯一序列号
- 确认应答机制:接收方返回ACK确认收到的数据
- 超时重传机制:未收到ACK时重传数据
- 流量控制:通过滑动窗口控制发送速率
- 拥塞控制:实现简单的拥塞避免算法
6.2 实现示例
class ReliableUDP {
private:
uint32_t next_seq; // 下一个发送序号
uint32_t ack_seq; // 已确认序号
std::map<uint32_t, Packet> unacked_packets; // 未确认数据包
std::mutex mtx;
// 重传定时器
void start_retransmit_timer(uint32_t seq) {
std::thread([this, seq](){
std::this_thread::sleep_for(std::chrono::milliseconds(RTO));
std::lock_guard<std::mutex> lock(mtx);
if (unacked_packets.count(seq)) {
resend_packet(seq);
}
}).detach();
}
public:
void send(const std::string& data) {
std::lock_guard<std::mutex> lock(mtx);
Packet packet = create_packet(next_seq++, data);
unacked_packets[packet.seq] = packet;
udp_send(packet);
start_retransmit_timer(packet.seq);
}
void on_ack_received(uint32_t ack_seq) {
std::lock_guard<std::mutex> lock(mtx);
// 移除已确认的数据包
while (!unacked_packets.empty() &&
unacked_packets.begin()->first <= ack_seq) {
unacked_packets.erase(unacked_packets.begin());
}
this->ack_seq = ack_seq;
}
};
6.3 开源实现参考
- QUIC:Google开发的基于UDP的可靠传输协议
- UDT:高性能UDP数据传输协议
- ENET:游戏网络库,提供可靠UDP传输
- RUDP:可靠UDP协议实现
6.4 注意事项
- 性能开销:可靠性机制会增加延迟和CPU使用率
- 实现复杂度:需要处理各种边界条件和异常情况
- 不适合所有场景:简单应用可能不需要完整可靠性
- 缓冲区管理:需要合理设置发送和接收缓冲区大小
6.5 选择建议
- 需要低延迟但又要一定可靠性的场景适合UDP+应用层可靠性
- 完全可靠性要求高的场景建议直接使用TCP
- 实时性要求极高的场景可以使用原生UDP
简易vector实现
以下是一个简化版的std::vector实现,展示了其核心机制:
template<typename T>
class SimpleVector {
private:
T* data; // 数据存储指针
size_t size_; // 当前元素数量
size_t capacity_; // 当前容量
public:
// 迭代器类型定义
using iterator = T*;
using const_iterator = const T*;
// 构造函数
SimpleVector() : data(nullptr), size_(0), capacity_(0) {}
// 移动构造函数
SimpleVector(SimpleVector&& other) noexcept
: data(other.data), size_(other.size_), capacity_(other.capacity_) {
other.data = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
// 析构函数
~SimpleVector() {
delete[] data;
}
// 移动赋值运算符
SimpleVector& operator=(SimpleVector&& 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;
}
// 迭代器支持
iterator begin() noexcept { return data; }
iterator end() noexcept { return data + size_; }
const_iterator begin() const noexcept { return data; }
const_iterator end() const noexcept { return data + size_; }
const_iterator cbegin() const noexcept { return data; }
const_iterator cend() const noexcept { return data + size_; }
// 添加元素
void push_back(const T& value) {
if (size_ >= capacity_) {
reserve(capacity_ == 0 ? 1 : capacity_ * 2);
}
data[size_++] = value;
}
// 添加元素(移动语义)
void push_back(T&& value) {
if (size_ >= capacity_) {
reserve(capacity_ == 0 ? 1 : capacity_ * 2);
}
data[size_++] = std::move(value);
}
// 原位构造元素
template<typename... Args>
void emplace_back(Args&&... args) {
if (size_ >= capacity_) {
reserve(capacity_ == 0 ? 1 : capacity_ * 2);
}
new(&data[size_++]) T(std::forward<Args>(args)...);
}
// 删除末尾元素
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];
}
// 交换两个vector
void swap(SimpleVector& other) noexcept {
std::swap(data, other.data);
std::swap(size_, other.size_);
std::swap(capacity_, other.capacity_);
}
// 当前元素数量
size_t size() const noexcept {
return size_;
}
// 当前容量
size_t capacity() const noexcept {
return capacity_;
}
// 扩容函数
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] = std::move(data[i]);
}
delete[] data;
data = new_data;
capacity_ = new_capacity;
}
};
关键点说明:
- 使用模板支持任意类型
- 动态内存管理,自动扩容(通常2倍增长)
- 提供基本接口:push_back/pop_back/size/capacity
- 实现了operator[]支持类似数组的访问
- 添加了完整的迭代器支持(begin/end/cbegin/cend)
- 实现了swap方法用于高效交换内容
- 优化了reserve方法避免不必要扩容
- 添加了const版本的操作符重载
- 所有不抛出异常的方法标记为noexcept
- 添加了移动构造函数和移动赋值运算符
- 添加了emplace_back方法支持原位构造
- 添加了右值引用版本的push_back
- 注意内存释放,防止内存泄漏
使用示例:
SimpleVector<std::string> vec;
// 使用push_back添加元素
vec.push_back("hello");
// 使用移动语义添加元素
std::string str = "world";
vec.push_back(std::move(str));
// 使用emplace_back原位构造元素
vec.emplace_back(3, 'x'); // 构造"xxx"
// 使用移动构造
SimpleVector<std::string> vec2(std::move(vec));
// 使用移动赋值
SimpleVector<std::string> vec3;
vec3 = std::move(vec2);
// 使用迭代器遍历
for (auto it = vec3.begin(); it != vec3.end(); ++it) {
std::cout << *it << " ";
}
// 使用范围for循环
for (const auto& s : vec3) {
std::cout << s << " ";
}
// 使用operator[]
std::cout << vec3[0] << std::endl;
// 使用swap
SimpleVector<std::string> vec4;
vec4.swap(vec3);
// 使用reserve预留空间
vec4.reserve(100);
改进版现场这个能写出来!
template<typename T>
class Vector {
public:
Vector() = default;
~Vector()
{
clear();
deallocate();
}
T& operator[](size_t index)
{
return data[index];
}
const T& operator[](size_t index) const
{
return data[index];
}
size_t size() const
{
return size;
}
size_t capacity() const
{
return _capacity;
}
bool empty() const
{
return size == 0;
}
void push_back(const T& value)
{
emplace_back(value);
}
void push_back(T&& value)
{
emplace_back(std::move(value));
}
template<typename... Args>
T& emplace_back(Args&&... args)
{
if (size == _capacity)
{
reallocate(_capacity > 0 ? _capacity * 2 : 1);
}
::operator new(data + size) T(std::forward<Args>(args)...);
return data[size++];
}
void clear()
{
for (size_t i = 0; i < size; i++) {
data[i].~T();
}
size = 0;
}
T& back()
{
return data[size - 1];
}
const T& back() const
{
return data[size - 1];
}
void pop_back()
{
data[--size].~T();
}
private:
T* allocate(size_t newCapacity)
{
return static_cast<T*>(::operator new(newCapacity * sizeof(T)));
}
void deallocate()
{
if (data != nullptr)
{
::operator delete(data);
data = nullptr;
_capacity = 0;
}
}
void reallocate(size_t newCapacity)
{
T* newData = allocate(newCapacity);
size_t newSize = 0;
try {
for (; i < size; newSize++) {
::operator new(newData + newSize) T(std::move_if_noexcept(data[newSize]));
}
}
catch (...) {
for (size_t i = 0; i < newSize; i++) {
data[i].~T();
}
::operator delete(newData);
throw;
}
clear();
deallocate();
data = newData;
_capacity = newCapacity;
size = newSize;
}
private:
T* data = nullptr;
size_t size = 0;
size_t _capacity = 0;
};
手撕共享指针
#include <atomic>
template<typename T>
class SharedPtr {
private:
T* ptr;
std::atomic<unsigned>* count;
public:
// 构造函数
explicit SharedPtr(T* p = nullptr)
: ptr(p), count(new std::atomic<unsigned>(1)) {}
// 拷贝构造函数
SharedPtr(const SharedPtr& other) noexcept
: ptr(other.ptr), count(other.count) {
count->fetch_add(1, std::memory_order_relaxed);
}
// 移动构造函数
SharedPtr(SharedPtr&& other) noexcept
: ptr(other.ptr), count(other.count) {
other.ptr = nullptr;
other.count = nullptr;
}
// 析构函数
~SharedPtr() {
if (count && count->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr;
delete count;
}
}
// 指针操作
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
// 获取引用计数
unsigned use_count() const {
return count ? count->load(std::memory_order_relaxed) : 0;
}
};
性能调优与崩溃分析
1. 性能调优工具
C++开发中常用的性能分析工具:
- gprof
- GNU性能分析工具
- 使用步骤:
g++ -pg program.cpp -o program ./program gprof program gmon.out > analysis.txt - 输出调用图和耗时统计
- perf
- Linux系统性能分析工具
- 常用命令:
perf stat ./program # 基本统计 perf record ./program # 记录性能数据 perf report # 分析记录
- Valgrind
- 内存调试和性能分析工具套件
- 主要工具:
- Memcheck: 内存错误检测
- Callgrind: 调用图分析
- Massif: 堆分析
| 工具 | 优势 | 局限性 |
|---|---|---|
| gprof | 简单易用 | 需要重新编译 |
| perf | 系统级分析,开销低 | 需要root权限 |
| Valgrind | 全面检测内存问题 | 运行速度慢 |
2. 性能调优流程
标准性能优化流程:
- 基准测试
- 建立性能基准
- 使用可靠计时方法
- 性能分析
- 使用工具识别热点
- 关注CPU缓存命中率
- 优化实施
- 算法优化优先
- 考虑数据局部性
- 减少内存分配
- 验证测试
- 确保优化后功能正确
- 比较性能提升
3. 崩溃定位解决方案
核心转储分析:
- 启用core dump生成:
ulimit -c unlimited echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern - 使用gdb分析:
gdb program core (gdb) bt # 查看调用栈 (gdb) info locals # 查看局部变量
常见崩溃原因:
- 空指针解引用
- 内存越界访问
- 双重释放
- 未捕获异常
调试技巧:
- 使用
-g编译选项保留调试信息 - 添加日志输出关键变量
- 使用AddressSanitizer检测内存错误:
g++ -fsanitize=address -g program.cpp