【爱上C++】详解string类2:模拟实现、深浅拷贝
在上一篇文章中我们介绍了string类的基本使用,本篇文章我们将讲解string类一些常用的模拟实现,其中有很多细小的知识点值得我们深入学习。Let’s go!
文章目录
- 类声明
- 默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 深浅拷贝问题
- 传统写法
- 现代写法
- 赋值运算符重载
- 传统写法
- 现代写法
- 容器操作
- 获取长度:size
- 获取当前容量:capacity
- 查询是否为空:empty
- 扩容:reserve
- 调整字符串大小:resize
- 字符串访问
- []访问
- 迭代器访问
- 插入类
- 尾插一个字符push_back
- append
- 在尾部追加一个string对象
- 在尾部追加一个C风格字符串
- 在尾部追加n个字符
- insert
- 在pos位置插入一个字符
- 在pos位置插入一个字符串
- operator+=
- 删除类
- erase
- 其他操作
- swap
- find
- 返回字符c在string中第一次出现的位置
- 返回子串s在string中第一次出现的位置
- substr
- printf_str
- clear
- c_str
- 逻辑判断
- 流操作
- 流插入<<
- 流提取>>
- 拓展:关于string其他常用函数
- to_string
- stoi
- 完整代码展示
- .h文件
- .cpp文件
类声明
namespace Mystring
{// 定义一个字符串类class string{public:// 公共成员函数和接口private:// 私有成员变量,限制直接访问底层数据size_t _capacity = 0; // 字符串的容量size_t _size = 0; // 字符串的长度char* _str = nullptr; // 指向字符串数据的指针const static size_t npos = -1; // 静态常量,表示未找到位置或无效位置};
}
在C++中,静态成员变量(static)的定义通常需要在类的外部进行,而非静态成员变量则需要在类的内部进行定义。然而,对于静态成员变量如果其为 const 且为整数类型(包括枚举类型),则可以在类内部直接进行初始化。因此,对于 const static size_t npos = -1; 这样的声明,其允许在类内部直接进行初始化。
对于npos的初始化,下面两种方式都可以
class string {
public:static const size_t npos = -1;
};
class string {
public:static const size_t npos;
};const size_t string::npos = -1;
声明变量时可以顺便初始化,这样可以确保对象在创建时具有合适的初始值。
结构上使用了命名空间Mystring来避免与标准库中的 std::string 冲突。
本篇文章的代码采用声明与定义分离的方式。声明放在string.h文件,定义放在string.cpp文件。.cpp文件中通过包对应头文件以及声明命名空间,然后通过类名::成员的方式定义和实现函数。
默认成员函数
构造函数
声明:
string(const char*str="");
//(提供了一个缺省值表示在没有提供参数时,str 默认初始化为一个空字符串
//(即一个以 null 结尾的字符数组,其中只有一个字符 '\0'))
这里是 "“不是” "。后者不为空, 有一个空格.
定义:
string(const char* str )
{_size = strlen(str); // 计算字符串长度_capacity = _size; // 初始容量与字符串长度相同_str = new char[_capacity + 1];// 为字符串分配内存空间,多开一个空间用于存放 '\0'strcpy(_str, str); // 将参数 str 的内容拷贝到 _str 中
}
如 string::string s1("Hello");
析构函数
~string()
{delete[] _str;// 释放字符串的内存空间,使用 delete[] 因为 _str 是数组形式的字符串_size = 0; // 将字符串长度置为 0,表示字符串已经被释放_capacity = 0;// 将容量置为 0,表示容量无效_str = nullptr; // 将指向字符串数据的指针置为 nullptr,防止出现悬空指针
}
析构函数在对象被销毁时自动调用,通常用来释放对象所持有的资源,例如动态分配的内存。
**拓展:**悬空指针
是指指向已经被释放或者无效的内存地址的指针。当一个指针被赋予了nullptr或者指向的内存已经被释放时,这个指针就变成了悬空指针。
在C++中,如果一个对象的析构函数中没有将指针设置为nullptr,那么当对象被销毁时,其指针成员可能会成为悬空指针。悬空指针引发的问题主要有两个:
- 未定义行为(Undefined Behavior):如果试图通过悬空指针访问内存,则会导致未定义行为,这可能会导致程序崩溃或者产生难以预料的结果。
- 内存泄漏或重复释放:悬空指针可能会导致内存泄漏,因为释放过的内存没有被正确释放,或者在程序的其他地方被重新分配,导致对同一块内存的多次释放。
在编程中,为了避免悬空指针的问题,通常有以下建议:
- 析构函数中将指针置为
nullptr:在对象被销毁时,确保将指针成员设置为nullptr,这样可以避免在对象的生命周期结束后访问悬空指针。- 使用智能指针:C++11引入的智能指针(如
std::unique_ptr和std::shared_ptr)可以帮助自动管理动态内存,避免手动释放内存和悬空指针问题。- 注意指针的生命周期:确保在指针可能成为悬空指针的情况下,适时将其置为
nullptr,或者避免在对象生命周期结束后继续使用该指针。
通过良好的编程实践和注意内存管理,可以有效避免悬空指针带来的问题,提高程序的健壮性和可靠性。
拷贝构造函数
深浅拷贝问题
如果我们不写拷贝构造函数,编译器会默认生成一个浅拷贝的拷贝构造函数。但是,默认生成的拷贝构造函数只会简单地逐成员进行赋值拷贝,这在处理指针成员变量时会导致严重问题
当使用默认的浅拷贝构造函数时,两个对象会共享同一个内存空间,会导致以下问题
- 共享内存:s1 和 s2 共享同一块内存,这意味着修改一个对象会影响另一个对象。
- 悬空指针:当 s1 或 s2 析构时,内存会被释放,另一个对象的指针会变成悬空指针。
- 双重释放:当 s1 和 s2 都析构时,会尝试释放同一块内存两次,导致程序崩溃。
为了解决浅拷贝带来的问题,我们需要实现一个深拷贝的拷贝构造函数。深拷贝会为新对象分配独立的内存空间,并将原对象的数据复制到新对象中,从而避免共享内存的问题。

传统写法
string(const string& s) {// 为新对象分配独立的内存空间,并且多分配一个字节用于存储终止符 '\0'_str = new char[s._capacity + 1];// 将原对象的字符串数据复制到新对象的内存空间strcpy(_str, s._str);// 复制原对象的大小和容量_size = s._size;_capacity = s._capacity;}
现代写法
void swap(string& s){std::swap(_str, s._str);//使用 std::swap 交换当前对象和临时对象的 _str 指针。std::swap(_size, s._size);//使用 std::swap 交换当前对象和临时对象的 _size 值。std::swap(_capacity, s._capacity);//使用 std::swap 交换当前对象和临时对象的 _capacity 值}//s2(s1) //下面的s 就是s1string(const string& s) :_str(nullptr),_size(0),_capacity(0){string tmp(s._str); // 注意!是构造
// 使用 s 对象的内部 C 风格字符串 _str 构造一个临时的字符串对象 tmpswap(tmp);
// 交换当前对象和临时对象的数据,使当前对象的内容变为 tmp 的内容,临时对象则被销毁}
//解析:tmp和s1有一样大的空间,一样的值。然后s2和tmp一交换,那s2就和s1一样了,就完成了。
图解: 交换前
交换后
在C++中,当我们用string s2(s1)来创建string对象时,s1是用来初始化string s2的源对象。现代写法中的string(const string& s)构造函数会被调用来实现这一点。
理解现代写法:
在这个构造函数中,我们可以理解成:
- 调用构造函数:
- 当我们写
string s2(s1)时,编译器调用string类的拷贝构造函数string(const string& s)。 - 这里的
s就是s1,表示用s1对象来初始化新创建的s2对象。
- 当我们写
- 创建临时对象:
- 在构造函数内部,首先使用
s对象(即s1)的内部 C 风格字符串_str来构造一个临时对象tmp。 string tmp(s._str)这行代码会调用另一个构造函数string(const char* str),用s1对象的字符串数据来初始化临时对象tmp。
- 在构造函数内部,首先使用
- 交换数据:
- 调用
swap(tmp)将当前对象(即s2)的成员变量与临时对象tmp的成员变量进行交换。 - 在交换之后,
s2对象持有了tmp的数据,即持有了s1的数据副本,而tmp则持有了s2的初始数据(在这时通常为空或者默认值)。
- 调用
- 析构临时对象:
- 当构造函数结束时,临时对象
tmp离开作用域,自动析构,释放它持有的资源。 - 由于
tmp持有的是s2的初始数据(在构造时通常是无效数据),所以释放时不会影响s2,也不会造成资源泄漏。
- 当构造函数结束时,临时对象
这种现代写法通过创建临时对象和交换数据,确保了拷贝构造的简洁性和异常安全性,同时避免了资源泄漏和浅拷贝带来的问题。
❓为什么要在初始化列表中给 _str 初始化为空指针?
string(const string& s)
: _str(nullptr)
如果不对它进行处理,一开始指向的是 未定义的(随机值)。在交换之后,这个随机值就给了tmp了,tmp出了作用域后调用析构函数进行释放会对随机值指向的空间进行释放。 这种情况下,系统可能无法正确处理释放操作,从而导致程序崩溃或者其他未定义行为。
delete 或者 free 一个空指针是安全的操作,不会导致运行时错误,所以这里把它初始化为nullptr,tmp最后释放空,不会出现问题。
赋值运算符重载
传统写法
string& operator=(const string& s)
{if (this != &s) // 防止自我赋值{char* tmp = new char[s._capacity + 1]; // 为临时存储空间分配内存,大小为 s 对象的容量加一(用于存放字符串末尾的 '\0')strcpy(tmp, s._str); // 将 s 对象的字符串复制到临时存储空间 tmpdelete[] _str; // 删除当前对象已有的字符串内存_str = tmp; // 将当前对象的 _str 指向新分配的字符串内存_size = s._size; // 更新当前对象的字符串长度_capacity = s._capacity; // 更新当前对象的容量}return *this; // 返回当前对象的引用,支持连续赋值操作
}
传统写法图解:
现代写法
//s1=s3
string& operator=(string s) // 使用传值方式传入参数 s,利用了移动语义
{swap(s); // 使用交换函数进行赋值操作,此时 s 是通过拷贝构造函数传入的临时对象return *this; // 返回当前对象的引用,支持连续赋值操作
}
交换前
交换后
在 string& operator=(string s) 中使用传值传参主要有以下几个原因:
- 移动语义的利用:
- 传值传参允许编译器在需要的时候使用移动语义,这样可以避免不必要的深拷贝,提升性能。
- 如果传递的参数是右值(例如,
s1 = std::move(s3)),则会调用移动构造函数而不是拷贝构造函数,从而避免了数据的复制。
- 简化代码:
- 通过传值传参,可以在函数体内直接交换当前对象和参数对象的数据。这使得代码更简洁,并且更容易理解和维护。
- 异常安全性:
- 传值传参结合交换操作可以确保资源的正确释放,避免资源泄漏和其他异常问题。
详细过程解释
假设我们有以下赋值操作:s1 = s3;
- 传值传参:
string s(s3); // 临时对象 s 通过拷贝构造或移动构造函数创建
- 当调用
s1 = s3;时,会创建一个临时对象s。这个临时对象s是通过拷贝构造函数(如果s3是左值)或移动构造函数(如果s3是右值)创建的。
- 交换操作:
swap(s); // 交换 s1 和 s 的数据
- 在赋值运算符的实现中,调用
swap(s);。这会交换当前对象s1和临时对象s的内部数据指针。
- 临时对象销毁:
// 临时对象 s 离开作用域,被销毁,释放旧资源
- 在赋值运算符函数结束时,临时对象
s离开作用域并被销毁,其析构函数会释放它所持有的资源。这些资源实际上是原来属于s1的旧资源。
- 返回当前对象:
return *this; // 返回当前对象的引用
- 返回当前对象
s1的引用,以支持连续赋值操作。
容器操作
获取长度:size
size_t size() const
//考虑到不需要修改,我们加上 const。{return _size;}
获取当前容量:capacity
size_t capacity() const{return _capacity;}
查询是否为空:empty
bool empty() const{return _size == 0;}
扩容:reserve
void reserve(size_t n)
{// 如果请求的容量大于当前的容量,才需要重新分配内存if (n > _capacity){char* tmp = new char[n + 1]; // 分配新的内存空间,比请求的容量多一个字符// 这个额外的字符用于存放字符串结尾的空字符 '\0',确保字符串的有效性和正确性strcpy(tmp, _str); // 将原字符串内容拷贝到新内存,也会拷贝结尾的 '\0'delete[] _str; // 释放原来的内存_str = tmp; // 更新指针,使其指向新的内存_capacity = n; // 更新容量}
}
扩容扩容,所以n要≥_capacity
调整字符串大小:resize
记得用缺省值,用户在调用 resize 函数时可以选择性地提供第二个参数.
假如有一个字符串对象 str,当前大小为 5,内容为 “hello”,容量为 10。调用 str.resize(8, ‘x’) 后 就是 helloxxx\0
声明void resize(size_t, char c = '\0');
定义
void resize(size_t n, char c)
{// 如果新的大小大于当前大小,需要扩展字符串if (n > _size){// 如果新的大小大于当前容量,需要扩展内存if (n > _capacity){reserve(n); // 调用 reserve 函数扩展容量}// 将新的字符填充到扩展后的字符串中for (size_t i = _size; i < n; i++){_str[i] = c;}}else if (n < _size){// 如果新的大小小于当前大小,只需更新大小_size = n; // 注意:此时容量不会改变}// 更新字符串的实际大小,并确保字符串以空字符结尾_str[_size] = '\0';
}
缩容就直接在下标为n的位置设置为\0即可。
字符串访问
[]访问
//仅能访问const char& operator[](size_t pos) const{assert(pos < _size);//assert 括号里为假的时候才会报错return _str[pos];}//访问+修改char& operator[](size_t pos){assert(pos <= _size);return _str[pos];}
迭代器访问
迭代器在 C++ 中常常被描述为类似指针的对象,它提供了对容器(比如字符串)中元素的访问和操作。
对于模拟实现的字符串类,我们可以直接使用原生指针来作为迭代器,通过 typedef 进行重命名,这样就可以在类中直接使用迭代器。
首先,我们使用 typedef 将指针重命名为迭代器,同时定义了常量迭代器:typedef char* iterator;typedef const char* const_iterator;
// 返回字符串的起始位置iterator begin() {return _str;}// 返回字符串的结束位置'\0' 的下一个位置(即 null 字符的位置)iterator end() {return _str + _size;}// 返回字符串的起始位置(const 版本,不能修改数据)
//常量成员函数
//const_iterator begin() const 和 const_iterator end() const 被声明为常量成员函数。
//这意味着它们不会修改对象的任何成员变量,并且它们可以被常量对象调用。//为什么需要最右边的const???
//如果没有最后的 const 修饰符,编译器将认为 begin() 和 end() 可能会修改对象。因此,当你试图在一个常量对象上调用这些函数时,会产生编译错误,因为编译器不允许通过常量对象调用非常量成员函数。const_iterator begin() const {return _str;}// 返回字符串的结束位置的下一个位置(const 版本)const_iterator end() const {return _str + _size;}
这些函数使得我们可以像操作指针一样操作迭代器,比如使用 ++ 和 – 来移动迭代器指向的位置,或者使用 * 来访问迭代器指向的元素。这样,我们就可以通过迭代器来遍历字符串中的字符了。
插入类
尾插一个字符push_back
void push_back(char ch){if (_size == _capacity)//大小和总容量一样的时候,说明不够用了{size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacity);}_str[_size] = ch;_size++;_str[_size] = '\0';//一定要处理好\0}
append
在尾部追加一个string对象
string& append(const string& str)
{// 检查是否需要扩展容量if (str._size >= _capacity - _size) // 判断是否需要扩容{reserve(_capacity + str._size + 1); // 扩容并预留足够空间}// 复制传入的字符串到当前字符串的末尾strcpy(_str + _size, str._str); // _str + _size 是当前字符串尾部// 更新当前字符串的大小_size += str._size; // 更新_size// 手动设置字符串的结尾_str[_size] = '\0'; // 手动设置字符串尾部的\0// 返回当前对象的引用return *this; // 返回string对象
}
在尾部追加一个C风格字符串
void append(const char* str) //注意传的是指针{ size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//char *strcpy(char *dest, const char *src); strcat(_str,str)也行,但是效率不行strcpy(_str + _size, str);_size += len;}
在尾部追加n个字符
void append(size_t n, char ch){// 检查是否需要扩展容量if (_size + n > _capacity){reserve(_size + n); // 扩展容量以容纳新字符}// 将字符 ch 追加 n 次到字符串末尾for (size_t i = 0; i < n; i++){_str[_size + i] = ch;}// 更新字符串的大小_size += n;// 确保字符串以 '\0' 结尾_str[_size] = '\0';}
注意:_size和_capacity是不计算\0的
insert
在pos位置插入一个字符
在 C++ 中,通常情况下,字符串的位置索引 pos 是从 0 开始的,即第一个字符的位置为 0,第二个为 1,依此类推。这种习惯是因为 C++ 中的数组和字符串的索引都是从 0 开始计数的。
_str 表示字符串的起始位置,即第一个字符的地址。
_str + 1 表示字符串中第二个字符的地址。
_str + pos 表示字符串中第 pos 个位置的地址,即要进行插入或其他操作的位置。
void insert(size_t pos, char ch)
{// 确保插入位置在有效范围内assert(pos <= _size); // pos 等于 _size 时表示尾插// 检查是否需要扩展容量if (_size == _capacity) // 大小和总容量一样的时候,说明不够用了{size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; // 扩展容量,最小扩展到 4reserve(newCapacity); // 调用 reserve 函数扩展容量}// 从后往前 移动数据以腾出插入位置int end = _size;while (end >= (int)pos) // 循环直到 end 小于 pos{_str[end + 1] = _str[end]; // 将当前位置的数据向后移动一位--end; // end 减 1}// 在指定位置插入新字符_str[pos] = ch; // 在 pos 位置插入字符 ch_size++; // 更新大小_str[_size] = '\0'; // 确保字符串以 '\0' 结尾
}
为什么 while (end >= (int)pos) 要强制转换成int类型呢?
因为end 是一个 size_t 类型的变量,这是一个无符号整数类型。pos 也是 size_t 类型。如果直接比较 end 和 pos,即使 end 被减到负值,由于 size_t 是无符号类型,负值会被当成一个非常大的正整数。这可能会导致无限循环和访问越界。
通过将 pos 强制转换为 int,确保 end 和 pos 在比较时都是有符号整数类型,从而避免了无符号整数类型转换的问题。这种做法保证了在 end 小于 pos 时,循环能正确退出。
在pos位置插入一个字符串
void insert(size_t pos, const char* str)
{// 确保插入位置在当前字符串长度范围内assert(pos <= _size);// 计算要插入字符串的长度size_t len = strlen(str);// 如果当前容量不足以容纳插入后的新字符串,则增加容量if (_size + len > _capacity){reserve(_size + len); // 调用 reserve 函数扩展容量}// 使用有符号整数类型的 end 变量,以避免无符号整数类型带来的潜在问题int end = _size;// 从字符串末尾向前移动字符,以腾出插入位置while (end >= (int)pos){_str[end + len] = _str[end]; // 将当前位置的数据向后移动 len 位--end; // end 减 1}// 将新的字符串插入到指定位置strncpy(_str + pos, str, len); // 使用 strncpy 复制字符串内容,但不包括末尾的 '\0'// 更新字符串的大小_size += len; // 新字符串的长度增加_str[_size] = '\0'; // 确保字符串以 '\0' 结尾
}
operator+=
string& operator+=(char ch)//+=一个字符{push_back(ch);return *this;}string& operator+=(const char* str)//+=一个 char* 字符串{append(str);return *this;}string& operator+= (const string& str) //+=一个string对象{append(str);return *this;}
删除类
erase
从pos位置开始,删除长度为len的字符串。若未给出len,则默认删完.void erase(size_t pos, size_t len = npos);
void erase(size_t pos,size_t len) //pos 是下标,删除1个就是pos位置的那个{//assert(pos <_size);// xxxx size=4,//assert(_size > 0);assert(pos < _size); // 这里不需要检查 pos >= 0,因为 pos 是无符号类型if (len == npos||pos+len>=_size)//要删完{//但我们不用删,直接缩大小,_str[pos] = '\0';_size = pos;}else {//后面数据挪过去覆盖// hello,wordl// ↑ ↑:pos+len// pos 删3个strcpy(_str + pos, _str + pos + len);_size -= len;//覆盖之后减少_size即可}//"abcdefghi"。假如pos是3,len是4。pos是下标//_str 指向字符串的第一个字符,即 'a'。//_str + pos 指向字符串的第 4 个字符,即 'd'。//_str + pos + len 指向字符串的第 8 个字符,即 'h'。}
其他操作
swap
尽管标准库中的 std::swap 可以用于交换两个对象,但是它仅在你提供的交换操作对你特定类的成员变量的交换上不能直接进行。
标准库的 std::swap 无法直接处理类的私有成员变量的交换,而必须通过类提供的接口进行交换操作。
所以 自定义类型要自己写,上面的拷贝构造和赋值重载的现代写法都用到了此处的swap函数
void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
find
返回字符c在string中第一次出现的位置
size_t find(char ch,size_t pos=0)
size_t find(char ch,size_t pos)//半缺省{for (size_t i =pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}
返回子串s在string中第一次出现的位置
size_t find(const char* str,size_t pos=0);
size_t find(const char* str, size_t pos )
{// 使用 strstr 函数从 _str + pos 位置开始查找子字符串 strconst char* ptr = strstr(_str + pos, str);// 如果 ptr 为空,表示没有找到子字符串if (ptr == nullptr){// 返回 npos 表示查找失败return npos;}else{// 返回子字符串在字符串中的起始位置return ptr - _str;}
}
strstr 是 C 语言标准库 (或 <string.h>)中的函数,用于在一个字符串中查找第一次出现另一个字符串的位置。
constchar* strstr(constchar* str1, constchar* str2);
- str1:要在其中搜索的主字符串。
- str2:要搜索的子字符串。
- 如果 str2 是 str1 的子串,则返回指向 str1 中第一次出现 str2 的位置的指针。
- 如果 str2 不是 str1 的子串,则返回 nullptr。
substr
从当前字符串中提取子串。 string substr(size_t pos = 0, size_t len = npos);
string substr(size_t pos , size_t len ){assert(pos < _size);size_t end = pos + len;if (len == npos || pos + len >= _size){end = _size;}string str;str.reserve(end - pos);for (size_t i = pos; i < end; i++){str += _str[i];}return str;//返回str的拷贝}
printf_str
打印C风格字符串。C风格字符串是以空字符 ‘\0’ 结尾的字符数组
void printf_str(const string& s)//权限放大,上面的末尾要加const
//在C++中,成员函数末尾的const关键字用于指示该函数不会修改对象的状态。这种函数被称为const成员函数,它们对于保证对象的不可变性非常重要。
//c_str() 和 size() 都是访问器函数,它们不会修改字符串对象的内容,因此应该声明为const成员函数以确保它们可以在const对象上调用。
//这样做不仅符合面向对象的设计理念,还允许用户在const对象上调用这些函数,以便于在const上下文中使用你的类。
//而对于const char& operator[](size_t pos) const,它是一个重载的下标运算符,用于访问字符串中指定位置的字符。由于该函数不会修改对象的内容,因此也应该声明为const成员函数。{for (size_t i = 0; i < s.size(); i++){// s[i]++; 参数加const 就是为了防止这里进行修改。cout << s[i] << " ";}cout << endl;}
clear
清空当前字符串对象,使其变为空字符串
void clear(){_size = 0;_str[0] = '\0';//}
c_str
获取字符串源指针
有些场景下,例如使用C语言的字符串操作函数,处理字符串时只能使用char*指针去传参,string为了兼容C字符串操作函数,支持获取字符串源指针,为了不破坏string的数据结构,这个返回的源字符串指针不支持修改,只能访问内容!
这个函数非常短小,直接在类中实现!
const char* c_str() const {assert(_str);return _str;}
逻辑判断
实现了小于和等于,其他的直接复用.
都被声明为 const 成员函数。 const 关键字的作用是告诉编译器这些成员函数不会修改类的成员变量 _str 和 _size。
在 C++ 中,类的 const 成员函数可以确保在函数内部不会修改对象的任何成员变量,从而提供了对调用者的额外保证。这样的设计有助于代码的可维护性和可理解性。
如果没有将比较操作符声明为 const,则无法在常量对象上调用这些操作符,因为常量对象只能调用 const 成员函数。例如,对于声明为 const 的对象或者在常量上下文中使用的对象(如 const String s1, s2;),可以正常地执行比较操作。
bool operator<(const string& s) const //小于
{return (strcmp(_str, s._str) < 0);
}bool operator==(const string& s) const //等于
{return (strcmp(_str, s._str) == 0);
}bool operator<=(const string& s) const //小于等于
{return (*this < s) || (*this == s);
}bool operator>(const string& s) const //大于
{return !(*this <= s);
}bool operator>=(const string& s) const //大于等于
{return !(*this < s);
}bool operator!=(const string& s) const //不等
{return !(*this == s);
}
流操作
当我们在 C++ 中定义流插入运算符 << 和流提取运算符 >> 时,如果将它们定义为类的成员函数,会遇到一个问题:类的成员函数默认会有一个隐含的 this 指针作为第一个参数。这样的话,如果我们试图将 operator<< 或 operator>> 定义为成员函数,形式上会与预期不符,因为它们需要接受两个参数(左操作数和右操作数),而类成员函数形式下只能接受一个参数(除非将其定义为静态成员函数,但这不符合重载运算符的惯用方式)。
因此,为了正确地重载这些运算符,我们将它们定义为类的友元函数。友元函数可以在不通过对象接口(即不使用 this 指针)的情况下访问类的私有成员和受保护成员。这种做法不仅符合语法要求,还能保持类的封装性和安全性,因为只有特定的函数(即声明为友元的函数)才能直接访问类的私有部分。
流插入<<
ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;//返回ostream对象 以支持cout<<s1<<s2<<s3}
流提取>>
istream& operator >>(istream& in, string& s){s.clear(); // 清空当前字符串,以免变成尾插了char buff[128] = {0}; // 创建一个缓冲区用于暂存读取的字符序列char ch = in.get(); // 从输入流中读取一个字符int i = 0; // 初始化缓冲区索引// 循环读取字符直到遇到空格或换行符while (ch != ' ' && ch != '\n'){buff[i++] = ch; // 将读取的字符存储到缓冲区中if (i == 127) // 如果缓冲区即将满了{buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符s += buff; // 将缓冲区中的字符序列插入到字符串对象中i = 0; // 重置缓冲区索引}ch = in.get(); // 读取下一个字符}// 处理剩余的字符序列if (i > 0){buff[i] = '\0'; // 在缓冲区末尾添加字符串终止符s += buff; // 将缓冲区中的字符序列插入到字符串对象中}return in; // 返回输入流对象的引用}
拓展:关于string其他常用函数
to_string
to_string 是 C++ 中的一个标准库函数,用于将各种类型的数据转换为对应的字符串表示形式。
头文件:#include<string>
语法:std::string to_string(类型 value); 类型可以是整数、浮点数。value: 要转换为字符串的数值。 返回转换后的 std::string 类型对象,表示数值的字符串形式。 
stoi
stoi 是 C++ 中的一个标准库函数,用于将字符串转换为对应的整数类型。
头文件:#include<string>int stoi(const std::string& str, size_t* pos = 0, int base = 10);
- str: 要转换的字符串。
- pos (可选): 指向 size_t 类型的指针,用于存储第一个无效字符的索引。
- base (可选): 数字的基数,默认为 10。
返回:
- 返回转换后的整数值。

完整代码展示
.h文件
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
using namespace std;
namespace Mystring
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}size_t size() const{return _size;}size_t capacity(){return _capacity;}bool empty() const{return _size == 0;}void printf_str(const string& s){for (size_t i = 0; i < s.size(); i++){cout << s[i] << " ";}cout << endl;}void clear(){_size = 0;_str[0] = '\0';//}const char* c_str() const{assert(_str);return _str;}string(const char* str = "");~string();string(const string& s);string& operator=(string s);void reserve(size_t n);void resize(size_t, char c = '\0');const char& operator[](size_t pos) const;char& operator[](size_t pos);void push_back(char ch);void append(const char* str);void append(size_t n, char ch);string& append(const string& str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);string& operator+=(const char* str);string& operator+=(char ch);string& operator+= (const string& str);void erase(size_t pos, size_t len = npos);void swap(string& s);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);string substr(size_t pos = 0, size_t len = npos);bool operator<(const string& s) const //小于{return (strcmp(_str, s._str) < 0);}bool operator==(const string& s) const //等于{return (strcmp(_str, s._str) == 0);}bool operator<=(const string& s) const //小于等于{return (*this < s) || (*this == s);}bool operator>(const string& s) const //大于{return !(*this <= s);}bool operator>=(const string& s) const //大于等于{return !(*this < s);}bool operator!=(const string& s) const //不等{return !(*this == s);}private:size_t _capacity = 0;size_t _size = 0;char* _str = nullptr;const static size_t npos = -1;};istream& operator>>(istream& in, string& s);ostream& operator<<(ostream& out, const string& s);
}
.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
//这个是声明和定义分离的版本
#include"string16.h"namespace Mystring
{//构造函数string::string(const char* str){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}//析构函数string::~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}//拷贝构造 现代写法string::string(const string& s){string tmp(s._str);swap(tmp);}//运算符重载string& string::operator=(string s){swap(s);return *this;}//仅能访问const char& string::operator[](size_t pos) const{assert(pos < _size);//assert 括号里为假的时候才会报错return _str[pos];}//访问+修改char& string::operator[](size_t pos){assert(pos <= _size);return _str[pos];}void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void string::resize(size_t n, char c){if (n > _size){if (n > _capacity){reserve(n); }for (size_t i = _size; i < n; i++){_str[i] = c;}}else if (n < _size){_size = n;}_str[_size] = '\0';}void string::push_back(char ch){if (_size == _capacity){size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacity);}_str[_size] = ch;_size++;_str[_size] = '\0';}void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;}string& string::append(const string& str){// 检查是否需要扩展容量if (str._size >= _capacity - _size) // 判断是否需要扩容{reserve(_capacity + str._size + 1); // 扩容并预留足够空间}// 复制传入的字符串到当前字符串的末尾strcpy(_str + _size, str._str); // _str + _size 是当前字符串尾部// 更新当前字符串的大小_size += str._size; // 更新_size// 手动设置字符串的结尾_str[_size] = '\0'; // 手动设置字符串尾部的\0// 返回当前对象的引用return *this; // 返回string对象}void string::append(size_t n, char ch){// 检查是否需要扩展容量if (_size + n > _capacity){reserve(_size + n); // 扩展容量以容纳新字符}// 将字符 ch 追加 n 次到字符串末尾for (size_t i = 0; i < n; i++){_str[_size + i] = ch;}// 更新字符串的大小_size += n;// 确保字符串以 '\0' 结尾_str[_size] = '\0';}void string::insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newCapacity);}/*int end = _size;while (end >= (int)pos){_str[end + 1] = _str[end];--end;}*/size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;_size++;}void string::insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];--end;}strncpy(_str + pos, str, len);_size += len;}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}void string::erase(size_t pos, size_t len){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}}void string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}size_t string::find(char ch, size_t pos){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}size_t string::find(const char* str, size_t pos){const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}}string string::substr(size_t pos, size_t len){assert(pos < _size);size_t end = pos + len;if (len == npos || pos + len >= _size){end = _size;}string str;str.reserve(end - pos);for (size_t i = pos; i < end; i++){str += _str[i];}return str;}ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();char buff[128] = { 0 };char ch = in.get();int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
}

- 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
- 本人也很想知道这些错误,恳望读者批评指正!
相关文章:
【爱上C++】详解string类2:模拟实现、深浅拷贝
在上一篇文章中我们介绍了string类的基本使用,本篇文章我们将讲解string类一些常用的模拟实现,其中有很多细小的知识点值得我们深入学习。Let’s go! 文章目录 类声明默认成员函数构造函数析构函数拷贝构造函数深浅拷贝问题传统写法现代写法…...
狄克斯特拉算法
狄克斯特拉算法(Dijkstra’s algorithm)是一种用于在带权图中找到从单一源点到所有其他顶点的最短路径的算法。它适用于处理带有非负权值的图。 下面将详细解释算法的工作原理、时间复杂度以及如何通过优化数据结构来改进其性能。 狄克斯特拉算法的工作…...
2024推荐整理几个磁力导航网站可提供海量资源的
都2024现在网上找资源像流水得鱼一样,抓一大把结果很难吃,我通宵特意整理的网站,网上有许多磁力导航网站可以提供海量的磁力链接资源,以下是一些有效的磁力导航网站推荐: 磁力搜索 链接: 资源类型&#x…...
链式访问:C语言中的函数调用技巧
链式访问:C语言中的函数调用技巧 在C语言编程中,链式访问(chained calls)是一个常见的编程技巧,它允许你在一行代码中连续调用多个函数或方法。这种技巧不仅能够让代码更加简洁和易读,还能减少临时变量的使…...
数据库设计(实战项目)-1个手机号多用户身份
一. 背景: 该需求是一个互联网医院的预约单场景,护士在小程序上申请患者查房预约单,医生在小程序上对预约单进行接单,护士开始查房后填写查房小结,客户需要对用户信息进行授权,医生查房后进行签字ÿ…...
vue+fineReport 使用前端搜索+报表显示数据
--fineReprot 将需要搜索的参数添加到模版参数 sql: --前端传递参数 注:因为每次点击搜索的结果需要不一样,还要传递一个时间戳的参数: let timesamp new Date().getTime()...
高阶面试-存储系统的设计
概述 分类 块存储 block storage文件存储 file storage对象存储 object storage 区别: 块存储 概述 位于最底层,块,是物理存储设备上数据存储的最小单位。硬盘(Hard Disk Drive,HDD)就属于块存储。常见的还有固态硬盘(SSD)、…...
柔性测斜仪:土木工程与地质监测的得力助手
在现代土木工程和地质工程领域,精确监测土壤和岩石的位移情况对于确保工程安全至关重要。柔性测斜仪作为一种高精度、稳定性和灵活性兼备的测量设备,已逐渐成为工程师和研究人员的得力助手。本文将深入探讨柔性测斜仪在多个关键领域的应用及其重要性。 点…...
数字资产和数据资产你真的了解吗?
数据作为新型生产要素,是数字化、网络化、智能化的基础,已快速融入生产、分配、流通、消费和社会服务管理等各环节,深刻改变着生产方式、生活方式和社会治理方式。 何为数据资产?即由个人或企业拥有或控制的,能为企业带…...
【每日一练】python运算符
1. 算术运算符 编写一个Python程序,要求用户输入两个数,并执行以下运算:加法、减法、乘法、求余、除法、以及第一个数的第二个数次方。将结果打印出来。 a input("请输入第一个数:") b input("请输入第二个数&…...
CesiumJS【Basic】- #032 绘制虚线(Primitive方式)
文章目录 绘制虚线(Primitive方式)1 目标2 代码2.1 main.ts绘制虚线(Primitive方式) 1 目标 使用Primitive方式绘制虚线 2 代码 2.1 main.ts // 定义线条的起点和终点var start = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883)...
海尔智家:科技优秀是一种习惯
海尔智家:科技优秀是一种习惯 2024-06-28 15:19代锡海 6月24日,2023年度国家科学技术奖正式揭晓。海尔智家“温湿氧磁多维精准控制家用保鲜电器技术创新与产业化”项目荣获国家科学技术进步奖,成为家电行业唯一牵头获奖企业。 很多人说&…...
【Android】实现图片和视频混合轮播(无限循环、视频自动播放)
目录 前言一、实现效果二、具体实现1. 导入依赖2. 布局3. Banner基础配置4. Banner无限循环机制5. 轮播适配器6. 视频播放处理7. 完整源码 总结 前言 我们日常的需求基本上都是图片的轮播,而在一些特殊需求,例如用于展览的的数据大屏,又想展…...
VLAN基础
一、什么是Vlan VLAN(Virtual Local Area Network)是虚拟局域网的简称,是一种将单一物理局域网(LAN)在逻辑层面上划分为多个独立的广播域的技术。每个VLAN都是一个独立的广播域,其内部主机可以直接通信&am…...
pytest-yaml-sanmu(五):跳过执行和预期失败
除了手动注册标记之外,pytest 还内置了一些标记可直接使用,每种内置标记都会用例带来不同的特殊效果,本文先介绍 3 种。 1. skip skip 标记通常用于忽略暂时无法执行,或不需要执行的用例。 pytest 在执行用例时,如果…...
linux指令整合(centos系统持续更新中。。。)
1、查询java进程 ps -ef|grep java 2、查询端口占用 lsof -i:端口号 3、 启动java程序 java -jar jar包路径 后台启动 nohup java -jar jar包路径 -Xms512m -Xmx512m > 日志路径 2>&1 & 4、查看服务器资源占用 top 5、关闭进程 kill -9 进程号...
个人开发实现AI套壳网站快速搭建(Vue+elementUI+SpringBoot)
目录 一、效果展示 二、项目概述 三、手把手快速搭建实现本项目 3.1 前端实现 3.2 后端方向 五、后续开发计划 一、效果展示 默认展示 一般对话展示: 代码对话展示: 二、项目概述 本项目是一个基于Web的智能对话服务平台,通过后端与第…...
Cesium与Three相机同步(3)
Cesium与Three融合的案例demo <!DOCTYPE html> <html lang"en" class"dark"><head><meta charset"UTF-8"><link rel"icon" href"/favicon.ico"><meta name"viewport" content&q…...
PMP考试报名项目经历怎么填写?指引请收好
PMP,这一全球公认的项目管理金牌认证,不仅是对项目管理能力的认可,更是职业生涯中的一大助力。然而,在报名PMP时,很多小伙伴都面临一个共同的难题:如何书写项目经验?今天,就让我们一…...
Git的基本使用方法
Git的基本使用方法 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨Git的基本使用方法,Git作为目前最流行的版本控制系统之一&…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
yaml读取写入常见错误 (‘cannot represent an object‘, 117)
错误一:yaml.representer.RepresenterError: (‘cannot represent an object’, 117) 出现这个问题一直没找到原因,后面把yaml.safe_dump直接替换成yaml.dump,确实能保存,但出现乱码: 放弃yaml.dump,又切…...
虚幻基础:角色旋转
能帮到你的话,就给个赞吧 😘 文章目录 移动组件使用控制器所需旋转:组件 使用 控制器旋转将旋转朝向运动:组件 使用 移动方向旋转 控制器旋转和移动旋转 缺点移动旋转:必须移动才能旋转,不移动不旋转控制器…...
【技巧】dify前端源代码修改第一弹-增加tab页
回到目录 【技巧】dify前端源代码修改第一弹-增加tab页 尝试修改dify的前端源代码,在知识库增加一个tab页"HELLO WORLD",完成后的效果如下 [gif01] 1. 前端代码进入调试模式 参考 【部署】win10的wsl环境下启动dify的web前端服务 启动调试…...
数据挖掘是什么?数据挖掘技术有哪些?
目录 一、数据挖掘是什么 二、常见的数据挖掘技术 1. 关联规则挖掘 2. 分类算法 3. 聚类分析 4. 回归分析 三、数据挖掘的应用领域 1. 商业领域 2. 医疗领域 3. 金融领域 4. 其他领域 四、数据挖掘面临的挑战和未来趋势 1. 面临的挑战 2. 未来趋势 五、总结 数据…...
