侯捷老师将类分为两种:class without pointer member(s)和 class with pointer member(s)。
本文主要介绍 class with pointer member(s),下面以 Mystring 类的实现为例:
构造函数
class Mystring {...}
Mystring str1;Mystring str2("abc");
复制代码
为了支持上述两种构造函数,需要完成默认构造和有参构造
class Mystring { public: Mystring(); Mystring(const char* pstr); ~Mystring();
private: char* m_data; unsigned int m_len;};
Mystring::Mystring() { m_len = 0; m_data = new char[1];}
Mystring::Mystring(const char* pstr) { m_len = std::strlen(pstr); m_data = new char[m_len + 1]; std::strcpy(m_data, pstr);}
Mystring::~Mystring() { if (m_data != nullptr) { delete[] m_data; m_data = nullptr; }}
复制代码
上述代码中,在默认构造函数中,使用 new char[1]和 new char(),效果是一样的,为什么这样使用呢,因为在析构函数中,使用了 delete[],所以构造函数中必须使用 new [],new 和 delete 必须相互兼容,new 对应 delete,new[]对应 delete[]。
拷贝构造函数
Mystring str1("123");Mystring str2("456");str1 = str2;
复制代码
为了保证上述代码的运行,需要完成默认构造和有参构造
Mystring& Mystring::operator=(const Mystring& other) { if (this == &other) //考虑自赋值 return *this;
delete m_data; //释放原始内存 m_data = new char[other.m_len + 1]; //申请新内存 std::strcpy(m_data, other.m_data); //复制内容 m_len = other.m_len; return *this;}
复制代码
重点是考虑自赋值,因为可能会写出这样的代码,str1 = str1,如果没有考虑自赋值,在 Mystring::operator=()会直接释放内存,那就没有可复制的对象了。
拷贝赋值函数
需要支持如下操作:
Mystring str1 = "123";Mystring str2(str1);Mystring str3 = str1;
复制代码
必须要完成拷贝赋值函数
Mystring::Mystring(const Mystring& other){ m_data = new char[ strlen(other.m_data) + 1 ]; m_len = other.m_len; strcpy(m_data, other.m_data);}
复制代码
入参必须用引用,如果用值传递会导致递归构造。
以前在 effctive c++里面看到拷贝构造和拷贝赋值不能相互调用,当时还不理解为什么,现在观察上面的代码,确实有差异,拷贝赋值是初始化,因为是第一次申请内存,所以不用负责释放。而拷贝构造不是第一次构造,需要复制释放原先申请的内存。
操作符重载
operator[]
const Mystring str1("123");Mystring str2("123");
str1[0] = '3'; //errorstr2[0] = '3'; //okstr1[0] = str2[0]; //ok
复制代码
为了保证上面的用例通过,需要重载 operator[];
char& Mystring::operator[](int i) { return m_data[i];}
const char& Mystring::operator[](int i) const { return m_data[i];}
复制代码
operator<<
Mystring str1("123");Mystring str2("456");Mystring str3("789");
cout << str1;cout << str2 << str3;
复制代码
为了保证上面的用例通过,需要重载 operator<<;
方案 1:将其设置为成员函数
ostream& Mystring::operator<<(ostream& os) { os << str.m_data; return os;}
复制代码
上面的代码是能实现 Mystring 输出的,但是由于入参第一个是 this,所以输出变成了如下这个样子
str1 << cout; //str1->operator<<(cout);str3 << (str2 << cout); //str3->(str2->operator<<(cout))
复制代码
不符合正常的输出,很奇怪是吧。
方案 2:友元函数
class Mystring { ... friend ostream& operator<<(ostream& os, const Mystring& str); }
ostream& operator<<(ostream& os, const Mystring& str) { os << str.m_data; return os;}
复制代码
这个就符合常规输出方式了。
operator>>
有了上面的例子,那 operator<<就比较简单了
class Mystring { ... friend ostream& operator<<(ostream& os, const Mystring& str); const static CINLIM = 100;}
istream& operator<<(istream& is, Mystring& str) { char temp[Mystring::CINLIM]; is.get(temp, Mystring::CINLIM); if (is) str = temp; while (is && is.get() != '\n') continue; return is;}
复制代码
上面关注一点,就是 static 成员变量的初始化问题,到底应该在哪里初始化。这里分为两种情况
operator+
二元操作符
Mystring str1("123");Mystring str2("456");Mystring str3 = str1 + str2;Mystring str4 = str1 + str2 + str3;
复制代码
为了保证上述用例通过,我们必须重载 operator+,这个时候也有两种方案,到底是成员函数还是友元函数呢?
方案 1:成员函数
Mystring Mystring::operator+(const Mystring &other)const{ Mystring temp; delete[] temp.m_data; // 使用delete[] 与构造函数匹配 temp.m_data = new char[this->m_len + other.m_len +1]; temp.m_len = this->m_len + other.m_len; std::strcpy(temp.m_data, this->m_data); std::strcat(temp.m_data, other.m_data);
return temp;}
复制代码
思考上述实现方法,因为是函数内的局部变量,所以使用的是值传递,用例都是可以通过的。
Mystring str3 = str1 + str2; //str1.operate+(str2)Mystring str4 = str1 + str2 + str3; // (str1.operate+(str2)).operate+(str3);
复制代码
思考一下,我们还可能出现如下的场景
Mystring str1("123");Mystring str2 = str1 + "456"; //编译通过Mystring str3 = "456" + str1 ; //编译不通过
复制代码
我们没有对构造函数增加 explicit 关键字修饰,所以是支持隐式转换的。但是上述函数没有编译通过,进一步思考上述函数的本质。
Mystring str2 = str1 + "456"; //str1.operator+("456")Mystring str3 = "456" + str1 ; //"456"->operator+(str1);
复制代码
我们期望"456"能转换为 Mystring,但是只有在函数的参数列表中,才是隐式转换的合格候选者,其他情况是不会转换的,所以上述编译不通过,使用成员函数的方式有缺陷。
方案 2:友元函数
MyString operator+(const MyString &str1, const MyString &str2){ MyString temp; delete temp.m_data; // temp.data是仅含‘/0’的字符串 temp.m_data = new char[str1.m_len + str2.m_len +1]; std::strcpy(temp.m_data, str1.m_data); std::strcat(temp.m_data, str2.m_data);
return temp;}
复制代码
由于友元函数两个入参都在参数列表中,所以可以形成隐式转换
Mystring str1("123");Mystring str2 = str1 + "456"; //编译通过Mystring str3 = "456" + str1 ; //编译通过
复制代码
进一步思考会不会出现如下的场景
上述代码毫无意义,但是确实会有出现的场景,比如我们期望的是判断是否相等
但是少写了一个等号。就出现上述场景了,如何避免这种错误,我们期望在编译阶段就识别出这种错误。禁止 operator+()的返回值充当左值。
const MyString operator+(const MyString &str1, const MyString &str2){ MyString temp; delete temp.m_data; // temp.data是仅含‘/0’的字符串 temp.m_data = new char[str1.m_len + str2.m_len +1]; std::strcpy(temp.m_data, str1.m_data); std::strcat(temp.m_data, str2.m_data);
return temp;}
复制代码
将返回值变为 const 类型即可。将错误尽可能在编译阶段暴露。
复习总结开发过程:
new 初始化对象
若类含有指针变量,必须在构造函数中使用 new 来初始化指针变量,在析构函数中 delete
new 和 delete 必须相互兼容,new 对应 delete,new[]对应 delete[]
如果有多个构造函数,必须以相同的方式使用 new,要么都带中括号,要么都不带
必须重新定义拷贝构造函数和拷贝赋值函数
拷贝构造函数必须要考虑自赋值的情况
有关返回对象的说明
如果是函数体内的局部变量,只能使用 pass by value。
如果是外部变量,为提高效率,应使用 pass by reference。
如果不期望返回值作为左值,将返回值用 const 修饰
评论