C++算法学习——经典的抽象设计——charstack的深层复制

来源:互联网 发布:天穹网络老板 编辑:程序博客网 时间:2024/05/21 09:27

就目前而言CharStack类的实现尚未完成。只要你通过引用传递每个CharStack对象,并且不将一个CharStack值分配给另一个,一切都将正常工作。 但是,如果你的代码最终通过值传递CharStack,或者尝试创建现有CharStack的副本,则你的程序可能会以某种不可预测的方式崩溃。

浅层和深层复制的区别

CharStack类当前实现中的问题的症结在于C ++常常以一种有意义的方式解释一个对象的赋值,但是如果该对象包含任何动态分配的内存,则通常会失败。 默认情况下,C ++通过复制每个实例变量的值来将一个对象分配给另一个对象。 如果这些值是通常类型的数据值(比如数字,字符等)。复制操作完全符合要求。 但是,如果该值是一个指针,则复制指针实际上不会复制基础值。C ++的默认行为称为浅层复制(shallow copying),因为它不会在表面下方延伸。复制包含动态内存的对象时,通常还要复制底层数据。这个过程称为深层复制(deep copying)。
要更多地了解这个问题的重要性,想想如果你调用以下代码会发生什么:

CharStack s1, s2;s1.push('A');s2 = s1;

你想要这个程序要做的是将s2初始化为s1的副本,这意味着这两个堆栈都将包含独立堆栈顶部的字符A. 这正是如果C ++使用深度复制来初始化s2会发生的事情。但是,如果C ++使用其默认的浅层复制技术,则执行这些语句几乎肯定会导致程序某些时候出现问题。
看看这个代码如何工作的最简单的方法是绘制一个堆栈图,显示在这个语句序列结尾处的内存状态。 该图如下所示:
这里写图片描述

浅层拷贝正确地复制了计数和容量字段,但是在每个结构中的数组字段指向同一个动态数组。如果要将栈顶s2的顶端字符弹出,然后再推一些其他字符,那么该操作也会改变s1的内容,如果堆栈应该是独立副本,那么这不是你想要的。

更糟糕的是,当声明变量s1和s2的函数返回时,该程序可能会崩溃。当这种情况发生时,两个变量都超出了范围,每个变量都触发了对CharStack析构函数的调用。第一个析构函数调用将释放数组,第二个将尝试做同样的事情。释放相同的内存两次是非法的,但不能保证C ++会检测并报告错误。在某些机器上,第二次调用可能会损害堆的内部结构,这在某种程度上会导致程序失败。

如果要复制CharStack以使副本与原始文件无关,则需要进行深层复制,如下所示:
这里写图片描述

编写代码进行深入复制并不困难。有趣的挑战是当你将一个CharStack分配给另一个时,让C ++会调用该代码。

赋值和复制构造函数

在C ++中,我们可以通过重新定义两种方法来更改默认的浅层复制行为。其中一种方法是重载赋值运算符。第二个是构造函数的一种特殊形式,称为复制构造函数(copy constructor),它从现有的构造函数初始化一个对象。在C ++中,只有在为已经存在的对象分配值时才调用赋值运算符。无论何时第一次初始化一个对象,C++将调用该复制构造函数。
默认情况下,赋值运算符和复制构造函数创建一个浅层拷贝,如之前所述。如果你希望你的类支持深层复制,你所要做的就是为堆中的数据复制这两种方法提供新的定义。
在C ++中,覆盖复制构造函数和赋值运算符所需的定义充满了你尝试从头开始编写这些定义的容易出错的详细信息。在大多数情况下,最好的方法是将这些方法从你所熟知的模式中复制出来,然后进行任何必要的更改来支持你的类。作为示例,必须添加到charstackpriv.h文件以实现深层复制。
代码如下:

public:    /*      *复制构造函数:CharStack     *用深度复制初始化当前对象的值     */     CharStack(const CharStack &cstk){        copyInternalData(cstk);     }     /*      *重载操作符 =       *用法:s1 = s2;      *-----------------      *将s2分配给s1,以便两个堆栈用作独立副本。       *对于CharStack类,这种分配需要包含动态数组的深层复制。      */      CharStack operator=(const CharStack &rhs){        if(this != &rhs){            delete[] array;            copyInternalData(rhs);        }        return *this;      }private:    /* 实例化参数 */    char *array; /* 字符型的动态数组 */    int capacity; /* 为数组分配空间 */    int count; /* 当前被压入的栈中的元素个数 */    /* 私有函数声明 */    void expandCapacity();    /*     *私有方法: copyInternalData     *---------------------------     *将来自作为参数传递的CharStack的所有数据复制到当前对象中,     *包括动态数组中的字符      */     void copyInternalData(const CharStack &cstk){        array = new char[cstk.count];        for (int i = 0; i < cstk.count; i++) {            array[i] = cstk.array[i];        }        count = cstk.count;        capacity = cstk.count;    }

上面的代码需要一些额外的解释。你可能会注意到的第一件事是,每个参数声明都包含const关键字,如copyInternalData方法所示,该方法负责制作CharStack对象的深层副本:

void copyInternalData(const CharStack &cstk){        array = new char[cstk.count];        for (int i = 0; i < cstk.count; i++) {            array[i] = cstk.array[i];        }        count = cstk.count;        capacity = cstk.count;    }

在这种情况下,const关键字告诉编译器即使通过引用传递函数,函数也不会更改cstk参数的值。参数传递的这种风格称为常数引用(constant call by reference)。
乍一看,声明一个常引用参数的想法可能看起来很愚蠢。到目前为止,我们使用调用的主要原因是这样做可以改变调用参数。然而,通过引用来调用具有其他优点。特别是如果你正在使用大型数据结构,则通过引用调用可以使程序更有效,因为当将其作为参数传递时,不需要复制这些值。使用关键字const标记这些参数可以提高该效率,同时让编译器知道调用参数不会被更改。
在copyInternalData方法的情况下,避免通过复制来实现更加引人关注。编写上面中的代码的全部原因是C ++用来复制CharStack对象的默认实现是不正确的。因此,必须防止C++做这个浅层复制。更糟糕的是,会对copy构造函数进行无限循环的递归调用链。在这种情况下,使用通过引用的调用至关重要。
如果说复制构造函数的代码本身只是调用copyInternalData方法将内部数据从原来的CharStack复制到当前的。那么更有趣的方法是重载赋值运算符:

CharStack operator=(const CharStack &rhs){        if(this != rhs){            delete[] array;            copyInternalData(rhs);        }        return *this;      }

赋值运算符的这个代码包括一些值得进一步讨论的功能。第一个是if语句,它检查分配的左侧和右侧是否实际上是相同的值。如果你将同一个对象分配给自己,则制作副本显然是不必要的。然而,如果你放弃了if语句,那么将CharStack赋给它自己就会最终尝试从已经被释放的数组中复制数据。第二个可能的混乱来源是return语句。在C ++中,赋值运算符被定义为使其返回左侧的值。this关键字这是一个指向该对象的指针,因此表达式 *this 表示对象本身。
如果定义这些方法的完整实现的过程太混乱,那么你还有另一个选择。下面的代码的简单模式定义了复制构造函数和赋值运算符的私有版本。

private:/** Implementation note: copy constructor and assignment operator* -------------------------------------------------------------* 以下行通过定义复制构造函数和赋值运算符的私有版本来复制CharStack是非法的。*/CharStack(const CharStack & cstk) { }CharStack & operator=(const CharStack & rhs) { return *this; }

这些定义会覆盖C ++提供的默认值,但使这些方法对类的客户端不可用。净效果是防止客户端在任何情况下复制CharStack。例如,标准C ++库使用这种方法来使流复制不合法

了解这些复杂性并不像知道如何将这些模式整合到您自己的类的设计中那么重要。如果将动态内存分配为类的一部分,则有责任重新定义复制构造函数和赋值运算符。除非你通过定义复制构造函数和赋值运算符的重载版本,否则编译器会自动定义这些方法的错误的版本。因此,对于设计的每个类,选择其中一个策略:实施深度复制或禁止复制。然后,你可以使用上面中的代码作为模型,对自己的类进行任何替换。

阅读全文
0 0
原创粉丝点击