0

リンク リストのオーバーロードされた = 演算子の実装に問題があります。List クラスには、Node* headポインターとそれをstruct Node含むT* dataandが含まれていますNode* next。ここで、T はテンプレートの型名です。makeEmptyリストを反復処理して新しいリストを作成した後、演算子の終わりまでにデストラクタ (この場合はによって処理される) が 2 回呼び出されている演算子関数の最後で何が起こっているのかに問題があります。同じノードと、オペレーター関数の終了後に 1 回。

makeEmpty の実装は次のとおりです。

// Does the work of the Destructor
template <typename T>
void List<T>::makeEmpty() {

cout << endl << endl << "DESTRUCTOR CALLED" << endl << endl;
List<T>::Node* tempPtr = head;

if (head != NULL) {

    List<T>::Node* nextPtr = head->next;

    for(;;) {
        if (tempPtr != NULL) {

            delete tempPtr->data;
            tempPtr = nextPtr;

            if (nextPtr != NULL)
                nextPtr = nextPtr->next;

            /*tempPtr = head->next;
            delete head;
            head = tempPtr;*/
        }

        else break;
    }
}

}

operator= オーバーロードの実装は次のとおりです。

// Overloaded to be able to assign one list to another
template <typename T>
List<T> List<T>::operator=(const List& listToCopy) {

List<T> listToReturn;
listToReturn.head = NULL;
List<T>::Node* copyPtr = listToCopy.head;

List<T>::Node* thisPtr = head;

if (copyPtr != NULL && thisPtr != NULL) {
    for(;;) {
        if (copyPtr != NULL) {

            T* toInsert = new T(*copyPtr->data);
            listToReturn.insert(toInsert);

            copyPtr = copyPtr->next;
        }

        else{cout << endl << listToReturn << endl << endl; return listToReturn;}
    }
}
// if right-hand list is NULL, return an empty list
return listToReturn;
}

私は少しデバッグしてきましたが、デストラクタが 2 回目に呼び出されたときに、破棄されるリストの先頭に読み取り不能なメモリが含まれているようです。破棄する必要がある唯一のリストは、返された後の listToReturn であるため、演算子内でデストラクタが 2 回呼び出される理由もわかりません。(多分私の論理はどこかに欠陥があります...私はこれについてあまりにも長い間考えてきました)

コードに関する情報がさらに必要な場合は、喜んで提供します。いつものように、これは課題のためなので、正しい方向に進むのに役立つヒントのみを求めています. 見てくれて助けてくれたみんなに感謝します!

4

1 に答える 1

4

あなたはポインタとヒントを求めたので、私はそれを与えます:

1) 不要な動的データ メンバ

基になるデータ値の動的メンバーは必要ありList<T>::Nodeませんこれは a から構築可能である必要がありconst T&、C++11 準拠の move-construction イディオムを実装する場合は、aT&&も同様です。そして、どちらもnextメンバーを初期化する必要がありますnullptr

2) 必須のコピーList<T>コンストラクター

Rules of Three、Four、または Fiveに従って、クラスには動的メンバーが含まれているため、コピー構築および代入演算子アクションでそれらを適切に管理する必要があります (または、前述の実装を非表示にしますが、明らかにそれはオプションではありません)。あなたの任務の一部であるため)。

3) 代入演算子のオーバーロードにクラス コピー コンストラクターを利用する

動的割り当てが関係する代入演算子をオーバーロードする (リンクされたリストがNode義務付けているように、ここにあります) 理想的には、ライフタイム管理にコピー コンストラクターとコピー/スワップ イディオムを利用する必要があります。これには多くの利点があります。最も重要な 2 つは、最適化コンパイラによるコピー操作を省略できる可能性と、オブジェクトを元の状態に維持する例外の安全性です。

4)List<T>::operator =オーバーライドは現在のオブジェクトへの参照を返す必要があります

割り当てられている現在のオブジェクト(演算子の左側) は、参照による戻り値である必要があります。変更されるのはオブジェクトである必要があります。これは、そのような演算子の標準実装の一部です。コピーを返しますが、元のオブジェクトはそのまま残り、代入演算子の目的を完全に無効にします (つまり、実装で左辺値側は実際にはまったく変更されません)。

これらのそれぞれについて、以下で詳しく説明します。


不要な動的データ メンバー

掲載されていないので、List見た目はなんとなく勘弁してください。私は次のようなものを想像します:

template<class T>
class List
{
private:
    struct Node
    {
        T* data;
        Node* next;
    };
    Node *head;

    // other members and decls...
};

Tこれにより、挿入操作とコピー操作は、必要のないオブジェクトの動的割り当てを管理するために大きな利点を得る必要があります。List<T>確かにNode;のチェーンを所有する必要があります。ただしNode、実際のTオブジェクトを所有し、その中でそれを管理する責任があります。ありません List<T>。代わりにこれを考慮してください:

template<class T>
class List
{
private:
    struct Node
    {
        T data;
        Node* next;

        Node(const T& arg) 
            : data(arg), next()
        {}

        Node(const Node& arg)
            : data(arg.data), next()
        {}

    private:
        // should never be called, and therefore hidden. A
        // C++11 compliant toolchain can use the `delete` declarator.
        Node& operator =(const Node&);
    };
    Node *head;

    // other members and decls...
};

オブジェクトを保持するために新しいノードが必要なT場合 (挿入操作など)、次の操作を実行できます。

template<typename T>
void List<T>::someFunction(const T& obj)
{ 
    Node *p = new Node(obj);
    // ...use p somewhere...
}

のコピーコンストラクターList<T>必須です

リンクされたリストは、その性質上、動的チェーンを管理します。したがって、このクラスはコピー構築と割り当て操作を実装する必要があります。後者は割り当ての一部であり、質問を投稿した理由です。リンクされたリストを複製するのはかなり簡単ですが、何らかの理由でそれが思ったより難しくなるものもあります。以下は、それを行うための私の好ましい方法の1つです。

template<typename T>
List<T>::List(const List<T>& arg)
    : head()
{
    Node **dst = &head;
    const Node* src = arg.head;
    while (src)
    {
        *dst = new Node(*src);     // invoke Node copy-construction
        dst = &(*dst)->next;       // move target to new node's next pointer
        src = src->next;           // advance source
    }
}

これは、ポインタからポインタへの単純な手法を使用して、次の新しいノードが取り込まれるポインタのアドレスを保持します。最初は、ヘッド ポインターのアドレスを保持します。新しいノードが追加されるたびに、新しく追加されたノードのnextメンバーのアドレスを保持するように進められます。Node(const Node&)すでに に設定nextされているためnullptr(前のセクションを参照)、リストは常に適切に終了します。


代入演算子のオーバーロードにクラス コピー コンストラクターを利用する

オーバーライドは現在のオブジェクトへのList<T>::operator =参照を返す必要があります

しっかりしたコピー コンストラクターを取得したら、それを使用して代入演算子をオーバーライドできます。これはあまり目立たない方法で行われますが、コードの後で説明します。

template<typename T>
List<T>& List<T>::operator=(List<T> byval)
{
    std::swap(head, byval.head); // we get his list; he gets ours
    return *this;
}

これを見て「え?」と思った方も多いのではないでしょうか。説明が必要です。byval渡されるパラメーターを注意深く見て、なぜそのように名前を付けたのかを考えてください。これは、おそらく見慣れた従来のリファレンスではありません。代入式の右辺のconstコピーです。したがって、それを作成するために、コンパイラは new を生成しList<T>、コピーコンストラクタを呼び出してそうします。そのコピーの結果は一時オブジェクトですbyvalパラメータとして持っています。ヘッドポインタを交換するだけです。それが何をするか考えてみてください。ヘッド ポインターを交換することで、私たちは彼のリストを取得し、彼は私たちのリストを取得します。しかし、彼は代入式の元の右側のコピーであり、私たちのものは削除したいと考えています。そして、この関数が終了した後にデストラクタが起動されると、まさにそれが起こります。byval

要するに、次のようなコードになります。

List<int> lst1, lst2;
lst1.insert(1);
lst2.insert(2);
lst1 = lst2; // <== this line

マークされた行で関数を実行します。その関数は のコピーを作成しlst2、それを代入演算子に渡します。ここでlst1、ヘッド ポインターが一時コピーと交換されます。その結果、lst1の古いノードが のデストラクタによってクリーンアップされbyval、新しいノード リストが適切に配置されます。

これにはいくつかの理由があります。まず、代入演算子を例外セーフにします。例外が発生した場合 (通常はメモリ割り当ての例外ですが、問題ではありません)、メモリ リークは発生せず、元のオブジェクトlst1は元の状態のままです。第 2 に、コンパイラが選択し、条件が正しければ、コンパイラはこれを完全に排除できます。

とにかく、これらは実装におけるいくつかのアイデアとバグのいくつかのポイントです。それらがお役に立てば幸いです。

于 2013-10-27T09:32:29.343 に答える