【爱上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,那么当对象被销毁时,其指针成员可能会成为悬空指针。悬空指针引发的问题主要有两个:

  1. 未定义行为(Undefined Behavior):如果试图通过悬空指针访问内存,则会导致未定义行为,这可能会导致程序崩溃或者产生难以预料的结果。
  2. 内存泄漏或重复释放:悬空指针可能会导致内存泄漏,因为释放过的内存没有被正确释放,或者在程序的其他地方被重新分配,导致对同一块内存的多次释放。

在编程中,为了避免悬空指针的问题,通常有以下建议:

  • 析构函数中将指针置为 nullptr:在对象被销毁时,确保将指针成员设置为nullptr,这样可以避免在对象的生命周期结束后访问悬空指针。
  • 使用智能指针:C++11引入的智能指针(如std::unique_ptrstd::shared_ptr)可以帮助自动管理动态内存,避免手动释放内存和悬空指针问题。
  • 注意指针的生命周期:确保在指针可能成为悬空指针的情况下,适时将其置为nullptr,或者避免在对象生命周期结束后继续使用该指针。

通过良好的编程实践和注意内存管理,可以有效避免悬空指针带来的问题,提高程序的健壮性和可靠性。

拷贝构造函数

深浅拷贝问题

如果我们不写拷贝构造函数,编译器会默认生成一个浅拷贝的拷贝构造函数。但是,默认生成的拷贝构造函数只会简单地逐成员进行赋值拷贝,这在处理指针成员变量时会导致严重问题
当使用默认的浅拷贝构造函数时,两个对象会共享同一个内存空间,会导致以下问题
image.png

  • 共享内存:s1 和 s2 共享同一块内存,这意味着修改一个对象会影响另一个对象。
  • 悬空指针:当 s1 或 s2 析构时,内存会被释放,另一个对象的指针会变成悬空指针。
  • 双重释放:当 s1 和 s2 都析构时,会尝试释放同一块内存两次,导致程序崩溃。

为了解决浅拷贝带来的问题,我们需要实现一个深拷贝的拷贝构造函数。深拷贝会为新对象分配独立的内存空间,并将原对象的数据复制到新对象中,从而避免共享内存的问题。

image.png

传统写法

    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一样了,就完成了。

图解: 交换前
image.png
交换后
image.png
在C++中,当我们用string s2(s1)来创建string对象时,s1是用来初始化string s2的源对象。现代写法中的string(const string& s)构造函数会被调用来实现这一点。
理解现代写法:
在这个构造函数中,我们可以理解成:

  1. 调用构造函数
    • 当我们写string s2(s1)时,编译器调用string类的拷贝构造函数string(const string& s)
    • 这里的s就是s1,表示用s1对象来初始化新创建的s2对象。
  2. 创建临时对象
    • 在构造函数内部,首先使用s对象(即s1)的内部 C 风格字符串_str来构造一个临时对象tmp
    • string tmp(s._str)这行代码会调用另一个构造函数string(const char* str),用s1对象的字符串数据来初始化临时对象tmp
  3. 交换数据
    • 调用swap(tmp)将当前对象(即s2)的成员变量与临时对象tmp的成员变量进行交换。
    • 在交换之后,s2对象持有了tmp的数据,即持有了s1的数据副本,而tmp则持有了s2的初始数据(在这时通常为空或者默认值)。
  4. 析构临时对象
    • 当构造函数结束时,临时对象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; // 返回当前对象的引用,支持连续赋值操作
}

传统写法图解:
image.png

现代写法

//s1=s3
string& operator=(string s) // 使用传值方式传入参数 s,利用了移动语义
{swap(s); // 使用交换函数进行赋值操作,此时 s 是通过拷贝构造函数传入的临时对象return *this; // 返回当前对象的引用,支持连续赋值操作
}

交换前
image.png
交换后
image.png
string& operator=(string s) 中使用传值传参主要有以下几个原因:

  1. 移动语义的利用
    • 传值传参允许编译器在需要的时候使用移动语义,这样可以避免不必要的深拷贝,提升性能。
    • 如果传递的参数是右值(例如,s1 = std::move(s3)),则会调用移动构造函数而不是拷贝构造函数,从而避免了数据的复制。
  2. 简化代码
    • 通过传值传参,可以在函数体内直接交换当前对象和参数对象的数据。这使得代码更简洁,并且更容易理解和维护。
  3. 异常安全性
    • 传值传参结合交换操作可以确保资源的正确释放,避免资源泄漏和其他异常问题。

详细过程解释
假设我们有以下赋值操作:s1 = s3;

  1. 传值传参
string s(s3); // 临时对象 s 通过拷贝构造或移动构造函数创建
  • 当调用s1 = s3;时,会创建一个临时对象s。这个临时对象s是通过拷贝构造函数(如果s3是左值)或移动构造函数(如果s3是右值)创建的。
  1. 交换操作
swap(s); // 交换 s1 和 s 的数据
  • 在赋值运算符的实现中,调用swap(s);。这会交换当前对象s1和临时对象s的内部数据指针。
  1. 临时对象销毁
// 临时对象 s 离开作用域,被销毁,释放旧资源
  • 在赋值运算符函数结束时,临时对象s离开作用域并被销毁,其析构函数会释放它所持有的资源。这些资源实际上是原来属于s1的旧资源。
  1. 返回当前对象
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 类型对象,表示数值的字符串形式。 image.png

stoi

stoi 是 C++ 中的一个标准库函数,用于将字符串转换为对应的整数类型。
头文件:#include<string>
int stoi(const std::string& str, size_t* pos = 0, int base = 10);

  • str: 要转换的字符串。
  • pos (可选): 指向 size_t 类型的指针,用于存储第一个无效字符的索引。
  • base (可选): 数字的基数,默认为 10。

返回:

  • 返回转换后的整数值。

image.png

完整代码展示

.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;}
}

07c03ae6d77b4b153f6d1ec710be7c14_7a80245f0b5f4021a033b3789a9efdeb.png

  1. 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
  2. 本人也很想知道这些错误,恳望读者批评指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/365597.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

外接电容选择不当会对晶振电路造成什么影响?

在电子设备中&#xff0c;晶振电路负责提供稳定的时钟信号&#xff0c;这对于电路的同步和正确操作至关重要。外接电容在晶振电路中发挥着关键作用&#xff0c;其选择是否得当直接影响到晶振的性能。以下是不当选择外接电容可能带来的问题&#xff1a; 频率稳定性下降&#xf…

商场配电新思维:智能网关驱动的自动化管理系统

在商场配电室监控系统中&#xff0c;主要是以无线网络为载体&#xff0c;目的就是便于对变电站等实时监测与控制。其中&#xff0c;4G配电网关非常关键&#xff0c;可以将配电室系统终端上的信息数据及时上传到服务器&#xff0c;再由服务器下达控制指令到各模块中&#xff0c;…

电脑技巧:告别卡顿,迎接流畅——Wintune系统优化软件功能详解

目录 一、Wintune介绍 二、Wintune核心功能介绍 2.1 系统优化 2.2 隐私功能 2.3 文件管理模块 2.4 可选选项 2.5 UWP app服务 2.6 startup Manager 2.7、主机编辑 三、总结 电脑是大家目前日常办公娱乐必不可小的工具&#xff0c;软件市场上的系统优化软件层出不穷&a…

泛型的使用(<T>)

文章目录 前言一、泛型是什么&#xff1f;二、泛型的使用 1.定义泛型类2.泛型的常规用法总结 前言 强制类型转换存在一定隐患&#xff0c;如数据丢失、内存溢出、运行时错误、程序逻辑错误等。所以提供了泛型机制&#xff0c;使程序员可以定义安全的数据类型进行操作。通俗的理…

宠物医院管理系统-计算机毕业设计源码07221

目 录 1 绪论 1.1 选题背景和意义 1.2国内外研究现状 1.3论文结构与章节安排 2 宠物医院管理系统系统分析 2.1 可行性分析 2.1.1技术可行性分析 2.1.2 操作可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分…

Soul社交元宇宙智能连接安全相伴,打造值得用户信赖的社交环境

随着人工智能技术的快速发展,社交平台正在迎来一场革命性的变革。从智能推荐到情感分析,社交平台通过深度学习和数据分析为用户提供更加个性化、智能化的社交体验。与此同时,数字时代人们的安全意识正逐渐增强。为此,一个智能、安全的社交平台成为人们迫切需要。而新型社交平台…

五种肉苁蓉属植物叶绿体基因组-文献精读25

Structural mutations of small single copy (SSC) region in the plastid genomes of five Cistanche species and inter-species identification 五种肉苁蓉属植物叶绿体基因组中小单拷贝 (SSC) 区域的结构突变及物种间鉴定 摘要 背景 肉苁蓉属是列当科的重要属类&#xf…

[SwiftUI 开发] 嵌套的ObservedObject中的更改不会更新UI

1. 发生问题的demo 业务逻辑代码 class Address: ObservableObject {Published var street "123 Apple Street"Published var city "Cupertino" }class User: ObservableObject {Published var name "Tim Cook"Published var address Addr…

嵌入式linux系统中动态链接库实现详解

大家好,linux系统中动态库是如何实现相互链接的?今天简单聊聊动态链接库的实现原理。 假设有这样两段代码,第一段代码定义了一个全量变量a以及函数foo,函数foo中引用了下一段代码中定义的全局变量b。 第二段代码定义了全局变量b以及main函数,同时在main函数中调用了第一个…

ZXL-2000砌体砂浆强度点荷仪

一、产品简介&#xff1a; 砌体砂浆强度点荷仪&#xff08;又名&#xff1a;砂浆点荷仪&#xff09;&#xff0c;是根据GB/T50315-2000《砌体工程现场检验技术规程》而研制生产的。是砌体砂浆强度检测的专用仪器&#xff0c;其特点是能在现场或试验室直接测试&#xff0c;不影…

最短路模型——AcWing 188. 武士风度的牛

最短路模型 定义 最短路模型是图论中的一个经典问题&#xff0c;旨在寻找从图中一个顶点到另一个顶点的路径&#xff0c;使得这条路径上的边&#xff08;或边的权重&#xff09;之和最小。这一模型在许多实际问题中有着广泛的应用&#xff0c;比如网络路由、地图导航、物流配…

【深度学习】图生图img3img论文原理,SD EDIT

https://arxiv.org/abs/2108.01073 摘要 引导图像合成技术使普通用户能够以最小的努力创建和编辑逼真的图像。关键挑战在于平衡对用户输入&#xff08;例如&#xff0c;手绘的彩色笔画&#xff09;的忠实度和合成图像的真实感。现有的基于GAN的方法试图通过使用条件GAN或GAN反…

面试相关-接口测试常问的问题

1.为什么要做接口测试 (1)现在大多系统都是前后端分离的项目,前端和后端的进度可能不一样,那为了尽早的进入测试,前端界面没有开发完成的情况下,只要后端的接口开发完了,就可以提前做接口测试了; (2)基于安全考虑,只依赖前端进行限制,已经完全不满足系统的安全性…

c++习题02-浮点数求余

目录 一&#xff0c;问题 二&#xff0c;思路 三&#xff0c;代码 一&#xff0c;问题 二&#xff0c;思路 虽然在浮点类型中没有取余的运算&#xff08;无法直接使用%符号取余&#xff09;&#xff0c;但是我们都知道在数学中&#xff0c;除法是减法的连续运算&#xff…

trie[讲课留档]

字典树 1.字典树简介 字典树 ( Trie 树 ) 又称单词查找树&#xff0c; 是一种用于在字符串集合中高效地存储和查找字符串的树形数据结构。 我们首先通过一张图来理解字典树的结构&#xff1a; 我们假定结点的顺序按照图中给定的顺序进行编号&#xff0c;容易发现&#xff0c…

Golang-slice理解

slice golang-slice语雀笔记整理 slicego为何设计slice&#xff1f;引用传递实现扩容机制 go为何设计slice&#xff1f; 切片对标其他语言的动态数组&#xff0c;底层通过数组实现&#xff0c;可以说是对数组的抽象&#xff0c;底层的内存是连续分配的所以效率高&#xff0c;可…

Spring Boot项目的两种发布方式

一、通过jar包发布 1、在pom中添加一个SpringBoot的构建的插件 <build><plugins><plugin><groupId>org.springframework.boot</groupId><!--自动检测项目中的 main 函数--><artifactId>spring-boot-maven-plugin</artifactId>…

一文get懂kwai短视频助力巴西博弈slots游戏广告优势

一文get懂kwai短视频助力巴西博弈slots游戏广告优势 在数字化时代&#xff0c;短视频广告凭借其独特的魅力和高效的传播方式&#xff0c;成为了各大品牌进行营销推广的重要手段。特别是在巴西这个充满活力的国家&#xff0c;kwai短视频广告以其独特的方式&#xff0c;为博弈游…

2024企业数据资产化及数据资产入表方案梳理

01 数据资产入表&#xff1a;是一个将组织的各类数据资产进行登记、分类、评估和管理的流程。 数据资产包括&#xff1a;客户信息、交易记录、产品数据、财务数据等。 做个比喻吧&#xff1a;数据资产入表就像是给公司的数据资产做“人口普查”—— ①找出公司有哪些数据找…

Pytorch课程论文设计参考

Pytorch下基于卷积神经网络的手写数字识别 论文格式 利用wps初步美化论文格式教程 wps论文格式变的的原因 格式变的根本原因是word为流式文件&#xff0c;就算同是word同一个版本不同电脑也会有可能变&#xff0c;字体变是因为没有嵌入字体然后观看的那台没有这个字体。 一、…