自己实现一个string类
1.要求
自己实现一个string类,包含构造函数、拷贝构造函数、析构函数、赋值运算符重载函数、左移运算符重载函数。
class CMyString
{
public:
CMyString(const char* pData = nullptr); // 构造函数
CMyString(const CMyString& str); // 拷贝构造函数
~CMyString(); // 析构函数
CMyString& operator =(const CMyString& str); // 赋值运算符重载函数
friend ostream& operator<<(ostream& out, CMyString& str); // 左移运算符重载
private:
char* m_pData;
};
CMyString
的成员变量是char*指针。
构造函数:参数是const类型(保证字符串常量也能够初始化),默认参数是空指针nullptr。代码实现如下,先判断是否为空指针,如果是,则分配大小为1的char数组空间,用来保存结束符\0
;如果不是,则分配大小为strlen(pData) + 1
的char数组空间,然后将pData
中的数据复制到m_pData
。
CMyString::CMyString(const char* pData)
{
if (pData == nullptr) {
m_pData = new char[1];
*m_pData = '\0';
}
else {
m_pData = new char[strlen(pData) + 1];
strcpy(m_pData, pData);
}
}
拷贝构造函数:函数的参数是const的CMyString类型,并且传入的是引用。注意这里必须传递引用,因为是值传递的话,就会拷贝一份实参str,又会调用拷贝构造函数,此时又是值传递,又会调用拷贝构造函数,从而进入死循环,故必须传入引用。函数的实现如下,和构造函数类似。
CMyString::CMyString(const CMyString & str)
{
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
}
析构函数:释放char类型的指针m_pData对应的内存空间,从而防止内存泄漏。函数实现如下:
CMyString::~CMyString()
{
delete[]m_pData;
}
赋值运算符重载函数:函数的参数是const的CMyString类型,并且传入的是引用,这样能够提高效率,因为值传递会调用一次拷贝构造函数,同时我们在赋值运算符重载函数中不会改变传入实例的状态,因此给传入的引用参数加上const关键字。函数的返回值是CMyString &
,并在函数结束前返回自身的引用*this
,返回引用的目的是为了能够实现连续的赋值,比如str1=str2=str3
。
在实现函数的过程中要注意以下两点:
- 要先释放当前对象的内存,然后再分配新的内存空间,进行复制。如果没有释放,就会造成内存泄漏。
- 要判断是否是自拷贝,如果是传入的参数就是当前的实例
*this
,就不进行赋值,直接返回。如果是自拷贝,先释放了当前实例的内存,即传入进来的参数也被释放,这样就再也找不到传进来的参数了,就无法进行复制了。
CMyString & CMyString::operator=(const CMyString & str)
{
// 如果是自己赋值给自己,直接返回
if (this == &str) return *this;
// 释放当前对象的内存
if (m_pData != nullptr) delete[]m_pData;
// 赋值
m_pData = new char[strlen(str.m_pData)+1];
strcpy(m_pData, str.m_pData);
return *this; //返回当前的实例
}
本来觉得写成这样已经觉得可以了,最近看了剑指offer,发现还可以进行优化。因为这样也会有问题,当释放完当前对象的内存后,如果此时的内存空间不足会导致new char
会抛出异常,而此时实例已经被释放了,m_pData
是一个空指针,这样容易导致程序崩溃。考虑异常安全,我们可以先创建一个临时对象,再交换临时对象和原来的值。代码实现如下:
CMyString & CMyString::operator=(const CMyString & str)
{
// 如果是自己赋值给自己,直接返回
if (this != &str) {
CMyString tmp(str); // 临时对象
char* ptr = tmp.m_pData;
tmp.m_pData = m_pData;
m_pData = ptr;
}
return *this; //返回当前的实例
}
在函数中,先创建了一个临时对象tmp
,然后交换tmp.m_pData
和实例自身的m_pData
。tmp
是一个临时变量当程序执行到if的外面,则离开了tmp
变量的作用域,那么就会自动调用tmp
的析构函数,把tmp.m_pData
所指向的内存释放掉,由于tmp.m_pData
指向的内存就是实例之前的m_pData
所指的内存,这就相当于借用临时变量tmp
的析构函数来释放当前的内存空间。
在考虑异常安全的代码中,我们在CMyString
的构造函数中用new分配内存,如果内存不足抛出异常如bad_alloc
等,这时我们并没有修改原来实例的状态,从而保证了异常安全。
左移运算符重载:需要声明友元函数来实现,函数参数是输出流对象ostream & out
和CMyString & str
,返回值为ostream &
,这里需要返回引用,才能实现连续打印,比如cout<<str1<<str2
。
ostream & operator<<(ostream & out, CMyString & str)
{
out << str.m_pData;
return out;
}
2.代码实现
编译环境:Visual Studio 2017
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class CMyString
{
public:
CMyString(const char* pData = nullptr); // 构造函数
CMyString(const CMyString& str); // 拷贝构造函数
~CMyString(); // 析构函数
CMyString& operator =(const CMyString& str); // 赋值运算符重载函数
friend ostream& operator<<(ostream& out, CMyString& str); // 左移运算符重载
private:
char* m_pData;
};
CMyString::CMyString(const char* pData)
{
if (pData == nullptr) {
m_pData = new char[1];
*m_pData = '\0';
}
else {
m_pData = new char[strlen(pData) + 1];
strcpy(m_pData, pData);
}
}
CMyString::CMyString(const CMyString & str)
{
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
}
CMyString::~CMyString()
{
delete[]m_pData;
}
#if 0
CMyString & CMyString::operator=(const CMyString & str)
{
// 如果是自己赋值给自己,直接返回
if (this == &str) return *this;
// 释放当前对象的内存
if (m_pData != nullptr) delete[]m_pData;
// 赋值
m_pData = new char[strlen(str.m_pData)+1];
strcpy(m_pData, str.m_pData);
return *this; //返回当前的实例
}
#endif
// 进一步优化:考虑异常的安全性,如果释放内存过后,新开辟内存时,空间不足
CMyString & CMyString::operator=(const CMyString & str)
{
// 如果是自己赋值给自己,直接返回
if (this != &str) {
CMyString tmp(str); // 临时对象
char* ptr = tmp.m_pData;
tmp.m_pData = m_pData;
m_pData = ptr;
}
return *this; //返回当前的实例
}
ostream & operator<<(ostream & out, CMyString & str)
{
out << str.m_pData;
return out;
}
int main() {
CMyString str0; //测试默认构造函数
CMyString str1("I am str1"); // 测试有参构造函数
CMyString str2("I am str2");
CMyString str3(str2); // 测试拷贝构造函数
cout << "str0: " << str0 << endl;
cout << "str1: " << str1 << endl;
cout << "str2: " << str2 << endl;
cout << "str3: " << str3 << endl;
str0 = str1; // 测试实例a赋值给实例b
str2 = str2; // 测试自拷贝
cout << "str0: " << str0 << endl;
str0 = str2 = str1; // 测试连续赋值
cout << "str0: " << str0 << endl;
system("pause");
return 0;
}
执行结果:
str0:
str1: I am str1
str2: I am str2
str3: I am str2
str0: I am str1
str0: I am str1
请按任意键继续. . .
3.参考
《剑指offer》