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;
}

关键点:

  1. 虽然实际调用的是Derived::foo(),但使用的默认参数是Base::foo()的10
  2. 默认参数值在编译时根据指针类型(Base*)确定
  3. 虚函数调用在运行时根据实际对象类型(Derived)确定

最佳实践:

  • 避免在虚函数中使用默认参数
  • 如果必须使用,确保派生类和基类的默认参数一致
  • 可以使用非虚函数提供默认参数,调用虚函数实现:
class Base {
public:
    void foo(int x = 10) { do_foo(x); }
protected:
    virtual void do_foo(int x) { /*...*/ }
};
  • 虚拟内存分区:栈区(栈区之上时内核空间)、文件映射区、堆区、数据区(静态存储区)和代码区。 可执行程序中不同部分存储的内容如下:
可执行程序中(磁盘) 作用 对应虚拟内存分区(内存)
.bss 未初始化的或初始化为0的全局、静态变量 静态存储区
.data 初始化的全局、静态变量 静态存储区
.rodata 只读数据段和虚函数表 代码区
.text 具体代码表 代码区
  • 虚函数表指针的创建时机:
  1. 类对象构造的时候,把类的虚函数表地址赋值给vptr。
  2. 没有构造函数,编译器会生成默认的构造函数(有第一条的作用)。
  3. 继承的情况下(例如B继承A),先调用基类的构造函数,把A的虚函数表的地址赋值给vptr。再调用子类构造函数,把B的虚函数表的地址赋值给vptr。

虚函数表和虚函数表指针的关系

每个类只有一个虚函数表,类的不同对象通常来说虚函数表指针vptr(每个类实例有一个)是不一样的(自身地址不一样,指向的内容一样,为深拷贝。浅拷贝会导致一个置null后另一个类实例找不到虚函数表)。使用的时候需要显式地定义拷贝构造函数或重载赋值运算符。

默认拷贝构造函数何时生成

背景 不提供的话,位拷贝,全盘复制。

  • 危害1:堆上资源也会复制
  • 危害2:文件句柄,socket也复制 持有相同的对象资源,句柄,一者释放,另一者就拿不到数据。

触发默认构造函数时机

  • A b;
  • A a(b);
  • A a=b;
  • 函数传参,形参为类对象
  • 函数返回值为类对象(有返回值优化),无优化会多次调用。(C++11之后还会看是否有移动构造,然后看类有没有默认构造,再没有,报错)

生成时间 编译时生成。

  1. 类成员变量是一个类,该成员类有默认构造函数。
  2. 类继承自基类,基类有默认构造函数。
  3. 类成员有虚函数。
  4. 类继承自基类,基类有虚函数。

进程和线程的区别

本质:进程是资源费配的基本单位,线程是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++20std::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. 动态库加载过程

  1. 查找动态库路径(按以下顺序):
    • LD_PRELOAD环境变量指定路径
    • RPATH/RUNPATH指定的路径
    • LD_LIBRARY_PATH环境变量
    • /etc/ld.so.cache缓存
    • 默认路径(/lib, /usr/lib等)
  2. 加载动态库到内存
  3. 解析符号和重定位

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

解决方案:

  1. 检查动态库是否存在
  2. 设置LD_LIBRARY_PATH环境变量
  3. 使用patchelf修改RPATH
    patchelf --set-rpath '$ORIGIN/lib' /path/to/program
    

问题2:符号未定义

undefined symbol: SomeFunction

解决方案:

  1. 检查动态库是否包含该符号
    nm -D /path/to/library.so | grep SomeFunction
    
  2. 检查链接顺序是否正确
  3. 检查编译器版本和选项是否一致

问题3:ABI不兼容

Segmentation fault (core dumped)

解决方案:

  1. 确保所有动态库使用相同编译器版本编译
  2. 检查-std和-abi版本是否一致
  3. 使用统一的编译选项

5. 预防措施

  1. 使用版本号管理动态库
    libfoo.so.1.2.3
    
  2. 使用-Wl,–as-needed减少不必要的依赖
  3. 定期检查动态库依赖关系
  4. 使用静态分析工具检查ABI兼容性

6. 内存问题排查

  1. 使用valgrind检测内存泄漏
    valgrind --leak-check=full /path/to/program
    
  2. 使用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是字节流协议,接收方无法区分消息边界,多个应用层数据包可能被粘在一起接收。
  • 原因
    1. Nagle算法将多个小包合并发送
    2. 接收方缓冲区中数据堆积
  • 解决方案
    1. 固定长度消息
    2. 特殊分隔符(如\n)
    3. 消息头包含长度字段(常用)

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++的编译模型和头文件包含机制:

  1. 编译单元模型
    • 每个.cpp文件作为独立编译单元
    • 编译器需要看到所有使用的类型/函数的完整定义
    • 头文件通过文本替换方式包含
  2. 包含展开过程
    // A.h
    #include "B.h"  // 展开B.h的内容
       
    // B.h 
    #include "A.h"  // 展开A.h的内容
    // 形成无限递归包含
    
  3. 具体发生过程示例: ```text 编译A.cpp时:
    1. 展开A.h
    2. 遇到#include “B.h”
    3. 展开B.h
    4. 遇到#include “A.h”
    5. 再次展开A.h… → 无限递归 ```
  4. 编译器视角
    • 需要为每个类生成完整的内存布局
    • 循环引用导致无法确定类大小
    • 典型错误:”incomplete type”错误

2. 头文件循环包含示例

典型循环包含场景:

// A.h
#include "B.h"  
class A {
    B* b;  // 需要知道B的大小
};

// B.h
#include "A.h"
class B {
    A* a;  // 需要知道A的大小
};

2. 导致的问题

  1. 编译错误:编译器会陷入无限循环
  2. 符号重定义:同一个类/函数被多次定义
  3. 增加编译时间

3. 解决方案

3.1 使用前置声明(Forward Declaration)

前置声明能防止循环包含的原理:

  1. 前置声明只告诉编译器类的存在,不引入类的完整定义
  2. 编译器只需要知道类名即可处理指针/引用
  3. 避免了头文件的相互包含导致的无限递归

技术细节:

  • 编译器处理指针/引用时只需要知道类型大小
  • 指针大小固定(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)应用:

  1. 将大接口拆分为多个小接口
  2. 客户端只依赖需要的接口
  3. 通过纯虚基类定义抽象接口

示例:

// 不好的设计:庞大接口
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)模式:

  1. 将实现细节隐藏到Impl类中
  2. 头文件只包含接口和前置声明
  3. 实现放到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(); }

优势:

  1. 彻底消除头文件依赖
  2. 提高编译速度
  3. 保持ABI兼容性
  4. 实现可自由修改不影响接口

3.4 使用#pragma once

虽然不能解决循环包含,但可以防止重复包含:

#pragma once
// 头文件内容

4. 最佳实践

  1. 尽量使用前置声明代替包含头文件
  2. 遵循单一职责原则,减少头文件依赖
  3. 使用接口隔离原则
  4. 使用Pimpl等设计模式降低耦合
  5. 保持头文件简洁,只包含必要的声明

5. 检测工具

  1. 编译器警告:开启-Wall -Wextra
  2. 静态分析工具:clang-tidy, cppcheck
  3. 依赖关系可视化工具: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滑动窗口实现细节

滑动窗口工作原理

  1. 发送方维护一个发送窗口,包含:
    • 已发送待确认的数据
    • 可以发送但未发送的数据
  2. 接收方维护一个接收窗口,通告剩余缓冲区大小
  3. 窗口滑动条件:
    • 收到新的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 基本实现思路

  1. 序列号机制:为每个数据包分配唯一序列号
  2. 确认应答机制:接收方返回ACK确认收到的数据
  3. 超时重传机制:未收到ACK时重传数据
  4. 流量控制:通过滑动窗口控制发送速率
  5. 拥塞控制:实现简单的拥塞避免算法

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 开源实现参考

  1. QUIC:Google开发的基于UDP的可靠传输协议
  2. UDT:高性能UDP数据传输协议
  3. ENET:游戏网络库,提供可靠UDP传输
  4. RUDP:可靠UDP协议实现

6.4 注意事项

  1. 性能开销:可靠性机制会增加延迟和CPU使用率
  2. 实现复杂度:需要处理各种边界条件和异常情况
  3. 不适合所有场景:简单应用可能不需要完整可靠性
  4. 缓冲区管理:需要合理设置发送和接收缓冲区大小

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;
    }
};

关键点说明:

  1. 使用模板支持任意类型
  2. 动态内存管理,自动扩容(通常2倍增长)
  3. 提供基本接口:push_back/pop_back/size/capacity
  4. 实现了operator[]支持类似数组的访问
  5. 添加了完整的迭代器支持(begin/end/cbegin/cend)
  6. 实现了swap方法用于高效交换内容
  7. 优化了reserve方法避免不必要扩容
  8. 添加了const版本的操作符重载
  9. 所有不抛出异常的方法标记为noexcept
  10. 添加了移动构造函数和移动赋值运算符
  11. 添加了emplace_back方法支持原位构造
  12. 添加了右值引用版本的push_back
  13. 注意内存释放,防止内存泄漏

使用示例:

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++开发中常用的性能分析工具:

  1. gprof
    • GNU性能分析工具
    • 使用步骤:
      g++ -pg program.cpp -o program
      ./program
      gprof program gmon.out > analysis.txt
      
    • 输出调用图和耗时统计
  2. perf
    • Linux系统性能分析工具
    • 常用命令:
      perf stat ./program  # 基本统计
      perf record ./program # 记录性能数据
      perf report # 分析记录
      
  3. Valgrind
    • 内存调试和性能分析工具套件
    • 主要工具:
      • Memcheck: 内存错误检测
      • Callgrind: 调用图分析
      • Massif: 堆分析
工具 优势 局限性
gprof 简单易用 需要重新编译
perf 系统级分析,开销低 需要root权限
Valgrind 全面检测内存问题 运行速度慢

2. 性能调优流程

标准性能优化流程:

  1. 基准测试
    • 建立性能基准
    • 使用可靠计时方法
  2. 性能分析
    • 使用工具识别热点
    • 关注CPU缓存命中率
  3. 优化实施
    • 算法优化优先
    • 考虑数据局部性
    • 减少内存分配
  4. 验证测试
    • 确保优化后功能正确
    • 比较性能提升

3. 崩溃定位解决方案

核心转储分析:

  1. 启用core dump生成:
    ulimit -c unlimited
    echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
    
  2. 使用gdb分析:
    gdb program core
    (gdb) bt  # 查看调用栈
    (gdb) info locals  # 查看局部变量
    

常见崩溃原因:

  • 空指针解引用
  • 内存越界访问
  • 双重释放
  • 未捕获异常

调试技巧:

  • 使用-g编译选项保留调试信息
  • 添加日志输出关键变量
  • 使用AddressSanitizer检测内存错误:
    g++ -fsanitize=address -g program.cpp