C++PrimerPlus6-12-类和动态内存分配

第12章 类和动态内存分配

本章介绍如何对类使用new和delete,以及如何处理由于使用动态内存而引起的一些微妙的问题。

  • 对类成员使用动态内存分配
  • 隐式和显式复制构造函数
  • 隐式和显式重载赋值运算符
  • 在构造函数中使用new所必须完成的工作
  • 使用静态类成员
  • 将定位new运算符用于对象
  • 使用指向对象的指针
  • 实现队列抽象数据类型(ADT)

12.1 动态内存和类

在运行时决定内存分配,更灵活。

12.1.1 复习示例和静态类成员

class StringBad
{
private:
  char* str;                    // pointer to string
  int len;                      // length of string
  static int num_strings;       // number of objects
public:
  StringBad(const char* s);     // constructor
  StringBad();                  // default constructor
  ~StringBad();                 // destructor
  friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};

num_strings是静态类成员,无论创建了多少对象,程序都只创建一个静态类变量副本。用类声明以外单独语句来初始化(在类声明h中声明,在包含类方法的文件cpp中初始化):

int StringBad::num_strings = 0;

除了自定义的这两个构造函数,还有一个隐藏的复制构造函数。用一个对象初始化另一个对象时,编译器会自动生成复制构造函数:

StringBad(const StringBad&); // 复制构造函数
StringBad sailor = sports; // 会去调用复制构造函数

12.1.2 特殊成员函数

C++自动提供了下面这些成员函数:

  • 默认构造函数
  • 默认析构函数
  • 复制构造函数:不是常规赋值过程,而是初始化过程中(包括按值传递)。默认的赋值构造函数功能,是逐个复制非静态成员(浅复制)。
  • 赋值运算符
  • 地址运算符

12.1.3 回到StringBad:复制构造函数的哪里出了问题

如果类中包含一个计数的静态数据成员(值将在新对象被创建时发生变化),则应该提供一个显式复制构造函数来处理计数问题。

StringBad::StringBad(const StringBad& s)
{
  num_strings++;
  ...// important stuff to go here
}

如果按值复制的是指针,而指向的东西delete了,会出错。解决方法是使用深复制(deep copy),复制构造函数将复制字符串闭并将副本的地址给str成员,而不仅仅复制字符串地址。

StringBad::StringBad(const StringBad& st)
{
  num_strings++;              // handle static member update
  len = st.len;               // same length
  str = new char[len + 1];    // allot space
  std::strcpy(str, st.str);   // copy string to new location
}
  • 浅复制:仅浅浅的复制指针信息,不会深入“挖掘”以复制指针引用的结构。
  • 深复制:如果类中包含了使用new初始化的指针成员,应定义一个复制构造函数,以复制指向的数据。而不是指针。

12.1.4 StringBad的其他问题:赋值运算符

赋值运算符原型Class_name& Class_name::operator=(const Class_name&);

StringBad& StringBad::operator=(const StringBad&);

赋值也会出现浅复制导致使用了被释放的内存的问题,解决方法是提供赋值运算符定义(进行深复制)。

  • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放。
  • 应避免将自身赋给自身。否则会被释放。
  • 返回一个指向调用对象的引用。
StringBad& StringBad::operator=(const StringBad& st)
{
  if (this == &st)            // object assigned to itself,赋值自己给自己
    return *this;             // all done
  delete[] str;               // free old string
  len = st.len;               
  str = new char[len + 1];    // get space for new string
  std::strcpy(str, st.str);   // copy the string
  return *this;               // return reference to invoking object,返回调用对象的引用
}

12.2 改进后的新String类

将StringBad类改头换面为模仿的String类,他有以下功能:

int length() const {return len;}
friend bool operator<(const String& st1, const String& st2);
friend bool operator>(const String& st1, const String& st2);
friend bool operator==(const String& st1, const String& st2);
friend istream& operator>>(istream& is, String& st);
char& operator[](int i);
const char& operator[](int i) const;
static int HowMany();

12.2.1 修订后的默认构造函数

12.2.2 比较成员函数

12.2.3 使用中括号表示法访问字符

12.2.4 静态类成员函数

12.2.5 进一步重载赋值运算符

12.3 在构造函数中使用new时应注意的事项

  • 如果在构造函数中使用new来初始化指针成员,应在析构函数中使用delete
  • new和delete必须兼容。new对应delete,new[]对应delete[]
  • 多个构造函数,用相同的方式用new,要么都[]要么都不带
  • 空指针用nullptr,代替0或者NULL
  • 需要定义一个复制构造函数,通过深复制将一个对象初始化为另一个对象
  • 需要定义一个赋值运算符,通过深复制将一个对象复制给另一个对象

12.3.1 应该和不应该

12.3.2 包含类成员的类的逐成员复制

12.4 有关返回对象的说明

12.4.1 返回指向const对象的引用

如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率。

const Vector& Max(const Vector& v1, const Vector& v2)
{
  if (v1.magval() > v2.magval())
    return v1;
  else
    return v2;
}

12.4.2 返回指向非const对象的引用

  • 重载赋值运算符,提高效率。operator=()返回值用于连续赋值。
  • 重载<<,用于串接输出。operator<<(cout, s1)

12.4.3 返回对象

如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回他,应返回对象。例如重载的算数运算符。

Vector Vector::operator+(const Vector& b) const
{
  return Vector(x + b.x, y + b.y);
}

12.4.4 返回const对象

让返回的对象不被修改。

12.5 使用指向对象的指针

12.5.1 再谈new和delete

析构函数的调用时机:

  • 对象是动态的,执行完定义该对象的程序块时,调用。
  • 对象是静态的,程序结束后调用。
  • 对象是new的,显式使用delete时调用。

12.5.2 指针和对象小结

  1. 使用常规表示法来声明指向对象的指针
  2. 可以将指针初始化为指向已有的对象
  3. 可以用new来初始化指针,这将创建一个新的对象
  4. 对类使用new将调用相应的类构造函数来初始化新创建的对象
  5. 可以使用->运算符用过指针访问类方法
  6. 可以对对象指针用解除因引用运算符(*)来获得对象
String* glamour;                                  // 1
String* first = &sayings[0];                      // 2
String* favorite = new String(sayings[choice]);   // 3
String* gleep = new String;                       // 4
if (sayings[i].length() < shortest->length());    // 5
if (sayings[i] < *first>);                        // 6

12.5.3 再谈定位new运算符

定位new运算符,能够在分配内存时指定内存位置。
略,感觉暂时不用。(P371)

12.6 复习各种技术

12.6.1 重载<<运算符

定义友元运算符函数:

ostream& operator<<(ostream& os, const c_name& obj)
{
  os << ...; // display object contents
  return os;
}

12.6.2 转换函数

要将单个值转换为类类型,创建如下类构造函数:
c_name(type_name value);

要将类转换为其他类型,创建如下成员函数:
operator type_name();

12.6.3 其构造函数使用new的类

同12.3

12.7 队列模拟

队列先进先出

12.6.1 队列类

队列的特征:

  • 存储有序的项目序列
  • 容纳的项目数有一定的限制
  • 能够创建空队列
  • 能够检查队列是否为空
  • 能够检查队列是否是满的
  • 能够在队尾添加项目
  • 能够从队首删除项目
  • 能够确定队列中的项目数
class Queue
{
  enum {Q_SIZE = 10};
private:
// private representation to be developed later
public:
  Queue(int qs = Q_SIZE); // create queue with a qs limit
  ~Queue();
  bool isempty() const;
  bool isfull() const;
  int queuecount() const;
  bool enqueue(const Item& item); // add item to end
  bool dequeue(Item& item); // remove item from front
}

用链表表示节点:

struct Node
{
  Item item;          // data stored in the node
  struct Node* next;  // pointer to next node
}

成员初始化列表:

  • 只能用于构造函数
  • 必须用这种格式来初始化非静态const数据成员
  • 必须用这种格式来初始化引用数据成员
Queue::Queue(int qs) : qsize(qs) // initialize qsize to qs
{
  ...
}

12.6.2 Customer类

12.6.3 ATM模拟

12.8 总结

  • 在类构造函数中使用new来分配内存时,应在类析构函数中delete。
  • 复制构造函数。
  • 重载赋值运算符。
  • 定位new。
  • 成员初始化列表。

12.9 复习题

12.10 编程练习


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 cdd@ahucd.cn

×

喜欢就点赞,疼爱就打赏

B站 cdd的庇护之地 github itch