1.定义类的基本结构
因为字符串的长度是可变的(今天存 "Hi",明天可能存 "Hello World"),我们不能在类里面写死一个固定大小的数组(比如 char data[100],这样太浪费空间或者根本不够用)。所以,我们必须使用动态内存分配。
在 C++ 中,管理动态内存的黄金搭档就是:一个指针 + 一个长度变量。(空间容量可选)
cpp
class MyString
{
private:
char *_data;
size_t _length;
size_t _capacity;
public:
// 构造与析构
MyString();
MyString(const char *str);
~MyString();
// 拷贝构造与移动构造
MyString(const MyString &other);
MyString(MyString &&other) noexcept;
// 赋值运算符
MyString &operator=(const MyString &other);
MyString &operator=(MyString &&other) noexcept;
// 常用功能
const char *c_str() const;
size_t size() const;
void set_char(size_t index, char ch);
// 运算符重载 (成员函数)
char &operator[](size_t index);
const char &operator[](size_t index) const;
MyString &operator+=(const MyString &other);
};
2.构造函数
分成无参构造函数和带参构造函数(也可以直接写成带默认值的构造函数)
- 无参构造函数 :创建一个空字符串(比如
MyString s1;)。 关键点来了!new是 C++ 里的"圈地"命令。它会向操作系统申请一块内存。虽然是空字符串,但我们在底层也要申请 1 个字节的空间,用来放'\0'。 - 带参构造函数 :用一个 C 语言风格的字符串来初始化(比如
MyString s2("Hello");)。
cpp
// 1. 默认构造函数
MyString::MyString() : _length(0), _capacity(0)
{
_data = new char[1];
_data[0] = '\0';
}
// 2. 带参构造函数
MyString::MyString(const char *str)
{
if (str != nullptr)
{
_length = strlen(str);
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, str);
}
else
{
_length = 0;
_capacity = 0;
_data = new char[1];
_data[0] = '\0';
}
}
疑惑解答:带参构造函数里面为什么不直接 _data = str;?
-
如果直接等于,那是"浅拷贝",两个指针指向了同一块地方。一旦外面的
str销毁了,我们的 _data就变成无家可归的"野指针"了。 -
所以必须用
new重新开辟一块属于MyString自己的专属空间(5个字符 + 1个结束符 = 6个字节)。
3.析构函数和c_str()函数
cpp
// 3. 析构函数
MyString::~MyString()
{
delete[] _data;
}
// 辅助函数
const char *MyString::c_str() const
{
return _data ? _data : "";
}
辅助函数:const char* c_str() const
- 干嘛用的 :现在的
MyString是个自定义类,std::cout还不认识它,直接打印会报错。这个函数把内部的底层指针 _data借给外面用一下,方便配合std::cout打印。 - 后面的
const是干嘛的 :这叫"常成员函数"。意思是承诺在这个函数内部,绝对不会悄悄修改类里的任何变量(比如不修改 _length),只是纯粹地读数据。
接下来开始测试一下我们写的函数
cpp
void test1()
{
MyString s1;
cout << s1.c_str() << endl;
MyString s2("hello c++");
cout << s2.c_str() << endl;
}
4.拷贝构造函数(防止浅拷贝)
如果你自己不写,C++ 编译器会非常热心地帮你自动生成一个默认的拷贝构造函数。但这个默认的函数是个"睁眼瞎",它只会搞浅拷贝(Shallow Copy)。
浅拷贝的灾难 :它只是简单地把 s1 内部的 _length 和 _data 指针的值复制给 s2。结果就是 s1._data 和 s2._data 指向了堆内存中的同一块地方
后果:
-
互相牵连 :你改
s2的字符,s1的字符也跟着变了,它们没有独立主权。 -
内存崩溃(Double Free) :当函数结束,
s2先死,调用析构函数把这块内存delete[]了。接着s1死,它又去delete[]这块已经被释放的内存!程序瞬间崩溃。
cpp
// 4. 拷贝构造函数
MyString::MyString(const MyString &other)
{
_length = other._length;
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, other._data);
}
-
为什么参数是
const:因为我们只是去"抄作业"的,我们保证在复制的过程中,绝对不会不小心修改了被抄的对象(other)。 -
为什么一定要加引用符号
&?:-
如果不加
&,参数就会变成"值传递"。 -
值传递在传参的一瞬间,又会去调用拷贝构造函数。
-
这样就会陷入:为了调用拷贝构造函数 -> 需要传参 -> 传参又触发拷贝构造函数 -> 再次传参... 的死循环(无限递归),直到电脑内存耗尽程序崩溃。
-
加上
&变成引用,就只是给旧对象起个别名叫other,直接拿来用,不会触发新的拷贝。
-
接下来我们测试一下,同时补充两个辅助函数
cpp
size_t MyString::size() const
{
return _length;
}
void MyString::set_char(size_t index, char ch)
{
if (index < _length)
{
_data[index] = ch;
}
}
cpp
void test2()
{
MyString s1("hello");
cout << s1.c_str() << endl;
MyString s2 = s1;
cout << s2.c_str() << endl;
s2.set_char(0, 'B');
cout << "修改后s2为:" << s2.c_str() << endl;
}
5.拷贝赋值运算符
很多人经常把这一步和上一步的"拷贝构造"搞混。我们先用一句话区分它们:
-
拷贝构造 :对象刚诞生,用别人来初始化自己(比如
MyString s2 = s1;)。 -
拷贝赋值 :对象已经出生很久了 ,中途想换个新值(比如
MyString s1("Hello"); MyString s2("World"); s2 = s1;)。
cpp
// 5. 拷贝赋值运算符
MyString &MyString::operator=(const MyString &other)
{
if (this == &other)
{
return *this;
}
delete[] _data;
_length = other._length;
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, other._data);
return *this;
}
为什么要检查 if (this == &other)?(天坑一:自我毁灭)
-
this是当前对象的指针,&other是传进来的对象的地址。 -
想象一下,如果用户不小心写了
s1 = s1;。如果你不加这行检查,程序会直接执行第二步:delete[] _data;。 -
结果就是:你把自己的数据删了,紧接着第三步你试图去复制
other._data,但other就是你自己,它的数据已经被你删干净了! 这就叫自我毁灭。所以必须先检查是不是同一个人。
为什么要 delete[] _data;?(天坑二:内存泄漏)
-
因为
s2在被赋值之前,它自己肚子里已经有一块用new申请的内存了(比如装了"World")。 -
如果你不先
delete[]掉它,就直接让 _data = new ...指向新房子,那么"World"那块内存就彻底断联了,变成了无法回收的僵尸内存。
为什么返回值是 MyString&,最后要 return *this;?
-
C++ 允许你写这样的代码:
s3 = s2 = s1;(把 s1 赋给 s2,再把结果赋给 s3)。 -
为了支持这种连等操作,
operator=必须把赋值完成后的自己 当作结果返回回去。*this代表当前对象本身,返回它的引用MyString&效率最高。
接下来我们再测试一下
cpp
void test3()
{
MyString s1("Apple");
MyString s2("Banana");
MyString s3("Orange");
cout << "赋值前:" << endl;
cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << ", s3: " << s3.c_str() << endl;
// 测试点 1:连续赋值 (s3 = s2 = s1)
s3 = s2 = s1;
cout << "\n连续赋值后:" << endl;
cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << ", s3: " << s3.c_str() << endl;
// 测试点 2:修改 s2,看看 s1 和 s3 会不会被影响(验证深拷贝)
s2.set_char(0, 'G'); // 把 s2 改成 "Gpple"
cout << "\n修改 s2 后的独立性测试:" << endl;
cout << "s1 (应该还是 Apple): " << s1.c_str() << endl;
cout << "s2 (应该变成了 Gpple): " << s2.c_str() << endl;
cout << "s3 (应该还是 Apple): " << s3.c_str() << endl;
// 测试点 3:自我赋值测试,确保不崩溃
s1 = s1;
cout << "\n自我赋值后 s1 依然完好: " << s1.c_str() << endl;
}
6.移动语义(现代写法)
-
拷贝语义 :买一个新笔记本,把 1 万字抄一遍,然后把旧笔记本烧了。这叫深拷贝,巨慢!
-
移动语义 :直接把旧笔记本上的名字标签撕了,贴上你的新名字。文字根本没动。这叫移动,瞬间完成!
移动语义的核心思想就是:既然你这个临时对象马上就要死了,那与其让我拷贝一遍,不如把你的内存指针直接"偷"过来据为己有!
cpp
// 6. 移动构造函数
MyString::MyString(MyString &&other) noexcept
: _data(other._data), _length(other._length), _capacity(other._capacity)
{
other._data = nullptr;
other._length = 0;
other._capacity = 0;
cout << "【触发移动构造函数】" << endl;
}
// 7. 移动赋值运算符
MyString &MyString::operator=(MyString &&other) noexcept
{
cout << "【触发移动赋值运算符】" << endl;
if (this != &other)
{
delete[] _data;
_data = other._data;
_length = other._length;
_capacity = other._capacity;
other._data = nullptr;
other._length = 0;
other._capacity = 0;
}
return *this;
}
注意那个 &&!它读作"右值引用"。只要传进来的是临时对象,编译器就会自动优先调用这个函数,而不是拷贝构造。
noexcept 是一个关键字,它的字面意思是 "No Exception"(不抛出异常) 。把它写在函数后面,就是向编译器和系统发出的一个硬核保证
为什么要执行 other._data = nullptr;?
-
这是移动语义中最关键的一步! * 此时,我们的 _
data指针已经指到了other._data的地盘。如果我们不把other._data改为nullptr,那么一会儿other生命周期结束调用析构函数时,就会执行delete[] _data;。 -
结果就是:把我们刚刚偷过来的精美大宅子,直接给炸飞了!
-
把它变成
nullptr之后,other析构时执行delete[] nullptr;,这在 C++ 里是完全安全且合法的,啥也不会发生。
在 main 函数中进行测试
因为平时我们写代码时很难手动制造一个"完美的临时对象",所以 C++ 提供了 std::move() 函数,它可以把一个普通的左值强制伪装成一个"快要死掉的临时对象",从而强行触发移动语义。
cpp
void test3()
{
MyString s1("very long string");
cout << "s1初始内容:" << s1.c_str() << endl;
MyString s2 = move(s1);
cout << "移动后" << endl;
cout << "s2的内容:" << s2.c_str() << endl;
cout << "s1的内容:" << s1.c_str() << endl;
}
7.重载下标操作符
为什么要重载两次?
在 C++ 中,当我们想要通过 s[index] 去访问字符串里的字符时,通常有两种情况:
-
读写模式 :比如
s[0] = 'A';,我们要去修改里面的字符。 -
只读模式 :比如把字符串传给一个只读函数
void print(const MyString& s)时,我们只想打印s[0],不能修改。
为了应对这两种情况,我们必须重载两个版本的 operator[]:一个是普通版本(返回引用,可修改),一个是常数版本(返回常量引用,只读)。
简单起见,这里不进行越界检查。 在真实的 std::string 中,\[\] 通常也不检查越界(为了极致性能), 另一个函数 at() 才会检查越界并抛出异常
cpp
// 8. 下标操作符 (普通)
char &MyString::operator[](size_t index)
{
return _data[index];
}
// 9. 下标操作符 (常量)
const char &MyString::operator[](size_t index) const
{
return _data[index];
}
在 main 函数中进行测试
cpp
void print_first_char(const MyString &str)
{
if (str.size() > 0)
{
cout << "函数内部读取的首字母: " << str[0] << endl;
}
}
void test4()
{
MyString s1("hello");
cout << "初始内容:" << s1.c_str() << endl;
s1[0] = 'H';
cout << "修改s1[0]后:" << s1.c_str() << endl;
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
print_first_char(s1);
}
8.字符串拼接
operator+=(追加) :比如 s1 += s2。这会改变 s1 自己 的内容。我们需要在堆内存里开辟一块更大的新房子,把 s1 原来的字搬进去,再把 s2 的字接着搬进去,最后把 s1 的旧房子炸掉。
operator+(相加) :比如 s3 = s1 + s2。注意,s1 和 s2 都不能被改变! 我们需要凭空创造一个全新的字符串 s3 来装它们俩的结果。
cpp
// 10. 追加操作符 (+=)
MyString &MyString::operator+=(const MyString &other)
{
if (other._length == 0)
return *this;
size_t new_length = _length + other._length;
char *new_data = new char[new_length + 1];
if (_data)
{
strcpy(new_data, _data);
}
else
{
new_data[0] = '\0';
}
if (other._data)
{
strcat(new_data, other._data);
}
delete[] _data;
_data = new_data;
_length = new_length;
_capacity = new_length;
return *this;
}
在 MyString 类外部 添加 operator+
之所以不把它写进类里面,不是因为不能,而是为了追求 C++ 中至高无上的"对称性(Symmetry)"。我们用一个非常现实的例子来解释。因为写在类里面的函数默认第一个参数其实是this指针。
- 正常运行
MyString s1("World");
MyString s2 = s1 + "Hello"; // 相当于 s1.operator+("Hello")
- 直接报错崩溃
MyString s2("World");
MyString s3 = "Hello" + s2; // 相当于 "Hello".operator+(s2) ???
你可能会问:"它既然在类外面,那如果它需要读取类里面的 private 变量(比如 data_),它不是没权限吗?"
有两种解决办法:
-
高级写法(我们用的这种) :我们在全局
+里面,只调用了类内部公开的+=运算符。因为+=是类的内部成员,它可以合法访问所有私有变量。这种不依赖特权、只用公开接口的写法,在设计模式里叫低耦合,是非常高级的代码风格。 -
特殊通行证(
friend友元) :如果它真的非要直接读私有变量,我们可以在类内部给它开个后门,写一行friend MyString operator+(...);。这样它就成了这个类的"好基友",可以随意翻看私有隐私了。
cpp
// 11. 相加操作符 (+)
MyString operator+(const MyString &lhs, const MyString &rhs)
{
MyString result(lhs);
result += rhs;
return result;
}
还记得我们第六步学的移动语义 吗? 这里的 operator+ 在 return result; 的时候,返回的是一个临时对象 。当你写 MyString s3 = s1 + s2; 时,编译器会发现 s1 + s2 产生的结果是个即将死去的临时工,于是完美触发了我们之前写的移动构造函数(Move Constructor) !s3 会直接把 result 的底层内存"偷"走,完全不需要再发生一次深拷贝!性能直接拉满!
在 main 函数中进行测试
cpp
void test5()
{
MyString s1("hello");
MyString s2(" world");
cout << "测试 + 号" << endl;
MyString s3 = s1 + s2;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
cout << "测试 += 号" << endl;
s1 += s2;
cout << s1.c_str() << endl;
}
9.重载输出流操作符
为什么它也必须写在类外面?
- std::cout << s1;在这个式子里,左边 的操作数是
std::cout(它属于std::ostream类),右边 的操作数才是我们的MyString。 - 如果我们要把
<<写成MyString的成员函数,那我们的字符串就必须放在左边,代码就会变成极其反人类的:s1 << std::cout; - 既然左边是被 C++ 标准库霸占的
std::ostream,我们又没有权限去修改 C++ 原生的std::ostream源码,那唯一的办法就是:在外面写一个全局函数,把std::cout和MyString当作两个平等的参数传进去。
cpp
// 12. 重载输出流操作符 (<<)
ostream &operator<<(ostream &os, const MyString &str)
{
os << str.c_str();
return os;
}
这里的参数和返回值大有学问:
-
第一个参数
std::ostream& os: 这里必须 加&(引用)。因为 C++ 严禁拷贝输入输出流对象(你想想,屏幕和键盘怎么能被复制呢?)。所以我们只能借用原本的那个cout。 -
为什么返回值是
std::ostream&,且最后要return os;? 这是为了支持连续打印(链式调用) ! 当你写std::cout << s1 << " " << s2;时:-
系统先执行
std::cout << s1。 -
我们写好的函数打印完
s1后,把std::cout又返回了回来。 -
于是式子变成了
std::cout << " ",继续打印空格。 -
空格打印完,又返回
cout,接着打印s2。 一环扣一环,完美衔接!
-
在 main 函数中进行测试 我们可以把之前所有臃肿的 .c_str() 全都删掉了
cpp
void test6()
{
MyString s1("hello");
MyString s2("C++");
MyString s3("world");
cout << s1 << " " << s2 << " " << s3 << endl;
}
10.让字符串可以被比较
还记得我们在写 operator+ 时学到的"对称性(Symmetry)"吗?
如果我们要判断 "Hello" == s1,为了让原生的 C 风格字符串也能在左边公平地参与比较,所有的比较运算符都必须写在类外面,做成全局函数。
在实现比较时,我们不用自己去傻乎乎地用 for 循环一个一个字符对比,C 语言的标准库里早就为我们准备好了神器:std::strcmp()。
-
strcmp(a, b) == 0:说明两者完全相等。 -
strcmp(a, b) < 0:说明 a 的字典序排在 b 前面(比如 "apple" < "banana")。 -
strcmp(a, b) > 0:说明 a 的字典序排在 b 后面。
cpp
// 13. 比较操作符
bool operator==(const MyString &lhs, const MyString &rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
bool operator!=(const MyString &lhs, const MyString &rhs)
{
return !(lhs == rhs);
}
bool operator<(const MyString &lhs, const MyString &rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}
bool operator>(const MyString &lhs, const MyString &rhs)
{
return rhs < lhs;
}
bool operator<=(const MyString &lhs, const MyString &rhs)
{
return !(lhs > rhs);
}
bool operator>=(const MyString &lhs, const MyString &rhs)
{
return !(lhs < rhs);
}
在 main 函数中进行测试
cpp
void test7()
{
MyString s1("apple");
MyString s2("banana");
MyString s3("apple");
cout << (s1 == s3 ? "Yes" : "No") << endl;
cout << (s1 != s2 ? "Yes" : "No") << endl;
}
11.重载输入流操作符和实现全局的getline函数
标准 C++ 的 cin >> 有一个雷打不动的规矩:跳过前面的空格/回车,遇到货就装,一旦再次遇到空格/回车,就立刻停手。
operator>> 的实现代码拆成三步
- 清空旧仓库 2. 扔掉传送带前段的"垃圾"(空格、回车)3. 用"临时水桶"装货
cpp
// 14. 重载流提取运算符 (>>)
istream &operator>>(istream &is, MyString &str)
{
// 先清空原本字符串里的旧数据(直接赋一个空字符串对象)
str = MyString();
char ch;
// 跳过前导空白字符(空格、回车、制表符等)
while (is.get(ch) && isspace(ch))
{
}
// 如果读到文件尾(EOF)或者流出错,直接返回
if (!is)
return is;
// 此时 ch 已经是第一个有效字符了
char buf[1024];
size_t index = 0;
buf[index++] = ch;
// 循环读取,直到遇到下一个空白字符
while (is.get(ch) && !isspace(ch))
{
buf[index++] = ch;
if (index >= 1023)
{
buf[index] = '\0';
str += MyString(buf);
index = 0;
}
}
// 把最后残留在缓冲区的数据追加到 str
if (index > 0)
{
buf[index] = '\0';
str += MyString(buf);
}
return is;
}
工人开始从传送带上拿东西(is.get(ch) 表示拿一个字符)
isspace(ch) 是个判断函数:如果这个字符是空格、制表符(Tab)或者回车,它就返回"真"。
因为我们不知道用户到底会输入多长的字符串,如果每拿一个字符,就去堆内存申请一次空间(new 一次),电脑会累死。所以我们准备了一个能装 1024 个字符的大水桶 char buf[1024]。
为什么不用 cin >> ch 而是用 is.get(ch)?
- 因为
cin >>实在太傲娇了,它一旦看到空格,连碰都不碰。 如果我们用is >> ch,那第二个while循环(判断什么时候结束的那个)就永远没办法读到单词后面的那个空格!它会误以为后面一直有货,导致把所有单词连在一起读进来。 - 而
is.get(ch)就像一个毫无感情的机器人,不管是字母、数字、还是空格、换行,只要在传送带上,它通通都能拿起来。这样我们才能通过isspace(ch)亲手去掌控"什么时候该停手"。
getline函数
getline 的核心目标只有一个:"不管你里面有几个空格、几个Tab,只要没遇到那堵'叹息之墙'(换行符),我就全给你铲进箱子里!"
cpp
// 15. 实现 getline 函数
// 核心逻辑:不跳过前导空白,一直读取直到遇到指定的结束符(默认是 '\n')
istream &getline(istream &is, MyString &str, char delim)
{
// 清空原本的数据
str = MyString();
char buf[1024];
size_t index = 0;
char ch;
// 循环读取,直到读到分隔符或者流结束
while (is.get(ch) && ch != delim)
{
buf[index++] = ch;
if (index >= 1023) // 缓冲区满了,先倒进 str
{
buf[index] = '\0';
str += MyString(buf);
index = 0;
}
}
// 倒出最后残留的数据
if (index > 0)
{
buf[index] = '\0';
str += MyString(buf);
}
return is;
}
你仔细看 getline 的代码,相比之前的 cin >>,它少了一个 while (isspace(ch)) 的循环。
ch != delim:看看这个字符是不是 delim。我们前面设定了 delim 默认是 '\n'(回车键)。所以这就相当于问:"这是回车键吗?" 如果不是,就继续装!
在 main.cpp 里面测试
cpp
void test8()
{
MyString s1;
MyString s2;
cout << "========= 测试 cin >> =========" << endl;
cout << "请输入两个单词(用空格隔开): ";
cin >> s1 >> s2;
cout << "读取到的单词 1: " << s1 << endl;
cout << "读取到的单词 2: " << s2 << endl;
// 吸收掉残留的回车符,防止影响接下来的 getline
char flush_char;
cin.get(flush_char);
cout << "\n========= 测试 getline =========" << endl;
cout << "请输入一整行带空格的句子: ";
getline(cin, s1);
cout << "读取到的整行内容为: " << s1 << endl;
}
12.完整代码
MyString.h
cpp
#ifndef MYSTRING_H
#define MYSTRING_H
#include <iostream>
#include <cstring>
#include <utility> // 为了 move
class MyString
{
private:
char *_data;
size_t _length;
size_t _capacity;
public:
// 构造与析构
MyString();
MyString(const char *str);
~MyString();
// 拷贝构造与移动构造
MyString(const MyString &other);
MyString(MyString &&other) noexcept;
// 赋值运算符
MyString &operator=(const MyString &other);
MyString &operator=(MyString &&other) noexcept;
// 常用功能
const char *c_str() const;
size_t size() const;
void set_char(size_t index, char ch);
// 运算符重载 (成员函数)
char &operator[](size_t index);
const char &operator[](size_t index) const;
MyString &operator+=(const MyString &other);
};
// 运算符重载 (非成员函数声明)
MyString operator+(const MyString &lhs, const MyString &rhs);
std::ostream &operator<<(std::ostream &os, const MyString &str);
std::istream &operator>>(std::istream &is, MyString &str);
std::istream &getline(std::istream &is, MyString &str, char delim = '\n');
bool operator==(const MyString &lhs, const MyString &rhs);
bool operator!=(const MyString &lhs, const MyString &rhs);
bool operator<(const MyString &lhs, const MyString &rhs);
bool operator>(const MyString &lhs, const MyString &rhs);
bool operator<=(const MyString &lhs, const MyString &rhs);
bool operator>=(const MyString &lhs, const MyString &rhs);
#endif // MYSTRING_H
MyString.cpp
cpp
#include "MyString.h"
using namespace std; // 在 .cpp 文件中可以使用这个,不影响其他文件
// 1. 默认构造函数
MyString::MyString() : _length(0), _capacity(0)
{
_data = new char[1];
_data[0] = '\0';
}
// 2. 带参构造函数
MyString::MyString(const char *str)
{
if (str != nullptr)
{
_length = strlen(str);
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, str);
}
else
{
_length = 0;
_capacity = 0;
_data = new char[1];
_data[0] = '\0';
}
}
// 3. 析构函数
MyString::~MyString()
{
delete[] _data;
}
// 辅助函数
const char *MyString::c_str() const
{
return _data ? _data : "";
}
size_t MyString::size() const
{
return _length;
}
void MyString::set_char(size_t index, char ch)
{
if (index < _length)
{
_data[index] = ch;
}
}
// 4. 拷贝构造函数
MyString::MyString(const MyString &other)
{
_length = other._length;
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, other._data);
}
// 5. 拷贝赋值运算符
MyString &MyString::operator=(const MyString &other)
{
if (this == &other)
{
return *this;
}
delete[] _data;
_length = other._length;
_capacity = _length;
_data = new char[_capacity + 1];
strcpy(_data, other._data);
return *this;
}
// 6. 移动构造函数
MyString::MyString(MyString &&other) noexcept
: _data(other._data), _length(other._length), _capacity(other._capacity)
{
other._data = nullptr;
other._length = 0;
other._capacity = 0;
cout << "【触发移动构造函数】" << endl;
}
// 7. 移动赋值运算符
MyString &MyString::operator=(MyString &&other) noexcept
{
cout << "【触发移动赋值运算符】" << endl;
if (this != &other)
{
delete[] _data;
_data = other._data;
_length = other._length;
_capacity = other._capacity;
other._data = nullptr;
other._length = 0;
other._capacity = 0;
}
return *this;
}
// 8. 下标操作符 (普通)
char &MyString::operator[](size_t index)
{
return _data[index];
}
// 9. 下标操作符 (常量)
const char &MyString::operator[](size_t index) const
{
return _data[index];
}
// 10. 追加操作符 (+=)
MyString &MyString::operator+=(const MyString &other)
{
if (other._length == 0)
return *this;
size_t new_length = _length + other._length;
char *new_data = new char[new_length + 1];
if (_data)
{
strcpy(new_data, _data);
}
else
{
new_data[0] = '\0';
}
if (other._data)
{
strcat(new_data, other._data);
}
delete[] _data;
_data = new_data;
_length = new_length;
_capacity = new_length;
return *this;
}
// --- 下面是非成员函数的实现 ---
// 11. 相加操作符 (+)
MyString operator+(const MyString &lhs, const MyString &rhs)
{
MyString result(lhs);
result += rhs;
return result;
}
// 12. 重载输出流操作符 (<<)
ostream &operator<<(ostream &os, const MyString &str)
{
os << str.c_str();
return os;
}
// 14. 重载流提取运算符 (>>)
istream &operator>>(istream &is, MyString &str)
{
// 先清空原本字符串里的旧数据(直接赋一个空字符串对象)
str = MyString();
char ch;
// 跳过前导空白字符(空格、回车、制表符等)
while (is.get(ch) && isspace(ch))
{
}
// 如果读到文件尾(EOF)或者流出错,直接返回
if (!is)
return is;
// 此时 ch 已经是第一个有效字符了
char buf[1024];
size_t index = 0;
buf[index++] = ch;
// 循环读取,直到遇到下一个空白字符
while (is.get(ch) && !isspace(ch))
{
buf[index++] = ch;
if (index >= 1023)
{
buf[index] = '\0';
str += MyString(buf);
index = 0;
}
}
// 把最后残留在缓冲区的数据追加到 str
if (index > 0)
{
buf[index] = '\0';
str += MyString(buf);
}
return is;
}
// 15. 实现 getline 函数
// 核心逻辑:不跳过前导空白,一直读取直到遇到指定的结束符(默认是 '\n')
istream &getline(istream &is, MyString &str, char delim)
{
// 清空原本的数据
str = MyString();
char buf[1024];
size_t index = 0;
char ch;
// 循环读取,直到读到分隔符或者流结束
while (is.get(ch) && ch != delim)
{
buf[index++] = ch;
if (index >= 1023) // 缓冲区满了,先倒进 str
{
buf[index] = '\0';
str += MyString(buf);
index = 0;
}
}
// 倒出最后残留的数据
if (index > 0)
{
buf[index] = '\0';
str += MyString(buf);
}
return is;
}
// 13. 比较操作符
bool operator==(const MyString &lhs, const MyString &rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
bool operator!=(const MyString &lhs, const MyString &rhs)
{
return !(lhs == rhs);
}
bool operator<(const MyString &lhs, const MyString &rhs)
{
return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}
bool operator>(const MyString &lhs, const MyString &rhs)
{
return rhs < lhs;
}
bool operator<=(const MyString &lhs, const MyString &rhs)
{
return !(lhs > rhs);
}
bool operator>=(const MyString &lhs, const MyString &rhs)
{
return !(lhs < rhs);
}
main.cpp
cpp
#include "MyString.h"
using namespace std;
void test1()
{
MyString s1;
cout << s1.c_str() << endl;
MyString s2("hello c++");
cout << s2.c_str() << endl;
}
void test2()
{
MyString s1("hello");
cout << s1.c_str() << endl;
MyString s2 = s1;
cout << s2.c_str() << endl;
s2.set_char(0, 'B');
cout << "修改后s2为:" << s2.c_str() << endl;
}
void test3_1()
{
MyString s1("Apple");
MyString s2("Banana");
MyString s3("Orange");
cout << "赋值前:" << endl;
cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << ", s3: " << s3.c_str() << endl;
// 测试点 1:连续赋值 (s3 = s2 = s1)
s3 = s2 = s1;
cout << "\n连续赋值后:" << endl;
cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << ", s3: " << s3.c_str() << endl;
// 测试点 2:修改 s2,看看 s1 和 s3 会不会被影响(验证深拷贝)
s2.set_char(0, 'G'); // 把 s2 改成 "Gpple"
cout << "\n修改 s2 后的独立性测试:" << endl;
cout << "s1 (应该还是 Apple): " << s1.c_str() << endl;
cout << "s2 (应该变成了 Gpple): " << s2.c_str() << endl;
cout << "s3 (应该还是 Apple): " << s3.c_str() << endl;
// 测试点 3:自我赋值测试,确保不崩溃
s1 = s1;
cout << "\n自我赋值后 s1 依然完好: " << s1.c_str() << endl;
}
void test3_2()
{
MyString s1("very long string");
cout << "s1初始内容:" << s1.c_str() << endl;
MyString s2 = move(s1);
cout << "移动后" << endl;
cout << "s2的内容:" << s2.c_str() << endl;
cout << "s1的内容:" << s1.c_str() << endl;
}
void print_first_char(const MyString &str)
{
if (str.size() > 0)
{
cout << "函数内部读取的首字母: " << str[0] << endl;
}
}
void test4()
{
MyString s1("hello");
cout << "初始内容:" << s1.c_str() << endl;
s1[0] = 'H';
cout << "修改s1[0]后:" << s1.c_str() << endl;
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i] << " ";
}
cout << endl;
print_first_char(s1);
}
void test5()
{
MyString s1("hello");
MyString s2(" world");
cout << "测试 + 号" << endl;
MyString s3 = s1 + s2;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
cout << "测试 += 号" << endl;
s1 += s2;
cout << s1.c_str() << endl;
}
void test6()
{
MyString s1("hello");
MyString s2("C++");
MyString s3("world");
cout << s1 << " " << s2 << " " << s3 << endl;
}
void test7()
{
MyString s1("apple");
MyString s2("banana");
MyString s3("apple");
cout << (s1 == s3 ? "Yes" : "No") << endl;
cout << (s1 != s2 ? "Yes" : "No") << endl;
}
void test8()
{
MyString s1;
MyString s2;
cout << "========= 测试 cin >> =========" << endl;
cout << "请输入两个单词(用空格隔开): ";
cin >> s1 >> s2;
cout << "读取到的单词 1: " << s1 << endl;
cout << "读取到的单词 2: " << s2 << endl;
// 吸收掉残留的回车符,防止影响接下来的 getline
char flush_char;
cin.get(flush_char);
cout << "\n========= 测试 getline =========" << endl;
cout << "请输入一整行带空格的句子: ";
getline(cin, s1);
cout << "读取到的整行内容为: " << s1 << endl;
}
int main()
{
cout << "--- 测试 ---" << endl;
// test1();
// test2();
// test3();
// test4();
// test5();
// test6();
// test7();
test8();
return 0;
}