12-bo‘lim

Nusxalash va ko'chirish

Nol qoidasi, uch qoidasi va besh qoidasi - obyekt nusxalanganda va ko'chirilganda nima sodir bo'ladi.

🕑 15 daqiqa o‘qish 📄 959 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Standart nusxalash
  2. Resurs boshqarganda nima buziladi
  3. Uch qoidasi
  4. Ko'chirish nima uchun kerak
  5. noexcept nima uchun majburiy
  6. Besh qoidasi to'liq
  7. Ko'chirish qachon o'zi sodir bo'ladi
  8. Qaysi qoidani tanlash
  9. Xulosa

Ikki obyekt bir xil resursga ega bo'lsa - falokat. Bu bo'limda C++ ning eng ko'p yanglishtiradigan mavzusini bosqichma-bosqich ochamiz.

Standart nusxalash #

C++
#include <print>
#include <string>
#include <vector>
#include <utility>
#include <type_traits>
#include <cstring>
C++
struct Sozlama
{
    std::string nom;
    int qiymat{};
    std::vector<int> tarix;
};

int main()
{
    Sozlama a{"balandlik", 10, {1, 2, 3}};

    Sozlama b = a;              // nusxa
    b.nom = "kenglik";
    b.qiymat = 20;
    b.tarix.push_back(4);

    std::println("a: {} = {}, tarix hajmi {}", a.nom, a.qiymat, a.tarix.size());
    std::println("b: {} = {}, tarix hajmi {}", b.nom, b.qiymat, b.tarix.size());

    std::println("");
    std::println("mustaqilmi: {}", a.tarix.size() != b.tarix.size());

    return 0;
}
Natija
a: balandlik = 10, tarix hajmi 3
b: kenglik = 20, tarix hajmi 4

mustaqilmi: true
Nol qoidasi - eng yaxshi sinf hech narsa yozmaydi

Sozlama da konstruktor ham, destruktor ham, nusxa operatori ham yo'q. Lekin u mukammal nusxalanadi.

Sabab: uning a'zolari (std::string, std::vector) o'zlarini to'g'ri nusxalashni biladi. Kompilyator yaratgan nusxa konstruktori har bir a'zoni alohida nusxalaydi - va bu yetarli.

Bu nol qoidasi (Rule of Zero):

Agar sinfingiz resursni to'g'ridan-to'g'ri boshqarmasa, hech qanday maxsus a'zo funksiya yozmang.

Zamonaviy C++ da sinflarning katta ko'pchiligi shu toifaga kiradi. unique_ptr, vector, string ishlating - va besh qoidasi haqida o'ylash kerak bo'lmaydi.

Resurs boshqarganda nima buziladi #

C++
class Buferr
{
    char* malumot_;
    std::size_t hajm_;

public:
    explicit Buferr(const char* matn)
        : hajm_(std::strlen(matn))
    {
        malumot_ = new char[hajm_ + 1];
        std::strcpy(malumot_, matn);
        std::println("  + '{}' ajratildi", malumot_);
    }

    ~Buferr()
    {
        std::println("  - '{}' bo'shatildi", malumot_);
        delete[] malumot_;
    }

    const char* matn() const { return malumot_; }
};

int main()
{
    std::println("bitta bufer:");
    {
        Buferr a{"salom"};
        std::println("  matn: {}", a.matn());
    }

    std::println("");
    std::println("nusxa olsak nima bo'ladi - PASTGA QARANG");
    // {
    //     Buferr a{"salom"};
    //     Buferr b = a;        ← ikkalasi ham bir xil ko'rsatkichga ega
    // }                        ← delete[] IKKI MARTA - halokat

    return 0;
}
Natija
bitta bufer:
  + 'salom' ajratildi
  matn: salom
  - 'salom' bo'shatildi

nusxa olsak nima bo'ladi - PASTGA QARANG
Sayoz nusxa - ikki marta o'chirish

Kompilyator yaratgan nusxa konstruktori a'zolarni bittalab nusxalaydi. char* malumot_ uchun bu ko'rsatkichning o'zini nusxalash degani - u ko'rsatgan xotirani emas.

Natijada:

QadamHolat
Buferr a{"salom"}a.malumot_ → 0x1000
Buferr b = ab.malumot_0x1000 (o'sha joy)
b yo'q qilinadidelete[] 0x1000
a yo'q qilinadidelete[] 0x1000 yana

Ikkinchi delete[] - aniqlanmagan xatti-harakat. Odatda dastur "double free" xabari bilan qulaydi, ba'zan esa jimgina xotirani buzadi.

Xuddi shu muammo b = a tayinlashda ham bor, ustiga b ning eski buferi ham sizib chiqadi.

Uch qoidasi #

C++
class Buferr
{
    char* malumot_;
    std::size_t hajm_;

public:
    explicit Buferr(const char* matn) : hajm_(std::strlen(matn))
    {
        malumot_ = new char[hajm_ + 1];
        std::strcpy(malumot_, matn);
    }

    // 1. Destruktor
    ~Buferr() { delete[] malumot_; }

    // 2. Nusxa konstruktori - CHUQUR nusxa
    Buferr(const Buferr& b) : hajm_(b.hajm_)
    {
        malumot_ = new char[hajm_ + 1];
        std::strcpy(malumot_, b.malumot_);
        std::println("  [nusxa konstruktori: '{}']", malumot_);
    }

    // 3. Nusxa tayinlash operatori
    Buferr& operator=(const Buferr& b)
    {
        if (this == &b) return *this;          // o'ziga tayinlash

        char* yangi = new char[b.hajm_ + 1];   // AVVAL ajratamiz
        std::strcpy(yangi, b.malumot_);

        delete[] malumot_;                     // keyin eskisini o'chiramiz
        malumot_ = yangi;
        hajm_ = b.hajm_;

        std::println("  [nusxa tayinlash: '{}']", malumot_);
        return *this;
    }

    const char* matn() const { return malumot_; }
    std::size_t hajm() const { return hajm_; }
};

int main()
{
    Buferr a{"birinchi"};
    std::println("a = '{}'", a.matn());

    Buferr b = a;                  // nusxa konstruktori
    std::println("b = '{}'", b.matn());

    Buferr c{"uchinchi"};
    c = a;                         // nusxa tayinlash
    std::println("c = '{}'", c.matn());

    std::println("");
    std::println("uch buferning manzillari har xilmi: {}",
                 a.matn() != b.matn() && b.matn() != c.matn());

    a = a;                         // o'ziga tayinlash - xavfsiz
    std::println("o'ziga tayinlashdan keyin: '{}'", a.matn());

    return 0;
}
Natija
a = 'birinchi'
  [nusxa konstruktori: 'birinchi']
b = 'birinchi'
  [nusxa tayinlash: 'birinchi']
c = 'birinchi'

uch buferning manzillari har xilmi: true
o'ziga tayinlashdan keyin: 'birinchi'
Tayinlashda tartib muhim

Ko'p qo'llanmalarda shunday yoziladi:

KOD
delete[] malumot_;                 // AVVAL o'chirish
malumot_ = new char[b.hajm_ + 1];  // keyin ajratish

Bu xato. Agar new istisno tashlasa, obyekt buzilgan holatda qoladi: malumot_ osilgan ko'rsatkich va destruktor uni yana o'chirishga uradi.

To'g'ri tartib: avval ajrat, keyin o'chir. Shunda new muvaffaqiyatsiz bo'lsa ham obyekt eski, to'g'ri holatida qoladi. Bu kuchli istisno kafolati.

O'ziga tayinlash tekshiruvi (if (this == &b)) ham shuning uchun kerak - a = a da eski buferni o'chirib, keyin undan nusxa olishga urinish falokat bo'lardi.

Nusxa olish va ko'chirish farqi NUSXA (copy) manba "salom" nusxa "salom" YANGI Xotira ajratiladi, baytlar ko'chiriladi. Manba o'zgarmaydi. Narxi: O(n). KO'CHIRISH (move) manba nullptr yangi "salom" O'SHA Faqat ko'rsatkich o'tadi. Ajratish yo'q. Manba bo'sh qoladi. Narxi: O(1). Besh maxsus a'zo funksiya ~T() destruktor T(const T&) nusxa konstruktori T& operator=(const T&) nusxa tayinlash T(T&&) noexcept ko'chirish konstruktori T& operator=(T&&) noexcept ko'chirish tayinlash Bittasini yozsangiz - beshtasini o'ylang NOL QOIDASI - eng yaxshi yechim Xom ko'rsatkich o'rniga unique_ptr, vector, string ishlating. Ular beshtasini o'zlari to'g'ri bajaradi. Siz esa hech narsa yozmaysiz - va hech narsani buzmaysiz.
Nusxa yangi resurs yaratadi, ko'chirish esa mavjudini uzatadi

Ko'chirish nima uchun kerak #

C++
class KattaMalumot
{
    std::vector<int> ma_;

public:
    explicit KattaMalumot(std::size_t n) : ma_(n, 7)
    {
        std::println("  konstruktor: {} element", ma_.size());
    }

    KattaMalumot(const KattaMalumot& b) : ma_(b.ma_)
    {
        std::println("  NUSXA: {} element ko'chirildi", ma_.size());
    }

    KattaMalumot(KattaMalumot&& b) noexcept : ma_(std::move(b.ma_))
    {
        std::println("  KO'CHIRISH: ko'rsatkich o'tdi, manba hajmi {}",
                     b.ma_.size());
    }

    std::size_t hajm() const { return ma_.size(); }
};

KattaMalumot yarat(std::size_t n)
{
    KattaMalumot k{n};
    return k;
}

int main()
{
    std::println("1. nusxa olish:");
    KattaMalumot a{1000};
    KattaMalumot b = a;
    std::println("   a: {}, b: {}", a.hajm(), b.hajm());

    std::println("");
    std::println("2. oshkora ko'chirish:");
    KattaMalumot c = std::move(a);
    std::println("   a: {}, c: {}", a.hajm(), c.hajm());

    std::println("");
    std::println("3. funksiyadan qaytarish:");
    KattaMalumot d = yarat(500);
    std::println("   d: {}", d.hajm());
    std::println("   (nusxa ham, ko'chirish ham yo'q - NRVO)");

    return 0;
}
Natija
1. nusxa olish:
  konstruktor: 1000 element
  NUSXA: 1000 element ko'chirildi
   a: 1000, b: 1000

2. oshkora ko'chirish:
  KO'CHIRISH: ko'rsatkich o'tdi, manba hajmi 0
   a: 0, c: 1000

3. funksiyadan qaytarish:
  konstruktor: 500 element
   d: 500
   (nusxa ham, ko'chirish ham yo'q - NRVO)
Ko'chirilgan obyekt qanday holatda qoladi

Standart faqat shuni kafolatlaydi: ko'chirilgan obyekt to'g'ri, lekin noaniq holatda qoladi.

Bu nimani anglatadi:

MumkinMumkin emas
Yo'q qilishQiymatiga tayanish
Yangi qiymat tayinlashAniq holat kutish
size(), clear() chaqirishfront() chaqirish

Standart konteynerlar amalda bo'sh qoladi, lekin bunga tayanmang - bu kafolatlanmagan.

Ko'chirish konstruktorini yozganingizda manbani ataylab aniq holatga keltiring:

KOD
KattaMalumot(KattaMalumot&& b) noexcept
    : ma_(std::move(b.ma_))
{
    b.ma_.clear();     // aniq bo'sh
}

Bu qo'shimcha xarajat emas - lekin xatoni kamaytiradi.

noexcept nima uchun majburiy #

C++
struct Xavfsiz
{
    std::vector<int> ma_{1, 2, 3};

    Xavfsiz() = default;
    Xavfsiz(const Xavfsiz& b) : ma_(b.ma_) {}
    Xavfsiz(Xavfsiz&& b) noexcept : ma_(std::move(b.ma_)) {}
};

struct Xavfli
{
    std::vector<int> ma_{1, 2, 3};

    Xavfli() = default;
    Xavfli(const Xavfli& b) : ma_(b.ma_) {}
    Xavfli(Xavfli&& b) : ma_(std::move(b.ma_)) {}   // noexcept YO'Q
};

int main()
{
    std::println("Xavfsiz ko'chirish noexcept mi: {}",
                 std::is_nothrow_move_constructible_v<Xavfsiz>);
    std::println("Xavfli  ko'chirish noexcept mi: {}",
                 std::is_nothrow_move_constructible_v<Xavfli>);

    std::println("");
    std::println("vector o'sganda nima qiladi:");
    std::println("  Xavfsiz -> ko'chiradi  (tez)");
    std::println("  Xavfli  -> NUSXA oladi (sekin)");

    std::println("");
    std::println("Sabab: vector kuchli istisno kafolatini saqlashi kerak.");
    std::println("Ko'chirish yarim yo'lda istisno tashlasa,");
    std::println("eski ham, yangi ham buzilgan bo'ladi.");
    std::println("Shuning uchun noexcept bo'lmasa - nusxaga qaytadi.");

    return 0;
}
Natija
Xavfsiz ko'chirish noexcept mi: true
Xavfli  ko'chirish noexcept mi: false

vector o'sganda nima qiladi:
  Xavfsiz -> ko'chiradi  (tez)
  Xavfli  -> NUSXA oladi (sekin)

Sabab: vector kuchli istisno kafolatini saqlashi kerak.
Ko'chirish yarim yo'lda istisno tashlasa,
eski ham, yangi ham buzilgan bo'ladi.
Shuning uchun noexcept bo'lmasa - nusxaga qaytadi.
Ko'chirish operatsiyalarini har doim noexcept qiling

std::vector o'sganda elementlarni yangi xotiraga ko'chirishi kerak. U buni faqat ko'chirish noexcept bo'lsagina qiladi.

Aks holda vektor nusxa oladi - garchi siz ko'chirish konstruktorini yozgan bo'lsangiz ham. Natijada butun optimallashtirish behuda ketadi va buni sezmaysiz ham.

Ko'chirish deyarli har doim noexcept bo'lishi mumkin, chunki u faqat ko'rsatkichlarni almashtiradi - xotira ajratmaydi.

Tekshirish usuli:

KOD
static_assert(std::is_nothrow_move_constructible_v<MeningTurim>);
static_assert(std::is_nothrow_move_assignable_v<MeningTurim>);

Bu ikki qatorni resurs boshqaradigan har bir sinfingizga qo'ying.

Besh qoidasi to'liq #

C++
class Bufer
{
    int* malumot_{nullptr};
    std::size_t hajm_{0};

public:
    Bufer() = default;

    explicit Bufer(std::size_t n) : malumot_(new int[n]{}), hajm_(n) {}

    // 1. Destruktor
    ~Bufer() { delete[] malumot_; }

    // 2. Nusxa konstruktori
    Bufer(const Bufer& b) : malumot_(new int[b.hajm_]), hajm_(b.hajm_)
    {
        for (std::size_t i = 0; i < hajm_; ++i) malumot_[i] = b.malumot_[i];
    }

    // 3. Ko'chirish konstruktori
    Bufer(Bufer&& b) noexcept
        : malumot_(std::exchange(b.malumot_, nullptr)),
          hajm_(std::exchange(b.hajm_, 0)) {}

    // 4 va 5. Nusxa-almashtirish idiomasi ikkalasini qamrab oladi
    Bufer& operator=(Bufer b) noexcept     // QIYMAT bo'yicha
    {
        std::swap(malumot_, b.malumot_);
        std::swap(hajm_, b.hajm_);
        return *this;
    }

    std::size_t hajm() const { return hajm_; }
    int& operator[](std::size_t i) { return malumot_[i]; }
    int operator[](std::size_t i) const { return malumot_[i]; }
    bool boshmi() const { return malumot_ == nullptr; }
};

int main()
{
    Bufer a{5};
    for (std::size_t i = 0; i < a.hajm(); ++i) a[i] = static_cast<int>(i * i);

    std::print("a:");
    for (std::size_t i = 0; i < a.hajm(); ++i) std::print(" {}", a[i]);
    std::println("");

    Bufer b = a;                   // nusxa
    b[0] = 999;
    std::println("nusxadan keyin a[0]={}, b[0]={}", a[0], b[0]);

    Bufer c = std::move(a);        // ko'chirish
    std::println("ko'chirishdan keyin a bo'shmi: {}, c hajmi: {}",
                 a.boshmi(), c.hajm());

    Bufer d;
    d = b;                         // nusxa-tayinlash (b nusxalanadi)
    std::println("d[0]={}, b hali tirik: {}", d[0], !b.boshmi());

    Bufer e;
    e = std::move(c);              // ko'chirish-tayinlash
    std::println("e hajmi: {}, c bo'shmi: {}", e.hajm(), c.boshmi());

    std::println("");
    std::println("noexcept ko'chirish: {}",
                 std::is_nothrow_move_constructible_v<Bufer>);

    return 0;
}
Natija
a: 0 1 4 9 16
nusxadan keyin a[0]=0, b[0]=999
ko'chirishdan keyin a bo'shmi: true, c hajmi: 5
d[0]=999, b hali tirik: true
e hajmi: 5, c bo'shmi: true

noexcept ko'chirish: true
Nusxa-almashtirish idiomasi

operator=(Bufer b) parametrni qiymat bo'yicha oladi. Bu bitta funksiya bilan ikkala tayinlashni qamrab oladi:

Chaqiruvb qanday quriladi
d = xnusxa konstruktori
d = std::move(x)ko'chirish konstruktori

Keyin swap ni bajaramiz: bizning eski ma'lumotimiz b ga o'tadi va b funksiya oxirida uni o'zi yo'q qiladi.

Ustunliklari:

  • O'ziga tayinlash avtomatik xavfsiz
  • Kuchli istisno kafolati - nusxa swap dan oldin tugaydi
  • Ikki funksiya o'rniga bitta
  • Kod takrorlanmaydi

Kamchiligi: d = x da har doim yangi ajratish bo'ladi, hatto d ning hajmi yetarli bo'lsa ham. Ishlash muhim bo'lgan joyda alohida ikki operator yozing.

std::exchange(b.malumot_, nullptr) esa "qiymatni ol va o'rniga nullptr qo'y" degani - ko'chirish konstruktorlari uchun ideal.

Ko'chirish qachon o'zi sodir bo'ladi #

C++
struct Kuzatuv
{
    std::string nom;

    explicit Kuzatuv(std::string n) : nom(std::move(n)) {}

    Kuzatuv(const Kuzatuv& b) : nom(b.nom)
    { std::println("    nusxa: {}", nom); }

    Kuzatuv(Kuzatuv&& b) noexcept : nom(std::move(b.nom))
    { std::println("    ko'chirish: {}", nom); }
};

void qabul(Kuzatuv k) { std::println("    qabul qilindi: {}", k.nom); }

int main()
{
    Kuzatuv a{"birinchi"};

    std::println("1. nomli o'zgaruvchini uzatish:");
    qabul(a);

    std::println("");
    std::println("2. vaqtinchalik obyektni uzatish:");
    qabul(Kuzatuv{"vaqtinchalik"});

    std::println("");
    std::println("3. std::move bilan:");
    qabul(std::move(a));

    std::println("");
    std::println("4. vektorga qo'shish:");
    std::vector<Kuzatuv> v;
    v.reserve(4);
    Kuzatuv b{"b"};
    v.push_back(b);                    // nusxa
    v.push_back(std::move(b));         // ko'chirish
    v.emplace_back("joyida");          // ikkalasi ham yo'q

    std::println("");
    std::println("vektorda {} element", v.size());

    return 0;
}
Natija
1. nomli o'zgaruvchini uzatish:
    nusxa: birinchi
    qabul qilindi: birinchi

2. vaqtinchalik obyektni uzatish:
    qabul qilindi: vaqtinchalik

3. std::move bilan:
    ko'chirish: birinchi
    qabul qilindi: birinchi

4. vektorga qo'shish:
    nusxa: b
    ko'chirish: b

vektorda 3 element
std::move hech narsani ko'chirmaydi

std::move - bu funksiya emas, aylantirish. U faqat T&& ga o'zgartiradi va shu bilan ortiqcha yuk tanlovini o'zgartiradi.

Ko'chirishning o'zini ko'chirish konstruktori bajaradi. Agar tur ko'chirish konstruktoriga ega bo'lmasa, std::move jimgina nusxaga tushadi:

KOD
const std::string s = "salom";
std::string t = std::move(s);     // NUSXA - s const

const obyektni ko'chirib bo'lmaydi. Kompilyator xato bermaydi - shunchaki nusxa oladi.

Yana bir tuzoq:

KOD
return std::move(mahalliy);       // YOMON
return mahalliy;                  // YAXSHI

return std::move(x) NRVO ni buzadi - kompilyator obyektni to'g'ridan-to'g'ri qaytariladigan joyda qurish imkoniyatidan mahrum bo'ladi.

Qaysi qoidani tanlash #

C++
int main()
{
    std::println("{:<34} {}", "SINF QANDAY", "NIMA YOZASIZ");
    std::println("{}", std::string(64, '-'));
    std::println("{:<34} {}", "Faqat qiymat a'zolari", "HECH NARSA (nol)");
    std::println("{:<34} {}", "vector/string/unique_ptr", "HECH NARSA (nol)");
    std::println("{:<34} {}", "Xom new/delete", "BESHTASI");
    std::println("{:<34} {}", "Fayl tutqichi, rozetka", "BESHTASI");
    std::println("{:<34} {}", "Nusxalanmasligi kerak", "delete + ko'chirish");
    std::println("{:<34} {}", "Bazaviy polimorf sinf", "virtual ~T()");

    std::println("");
    std::println("Zamonaviy tavsiya:");
    std::println("  Resursni ALOHIDA kichik sinfga o'rang.");
    std::println("  Faqat o'sha sinf beshtasini yozsin.");
    std::println("  Qolgan barcha sinflar nol qoidasida qolsin.");

    return 0;
}
Natija
SINF QANDAY                        NIMA YOZASIZ
----------------------------------------------------------------
Faqat qiymat a'zolari              HECH NARSA (nol)
vector/string/unique_ptr           HECH NARSA (nol)
Xom new/delete                     BESHTASI
Fayl tutqichi, rozetka             BESHTASI
Nusxalanmasligi kerak              delete + ko'chirish
Bazaviy polimorf sinf              virtual ~T()

Zamonaviy tavsiya:
  Resursni ALOHIDA kichik sinfga o'rang.
  Faqat o'sha sinf beshtasini yozsin.
  Qolgan barcha sinflar nol qoidasida qolsin.
Amaliy topshiriq
  1. Faqat string va vector a'zoli sinf yozing - nusxalanadimi?
  2. char* a'zoli sinf yozing va nusxa oling - nima bo'ladi?
  3. Chuqur nusxa konstruktorini yozing.
  4. Nusxa tayinlashda avval ajratib, keyin o'chiring.
  5. O'ziga tayinlash tekshiruvini qo'shing va a = a sinang.
  6. Ko'chirish konstruktorini std::exchange bilan yozing.
  7. noexcept ni olib tashlab is_nothrow_move_constructible_v ni tekshiring.
  8. Nusxa-almashtirish idiomasi bilan bitta operator= yozing.
  9. push_back(x) va push_back(std::move(x)) chiqishini solishtiring.
  10. emplace_back ni qo'shing - nechta konstruktor chaqirildi?

Xulosa #

  • Nol qoidasi: resursni bevosita boshqarmasangiz, hech narsa yozmang.
  • Kompilyator nusxasi a'zolarni bittalab nusxalaydi - xom ko'rsatkich uchun bu sayoz.
  • Sayoz nusxa ikki marta o'chirish va sizishga olib keladi.
  • Uch qoidasi: destruktor, nusxa konstruktori, nusxa tayinlash - biri bo'lsa uchalasi.
  • Besh qoidasi: ustiga ko'chirish konstruktori va ko'chirish tayinlash.
  • Tayinlashda avval ajrating, keyin o'chiring - kuchli istisno kafolati.
  • Ko'chirish resursni uzatadi, nusxa esa yangisini yaratadi.
  • Ko'chirish operatsiyalarini har doim noexcept qiling - aks holda vector nusxa oladi.
  • Nusxa-almashtirish idiomasi ikki tayinlashni bitta funksiyaga jamlaydi.
  • std::move ko'chirmaydi - u faqat aylantirish; return std::move(x) yozmang.

Keyingi bo'limda operatorlarni ortiqcha yuklash va qiymat semantikasini ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.