14-bo‘lim

Vorislik va polimorfizm

Virtual funksiyalar, abstrakt interfeyslar, kesilish muammosi va vorislik qachon kerak emasligi.

🕑 13 daqiqa o‘qish 📄 798 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Virtual funksiya
  2. override - ixtiyoriy emas
  3. Kesilish (slicing)
  4. Abstrakt interfeys
  5. Kompozitsiya - ko'pincha yaxshiroq
  6. std::function bilan
  7. Virtual chaqiruvning narxi
  8. Xulosa

Vorislik - C++ ning eng ko'p suiiste'mol qilinadigan imkoniyati. Bu bo'limda uni qachon ishlatish va - eng muhimi - qachon ishlatmaslik kerakligini ko'ramiz.

Virtual funksiya #

C++
#include <print>
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <algorithm>
C++
class Xodim
{
    std::string ism_;
    long long oylik_;

public:
    Xodim(std::string ism, long long oylik)
        : ism_(std::move(ism)), oylik_(oylik) {}

    virtual ~Xodim() = default;          // MAJBURIY

    const std::string& ism() const { return ism_; }
    long long oylik() const { return oylik_; }

    virtual long long oylikTolov() const { return oylik_; }
    virtual std::string lavozim() const { return "xodim"; }
};

class Dasturchi : public Xodim
{
    int loyihalar_;

public:
    Dasturchi(std::string ism, long long oylik, int loyihalar)
        : Xodim(std::move(ism), oylik), loyihalar_(loyihalar) {}

    long long oylikTolov() const override
    {
        return oylik() + loyihalar_ * 500'000;
    }

    std::string lavozim() const override { return "dasturchi"; }
};

class Menejer : public Xodim
{
    int qol_ostidagilar_;

public:
    Menejer(std::string ism, long long oylik, int qol)
        : Xodim(std::move(ism), oylik), qol_ostidagilar_(qol) {}

    long long oylikTolov() const override
    {
        return oylik() + qol_ostidagilar_ * 200'000;
    }

    std::string lavozim() const override { return "menejer"; }
};

int main()
{
    std::vector<std::unique_ptr<Xodim>> jamoa;
    jamoa.push_back(std::make_unique<Dasturchi>("Husanboy", 8'000'000, 3));
    jamoa.push_back(std::make_unique<Menejer>("Nodira", 9'000'000, 6));
    jamoa.push_back(std::make_unique<Xodim>("Kamola", 5'000'000));

    long long jami = 0;
    for (const auto& x : jamoa) {
        std::println("{:<10} {:<11} {:>10}", x->ism(), x->lavozim(), x->oylikTolov());
        jami += x->oylikTolov();
    }

    std::println("{}", std::string(33, '-'));
    std::println("{:<22} {:>10}", "JAMI", jami);

    return 0;
}
Natija
Husanboy   dasturchi      9500000
Nodira     menejer       10200000
Kamola     xodim          5000000
---------------------------------
JAMI                     24700000
Bazaviy sinfda virtual destruktor shart
KOD
Xodim* x = new Dasturchi{"...", 1, 1};
delete x;              // virtual ~Xodim() bo'lmasa - UB

Virtual destruktorsiz faqat ~Xodim() chaqiriladi. Dasturchi ning a'zolari yo'q qilinmaydi - va agar ular resurs boshqarsa, u sizib chiqadi.

Qoida oddiy:

SinfDestruktor
Vorislik uchun mo'ljallanganvirtual ~T() = default;
Vorislik uchun mo'ljallanmaganfinal qiling
Faqat interfeys, o'chirilmaydiprotected: ~T() = default;

Uchinchi variant kamroq ma'lum, lekin foydali: u bazaviy ko'rsatkich orqali o'chirishni kompilyatsiya paytida taqiqlaydi va virtual jadval xarajatini keltirmaydi.

override - ixtiyoriy emas #

C++
class Asos
{
public:
    virtual ~Asos() = default;
    virtual void ishla(int n) const { std::println("  Asos::ishla({})", n); }
    virtual void tekshir() const     { std::println("  Asos::tekshir()"); }
};

class Voris : public Asos
{
public:
    // override YOZILGAN - kompilyator imzoni tekshiradi
    void ishla(int n) const override { std::println("  Voris::ishla({})", n); }

    // override YOZILMAGAN - va imzo mos emas (const yo'q)
    // Bu YANGI funksiya, qayta aniqlash EMAS
    void tekshir() { std::println("  Voris::tekshir() - const YO'Q"); }
};

int main()
{
    Voris v;
    Asos& a = v;

    std::println("Asos havolasi orqali:");
    a.ishla(5);
    a.tekshir();          // Asos ning versiyasi - kutilmagan!

    std::println("");
    std::println("To'g'ridan-to'g'ri:");
    v.ishla(5);
    v.tekshir();

    std::println("");
    std::println("Ikkinchi holatda ikki xil funksiya chaqirildi.");
    std::println("override yozilganida kompilyator xato berardi.");

    return 0;
}
Natija
Asos havolasi orqali:
  Voris::ishla(5)
  Asos::tekshir()

To'g'ridan-to'g'ri:
  Voris::ishla(5)
  Voris::tekshir() - const YO'Q

Ikkinchi holatda ikki xil funksiya chaqirildi.
override yozilganida kompilyator xato berardi.
Har bir qayta aniqlashga override yozing

override funksiyani virtual qilmaydi - u kompilyatordan tekshirishni so'raydi: "bu haqiqatan ham bazaviy sinfdagi virtual funksiyanimi?"

Mos kelmaydigan holatlar:

FarqNatija override siz
const qo'shildi/tushdiYangi funksiya
Parametr turi boshqaOrtiqcha yuk
Qaytish turi boshqaKompilyatsiya xatosi
Nom xato yozilganYangi funksiya

override bilan bularning hammasi kompilyatsiya xatosi bo'ladi.

final ham bor: void ishla(int) const override final; bu funksiyani boshqa qayta aniqlab bo'lmasligini bildiradi va kompilyatorga virtual chaqiruvni to'g'ridan-to'g'ri chaqiruvga aylantirish imkonini beradi.

Kesilish (slicing) #

C++
struct Shakl
{
    virtual ~Shakl() = default;
    virtual std::string nomi() const { return "shakl"; }
    virtual double yuza() const { return 0.0; }
};

struct Doira : Shakl
{
    double r{};
    explicit Doira(double radius) : r(radius) {}
    std::string nomi() const override { return "doira"; }
    double yuza() const override { return 3.14159265 * r * r; }
};

void qiymatBoyicha(Shakl s)         // KESILADI
{
    std::println("  qiymat bo'yicha: {} yuza {:.3f}", s.nomi(), s.yuza());
}

void havolaBoyicha(const Shakl& s)  // to'g'ri
{
    std::println("  havola bo'yicha: {} yuza {:.3f}", s.nomi(), s.yuza());
}

int main()
{
    Doira d{2.0};

    std::println("Doira{{2.0}} - haqiqiy yuzasi {:.3f}", d.yuza());
    std::println("");

    havolaBoyicha(d);
    qiymatBoyicha(d);

    std::println("");
    std::println("Konteynerda ham xuddi shunday:");
    std::vector<Shakl> yomon;
    yomon.push_back(Doira{3.0});
    std::println("  vector<Shakl> ichida: {} yuza {:.3f}",
                 yomon[0].nomi(), yomon[0].yuza());

    std::vector<std::unique_ptr<Shakl>> yaxshi;
    yaxshi.push_back(std::make_unique<Doira>(3.0));
    std::println("  vector<unique_ptr> ichida: {} yuza {:.3f}",
                 yaxshi[0]->nomi(), yaxshi[0]->yuza());

    return 0;
}
Natija
Doira{2.0} - haqiqiy yuzasi 12.566

  havola bo'yicha: doira yuza 12.566
  qiymat bo'yicha: shakl yuza 0.000

Konteynerda ham xuddi shunday:
  vector<Shakl> ichida: shakl yuza 0.000
  vector<unique_ptr> ichida: doira yuza 28.274
Kesilish - jimgina ma'lumot yo'qotish

Shakl s = d; da faqat Shakl qismi nusxalanadi. Doira ning r maydoni tashlab yuboriladi va virtual jadval ko'rsatkichi Shakl niki bo'lib qoladi.

Kompilyator hech qanday ogohlantirish bermaydi.

Kesilish sodir bo'ladigan joylar:

KodKesiladimi
void f(Shakl s)Ha
void f(const Shakl& s)Yo'q
void f(Shakl* s)Yo'q
std::vector<Shakl>Ha
std::vector<std::unique_ptr<Shakl>>Yo'q
Shakl s = d;Ha
Shakl& s = d;Yo'q

Himoya usuli: polimorf bazaviy sinfning nusxa operatsiyalarini protected yoki = delete qiling. Shunda kesilish kompilyatsiya xatosiga aylanadi.

Abstrakt interfeys #

C++
// Sof virtual funksiya - ta'rifsiz
class Toshkichik
{
public:
    virtual ~Toshkichik() = default;
    virtual bool tolash(long long tiyin) = 0;
    virtual std::string nomi() const = 0;
    virtual long long komissiya(long long tiyin) const = 0;
};

class Karta : public Toshkichik
{
    long long balans_;

public:
    explicit Karta(long long balans) : balans_(balans) {}

    bool tolash(long long tiyin) override
    {
        long long jami = tiyin + komissiya(tiyin);
        if (jami > balans_) return false;
        balans_ -= jami;
        return true;
    }

    std::string nomi() const override { return "karta"; }
    long long komissiya(long long tiyin) const override { return tiyin / 100; }
    long long balans() const { return balans_; }
};

class Naqd : public Toshkichik
{
public:
    bool tolash(long long) override { return true; }
    std::string nomi() const override { return "naqd"; }
    long long komissiya(long long) const override { return 0; }
};

void xarid(Toshkichik& usul, long long narx)
{
    bool natija = usul.tolash(narx);
    std::println("  {:<6} {:>10} tiyin, komissiya {:>6}, natija: {}",
                 usul.nomi(), narx, usul.komissiya(narx), natija);
}

int main()
{
    Karta k{1'000'000};
    Naqd n;

    std::println("to'lovlar:");
    xarid(k, 300'000);
    xarid(n, 300'000);
    xarid(k, 900'000);

    std::println("");
    std::println("kartada qoldi: {} tiyin", k.balans());

    // Toshkichik t;   ← kompilyatsiya xatosi: abstrakt sinf

    return 0;
}
Natija
to'lovlar:
  karta      300000 tiyin, komissiya   3000, natija: true
  naqd       300000 tiyin, komissiya      0, natija: true
  karta      900000 tiyin, komissiya   9000, natija: false

kartada qoldi: 697000 tiyin
Vorislik kerakmi - qaror daraxti Bir necha xil xatti-harakat kerakmi? ha Tanlov ISH PAYTIDA aniqlanadimi? yo'q SHABLON ishlating Kompilyatsiya paytida hal bo'ladi. Virtual jadval xarajati yo'q. template <typename T> ha VIRTUAL funksiya Abstrakt interfeys + unique_ptr. Bitta bilvosita chaqiruv narxi. virtual void f() = 0; yo'q ODDIY SINF Vorislik kerak emas. Kompozitsiya bas. Kod qayta ishlatish uchun voris QILMANG "Menda shu funksiya bor" - bu a'zo qiling, voris emas. Liskov sinovi: vorisni bazaviy sinf o'rniga qo'yganda kod BUZILMASLIGI kerak. Buzilsa - bu "is-a" emas, "has-a". Kompozitsiya ishlating.
Vorislik - interfeys uchun, kod qayta ishlatish uchun emas

Kompozitsiya - ko'pincha yaxshiroq #

C++
// YOMON: Stack Vector dan voris qilingan
// class Stack : public std::vector<int> { };
// Muammo: Stack endi push_back, insert, operator[] ni ham beradi

// YAXSHI: kompozitsiya
class Stek
{
    std::vector<int> ma_;          // A'ZO, bazaviy sinf emas

public:
    void qoy(int q) { ma_.push_back(q); }

    int ol()
    {
        int oxirgi = ma_.back();
        ma_.pop_back();
        return oxirgi;
    }

    int tepa() const { return ma_.back(); }
    bool boshmi() const { return ma_.empty(); }
    std::size_t soni() const { return ma_.size(); }
};

int main()
{
    Stek s;
    s.qoy(10);
    s.qoy(20);
    s.qoy(30);

    std::println("elementlar: {}", s.soni());
    std::println("tepa: {}", s.tepa());

    std::println("");
    std::print("chiqarish tartibi:");
    while (!s.boshmi()) std::print(" {}", s.ol());
    std::println("");

    std::println("");
    std::println("Stek faqat 5 ta amalni ochadi.");
    std::println("vector dan voris qilinsa - 40 dan ortiq amal ochilardi,");
    std::println("shu jumladan stek qoidasini buzadiganlari.");

    return 0;
}
Natija
elementlar: 3
tepa: 30

chiqarish tartibi: 30 20 10

Stek faqat 5 ta amalni ochadi.
vector dan voris qilinsa - 40 dan ortiq amal ochilardi,
shu jumladan stek qoidasini buzadiganlari.
Vorislik o'rniga uch muqobil

Ko'p holatda vorislik kerak emas:

1. Kompozitsiya - "menda bor" munosabati:

KOD
class Mashina {
    Dvigatel dvigatel_;      // voris emas, a'zo
};

2. std::function - bitta xatti-harakat kerak bo'lsa, butun sinf ierarxiyasi ortiqcha:

KOD
class Tugma {
    std::function<void()> bosilganda_;
};

3. Shablon - tanlov kompilyatsiya paytida ma'lum:

KOD
template <typename Saqlash>
class Kesh { Saqlash saqlash_; };

Uchinchisi eng tez - virtual chaqiruv umuman yo'q.

Vorislikni faqat ish paytida turli obyektlarni bir xil interfeys orqali boshqarish kerak bo'lganda ishlating.

std::function bilan #

C++
class Hisoblagich
{
    std::string nom_;
    int qiymat_{};
    std::function<void(int)> ozgarganda_;

public:
    explicit Hisoblagich(std::string nom) : nom_(std::move(nom)) {}

    void kuzat(std::function<void(int)> f) { ozgarganda_ = std::move(f); }

    void osir(int n = 1)
    {
        qiymat_ += n;
        if (ozgarganda_) ozgarganda_(qiymat_);
    }

    int qiymat() const { return qiymat_; }
    const std::string& nom() const { return nom_; }
};

int main()
{
    Hisoblagich h{"tashriflar"};

    std::println("kuzatuvchisiz:");
    h.osir();
    h.osir(5);
    std::println("  qiymat: {}", h.qiymat());

    std::println("");
    std::println("kuzatuvchi bilan:");
    int ogohlantirish = 0;
    h.kuzat([&ogohlantirish](int q) {
        std::println("  o'zgardi -> {}", q);
        if (q > 10) ++ogohlantirish;
    });

    h.osir(3);
    h.osir(4);
    h.osir();

    std::println("");
    std::println("10 dan oshgan holatlar: {}", ogohlantirish);
    std::println("Bu yerda birorta virtual funksiya yo'q.");

    return 0;
}
Natija
kuzatuvchisiz:
  qiymat: 6

kuzatuvchi bilan:
  o'zgardi -> 9
  o'zgardi -> 13
  o'zgardi -> 14

10 dan oshgan holatlar: 2
Bu yerda birorta virtual funksiya yo'q.

Virtual chaqiruvning narxi #

C++
struct Bazaviy
{
    virtual ~Bazaviy() = default;
    virtual int hisobla(int n) const { return n; }
};

struct Voris : Bazaviy
{
    int hisobla(int n) const override { return n * 2; }
};

struct Oddiy
{
    int hisobla(int n) const { return n * 2; }
};

int main()
{
    std::println("Bo'sh oddiy sinf:      {} bayt", sizeof(Oddiy));
    std::println("Bo'sh virtual sinf:    {} bayt", sizeof(Bazaviy));
    std::println("Ko'rsatkich hajmi:     {} bayt", sizeof(void*));
    std::println("");
    std::println("Virtual sinfda yashirin jadval ko'rsatkichi bor.");
    std::println("Bo'sh oddiy sinf 1 bayt - chunki C++ da har bir");
    std::println("obyektning manzili boshqacha bo'lishi kerak.");

    std::println("");
    std::println("Chaqiruv narxi:");
    std::println("  oddiy:   to'g'ridan-to'g'ri, inline bo'lishi mumkin");
    std::println("  virtual: jadvaldan o'qish + bilvosita chaqiruv");
    std::println("  inline BO'LMAYDI (odatda)");

    std::unique_ptr<Bazaviy> p = std::make_unique<Voris>();
    std::println("");
    std::println("virtual natija:  {}", p->hisobla(21));
    Oddiy o;
    std::println("oddiy natija:    {}", o.hisobla(21));

    return 0;
}
Natija
Bo'sh oddiy sinf:      1 bayt
Bo'sh virtual sinf:    8 bayt
Ko'rsatkich hajmi:     8 bayt

Virtual sinfda yashirin jadval ko'rsatkichi bor.
Bo'sh oddiy sinf 1 bayt - chunki C++ da har bir
obyektning manzili boshqacha bo'lishi kerak.

Chaqiruv narxi:
  oddiy:   to'g'ridan-to'g'ri, inline bo'lishi mumkin
  virtual: jadvaldan o'qish + bilvosita chaqiruv
  inline BO'LMAYDI (odatda)

virtual natija:  42
oddiy natija:    42
Narx katta emas, lekin bepul ham emas

Virtual chaqiruv qo'shadi:

XarajatMiqdor
Obyekt hajmiBitta ko'rsatkich (8 bayt)
ChaqiruvBitta bilvosita o'qish
OptimallashtirishInline qilib bo'lmaydi

Ko'p dasturda bu sezilmaydi. Lekin ichki tsiklda millionlab marta chaqirilsa - farq seziladi, chunki kompilyator inline qila olmaydi va uning atrofidagi optimallashtirishlar ham yo'qoladi.

Shuning uchun o'yin dvigatellari va ma'lumotlar bazalari issiq yo'llarda vorislik o'rniga shablon yoki switch ishlatadi.

Lekin: avval o'lchang. Virtual chaqiruvni optimallashtirish - odatda oxirgi qadam, birinchi emas.

Amaliy topshiriq
  1. Bazaviy sinf va ikki voris yozing, virtual funksiyani qayta aniqlang.
  2. virtual ~T() ni olib tashlang va vorisni bazaviy ko'rsatkich orqali o'chiring.
  3. override siz const ni tushirib qoldiring - qaysi funksiya chaqiriladi?
  4. Voris obyektini qiymat bo'yicha funksiyaga uzating - kesilishni ko'ring.
  5. std::vector<Bazaviy> va std::vector<unique_ptr<Bazaviy>> ni solishtiring.
  6. Sof virtual funksiya bilan abstrakt interfeys yozing.
  7. Abstrakt sinfdan obyekt yaratishga urinib ko'ring.
  8. std::vector dan voris qilingan sinf yozing - qanday amallar ochiladi?
  9. Xuddi shu sinfni kompozitsiya bilan qayta yozing.
  10. Virtual sinf va oddiy sinf hajmini sizeof bilan solishtiring.

Xulosa #

  • Vorislik uchun mo'ljallangan sinfda virtual ~T() = default shart.
  • Har bir qayta aniqlashga override yozing - kompilyator imzoni tekshiradi.
  • override siz mos kelmagan imzo yangi funksiya yaratadi, xato bermaydi.
  • Kesilish: qiymat bo'yicha uzatishda voris qismi jimgina yo'qoladi.
  • Polimorf obyektlarni havola, ko'rsatkich yoki unique_ptr orqali boshqaring.
  • Sof virtual funksiya (= 0) abstrakt interfeys yaratadi.
  • Vorislik - interfeys uchun, kod qayta ishlatish uchun emas.
  • Kod qayta ishlatish kerak bo'lsa - kompozitsiya (a'zo qiling).
  • Bitta xatti-harakat kerak bo'lsa - std::function ierarxiyadan yengil.
  • Tanlov kompilyatsiya paytida ma'lum bo'lsa - shablon eng tez.
  • Virtual chaqiruv narxi kichik, lekin nol emas - o'lchamasdan optimallashtirmang.

Keyingi bo'limda shablonlar va konseptlar ni 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.