【C++】深入理解string类(5)
如果在strcpy拷贝的时候,遇到\0,就会直接停止拷贝(例如:hello world\0yyy\0),如果是在字符串的中间有\0,那么就会造成拷贝的不完全,所以不能使用strcpy,而是用memcpy。开辟一个和s3一样大的空间,s1指向该空间,释放s1原本的旧空间,将s3的内容拷贝给s1。传统写法和现代写法的算法效率是一样的,只是现代写法的代码较短,代码写法不同,充分利用了复用,本质上区别不大
目录
一 模拟实现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 的值赋给 a
b = 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 构造临时对象 tmp
swap(tmp); // 交换当前对象与 tmp 的资源
}
return *this;
}
(2)
string& string::operator=(string tmp) // 注意:参数是值传递,会先拷贝构造 tmp
{
swap(tmp); // 交换当前对象与 tmp 的资源
return *this;
}
- 逻辑:
- 利用 值传递的特性:调用这个函数时,编译器会自动拷贝一份实参(即要赋值的对象)到
tmp
中(这一步是 “拷贝构造”)。- 直接交换当前对象与
tmp
的资源,tmp
销毁时会带走当前对象原来的旧资源,当前对象则获得tmp
拷贝来的新资源。- 这种写法连 “自赋值判断” 都省略了:因为值传递的
tmp
是独立拷贝,即使自赋值,交换后也不影响正确性(只是多一次拷贝)。
3 二者使用有什么区别?
传统写法和现代写法的算法效率是一样的,只是现代写法的代码较短,代码写法不同,充分利用了复用,本质上区别不大
四 面试中实现string类
更多推荐
所有评论(0)