侯捷老师将类分为两种: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'; //error
str2[0] = '3'; //ok
str1[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 修饰
评论