1

機能の違いについてお聞きしたいのですが。以下のメインメソッドのオプションの1つから選択する必要があるシナリオの例を尋ねるかもしれません。

#include <iostream>

using namespace std;

class A{
    private:
        int x, y;
    public:
        A(int, int);
    };

class B{
    private:
        int *x, *y;
    public:
        B(int, int);
        ~B();
    };

A:: A(int x, int y){
    this->x = x; this->y = y;
    }

B:: B(int x, int y){
    this->x = new int(x);
    this->y = new int(y); 
    }

B:: ~B(){
    delete this->x;
    delete this->y;
    }

int main(){
    int x = 0, y = 0;
    A* objA = new A(x, y);  // line 1
    B objB1(x, y);          // line 2
    B* objB2 = new B(x, y); // line 3

    delete objA;
    delete objB2;
    return 0;
    }

mainメソッドの2番目の宣言が他の2と明らかに異なることは理解していB objB1(x, y)ますが、1と3のラベルが付いた行で、コンストラクターの機能の違いを誰かに説明してもらえますか?どちらの宣言にも悪い習慣はありますか?

ありがとう

NAX

アップデート

まず第一に、私は皆が与えているすべての答えに感謝します、私は本当にいくつかの良い洞察を得ています。上記のコードを編集したのは、使用したオブジェクトを削除していないという回答がいくつかあったためです。これは公平ですべてですが、それは私の質問の目的ではありませんでした。クラスを作成するためのさまざまなアプローチの機能の違いについて、いくつかの洞察を得たいと思いました。そして、その点を対象としたすべてに感謝します。私はまだ答えを読んでいます。

4

5 に答える 5

1

一般的に、クラスAの方法はクラスBよりもはるかに望ましいです。正当な理由がない限り、Aと同様の設計を使用する必要があります。単純な場合やこのような単純なデータ構造の場合、クラスBの実装方法は次のようになります。悪い習慣と見なされます。

これにはいくつかの理由がありますが、ここでは特定の順序ではありません。

  1. クラスBは、Aよりも2つの動的メモリ割り当てを実行します。実行時のメモリの割り当ては遅くなる可能性があり、さまざまなサイズの多くのブロックを割り当てて解放すると、いわゆる「メモリの断片化*」が発生する可能性があります(これは悪いことです)。
  2. クラスBのインスタンスは、クラスAのインスタンスよりも大きくなります。Aのインスタンスは2つの整数のサイズであり、通常はそれぞれ32ビットであるため、インスタンス全体が8バイトになります。Bのインスタンスには、2つのポインター(コードが32ビットまたは64ビットアーキテクチャ用にコンパイルされているかどうかに応じて、それぞれ32ビットまたは64ビット)と2つの実際の整数(それぞれ4バイト)、およびヒープアロケーターが割り当てごとに格納するメタデータが必要です。 、割り当てごとに0〜32バイト以上になる可能性があります。したがって、Bの各インスタンスは、基本的に同じジョブを実行しながら、Aの各インスタンスよりも8、16、または(はるかに)大きいバイトになります。
  3. Bのインスタンス内のフィールド(xおよびy)へのアクセスは、Aのインスタンス内のフィールドよりも低速です。Bのインスタンスのメンバーにアクセスする場合、必要なのはポインターの場所だけです。xしたがって、CPUはポインタをフェッチし、との値を保持する実際の整数のアドレスを知ることができます。そのyとき、CPUはそれらの値を読み書きできます。
  4. Aのインスタンスでは、とが連続したメモリアドレスに格納されていることを確認してxくださいy。これは、CPUキャッシュを最大限に活用するための最良のシナリオです。Bのインスタンスでは、実際のアドレスxy配置されているアドレスが互いに遠く離れている可能性があり、CPUキャッシュのメリットが少なくなります。
  5. Aでは、メンバーの存続期間は、メンバーを含むオブジェクトの存続期間とまったく同じです。Bの場合、そのような固有の保証はありません。これはこの単純な例には当てはまりませんが、より複雑な場合、特に例外が存在する場合、この点は明白かつ現在の危険になります。Bの場合、プログラミングエラー(たとえばdelete、デストラクタのまれに実行されるパスの1つのメンバーを忘れる)も問題になります。

オブジェクトの存続期間をメンバーデータから切り離すことが実際に必要な場合もありますが、これは一般的に適切な設計とは見なされないことに注意してください。詳細を知りたい場合は、C++でRAIIパターンを調べてください。

ちなみに、他のコメントで指摘されているように、privateクラスBのコピーコンストラクターと代入演算子を実装(または宣言)する必要があります。

上記の概要と同じ理由で、new可能であればデータを使用しないようにする必要があります。つまり、1、2、3のラベルが付いた行の中で、実際には2行がインスタンスを作成するためのより良い方法です。

于 2013-03-11T05:37:40.550 に答える
1

パターンAを使用するやむを得ない理由がない限り、私は通常、-style オブジェクトの方が効率的であるという理由だけで、-style オブジェクトを好みます。BA

たとえば、Aオブジェクトが割り当てられると、2 int (マシンではおそらく 8 バイト) のメモリが予約され、コンストラクタに渡された引数によって初期化されます。Bオブジェクトが割り当てられると、2 つのポインター用のメモリが予約intれます (マシン上ではおそらく 8 バイト) が、Bオブジェクトがコンストラクターで初期化されると、渡された各値が新しく作成されたint(ヒープ上) にコピーされます。 )、したがって、合計でさらに 8 バイトのメモリを使用します。したがって、この単純な例では、Bオブジェクトはオブジェクトの 2 倍のメモリを占有していAます。

xさらに、およびオブジェクトによって参照される値にアクセスするたびに、ポインターを逆参照する必要があり、間接参照と非効率のレベルが追加されyますB(また、多くのユースケースでは、おそらく NULL チェックも必要になります)。安全、ブランチを追加します)。Bそしてもちろん、オブジェクトが破棄されるたびに実行する必要がある追加のヒープ「クリーンアップ」があります。(ヒープの多くが非常に頻繁に作成および破棄されると、ヒープの断片化が徐々に発生する可能性があります。)

于 2013-03-11T05:27:50.650 に答える
1

「機能の違いは…」

行 1 では、キーワードを使用して、タイプ A のオブジェクトをヒープに割り当てます。ヒープでは、ivar 定義に従って、ヒープ上に2 を意味するポイントが連続して作成さnewれるオブジェクトにスペースが割り当てられます。objAints

2 行目で、スタックにクラス B の新しいオブジェクトを作成します。スコープ外になると、デストラクタが自動的に呼び出されます。ただし、B が割り当てられると、2 つのint ポインター(int ではない) 用のスペースが割り当てられ、B のコンストラクターで指定したようにヒープに割り当てられます。objB1スコープ外になると、ポインタはデストラクタによって正常に取得されdeletedます。

3 行目で、クラス B の新しいオブジェクトをheapに作成します。したがって、スペースは 2 つのint ポインター(int ではありません) 用にヒープに割り当てられ、次にこれらの intはキーワードを使用してヒープの別の場所に割り当てられます。newするとdelete objB2、デストラクタが呼び出されるため、2 つの「他の場所の整数」の割り当てが解除され、元のオブジェクト atobjB2もヒープから割り当て解除されます。

WhozCraig のコメントに沿って、クラスAは、例で示した 2 つの中で最も好ましいクラス定義です。


編集 (コメント応答):

WhozCraig のリンクは、基本的に生のポインターの使用を強く思いとどまらせます。Bこれに照らして、実際には、はい、同意します.Line 2は、技術的には独自のメモリを管理するため、純粋にメモリ管理に基づいて優先されます(ただし、まだ生のポインターを使用します)。

ただし、同等のスタック (または) 割り当てよりもはるかに遅いため、new内部クラスの (過剰な) 使用は一般的に嫌いです。したがって、個々のコンポーネントよりもクラス全体を好むのは、1 回の呼び出しだけで済み、とにかくすべての ivar がヒープに割り当てられるためです。(なお良いですが、それはこの質問の範囲をはるかに超えています)。newnon-newnewnewplacement new

要約すると:

行 2 ( class B) は、メモリ管理に基づいて優先されますが、それよりもさらに優れています。

A objAOnStack(x, y); // Avoids heap altogether

std::shared_ptr行 1 は、またはstd::unique_ptr類似のものなどのスマート ポインターでラップする場合、イコール ベストです。

行 3 は、スマート ポインター ラッパーなしで実際に考慮すべきではありません (また、一般的には、ネストされたものを避けた方がパフォーマンスが向上しnewます)。

于 2013-03-11T05:29:24.557 に答える
1

クラス B のコピー コンストラクターと代入演算子を定義する必要があります。そうしないと、これらのポインターで深刻な問題が発生します。これとは別に、行 1 と行 3 の間に機能上の違いはありません。唯一の違いは実装にあります。

そうは言っても、B 内でポインターを使用する理由はありません。固定数の整数が必要な場合は、プレーンな整数またはプレーンな配列を使用してください。可変数の整数が必要な場合は、 を使用しますstd::vector。また、動的メモリを割り当てる必要がある場合は、細心の注意を払い、スマート ポインターの使用を検討してください。

クラス B に含まれる [ポインタへの] 整数が 1 つだけの場合、次のようになります。

class B
{
    private:

        int * x;

    public:

        B (int i)       { x = new int(i); }
        B (const B & b) { x = new int(*b.x); }
        ~B()            { delete x; }

        B & operator= (const B & b)  // Corner cases:
        {                            //
            int * p = x;             // 1) b and *this might
            x = new int(*b.x);       //    be the same object
            delete p;                //
            return *this;            // 2) new might throw
        }                            //    an exception
};

このコードは、コメントされたコーナーケースでも「正しいこと (TM)」を実行します。

別のオプションは次のとおりです。

#include <utility>   // std::swap

class B
{
    private:

        int * x;

    public:

        B (int i)       { x = new int(i); }
        B (const B & b) { x = new int(*b.x); }
        ~B()            { delete x; }

        void swap (B & b)
        {
            using std::swap;
            swap (x, b.x);
        }

        B & operator= (const B & b)  // Corner cases:
        {                            //
            B tmp(b);                // 1) b and *this might
            swap (tmp);              //    be the same object
            return *this;            //
        }                            // 2) new might throw
};                                   //    an exception

ただし、例のように 2 つのポインターがある場合は、new2 回呼び出す必要があります。2 番目が例外のスローに失敗した場合、最初のメモリによって予約されたメモリnewを自動的に...deletenew

#include <utility>   // std::swap

class B
{
    private:

        int * x;
        int * y;

        void init (int i, int j)
        {
            x = new int(i);

            try
            {
                y = new int(j);
            }
            catch (...)     // first new was OK but
            {               // second failed, so undo
                delete x;   // first allocation and
                throw;      // continue the exception
            }
        }

    public:

        B (int i, int j) { init (i, j); }
        B (const B & b)  { init (*b.x, *b.y); }
        ~B()             { delete x; delete y; }

        void swap (B & b)
        {
            using std::swap;
            swap (x, b.x);
            swap (y, b.y);
        }

        B & operator= (const B & b)  // Corner cases:
        {                            //
            B tmp(b);                // 1) b and *this might
            swap (tmp);              //    be the same object
            return *this;            //
        }                            // 2) new might throw
};                                   //    an exception

[ポインタへの] int が 3 つまたは 4 つあると、コードはさらに醜くなります。そこで、スマート ポインターと RAII (Resource Acquisition Is Initialization) が本当に役立ちます。

#include <utility>   // std::swap
#include <memory>    // std::unique_ptr (or std::auto_ptr)

class B
{
    private:

        std::auto_ptr<int> x;   // If your compiler supports
        std::auto_ptr<int> y;   // C++11, use unique_ptr instead

    public:

        B (int i, int j) : x(new int(i)),      // If 2nd new
                           y(new int(j)) {}    // fails, 1st is
                                               // undone
        B (const B & b)  : x(new int(*b.x)),
                           y(new int(*b.y)) {}

        // No destructor is required

        void swap (B & b)
        {
            using std::swap;
            swap (x, b.x);
            swap (y, b.y);
        }

        B & operator= (const B & b)  // Corner cases:
        {                            //
            B tmp(b);                // 1) b and *this might
            swap (tmp);              //    be the same object
            return *this;            //
        }                            // 2) new might throw
};                                   //    an exception
于 2013-03-11T05:30:21.033 に答える
0

行 1 は objA を作成し、objA が削除されていないため、メモリ リークを残します。削除された場合、メンバー x および y も削除されます。また、objA はコピー コンストラクターと代入演算子をサポートしています。これらの呼び出しには問題はありません。

func1(*objA)
A objB = *objA.

objB2 で同じ行を実行すると、x と y が指す同じメモリが 2 回削除されるため、メモリ アクセス違反が発生します。それを防ぐには、プライベートコピーコンストラクターと代入演算子を作成する必要があります。

シナリオについて:

  1. 1 行目と 3 行目は、呼び出し元の関数にオブジェクトを返すのに適しています。呼び出し元の関数は、それを削除する責任を負う必要があります。クラス B では、x と y は基本クラスへのポインタにすることができます。したがって、それらはポリモーフィックになる可能性があります。
  2. Line2 は、このオブジェクトをコール スタックの下の呼び出された関数に渡すのに適しています。現在の関数が終了すると、オブジェクトは削除されます。
于 2013-03-11T05:21:24.943 に答える