上节内容我们使用了它的函数接口,但是没有实现这次我们对string类进行手写实现
一.vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义
string中字符串的存储空间:当字符串长度小于16时,使用内部固定的字符数组来存放;当字符串长度大于等于16时,从堆上开辟空间
cpp
union _Bxty
{
// storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建
好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。其次 :还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量.最后: 还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。

g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个
指针,该指针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
- 指向堆空间的指针,用来存储字符串。
cpp
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
二. string类的模拟实现
string 实际上就是一个管理 字符数组 的顺序表。
上节已经对string类进行了简单的介绍,大家只要能够正常使用即可,主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数,在接下来进行手写实践
分为三个文件:
string.h
string.h定义了一个完整的bit::string类,包含迭代器、构造/析构、容量操作、元素访问、修改操作、查找操作、全局比较和流运算符。短小函数直接在.h中定义(inline),复杂函数在.h中声明,需要在.cpp中实现。
完整代码:
cpp
#include<assert.h>
using namespace std;
namespace bit
{
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
//返回迭代器
// 1. 非const对象调用,能修改元素
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// 2. const对象专属,只读,不会修改成员
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//短小频繁调用的函数,可以直接定义在类里面。默认是inline
string(const char* str = "")
{
_size = strlen(str);
//_capacity不包括\0
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 深拷贝问题
// s2(s1)
/*string(const string& s)
{
_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(_size, s._size);
std::swap(_capacity, s._capacity);
}
// s2(s1)
// 现代写法
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
// s2 = s1
// s1 = s1
/*string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}*/
// s1 = s3;
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// //string tmp(s._str);
// string tmp(s);
// swap(tmp);
// }
// return *this;
//}
// s1 = s3;
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
////////////////////////////////////////////////////////////////////////
////赋值重载 深拷贝(开新空间 + 复制内容)
////s2=s1
////s1=s1
//string& operator=(const string& s) // 返回当前对象的引用
//{
// if (this != &s) // 1. 防止自己给自己赋值
// {
// delete[] _str; // 2. 释放当前对象原来的内存
// _str = new char[s._capacity + 1]; // 3. 开新空间(大小和 s 一样)
// strcpy(_str, s._str); // 4. 拷贝内容
// _size = s._size; // 5. 拷贝大小
// _capacity = s._capacity; // 6. 拷贝容量
// }
// return *this; // 7. 返回当前对象
//}
///////////////////////////////////////////////////////
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//c_str() 返回内部指针(只读)
const char* c_str()const
{
return _str;
}
//清空字符串
void clear()
{
_str[0] = '\0';
_size = 0;
}
//返回字符串长度
size_t size() const
{
return _size;
}
//返回容量
size_t capacity() const
{
return _capacity;
}
//非 const 返回 char&(可读可写),const 返回 const char&(只读)
//下标访问运算符重载,让 string 对象可以像数组一样用 [ ] 访问字符。
// 普通对象调用(可读可写)
char& operator[](size_t pos)
{
assert(pos < _size); // 断言:pos 必须小于字符串长度
return _str[pos]; // 返回字符的引用
}
// const 对象调用(只读)
const char& operator[](size_t pos) const
{
assert(pos < _size); // 断言:pos 必须小于字符串长度
return _str[pos]; // 返回字符的常量引用
}
//这些在类里面声明,因为不频繁复=调用
void reserve(size_t n);//预留空间,避免频繁扩容
void push_back(char ch);//在字符串末尾追加一个字符
void append(const char* str);//在末尾追加一个 C 字符串
//追加字符或字符串(更简洁的写法)
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);//在 pos 位置插入一个字符
void insert(size_t pos, const char* str);//在 pos 位置插入一个 C 字符串
void erase(size_t pos, size_t len = npos);//从 pos 开始删除 len 个字符
size_t find(char ch, size_t pos = 0);//从 pos 开始查找字符 ch,返回位置,找不到返回 npos
size_t find(const char* str, size_t pos = 0);//从 pos 开始查找子串 str
string substr(size_t pos = 0, size_t len = npos);//从 pos 开始截取 len 个字符,返回新字符串
private:
//char _buff[16];
char* _str;
size_t _size;
size_t _capacity;
//static const size_t npos = -1;
static const size_t npos = -1;
};
//全局函数
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
//流插入 operator<< 从程序 -->输出流 读取 s 的内容 const string&(只读)
//流提取 operator>> 从输入流 -->程序 写入 s string& (可修改)
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
具体分块进行说明:
1.迭代器(支持范围 for)
cpp
iterator begin(); // 返回指向第一个字符的迭代器
iterator end(); // 返回指向末尾的迭代器
const_iterator begin() const; // const 版本
const_iterator end() const; // const 版本
2.构造 / 析构 / 赋值
cpp
string(const char* str = ""); // 构造(支持默认空串)
string(const string& s); // 拷贝构造(深拷贝)
string& operator=(string tmp); // 赋值(现代写法:传值+交换)
~string(); // 析构(释放内存)
3.容量操作
cpp
size_t size() const; // 返回字符串长度
size_t capacity() const; // 返回当前容量
void clear(); // 清空字符串
void reserve(size_t n); // 预留空间(避免扩容)
4.元素访问
cpp
char& operator[](size_t pos); // 可读可写(普通对象)
const char& operator[](size_t pos) const; // 只读(const 对象)
const char* c_str() const; // 返回 C 风格字符串
5.修改操作
cpp
void push_back(char ch); // 尾插字符
void append(const char* str); // 尾插字符串
string& operator+=(char ch); // 尾插字符(简洁)
string& operator+=(const char* str); // 尾插字符串(简洁)
void insert(size_t pos, char ch); // 插入字符
void insert(size_t pos, const char* str);// 插入字符串
void erase(size_t pos, size_t len); // 删除
void swap(string& s); // 交换两个字符串
6.查找操作
cpp
size_t find(char ch, size_t pos = 0); // 查找字符
size_t find(const char* str, size_t pos = 0);// 查找子串
7.截取操作
cpp
string substr(size_t pos = 0, size_t len = npos); // 截取子串
8.全局运算符(比较 / 输入 / 输出)
cpp
// 比较运算符
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
// 流插入 / 流提取
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
问1:为什么有的函数写在类里面,有的只写在类外面?
答 :因为短小频繁的函数适合放类内 (自动变成内联,效率高),复杂冗长的函数适合放类外(减少头文件膨胀,编译快)
问2:为什么 operator== 这些比较函数放类外?
答:为了支持左右操作数都可以隐式转换。
cpp
// 放类外:两边都能转换
"hello" == s; // 左边 const char* 可以转换
s == "hello"; // 右边 const char* 可以转换
// 如果放类内(成员函数),左边必须是 string 对象
// s.operator==("hello")
// "hello".operator==(s) 报错! 左边不是 string 对象
问3.:为什么 cin >> s 和 cout << s 放类外?
答:因为左操作数是流对象(
cin/cout),不是string对象。
cpp
// 左操作数是 istream,不是 string
istream& operator>>(istream& in, string& s);
// 左操作数是 ostream,不是 string
ostream& operator<<(ostream& out, const string& s);
如果放在类内作为成员函数,调用方式会变成:
s >> cin; // 书写不符合习惯
s << cout; // 同上
问4:你的代码里 swap 为什么放类内?
答 :
swap很简短,只有3行,放类内自动变成内联,效率高。
cpp
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
代码中:1行小函数(
begin、size、c_str)放类内;复杂函数(reserve、insert、find)只声明;比较和流运算符放类外(因为左操作数不是 string)。好处:短小函数放类内(自动内联,调用快),复杂函数放类外(减少头文件依赖,编译快),全局运算符放类外(支持左右操作数隐式转换,且符合
cin >> s的使用习惯)
string.cpp
.cpp文件实现了bit::string所有成员函数和全局运算符,包括扩容、尾插、插入、删除、查找、截取、比较和输入输出。namespace bit里是为了和头文件的类匹配。
完整代码如下:
cpp
//手写函数不是用库里面的
#include"string.h"
//如果 .cpp 不包 namespace bit,你写 void string::reserve
// 编译器会认为这是全局域的 string 类,和头文件里
// bit::string 是两个完全无关的类
// ,直接报未定义、匹配不上。套上同一个 namespace bit 后,
// string::reserve 等价于 bit::string::reserve,
// 和头文件声明的类匹配上。
namespace bit
{
//const size_t string::npos = -1;
//预留空间函数
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);//把原内容复制到新空间
delete[] _str;//删除原来的空间
_str = tmp;//tmp代替_str最初的位置
_capacity = n;//容量变为n
}
}
//尾插一个字符串
void string::push_back(char ch)
{
//空间满的话
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;//this 指向当前对象的指针
//*this 当前对象本身(解引用)
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)//超出容量空间
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
//insert 先检查位置和空间,然后从后往前把 [pos, _size] 的字符(包括 \0)往后挪一位,腾出位置插入新字符,最后更新 _size。
void string::insert(size_t pos, char ch)
{
assert(pos <= _size); // 检查pos合法性,不能超过字符串当前长度
// 扩容:如果容量用完,按规则扩容(空串扩为4,否则翻倍)
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 1. 挪动数据:从末尾开始,把字符向后移1位,腾出pos位置
size_t end = _size + 1; // 扩容后长度+1,包括'\0'
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
// 2. 插入字符,更新长度
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s); // 计算插入字符串的长度
// 扩容:如果总长度超过容量,按规则扩容(不够就扩到刚好,否则翻倍)
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
// 1. 挪动数据:从末尾开始,把字符向后移len位
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 2. 插入字符串,更新长度
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
// 情况1:删除长度超过剩余字符,直接截断到pos位置
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
// 情况2:正常删除,挪动数据覆盖被删除的部分
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
// 从pos位置开始,向后找字符ch
size_t string::find(char ch, size_t pos)
{
// 检查:起始位置必须是有效字符位,不能是\0
assert(pos < _size);
// 从下标pos开始,逐个遍历有效字符
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
// 找到,返回当前下标
return i;
}
}
// 遍历完都没找到,返回npos(表示不存在)
return npos;
}
// 从pos位置开始,向后找子串s
size_t string::find(const char* s, size_t pos)
{
assert(pos < _size);
// _str + pos:指针跳转到查找起始位置
const char* ptr = strstr(_str + pos, s);
if (ptr == nullptr)
{
// 没找到
return npos;
}
else
{
// 指针相减 = 子串起始下标
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len)
{
// 只能从有效字符位置开始截取,不能取 \0
assert(pos < _size);
// 容错:剩余字符不够 len 个,就截到字符串末尾为止
if (len >= _size - pos)
{
len = _size - pos;
}
string sub; // 新建空字符串,用来存截取结果
sub.reserve(len); // 提前开空间,避免频繁扩容
// 逐个拷贝字符
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
//流提取
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
//in >> ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i++] = '\0';
s += buff;
i = 0;
}
//in>>ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
分块描述:
1.文件头 + 命名空间
cpp
#include "string.h"
namespace bit
{
}
包含头文件,所有实现包在
namespace bit里,和头文件匹配。
2.容量管理
cpp
void string::reserve(size_t n)
{
if (n > _capacity) // 只有 n 大于当前容量才扩容
{
char* tmp = new char[n + 1]; // 开新空间
strcpy(tmp, _str); // 拷贝原内容
delete[] _str; // 释放旧空间
_str = tmp; // 指向新空间
_capacity = n; // 更新容量
}
}
手动扩容,提前开好空间,避免多次扩容。
3.尾部插入
cpp
void string::push_back(char ch)
{
if (_size == _capacity) // 空间满了
{
reserve(_capacity == 0 ? 4 : _capacity * 2); // 扩容
}
_str[_size] = ch; // 放字符
++_size; // 长度+1
_str[_size] = '\0'; // 末尾加结束符
}
string& string::operator+=(char ch)
{
push_back(ch); // 复用 push_back
return *this; // 返回自身,支持链式调用
}
void string::append(const char* str)
{
size_t len = strlen(str); // 要追加的字符串长度
if (_size + len > _capacity) // 空间不够
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str); // 拷贝到末尾
_size += len; // 更新长度
}
string& string::operator+=(const char* str)
{
append(str); // 复用 append
return *this;
}
尾插字符/字符串,空间不够就扩容,
+=是简洁写法。
4.中间插入
cpp
void string::insert(size_t pos, char ch)
{
assert(pos <= _size); // 位置合法检查
if (_size == _capacity) // 空间不够就扩容
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1; // 从末尾(含\0)开始
while (end > pos) // 从后往前挪数据
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch; // 插入字符
++_size; // 长度+1
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity) // 空间不够就扩容
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len; // 从末尾开始
while (end > pos + len - 1) // 从后往前挪 len 位
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++) // 插入字符串
{
_str[pos + i] = s[i];
}
_size += len;
}
在指定位置插入,从后往前挪数据腾出位置(不会覆盖未处理数据)。
5.删除
cpp
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos) // 删除长度超过剩余字符
{
_str[pos] = '\0'; // 直接截断
_size = pos;
}
else // 正常删除
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i]; // 后面数据往前覆盖
}
_size -= len;
}
}
删除指定位置的字符,两种情况:删到末尾直接截断,删中间则后面数据往前覆盖。
6.查找
cpp
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; // 找不到返回 npos
}
size_t string::find(const char* s, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, s); // C 语言查找子串
if (ptr == nullptr) return npos;
return ptr - _str; // 指针相减得下标
}
查找字符或子串,找到返回下标,找不到返回
npos。
7.截取子串
cpp
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos) len = _size - pos; // 不能超出范围
string sub;
sub.reserve(len); // 提前开空间
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i]; // 逐个拷贝
}
return sub;
}
截取子串,提前开空间避免频繁扩容。
8.比较运算符
cpp
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2; // 复用 < 和 ==
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2); // 复用 <=
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2); // 复用 <
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2); // 复用 ==
}
只实现
<和==,其他运算符通过逻辑关系复用它们。
9.输入输出
cpp
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s) // 范围 for 遍历
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch = in.get(); // 读一个字符
while (ch != ' ' && ch != '\n') // 读到空格或换行停止
{
buff[i++] = ch;
if (i == N - 1) // 缓冲区满了
{
buff[i] = '\0';
s += buff; // 追加到字符串
i = 0;
}
ch = in.get();
}
if (i > 0) // 剩余字符
{
buff[i] = '\0';
s += buff;
}
return in;
}
输出用范围 for 遍历;输入用缓冲区逐字读取,遇到空格或换行停止,拼成一个单词。
test.cpp
测试函数包括我手写
string类的功能:构造、迭代器、插入、删除、查找、截取、拷贝、赋值、比较、输入输出、交换。
cpp
#include"string.h"
namespace bit
{
void test_string1()
{
string s1;
string s2("hello world");
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
for (size_t i = 0; i < s2.size(); i++)
{
s2[i] += 2;
}
cout << s2.c_str() << endl;
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
string::iterator it = s2.begin();
while (it != s2.end())
{
//*it += 2;
cout << *it << " ";
++it;
}
cout << endl;
}
void test_string2()
{
string s1("hello world");
s1 += 'x';
s1 += '#';
cout << s1.c_str() << endl;
s1 += "hello world";
cout << s1.c_str() << endl;
s1.insert(5, '$');
cout << s1.c_str()<< endl;
s1.insert(0, '$');
cout << s1.c_str() << endl;
string s2("hello world");
cout << s2.c_str() << endl;
s2.insert(5, "$$$");
cout << s2.c_str() << endl;
s2.insert(0, "$$$$$$$$$$$$$$$$$$$$$$$$$");
cout << s2.c_str() << endl;
}
void test_string3()
{
string s1("helllo world");
s1.erase(6, 100);
cout << s1.c_str() << endl;
string s2("hello world");
s2.erase(6);
cout << s2.c_str() << endl;
string s3("hello world");
s3.erase(6, 3);
cout << s3.c_str() << endl;
}
//_str() 的作用
// const char* c_str() const;
// 特点 说明
// 返回值 const char* (只读,不能修改)
// 结尾 保证有 \0
// 用途 传给 C 函数(printf、strcpy、fopen 等)
//这个函数测试了:find 查找、substr 截取、
// 拷贝构造、赋值重载、自赋值。预期输出取决于 find
// 是正向还是反向查找,用 find('.')
// 会取到第一个点,用 rfind('.') 才取到最后一个点(后缀)。
void test_string4()
{
string s("test.cpp.zip");
size_t pos = s.find('.');
string suffix = s.substr(pos);
cout << suffix.c_str() << endl;
string copy(s);
cout << copy.c_str() << endl;
s = suffix;
cout << suffix.c_str() << endl;
cout << s.c_str() << endl;
s = s;
cout << s.c_str() << endl;
}
void test_string5()
{
string s1("hello world");
string s2("hello world");
cout << (s1 < s2) << endl;
cout << (s1 == s2) << endl;
cout << ("hello world <s2") << endl;
cout << (s1 == "hello world") << endl;//隐式转换成 string 对象,然后再比较内容。
//地址进行比较
//cout<<("hello world " == "hello world")<<endl;
cout << s1 << s2 << endl;
string s0;
cin >> s0;
cout << s0 << endl;
}
//拷贝构造 string s2 = s1; string(const string& s) 创建新对象,从已有对象拷贝
//拷贝赋值 s1 = s3; string& operator=(const string& s) 对象已存在,把另一个对象赋值给它
void test_string6()
{
string s1("hello world");
string s2 = s1;
cout << s1 << endl;
cout << s2 << endl;
string s3("$$$$$$$");
s1 = s3;
cout << s1 << endl;
cout << s3 << endl;
}
void test_string7()
{
string s1("hello world");
string s2("$$$$$$$$$$$$$");
std::swap(s1, s2);
s1.swap(s2);
cout << s1 << s2 << endl;
}
}
分块描述:
文件结构
cpp
#include "string.h" // 包含自定义的 string 类
namespace bit // 和 string.h 同一个命名空间
{
// 所有测试函数
}
测试手写的
bit::string类是否正常工作。
2.测试函数模块解析
模块1:test_string1
cpp
void test_string1()
{
string s1; // 测试无参构造
string s2("hello world"); // 测试有参构造
cout << s1.c_str() << endl; // 输出空串
cout << s2.c_str() << endl; // 输出 hello world
for (size_t i = 0; i < s2.size(); i++)
{
s2[i] += 2; // 测试 operator[] 修改
}
cout << s2.c_str() << endl; // 输出 jgnnq yqtnf
for (auto e : s2) // 测试范围 for(需要迭代器)
{
cout << e << " "; // 输出 j g n n q y q t n f
}
cout << endl;
string::iterator it = s2.begin(); // 测试迭代器
while (it != s2.end())
{
cout << *it << " "; // 输出 j g n n q y q t n f
++it;
}
cout << endl;
}

测试内容:
构造、
c_str()、operator[]、范围 for、迭代器
模块2:test_string2 (尾部插入 + 中间插入)
cpp
void test_string2()
{
string s1("hello world");
s1 += 'x'; // 测试 operator+=(char)
s1 += '#'; // 测试 operator+=(char)
cout << s1.c_str() << endl;
s1 += "hello world"; // 测试 operator+=(const char*)
cout << s1.c_str() << endl;
s1.insert(5, '$'); // 测试 insert(pos, char)
cout << s1.c_str() << endl;
s1.insert(0, '$'); // 测试头插
cout << s1.c_str() << endl;
string s2("hello world");
cout << s2.c_str() << endl; // hello world
s2.insert(5, "$$$"); // 测试 insert(pos, str)
cout << s2.c_str() << endl; // hello$$$ world
s2.insert(0, "$$$$$$$$$$$$$$$$$$$$$$$$$"); // 长字符串头插
cout << s2.c_str() << endl;

测试内容:
operator+=(字符/字符串)
insert(字符/字符串,头插/中间插)
模块3:test_string3 (删除测试)
cpp
void test_string3()
{
string s1("helllo world");
s1.erase(6, 100); // 删除长度超过剩余就截断
cout << s1.c_str() << endl; // helllo
string s2("hello world");
s2.erase(6); // len 默认 npos删到末尾
cout << s2.c_str() << endl; // hello
string s3("hello world");
s3.erase(6, 3); // 删除中间 3 个字符
cout << s3.c_str() << endl; // hello rld
}

测试内容:
erase三种情况:超长度删除、默认长度、中间删除
模块4:test_string4 ( 查找 + 截取 + 拷贝 + 赋值)
cpp
void test_string4()
{
string s("test.cpp.zip");
size_t pos = s.find('.'); // 正向查找第一个点
string suffix = s.substr(pos); // 截取子串
cout << suffix.c_str() << endl; // .cpp.zip(第一个点开始)
string copy(s); // 拷贝构造
cout << copy.c_str() << endl; // test.cpp.zip
s = suffix; // 赋值
cout << suffix.c_str() << endl; // .cpp.zip suffix 不变
cout << s.c_str() << endl; // .cpp.zip s 变了
s = s; // 自赋值测试
cout << s.c_str() << endl; // .cpp.zip
}
测试内容:
find、substr、拷贝构造、赋值、自赋值注意:
find('.')找第一个点,结果是.cpp.zip;如果想取后缀.zip,要用rfind('.')
模块5:test_string5 ( 比较 + 输入输出)
cpp
void test_string5()
{
string s1("hello world");
string s2("hello world");
cout << (s1 < s2) << endl; // 0 (相等不小于)
cout << (s1 == s2) << endl; // 1 (相等)
cout << ("hello world <s2") << endl; // 这是字符串,不是比较
cout << (s1 == "hello world") << endl; // 隐式转换,1
cout << s1 << s2 << endl; // 测试 operator<<
string s0;
cin >> s0; // 测试 operator>>
cout << s0 << endl;
}
测试内容:
比较运算符
<、==
operator<<、operator>>隐式类型转换(
const char*变成string)注意:
cout << ("hello world <s2")输出的是字符串字面量,不是比较结果
模块6:test_string6 (拷贝构造 和拷贝赋值)
cpp
void test_string6()
{
string s1("hello world");
string s2 = s1; // 拷贝构造(s2 还没创建)
cout << s1 << endl; //hello world
cout << s2 << endl; // hello world
string s3("$$$$$$$");
s1 = s3; // 拷贝赋值(s1 已存在)
cout << s1 << endl; // $$$$$$$
cout << s3 << endl; // $$$$$$$
}
测试内容:
拷贝构造 和 拷贝赋值的区别,深拷贝验证验证修改 s3 不影响 s1。
模块7:test_string7 (交换)
cpp
void test_string7()
{
string s1("hello world");
string s2("$$$$$$$$$$$$$");
std::swap(s1, s2); // 标准库 swap
s1.swap(s2); // 成员函数 swap
cout << s1 << s2 << endl; // s1 和 s2 交换
}
测试内容:
swap功能(交换内容,不拷贝)
cpp
// 为了和标准库区分,此处使用String
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
这个
String类只有构造和析构,没有拷贝构造。当String s2(s1)时会浅拷贝,两个对象指向同一块内存,析构时重复释放导致崩溃。需要实现深拷贝拷贝构造来修复。
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝
浅拷贝
浅拷贝:也称位拷贝 ,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

引发的问题:浅拷贝只复制指针,不复制内容,导致两个对象共享同一块内存。结果就是修改一个影响另一个,析构时重复释放导致崩溃。
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供
传统版写法的String类
cpp
class String
{
public:
// 构造函数:用 C 字符串构造 String 对象
String(const char* str = "")
{
// 不允许传入空指针
if (nullptr == str)
{
assert(false); // 触发断言,提示错误
return;
}
// 开空间(长度 + 1 给 '\0')
_str = new char[strlen(str) + 1];
// 拷贝内容
strcpy(_str, str);
}
// 拷贝构造:用已有对象构造新对象(深拷贝)
String(const String& s)
: _str(new char[strlen(s._str) + 1]) // 初始化列表:直接开空间
{
// 拷贝内容
strcpy(_str, s._str);
}
// 赋值重载:将 s 赋值给当前对象(深拷贝)
String& operator=(const String& s)
{
// 防止自己给自己赋值
if (this != &s)
{
// 开新空间(独立于原空间)
char* pStr = new char[strlen(s._str) + 1];
// 拷贝内容到新空间
strcpy(pStr, s._str);
// 释放当前对象的旧空间
delete[] _str;
// 指向新空间
_str = pStr;
}
// 返回自身,支持连续赋值
return *this;
}
// 析构函数:释放堆内存
~String()
{
if (_str) // 指针非空才释放
{
delete[] _str; // 释放内存
_str = nullptr; // 置空,避免野指针
}
}
private:
char* _str; // 指向堆上的字符串
};
传统赋值采用先开空间再释放的顺序:先
new出一块新空间并拷贝数据,成功后再delete释放旧空间,最后让_str指向新空间。这样即使new失败抛异常,原对象的数据也不会丢失,保证了强异常安全。
现代版写法的String类
cpp
class String
{
public:
// 构造函数:用 C 字符串构造 String 对象
String(const char* str = "")
{
if (nullptr == str) // 禁止空指针
{
assert(false);
return;
}
_str = new char[strlen(str) + 1]; // 开空间(多1给\0)
strcpy(_str, str); // 拷贝内容
}
写法一:
// 拷贝构造:用已有对象构造新对象
String(const String& s)
: _str(nullptr) // 先置空,避免 swap 时交换随机值
{
String strTmp(s._str); // 用 s 的字符串构造临时对象(深拷贝)
swap(_str, strTmp._str); // 交换指针,临时对象负责释放旧空间
}
写法二:
// 赋值(现代写法):传值 + 交换
String& operator=(String s) // 传值,传参时已拷贝一份
{
swap(_str, s._str); // 交换指针,s 析构时释放旧空间
return *this; // 返回自身,支持连续赋值
}
// 析构函数:释放堆内存
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
传统写法是自己手动开空间、拷贝内容、释放旧空间,代码冗长且需要手动处理自赋值检查;
现代写法的核心思想是"不自己干活,而是借助临时对象 + swap":拷贝构造时用源对象构造一个临时对象,然后交换指针;赋值时直接传值(传参时自动完成拷贝),再交换指针,让临时对象析构时自动释放旧空间。现代写法更简洁、自动处理自赋值、异常安全。
拷贝构造函数的深拷贝(现代写法):
现代写法拷贝构造的核心是:先将当前对象的
_str置空(因为正在构造的对象成员变量是随机值),然后拿源对象s1的字符串构造一个临时对象tmp(此时tmp拥有深拷贝的数据),接着交换tmp和当前对象的_str指针,让当前对象拿到拷贝好的数据,而tmp指向空。当函数结束,临时对象tmp自动析构,由于它指向的是空指针,不会释放任何内存,而s1原有的内存依然由s1自己管理。整个过程无需手动开空间和拷贝,全部复用构造和析构函数,异常安全且代码简洁

赋值运算符重载函数的深拷贝(现代写法):
现代写法赋值的核心是"传值即拷贝,交换即转移":当执行
s1 = s2时,参数String s不是传引用而是传值,这意味着在传参的同时就已经用s2拷贝构造 出了一个独立的对象s(深拷贝已完成)。接着,交换s1和s的_str指针,让s1拿到s中刚刚拷贝好的数据,而s则指向s1原来的旧空间。当函数结束时,局部对象s自动析构,顺手帮s1释放了那块旧空间。整个过程只需一次交换,无需手动写new/delete,无需自赋值检查,且天然强异常安全

写时拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
**引用计数:**用来记录资源使用者的个数。
在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源
扩展阅读关于写时拷贝的
三.扩展阅读
1.C++面试中string类的一种正确写法
