文章目录
我们在使用STL内置的string类型的时候:
#include <string>
int main () {
string str1;
string str2 = "aaa"; // String(const char*)
string str3 = "bbb";
string str4 = str2 + str3;
string str5 = str2 + "ccc";
string str6 = "ddd" + str2;
cout << "str6: " << str6 << endl;
if (str5 > str6) {
cout << str5 << " > " << str6 << endl;
} else {
cout << str5 << " < " << str6 << endl;
}
int len = str6.length();
for (int i = 0; i < len; ++i) {
//str6.operator[](i)
cout << str6[i] << " ";
}
cout << endl;
// string -> char*
char buf[1024] = { 0 };
strcpy(buf, str6.c_str()); //c_str()返回一个const char*类型
cout << "buf: " << buf << endl;
return 0;
}
从上文代码可以看出,STL中的string类型包含了加法运算符重载、拷贝构造、重载赋值运算符,重载了<< ,重载了[],让string能够像数组一样完成调用。
现在我们一步步实现代码吧。
确定成员变量
class string {
public:
private:
char *_pstr;
}
确定构造函数和析构函数
String(const char *p = nullptr) {
if (p != nullptr) {
_pstr = new char[strlen(p) + 1]
strcpy(_pstr, p);
} else {
_pstr = new char[1];
*_pstr = '\0';
}
}
- 构造函数参数:
从string str2 = "aaa";
可以看出,我们的构造函数应该接受一个常量指针参数’p’,默认值为nullptr,意味着如果创建对象时没有提供参数,p 将默认为空指针。 - 检查参数是否为空
- 分配内存并复制字符串
strlen(p) + 1
:计算字符串 p 的长度,并加上 1 以包含终止字符 \0。new char[strlen(p) + 1]
:为 _pstr 分配足够的内存来存储传入的字符串。strcpy(_pstr, p)
:将传入的字符串 p 复制到 _pstr 所指向的内存区域。
- 处理空指针的情况
如果p
是空指针,则分配一个大小为 1 的字符数组,并将其初始化为空字符串(即仅包含终止字符\0
)。
关于析构函数就比较简单了。
直接delete掉指针指向的堆内存,然后指针置为空。
~String() {
delete[] _pstr;
_pstr = nullptr;
}
确定拷贝构造和复制构造
String(const String &str) {
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
拷贝构造很简单,直接开辟一个新内存,然后把源字符串的内容拷贝进去即可。
String& operator=(const String &str) {
if (this == &str) return *this;
delete[] _pstr;
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
return *this;
}
关于赋值运算符,首先要看自赋值的情况,如果没有自赋值,释放当前对象的已有内存资源,以防止内存泄漏。
重载 > == <
bool operator>(const String &str) const {
return strcmp(_pstr, str._pstr) > 0;
}
bool operator<(const String &str) const {
return strcmp(_pstr, str._pstr) < 0;
}
bool operator==(const String &str) const {
return strcmp(_pstr, str._pstr) == 0;
}
比较简单不解释
重载[]
重载下标运算符 operator[],以便访问字符串中的字符。这里定义了两个重载版本的 operator[],分别用于非常量和常量对象。这两个重载函数实现了不同的访问权限:一个允许修改字符串中的字符,另一个只允许读取字符。
char& operator[](int index) { return _pstr[index]; }
const char& operator[](int index) const { return _pstr[index]; }
实现length()和c_str()
length()和c_str()分别返回字符串的有效字符长度和C风格字符串,其实就是返回一个const char*类型
int length() const { return strlen(_pstr); }
const char* c_str() const { return _pstr;}
重载 operator+ 和 operator<<
为了实现字符串的拼接str1 + str2
,我们必须重载operator+。并且选择将其作为全局函数并在类中声明为友元有一些优势,尤其是在涉及对称性和类型转换的情况下。
- 对称性主要指的是我们可以处理“其他类型+String”、“String+其他类型”和“String+String”,如果我们把operator+作为成员函数,那么第一个操作数必须是类的实例:
String operator+(const String &str) const { ... }
- 类型转换主要指的也是上面这种情况,编译器可以处理 “其他类型 + String”和“String + 其他类型”的情况。(这是编译器自己进行处理的隐式类型转换)
具体实现如下:
String operator+(const String &lhs, const String &rhs) {
char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
strcpy(ptmp, lhs._pstr);
strcat(ptmp, rhs._pstr);
String tmp(ptmp);
delete[] ptmp;
return tmp;
}
- 计算新字符串的长度:新字符串的长度是 lhs 和 rhs 两个字符串长度之和,再加上一个用于存储终止空字符 \0 的额外空间。
- 分配内存:使用 new 分配足够的内存来存储新字符串。
- 复制和连接字符串:使用 strcpy 将 lhs 的字符串复制到新分配的内存中。使用 strcat 将 rhs 的字符串连接到新字符串的末尾。
- 创建临时 String 对象:使用连接后的字符串创建一个临时的 String 对象(这个临时对象在堆上)。
- 释放临时内存:删除用于临时存储连接字符串的内存。
- 返回新 String 对象:返回包含连接字符串的临时 String 对象。
重载<<就相当简单了,就是基本套路
ostream& operator<<(ostream &out, const String &str) {
out << str._pstr;
return out;
}
随后我们记得声明友元:
private:
char *_pstr;
friend ostream& operator<<(ostream &out, const String &str);
friend String operator+(const String &lhs, const String &rhs);
测试
这里再次使用代码进行测试:
int main () {
String str1;
String str2 = "aaa"; // String(const char*)
String str3 = "bbb";
String str4 = str2 + str3;
String str5 = str2 + "ccc";
String str6 = "ddd" + str2;
cout << "str6: " << str6 << endl;
if (str5 > str6) {
cout << str5 << " > " << str6 << endl;
} else {
cout << str5 << " < " << str6 << endl;
}
int len = str6.length();
for (int i = 0; i < len; ++i) {
//str6.operator[](i)
cout << str6[i] << " ";
}
cout << endl;
// String -> char*
char buf[1024] = { 0 };
strcpy(buf, str6.c_str()); //c_str()返回一个const char*类型
cout << "buf: " << buf << endl;
return 0;
}
可以自行测试,肯定是和我们使用std::string输出是一样的。
结语
我们还需要重点关注operator+
,其实该函数功能上确实是没有问题的,而且也没有内存泄漏的问题。
String operator+(const String &lhs, const String &rhs) {
char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
strcpy(ptmp, lhs._pstr);
strcat(ptmp, rhs._pstr);
String tmp(ptmp);
delete[] ptmp;
return tmp;
}
但是该函数的效率比较低,主要是因为
首先char *ptmp = new char[...]
,ptmp指向了我们new出来的一块大内存,然后我们分别进行字符串拷贝和字符串连接;
随后传入到tmp构造函数中String tmp(ptmp);
。然后这个tmp又会根据我们外面传进来的指针再进行一次new,然后再次进行拷贝strcpy;
String(const char *p = nullptr) {
if (p != nullptr) {
_pstr = new char[strlen(p) + 1]
strcpy(_pstr, p);
} else {
_pstr = new char[1];
*_pstr = '\0';
}
}
最后我们又delete掉ptmpdelete[] ptmp;
。
虽然return tmp,由于tmp是这个函数的局部对象,所以tmp要析构,所以又把我们在String tmp(ptmp)
中new的那块内存又delete掉了。
也就是说,里面总共两次new了内存,又两次delete(这里考虑了ROV),未免太麻烦了。在下一节,我们会讨论如何解决该问题
解决operator+效率低下的问题
我们采用如下解决方案:
String operator+(const String &lhs, const String &rhs) {
//char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
String tmp;
tmp._pstr = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1]
strcpy(tmp._pstr, lhs._pstr);
strcat(tmp._pstr, rhs._pstr);
//delete[] ptmp;
return tmp;
}
直接定义一个tmp对象,我直接给它的底层进行一个内存开辟,然后在进行字符串拷贝和连接直接给到临时对象底层的内存中。这样我们就避免了一个new和delete。
我们在第一次优化中,仍然返回的是一个tmp的值,这里面如果我想去返回引用那是不行的,因为我们不能返回一个局部对象的地址或者引用返回到函数外面去。
这里面我们一定要给我们的string提供右值引用的拷贝构造和赋值重载函数,这里一直还是我们要说的对象优化问题。以后有时间一定写出来…
扩展内容:实现string::iterator迭代器
迭代器是连接STL容器和STL算法的桥梁,所以无论如何,我们都应该实现它的容器迭代器。
请移步:【string字符串对象的迭代器实现string::iterator】
string总体代码(含迭代器)
class String {
public:
String(const char *p = nullptr) {
if (p != nullptr) {
_pstr = new char[strlen(p) + 1];
strcpy(_pstr, p);
} else {
_pstr = new char[1];
*_pstr = '\0';
}
}
~String() {
delete[] _pstr;
_pstr = nullptr;
}
String(const String &str) {
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
String& operator=(const String &str) {
if (this == &str)
return *this;
delete[] _pstr;
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
return *this;
}
bool operator>(const String &str) const {
return strcmp(_pstr, str._pstr) > 0;
}
bool operator==(const String &str) const {
return strcmp(_pstr, str._pstr) == 0;
}
bool operator<(const String &str) const {
return strcmp(_pstr, str._pstr) < 0;
}
int length() const { return strlen(_pstr); }
// char ch = str6[6]; str6[6] = '7'
char& operator[](int index) { return _pstr[index]; }
// char ch = str6[6]; 不允许修改! str6[6] = '7'
const char& operator[](int index) const { return _pstr[index]; }
const char* c_str() const { return _pstr; }
//给String字符串类型提供迭代器的实现
class iterator {
public:
iterator(char *p = nullptr) : _p(p) { }
bool operator!=(const iterator &it) {
return _p != it._p;
}
void operator++() {
++_p;
}
char& operator*() { return *_p;}
private:
char *_p;
};
//begin返回的是容器底层首元素的迭代器表示
iterator begin() { return iterator(_pstr); }
//end返回的是容器底层末尾元素后继位置的迭代器表示
iterator end() { return iterator(_pstr + length()); }
private:
char *_pstr;
friend ostream& operator<<(ostream &out, const String &str);
friend String operator+(const String &lhs, const String &rhs);
};
String operator+(const String &lhs, const String &rhs) {
//char *ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
String tmp;
tmp._pstr = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
strcpy(tmp._pstr, lhs._pstr);
strcat(tmp._pstr, rhs._pstr);
//delete[] ptmp;
return tmp;
}
ostream& operator<<(ostream &out, const String &str) {
out << str._pstr;
return out;
}