当前位置: 首页 > news >正文

【C++】手搓一个STL风格的string容器

C++ string类的解析式高效实现

GitHub地址

有梦想的电信狗

1. 引言:字符串处理的复杂性

​ 在C++标准库中,string类作为最常用的容器之一,其内部实现复杂度远超表面认知。本文将通过一个简易仿照STLstring类的完整实现,揭示其设计精髓。我们将从内存管理、操作优化等维度,逐步构建一个简单支持核心功能的string类。


2. 基础架构设计

2.1 成员变量声明

  • 为了和标准库中的string区分,我们把自己实现的string封装在m_string这个命名空间中
  • string的底层是存放字符的顺序表,因此我们采用顺序表的结构来实现
  • 基本结构如下:
namespace m_string {class string {public://成员函数...etc...我们逐一实现//迭代器,运算符重载等...private:size_t _size;         // 当前有效字符数size_t _capacity;     // 存储容量(不含结束符'\0'),会进行扩容char* _str;           // 动态数组的指针public://静态成员变量 类内声明、类外(定义)初始化const static size_t npos; };//类内声明、类外初始化   特殊标记值  const size_t string::npos = -1;		//建议静态成员变量,声明和定义分离
}

设计要点

  • _size的大小代表了当前空间的内容,已存放的字符的个数:包括字符和\0
  • 三成员架构是顺序表结构的经典设计:
    • _size:记录有效数据个数
    • _capacity:管理当前的最大容量,扩容后容量更新,注意_capacity不包含末尾的\0
    • _str:指针指向堆内存
  • 静态常量npos的类外定义需特别注意,要在类外进行初始化。令size_t类型的npos值为-1,用来表示整型的最大值
  • 成员变量设为访问权限设为private, 对外提供public的成员函数,符合面向对象中封装的思想

2.2 迭代器实现

STL中的迭代器,可能是指针,也可能不是指针
string的迭代器本质上是char,因为string本质上就是一个字符数组,天生适合用指针来访问*

public:// 将char* 封装成 iterator 迭代器typedef char* iterator;typedef const char* const_iterator;// 普通对象的迭代器  // 普通对象的迭代器如果加了const, 会导致非const对象只能返回const_iterator,失去修改元素的能力(违反直觉)。iterator begin() {		return _str;	//数组名是第一个元素的地址}iterator end() {//指针相加 _str + _size 得到最后一个元素的下一个位置的指针return _str + _size;	}//const对象的迭代器const_iterator begin() const {return _str;	}const_iterator end() const {return _str + _size;}
  • begin()方法要返回数组的第一个位置的指针
  • end()方法要返回数组最后一个元素的下一个位置
  • 普通版本和const版本需分别实现

实现分析

  • 迭代器本质是原生指针的封装
  • 访问权限是public, 调用begin/end方法返回相应的迭代器。
  • 通过const重载实现常量迭代器
  • 与标准库的迭代器体系完全兼容

3. 构造函数与析构函数

3.1 基础构造函数

v1
// 初始实现(存在问题)
// 初始化列表是按照成员变量在类中声明的次序进行初始化的
string(const char* str = "\0") : _size(strlen(str)), _capacity(_size)				//利用_size来初始化_capacity, _str(new char[_capacity+1]) 
{strcpy(_str, str); // 中间含\0时会被截断
}

问题发现

  • 使用strcpy()会因"hello world\0hello Linux"这样中间含有'\0'的字符串导致数据丢失。
  • 初始化列表是按照成员变量在类中声明的次序完成初始化的,与初始化列表中实现的顺序无关
    • 如果有人调整了初始化列表的初始化顺序或成员变量的声明顺序,那么利用_size来初始化_capacity会发生未定义行为。

优化版本

v2
// 参数列表是  用c风格的字符串 const char* 进行构造。c风格的字符串默认以\0为结束符
// 因此传入 "hello world\0hello Linux" 这样中间含有'\0'的字符串,导致数据丢失是调用者的问题
string(const char* str = "") {	//默认构造会构造一个""空字符串_size = strlen(str);_capacity = _size;					//capacity表示可以存放的下的字符个数_str = new char[_capacity + 1];		//开空间+1  要存放'\0'//strcpy(_str, str);	//拷贝数据,遇到中间含有\0的字符串会有Bug//strcpy默认会 拷贝\0, 因此使用memcpy,需要将拷贝的字节数+1,考虑\0的位置memcpy(_str, str, _size + 1);	
}

思路

  • strlen(str)计算传入字符串的长度,并赋值给_size
  • _capacity = _size,,初始化时,默认使容量和有效字符和数相等,构造时不额外开空间
  • _str = new char[_capacity + 1],为字符串分配空间,_capacity + 1为字符串结尾的\0预留位置
  • memcpy(_str, str, _size + 1),最后将源字符串中的数据拷贝至新开辟的空间。

优化点

  • 使用memcpy()替代strcpy(),memcpy()是拷贝内存中的数据,遇到\0不会结束

  • 参数缺省值改为空字符串""

  • 均为内置类型,不使用初始化列表进行初始化,而是在构造函数内部完成初始化,消除初始化列表顺序依赖

3.2 析构函数

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

析构函数的思路和实现较为简单

  • 释放管理字符数组的空间_str,注意释放数组要使用delete []
  • 管理字符数组的空间的指针_str置空
  • _size_capacity置零

3.3 拷贝构造函数

往期文章对深拷贝的简单总结介绍:

https://blog.csdn.net/2301_80064645/article/details/145593384?fromshare=blogdetail&sharetype=blogdetail&sharerId=145593384&sharerefer=PC&sharesource=2301_80064645&sharefrom=from_link

传统实现

手动开辟释放内存。

// 拷贝构造  要实现深拷贝,开一块新的空间,拷贝数据,并初始化。
// 新开一块空间,并进行深拷贝防止两个指针指向同一块空间
string(const string& str) {_str = new char[str._capacity + 1];	// _capacity不包含\0, +1 考虑 \0//strcpy(_str, str._str);memcpy(_str, str._str, str._size + 1);_size = str._size;_capacity = str._capacity;
}

拷贝构造函数需要实现深拷贝。如果是浅拷贝的话,字符串指针会存下同一块空间的地址,析构时会对同一块空间析构两次,会引发错误

  • 开辟新空间,new char[str._capacity + 1],大小为_capacity + 1
  • memcpy拷贝原数据到新空间,拷贝大小为_size + 1,确保字符串末尾的\0也被拷贝。
  • 更新_size_capacity

现代写法

// 交换两个string对象成员变量的内容
void swap(string& str) {		std::swap(_str, str._str);		//不能直接交换两个对象std::swap(_size, str._size);std::swap(_capacity, str._capacity);
}
string(const string& s) :_str(nullptr),_size(0),_capacity(0)
{string tmp(s.c_str());	//用s的数据 调用构造函数  新构造一个 局部tmp对象, 具有不同的字符数组 地址this->swap(tmp);			//交换tmp和s2内的三个成员变量,//交换后tmp内的_str为nullptr,局部对象出了函数作用域销毁后,析构tmp对象//构造tmp时使用s.c_str()初始化,而c_str返回以\0结尾的C风格字符串//若s._str中间含有\0,tmp的构造会截断数据,导致拷贝不完整
}

s2(s1)

  • 将s2初始化为
    • _str(nullptr)
      ,_size(0)
      ,_capacity(0)
  • s1.c_str()构造一个和s1一样的局部对象tmp字符串数据的内容一样,但空间和地址不同
  • 交换s2tmp中成员变量的值,
    • 交换前tmp._str指向和s1内容一样的一块新空间。拷贝构造时,s2待初始化,没有空间
    • 交换后s2._str指向之前tmp._str管理的那块空间,tmp._str指向待初始化的那块空间
  • 之后局部对象出了函数作用域销毁后,析构待初始化tmp对象,析构前tmp内的数据为 nullptr 0 0

优势对比

方法异常安全代码复用可维护性
传统实现不安全
现代写法强保证优秀

4. 赋值运算符的进化

4.1 传统实现

//	s1 = s3
string& operator=(const string& str) {if (this != &str) {char* newSpace = new char[str._capacity + 1];	//开空间memcpy(newSpace, str._str, str._size + 1);		//拷数据delete[] _str;			//被赋值前可能为非空string,因此要释放原空间_str = newSpace;_size = str._size;_capacity = str._capacity;}return *this;
}
  • 赋值运算符要实现深拷贝,赋值后,两个string对象要拥有两块独立的空间,并对赋值前的那块空间进行机构

  • if (this != &str):自己给自己赋值时直接跳过。

  • 传统实现,手动开空间,拷贝数据

  • 更改指针前,析构原空间

    • delete[] _str 被赋值前可能为非空string,因此要释放原空间
    • _str = newSpace;
  • 更新_size_capacity

  • 返回*this,满足连续赋值的需求

潜在风险

  • new可能抛出异常导致原对象损坏

4.2 现代实现

//	s1 = s3
string& operator=(const string& str) {if (this != &str) {string tmp(str);	//反正tmp为局部对象,出了作用域也要销毁,不如让他销毁时,顺便把s1的空间析构了// s1 想要tmp管理的那块空间std::swap(_str, tmp._str);		//不能直接交换两个对象,否则会引发无穷赋值std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);//可以直接//this->swap(tmp);}return *this;
}
  • s1 = s3

  • 深拷贝创建局部对象tmp,反正tmp为局部对象,出了作用域也要销毁,不如让他销毁时,顺便把s1的空间析构了

  • 交换tmpthis对象的成员变量,更改指针指向。

    • 构造的tmpstr相同,且为深拷贝构造
    • this->swap(tmp)之后,s1接管了tmp的数据,tmp接管了s1的数据。
  • 返回*this,满足连续赋值的需求

  • 函数结束后,局部对象tmp销毁,调用析构,释放赋值前的旧空间(s1)

4.3 终版实现

// s1 = s3
string& operator=(string tmp) {		//直接利用函数参数,深拷贝s3,函数结束后,形参自动析构this->swap(tmp);			// 将s3的拷贝和s1 也就是 *this 交换return *this;		// 返回*this, 也就是 s1
}
  • 既然现代实现要构造局部对象,那不妨直接在形参中使用值传递的方式构造局部对象,形参tmp是右值实参的拷贝
    • 自定义类型对象在值传参时,会默认被要求去调用拷贝构造函数
    • 实参s3值传递,传递给形参tmpstring tmp = s3。局部对象tmp中有和s3一样的数据
  • this->swap(tmp);:直接和局部对象交换资源。
  • 函数结束后,形参对象tmp销毁,调用析构,释放s1的旧空间。

革命性改进

  1. 参数值传递,自动调用拷贝构造
  2. swap操作保证强异常安全
  3. 代码量减少60%
  4. 自动清理旧资源

5. 容量管理策略

5.1 reserve

///可以用reserve预留空间,来实现扩容
// reserve实现的均是异地扩容
//考虑特殊情况的话,memcpy会更好
void reserve(size_t request_capacity) {		//request_size指的是要新存放的字符的个数if (request_capacity > _capacity) {		//请求的空间大于_capacity时,才扩容char* newSpace = new char[request_capacity + 1];	//多开一个空间存放\0//strcpy(newSpace, _str);		//new是异地扩容memcpy(newSpace, _str, _size + 1);delete[] _str;_str = newSpace;_capacity = request_capacity;}
}

扩容思路

  • 期望容量request_capacity 大于当前容量_capacity时,才进行扩容
  • 采取异地扩容策略,newSpace存放新空间的地址
  • 使用memcpy而非strcpy
    • strcpy拷贝数据时,遇到\0就终止了,遇到中间含有\0的字符串会带来意外的结果。
    • 使用memcpy来拷贝空间中的所有数据,包括中间的\0
  • 将原来的字符串空间释放:delete[] _str;
  • 将新空间的地址赋值给管理字符数组的指针:_str = newSpace;
  • 更新容积:_capacity = request_capacity

扩容策略

  • 异地扩容保证数据完整性
  • 精确计算拷贝字节数(_size+1),此字节数是数组中有效字符的个数
  • 典型应用场景:push_back时的容量检查

5.2 resize

void resize(size_t newSize, char ch = '\0') {if (newSize < _size) {_size = newSize;_str[_size] = '\0';}else {//reserve会判断newSize和_capacity的大小,超过扩容,等于时不做处理reserve(newSize);//扩容后进行初始化for (size_t i = _size; i < newSize; ++i)_str[i] = ch;_size = newSize;_str[_size] = '\0';}
}

双模式操作

  • 收缩(if逻辑):直接截断
    • if (newSize < _size):判断指定newSize是否小于当前size
    • 小于的话,直接截断,更新_size = newSize;
    • 再将字符串最后一个字符的下一个位置设为\0_str[_size] = '\0';
  • 扩展(else逻辑):填充指定字符
    • 期望大小大于当前_size,通过reserve(newSize)进行扩容
      • 进入else逻辑后,newSize >= _size,等于时reserve(newSize)不做处理。
      • 大于时会进行扩容
    • 扩容后,对下标在_size及之后的位置进行初始化,用字符ch进行填充
    • 初始化后,更新_size,将末尾位置设置\0

6. 字符串操作优化

6.1 push_back

  • +=的本质是调用了push_back,因此先实现push_back
void push_back(char ch) {//先考虑扩容if (_size == _capacity) {//二倍扩容,并防止空构造的字符串reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;	 //放字符_str[_size] = '\0';	 //加上\0
}
  • +=操作可以直接复用push_backpush_back是在字符串末尾插入一个字符
  • 插入前考虑数组是否还有容量,_size == _capacity时表示容量已满
  • reserve扩容,_capacity == 0 ? 4 : _capacity * 2
    • push_back前为空串,则分配空间容量为4
    • push_back前为已有数据且容量已满,则二倍扩容
  • 更新状态
    • 插入字符
    • 在末尾放上\0;

6.2 append

void append(const char* str) {size_t len = strlen(str);if(_size + len > _capacity){//至少扩容到 _size + lenreserve(_size + len);}memcpy(_size, str, len + 1);	//拷贝大小为 len + 1, 要拷贝\0_size += len;    
}
  • 计算待插入的字符串的长度
  • 检查容量,if(_size + len > _capacity)为真,代表需要扩容
  • 扩容至少扩容到_size + len
  • 拷贝数据到_size位置开头的空间
  • 拷贝大小为len+1,为\0预留空间
  • 最后更新_size的大小

6.3 operator+=

利用+=可以方便的在字符串后面追加字符或字符串

+=字符
string& operator+=(const char ch){push_back(ch);return *this;
}
+=字符串
string& operator+=(const char* str){append(str);return *this;
}
  • +=字符/字符串直接复用push_backappend即可
  • 为了满足连续+=的功能,需要返回当前对象,也就是*this

6.3 insert实现

6.3.1 insert字符串
void insert(size_t pos, const char* str) {assert(pos <= _size);	//_size位置是字符串的末尾,可以在字符串的末尾插入assert(str != nullptr); // 确保str非空size_t len = strlen(str);//扩容if (_size + len > _capacity)reserve(_size + len);size_t end = _size;		//从末尾的 \0 开始挪动//如果在 0 位置插入 end最后会变成 size_t -1 也就是npos 整形的最大值while (end >= pos && end != npos) {	_str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; ++i)_str[pos + i] = str[i];_size += len;_str[_size] = '\0';		//添加字符串结束标志
}
  • 越界检查,确保待插入字符串str为非空
  • 计算待插入串的长度,根据长度判断是否需要扩容并扩容,至少要扩容到_size + len长度
  • size_t end = _sizesize\0的下标,挪动时从末尾的 \0 开始挪动
    • end + len是需要挪动的字符调整后的下标位置

    • end类型为size_t,跳出循环时,end 的值 为 pos - 1

      • pos0时,size_t end = -1的值为整型的最大值,是大于pos的。为了避免进入死循环,需增加循环条件为end >= pos && end != npos
    • _str[pos + i] = str[i]:挪动数据过后,再将待插入字符串逐个字符插入

  • 更新状态
    • _size += len:更新大小
    • _str[_size] = '\0':添加字符串结束标志
6.3.2 insert n个字符
  • pos位置开始,插入n个相同的字符
//让插入的那个字符的下标变成pos
void insert(size_t pos, size_t n, char ch) {assert(pos <= _size);	//_size位置是字符串的末尾,可以在字符串的末尾插入//扩容if (_size + n > _capacity)reserve(_size + n);//挪动数据//当传入的pos为0时,end会变成-1,end是size_t,-1会变成整形的最大值,会一直进入循环//size_t end = _size;	//int end = _size;	//while (end >= (int)pos) {	//运算符两端 两个操作数类型不一致时,会进行  提升//						// 一般是范围小的向范围大的进行提升//	_str[end + n] = _str[end];//	--end;//}size_t end = _size;while (end >= pos && end != npos) {_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; ++i) {_str[pos + i] = ch;}_size += n;_str[_size] = '\0';		//添加字符串结束标志
}
  • 越界检查,确保待插入位置有效
  • 根据待插入字符的个数,根据个数判断是否需要扩容并扩容,至少要扩容到_size + len长度
  • size_t end = _size_size\0的下标,挪动时从末尾的 \0 开始挪动
    • end + n是需要挪动的字符调整后的下标位置

    • end类型为size_t,跳出循环时,end 的值 为 pos - 1

      • pos0时,size_t end = -1的值为整形的最大值,是大于pos的。为了避免进入死循环,需增加循环条件为end >= pos && end != npos
    • _str[pos + i] = ch:挪动数据过后,再将待插入字符循环依次插入

  • 更新状态
    • _size += n:更新大小
    • _str[_size] = '\0':添加字符串结束标志
6.3.3 insert总结

在实现的过程中,我们不难发现,实现在pos位置插入n个字符和插入一个字符串的思路高度相似

  • 插入字符时:每个字符已经通过参数传入,挪动数据后直接赋值即可
  • 插入字符串时:每个字符需要从长度为len的字符串中取

insert多个字符和insert字符串,除了细节问题,逻辑上没有任何区别!

6.4 erase

//从pos位置开始删,向后删除len个字符,不传参默认全部删完
void erase(size_t pos, size_t len = npos) {assert(pos <= _size);if (len == 0)return;// 删完的情况 : pos + len == _size 时,pos + len位置放的是\0,大于时才全部删完if (len == npos || pos + len > _size) {		//这两种都是删完的情况_str[pos] = '\0';_size = pos;}// 删除一部分的情况else {size_t end = pos + len;		// 跳过要删除的三个字符,end从需要挪动的第一个字符开始//_str[end] == '\0'时(end == _size),可以把'\0'也挪过来,也可以不挪,最后统一设置\0while (end < _size) {	_str[pos++] = _str[end++];}_size -= len;}_str[_size] = '\0';	//统一设置结尾符
}

删除策略

  • 尾部删除:直接截断
  • 中间删除:前向覆盖

思路分析:

  • pos进行越界检查
  • 先判断要全部删完的情况
    • len == npos || pos + len > _size时,代表要删完pos及之后的字符,直接截断
      • 直接将pos位置设为\0
      • 修改_sizepos
  • else是删除完从pos开始的len个字符的情况:
    • 跳过要删除的三个字符,pos+len位置是需要挪动的第一个字符,开始依次向前挪动数据,到_size(\0)结束
    • 更新状态:
      • _size -= len_
  • 最后统一设置结束符_str[_size] = '\0'

7. 运算符重载的艺术

7.1 比较类运算符

<和==
bool operator<(const string& s) const {int ret = memcmp(_str, s._str, std::min(_size, s._size));	//前面小于后面时,返回值小于0return ret == 0 ? _size < str._size : ret < 0;
}bool operator==(const string& str) const {return _size == str._size			//两个字符串相等,其长度一定相等,优先比较长度&& memcmp(_str, str._str, _size) == 0;		//再比较其内容是否相等
}

实现技巧

  • 使用memcmp()提升比较效率
  • 长度优先比较策略

<逻辑

  • 只比较两字符串同等长度的内存(内容),用更小的那个长度进行比较

  • 将比较结果存入ret

  • ret == 0时,代表两字符串共有长度的部分相同,此时_size < str._size为真时才是小于(共有长度相同,谁短谁就小)。

  • ret != 0时,比较的结果memcmp已经帮我们比较好了

    • ret < 0代表前面小于后面
    • ret > 0代表前面大于后面

==逻辑

  • 只有长度相等的字符串才有可能相等
  • 长度相等后,再用memcmp(_str, str._str, _size) == 0判断是否相等
    • return _size == str._size && memcmp(_str, str._str, _size) == 0
    • 两个字符串相等,其长度一定相等。
      • 优先比较长度,如果长度不相等,会触发&&的截断特性
      • 再用memcmp比较其内容是否相等
其他逻辑复用<和==
// <= 小于或等于
bool operator<=(const string& str) const {return *this < str || *this == str;
}
// > 小于等于取反
bool operator>(const string& str) const {return !(*this <= str);
}
// >= 大于或等于
bool operator>=(const string& str) const {return (*this < str);
}
// !=  ==取反
bool operator!=(const string& str) const {return !(*this == str);
}

7.2 []重载

// []重载		
// const对象调const版本[]    普通对象调普通版本[]
char& operator[](size_t pos) {	//返回引用,读写版本assert(pos < _size);return _str[pos];
}
const char& operator[](size_t pos) const {	//只读版本assert(pos < _size);return _str[pos];
}
  • char&做返回值实现了普通对象可读可写的效果
  • assert(pos < _size)检查[]访问是否越界
  • []重载返回传入下标位置的字符的引用即可
  • const对象实现const版本[],普通对象实现普通版本[]

7.3 流运算符重载

流插入<<
// c的字符数组,以\0为终止算长度
// string不看\0,以size为终止算长度
ostream& operator<<(ostream& out, const m_string:: string& s) {//ostream这个类做了一件事,做了防拷贝,因此 ostream 类对象 做参数或返回值时要用引用//实现方式 1//out << s.c_str();		// 调用c_str() 可以直接打印//out << s.c_str;			// 访问s._str 实现字符串打印// 以上两种方式,打印 c_str 遇到 "hello world\0hello Linux" 中间含有\0的字符串时,是错误的// 流插入的要求是,有多少内容,打印多少内容 因此不能遇到\0终止// 使用一个一个遍历字符的方式  实现打印/*for (int i = 0; i < s.size(); ++i)out << s[i];*/for (auto& e : s)out << e;return out;
}

在这里插入图片描述

  • 成员函数的第一个形参是this,为了符合<<的习惯,期望第一个形参应当是流对象。根据实现方式的不同,<<要重载成全局函数或友元函数

    • 1. out << s.c_str():调用c_str() 可以直接用C风格的字符串打印
    • 2. out << s.c_str:访问s._str 实现字符串打印,此时<<需要重载为友元函数,访问私有变量s.c_str
    • 以上两种方式,打印 c_str 遇到 "hello world\0hello Linux" 中间含有\0的字符串时,是错误的。因为流插入的要求是,有多少内容,打印多少内容 因此不能遇到\0终止
  • 最终我们使用for循环一个一个遍历字符的方式,实现打印。传统的for循环和范围for都可以实现

  • const m_string:: string& s:对只读对象加const限定

  • ostream做了特殊处理,做了防止值拷贝,因此ostream类对象做参数或返回值时要传引用

  • 函数返回ostream& out对象,实现连续<<输出流

流提取>>
v1实现功能版
istream& operator>>(istream& in, m_string::string& str) {//一个一个字符读/*char ch;in >> ch;*///用in.get() 读str.clear();	// 对同一个string进行多次输入时,每次输入前要进行初始化    // 如果不初始化,会出现字符串堆叠char ch = in.get();//以空格或换行分割字符串while (ch != ' ' && ch != '\n') {str += ch;//in >> ch;ch = in.get();}return in;
}
  • 我们在输入数据时,是用空格和换行符分隔多个值的
  • cinscanf在读取数据时,也是以空格和换行符对多个值进行分隔的,因此,注定cinscanf默认读取不到空格和换行
  • istream类对象的get()方法来解决这个问题,get()可以读入任意的字符,包括空格和换行。
    • 我们可以通过对循环读取的条件的控制,来实现空格分隔\n分隔多个值
  • str.clear():可能会向同一个string对象中多次输入内容,为了防止数据重复,每次输入前要进行初始化(用clear清空)。
  • while (ch != ' ' && ch != '\n')
    • 此循环条件,遇到空格' '\n时,不进入循环,因此是以空格和换行符分隔多个值的
    • str += ch:将读到的字符,依次追加到str末尾实现字符串的读取
    • ch = in.get():追加后接着读取下一个字符
  • 最后返回in对象,实现连续读取

我们V1实现的方式存在一些问题:

  • ==我们输入的第一个字符不能是空格和换行!==V2版本解决这样的问题
  • 多次+=会调用push_back存在面对长字符串时多次扩容的性能损耗
  • V2版本将解决以上问题
v2最终优化版
istream& operator>>(istream& in, m_string::string& s) {s.clear();	//每次读取要先清空缓冲区,防止 多次输入数据时,数据重叠char ch = in.get();char buff[127] = { '\0' };//清空 第一个有效数据 来临前的空格和换行while (ch == ' ' || ch == '\n')ch = in.get();		// 读了之后,直接去读下一个字符,就可以表示清除了int i = 0;//while (ch != '\n') {				// 这种写法,类似于getline的实现,以换行符作为多个字符串的分隔,可以读到空格while (ch != ' ' && ch != '\n') {	//这种写法读不到字符串中的空格或换行//可以选择 使用 \n 还是 ' '作为字符串的分隔符//s += ch;		//+=,当输入的字符串非常大时,会不断扩容,利用buff减少扩容的次数buff[i++] = ch;if (i == 127) {buff[i] = '\0';s += buff;i = 0;}ch = in.get();}// i != 0 说明里面还有数据if (i != 0) {buff[i] = '\0';s += buff;}return in;
}
  • 每次读取前清空存放数据的字符串

  • char ch = in.get() char buff[127] = { '\0' },用get一个一个读取所有的字符char buff[127]利用一个缓冲区来减缓+=的频繁扩容问题

  • while (ch == ' ' || ch == '\n') ch = in.get(); 清空第一个有效字符来临前的空格和换行

    • 第一个有效字符来临前时,遇到空格或换行都不读入
    • ch = in.get():遇到非有效的空格和换行,读了之后,直接去读下一个字符,就可以表示清除了
  • i来记录当前字符的个数,通过i映射到缓冲区的下标

  • 两种不同条件的while循环,可以控制 使用 \n 还是 空格或\n 作为字符串的分隔符

    • while (ch != '\n') 这种写法,类似于getline的实现,以换行符作为多个字符串的分隔,可以读到空格
    • while (ch != ' ' && ch != '\n') :一般实现,这种写法,空格和换行都是字符串的分隔符,因此读不到字符串中的空格或换行
  • buff[i++] = ch:将读到的每个字符填充到提前开辟的缓冲区buff

    • i == 127时,代表缓冲区已满,将buff[127]位置元素设为字符串结尾\0
    • s += buff:再将缓冲区内的字符串追加到s
    • i = 0:最后将i置零,重新向缓冲区中填充数据
  • in.get接着读取剩余字符

  • 循环结束后, i != 0时,说明 i 未到达127buff内还存在有数据

    • buff[i] = '\0':设置字符串结尾
    • s += buff:将缓冲区中的数据追加到string

通过添加buff缓冲区减少了扩容次数

关键设计

  • 输出流直接遍历输出
  • 输入流采用缓冲区减少扩容
  • 跳过前导空白符空格和\n

8. 查找字符和子串

find字符

//查找单个字符
size_t find(char ch, size_t pos = 0) const {assert(pos < _size);for (size_t i = pos; i < _size; ++i) {if (_str[i] == ch)return i;}return npos;
}
  • 检查断言pos < _size
  • 循环遍历,找到了的话返回下标
  • 全部遍历结束时,没找到,返回npos

find子串

//查找子串,返回子串的下标
size_t find(const char* str, size_t pos = 0) const {if (!str || pos > _size) // 处理空指针和越界posreturn npos;const char* ptr = strstr(_str + pos, str);return ptr ? ptr - _str : npos;
}
  • strnullptrpos > _size返回npos
  • 调用C语言的库函数strstr,从_str + pos位置开始找,ptr接收函数的返回值
  • ptr不为空代表找到了,return ptr - _str,数组中,指针-指针,得到下标
  • ptr为空时代表未找到,返回npos

9. 其他重要接口

c_str()与clear()

//返回c_str    const 修饰this指针指向的对象,可以让普通对象和const对象都可以调用
//函数内不修改对象,建议加上const
const char* c_str() const { //返回数组名return _str; 
}//清空数据
void clear() {_str[0] = '\0';_size = 0;
}
  • 返回C语言风格的字符串const char*,与C风格的字符串形成良好的兼容。
  • 清空数据,代表数组中没有字符,则第一个元素直接设为字符串的结束标记\0
  • 再将_size设为0

size()与capacity()

 //const 修饰 this指针指向的对象,可以让普通对象和const对象都可以调用
size_t size() const { //直接返回字符串当前的大小return _size; 
}	
size_t capacity() const { //返回当前string对象的容积return _capacity; 
}	
  • const 修饰 this指针指向的对象,可以让普通对象const对象都可以调用
  • size()直接返回当前已有的字符的个数,返回_size
  • capacity()直接返回当前字符数组的最大容量,用_capacity表示
  • 函数内不修改当前对象,加上const

10. 结语

​ 通过从零构建高性能字符串类的实践,我们深刻理解了STL容器的设计哲学。该实现以三成员架构size/capacity/str)为核心,采用深拷贝确保数据独立性,通过swap技巧实现异常安全的赋值操作。现代写法通过参数值传递自动完成资源转移,将代码复杂度降低60%。容量管理采用二倍扩容策略与精准内存预分配,平衡了空间效率与时间性能。运算符重载通过内存级比较和流缓冲优化,实现了接近原生指针的访问效率。查找算法巧妙复用C库函数,在保证正确性的前提下提升执行效能。整个设计严格遵循RAII原则,通过迭代器封装实现STL兼容,最终在功能完备性与运行效率之间达到精妙平衡。

以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流

分享到此结束啦
一键三连,好运连连!

相关文章:

【C++】手搓一个STL风格的string容器

C string类的解析式高效实现 GitHub地址 有梦想的电信狗 1. 引言&#xff1a;字符串处理的复杂性 ​ 在C标准库中&#xff0c;string类作为最常用的容器之一&#xff0c;其内部实现复杂度远超表面认知。本文将通过一个简易仿照STL的string类的完整实现&#xff0c;揭示其设…...

无实体对话式社交机器人 拟人化印象形成机制:基于多模态交互与文化适配的拓展研究

《如何感知AI对话者:无实体对话式社交机器人拟人化对其印象形成效果影响机制的实验研究》解析 一、研究背景与核心问题 (一)技术背景与研究动机 随着生成式AI技术发展,以ChatGPT、文心一言为代表的无实体对话式社交机器人兴起,用户对其高度拟人化特征有显著需求,如扮演…...

存储器:DDR和独立显卡的GDDR有什么区别?

本文来简要对比DDR&#xff08;Double Data Rate SDRAM&#xff09;和GDDR&#xff08;Graphics Double Data Rate SDRAM&#xff09;的区别&#xff0c;重点说明它们在设计、性能和应用上的差异&#xff1a; 1. 设计目标与架构 DDR&#xff1a;通用型DRAM&#xff0c;设计为…...

viewDesign里的table内嵌套select动态添加表格行绑定内容丢失

问题 描述 viewDesign里的table内嵌套select&#xff0c;表格的行数是手动点击按钮添加的&#xff0c;添加第一行选择select的内容能正常展示&#xff0c;添加第二行第一行的select的内容消失 代码 <FormItem label"内饰颜色"><Tableclass"mt_10&q…...

vue v-html无法解析<

vue v-html无法解析字符串的小于号 方法一&#xff1a;可以替换成转义符 (实际还是会报错) let str 12345<445667 str.replaceAll(<, <)方法二&#xff1a;可以替换成中文小于号 let str 12345<445667 str.replaceAll(<, &#xff1c;)...

COLT_CMDB_linux_userInfo_20250508.sh修复历史脚本输出指标信息中userName与输出信息不一致问题

#!/bin/bash #IT_BEGIN #IT_TYPE3 #IT SYSTEM_LINUX_AGENTUSERDISCOVER|discovery.user[disc] #原型指标 #IT_RULE SYSTEM_LINUX_AGENTUSERGROUPID|groupId[{#USERNAME}] #IT_RULE SYSTEM_LINUX_AGENTUSERHOME|userHome[{#USERNAME}] #IT_RULE SYSTEM_LINUX_AGENTUSERNAME|user…...

A. Row GCD(gcd的基本性质)

Problem - 1458A - Codeforces 思路&#xff1a; 首先得知道gcd的两个基本性质&#xff1a; (1) gcd(a,b)gcd(a,|b-a|) (2) gcd(a,b,c)gcd(a,gcd(b,c)) 结合题目所给的a1bj&#xff0c;a2bj...... anbj 根据第一条性质得到&#xff1a; gcd(a1bj&#xff0c;a2bj)gcd(…...

k8s术语之Horizontal Pod Autoscaling

应用的资源使用率通常都有高峰和低谷的时候&#xff0c;如何削峰填谷&#xff0c;提高整体的整体资源利用率&#xff0c;让service中的Pod个数自动调整呢&#xff1f;Horizontal Pod Autoscaling:使pod水平自动缩放。这个Object也是最能体现kubernetes之于传统运维价值的地方&a…...

函数级重构:如何写出高可读性的方法?

1. 引言:为什么方法级别的重构如此重要? 在软件开发中,方法(函数)是程序逻辑的基本单元。一个高质量的方法不仅决定了程序是否能正常运行,更直接影响到: 代码的可读性:能否让其他开发者快速理解可维护性:未来修改是否容易出错可测试性:是否便于编写单元测试协作效率…...

手撕基于AMQP协议的简易消息队列-8(单元测试的编写)

在MQTest中编写模块的单元测试 在MQTest中编写makefile文件来编译客户端模块 all:Test_FileHelper Test_Exchange Test_Queue Test_Binding Test_Message Test_VirtualHost Test_Route Test_Consumer Test_Channel Test_Connection Test_VirtualHost:Test_VirtualHost.cpp ..…...

硬件选型:工控机的选择要素

在机器视觉应用中&#xff0c;工控机作为核心计算设备&#xff0c;承担着图像处理、数据分析和设备控制等多重任务。由于机器视觉常常在工业自动化、质量检测和精密控制中发挥重要作用&#xff0c;工控机的选型直接影响系统的性能和可靠性。 1. 应用场景与需求 机器视觉系统广…...

【芯片设计- RTL 数字逻辑设计入门 4.1 -- verilog 组合逻辑和时序逻辑延时比较】

文章目录 Overview时间线简单示意Overview 我们来详细分析下面这段 RTL Code , sbcs_sbbusy 为什么会比 sbcs_sbbusy_nx 慢一拍(晚一个时钟周期变化)。 assign sbcs_sbbusy_nx = set_sbcs_sbbusy;always @(posedge clk or negedge dmi_resetn) beginif (!dmi_resetn) begi…...

关于ubuntu下交叉编译arrch64下的gtsam报错问题,boost中boost_regex.so中连接libicui18n.so.55报错的问题

交叉编译gtsam时遇到的报错信息如下&#xff1a;gtsam需要连接boost&#xff0c; 解决办法&#xff1a; 1.重新编译boost可解决。 2.自己搞定生成一个libicui18n.so.55。 由于我们的boost是公用的&#xff0c;因此1不太可能&#xff08;我试过重新编译完boost,在编译gtsam完…...

IoT平台和AIoT平台的区别

1. 什么是AIoT平台&#xff1f; AIoT&#xff08;人工智能物联网&#xff0c;Artificial Intelligence of Things&#xff09;平台 是 人工智能&#xff08;AI&#xff09; 与 物联网&#xff08;IoT&#xff09; 深度融合的技术框架&#xff0c;通过将AI算法嵌入物联网终端或…...

iOS 模块化开发流程

iOS模块化开发是一种将大型项目拆分为独立、可复用模块的开发模式&#xff0c;能够提升代码可维护性、团队协作效率和动态交付能力。以下是iOS模块化开发的核心流程与关键要点&#xff1a; 一、模块化设计阶段 业务解耦与模块划分 横向分层&#xff1a;基础层&#xff08;网络、…...

云原生安全治理体系建设全解:挑战、框架与落地路径

📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:云原生环境下,安全治理正在被重构 在传统IT架构中,安全防护多依赖边界设备(如防火墙、WAF、堡垒机)进行集中式防护。然而,在云原生环境下,这种“边界式”安全模型正面临颠覆。 应用微服务化…...

如何在Vue-Cli中使用Element-UI和Echarts和swiper插件(低版本)

1st.Element-UI 1.1 安装 在终端输入 npm install element-ui 1.2 导入 在全局main.js中全局导入Element-UI&#xff1a; // 导入element-ui组件库 import ElementUI from element-ui; // 导入element-ui组件库的样式 import element-ui/lib/theme-chalk/index.css; // 注…...

[特殊字符]【实战教程】用大模型LLM查询Neo4j图数据库(附完整代码)

&#x1f31f; 核心要点速览 ✅ 基于LangChain框架实现LLM查询Neo4j ✅ 使用Qwen2.5模型(实测Llama3.1查不出内容) ✅ 包含完整数据准备代码实现效果演示 ✅ GitHub/Gitee源码已同步(文末获取) &#x1f6e0;️ 环境准备 1️⃣ 安装Neo4j图数据库 # Windows安装指南参考&…...

Qt获取CPU使用率及内存占用大小

Qt 获取 CPU 使用率及内存占用大小 文章目录 Qt 获取 CPU 使用率及内存占用大小一、简介二、关键函数2.1 获取当前运行程序pid2.2 通过pid获取运行时间2.3 通过pid获取内存大小 三、具体实现五、写在最后 ​ 一、简介 近期在使用软件的过程中发现一个有意思的东西。如下所示&a…...

算法解密:除自身以外数组的乘积问题详解

算法解密:除自身以外数组的乘积问题详解 一、引言 在算法的奇妙旅程中,我们时常会遇到一些看似简单却蕴含深刻智慧的问题,“除自身以外数组的乘积”就是其中之一。这个问题不仅考验我们对数组操作的熟练程度,还要求我们在特定的限制条件下(不能使用除法且时间复杂度为O(n…...

基于Kubernetes的Apache Pulsar云原生架构解析与集群部署指南(上)

#作者&#xff1a;闫乾苓 文章目录 概念和架构概述主要特点消息传递核心概念Pulsar 的消息模型Pulsar 的消息存储与分发Pulsar 的高级特性架构BrokerBookKeeperZooKeeper 概念和架构 概述 Pulsar 是一个多租户、高性能的服务器到服务器消息传递解决方案。Pulsar 最初由雅虎开…...

信创生态核心技术栈:数据库与中间件

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#, Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&#xff1b;擅长C/C、C#等开发语言&#xff0c;熟悉Java常用开…...

CMU-15445(3)——PROJECT#1-BufferPoolManager-Task#1

PROJECT#1-BufferPoolManager 在完成了前面基础的PROJECT#0后&#xff0c;从本节开始才正式进入了CMU-15445的学习&#xff0c;最终目的是构建一个面向磁盘的数据库管理系统。 PROJECT#1 的主要任务是实现数据库管理系统的缓冲池管理器&#xff0c;缓冲池负责在主存缓冲区与持…...

《数据结构初阶》【链式二叉树】

《数据结构初阶》【链式二叉树】 前言&#xff1a;---------------树---------------什么是树&#xff1f;&#x1f4cc;爱心❤小贴士&#xff1a;树与非树&#xff1f;树的基本术语有哪些&#xff1f;关于节点的一些定义&#xff1a;关于树的一些定义&#xff1a;关于森林的定…...

Oracle免费认证来袭

1、Oracle Cloud Infrastructure 2025 Foundations Associate” &#x1f517; 考证地址&#xff1a;https://mylearn.oracle.com/ou/exam-unproctored/oracle-cloud-infrastructure-2025-foundations-associate-1z0-1085-25/148056/241954 2、Oracle Cloud Infrastructure 2…...

Vim 编辑器常用快捷键速查表

Vim 编辑器常用快捷键速查表 Vim 快捷键大全 **1. 基础操作****2. 光标移动****3. 编辑文本****4. 查找替换****5. 分屏操作****6. 可视化模式** **附&#xff1a;Vim 模式切换流程图** 1. 基础操作 快捷键功能说明i进入插入模式&#xff08;光标前&#xff09;a进入插入模式&…...

从父类到子类:C++ 继承的奇妙旅程(1)

前言&#xff1a; 在前文&#xff0c;小编讲述了C模板的进阶内容&#xff0c;下面我们就要结束C初阶的旅行&#xff0c;开始进入C进阶容的旅c程&#xff0c;今天旅程的第一站就是C三大特性之一——继承的旅程&#xff0c;各位扶好扶手&#xff0c;开始我们今天的C继承的奇妙旅程…...

HTML9:页面结构分析

页面结构分析 元素名描述header标题头部区域的内容&#xff08;用于页面或页面中的一块区域&#xff09;footer标记脚部区域的内容&#xff08;用于整个页面或页面的一块区域&#xff09;sectionWeb页面的一块独立区域article独立的文章内容aside相关的内容或应用&#xff08;…...

LabVIEW超声波液位计检定

在工业生产、运输和存储等环节&#xff0c;液位计的应用十分广泛&#xff0c;其中超声波液位计作为非接触式液位测量设备备受青睐。然而&#xff0c;传统立式水槽式液位计检定装置存在受建筑高度影响、量程范围受限、流程耗时长等问题&#xff0c;无法满足大量程超声波液位计的…...

maven 安装 本地 jar

命令&#xff1a; mvn install:install-file -DgroupIdnet.pingfang.application -DartifactIdjna -Dversion5.1.0 -Dpackagingjar -DfileD:\maven\repository1\jna\5.1.0\jna-5.1.0.jarmvn&#xff1a;这是Maven的执行命令。 install:install-file&#xff1a;这是Maven插件目…...

leetcode 141. Linked List Cycle

题目描述&#xff1a; 代码&#xff1a; 用哈希表也可以解决&#xff0c;但真正考察的是用快慢指针法。 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/ class Soluti…...

【Python】通过`Editable Install`模式详解,解决Python开发总是import出错的问题

摘要 田辛老师在很久以前&#xff0c;写过一篇关于Python的模块、包之间的内部关系的博客&#xff0c;叫做【Python】__init__.py 文件详解。 虽然我觉得这篇文章已经足够了&#xff0c; 但是还是有很多朋友碰到开发的过程中import包报错的问题。 今天&#xff0c; 田辛老师想…...

C 语言网络编程问题:E1696 无法打开 源 文件 “sys/socket.h“

#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>在 C 语言网络编程中&#xff0c;上述代码报如下错误 E1696 无法打开 源 文件 "sys/socket.h"E1696 无法打开 源 文件 "netinet/in.h" E1696 无法打开 源 文件…...

操作指南*

任务1: 环境搭建 1.1 创建Spring Boot项目 操作步骤&#xff1a; 使用IDEA创建项目&#xff1a; 打开IDEA → File → New → Project选择 Spring Initializr → 设置项目信息&#xff08;Group、Artifact、Java版本&#xff09;选择依赖&#xff1a;Spring Web、MySQL Drive…...

VRM Add-on for Blender 学习笔记

VRM Add-on for Blender 使用教程-CSDN博客 VRM Add-on for Blender 是 Blender 的一个官方插件&#xff0c;主要用于 导入和导出 VRM 格式的 3D 模型。VRM&#xff08;Virtual Reality Model&#xff09;是一种开放标准的 3D 人形角色模型格式&#xff0c;起源于日本&#xf…...

C++ 完美转发

C 完美转发逐步详解 1. 问题背景与核心目标 在 C 模板编程中&#xff0c;若直接将参数传递给其他函数&#xff0c;参数的 值类别&#xff08;左值/右值&#xff09;和 类型信息&#xff08;如 const&#xff09;可能会丢失。例如&#xff1a; template<typename T> voi…...

学习记录:DAY23

项目开发与学习记录&#xff1a;字段注入优化 前言 我总有一种什么大的要来了的危机感。还是尽快把项目做起来吧&#xff0c;现在全在弄底层的框架。这是一个两天的blog&#xff0c;前一天bug没修好&#xff0c;气到连blog都没写。 日程 5月7日 晚上7点&#xff1a;本来想玩…...

Linux 信号(下篇)

Linux 信号-CSDN博客&#xff08;上篇&#xff09; 前言&#xff1a;在我上一篇博客写到了信号产生的三种条件分别是键盘组合键、kill命令、系统调用接口&#xff1b; 接下来我要把信号产生剩余的两个条件介绍完毕并理解信号的保存&#xff0c;和信号从产生到保存到处理整个过…...

hadoop中的序列化和反序列化(1)

1. 什么是序列化和反序列化 序列化&#xff08;Serialization&#xff09; 是将对象的状态信息转换为可以存储或传输的格式的过程。序列化后的对象可以保存到文件中&#xff0c;或者通过网络传输。 反序列化&#xff08;Deserialization&#xff09; 是序列化的逆过程&#x…...

linux查java进程CPU高的原因

问题&#xff1a;linux查java进程CPU高的原因 解决&#xff1a;用jdk带的工具分析 被查的java最好也使用jdk启动 systemctl启动的注意要去掉PrivateTmptrue /opt/jdk1.8.0_441/bin/jps -l top -Hp 8156 printf "%x" 8533 /opt/jdk1.8.0_441/bin/jstack 8156 |…...

鸿蒙开发——3.ArkTS声明式开发:构建第一个ArkTS应用

鸿蒙开发——3.ArkTS声明式开发:构建第一个ArkTS应用 一、创建ArkTS工程二、ArkTS工程目录结构&#xff08;Stage模型&#xff09;三、构建第一个页面四、构建第二个页面五、实现页面之间的跳转六、模拟器运行 一、创建ArkTS工程 1、若首次打开DevEco Studio&#xff0c;请点击…...

vue3+ts的watch全解!

vue3中的watch只能监听以下四种数据&#xff1a; 1.ref定义的数据 2.reactive定义的数据 3.函数返回一个值&#xff08;getter函数&#xff09; 4.一个包含上述内容的数组 通常我们在使用watch的时候&#xff0c;通常会遇到以下几种情况&#xff1a; 情况一&#xff1a; …...

yarn的概述

1.Yarn的定义 2.Yarn的三大组件 3.Yarn的调度策略 1. YARN的定义 YARN&#xff08;Yet Another Resource Negotiator&#xff09; 是Hadoop生态系统中的一个资源管理框架&#xff0c;用于管理和调度集群中的计算资源。它允许多个应用程序在同一个集群上高效地运行&#xff0c;…...

C++初阶-string类4

目录 1.String operations 1.1string::c_str 1.2string::data 1.3string::copy 1.4string::find 1.5string::rfind 1.6string::find_first_of 1.7string::find_last_of 1.8string::find_first_not_of和string::find_last_not_of find_first_not_of 功能 典型用途 f…...

HarmonyOS NEXT深度解析:自研框架ArkUI-X的技术革命与跨平台实践

HarmonyOS NEXT&#xff5e;深度解析&#xff1a;自研框架ArkUI-X的技术革命与跨平台实践 引言&#xff1a;ArkUI-X的诞生背景与战略意义 在HarmonyOS NEXT全面摒弃AOSP代码的历史性转折点上&#xff0c;华为推出的ArkUI-X框架标志着国产操作系统研发进入深水区。根据华为202…...

CUDA:out of memory的解决方法(实测有效)

一、问题概述 1.问题分析 CUDA out of memory问题通常发生在深度学习训练过程中&#xff0c;当GPU的显存不足以容纳模型、输入数据以及中间计算结果时就会触发。这个问题可能由几个因素引起&#xff1a; 模型和数据规模&#xff1a;深度学习模型尤其是大型模…...

canal mysqltomysql增加同步的库操作

例如增加库 online 1、停止canal.adapter 服务。 ./bin/stop.sh2、备份数据库online&#xff0c;导入目标mysql 备份 mysqldump -h 127.0.0.1 -P 3307 --single-transaction -uroot -p -B online > online.sql导入 mysql -h 127.0.0.1 -P 3308 -uroot -p < onl…...

【AI】模型与权重的基本概念

在 ModelScope 平台上&#xff0c;「模型」和「权重」的定义与工程实践紧密结合&#xff0c;理解它们的区别需要从实际的文件结构和加载逻辑入手。以下是一个典型 ModelScope 模型仓库的组成及其概念解析&#xff1a; 1. ModelScope 模型仓库的典型结构 以 deepseek-ai/deepse…...

k8s 中 deployment 管理的多个 pod 构成集群吗

在 Kubernetes (k8s) 中&#xff0c;通过 Deployment 创建的多个 Pod 本身并不构成一个“集群”&#xff0c;而是属于同一个 工作负载&#xff08;Workload&#xff09; 的多个副本实例。它们的角色是 无状态服务副本&#xff0c;而非独立的集群节点。以下是详细解释&#xff1…...

「动态规划」线性DP:股票问题合集 / LeetCode 121|122|123|188 (C++)

目录 概述 Question1 思路 算法过程 Code 复杂度 Question2 思路 解题过程 Code 复杂度 Question3 思路 解题过程 Code 复杂度 Question4 思路 解题过程 Code 复杂度 总结 概述 我们已经了解过了线性DP&#xff1a; 「动态规划」线性DP&#xff1a;最长…...