写点什么

类和动态内存分配

作者:Maybe_fl
  • 2022-11-02
    陕西
  • 本文字数:3215 字

    阅读完需:约 11 分钟

类和动态内存分配

侯捷老师将类分为两种: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 成员变量的初始化问题,到底应该在哪里初始化。这里分为两种情况

  • const static 变量可以在类内直接初始化

  • 非 const 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 ;  //编译通过
复制代码

进一步思考会不会出现如下的场景

str1 + str2 = str3;
复制代码

上述代码毫无意义,但是确实会有出现的场景,比如我们期望的是判断是否相等

if(str1 + str2 == str3);
复制代码

但是少写了一个等号。就出现上述场景了,如何避免这种错误,我们期望在编译阶段就识别出这种错误。禁止 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,要么都带中括号,要么都不带

  • 必须重新定义拷贝构造函数和拷贝赋值函数

  • 拷贝构造函数必须要考虑自赋值的情况

有关返回对象的说明

  1. 如果是函数体内的局部变量,只能使用 pass by value。

  2. 如果是外部变量,为提高效率,应使用 pass by reference。

  3. 如果不期望返回值作为左值,将返回值用 const 修饰

用户头像

Maybe_fl

关注

还未添加个人签名 2019-11-11 加入

还未添加个人简介

评论

发布
暂无评论
类和动态内存分配_Maybe_fl_InfoQ写作社区