Kelebihan dalam C ++. Bagian III. Overloading pernyataan baru / hapus


Kami melanjutkan seri "C ++, menggali lebih dalam." Tujuan dari seri ini adalah untuk memberi tahu sebanyak mungkin tentang berbagai fitur bahasa, mungkin cukup istimewa. Artikel ini membahas tentang kelebihan operator new/delete. Ini adalah artikel ketiga dalam seri ini, yang pertama didedikasikan untuk fungsi dan templat yang berlebihan, terletak di sini , yang kedua didedikasikan untuk operator yang kelebihan beban, yang terletak di sini . Artikel ini menyimpulkan seri tiga artikel tentang kelebihan muatan di C ++.


Daftar Isi


Daftar Isi
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. Bentuk standar dari operator baru / hapus


C ++ mendukung beberapa opsi operator new/delete. Mereka dapat dibagi menjadi standar dasar, standar tambahan dan kebiasaan. Bagian ini dan bagian 2 membahas formulir standar, formulir kustom akan dibahas di bagian 3.

1.1. Bentuk standar dasar


Bentuk standar utama dari operator yang new/deletedigunakan saat membuat dan menghapus objek dan array jenis adalah Tsebagai berikut:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

Pekerjaan mereka dapat digambarkan sebagai berikut. Ketika operator dipanggil new, memori pertama dialokasikan ke objek. Jika seleksi berhasil, konstruktor dipanggil. Jika konstruktor melempar pengecualian, maka memori yang dialokasikan dibebaskan. Ketika operator dipanggil, deletesemuanya terjadi dalam urutan terbalik: pertama, destructor dipanggil, kemudian memori dibebaskan. Destruktor seharusnya tidak melempar pengecualian.

Ketika operatornew[]digunakan untuk membuat berbagai objek, memori pertama dialokasikan untuk seluruh array. Jika seleksi berhasil, maka konstruktor default (atau konstruktor lain, jika ada penginisialisasi) dipanggil untuk setiap elemen array mulai dari nol. Jika ada konstruktor yang melempar pengecualian, maka untuk semua elemen array yang dibuat, destruktor dipanggil dalam urutan terbalik dari panggilan konstruktor, maka memori yang dialokasikan akan dibebaskan. Untuk menghapus array, Anda harus memanggil operator delete[], dan untuk semua elemen array, destructor dipanggil dengan urutan terbalik dari konstruktor, kemudian memori yang dialokasikan dibebaskan.

Perhatian! Penting untuk memanggil bentuk operator yang benardeletetergantung pada apakah satu objek atau array dihapus. Aturan ini harus dipatuhi dengan ketat, jika tidak Anda bisa mendapatkan perilaku tidak terdefinisi, yaitu, apa pun bisa terjadi: kebocoran memori, kerusakan, dll. Lihat [Meyers1] untuk detailnya.

Dalam uraian di atas, satu klarifikasi diperlukan. Untuk apa yang disebut tipe sepele (tipe bawaan, struktur gaya C), konstruktor default mungkin tidak dipanggil, dan destruktor tidak melakukan apa pun dalam hal apa pun.

Fungsi alokasi memori standar, ketika tidak mungkin untuk memenuhi permintaan, melempar pengecualian tipe std::bad_alloc. Tetapi pengecualian ini dapat ditangkap, untuk ini Anda perlu menginstal pencegat global menggunakan panggilan fungsi set_new_handler(), untuk lebih jelasnya lihat [Meyers1].

Semua bentuk operatordeleteterapkan dengan aman ke null pointer.

Saat membuat array dengan operator, new[]ukurannya dapat diatur ke nol.

Kedua bentuk operator newmemungkinkan penggunaan inisialisasi dalam kawat gigi.

new int{42}
new int[8]{1,2,3,4}

1.2. Formulir standar tambahan


Saat menghubungkan file header <new>, 4 formulir operator standar tersedia new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

Dua yang pertama disebut newpenempatan non-alokasi new. Argumen ptradalah penunjuk ke wilayah memori yang cukup besar untuk menampung instance atau array. Selain itu, area memori harus memiliki perataan yang sesuai. Versi operator newini tidak mengalokasikan memori, melainkan hanya memberikan panggilan ke konstruktor. Dengan demikian, opsi ini memungkinkan Anda untuk memisahkan fase alokasi memori dan inisialisasi objek. Fitur ini secara aktif digunakan dalam wadah standar. Operator deleteuntuk objek yang dibuat dengan cara ini tentu saja tidak dapat dipanggil. Untuk menghapus objek, Anda harus langsung memanggil destruktor, dan kemudian membebaskan memori dengan cara yang tergantung pada metode alokasi memori.

Dua opsi kedua disebut operator tidak melempar pengecualian new(nothrow new) dan berbeda dalam hal jika tidak mungkin memenuhi permintaan, mereka kembali nullptr, tetapi tidak melempar pengecualian tipe std::bad_alloc. Menghapus objek terjadi menggunakan operator utama delete. Opsi ini dianggap usang dan tidak disarankan untuk digunakan.

1.3. Alokasi Memori dan Fungsi Gratis


Bentuk standar operator new/deletemenggunakan fungsi alokasi dan deallokasi berikut:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

Fungsi-fungsi ini didefinisikan dalam namespace global. Fungsi alokasi memori untuk pernyataan host newtidak melakukan apa pun dan mengembalikannya ptr.

C ++ 17 mendukung bentuk alokasi memori dan fungsi deallokasi tambahan, yang menunjukkan penyelarasan. Inilah beberapa di antaranya:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

Formulir ini tidak secara langsung dapat diakses oleh pengguna, mereka digunakan oleh kompiler untuk objek yang persyaratan penyelarasannya lebih unggul __STDCPP_DEFAULT_NEW_ALIGNMENT__, sehingga masalah utama adalah bahwa pengguna tidak sengaja menyembunyikannya (lihat bagian 2.2.1). Ingatlah bahwa dalam C ++ 11 menjadi mungkin untuk secara eksplisit mengatur penyelarasan tipe pengguna.

struct alignas(32) X { /* ... */ };

2. Kelebihan bentuk standar operator baru / hapus


Overloading bentuk standar operator new/deleteterdiri dari mendefinisikan fungsi yang ditentukan pengguna untuk mengalokasikan dan membebaskan memori yang tandatangannya sesuai dengan yang standar. Fungsi-fungsi ini dapat didefinisikan dalam namespace global atau dalam kelas, tetapi tidak dalam namespace selain global. Fungsi alokasi memori untuk pernyataan host standar newtidak dapat didefinisikan dalam namespace global. Setelah definisi seperti itu, operator yang sesuai new/deleteakan menggunakannya, bukan yang standar.

2.1. Kelebihan dalam namespace global


Misalkan, misalnya, dalam sebuah modul dalam ruang nama global yang fungsi-fungsi yang ditentukan pengguna didefinisikan:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

Dalam hal ini, sebenarnya akan ada penggantian (penggantian) fungsi standar untuk mengalokasikan dan membebaskan memori untuk semua panggilan operator new/deleteuntuk setiap kelas (termasuk yang standar) di seluruh modul. Ini dapat menyebabkan kekacauan total. Perhatikan bahwa mekanisme substitusi yang dijelaskan adalah mekanisme khusus yang diterapkan hanya untuk kasus ini, dan bukan mekanisme C ++ umum. Dalam hal ini, ketika mengimplementasikan fungsi pengguna untuk mengalokasikan dan membebaskan memori, menjadi tidak mungkin untuk memanggil fungsi standar yang sesuai, mereka sepenuhnya tersembunyi (operator ::tidak membantu), dan ketika Anda mencoba memanggilnya, panggilan rekursif ke fungsi pengguna terjadi.

Fungsi Ditentukan Namespace Global

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

Ini juga akan menggantikan yang standar, tetapi akan ada lebih sedikit potensi masalah, karena operator yang tidak melempar pengecualian newjarang digunakan. Tetapi formulir standar juga tidak tersedia.

Situasi yang sama dengan fungsi untuk array.

Pernyataan berlebihan new/deletedi namespace global sangat tidak disarankan.

2.2. Kelas terlalu banyak


Operator kelebihan muatan new/deletedi kelas tidak memiliki kelemahan yang dijelaskan di atas. Overloading hanya efektif ketika membuat dan menghapus instance dari kelas yang sesuai, terlepas dari konteks memohon operator new/delete. Saat menerapkan fungsi yang ditentukan pengguna untuk mengalokasikan dan membebaskan memori menggunakan operator, ::Anda dapat mengakses fungsi standar yang sesuai. Pertimbangkan sebuah contoh.

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

Dalam contoh ini, pelacakan hanya ditambahkan ke operasi standar. Sekarang, dalam hal new X()dan new X[N]akan menggunakan fungsi-fungsi ini untuk mengalokasikan dan membebaskan memori.

Fungsi-fungsi ini secara formal statis dan dapat dinyatakan sebagai static. Tetapi pada dasarnya mereka adalah instance, dengan pemanggilan fungsi operator new(), penciptaan instance dimulai, dan pemanggilan fungsi operator delete()menyelesaikan penghapusannya. Fungsi-fungsi ini tidak pernah dipanggil untuk tugas lain. Selain itu, seperti yang akan ditunjukkan di bawah, fungsi operator delete()ini pada dasarnya virtual. Jadi lebih tepat untuk mendeklarasikannya tanpa static.

2.2.1. Akses ke bentuk standar dari operator baru / hapus


Operator new/deletedapat digunakan dengan operator resolusi ruang lingkup tambahan, misalnya ::new(p) X(). Dalam hal ini, fungsi yang operator new()didefinisikan di kelas akan diabaikan, dan standar yang sesuai akan digunakan. Dengan cara yang sama, Anda dapat menggunakan operator delete.

2.2.2. Menyembunyikan bentuk lain dari operator baru / hapus


Jika sekarang untuk kelas Xkami mencoba menggunakan melempar atau tidak melempar pengecualian new, kami mendapatkan kesalahan. Faktanya adalah bahwa fungsi operator new(std::size_t size)akan menyembunyikan bentuk lain operator new(). Masalahnya bisa diselesaikan dengan dua cara. Pada yang pertama, Anda perlu menambahkan opsi yang sesuai ke kelas (opsi ini seharusnya hanya mendelegasikan operasi fungsi standar). Dalam yang kedua, Anda perlu menggunakan operator newdengan operator resolusi lingkup, misalnya ::new(p) X().

2.2.3. Kontainer standar


Jika kita mencoba menempatkan instance Xdalam beberapa wadah standar, misalnya std::vector<X>, kita akan melihat bahwa fungsi kita tidak digunakan untuk mengalokasikan dan mengosongkan memori. Faktanya adalah bahwa semua kontainer standar memiliki mekanisme sendiri untuk mengalokasikan dan membebaskan memori (kelas pengalokasi khusus, yang merupakan parameter templat dari wadah), dan mereka menggunakan operator penempatan untuk menginisialisasi elemen new.

2.2.4. Warisan


Fungsi untuk mengalokasikan dan membebaskan memori diwarisi. Jika fungsi-fungsi ini didefinisikan di kelas dasar, tetapi tidak di yang diturunkan, maka operator akan kelebihan beban untuk kelas turunan new/deletedan fungsi-fungsi yang didefinisikan dan dialokasikan di kelas dasar akan digunakan untuk mengalokasikan dan membebaskan memori.

Sekarang pertimbangkan hirarki kelas polimorfik, di mana setiap kelas membebani operator new/delete. Sekarang, biarkan instance dari kelas turunan dihapus menggunakan operator deletemelalui pointer ke kelas dasar. Jika destruktor dari kelas dasar adalah virtual, maka standar menjamin bahwa destruktor dari kelas turunan ini dipanggil. Dalam hal ini operator delete(), pemanggilan fungsi yang ditentukan untuk kelas turunan ini juga dijamin . Jadi, fungsinya operator delete()sebenarnya virtual.

2.2.5. Bentuk alternatif fungsi delete () operator


Di kelas (terutama ketika warisan digunakan), kadang-kadang nyaman untuk menggunakan bentuk alternatif fungsi untuk membebaskan memori:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

Parameter sizemenentukan ukuran elemen (bahkan dalam versi untuk array). Formulir ini memungkinkan Anda untuk menggunakan berbagai fungsi untuk mengalokasikan dan mengosongkan memori, tergantung pada kelas turunan tertentu.

3. Operator pengguna baru / hapus


C ++ dapat mendukung bentuk operator kustom dari newformulir berikut:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

Agar bentuk-bentuk ini didukung, perlu untuk menentukan fungsi yang sesuai untuk mengalokasikan dan membebaskan memori:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

Daftar parameter tambahan fungsi alokasi memori tidak boleh kosong dan tidak boleh terdiri dari satu void*atau const std::nothrow_t&, yaitu, tanda tangannya tidak boleh bertepatan dengan salah satu yang standar. Daftar parameter tambahan di operator new()dan operator delete()harus cocok. Argumen yang diteruskan ke operator newharus sesuai dengan parameter tambahan fungsi alokasi memori. Fungsi kustom operator delete()juga bisa dalam bentuk dengan parameter ukuran opsional.

Fungsi-fungsi ini dapat didefinisikan di namespace global atau di kelas, tetapi tidak di namespace selain global. Jika mereka didefinisikan dalam namespace global, mereka tidak menggantikan, tetapi overload, fungsi standar mengalokasikan dan membebaskan memori, sehingga penggunaannya dapat diprediksi dan aman, dan fungsi standar selalu tersedia. Jika mereka didefinisikan di kelas, mereka menyembunyikan formulir standar, tetapi akses ke formulir standar dapat diperoleh dengan menggunakan operator ::, ini dijelaskan secara rinci dalam bagian 2.2. Formulir

operator newyang newditentukan pengguna disebut penempatan yang ditentukan pengguna new. Mereka tidak boleh bingung dengan operator penempatan standar (tidak mengalokasikan) yang newdijelaskan dalam bagian 1.2.

Bentuk operator yang sesuai deletetidak ada. Ada newdua cara untuk menghapus objek yang dibuat menggunakan operator yang ditentukan pengguna . Jika fungsi yang ditentukan pengguna operator new()mendelegasikan operasi alokasi memori ke fungsi alokasi memori standar, maka operator standar dapat digunakan delete. Jika tidak, Anda harus memanggil destruktor secara eksplisit, dan kemudian fungsi yang ditentukan pengguna operator delete(). Kompilator memanggil fungsi yang ditentukan pengguna operator delete()hanya dalam satu kasus: ketika newkonstruktor melempar pengecualian selama operasi operator yang ditentukan pengguna .

Berikut ini sebuah contoh (dalam lingkup global).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. Definisi fungsi alokasi memori


Dalam contoh ini, fungsi pengguna operator new()dan operator delete()operasi yang didelegasikan sesuai dengan fungsi standar. Terkadang opsi ini berguna, tetapi tujuan utama overloading new/deleteadalah menciptakan mekanisme baru untuk mengalokasikan / membebaskan memori. Tugas ini tidak sederhana, dan sebelum melakukannya, seseorang harus dengan cermat memikirkan semuanya. Scott Meyers [Meyers1] membahas kemungkinan motif untuk membuat keputusan semacam itu (tentu saja, yang utama adalah efisiensi). Dia juga membahas masalah teknis utama yang terkait dengan implementasi yang benar dari fungsi yang ditentukan pengguna untuk mengalokasikan dan membebaskan memori (menggunakan fungsiset_new_handler(), sinkronisasi multi-utas, penyelarasan). Guntheroth memberikan contoh implementasi fungsi alokasi dan deallokasi memori yang didefinisikan pengguna yang relatif sederhana. Sebelum membuat versi Anda sendiri, Anda harus mencari solusi yang sudah jadi, sebagai contoh, Anda bisa membawa perpustakaan Pool dari proyek Boost.

5. Kelas allocator dari kontainer standar


Seperti disebutkan di atas, kontainer standar menggunakan kelas pengalokasi khusus untuk mengalokasikan dan membebaskan memori. Kelas-kelas ini adalah templat parameter wadah dan pengguna dapat menentukan versinya untuk kelas tersebut. Motif untuk solusi semacam itu kira-kira sama dengan untuk operator kelebihan muatan new/delete. [Guntheroth] menjelaskan cara membuat kelas seperti itu.

Bibliografi


[Guntheroth]
Gunteroth, Kurt. Optimalisasi program dalam C ++. Metode yang terbukti untuk meningkatkan produktivitas: Per. dari bahasa Inggris - SPb.: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Penggunaan C ++ secara efektif. 55 cara pasti untuk meningkatkan struktur dan kode program Anda:: Per. dari bahasa Inggris - M .: DMK Press, 2014.

All Articles