C++——手撕智能指针、单例模式、线程池、String

智能指针

今天我们来学习一下C++中的智能指针,如果有人不知道C++中的智能指针的概念的话:

C++智能指针是一种基于RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制的高级内存管理工具,用于自动化动态内存的分配与释放,从而避免内存泄漏、悬空指针等问题。其核心思想是将资源(如堆内存)的生命周期绑定到对象的生命周期上——对象构造时获取资源,析构时自动释放资源

目前主流的智能指针包含两种:独占式指针和共享式指针。

独占式指针

什么是独占式指针?

我们来看一个简化版的实现:

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:T* ptr = nullptr;       // 管理的裸指针Deleter deleter;        // 删除器(默认为 std::default_delete)public:// 1. 构造与析构explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}~unique_ptr() noexcept {if (ptr) deleter(ptr);  // 自动调用删除器}// 2. 禁用拷贝unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;// 3. 允许移动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;}// 4. 关键接口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; }void reset(T* p = nullptr) noexcept {if (ptr) deleter(ptr);  // 释放旧资源ptr = p;                // 接管新资源}T* release() noexcept {T* old_ptr = ptr;ptr = nullptr;return old_ptr;         // 放弃所有权,返回裸指针}
};

我们来一点一点介绍:

template <typename T, typename Deleter = std::default_delete<T>>

这是模板的定义,智能指针本质上是一个封装了指针的类模板。typename T表明泛型,同时定义一个默认类型为std::default_delete<T>的名为Deleter的模板参数来作为删除器。

然后我们定义好指针和删除器:

private:T* ptr = nullptr;       // 管理的裸指针Deleter deleter;        // 删除器(默认为 std::default_delete)

构造函数和析构函数:

    // 1. 构造与析构explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}~unique_ptr() noexcept {if (ptr) deleter(ptr);  // 自动调用删除器}

这里的explicit和noexcept关键字的作用:禁止隐式类型转换,强制要求显式构造对象或类型转换,避免意外行为;声明函数不抛出异常,优化性能并提升可靠性;

当我们调用析构函数后,如果检测到独占式指针存在就删除掉这个指针。

    // 2. 禁用拷贝unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;

这是我们的拷贝构造函数和赋值运算符重载,我们都修改为delete。

一般的拷贝构造函数和赋值运算符重载的格式如下:

ClassName(const ClassName& other);
...
ClassName& operator=(const ClassName& other);

可以看到参数列表中的内容格式是固定的。

    // 3. 允许移动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;}

这是我们的移动语义的内容,如果参数是右值则将当前资源转移到另一个指针。

    // 4. 关键接口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; }

这是一系列的接口实现,包括get方法获取指针,重载*和->模拟指针操作,

    void reset(T* p = nullptr) noexcept {if (ptr) deleter(ptr);  // 释放旧资源ptr = p;                // 接管新资源}T* release() noexcept {T* old_ptr = ptr;ptr = nullptr;return old_ptr;         // 放弃所有权,返回裸指针}

这个是转移指针指向资源的所有权和释放内存的实现。

接下来我们加入一段测试代码:

// 自定义测试类,通过构造函数/析构函数打印验证生命周期
class TestResource {
public:explicit TestResource(int id) : id(id) {std::cout << "TestResource #" << id << " created\n";}~TestResource() {std::cout << "TestResource #" << id << " destroyed\n";}void print() const {std::cout << "Accessing resource #" << id << "\n";}private:int id;
};
//
......
//
int main() {std::cout << "\n=== 测试1: 基础功能与自动释放 ===" << std::endl;{unique_ptr<TestResource> p1(new TestResource(1));  // 创建资源 #1// 验证访问功能assert(p1 && "指针应为非空");p1->print();       // 通过 -> 访问(*p1).print();     // 通过 * 访问std::cout << "离开作用域,应自动释放资源..." << std::endl;} // p1 在此析构,资源 #1 应被自动释放std::cout << "\n=== 测试2: 移动语义 ===" << std::endl;{unique_ptr<TestResource> p1(new TestResource(2));unique_ptr<TestResource> p2 = std::move(p1);  // 移动构造assert(!p1 && "移动后原指针应为空");assert(p2 && "新指针应接管资源");std::cout << "资源所有权已转移至 p2" << std::endl;unique_ptr<TestResource> p3;p3 = std::move(p2);  // 移动赋值assert(!p2 && "移动赋值后源指针应为空");assert(p3 && "目标指针应接管资源");std::cout << "资源所有权再次转移至 p3" << std::endl;} // p3 析构时释放资源 #2std::cout << "\n=== 测试3: reset() 与 release() ===" << std::endl;{unique_ptr<TestResource> p(new TestResource(3));// 测试 reset()p.reset(new TestResource(4));  // 释放旧资源 #3,接管新资源 #4p->print();  // 应访问资源 #4// 测试 release()TestResource* raw_ptr = p.release();assert(!p && "release()后智能指针应为空");std::cout << "已释放所有权,手动管理资源..." << std::endl;delete raw_ptr;  // 需手动释放} // p 析构时不释放资源(已release)std::cout << "\n=== 测试4: 禁止拷贝(编译时验证)===" << std::endl;{unique_ptr<TestResource> p1(new TestResource(5));// unique_ptr<TestResource> p2 = p1;  // 应产生编译错误:尝试拷贝构造// unique_ptr<TestResource> p3;// p3 = p1;                           // 应产生编译错误:尝试拷贝赋值std::cout << "拷贝操作被正确禁止(未使用注释代码时正常编译)" << std::endl;}std::cout << "\n=== 测试5: 布尔转换验证 ===" << std::endl;{unique_ptr<TestResource> p1(new TestResource(6));unique_ptr<TestResource> p2;if (p1) {std::cout << "p1 有效(布尔转换正确)" << std::endl;}if (!p2) {std::cout << "p2 无效(布尔转换正确)" << std::endl;}}std::cout << "\n所有测试通过!" << std::endl;return 0;
}

整个测试的逻辑如下:

如果有人不知道assert的作用的话:assert(断言)是一种在代码中嵌入检查点的调试机制,用于在运行时或编译时验证程序逻辑的假设条件是否成立。

结果输出如下:

共享式指针

然后是我们的共享式指针。

template <typename T>
class SharedPtr {
private:T* ptr = nullptr;                   // 指向动态资源的指针std::atomic<size_t>* ref_count = nullptr; // 原子引用计数器// 释放资源并更新引用计数void release() noexcept {if (!ref_count) return;// 原子减少计数,若归零则销毁资源if (--(*ref_count) == 0) {delete ptr;          // 释放对象delete ref_count;    // 释放计数器ptr = nullptr;ref_count = nullptr;}}public:// === 构造函数 ===SharedPtr() noexcept = default; // 默认构造(空指针)// 从原始指针构造(独占资源)explicit SharedPtr(T* raw_ptr): ptr(raw_ptr), ref_count(new std::atomic<size_t>(1)) {}// 拷贝构造(共享所有权)SharedPtr(const SharedPtr& other) noexcept: ptr(other.ptr), ref_count(other.ref_count) {if (ref_count) (*ref_count)++; // 引用计数增加}// 移动构造(转移所有权)SharedPtr(SharedPtr&& other) noexcept: ptr(other.ptr), ref_count(other.ref_count) {other.ptr = nullptr;other.ref_count = nullptr;}// === 析构函数 ===~SharedPtr() { release(); }// === 赋值运算符 ===// 拷贝赋值SharedPtr& operator=(const SharedPtr& other) noexcept {if (this != &other) {release();                // 释放当前资源ptr = other.ptr;          // 共享资源ref_count = other.ref_count;if (ref_count) (*ref_count)++;}return *this;}// 移动赋值SharedPtr& operator=(SharedPtr&& other) noexcept {if (this != &other) {release();                // 释放当前资源ptr = other.ptr;          // 接管资源ref_count = other.ref_count;other.ptr = nullptr;other.ref_count = nullptr;}return *this;}// === 访问操作 ===T& operator*() const noexcept { return *ptr; }T* operator->() const noexcept { return ptr; }explicit operator bool() const noexcept { return ptr != nullptr; }// === 工具函数 ===size_t use_count() const noexcept {return ref_count ? ref_count->load() : 0;}T* get() const noexcept { return ptr; }// 重置指针(可接管新资源)void reset(T* new_ptr = nullptr) noexcept {release(); // 释放旧资源if (new_ptr) {ptr = new_ptr;ref_count = new std::atomic<size_t>(1); // 新计数器}}
};

可以看到共享式指针就要复杂得多。

    T* ptr = nullptr;                   // 指向动态资源的指针std::atomic<size_t>* ref_count = nullptr; // 原子引用计数器

这里涉及到了原子操作:这行代码定义了一个指向原子引用计数器的指针

    // 释放资源并更新引用计数void release() noexcept {if (!ref_count) return;// 原子减少计数,若归零则销毁资源if (--(*ref_count) == 0) {delete ptr;          // 释放对象delete ref_count;    // 释放计数器ptr = nullptr;ref_count = nullptr;}}

负责检查计数并释放资源。

// === 构造函数 ===SharedPtr() noexcept = default; // 默认构造(空指针)// 从原始指针构造(独占资源)explicit SharedPtr(T* raw_ptr): ptr(raw_ptr), ref_count(new std::atomic<size_t>(1)) {}// 拷贝构造(共享所有权)SharedPtr(const SharedPtr& other) noexcept: ptr(other.ptr), ref_count(other.ref_count) {if (ref_count) (*ref_count)++; // 引用计数增加}// 移动构造(转移所有权)SharedPtr(SharedPtr&& other) noexcept: ptr(other.ptr), ref_count(other.ref_count) {other.ptr = nullptr;other.ref_count = nullptr;}

构造函数,可以看到实现了拷贝和移动的构造函数。

    // === 析构函数 ===~SharedPtr() { release(); }

析构函数——直接调用我们写好的释放内存的函数即可。

剩下的内容和独占式的大差不差,不再赘述。

加入以下测试代码后:

// 测试类
class TestObject {
public:TestObject(int id) : id(id) {std::cout << "TestObject[" << id << "] created\n";}~TestObject() {std::cout << "TestObject[" << id << "] destroyed\n";}void log() const {std::cout << "Accessing object " << id << "\n";}
private:int id;
};// === 测试代码 ===
int main() {// 测试1:基础构造与析构std::cout << "=== Test 1: Basic Lifecycle ===\n";{SharedPtr<TestObject> p1(new TestObject(1));std::cout << "p1 use_count: " << p1.use_count() << "\n"; // 1} // 自动销毁// 测试2:拷贝语义std::cout << "\n=== Test 2: Copy Semantics ===\n";{SharedPtr<TestObject> p1(new TestObject(2));auto p2 = p1; // 拷贝构造p1->log();    // 访问对象std::cout << "p1/p2 use_count: "<< p1.use_count() << "/" << p2.use_count() << "\n"; // 2/2SharedPtr<TestObject> p3;p3 = p2; // 拷贝赋值std::cout << "p1/p2/p3 use_count: "<< p1.use_count() << "/" << p2.use_count()<< "/" << p3.use_count() << "\n"; // 3/3/3} // 所有指针离开作用域,对象销毁// 测试3:移动语义std::cout << "\n=== Test 3: Move Semantics ===\n";{SharedPtr<TestObject> p1(new TestObject(3));auto p2 = std::move(p1); // 移动构造std::cout << "p1 valid? " << (p1 ? "yes" : "no") << "\n"; // nostd::cout << "p2 use_count: " << p2.use_count() << "\n"; // 1SharedPtr<TestObject> p3;p3 = std::move(p2); // 移动赋值std::cout << "p2 valid? " << (p2 ? "yes" : "no") << "\n"; // nop3->log();}// 测试4:reset() 和线程安全std::cout << "\n=== Test 4: reset() & Thread Safety ===\n";{SharedPtr<TestObject> p1(new TestObject(4));p1.reset(new TestObject(5)); // 重置(先销毁4,再接管5)std::cout << "p1 use_count: " << p1.use_count() << "\n"; // 1}std::cout << "\nAll tests passed!\n";return 0;
}

单例模式

什么是单例模式?

单例模式(Singleton Pattern)是一种创建型设计模式,其核心目的是确保一个类在整个系统中仅有一个实例,并提供该实例的全局访问点,从而避免重复创建对象造成的资源浪费或状态不一致问题。

#include <iostream>
#include <mutex>
#include <atomic>class Singleton {
public:// 禁用拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 获取单例实例static Singleton* getInstance() {Singleton* tmp = instance.load(std::memory_order_acquire);if (tmp == nullptr) {std::lock_guard<std::mutex> lock(mutex);tmp = instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton();instance.store(tmp, std::memory_order_release);}}return tmp;}void log() { std::cout << "Singleton in use\n"; }private:// 私有构造函数Singleton() { std::cout << "Singleton created\n"; }~Singleton() = default;// 静态成员static std::atomic<Singleton*> instance;static std::mutex mutex;
};// 初始化静态成员
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mutex;

手撕单例最核心的部分就是去彻底禁用拷贝和赋值(类似独占式指针)。

    // 禁用拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;

这是实时获取单例实例的方法:

    // 获取单例实例static Singleton* getInstance() {Singleton* tmp = instance.load(std::memory_order_acquire);if (tmp == nullptr) {std::lock_guard<std::mutex> lock(mutex);tmp = instance.load(std::memory_order_relaxed);if (tmp == nullptr) {tmp = new Singleton();instance.store(tmp, std::memory_order_release);}}return tmp;}

这里有很多新的知识点啊:

instance是atomic<Singleton*>类的成员,这个类的含义是Singleton*类的原子变量。

然后instance分别调用了load,store两个函数:

std::lock_guard是一个自动锁管理的模板类,关于这句代码:

std::lock_guard<std::mutex> lock(mutex);

lock_guard规定了锁的管理方式,mutex是C++自带的互斥锁类,而lock是这个互斥锁类的名称,括号里的mutex则是一个已定义的 std::mutex 类型的具体对象,代表需要被管理的互斥锁。

整个流程就是基于双重检查锁定(DCLP)​​ 的线程安全单例模式,其执行流程如下:首先,通过 instance.load(std::memory_order_acquire) 原子读取当前单例指针 tmp,若 tmp 非空(表明实例已初始化),则直接返回实例以跳过锁开销;若 tmp 为空,则进入临界区,通过 std::lock_guard<std::mutex> lock(mutex) 锁定互斥量,确保同一时间仅一个线程执行初始化操作,并在加锁后再次调用 instance.load(std::memory_order_relaxed) 检查实例是否已被其他线程创建(避免重复初始化);若二次检查仍为空,则调用 new Singleton() 创建实例,并通过 instance.store(tmp, std::memory_order_release) 原子存储指针,其中 memory_order_release 保证对象构造完成后再更新指针,防止其他线程读到未初始化的内存;最终返回 tmp,后续线程通过首次无锁检查即可直接获取实例。

线程池

什么是线程池?

线程池在程序启动时预先创建一定数量的线程​(核心线程),并将它们置于空闲状态等待任务。当有任务提交时,线程池从池中分配一个空闲线程执行任务;任务完成后,线程不被销毁,而是返回池中等待新任务。这种机制通过维护“线程池+任务队列”实现线程的复用和任务的调度管理。

#include <iostream>
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>class ThreadPool {
public:explicit ThreadPool(size_t thread_count = std::thread::hardware_concurrency()): stop(false) {for (size_t i = 0; i < thread_count; ++i) {workers.emplace_back([this] {while (true) {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(); // 执行任务(在锁外执行以减少锁持有时间)}});}}~ThreadPool() {{   // 设置停止标志std::unique_lock<std::mutex> lock(queue_mutex);stop = true;}condition.notify_all(); // 唤醒所有线程for (auto& worker : workers) {if (worker.joinable()) worker.join(); // 等待线程结束}}// 提交任务接口(支持任意可调用对象及参数)template <typename F, typename... 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)(); }); // 封装为void()类型}condition.notify_one(); // 通知一个等待线程return res;}private:std::vector<std::thread> workers;     // 工作线程集合std::queue<std::function<void()>> tasks; // 任务队列std::mutex queue_mutex;               // 队列互斥锁std::condition_variable condition;    // 条件变量(任务通知)bool stop;                            // 终止标志
};

我先介绍一下这个condition_variable和future库:

private:std::vector<std::thread> workers;     // 工作线程集合std::queue<std::function<void()>> tasks; // 任务队列std::mutex queue_mutex;               // 队列互斥锁std::condition_variable condition;    // 条件变量(任务通知)bool stop;                            // 终止标志

这一系列的变量的用途:

    explicit ThreadPool(size_t thread_count = std::thread::hardware_concurrency()): stop(false) {for (size_t i = 0; i < thread_count; ++i) {workers.emplace_back([this] {while (true) {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(); // 执行任务(在锁外执行以减少锁持有时间)}});}}

这是线程池的构造函数,其中std::thread::hardware_concurrency() 是 C++ 标准库中用于获取硬件支持的并发线程数的静态成员函数。workers.emplace_back([this]{ ... }) 中[this]{ ... }是lambda表达式,后续的{...}是线程实际执行的逻辑。while(true)则是表明线程进入循环等待任务。

std::function<void()> task声明一个无参数、无返回值的可调用对象,用于存储从队列中取出的任务;{ std::unique_lock<std::mutex> lock(queue_mutex); ... }从临界区开始, std::unique_lock 锁定互斥锁 queue_mutex,保护共享资源(任务队列 tasks);condition.wait(lock, [this] { return stop || !tasks.empty(); })的作用就是让线程休眠,直到满足唤醒条件:线程池需终止或者任务队列非空;若线程池已停止且任务队列为空,则线程退出循环并销毁。

    ~ThreadPool() {{   // 设置停止标志std::unique_lock<std::mutex> lock(queue_mutex);stop = true;}condition.notify_all(); // 唤醒所有线程for (auto& worker : workers) {if (worker.joinable()) worker.join(); // 等待线程结束}}

这是线程池的析构函数,我们进入临界区把停止标志设置为true之后并唤醒所有线程之后检查线程是否可合并(即是否在运行中),并通过 join() 等待其自然退出。

为什么要判断线程是否可以合并?这里牵扯到的是线程的一些内容:

这里我补充一下关于所谓的临界区和进入临界区的概念:

在并发编程中,“进入临界区”是指一个线程成功获取了同步锁(如互斥锁、临界区对象等),开始执行受保护的共享资源访问代码的过程。

    // 提交任务接口(支持任意可调用对象及参数)template <typename F, typename... 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)(); }); // 封装为void()类型}condition.notify_one(); // 通知一个等待线程return res;}

这段代码实现了提交任务的接口enqueue,支持任意类型任务的提交,并返回 std::future 以便异步获取结果,同时确保线程安全的任务入队和线程唤醒。其中typename... Args 声明了一个名为 Args 的类型参数包,允许模板接受多个未知类型。接受任意可调用对象及其参数,通过std::bind和完美转发将任务与参数打包成一个无参函数,再封装进std::packaged_task以捕获返回值类型并创建关联的std::future;任务函数被安全地放入线程池任务队列后(此过程需加锁保护并检查线程池是否已停止),随即通过条件变量唤醒一个等待的工作线程执行任务,最终将用于异步获取任务执行结果的std::future对象返回给调用者。

我们加入以下测试代码:

// 测试函数
void printNumber(int num) {std::cout << "Thread " << std::this_thread::get_id()<< ": " << num << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时
}int main() {ThreadPool pool(4); // 创建4个工作线程// 提交20个任务std::vector<std::future<void>> results;for (int i = 0; i < 20; ++i) {results.emplace_back(pool.enqueue(printNumber, i));}// 等待所有任务完成for (auto&& result : results)result.wait();std::cout << "所有任务执行完毕!" << std::endl;return 0;
}

输出如下:

String

想要自己实现string类,要注意的内容包含这些方面:

内存管理

  • 动态分配​:字符串内容需要在堆上动态分配内存
  • 深拷贝​:拷贝构造/赋值时必须复制内容而非指针
  • 内存释放​:析构函数必须释放分配的内存
  • 容量管理​:实现类似vector的容量(capacity)概念以减少重新分配

基本功能实现

可以看到功能需求还是非常多的。

#include <cstring>  // 用于字符串操作函数class MyString {
private:char* m_data;   // 实际存储字符串数据的指针size_t m_size;  // 当前字符串长度(不含结尾'\0')size_t m_cap;   // 当前分配的容量(含结尾'\0')// 辅助函数:确保有足够的容量void ensure_capacity(size_t new_size) {if (new_size < m_cap) return;// 加倍策略扩容(避免频繁扩容)size_t new_cap = (new_size > m_cap * 2) ? new_size + 1 : m_cap * 2;// 分配新内存并复制内容char* new_data = new char[new_cap];if (m_data) {std::strncpy(new_data, m_data, m_size);delete[] m_data;  // 释放旧内存}m_data = new_data;m_cap = new_cap;}public:// 默认构造函数:创建空字符串MyString() : m_data(nullptr), m_size(0), m_cap(0) {ensure_capacity(1);  // 保证至少有1字节容量m_data[0] = '\0';}// C字符串构造函数MyString(const char* str) : m_data(nullptr), m_size(0), m_cap(0) {if (str) {m_size = std::strlen(str);ensure_capacity(m_size);std::strcpy(m_data, str);} else {ensure_capacity(1);m_data[0] = '\0';}}// 拷贝构造函数MyString(const MyString& other) : m_data(nullptr), m_size(0), m_cap(0) {*this = other;  // 复用赋值操作符}// 析构函数~MyString() {delete[] m_data;}// 拷贝赋值操作符MyString& operator=(const MyString& other) {if (this != &other) {delete[] m_data;m_size = other.m_size;ensure_capacity(m_size);std::strcpy(m_data, other.m_data);}return *this;}// 获取C风格字符串const char* c_str() const {return m_data ? m_data : "";}// 获取字符串长度size_t size() const {return m_size;}// 获取当前容量size_t capacity() const {return m_cap - 1;  // 不计数结尾的'\0'}// 下标访问(支持const和非const)char& operator[](size_t index) {return m_data[index];}const char& operator[](size_t index) const {return m_data[index];}// 字符串连接操作MyString operator+(const MyString& other) const {MyString result(*this);result += other;return result;}// 连接赋值操作MyString& operator+=(const MyString& other) {size_t new_size = m_size + other.m_size;ensure_capacity(new_size);std::strcat(m_data, other.m_data);m_size = new_size;return *this;}// 比较操作符bool operator==(const MyString& other) const {if (m_size != other.m_size) return false;return std::strcmp(m_data, other.m_data) == 0;}
};

先写到这,后续再更新详细介绍一下这段代码的逻辑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88009.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88009.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mybatis----留言板

基础项目&#xff1a;留言板 截止到目前为止&#xff0c;我们已经学习了 Spring&#xff08;只学习了DI&#xff09;、Spring MVC、SpringBoot、Mybatis 这些知识了&#xff0c;已经满足了做简单项目的基本要求了&#xff0c;所以接下来我们就从0到1实现表白墙项目。 需求分析…

Web-API-day3 DOM事件进阶

一、 事件流 1.事件冒泡 const fa document.querySelector(.father)const son document.querySelector(.son)document.addEventListener(click, function () {alert(我是爷爷)})fa.addEventListener(click, function () {alert(我是爸爸)})son.addEventListener(click, fun…

小波增强型KAN网络 + SHAP可解释性分析(Pytorch实现)

效果一览一、传统KAN网络的痛点与突破 1. 传统KAN的局限性 传统Kolmogorov-Arnold网络&#xff08;KAN&#xff09;虽在理论上有可靠的多变量函数逼近能力&#xff0c;但存在显著瓶颈&#xff1a; 计算效率低&#xff1a;训练速度慢于MLP&#xff0c;资源消耗大&#xff0c;尤其…

tomcat部署多个端口以及制定路径部署-vue3

vue3项目tomcat部署记录 使用hash路由 字符串拼接的图片地址可以使用import.meta.env.BASE_URL 默认8080 如果部署地址为8080/xc 则设置 vite.config.js中设置base为’/xc/’ outDir设置为xc 打包产物直接拖到webapps目录下 如果另开一个端口 如8081 设置根目录访问 conf/ser…

LeetCode三数之和-js题解

给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组。 示例 1&…

Flink SQLServer CDC 环境配置与验证

一、SQL Server 数据库核心配置 1. 启用 CDC 功能&#xff08;Change Data Capture&#xff09; SQL Server CDC 依赖数据库级别的 CDC 功能及表级别的捕获配置&#xff0c;需按以下步骤启用&#xff1a; 启用数据库 CDC -- 以管理员身份连接数据库 USE master; GO-- 检查数…

软考(软件设计师)存储管理—设备管理,磁盘调度

I/O软件的核心目标是管理硬件差异、提供统一接口、实现高效可靠的数据传输。 核心目标&#xff1a; 设备无关性&#xff1a; 应用程序无需关心具体硬件细节。错误处理&#xff1a; 处理硬件错误和传输异常。同步/异步传输&#xff1a; 支持阻塞&#xff08;等待完成&#xff09…

[C语言] C语言数学函数库概览

C语言数学函数库概览 文章目录 C语言数学函数库概览一、概述二、基本数学函数详解1. 平方根函数 sqrt(x)2. 幂函数 pow(x, y)3. 绝对值函数 fabs(x)4. 向上取整函数 ceil(x)5. 向下取整函数 floor(x) 三、三角函数与双曲函数详解1. 正弦函数 double sin(double x)2. 余弦函数 d…

【简单三步】Stable diffusion Webai本地部署无法加载模型并报openai/clip-vit-large-patch14错误的解决方法

问题描述 Stable diffusion Webai本地部署成功后&#xff0c;手动加载本地模型checkpoint时&#xff0c;始终无法加载进去&#xff0c;确定模型存放位置无误&#xff08;位于models\Stable-diffusion&#xff09;查看cmd窗口时&#xff0c;发现一个报错提示&#xff1a;Can’t …

Java 命令行参数详解:系统属性、JVM 选项与应用配置

Java 命令行参数详解&#xff1a;系统属性、JVM 选项与应用配置 在 Java 应用启动命令中&#xff0c;如&#xff1a; java -jar -Dserver.port8088 xdr-demo-1.0-SNAPSHOT-assembly.jar &-Dserver.port8088是一个 系统属性&#xff08;System Property&#xff09; 设置。…

【论文笔记】World Models for Autonomous Driving: An Initial Survey

原文链接&#xff1a;https://ieeexplore.ieee.org/abstract/document/10522953 1. 世界模型的发展 A. 世界模型的结构基础 世界模型包含4个关键组件&#xff0c;以模拟人类连贯的思考和决策过程。 a&#xff09;感知模块使用如变分自编码器&#xff08;VAE&#xff09;、掩…

Spring Cloud Config(微服务配置中心详解)

关键词&#xff1a;Spring Cloud Config、配置中心、远程仓库、动态刷新、加密解密 ✅ 摘要 在微服务架构中&#xff0c;随着服务数量的增加&#xff0c;统一管理各服务的配置信息变得尤为重要。传统的本地配置文件方式难以满足多环境、多实例、集中化的需求。 Spring Cloud …

【Note】《深入理解Linux内核》 第二十章:深入理解 Linux 程序执行机制

《深入理解Linux内核》 第二十章&#xff1a;深入理解 Linux 程序执行机制&#xff08;Program Execution&#xff09;关键词&#xff1a;exec 系列系统调用、可执行文件格式&#xff08;ELF&#xff09;、用户地址空间、内存映射、动态链接、栈初始化、入口点、共享库、内核态…

服务器如何配置防火墙规则以阻止恶意流量和DDoS攻击?

防火墙是保护服务器免受恶意流量和 DDoS 攻击的第一道防线。通过合理配置防火墙规则&#xff0c;可以有效阻止恶意访问、限制不必要的流量&#xff0c;并减少攻击对服务器的影响。以下是配置防火墙规则的全面指南&#xff0c;包括基础规则设置、防御 DDoS 攻击的高级策略和最佳…

持续性投入是成就自我价值的关键一环

概述 时间&#xff0c;的唯一公平之处就是给你我的长度是相同的&#xff0c;这也是它唯一公平&#xff0c;也是不公平的地方。 所谓的公平&#xff0c;就是不患寡而患不均中所说的平均。 所谓的不公平就是&#xff0c;相同时间内我们彼此对应的标价不同&#xff0c;延伸到后…

使用allegro在BoardGeometry的Silkscreen_Top层画出图案

目录 1. 图形及图形放置显示2. 绘制 1. 图形及图形放置显示 绘制完成图案&#xff1a; 导出后图案&#xff1a; 2. 绘制 图层选中&#xff1b; 画圆型&#xff1b; 半径3.5mm&#xff0c;原点生成&#xff1b; 在图案中挖空&#xff1b; 用指令走线&#xff1a; …

Kotlin 协程:Channel 与 Flow 深度对比及 Channel 使用指南

前言 在 Kotlin 协程的异步编程世界里&#xff0c;Channel 和 Flow 是处理数据流的重要工具&#xff0c;它们有着不同的设计理念与适用场景。本文将对比二者功能与应用场景&#xff0c;详细讲解 Channel 的使用步骤及注意事项 。 一、Channel 与 Flow 的特性对比 Channel 是协程…

MYsql主从复制部署

MySQL 主从复制是将主数据库的变更自动同步到从数据库的过程&#xff0c;常用语读写分离、高可用性和数据备份。 1.环境准备 确保主从服务器已安装相同版本的 MySQL&#xff0c;并能通过网络互相访问。 # 检查 MySQL 版本 mysql -V 2.配置主服务器 &#xff08;1&#xff0…

安灯呼叫看板如何实现汽车生产异常秒级响应

在汽车零部件工厂的静置车间&#xff0c;传统生产管理依赖人工巡检与纸质记录&#xff0c;存在效率低、信息滞后、异常响应慢等问题。某汽车厂曾因物料静置时间未及时监控&#xff0c;导致批次混料&#xff0c;损失超10万元。而安灯呼叫看板系统的引入&#xff0c;通过实时状态…

构造函数注入在spring boot 中怎么使用详解

我们来详细讲解一下在 Spring Boot 中如何使用构造函数注入&#xff0c;并通过一个完整的、可运行的例子来演示。 构造函数注入是 Spring 官方最推荐的依赖注入方式&#xff0c;因为它能保证对象的不可变性和依赖的完整性。 核心理念 在 Spring Boot 中使用构造函数注入非常简单…