C++ std::string 类常用接口与模拟实现
C++ std::string 类涵盖常用接口、底层结构及模拟实现。内容涉及 auto 关键字与范围 for 循环的应用,string 对象的构造、容量、访问及修改操作,VS 与 g++ 下结构差异,浅拷贝与深拷贝机制,以及写时拷贝概念。结合 OJ 题目展示实际应用,强调内存管理的重要性。

C++ std::string 类涵盖常用接口、底层结构及模拟实现。内容涉及 auto 关键字与范围 for 循环的应用,string 对象的构造、容量、访问及修改操作,VS 与 g++ 下结构差异,浅拷贝与深拷贝机制,以及写时拷贝概念。结合 OJ 题目展示实际应用,强调内存管理的重要性。

C 语言中,字符串是以'\0'结尾的一些字符的集合。为了操作方便,C 标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
在 OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。
在使用 string 类时,必须包含 #include <string> 头文件以及 using namespace std;。
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量。后来这个不重要了。C++11 中,标准委员会变废为宝赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加&。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用。auto 不能直接用来声明数组。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int func1() { return 10; }
// 不能做参数
void func2(auto a) {}
// 可以做返回值,但是建议谨慎使用
auto func3() { return 3; }
int main() {
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:error C3531: 'e': 类型包含'auto'的符号必须具有初始值设定项
// auto e;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
// 编译报错:error C3538: 在声明符列表中,'auto'必须始终推导为同一类型
// auto aa = 1, bb = 2;
// 编译报错:error C3318: 'auto []': 数组不能具有其中包含'auto'的元素类型
// auto array[] = { 4, 5, 6 };
return 0;
}
#include<iostream>
#include <string>
#include <map>
using namespace std;
int main() {
std::map<std::string, std::string> dict = {
{ "apple", "苹果" },
{ "orange", "橙子" },
{"pear","梨"}
};
// auto 的用武之地
//std::map<std::string, std::string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end()) {
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号' :'分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。本质还是迭代器。范围 for 可以作用到数组和容器对象上进行遍历。范围 for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include<iostream>
#include <string>
#include <map>
using namespace std;
int main() {
int array[] = { 1, 2, 3, 4, 5 };
// C++98 的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
cout << array[i] << endl;
}
// C++11 的遍历
for (auto& e : array) e *= 2;
for (auto e : array) cout << e << " " << endl;
string str("hello world");
for (auto ch : str) {
cout << ch << " ";
}
cout << endl;
return 0;
}
#include <iostream>
#include <string>
using namespace std;
void test1() {
string s1;//默认构造 为空字符串
string s2("hello,world");//c-str 构造 string
string s3(3, 'a');//string 中有 n 个 char
string s4 = s2;//string s4(s2) 本质是拷贝构造
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
int main() {
test1();
return 0;
}
| 函数名称 | 功能说明 |
|---|---|
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty(重点) | 检测字符串是否为空串,是返回 true,否则返回 false |
clear(重点) | 清空有效字符 |
reserve(重点) | 为字符串预留空间 |
resize(重点) | 将有效字符的个数改成 n 个,多出的空间用字符 c 填充 |
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = "hello,world!";
cout << s1.size() << endl;
cout << s1.length() << endl;
cout << s1.empty() << endl;
cout << "s1:" << " " << s1 << endl;
cout << "clear 前的实际空间个数" << endl;
cout << s1.capacity() << endl;
cout << "clear" << endl;
s1.clear();//不会回收空间 只清理内容
cout << "clear 后的空间个数" << endl;
cout << s1.capacity() << endl;
cout << s1.empty() << endl;
cout << "reserve 后的实际空间个数" << endl;
s1.reserve(20);
cout << s1.capacity() << endl;
cout << s1.size() << endl;
cout << "resize" << endl;
s1.resize(10, 'a');
cout << s1 << endl;
return 0;
}
注意: size() 与 length() 方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用 size()。早期编写 string 时实现的 length 后面 stl 参考 string 模板类的实现时,stl 中的其他容器用的都是 size(),为了统一 string 也补充了 size()。 clear() 只是将 string 中有效字符清空,不改变底层空间大小。 resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到 n 个,不同的是当字符个数增多时:resize(n) 用 0 来填充多出的元素空间,resize(size_t n, char c) 用字符 c 来填充多出的元素空间。注意:resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。不同的编译器改变因 resize 导致的扩容方式可能不同。 reserve(size_t res_arg=0):为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于 string 的底层空间总大小时,reserver 不会改变容量大小。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
s += "hello,world";
cout << s << endl;
//本质还是迭代器
cout << "范围 for 遍历 s:" << endl;
for (auto& e : s) {
cout << e << " ";
}
cout << endl << endl << endl;
cout << "正向迭代器遍历 s:" << endl;
for (auto i = s.begin(); i != s.end(); i++) {
cout << *i << " ";
}
cout << endl;
cout << endl;
cout << endl;
cout << "逆向迭代器遍历 s" << endl;
for (auto i = s.rbegin(); i != s.rend(); i++) {
cout << *i << " ";
}
cout << endl;
return 0;
}
| 函数名称 | 功能说明 |
|---|---|
push_back | 在字符串末尾插入字符 c。例如 string s = "abc"; s.push_back('d');,执行后 s 为 "abcd" |
append | 在字符串后追加一个字符串。比如 string s = "hello"; s.append(" world");,结果 s 是 "hello world" |
operator+=(重点) | 在字符串后追加字符串 str,用法和 append 类似但更简洁,如 string s = "I "; s += "love C++";,s 变为 "I love C++" |
c_str(重点) | 返回 C 格式字符串(const char* 类型),可用于兼容 C 语言接口,比如 printf("%s", s.c_str());(s 是 string 对象) |
find + npos(重点) | 从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置(若找不到返回 string::npos)。例如 string s = "abcabc"; size_t pos = s.find('b', 2);,从索引 2 开始找 'b',会返回索引 3 |
rfind | 从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置。比如 string s = "abcabc"; size_t pos = s.rfind('b', 4);,从索引 4 往前找 'b',会返回索引 3 |
substr | 在字符串中从 pos 位置开始,截取 n 个字符并返回(若不指定 n,则截取到字符串末尾)。例如 string s = "abcdefg"; string sub = s.substr(2, 3);,sub 为 "cde" |
push_back():
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1;
//push_back 在字符串后尾插字符 c
s1.push_back('c');
char a = 'd';
s1.push_back(a);
/*s1.push_back(" const char* c_str");*/ //不能尾插字符串
cout << s1 << endl;
return 0;
}
find+npos:
find: npos: string 找不到对象时 返回 npos
#include <iostream>
#include <string>
using namespace std;
int main() {
//find 返回的是查找对象在 string 中的第一个字符的下标
//查找 string 类型的字符串
string s = "hello,world";
string s2 = "llo";
cout << s.find(s2) << endl;
//查找 c_string 类型的字符串
cout << s.find("wor") << endl;
//从 pos 位置查找 c_str 类型的字符串 中的几个
cout << s.find("wor", 5, 2) << endl;//wo
//从 pos 位置找 char
cout << s.find('l') << endl;//查找的是第一个 'l' 找到就停止了
cout << s.find('l', 5) << endl;//查找的是最后一个 'l'
cout << s.find("www") << endl;// 找不到即返回 npos
return 0;
}
rfind:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "hello,world";
cout << s.rfind('l') << endl;// 9
return 0;
}
substr:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "hello,world";
cout << s.substr(2, 3) << endl;//llo
return 0;
}
注意: 在 string 尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下 string 类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。对 string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好。减少后续不断添加字符导致的空间开辟浪费的性能。
输入输出的运算符重载 在本文下方模拟 string 类时实现。
这里 getline 该函数接口挺重要的也比较常用,希望大家能够熟练掌握。
getline 不是 string 类的封装函数 其在 std 命名空间里 想要用的话在 std 中直接调用即可
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cout << " 直接 cin 输入字符串(里面包含空格)的展现效果:" << endl;
cin >> s;//输入"abcdef *" 里面输入的有空格
cout << s << endl;
cout << "getline 方式实现含特殊字符的字符串:" << endl;
/*s.getline(cin, "dadad ", '\n');*/ //不能这样调用
getline(cin, s);//第三个参数不写默认为'\n'即:getline(cin, s,'\n');
cout << s << endl;
return 0;
}
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cout << "getline 输入:" << endl;
getline(cin, s, '*');
cout << s << endl;
return 0;
}
为什么第一个 getline 没有调用 getline 输入呢?在第一个代码中,cin >> s 会在输入缓冲区留下未读取的内容(包括空格和换行符),而 endl 只负责刷新输出缓冲区,对输入缓冲区没有任何影响。因此必须使用 cin.ignore() 来专门处理输入缓冲区的残留数据。而第二个代码 iostream 流中 即键盘文件中没有信息 getline 则会调用 以 cin 输入标准流的方式 将信息输入到键盘文件中 从而读取数据到 s 中。
上面的几个接口大家了解一下,下面的 OJ 题目中会有一些体现他们的使用。string 类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
注意:下述结构是在 32 位平台下进行验证,32 位平台下指针占 4 个字节。
vs 下 string 的结构
string 总共占 28 个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 string 中字符串的存储空间:当字符串长度小于 16 时,使用内部固定的字符数组来存放 (内存池)。内存池这里不做阐述 后续文章中会有该知识点的学习。当字符串长度大于等于 16 时,从堆上开辟空间。
参考:
union _Bxty {// storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE];// to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16,那 string 对象创建好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。 其次:还有一个 size_t 字段保存字符串长度,一个 size_t 字段保存从堆上开辟空间总的容量。 最后:还有一个指针做一些其他事情。 故总共占 16+4+4+4=28 个字节。
g++ 下 string 的结构
G++ 下,string 是通过写时拷贝实现的,string 对象总共占 4 个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:空间总大小字符串有效长度引用计数。
struct_Rep_base{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
指向堆空间的指针,用来存储字符串。
小试牛刀:
class Solution {
public:
bool isLetter(char ch) {
if (ch >= 'a' && ch <= 'z') return true;
if (ch >= 'A' && ch <= 'Z') return true;
return false;
}
string reverseOnlyLetters(string S) {
if (S.empty()) return S;
size_t begin = 0, end = S.size() - 1;
while (begin < end) {
while (begin < end && !isLetter(S[begin])) ++begin;
while (begin < end && !isLetter(S[end])) --end;
swap(S[begin], S[end]);
++begin;
--end;
}
return S;
}
};
class Solution {
public:
int firstUniqChar(string s) {
// 统计每个字符出现的次数
int count[256] = { 0 };
int size = s.size();
for (int i = 0; i < size; ++i) count[s[i]] += 1;
// 按照字符次序从前往后找只出现一次的字符
for (int i = 0; i < size; ++i)
if (1 == count[s[i]]) return i;
return -1;
}
};
#include<iostream>
#include<string>
using namespace std;
int main() {
string line;
// 不要使用 cin>>line,因为会它遇到空格就结束了
// while(cin>>line)
while (getline(cin, line)) {
size_t pos = line.rfind(' ');
cout << line.size() - pos - 1 << endl;
}
return 0;
}
class Solution {
public:
bool isLetterOrNumber(char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
}
bool isPalindrome(string s) {
// 先小写字母转换成大写,再进行判断
for (auto& ch : s) {
if (ch >= 'a' && ch <= 'z') ch -= 32;
}
int begin = 0, end = s.size() - 1;
while (begin < end) {
while (begin < end && !isLetterOrNumber(s[begin])) ++begin;
while (begin < end && !isLetterOrNumber(s[end])) --end;
if (s[begin] != s[end]) {
return false;
}
else {
++begin;
--end;
}
}
return true;
}
};
class Solution {
public:
string addStrings(string num1, string num2) {
// 从后往前相加,相加的结果到字符串可以使用 insert 头插
// 或者+=尾插以后再 reverse 过来
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int value1 = 0, value2 = 0, next = 0;
string addret;
while (end1 >= 0 || end2 >= 0) {
if (end1 >= 0) value1 = num1[end1--] - '0';
else value1 = 0;
if (end2 >= 0) value2 = num2[end2--] - '0';
else value2 = 0;
int valueret = value1 + value2 + next;//当前位上十进制的值
if (valueret > 9) {
next = 1;//进位
valueret -= 10;//当前为 -10
}
else {
next = 0;
}
//addret.insert(addret.begin(), valueret+'0');
addret += (valueret + '0');//转成数字字符
}
if (next == 1) {
//addret.insert(addret.begin(), '1');
addret += '1';
}
reverse(addret.begin(), addret.end());
return addret;
}
};
关于 string 的题目有很多,大家可以去刷题网站巩固,这里不做过多阐述了。
上面已经对 string 类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现 string 类,最主要是实现 string 类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下 string 类的实现是否有问题?
// 为了和标准库区分,此处使用 String
class String {
public:
/*String() :_str(new char[1]) {*_str = '\0';} */
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
String(const char* str = "") {
// 构造 String 类对象时,如果传递 nullptr 指针,可以认为程序非
if (nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString() {
String s1("hello bit!!!");
String s2(s1);
}
程序结束时 s1,s2 所指向的空间被连续释放了两次!!!
说明:上述 String 类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 构造 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2 共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
class String {
public:
String(const char* str = "") {
// 构造 String 类对象时,如果传递 nullptr 指针,可以认为程序非
if (nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s) :_str(new char[strlen(s._str) + 1]) {
strcpy(_str, s._str);
}
String& operator=(const String& s) {
if (this != &s) {
char* pStr = new char[strlen(s._str) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
class String {
public:
String(const char* str = "") {
if (nullptr == str) {
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s) :_str(nullptr) {
String strTmp(s._str);
swap(_str, strTmp._str);
}
// 对比下和上面的赋值那个实现比较好?
String& operator=(String s) {
swap(_str, s._str);
return *this;
}
/* String& operator=(const String& s) { if(this != &s) { String strTmp(s); swap(_str, strTmp._str); } return *this; } */
~String() {
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源
这里简单的模拟数据类型为 char 的 string,但是库里面的是满足各种编码的模板类。
#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;
class mystring {
public:
//构造
mystring(const char* s = "") {
//空指针非法
if (s == nullptr) {
assert(false);
return;
}
_str = new char[strlen(s) + 1];//分配空间 给'\0' 留一个空间
strcpy(_str, s);
_capacity = _size = strlen(s);
}
//拷贝
mystring(const mystring& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//析构
~mystring() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//赋值
mystring& operator=(const mystring& s) {
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
//比较运算符重载
//两个字符串相加
mystring operator+(const mystring& s) const {
mystring strtmp = *this;
//添加前 先确保空间够用
int newlenth = strlen(s._str) + strlen(_str) + 1;
if (newlenth > strtmp._capacity) {
//扩容
char* newchar = new char[newlenth * 2];
strcpy(newchar, strtmp._str);
delete[] strtmp._str;
strtmp._str = newchar;
strtmp._capacity = newlenth * 2;
}
strcat(strtmp._str, s._str);
strtmp._size = newlenth - 1;
return strtmp;
}
//+=
mystring& operator+=(const mystring& s) {
mystring strtmp = s;
*this = (*this + strtmp);
return *this;
}
//.....
//标准流 重载
friend ostream& operator<<(ostream& out, const mystring s);
friend istream& operator>>(istream& in, mystring& s);
//其他函数接口 如 swap find。。。
void swap(mystring& s) {
//三个值直接交换即可 空间上无需操作
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
void print() const {
for (int i = 0; i < _size; i++) {
printf("%c", _str[i]);
}
printf("\n");
}
private:
char* _str;
int _size;
int _capacity;
};
ostream& operator<<(ostream& out, const mystring s) {
s.print();
return out;
}
istream& operator>>(istream& in, mystring& s) {
//直接覆盖数据即可 无需清除之前的数据 修改_size 范围即可
int i = 0;
char ch;
while (in.get(ch) && ch != ' ' && ch != '\n') {
// 从 in 流读取,而非 stdin
s._str[i++] = ch;
}
s._str[i] = '\0';
s._size = i;
return in;
}
从用 char* 手动计算长度、反复处理内存溢出的'踩坑',到用 std::string 一行代码实现拼接、查找的'丝滑',再到亲手拆解其动态扩容、深拷贝的底层逻辑 —— string 类的学习,本质是一场对 C++'封装思想'的实践:它把复杂的内存管理藏在底层,把简洁的接口留给开发者,既解决了 C 语言字符串的痛点,又为后续 STL 容器的学习打下了'容器 + 迭代器 + 接口'的认知基础。
或许你现在对'小字符串优化(SSO)''迭代器失效'等细节仍有疑惑,或许在模拟实现时曾为深拷贝与浅拷贝的边界纠结 —— 但没关系,技术的精进本就是'先用透,再深究'的过程。后续可以试着用 string 结合 STL 算法(如 sort 排序字符串、find_if 筛选字符),或是在项目中用 stringstream 处理复杂字符串转换,让知识在实践中落地。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online