侯捷老师将类分为两种: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 修饰 
评论