C++STL:从 0 到 1 手写 C++ string以及高频易错点复盘

C++STL:从 0 到 1 手写 C++ string以及高频易错点复盘

目录

一、整体结构

二、构造/析构函数

2.1 默认构造

2.2 拷贝构造

2.3 析构函数

三、功能接口

3.1 reserve

3.2 c_str

3.3 PushBack

3.4 Append

3.5 Insert

3.6 erase

3.7 substr

3.8 Find

四、运算符重载

4.1 =

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

4.3 +=

4.4 <<、>>

五、完整代码

一、整体结构

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

1. char* _str

这是一个指向字符数组的指针,用来存储字符串的实际字符数据。

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

2.int _size

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

3. int _capacity

记录当前已分配内存能容纳的最大字符数(不包括末尾的'\0')。

它代表了在不需要重新分配内存的情况下,字符串最多能存储的字符数量。当 _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内部以 \0 结尾的 C 风格字符串的常量指针,用来兼容那些只支持 C 语言风格字符串的函数接口。

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

3.3 PushBack

Push_Back接口的作用是在原有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,并保证末尾有 \0
  • 灵活性:支持多种追加格式(单个字符、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,保证末尾有 '\0'
  • 插入位置 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 风格字符串(以 \0 结尾的字符数组)中查找另一个 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; } 

Read more

做鸿蒙 App 一个月:10 个 ArkUI 大坑

做鸿蒙 App 一个月:10 个 ArkUI 大坑

子玥酱(掘金 / 知乎 / ZEEKLOG / 简书 同名) 大家好,我是子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。 我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括前端工程化、小程序、React / RN、Flutter、跨端方案, 在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。 技术方向:前端 / 跨端 / 小程序 / 移动端工程化 内容平台:掘金、知乎、ZEEKLOG、简书 创作特点:实战导向、源码拆解、少空谈多落地 文章状态:长期稳定更新,大量原创输出 我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 openid_client 深度打通鸿蒙应用的单点登录 (SSO)(基于 OpenID Connect 标准)

Flutter for OpenHarmony: Flutter 三方库 openid_client 深度打通鸿蒙应用的单点登录 (SSO)(基于 OpenID Connect 标准)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在现代企业级 OpenHarmony 应用中,为了安全和便捷,往往会使用 OpenID Connect (OIDC) 协议进行统一身份认证。无论是集成 Google 登录、GitHub 登录,还是对接企业内部的 Keycloak、Okta 等身份提供商(IdP),我们都需要一个健壮的库来处理繁杂的 OAuth2 握手流程。 openid_client 是一个功能极其全面的 Dart 实现。它能够自动发现服务器端点(Discovery)、处理 PKCE 流程并安全地交换令牌,是构建高安全级别鸿蒙应用的首选。 一、核心认证流程 OIDC 认证流程通常是通过浏览器重定向完成的,openid_client 充当了流程的指挥官。 身份服务器 (IdP)openid_client鸿蒙

By Ne0inhk
本地部署 Stable Diffusion:零基础搭建 AI文生图模型

本地部署 Stable Diffusion:零基础搭建 AI文生图模型

本地部署 Stable Diffusion:零基础搭建 AI 文生图系统 Stable Diffusion 是一款强大的开源文生图(Text-to-Image)AI 模型,可以本地运行,无需联网或付费就能生成高质量图像。相比 Midjourney、DALL·E 等云服务,Stable Diffusion 更自由、更可控。 这篇文章将手把手教你如何使用 Stable Diffusion WebUI(AUTOMATIC1111) 在本地搭建一个高效、可定制的 AI 画图系统,适合 AI 爱好者、程序员和设计师。 ✅ 目录 1. 为什么选择 Stable Diffusion? 2. 环境准备:硬件 & 软件 3. 安装与部署 WebUI 4.

By Ne0inhk

WhisperLiveKit 会议纪要模板定制:适配不同场景的纪要样式

核心定制原则 * 场景分类:区分正式会议、头脑风暴、项目复盘等场景,匹配对应的结构化模板。 * 关键元素保留:时间、参与人、决议事项、待办任务为通用必选项,其他字段按需增减。 正式会议模板示例 标题格式:[类型]项目名_日期(如[决策]Q3预算会_20240520) 内容结构: * 背景说明(3行以内) * 决议事项(编号列表,含责任人与DDL) * 争议点记录(斜体标注未达成共识项) * 附件链接(直接粘贴WhisperLiveKit生成的会议录音/转录URL) 创意讨论模板示例 标题格式:[脑暴]主题_发起人 内容结构: * 灵感池(无序列表记录所有点子) * 投票结果(用✅×3形式标记票数) * 可行性筛选(分立即执行/长期储备两栏表格) 技术评审模板示例 标题格式:[评审]系统名_

By Ne0inhk