12-bo‘lim
Nusxalash va ko'chirish
Nol qoidasi, uch qoidasi va besh qoidasi - obyekt nusxalanganda va ko'chirilganda nima sodir bo'ladi.
Ushbu bo‘lim mundarijasi
Ikki obyekt bir xil resursga ega bo'lsa - falokat. Bu bo'limda C++ ning eng ko'p yanglishtiradigan mavzusini bosqichma-bosqich ochamiz.
Standart nusxalash #
#include <print>
#include <string>
#include <vector>
#include <utility>
#include <type_traits>
#include <cstring>
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;
}
a: balandlik = 10, tarix hajmi 3
b: kenglik = 20, tarix hajmi 4
mustaqilmi: true
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 #
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;
}
bitta bufer:
+ 'salom' ajratildi
matn: salom
- 'salom' bo'shatildi
nusxa olsak nima bo'ladi - PASTGA QARANG
Kompilyator yaratgan nusxa konstruktori a'zolarni
bittalab nusxalaydi. char* malumot_ uchun bu
ko'rsatkichning o'zini nusxalash degani - u ko'rsatgan
xotirani emas.
Natijada:
| Qadam | Holat |
|---|---|
Buferr a{"salom"} | a.malumot_ → 0x1000 |
Buferr b = a | b.malumot_ → 0x1000 (o'sha joy) |
b yo'q qilinadi | delete[] 0x1000 |
a yo'q qilinadi | delete[] 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 #
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;
}
a = 'birinchi'
[nusxa konstruktori: 'birinchi']
b = 'birinchi'
[nusxa tayinlash: 'birinchi']
c = 'birinchi'
uch buferning manzillari har xilmi: true
o'ziga tayinlashdan keyin: 'birinchi'
Ko'p qo'llanmalarda shunday yoziladi:
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.
Ko'chirish nima uchun kerak #
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;
}
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)
Standart faqat shuni kafolatlaydi: ko'chirilgan obyekt to'g'ri, lekin noaniq holatda qoladi.
Bu nimani anglatadi:
| Mumkin | Mumkin emas |
|---|---|
| Yo'q qilish | Qiymatiga tayanish |
| Yangi qiymat tayinlash | Aniq holat kutish |
size(), clear() chaqirish | front() chaqirish |
Standart konteynerlar amalda bo'sh qoladi, lekin bunga tayanmang - bu kafolatlanmagan.
Ko'chirish konstruktorini yozganingizda manbani ataylab aniq holatga keltiring:
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 #
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;
}
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.
noexcept qilingstd::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:
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 #
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;
}
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
operator=(Bufer b) parametrni qiymat bo'yicha
oladi. Bu bitta funksiya bilan ikkala tayinlashni
qamrab oladi:
| Chaqiruv | b qanday quriladi |
|---|---|
d = x | nusxa 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
swapdan 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 #
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;
}
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'chirmaydistd::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:
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:
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 #
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;
}
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.
- Faqat
stringvavectora'zoli sinf yozing - nusxalanadimi? char*a'zoli sinf yozing va nusxa oling - nima bo'ladi?- Chuqur nusxa konstruktorini yozing.
- Nusxa tayinlashda avval ajratib, keyin o'chiring.
- O'ziga tayinlash tekshiruvini qo'shing va
a = asinang. - Ko'chirish konstruktorini
std::exchangebilan yozing. noexceptni olib tashlabis_nothrow_move_constructible_vni tekshiring.- Nusxa-almashtirish idiomasi bilan bitta
operator=yozing. push_back(x)vapush_back(std::move(x))chiqishini solishtiring.emplace_backni 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
noexceptqiling - aks holdavectornusxa oladi. - Nusxa-almashtirish idiomasi ikki tayinlashni bitta funksiyaga jamlaydi.
std::moveko'chirmaydi - u faqat aylantirish;return std::move(x)yozmang.
Keyingi bo'limda operatorlarni ortiqcha yuklash va qiymat semantikasini ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.