刚接触C++的时候就听说,string不是一个基本类型,而是一个类(class)。但实际使用的时候,却感觉它用起来就像一个基本类型一样方便,这是如何实现的?假如我们自己实现一个string类,要怎样做?
注意:以下所有代码只是为了方便理解,并非string类的真实实现。
构造函数与赋值运算符
首先我们看看string类对象如何初始化。
string可以直接用字符串常量来初始化,也可以用另一个string对象初始化,用起来就和基本类型一样方便。例如:
string s = "string"; // 1 初始化 等价于 string s("string");
string t = s; // 2 初始化 等价于 string t(s);
string u; // 3 初始化
u = "string"; // 4 赋值语句
u = s; // 5 赋值语句
实际上,这是利用了C++类的构造函数实现的。在C++中,初始化一个类可以用括号的方式,也可以用等号的方式,都会调用类的构造函数。上例中,要支持 1 和 2 两种初始化方式分别调用了下面两种构造函数:
class string {
public:
string(const char* s);
string(const string& s); // 也叫复制构造函数
};
而对象u则是先初始化一个对象,再用等号赋值,那么 4 和 5 就不再调用构造函数,而是用到的C++中的运算符重载的特性,重载了"="。3 则是调用默认构造函数。
class string {
public:
string(); // 默认构造函数
string operator=(const char* s); // 重载了赋值运算符
string operator=(const string& s); // 也叫复制赋值运算符
};
除了赋值运算符 = 需要重载,我们还可以重载其他常用的运算符,例如+、<、>、== 等,实现字符串的拼接、比较等操作,让string用起来更像基本类型。
new与delete的配合
实际上,string类内部,字符串还是存储为C-字符串。但是,字符串长度不确定,因此字符串不能存储在栈中,只能用new的方式在堆中申请内存。实际上,平时的开发中不建议用new,可以用智能指针代替。因为用new很容易出现内存泄漏等问题。
那么,为什么string类不会造成内存泄漏呢?
动态内存的使用规则很简单,用new申请的内存,一定要记得用delete来释放掉,而且只能释放一次。
聪明的你可能已经想到,只要把delete语句放到析构函数中,相应的,在每个构造函数中都加上new语句,这样就可以保证在创建对象的时候执行一次new语句,销毁对象时执行一次delete语句。
例如,复制构造函数我们可以这样写:
class string {
public:
string(const string& st);
private:
char* str;
int len;
};
string::string(const string& st) {
len = st.len;
str = new char[len+1];
strcpy(str, st.str); // 注意不能用 str = st.str直接赋值,因为str是指针,必须用strcopy复制内容。
}
除了构造函数和析构函数,任何修改字符串内容的操作都可能需要使用new和delete,但一定要记得成对使用。例如复制赋值运算符:
string string::operator=(const string& st) {
if (this == &st) { // 相同地址则不需要复制
return *this;
delete []str; // 先删除上次分配的内存
len = st.len;
str = new char [len + 1]; // 再申请新内存
strcpy(str, st.str);
return *this;
}
当然为了减少内存分配过程造成的开销,如果当前分配的内存空间能容纳新的字符串,则不需要重新申请新的内存,可以直接复用原有内存空间。
需要注意的是,默认构造函数、复制构造函数、赋值运算符 如果没有显示定义,编译器会帮我们定义默认版本,而这可能导致构造函数中没有new却在析构函数中delete了,所以一定要显示定义这些方法。
友元函数
我们知道可以用cout来打印字符串:
char * s = "string";
string str = s;
cout << s << str << endl;
<<是一个二目运算符,那么它能够打印string对象,是string重载了<<运算符吗?
事实上并不是。因为<<的左边是ostream对象,右边是string对象,所以string提供成员函数重载<<运算符并不能达到效果,那到底是怎么做的呢?
其实,运算符重载不一定要作为成员函数,也可以是一个普通函数,例如:
ostream& operator<<(ostream& os, const string& st) {
...
}
唯一的问题是普通函数无法访问string对象的私有成员。要解决这个问题,只要将函数声明为类的友元函数即可:
class string {
friend ostream& operator<<(ostream& os, const string& st);
}