C++第5弾---スタックとコピー構造関数

7552 ワード

ダイナミックメモリ割り当て
  • スタックC++プログラムに関するメモリ構造は、通常、グローバルデータ領域、コード領域、スタック領域、スタック領域
  • の4つの領域に分けられる.
  • グローバル変数、静的データ、定数はグローバルデータ領域に格納され、すべてのクラスメンバー関数と非メンバー関数コードはコード領域に格納され、関数を実行するために割り当てられたローカル変数、関数パラメータ、戻りタイプ、戻りデータ、戻りアドレスなどはスタック領域に格納され、残りの空間はスタック領域として格納される.
  • 関数mallocとfree、ヘッダファイルmalloc.hでは声名が使われ、オペレータnewとdeleteはC++言語の一部であり、ヘッダファイルを含まなくてもよい.いずれもスタックからメモリブロックを割り当てて解放しますが、特定の操作では大きな違いがあります.
  • スタックメモリを操作する場合、メモリが割り当てられている場合は、それを回収する責任があります.そうしないと、メモリが漏洩します.これは,関数におけるスタック領域に局所変数を割り当てる本質とは異なる.
  • newとdeleteが必要な理由mallocとfreeはC+/C言語の標準ライブラリ関数であり、new/deleteはC++の演算子である.ダイナミックメモリの申請やメモリの解放に使用できます.内部データ型以外のオブジェクトでは、malloc/freeだけではダイナミックオブジェクトの要件を満たすことはできません.オブジェクトは作成と同時にコンストラクション関数を自動的に実行し、オブジェクトは消滅する前にコンストラクション関数を自動的に実行します.malloc/freeは演算子ではなくライブラリ関数であるため、コンパイラ制御権限内ではなく、コンストラクション関数とコンストラクション関数を実行するタスクをmalloc/freeに押し付けることはできません.したがって、C++言語には、動的メモリ割り当てと初期化作業を完了できる演算子newと、メモリのクリーンアップと解放作業を完了できる演算子deleteが必要です.注意newとdeleteはライブラリ関数ではありません.

  • コピーコンストラクタ
  • コピーコンストラクタのコンセプトコピーコンストラクタは、オブジェクトの作成時に同じクラスの前に作成されたオブジェクトを使用して新しく作成されたオブジェクトを初期化する特殊なコンストラクタです.コピーコンストラクタは、通常、
  • に使用されます.
  • は、別の同じタイプのオブジェクトを使用することによって、新しく作成されたオブジェクトを初期化する.
  • コピーオブジェクトは、パラメータとして関数に渡されます.
  • オブジェクトをコピーし、関数からオブジェクトを返します.

  • クラスにコピーコンストラクション関数が定義されていない場合、コンパイラは独自に定義します.クラスにポインタ変数があり、ダイナミックメモリ割り当てがある場合は、コピーコンストラクション関数が必要です.コピーコンストラクション関数の最も一般的な形式は次のとおりです.
    classname (const classname &obj) {
       //        
    }
    

    ここでobjは、別のオブジェクトを初期化するためのオブジェクト参照である.
    次に、クラスオブジェクトのコピーの簡単な例を示します.
    #include 
    using namespace std;
    
    class CExample {
    private:
         int a;
    public:
          //    
         CExample(int b)
         { a = b;}
    
          //    
         void Show ()
         {
            cout<

    プログラムを実行し、画面出力100を実行します.以上のコードの実行結果から,システムはオブジェクトBにメモリを割り当て,オブジェクトAとのコピープロセスを完了していることが分かる.クラスオブジェクトの場合、同じタイプのクラスオブジェクトは、コンストラクション関数をコピーすることによってレプリケーションプロセス全体を完了します.
    次に、コピーコンストラクション関数の作業手順を例に挙げて説明します.
    #include 
    using namespace std;
    
    class CExample {
    private:
        int a;
    public:
        //    
        CExample(int b)
        { a = b;}
        
        //      
        CExample(const CExample& C)
        {
            a = C.a;
        }
    
        //    
        void Show ()
        {
            cout<

    CExample(const CExample&C)は、カスタマイズされたコピーコンストラクション関数です.コピーコンストラクション関数は、クラス名と一致する特殊なコンストラクション関数であり、このタイプの参照変数である必要があります.
  • コピーコンストラクション関数の使用タイミングはC++で、次の3つのオブジェクトはコピーコンストラクション関数を呼び出す必要があります!
  • オブジェクトは、関数パラメータ
  • に値伝達で伝達する.
    class CExample   
    {  
      private:  
        int a;  
      public:  
      //    
        CExample(int b) 
        {   
          a = b;  
          cout<

    呼び出しg_Fun()の場合、以下の重要なステップが発生する:(1).testオブジェクトがパラメータに入力されると,まず一時変数が生成されるので,Cと呼ぶ.(2).次にコピーコンストラクション関数を呼び出してtestの値をCに与えます.この2つのステップ全体が少し似ています:CExample C(test);(3).等g_Fun()が実行されると,Cオブジェクトが析出する.
  • オブジェクトは、値伝達によって関数から
  • を返す.
    class CExample   
    {  
      private:  
        int a;  
      public:  
        //        
        CExample(int b)  
        {   
          a = b;  
        }  
    
        //      
        CExample(const CExample& C)  
        {  
          a = C.a;  
          cout<

    g_Fun()関数がreturnに実行されると、(1).まず、一時変数が発生します.XXXXと言います.(2). コピーコンストラクタを呼び出してtempの値をXXXXに与えます.この2つのステップ全体が少し似ています:CExample XXXX(temp);(3). 関数が最後に実行されるまでtempローカル変数を解析します.(4). 等g_Fun()実行が完了してからXXXXオブジェクトを解析します.
  • オブジェクトは、別のオブジェクトによって初期化される必要がある.
  • CExample A(100);  
    CExample B = A;   
    // CExample B(A);   
    

    次の2つの文では、コピーコンストラクション関数が呼び出されます.
  • デフォルトのコピーコンストラクション関数は、コピーコンストラクション関数を知らない場合に、関数パラメータにオブジェクトを渡すか、関数がオブジェクトを返すかをうまく行うことができます.これは、コンパイラが自動的にコピーコンストラクション関数を生成するためです.これが「デフォルトのコピーコンストラクション関数」です.このコンストラクション関数は簡単です.「古いオブジェクト」のデータ・メンバーの値のみを使用して、「新しいオブジェクト」のデータ・メンバーを1つずつ割り当てます.通常、
  • という形式があります.
    Rect::Rect(const Rect& r)
    {
        width = r.width;
        height = r.height;
    }
    

    もちろん、以上のコードは私たちが書く必要はありません.コンパイラは私たちのために自動的に生成します.しかし、これでオブジェクトのレプリケーションの問題が解決できると考えられている場合は間違っています.次のコードを考えてみましょう.
    class Rect
    {
    public:
        Rect()      //     ,    1
        {
            count++;
        }
        ~Rect()     //     ,    1
        {
            count--;
        }
        static int getCount()       //        
        {
            return count;
        }
    private:
        int width;
        int height;
        static int count;       //           
    };
    
    int Rect::count = 0;        //       
    
    int main()
    {
        Rect rect1;
        cout<

    このコードは、前のクラスに対して、カウントを行うための静的メンバーを追加します.メイン関数では、まずオブジェクトrect 1を作成し、そのときのオブジェクト数を出力し、rect 1を使用してオブジェクトrect 2をコピーし、そのときのオブジェクト数を出力します.理解するように、このときは2つのオブジェクトが存在するはずですが、実際のプログラムの実行時には、出力されるのはすべて1で、1つのオブジェクトしか反応しません.また、オブジェクトを破棄すると、2つのオブジェクトの破棄が呼び出されるため、クラスの構造関数が2回呼び出され、カウンタが負の数になります.
    すなわち、コピーコンストラクタは静的データメンバーを処理する.
    これらの問題が最も根本的に発生したのは、オブジェクトをコピーするときにカウンタが増加しないことです.コピー構造関数を再作成します.次のようにします.
    class Rect
    {
    public:
        Rect()      //     ,    1
        {
            count++;
        }
        Rect(const Rect& r)   //       
        {
            width = r.width;
            height = r.height;
            count++;          //     1
        }
        ~Rect()     //     ,    1
        {
            count--;
        }
        static int getCount()   //        
        {
            return count;
        }
    private:
        int width;
        int height;
        static int count;       //           
    };
    
  • 浅いコピーと深いコピー
  • 浅いコピーと深いコピーの違い(1).コピーコンストラクション関数が定義されていない場合、デフォルトのコピー関数である浅いコピーが呼び出され、メンバーのコピーが完了します.データ・メンバーにポインタがない場合、浅いコピーは可能です.ただし、データ・メンバーにポインタがある場合、単純な浅いコピーを使用すると、2つのクラスの2つのポインタが同じアドレスを指し、オブジェクトがもうすぐ終了すると、2回の構造関数が呼び出され、ポインタのサスペンションが発生するため、この場合は深いコピーを使用する必要があります.(2). 深いコピーと浅いコピーの違いは、深いコピーがスタックメモリに別途スペースを申請してデータを格納することであり、ポインタのサスペンションの問題も解決します.簡単に言えば、データ・メンバーにポインタがある場合は、深いコピーを使用する必要があります.
  • 例は、c++のデフォルトのコピー構造関数が浅いコピー
  • であることを説明する.
    浅いコピーは、オブジェクトのデータ・メンバー間の単純な割り当てです.たとえば、クラスがなく、そのレプリケーション・コンストラクタが提供されていないように設計された場合、クラスのオブジェクトでオブジェクトに値を割り当てるときに実行されるプロセスは、次のような浅いコピーです.
    class A 
    { 
        public: 
        A(int _data) : data(_data){} 
        A(){}
        private: 
        int data;
     };
    int main() 
    { 
        A a(5), b = a; //              
    }
    

    この文b=a;浅いコピーで、この文を実行した後b.data=5です.オブジェクトにヒープ、ファイル、システムリソースなどの他のリソースがない場合は、深いコピーと浅いコピーの違いはありませんが、オブジェクトにこれらのリソースがある場合は、例:
    class A 
    { 
        public: 
        A(int _size) : size(_size)
        {
            data = new int[size];
        } //                
        A(){};
         ~A()
        {
            delete [] data;
        } //        
        private: 
        int* data;
        int size; 
    }
    int main() 
    { 
        A a(5), b = a; //       
    }
    

    ここでのb=aは、クラスAのレプリケーションコンストラクタがコンパイラによって生成されるため、b=aは浅いコピープロセスを実行する未定義の動作をもたらす.浅いコピーは、オブジェクトデータ間の単純な割り当てです.たとえば、次のようにします.
    b.size = a.size;
    b.data = a.data; // Oops!
    

    ここでbのポインタdataとaのポインタはスタック上の同じメモリを指し,aとbがプロファイル化されると,bはまずそのdataが指す動的に割り当てられたメモリを1回解放し,その後aがプロファイル化されると再びこの解放されたメモリを解放する.同じダイナミックメモリに対して2回以上解放を実行した結果は定義されていないため、メモリの漏洩やプログラムのクラッシュを引き起こす可能性があります.
    したがって、この問題を解決するには、コピー・オブジェクトに他のリソース(スタック、ファイル、システムなど)への参照がある場合(参照はポインタまたは参照であってもよい)、コピー・オブジェクトに他のリソースへの参照があるポインタまたは参照を単純に付与することなく、オブジェクトの別の新しいリソースを開く必要があります.
    class A 
    { 
        public: 
        A(int _size) : size(_size)
        {
            data = new int[size];
        } //                
        A(){};
        A(const A& _A) : size(_A.size)
        {
            data = new int[size];
        } //     
        ~A()
        {
            delete [] data;
        } //        
        private: 
        int* data; 
        int size;
     }
    int main() 
    { 
        A a(5), b = a; //         
    }
    

    まとめ:深いコピーと浅いコピーの違いは、オブジェクトの状態に他のオブジェクトの参照が含まれている場合、1つのオブジェクトをコピーする場合、そのオブジェクトが参照しているオブジェクトをコピーする必要がある場合は、深いコピーであり、そうでない場合は浅いコピーである.