【C++】深入理解string类(5)
目录
一 模拟实现string类的补充
1 resize
2 find
3 substr
4 流提取和流输出
5 getline
6 operator =
clear
8 其他运算符重载
9 三种swap
编辑
二 string拷贝构造的传统写法和现代写法
1 传统写法
2 现代写法
三 运算符重载(=)的传统写法和现代写法
1 传统写法
2 现代写法
3 二者使用有什么区别?
一 模拟实现string类的补充
之前写的string类如下:【c++】深入理解string类(4)
1 resize
功能是将字符串的有效长度(_size
)修改为指定值 n
,并根据需要填充新字符或截断字符串
void string::resize(size_t n, char ch){if (n <= _size){// 删除,保留前n个_size = n;_str[_size] = '\0';}else{reserve(n);//调用 reserve(n) 预分配至少能容纳 n 个字符的内存(避免后续填充时频繁扩容)for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\n';}}
2 find
查找字符或子串出现的位置:
1 查找字符:
size_t string::find(char ch, size_t pos){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch)return i;}return npos;}
2 查找子串:
strstr
是 C 语言标准库(string.h
)中的一个字符串处理函数,用于在一个字符串(主串)中查找另一个字符串(子串)的首次出现位置。
// 在当前字符串中查找子串 str 的第一次出现位置
// 参数:
// str:待查找的 C 风格字符串(以 '\0' 结尾)
// pos:起始查找位置(从 pos 开始向后查找,默认从 0 开始)
// 返回值:找到时返回子串首字符在当前字符串中的位置;未找到返回 npos(通常定义为 -1)
size_t string::find(const char* str, size_t pos)
{assert(pos < _size);// strstr(a, b) 功能:在字符串 a 中查找子串 b 第一次出现的位置,返回指向该位置的指针;// 若未找到,返回 nullptr。// 这里从 _str + pos 开始查找(即从当前字符串的 pos 位置向后)const char* ptr = strstr(_str + pos, str);if (ptr){// 若找到子串,计算其在当前字符串中的位置:// 指针 ptr 减去当前字符串的起始地址 _str,得到偏移量(即位置)return ptr - _str; // 注意:原代码中写的是 ptr - str,这里修正为 ptr - _str(原代码可能笔误)}else{// 未找到子串,返回 npos(表示“不存在的位置”)return npos;}
}
3 substr
功能:从 pos
位置开始,提取长度为 len
的子串(若 len
过长则提取到字符串末尾)
// 从当前字符串中提取子串(从 pos 位置开始,长度为 len)
// 参数:
// pos:子串的起始位置(必须小于当前字符串长度 _size)
// len:子串的长度(默认或传入 npos 时,表示提取到字符串末尾)
// 返回值:提取出的子串(string 对象)
string string::substr(size_t pos, size_t len)
{assert(pos < _size);// 处理 len 过长或为 npos 的情况:// 若 len 是 npos,或 len 超过从 pos 到末尾的剩余字符数,则将 len 修正为剩余字符数if (len == npos || len > _size - pos){len = _size - pos; // 剩余字符数 = 总长度 - 起始位置}// 创建一个空的 string 对象,用于存储子串string sub;// 提前为子串预留足够的空间(容量设为 len),避免循环中频繁扩容sub.reserve(len);// 从 pos 位置开始,拷贝 len 个字符到子串中for (size_t i = 0; i < len; i++){sub += _str[pos + i]; // 逐个字符追加到子串(利用 operator+= 操作)}// 返回提取到的子串return sub;
}
4 流提取和流输出
不一定必须写成友元函数
// out:输出流对象(如 cout)
// s:要打印的自定义 string 对象(加 const 确保不修改原对象)
std::ostream& operator<<(std::ostream& out, const string& s)
{for (auto ch : s){out << ch; // 将每个字符逐个输出到流中}return out; // 返回输出流,支持链式操作
}
此处的s不能加const
// in:输入流对象(如 cin)
// s:接收输入的 string 对象(非 const,需修改其内容)
// 返回值:输入流对象(支持链式调用,如 cin >> s1 >> s2)
std::istream& operator>>(std::istream& in, string& s)
{s.clear(); // 先清空原字符串,避免输入内容追加到原有数据后char buff[256]; // 临时缓冲区,每次最多存 255 个字符(+1 个 '\0')int i = 0; // 缓冲区当前填充位置char ch;// 用 in.get() 读取字符(相比 >> 运算符,get() 会读取空格和换行符,此处用于自定义终止条件)ch = in.get();// 循环读取字符,直到遇到换行符 '\n' 或空格 ' '(默认以空白字符作为输入结束标志)while (ch != '\n' && ch != ' '){buff[i++] = ch; // 将字符存入缓冲区// 若缓冲区已满(i=255,此时 buff[0..254] 已填满)if (i == 255){buff[i] = '\0'; // 手动添加字符串结束符s += buff; // 将缓冲区内容追加到 s 中i = 0; // 重置缓冲区索引,准备接收下一批字符}ch = in.get(); // 继续读取下一个字符}// 循环结束后,若缓冲区中还有未处理的字符(i > 0)if (i > 0){buff[i] = '\0'; // 添加结束符s += buff; // 追加到 s 中}return in; // 返回输入流,支持链式操作
}
5 getline
// 从输入流中读取一行字符串,直到遇到分隔符 delim 为止
// 参数:
// in:输入流对象(如 cin)
// s:接收读取内容的 string 对象
// delim:自定义分隔符(默认通常是 '\n',即换行符)
// 返回值:输入流对象(支持链式操作)
std::istream& getline(std::istream& in, string& s, char delim)
{s.clear(); // 清空目标字符串,确保读取的是新内容(而非追加到原有数据后)char buff[256]; // 临时缓冲区,每次最多存储 255 个字符(预留 1 个位置给 '\0')int i = 0; // 缓冲区当前填充的字符索引char ch;// 用 in.get() 读取单个字符(包括空格、制表符等空白字符,不会像 >> 那样自动跳过)ch = in.get();// 循环读取字符,直到遇到指定的分隔符 delim 为止while (ch != delim){// 将读取到的字符存入缓冲区buff[i++] = ch;// 若缓冲区已满(i=255,此时 buff[0] 到 buff[254] 已存满)if (i == 255){buff[i] = '\0'; // 手动添加字符串结束符,确保 buff 是合法的 C 风格字符串s += buff; // 将缓冲区内容追加到目标 string 对象 s 中i = 0; // 重置缓冲区索引,准备接收下一批字符}// 继续读取下一个字符ch = in.get();}// 循环结束后,若缓冲区中还有未处理的字符(i > 0)if (i > 0){buff[i] = '\0'; // 添加结束符s += buff; // 将剩余字符追加到 s 中}return in; // 返回输入流对象,支持链式调用(如 getline(cin, s1) >> s2)
}// 定义 string 类的静态成员 npos(表示“无效位置”或“到末尾”)
// npos 通常被定义为 size_t 的最大值(-1 转换为无符号类型后即为最大值)
const size_t string::npos = -1;
6 operator =
赋值操作
将s3赋值给s1,此处是深拷贝。开辟一个和s3一样大的空间,s1指向该空间,释放s1原本的旧空间,将s3的内容拷贝给s1
string& string::operator=(const string& s){if (this != &s){char* tmp = new char[s._capacity + 1];//strcpy(tmp, s._str);memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}
注意区分拷贝构造函数和赋值运算符重载:
但是上面的代码还是有bug:
如果在strcpy拷贝的时候,遇到\0,就会直接停止拷贝(例如:hello world\0yyy\0),如果是在字符串的中间有\0,那么就会造成拷贝的不完全,所以不能使用strcpy,而是用memcpy
相应的,在使用strcpy的部分都换成memecpy
所以,上面的拷贝构造就优化为:
clear
清除数据
void clear(){_str[0] = '\0';_size = 0;}
8 其他运算符重载
实现了一个后面的就可以复用
bool string::operator<(const string& s) const{return strcmp(_str, s._str) < 0;}bool string::operator<=(const string& s) const{return *this < s || *this == s;}bool string::operator>(const string& s) const{return !(*this <= s);}bool string::operator>=(const string& s) const{return !(*this > s);}bool string::operator==(const string& s) const{return strcmp(_str, s._str) == 0;}bool string::operator!=(const string& s) const {return !(*this == s);}
9 三种swap
1.
std::string::swap
(成员函数)
- 形式:
void swap(string& str);
- 特点:
string
类自身的成员函数,用于交换当前string
对象与参数str
的内容。- 效率:极高。因为内部只需交换
string
的 “指针、大小、容量” 等成员(类似 “指针交换”),无需深拷贝字符数据。2.
std::swap(string)
(针对string
的特化函数)
- 形式:
void swap(string& x, string& y);
- 特点:标准库为
string
专门优化的全局swap
函数(属于对通用swap
模板的 “特化”)。- 效率:与
string::swap
一致(内部通常直接调用string::swap
实现,因此效率相同)。3. 通用
std::swap
模板(早期实现,非专用于string
)
- 形式:
template <class T> void swap(T& a, T& b);
第三个是标准库里的swap,在实现的时候可能会造成三次深拷贝(string使用时),代价太大
前两个的模拟实现:
template <class T>
void swap(T& a, T& b) {T c(a); // 1. 拷贝构造临时对象 c,存储 a 的值a = b; // 2. 把 b 的值赋给 ab = c; // 3. 把临时对象 c(原 a 的值)赋给 b
}inline void swap(string& a, string& b) {a.swap(b); // 调用 string 自身的 swap 成员函数
}
二 string拷贝构造的传统写法和现代写法
1 传统写法
string::string(const string& s)
{_str = new char[s._capacity + 1]; // 分配新内存memcpy(_str, s._str, s._size + 1); // 拷贝字符串(包括'\0')_size = s._size; // 同步长度_capacity = s._capacity; // 同步容量
}
2 现代写法
string::string(const string& s)
{string tmp(s._str); // 先构造临时对象(利用已有的构造逻辑)swap(tmp); // 与临时对象交换资源(指针、大小、容量)
}
代码解析:
(1)
string tmp(s._str); // 构造临时对象 tmp
这里的逻辑是:
- 利用源对象的底层字符串:
s._str
是源对象s
中存储字符串的字符数组指针(C 风格字符串,以'\0'
结尾)。 - 调用
const char*
构造函数:string
类通常有一个接收const char*
类型参数的构造函数(带参构造函数),其作用是根据 C 风格字符串初始化string
对象(分配内存、拷贝字符串内容、设置_size
和_capacity
)。 - 临时对象
tmp
的状态:通过s._str
构造的tmp
,其内部的_str
指针指向一块新分配的内存,存储的字符串内容与s._str
完全相同,且_size
和_capacity
也与s
一致(因为拷贝了相同的字符串)。
为什么要这一步?
- 复用已有逻辑:
const char*
构造函数已经实现了 “根据字符串内容分配内存、拷贝数据、初始化成员变量” 的完整逻辑。通过构造tmp
,可以直接复用这部分代码,避免在拷贝构造函数中重复编写相同的逻辑(减少冗余,降低出错风险)。 - 为后续交换做准备:
tmp
此时已经是一个与源对象s
内容完全一致的 “副本”,接下来只需要通过swap
函数,将tmp
的资源(新分配的内存、大小、容量)转移给当前正在构造的对象即可。
(2)
string
类的 swap
成员函数的作用是将当前对象(this
指向的对象)与另一个对象的资源进行交换。它的函数原型通常是:
void swap(string& other); // 成员函数,仅需一个参数
- 调用者是当前对象(
*this
),参数other
是要交换的另一个对象。 - 函数内部通过交换两者的
_str
指针、_size
、_capacity
等成员,完成资源互换。
三 运算符重载(=)的传统写法和现代写法
1 传统写法
string& string::operator=(const string& s)
{if(*this != &s){char* tmp = new char[s.capacity+1];memcpy(tmp,s._str,s._szie+1);detlete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;
}
2 现代写法
(1)
string& string::operator=(const string& s)
{if (this != &s) // 防止自赋值(如 s1 = s1){string tmp(s._str); // 用 s 的 _str 构造临时对象 tmpswap(tmp); // 交换当前对象与 tmp 的资源}return *this;
}
(2)
string& string::operator=(string tmp) // 注意:参数是值传递,会先拷贝构造 tmp
{swap(tmp); // 交换当前对象与 tmp 的资源return *this;
}
- 逻辑:
- 利用 值传递的特性:调用这个函数时,编译器会自动拷贝一份实参(即要赋值的对象)到
tmp
中(这一步是 “拷贝构造”)。- 直接交换当前对象与
tmp
的资源,tmp
销毁时会带走当前对象原来的旧资源,当前对象则获得tmp
拷贝来的新资源。- 这种写法连 “自赋值判断” 都省略了:因为值传递的
tmp
是独立拷贝,即使自赋值,交换后也不影响正确性(只是多一次拷贝)。
3 二者使用有什么区别?
传统写法和现代写法的算法效率是一样的,只是现代写法的代码较短,代码写法不同,充分利用了复用,本质上区别不大