「C ++の新機能」を本当に理解していますか?

「C ++の新機能」を本当に理解していますか?

メモリ割り当て

メモリ解放

の種類

オーバーロードできますか

::新着

::削除

番号

新着

削除

番号

::新着[]

:: delete []

番号

新着[]

delete []

番号

::演算子new

::演算子の削除

関数

はい

::演算子new []

::演算子new []

関数

はい

新規演算子

演算子削除

関数

はい

演算子new []

演算子new []

関数

はい

新しい配置

配置削除

関数

はい

注意:

①:: operator newは、グローバルなnew演算子、つまりグローバルなnew関数を指し、operator newは、「クラス型で一般的にオーバーロードする新しい演算子、つまり、クラス型に属する新しい関数」を指します。 ;

②実際、placement newは一種のグローバルなnew演算子であり、データオブジェクトを組み込みメモリ空間に初期化できます。つまり、データを組み込みメモリ空間に配置できます。

③new演算子、つまりnew関数は、他の操作を行わずにメモリ空間に適用するためにのみ使用され、オブジェクトはvoid *データ型のポインタです。

④new関数の関数、new式の関数、placement newの関数がわかっているので、演算子new +配置new = newexpressionを簡単に知ることができます。

⑤一般的に、「新機能」は次のように分類できます。

new演算子は、3つの形式に分割された関数です(最初の2つはコンストラクターを呼び出さないため、new演算子とは異なります)。 

⑴void*演算子new(std :: size_t size)throw(std :: bad_alloc); 

⑵void*演算子new(std :: size_t size、const std :: nothrow_t&nothrow_constant)throw(); 

⑶void*演算子new(std :: size_t size、void * ptr)throw(); 

最初のタイプは、サイズバイトのストレージスペースを割り当て、メモリ内のオブジェクトタイプを整列させます。成功した場合は、最初のアドレスへのnull以外のポインタを返します。bad_alloc例外のスローに失敗しました。 

2番目のタイプは、割り当てが失敗しても例外をスローせず、NULLポインターを返します。 

3つ目は、配置の新しいバージョンです。これは、基本的に、#include <new>で定義されている演算子newのオーバーロードです。メモリを割り当てず、適切なコンストラクタを呼び出して、ptrが指す場所にオブジェクトを構築してから、実際のパラメータポインタptrを返します。 

3つの新しい関数はすべてオーバーロードできます。特定のオーバーロード形式については、以下を参照してください。

異なるタイプのnew式とdelete式は例外を引き起こします

newとdeleteはペアであり、arraynewとarraydeleteはペアであると言うことがあります。これを誤って使用すると、次のようなエラーが発生します。

①newとdelete []を一緒に使用します。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base;  // new expression
    delete[] ptr;  // array delete expression
}  

 

出力結果:

 

②new[]を使用して一緒に削除する

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  // array new expression
    delete ptr;  // delete expression
}  

 

出力結果:

 

どのような状況でdeleteを不適切に使用すると、メモリリークが発生しますか?

まず、メモリリークとは、「使用済みのメモリスペースを効果的に回収できない」ことです。

①クラスタイプのデストラクタが無意味な場合、削除してもしなくてもメモリリークは発生しません。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  
} 

 

現時点では、デストラクタの実装にはヒープメモリの回復が含まれていないため、デストラクタは無意味です。プログラムがメイン関数の実行を終了すると、システムはスタックスタック内のプロジェクト全体のすべてのメモリを解放します。したがって、delete main関数の実行が完了したが、メモリがヒープ領域に残っているかどうかは発生しません。

②デストラクタに意味がある場合、クラスオブジェクトを削除しないとメモリリークが発生します

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  
    delete[] ptr;  
} 

 

delete []を呼び出さずに、ptrが指すヒープ領域に適用されたメモリスペースを解放しない場合、オペレーティングシステムはそれを完了しません。オペレーティングシステムは、プロジェクトファイルによって占有されているスタックメモリスペースのみを解放します。 main関数の実行後。オペレーティングシステムがスタック領域のメモリを解放すると、ヒープ領域に適用するメモリに対応するアドレスが見つかりません。

注意:

メモリリークの理由は、オペレーティングシステムがスタック領域のメモリを解放した後、ptrのメイン関数のローカル変数も解放されるためです。ptrは、村の「GPSナビゲーション」を見つけることと同等であることがわかっています。ヒープ領域。失うと、このGPSナビゲーションでは、ヒープ領域のメモリを再利用する方法が見つからなくなり、メモリリークが発生します。

③例外がトリガーされる前にdeleteを呼び出すためにメモリを解放すると、メモリリークも発生します。

なぜあなたはこれを言いましたか?例外がトリガーされたときの処理フローを詳細に説明する「例外処理の記事」がありますが、これはここでは重要ではありません。明確にする必要があります。例外がトリガーされると、システムは自動的にの一部を回復します。実行されたリモデル関数。スタックエリアメモリですが、この関数内で適用された「ヒープエリアメモリ」はリサイクルされないため、この時点でメモリリークが発生します。

コンパイラがpsが配置されているスタック領域のメモリを解放すると、特定のアドレスを介してヒープ領域によって動的に適用されるメモリ空間を見つけることができなくなります。このメモリ空間を見つけることはできません。解放することはもちろん、同等です。 「あなたは夕食のために家に帰りたいのですが、食べることについて話すためのドアさえ見つけることができません。」次の図は、私が説明したいものです。

 

これを防ぐために、スマートポインタを使用できます。「C ++入門書」では、スマートポインターの出現は上記の例から派生しています。なぜ、スマートポインターを使用するのですか。

スマートポインタを使用すると、自動メモリ回復が現実のものになります。

通常のグローバル関数operatornewおよびoperatordeleteの低レベルの実装

 

通常のグローバル関数の実装ソースコード:: operator new:

①まず、基礎となる関数mallocを使用して、サイズバイトのメモリを継続的に適用します。

②アプリケーションが失敗した場合、戻りポインタ値= NULL(NULL = 0)。このとき、while関数は継続的にポーリングと待機を行い、_callnewh(size_t)関数を継続的に呼び出します。この関数を使用してnewを呼び出します。関数ハンドル。関数ハンドルのトリガータイミングは、「コンピューターに使用可能なメモリがないか、メモリがほとんど残っていないが、サイズバイトが十分でない」です。C++は非常にスマートです。新しい関数ハンドルは、「いつ残りのメモリがほとんどない/メモリが残っているしばらくの間、これらの状況を復元するためにどのようなアクションを実行する必要がありますか?これは私たちに節約の機会を与えています。

③C++が提供するこの機会を十分に活用しない場合、つまり新しい関数ハンドルを定義しない場合、ローパスは_CATCH(const exception&exp)マクロを呼び出して例外bad_allocをスローします。

新しい関数へのハンドルを含むプログラムの例:

#include <iostream>  
using namespace std;  
  
void InsuffientMemory()  
{  
    cout << "内存已不足,等待处理......" << endl;  
}  
  
int main()  
{  
    new_handler InsuffientMemory_Handler = set_new_handler(InsuffientMemory);  
  
    void* ptr = ::operator new(1000000000); // 内存不足会调用new函数句柄  
}  

 

new式と演算子newの関係

新しい式の実行プロセスは、次の3つの部分で構成されます。

①メモリをvoid *タイプのポインタに割り当てます。

②void*ポインタを指定された型のポインタにキャストします。

③指定された型のデフォルトコンストラクタを呼び出します。

上記のプロセスによると、完全に新しい式の実装コードは次のとおりです。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = ::operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    ptr->~Base();  
}  

 

しかし、物事は逆効果になり、結果は私たちが期待したものではありませんでした:

 

上記の例外の理由は、「規制に違反してデフォルトのコンストラクターを呼び出した」ためです。デフォルトのコンストラクターを呼び出すことができないため、「placement new」を考えました。この新しい演算子は非常に優れています。初期化にデフォルトのコンストラクターを直接呼び出すことはできませんが、データの配置の特性でplacementnewを使用できます。既存のメモリ空間で、「メモリ領域のアプリケーションとメモリ領域のデータの初期化」の2つの操作をそれぞれ実装するために使用されます。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = ::operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
} 

 

この場合、それは完全にOKなので、最初に言いました:

機能的には、new式=演算子new +配置new。

配置新機能の魅力は何ですか?

演算子newは、コンストラクタによって初期化されていない「純粋なメモリ」に適用されます。このメモリを使用すると、さまざまなタイプのデータを構築できます。基本的なデータ型のデータだけでなく、クラスタイプのオブジェクトも構築できます。 「操作するには、配置の新しい関数を使用する必要があります」。「メモリアプリケーションとメモリスペースの使用を分離する」に基づいて、「メモリプール」の概念を構築しました。そのため、「新しい配置」と「新しいオペレータ」を「2つの主要なメモリ」と比較することがよくあります。プール建設」。

配置新機能の機能例:

このようなシナリオがある場合は、同様のメモリスペースを大量に適用してから解放する必要があります。たとえば、クライアントのサーバーリクエストでは、各クライアントのアップストリームデータごとにメモリを適用する必要があります。リクエストの処理を終了し、クライアントにダウンストリームの応答を返すと、メモリが解放されます。表面的には、C ++のメモリ管理要件を満たしているようで、エラーはありませんが、慎重に検討するのは非常に無理です。リクエストごとにメモリの一部を再申請する必要があるのはなぜですか。内部アプリケーションごとに、システムがメモリ内の連続メモリスペースの適切なサイズを見つける必要があることを知っておくことが重要です。このプロセスは非常に遅いです(比較的言えば) 。極端な場合、多数のメモリが断片化されていて、適用したスペースが大きい場合、失敗することさえあります。事前に用意した思い出を共有できないのはなぜですか?はい、placement newを使用してオブジェクトを作成できます。そうすると、指定したメモリ空間にオブジェクトが作成されます。 

削除式と演算子削除の関係

式の削除実行のプロセス:

①ポインタを使用してクラスタイプのデストラクタを呼び出します;(基本データ型にはデストラクタがないため、呼び出す必要はありません)

②このブロックのメモリスペースを解放します。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    ptr->~Base();  
    operator delete(ptr, sizeof(Base));  
}  

 

ここでは、演算子newを使用しています。実際、演算子newをオーバーロードする前は、演算子newは:: operator newですが、オーバーロード後は異なります。

 

演算子deleteの原理は、operator newの原理と同じです。以下は、「operator new / deletefunction」をオーバーロードするためのコードです。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    static void* operator new(size_t Size)  
    {  
        cout << "进行更加人性化的操作......" << endl;  
        return malloc(Size);  
    }  
    static void* operator new(size_t Size, void* ptr)  
    {  
        Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数  
        return temp;  
    }  
    static void operator delete(void* ptr, size_t Size)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    ptr->~Base();  
    operator delete(ptr, sizeof(Base));  
}  

 

結果は次のとおりです。

 

ただし、コンパイラがカスタム削除関数を呼び出さないことに気付きました。配置削除関数は、次の2つの方法で呼び出されます。

①オーバーロードされた削除関数の呼び出しタイミングは、削除のドメイン名を明示的に指定した場合にのみ呼び出されます。

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    static void* operator new(size_t Size)  
    {  
        cout << "进行更加人性化的操作......" << endl;  
        return malloc(Size);  
    }  
    static void* operator new(size_t Size, void* ptr)  
    {  
        cout << "调用自定义placement new函数" << endl;  
        Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数  
        return temp;  
    }  
    static void operator delete(void* ptr, size_t Size)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    static void operator delete(void* ptr)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    Base::operator delete(ptr, sizeof(Base)); // 显式的指出Base::作用域名  
}  

 

演算結果:

注:クラスタイプ内で演算子new / operator delete関数をオーバーロードすると(オーバーロードされたローカル演算子new / operator delete関数のみ)、オーバーロードされた演算子new / operator delete関数は、デフォルトでコンパイラによって静的と見なされます。属性の関数、つまり演算子new / operator deleteは、クラスタイプの静的メンバー変数として存在するため、オーバーロードされたバージョンを使用する場合は、class_name ::を使用して呼び出しを表示する必要があります。

②配置新機能で例外が発生した場合、つまりメモリ割り当て中に例外が発生した場合、対応する配置削除機能が呼び出されます。

プレースメントの削除はありますか?配置削除式は絶対に何もありません。ただし、配置削除式ではなく、配置削除機能があります。また、この配置削除機能は直接呼び出すことはできません。配置new式が配置new関数を呼び出すときに、コンストラクター関数の構築中に例外が発生した場合、今回はメモリリークを防止し、次に割り当てられたメモリをクリーンアップするために、この配置削除関数が必要です。このことの定義は、<new>ヘッダーファイルを開くことで確認できます。

 

ただし、公開される削除式は2つだけです。

 

つまり、非配列と配列の場合は削除します。

次に、このコンストラクターで例外が発生したときに配置削除関数を呼び出す状況を説明する例を示します。

#include <cstdlib>  
#include <iostream>  
  
struct A {};  
struct E {};  
  
class T  
{  
public:  
    T() { throw E(); }  
};  
  
void * operator new (std::size_t, const A &)  
{  
    void* nothing = 0;  
    std::cout << "Placement new called." << std::endl;  
    return nothing;  
}  
void operator delete (void *, const A &)  
{  
    std::cout << "Placement delete called." << std::endl;  
}  
  
int main()  
{  
    A a;  
    try {  
        T * p = new (a) T;  
    }  
    catch (E exp) { std::cout << "Exception caught." << std::endl; }  
    return 0;  
}  

 

演算結果:

 

上記はコピーを知っている友人の説明ですが、この例ではこの結果を呼び出しませんでした。VS2017での操作結果は次のとおりです。

 

何度も試しましたが、例外が発生したときです。VS2017のC ++コンパイラは、配置のnew関数が例外をトリガーしたときに、カスタムの配置削除関数を呼び出してメモリを解放しません。グローバル配置削除関数を呼び出して、この「半製品」をクリーンアップしてから、例外メカニズムをトリガーする例外。

ただし、安全のために、newごとに対応する演算子deleteを定義しているため、メモリ割り当て中に例外がトリガーされると(オブジェクトの構築時に)、対応するdeleteがメモリ回復のために呼び出されます。呼び出しの結果はコンパイラによって異なると思います。

newとdeleteを使用して構築されたメモリ割り当ての方法

 

注意:

①演算子new / operator deleteの最下層はCRT(Cランタイムライブラリ)のmalloc / freeを使用して実装されるため、operator new / operatordeleteはmalloc / freeの「進化版」であり、malloc / freeは演算子Inと比較されます。 new / operator deleteの用語では、malloc / freeの重要な違いは、「malloc / freeはオーバーロードできません」です。

②new式とdelete式を使用する場合、演算子new / operator deleteがオーバーロードされているかどうかに関係なく、:: operator new / :: operator deleteが呼び出される可能性が高いため、「グローバル関数:: operator new / :: operator delete "最も賢明なオプションではありません!

③実際、私たちは通常、演算子new / operator deleteをオーバーロードし、基礎となる実装関数として「global function :: operator new / delete」を使用します。または、関数の基礎となる実装としてmalloc / freeを直接使用することもできます。

 

④演算子のnew / delete関数をオーバーロードすることの本質は、「自分の手でメモリアプリケーションを制御する」ことです。これにより、後続のメモリ処理中に、よりユーザーフレンドリーな操作を実行できます。

 

おすすめ

転載: blog.csdn.net/weixin_45590473/article/details/112970002