跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

从零手写 C++ String 类及高频易错点复盘

从零实现 C++ String 类涉及内存管理、深拷贝机制及运算符重载等核心知识点。内容涵盖默认构造、扩容策略(reserve)、插入删除逻辑以及赋值运算符的拷贝交换法。重点指出浅拷贝导致的野指针问题,并展示流输入输出实现细节。适合希望理解 STL 底层原理及提升 C++ 内存控制能力的开发者阅读。

奇形怪状发布于 2026/3/22更新于 2026/5/34 浏览
从零手写 C++ String 类及高频易错点复盘

一、整体结构

class string {
public:
private:
    char* _str;
    int _size;
    int _capacity;
};

1. char _str*

这是一个指向字符数组的指针,用来存储字符串的实际字符数据。在 C++ 标准库的 std::string 中,它指向一块动态分配的内存,里面存放着以 ' ' 结尾的字符序列。模拟实现时,需要自己管理这块内存的分配、扩容和释放。

2. int _size

记录当前字符串中有效字符的个数(不包括末尾的 ' ')。

3. int _capacity

记录当前已分配内存能容纳的最大字符数(不包括末尾的 ' ')。它代表了在不需要重新分配内存的情况下,字符串最多能存储的字符数量。当 _size 即将超过 _capacity 时,就需要触发扩容(比如申请一块更大的内存,将旧数据拷贝过去,再释放旧内存)。通常扩容会按一定倍数(如 1.5 倍或 2 倍)进行,以减少频繁扩容带来的性能开销。

二、构造与析构函数

2.1 默认构造

1. 无参构造

写构造函数时得注意,char* _str 不能初始化为 nullptr。需要开辟一字节的空间并默认初始化为 '\0' 也就是空串,因为字符串默认以 '\0' 结尾。否则就会导致字符串无法以 '\0' 结尾,打印的时候字符串尾部就会出现乱码。

string() { _str = new char[1]{'\0'}; _size = _capacity = 0; }

为了方便后续管理,这里开辟的一字节用来存储 '\0' 的空间不计入总的空间大小。

2. 字符串构造 string 类型

string(const char* str) { int len = strlen(str); _str = new char[len + 1]; strcpy(_str, str); _size = _capacity = len; }

这里需要注意的是,传进来的 str 字符串默认是带有 '\0' 的。strlen 会计算字符串的长度并记录在 len 变量中(不包含 '\0'),strcpy 会将 str(包含 '\0')拷贝在 _str 这个空间中,所以在开辟空间大小的时候开辟 是为了存储 。

len+1
'\0'

2.2 拷贝构造

string(const string& ch) { _str = new char[ch._capacity + 1]; strcpy(_str, ch._str); _size = ch._size; _capacity = ch._capacity; }

编写拷贝构造接口需要特别注意的是:C++ 语法明确禁止拷贝构造传值传参,否则会导致无限递归。这是因为使用传值传参编译器会默认调用拷贝构造,但是如果拷贝构造接口本身就是传值传参的话就会一直重复递归调用自己最终导致栈溢出。

要解决这个问题,拷贝构造函数的参数必须采用传常量引用(const 类名&),也可以传普通引用(类名&)。

2.3 析构函数

~string() { delete[] _str; _str = NULL; _size = _capacity = 0; }

三、功能接口

3.1 reserve

reserve 接口的唯一作用是修改 string 的 _capacity,它会提前为字符串分配一块足够大的内存空间,让后续添加字符时无需频繁触发自动扩容。reserve 接口通常有以下特点:

  • 它只改变容量(_capacity),不改变有效长度(_size):调用 reserve 后,字符串的有效字符个数不变,也不会填充任何额外的字符,只是预留了内存空间。
  • 它只负责扩容,不负责缩容:如果传入的参数小于当前的 _capacity,std::string 的 reserve 通常不会做任何操作。
  • 预留的内存空间包含末尾 '\0' 的位置:比如 reserve(10),意味着 _capacity 会被设置为 10,实际分配的内存大小是 11(额外存放 '\0')。
void reserve(int n) { if (n > _capacity) { char* temp = new char[n+1]; strcpy(temp,_str); delete[] _str; _str = temp; _capacity = n; } }

注意:如果我们提前实现了 reserve 可不可以在构造函数中调用 reserve 来开辟空间呢?

答案是,坚决不可以。因为调用 reserve 时其中的成员变量 _str,_size,_capacity 并没有第一时间初始化,其内容可能是随机值。执行 strcpy(temp,_str); 时 _str 可能指向的是一块非法的空间导致非法访问,或者有可能 _capacity 的随机值比 n 大导致开辟空间失败。

3.2 c_str

这个接口的核心作用是获取一个指向 string 内部以 ' ' 结尾的 C 风格字符串的常量指针,用来兼容那些只支持 C 语言风格字符串的函数接口。

  • 标准规定返回 const char*(常量字符指针),这意味着你只能读取该字符串的内容,不能通过这个指针修改字符串(否则会触发未定义行为)。
  • 返回的指针指向 string 内部存储的字符数组首地址,且该数组必然以 ' ' 结尾。
  • 返回的指针依赖于 string 对象的生命周期,且当 string 对象发生修改(如添加字符、扩容)或销毁后,这个指针会失效(变成野指针),不能再使用。
const char* c_str() const { return _str; }

3.3 PushBack

PushBack 接口的作用是在原有 string 的结尾添加一个字符,并将其的长度增加 1。

void string::PushBack(const char ch) { if (_size == _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } _str[_size++] = ch; _str[_size] = 0; }

这里需要注意的是当空间不够扩容时不能直接按照 2 倍或者 1.5 倍扩容,首先需要判断 _capacity 是否为 0,不然就会导致新空间的大小一直为 0 导致错误。

3.4 Append

append 这个接口的核心作用是在当前字符串的末尾追加字符或字符串(支持多种格式)。

  • 核心行为:在字符串有效字符的末尾追加内容,追加后会更新 _size,并保证末尾有 ' '。
  • 灵活性:支持多种追加格式(单个字符、C 风格字符串、string 对象、字符串的子串等)。
  • 内存处理:追加前会检查 _size + 追加内容长度 > _capacity,如果满足则触发扩容(和 push_back 逻辑一致),避免内存越界。

在代码中我们只模拟实现一种追加格式就是在原有 string 的末尾追加一段 C 风格字符串,代码如下:

void string::Append(const char* str) { int len = strlen(str); if (_size + len > _capacity) { reserve(_size + len > 2*_capacity ? _size + len : 2 * _capacity); } strcpy(_str+_size,str); _size += len; }

在这里同样需要注意的是,如果追加的字符串加上已有的字符串的长度大于已经开辟的空间大小 _capacity 就会触发扩容。但是我们不能一上来就开始 2 倍扩容,可能 _size+len 的大小比 2*_capacity 更大,这时我们就需要判断 _size+len 与 2*_capacity 的大小关系。如果 _size+len>2*_capacity 就按照 _size+len 的大小扩容,否则就按照 2 倍扩容。

3.5 Insert

insert 这个接口的核心作用是在字符串的指定位置插入字符或字符串,相比 append(仅能在末尾追加),insert 提供了更灵活的插入位置选择。

  • 在字符串的 pos 索引位置(从 0 开始)插入内容,插入后原 pos 及之后的字符会向后移动,同时更新 _size,保证末尾有 ' '。
  • 插入位置 pos 的有效范围是 [0, _size](pos = _size 等价于在末尾追加,和 append 效果一致),超出范围会触发越界错误。
  • 插入前会检查 _size + 插入内容长度 > _capacity,满足则触发扩容,避免内存越界。
  • 标准库中 insert 返回 string&(当前对象的引用),支持链式调用。
  • 因为插入会导致后续字符向后移动,频繁在字符串头部或中间插入数据,性能开销较大(时间复杂度 O(n))。

在我们的代码中主要实现两个版本,在指定位置插入字符或者在指定位置插入字符串:

// 在指定位置插入一个字符
void string::Insert(int pos, const char ch) { assert(pos <= _size && pos>=0); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } int cur = _size; while (cur >= pos) { _str[cur + 1] = _str[cur]; cur--; } _str[pos] = ch; _size++; }

// 在指定位置插入字符串
void string::Insert(int pos, const char* str) { assert(pos <= _size && pos >= 0); int len = strlen(str); if (_size+len > _capacity) { reserve(_size + len > 2*_capacity ? _size + len : 2 * _capacity); } int cur = _size; while (cur >= pos) { _str[cur + len] = _str[cur]; cur--; } // 这里不能 strcpy 因为 strcpy 会将 \0 也拷贝进去 strncpy(_str+pos,str,len); _size+=len; }

3.6 erase

string 的 erase 接口是用来删除字符串中指定位置或指定范围的字符的,它有三种重载的形式:

删除指定位置的单个字符

// pos:要删除的字符的下标(从 0 开始)
string& erase(size_t pos = 0);

从指定位置开始,删除指定长度的字符

// pos:起始删除下标;len:要删除的字符个数
string& erase(size_t pos, size_t len);

删除迭代器指向的字符 / 迭代器范围的字符

// 子形式 3.1:删除迭代器 it 指向的单个字符
iterator erase(iterator it);
// 子形式 3.2:删除 [first, last) 区间内的所有字符(左闭右开,不包含 last 指向的字符)
iterator erase(iterator first, iterator last);

库中的 erase 返回 string& 主要是支持链式调用,在我们的代码中实现了其中的一种情况即从指定位置删除指定长度的字符:

void string::erase(size_t pos, size_t len) { assert(pos>=0&&pos<_size); if (len >= _size - pos) { _str[pos] = 0; _size -= (_size - pos); } else { while (pos + len <= _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; } }

3.7 substr

substr 接口用于从原字符串中截取一段子字符串并返回,它不会修改原字符串本身。注意关键字 const,这表明该成员函数不会修改调用它的 std::string 对象。

string string::substr(size_t pos, size_t len) const { assert(pos>=0&&pos<_size); if (len > _size - pos) { len = _size - pos; } string temp; for (int i = pos; i < pos + len; i++) { temp += _str[i]; } return temp; }

需要注意的是,substr 接口必须使用值返回。代码执行到 return temp; 时会调用拷贝构造,当我们不显式地定义时编译器自动会生成一个拷贝构造。但是这里有个大坑,编译器默认生成的拷贝构造是浅拷贝(两个指针会指向同一块空间,运行程序的时候会导致同一空间被析构两次导致程序崩溃!)。所以在实现 substr 接口之前必须显式地定义拷贝构造函数。

下面我们可以将显式实现的拷贝构造函数禁用,使用编译器默认生成的拷贝构造进行浅拷贝复现一下这个 bug:

此时我们运行程序会发现提供调用 substr 拿到的字串打印出来是乱码,结果调试我们发现编译器自动生成的拷贝构造并没有为对象开辟新空间,而是将仅仅将就旧空间的地址拷贝到 ret 管理的空间指针中。substr 调用结束里面的临时对象就被析构回收了,此时外部 ret 拿到的就是一段非法空间,打印出来的结果自然是乱码。

所以这就要求我们显式地进行深拷贝,自己定义拷贝构造接口。在我们地拷贝构造中,并不是单纯的将旧空间地址这一数值拷贝给新对象。而是重新开辟一段空间,首先将要拷贝的数据放到新空间中,然后将新空间的地址交给新对象进行管理,从而完成深拷贝。

下面当我们自己显式进行深拷贝后,代码的运行结果正常。

3.8 Find

find 接口的核心作用是在字符串中查找指定的字符或子串,返回其第一次出现的起始下标,如果查找失败则返回 std::string::npos(这是 size_t 类型的最大值,表示无效下标,其可用于判断是否查找成功)。

size_t string::find(char ch, size_t pos ) { assert(pos<_size); for (int i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; }
size_t string::find(const char* str, size_t pos) { assert(pos < _size); char* ret=strstr(_str+pos,str); if (ret != nullptr) { return ret - _str; } return npos; }

在代码中我们实现了 Find 接口的两种不同形式,在指定位置之后查找指定字符或字符串第一次出现的位置并返回其下标。其中查找字符串我们使用到了 strstr,该函数的核心作用是**在一个 C 风格字符串(以 ' ' 结尾的字符数组)中查找另一个 C 风格子串的首次出现位置。**但是需要注意的是,strstr 的返回值是一个指向首次出现的起始位置的指针,查找失败则会返回 NULL,因为我们要求 Find 接口的返回值是被查找元素第一次出现位置的下标,所以我们还需要计算相对位置。

四、运算符重载

4.1 =

1. 传统写法

string& operator=(const string& str) { if(*this==str) { return; } delete[] _str; reserve(str._capacity); strcpy(_str,str._str); _size = str._size; return *this; }

2. 现代写法

void Swap(string& str) { std::swap(_str,str._str); std::swap(_size, str._size); std::swap(_capacity, str._capacity); }
string& operator=(string str) { Swap(str); return *this; }

在赋值运算符的传统写法中需要我们手动开辟空间并释放旧空间,然后将数据拷贝到新空间中,这种手动管理内存的代码不仅复杂而且还可能存在隐患。而现代写法的赋值运算符重载则要求传值传参,在此过程中拷贝构造函数就自动完成了临时对象 str 的拷贝构造,我们只需要把临时对象的资源交换给当前对象。因为 str 是临时的,而且经过交换之后它所管理的空间和数据都是旧空间和旧数据,函数调用结束 str 自动被析构,完成资源的回收。

特性传统写法现代写法(拷贝并交换)
异常安全❌ 存在风险✅ 绝对安全
代码复杂度较高,需手动管理内存极低,依赖拷贝构造和交换
自赋值处理需要额外判断 if (this != &str)✅ 天然支持,无需额外判断
性能正常与拷贝构造性能一致,无额外开销

注意:

string s2=s1;// 这里调用的是拷贝构造
string s2;
s2=s1; // 这里调用的才是赋值运算符重载

4.2 <、<=、>、>=、==、!=

bool operator<(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) < 0; }
bool operator<=(const string& s1, const string& s2) { return s1 < s2 || s1 == s2; }
bool operator>(const string& s1, const string& s2) { return !(s1 <= s2); }
bool operator>=(const string& s1, const string& s2) { return !(s1 < s2); }
bool operator==(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) == 0; }
bool operator!=(const string& s1, const string& s2) { return !(s1 == s2); }

在实现大小比较的运算符重载时我们只需要通过 strcmp 实现 < 和 ==,其余运算符都通过复用这两个基础实现来完成,减少了重复代码和出错概率。

还有一点需要注意的是,几个函数的声明不可以放在类内。因为类内的函数参数列表有隐含的 this 指针。

4.3 +=

void operator+=(const char* str) { Append(str); }
void operator+=(const char ch) { PushBack(ch); }

4.4 <<、>>

void clear() { _str[0] = 0; _size = 0; }
std::ostream& operator<<(std::ostream& out, const string& s) { for (auto ch : s) { out << ch; } return out; }
std::istream& operator>>(std::istream& in, string& s) { s.clear(); const int N = 256; char buff[N]; int i = 0; char ch; //in >> ch; ch = in.get(); while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == N - 1) { buff[i] = '\0'; s += buff; i = 0; } //in >> ch; ch = in.get(); } if (i > 0) { buff[i] = '\0'; s += buff; } return in; }

在流输入和流输出的代码实现中输出比较简单,就是将 string 中的各个字符依次输出到 ostream 对象中,这是最直接的实现方式,时间复杂度为 O(n),n 为字符串长度。

在流输入的代码实现比较复杂,代码流程如下:

  • 清空旧数据:先调用 s.clear(),避免输入内容追加到旧字符串之后。
  • 缓冲区设计:使用一个固定大小(256 字节)的字符数组 buffer 作为临时缓冲区,避免频繁的内存分配。
  • 读取到分隔符为止:用 in.get() 逐个读取字符,直到遇到空格(' ')或换行符('\n')才停止,这让它能读取一整段连续的非空白字符。
  • 缓冲区满时追加:当缓冲区满时,在末尾加上 '\0' 转成 C 风格字符串,追加到目标字符串 s 中,然后重置缓冲区索引。
  • 处理剩余字符:循环结束后,若缓冲区还有未处理的字符,同样追加到 s 中。
  • 返回输入流:返回 in 以支持链式调用。

需要注意的是,除了 clear 函数,>> 和 << 运算符重载的函数声明不可以放在类内。因为类内的函数参数列表有隐含的 this 指针。

五、完整代码

#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<string.h>
#include<assert.h>
#include<iostream>

class string {
public:
    typedef char* iterator;
    iterator begin() const { return _str; }
    iterator end() const { return _str+_size; }
    void clear() { _str[0] = 0; _size = 0; }
    void reserve(int n) { if (n > _capacity) { char* temp = new char[n+1]; strcpy(temp,_str); delete[] _str; _str = temp; _capacity = n; } }
    // 构造
    string() { _str = new char[1]{'\0'}; _size = _capacity = 0; }
    // 拷贝构造 (C++ 语法明确禁止拷贝构造传值传参,否则会导致无限递归)
    // string(const string ch)
    // 但是可以传引用传参
    // string(const string& ch)
    string(const char* str) { int len = strlen(str); _str = new char[len + 1]; strcpy(_str, str); _size=_capacity=len; }
    string(const string& ch) { // reserve(len); 构造函数中不能用 reserve,因为该 string 还没完成初始化 strcpy 会访问到 // 随机值或者空指针
        _str = new char[ch._capacity + 1];
        strcpy(_str, ch._str);
        _size = ch._size;
        _capacity = ch._capacity;
    }
    const char* c_str() const { return _str; }
    // 赋值运算符重载
    /*string& operator=(const string& str) { if (*this == str) { return; } delete[] _str; reserve(str._capacity); strcpy(_str,str._str); _size = str._size; return *this; }*/
    void Swap(string& str) { std::swap(_str,str._str); std::swap(_size, str._size); std::swap(_capacity, str._capacity); }
    string& operator=(string str) { Swap(str); return *this; }
    // 尾插一个字符
    void PushBack(const char ch);
    // 追加一个字符串
    void Append(const char* str);
    // 指定位置插入一个字符
    void Insert(int pos, const char ch);
    // 指定位置插入一个字符串
    void Insert(int pos, const char* str);
    // 删除指定位置之后的 len 个字符 (包括 pos)
    void erase(size_t pos, size_t len);
    // 将指定位置之后长度为 len 的子串返回 (包括 pos)
    string substr(size_t pos, size_t len) const;
    // 从 pos 位置开始寻找字符 ch 或者字符串 str 第一次出现的位置并返回下标
    size_t find(char ch, size_t pos = 0);
    size_t find(const char* str, size_t pos = 0);
    void operator+=(const char* str) { Append(str); }
    void operator+=(const char ch) { PushBack(ch); }
    char operator[](int pos) { assert(pos >= 0&&pos<_size); return _str[pos]; }
    int size() { return _size; }
    int capacity() { return _capacity; }
    ~string() { delete[] _str; _str = NULL; _size = _capacity = 0; }
private:
    char* _str;
    int _size;
    int _capacity;
    static const size_t npos;
};

bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
std::ostream& operator<<(std::ostream& out, const string& s);
std::istream& operator>>(std::istream& in, string& s);
#include"string.h"
// npos 为无符号整形赋值为 -1 会赋值成为整形最大值
const size_t string::npos = -1;

void string::PushBack(const char ch) { if (_size == _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } _str[_size++] = ch; _str[_size] = 0; }

void string::Append(const char* str) { int len = strlen(str); if (_size + len > _capacity) { reserve(_size + len > 2*_capacity ? _size + len : 2 * _capacity); } strcpy(_str+_size,str); _size += len; }

void string::Insert(int pos, const char ch) { assert(pos <= _size && pos>=0); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } int cur = _size; while (cur >= pos) { _str[cur + 1] = _str[cur]; cur--; } _str[pos] = ch; _size++; }

void string::Insert(int pos, const char* str) { assert(pos <= _size && pos >= 0); int len = strlen(str); if (_size+len > _capacity) { reserve(_size + len > 2*_capacity ? _size + len : 2 * _capacity); } int cur = _size; while (cur >= pos) { _str[cur + len] = _str[cur]; cur--; } // 这里不能 strcpy 因为 strcpy 会将 \0 也拷贝进去 strncpy(_str+pos,str,len); _size+=len; }

void string::erase(size_t pos, size_t len) { assert(pos>=0&&pos<_size); if (len >= _size - pos) { _str[pos] = 0; _size -= (_size - pos); } else { while (pos + len <= _size) { _str[pos] = _str[pos + len]; pos++; } _size -= len; } }

// 值返回调用拷贝构造,默认浅拷贝,需要实现深拷贝
string string::substr(size_t pos, size_t len) const { assert(pos>=0&&pos<_size); if (len > _size - pos) { len = _size - pos; } string temp; for (int i = pos; i < pos + len; i++) { temp += _str[i]; } return temp; }

size_t string::find(char ch, size_t pos ) { assert(pos<_size); for (int i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; }

size_t string::find(const char* str, size_t pos) { assert(pos < _size); char* ret=strstr(_str+pos,str); if (ret != nullptr) { return ret - _str; } return npos; }

bool operator<(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) < 0; }
bool operator<=(const string& s1, const string& s2) { return s1 < s2 || s1 == s2; }
bool operator>(const string& s1, const string& s2) { return !(s1 <= s2); }
bool operator>=(const string& s1, const string& s2) { return !(s1 < s2); }
bool operator==(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) == 0; }
bool operator!=(const string& s1, const string& s2) { return !(s1 == s2); }

std::ostream& operator<<(std::ostream& out, const string& s) { for (auto ch : s) { out << ch; } return out; }

std::istream& operator>>(std::istream& in, string& s) { s.clear(); const int N = 256; char buff[N]; int i = 0; char ch; //in >> ch; ch = in.get(); while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == N - 1) { buff[i] = '\0'; s += buff; i = 0; } //in >> ch; ch = in.get(); } if (i > 0) { buff[i] = '\0'; s += buff; } return in; }

目录

  1. 一、整体结构
  2. 二、构造与析构函数
  3. 2.1 默认构造
  4. 2.2 拷贝构造
  5. 2.3 析构函数
  6. 三、功能接口
  7. 3.1 reserve
  8. 3.2 c_str
  9. 3.3 PushBack
  10. 3.4 Append
  11. 3.5 Insert
  12. 3.6 erase
  13. 3.7 substr
  14. 3.8 Find
  15. 四、运算符重载
  16. 4.1 =
  17. 4.2 <、<=、>、>=、==、!=
  18. 4.3 +=
  19. 4.4 <<、>>
  20. 五、完整代码
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 小米智能家居 Miloco 分离式部署指南
  • Windows 系统部署龙虾 AI(OpenClaw)指南
  • 深入 Spring Bean 作用域、生命周期与自动装配源码解析
  • C++ 函数重载详解:调试技巧与性能优化实战
  • IBM Watson Conversation API 实战:机场客服场景与上下文管理
  • Linux 下 OpenClaw 快速安装、初始化及 Web UI 配置指南
  • Coze 工作流实战:将文章链接转为思维导图
  • 在昇腾 NPU 上部署与测评 CodeLlama-7b-Python
  • 3 分钟学会给 Cursor 配置代理
  • Java Spring Web MVC 基础与核心注解
  • 视觉 - 骨架双模态框架用于帕金森病步态的泛化评估
  • Linux 网络基础入门:协议栈与传输流程
  • Qt Creator 配置 GitHub Copilot AI 编程插件指南
  • Stable Diffusion WebUI Windows 部署流程与常见报错解决方案
  • 命令行启动 MongoDB 服务器报错:YAML 配置文件解析错误及解决
  • 基于 K-means 和决策树的餐饮企业客户分析实战
  • 小鹏 VLA 2.0 自动驾驶与机器人技术突破解析
  • OpenClaw Docker 部署教程:集成飞书钉钉 QQ 机器人
  • Dify Web 前端二次开发:隐藏探索功能与替换 Logo
  • 无人机数据采集系统构建:C 语言工程师的 7 个核心步骤

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online