まず第一に、あなたがchar*
弦をシャッフルするためにを使用しているという事実を克服することはできません。大声で叫ぶために、std::string
。これは、文字列を処理するための標準化された方法です。それはあなたがそれに投げたものから効率的にその方法を小刻みに動かすことができます。文字列リテラル、char配列、char *、またはさらに別の文字列です。
第二に、あなたの教授は専門家の助けを必要としています。「低レベルの直感」をもっと生々しく教えることは1つのことですが、C++を勉強するはずの学生にこれを強制することは明らかに悪い習慣です。
さて、目前の問題に。トップレベルの例、つまり、食器洗い機のコンストラクターにある生の「裸の」ポインターを取り上げますchar* str
。ご存知のように、それはポインタ、つまり型へのポインタchar
です。ポインタは、変数のメモリアドレス(問題の変数のタイプの最初のバイトであり、メモリ内でアドレス指定可能な最も低い単位)を格納します。
その明確な違いは非常に重要です。なんで?ポインタを他の何かに割り当てるとき、実際のオブジェクトをコピーするのではなく、その最初のバイトのアドレスだけをコピーするからです。したがって、実際には、同じオブジェクトを指す2つのポインタを取得するだけです。
あなたは間違いなく良い記憶の市民なので、おそらくこれを処理するためにデストラクタを定義しました:
washer_id = new char [(strlen(str)+1)];
基本的に、ヒープにstrlen(str)+1バイトを割り当てています。これは、システムによって管理されておらず、有能な手元に残っています。したがって、名前はヒープです。たくさんのもの、それへの参照を失い、あなたはそれを二度と見つけることは決してないでしょう、あなたはそれをすべて捨てることができます(プログラムがメインから戻ったときにすべてのリークに実際に何が起こるか)。したがって、システムを使い終わったときにシステムに通知するのはあなたの義務です。そして、あなたは、デストラクタを定義しました。
大きくて厄介ですが...
しかし...このスキームには問題があります。コンストラクターがあります。そしてデストラクタ。リソースの割り当てと割り当て解除を管理します。しかし、コピーはどうですか?
Dishwasher siemens(
Pump(160, "011219991143"),
Motor(1300, "081220031201"),
"010720081032",
17.5);
コンパイラが、基本的なコピーコンストラクタ、コピー代入演算子(すでに構築されているオブジェクトの場合)、およびデストラクタを暗黙的に作成しようとすることをおそらくご存知でしょう。暗黙的に生成されたデストラクタには手動で解放するものがないため(動的メモリと責任について説明しました)、空です。
動的メモリを使用し、テキストを格納するためにさまざまなサイズのバイトブロックを割り当てるため、デストラクタが配置されています(長いコードが示すように)。それはすべて問題ありませんが、変数の直接値をコピーする、暗黙的に生成されたコピーコンストラクターとコピー代入演算子を扱います。ポインタは値がメモリアドレスである変数であるため、暗黙的に生成されたコピーコンストラクタまたはコピー割り当て演算子は、そのメモリアドレス(これはシャローコピーと呼ばれます)をコピーするだけで、一意の単一バイトへの別の参照を作成します。メモリ内(および隣接するブロックの残りの部分)。
反対に、ディープコピーを使用して、新しいメモリを割り当て、着信ポインタのメモリアドレスに格納されている実際のオブジェクト(または任意の複合マルチバイトタイプ、配列データ構造など)にコピーします。このように、それらは、コピー元のオブジェクトの寿命ではなく、エンベロープするオブジェクトの寿命に関連付けられている別個のオブジェクトを指します。
上記の例では、スタック上に一時オブジェクトを作成していることに注意してください。これらのオブジェクトは、コンストラクターが動作している間は存続し、その後解放され、デストラクタが呼び出されます。
Dishwasher::Dishwasher(Pump p, Motor m, char *str, float f)
: pump(p), motor(m), max_load(f)
{
washer_id = new char [(strlen(str)+1)];
strcpy (washer_id,str);
time_built.id2time(washer_id);
date_built.id2date(washer_id);
}
初期化リストには、オブジェクトをデフォルト値に初期化せずにコピーを実行するという追加の利点があります。これは、コピーコンストラクター(この場合はコンパイラーによって暗黙的に生成される)を直接呼び出すことができるためです。
pump(p)
基本的にPump::Pump(const Pump&)を呼び出し、一時オブジェクトが初期化された値を渡します。Pumpクラスにはchar*が含まれています。これは、一時オブジェクトに押し込んだ文字列リテラルの最初のバイトのアドレスを保持しますPump(160, "011219991143")
。
コピーコンストラクターは一時オブジェクトを取得し、明示的に使用可能なすべてのデータをコピーします。つまり、文字列全体ではなく、char*ポインターに含まれるアドレスのみを取得します。したがって、2つの場所から同じオブジェクトを指すことになります。
一時オブジェクトはスタック上に存在するため、コンストラクターがそれらの処理を終了すると、リリースが解放され、デストラクタが呼び出されます。これらのデストラクタは、実際には、Dishwasherオブジェクトの作成中に配置した文字列を一緒に破棄します。これで、Dishwasherオブジェクト内のPumpオブジェクトは、無限のメモリの深淵で失われたオブジェクトのメモリアドレスへのポインタであるダングリングポインタを保持します。
解決?
独自のコピーコンストラクターとコピー代入演算子のオーバーロードを記述します。ポンプの例では:
Pump(const Pump &pumpSrc) // copy constructor
Pump& operator=(const Pump &pumpSrc) // copy assignment operator overload
このforeach
クラスを行います。
もちろん、あなたがすでに持っているデストラクタに加えて。これらの3人は、「三つのルール」と呼ばれる経験則の主人公です。それらのいずれかを明示的に宣言する必要がある場合は、おそらく残りの部分も明示的に宣言する必要があると記載されています。
おそらくルールの一部は、基本的には責任の欠如にすぎません。明示的な定義の必要性は、C ++でさらに進んだときに、実際には非常に明白です。たとえば、クラスが何をするかを考えることは、すべてを明示的に定義する必要があるかどうかを判断するための良い方法です。
例:クラスは、メモリの一部を指すネイキッドポインタに依存しており、メモリアドレスがすべてをプルします。これは、問題のオブジェクトの最初のバイトへのメモリアドレスのみを保持する単純な型付き変数です。
オブジェクトを破棄する際のリークを防ぐために、割り当てられたメモリを解放するデストラクタを定義しました。しかし、オブジェクト間でデータをコピーしますか?あなたが見てきたように、非常に可能性が高いです。データを割り当て、値をコピーするポインタデータメンバーにメモリアドレスを格納する一時オブジェクトを作成する場合があります。しばらくすると、その一時的なものはいつか破壊されることになり、データが失われます。残っているのは、役に立たない危険なメモリアドレスを持つダングリングポインタだけです。
公式の免責事項:私がこの主題に関する本を書くことを防ぐために、いくつかの簡略化が行われています。また、OPのコードではなく、常に目前の質問に集中するようにしています。つまり、採用されている慣行についてはコメントしません。コードは恐ろしい、美しい、またはその中間にある可能性があります。しかし、私はコードやOPを好転させようとはしません。質問に答えようとし、長期的にはOPに有益だと思うことを提案することがあります。はい、私たちは地獄のように正確であり、すべてを修飾することができます...ペアノの公理を使用して、2 + 3 = 3 + 2 = 5と大胆に述べる前に、数の集合と基本的な算術演算を定義することもできます。楽しい量。