Lietuvių

Susipažinkite su C++ išmaniosiomis rodyklėmis (unique_ptr, shared_ptr, weak_ptr) patikimam atminties valdymui, atminties nuotėkių prevencijai ir programos stabilumui. Geriausios praktikos ir pavyzdžiai.

Šiuolaikinės C++ galimybės: išmaniųjų rodyklių įvaldymas efektyviam atminties valdymui

Šiuolaikiniame C++ išmaniosios rodyklės yra nepakeičiami įrankiai saugiam ir efektyviam atminties valdymui. Jos automatizuoja atminties atlaisvinimo procesą, apsaugodamos nuo atminties nuotėkių ir kabančių rodyklių, kurios yra dažnos klaidos tradiciniame C++ programavime. Šis išsamus vadovas nagrinėja skirtingus C++ prieinamus išmaniųjų rodyklių tipus ir pateikia praktinių pavyzdžių, kaip juos efektyviai naudoti.

Išmaniųjų rodyklių poreikio supratimas

Prieš gilinantis į išmaniųjų rodyklių specifiką, labai svarbu suprasti problemas, kurias jos sprendžia. Klasikiniame C++ programuotojai yra atsakingi už rankinį atminties paskirstymą ir atlaisvinimą naudojant new ir delete. Šis rankinis valdymas yra linkęs į klaidas, vedančias prie:

Šios problemos gali sukelti programos strigimus, nenuspėjamą elgesį ir saugumo pažeidžiamumus. Išmaniosios rodyklės siūlo elegantišką sprendimą, automatiškai valdydamos dinamiškai paskirstytų objektų gyvavimo ciklą, laikantis Išteklių įgijimas yra inicializacija (RAII) principo.

RAII ir išmaniosios rodyklės: galingas derinys

Pagrindinė išmaniųjų rodyklių koncepcija yra RAII, kuri nurodo, kad ištekliai turėtų būti įgyjami objekto kūrimo metu ir atlaisvinami objekto naikinimo metu. Išmaniosios rodyklės yra klasės, kurios apgaubia neapdorotą rodyklę ir automatiškai ištrina objektą, į kurį rodoma, kai išmanioji rodyklė išeina iš apimties srities. Tai užtikrina, kad atmintis visada bus atlaisvinta, net ir esant išimtims.

Išmaniųjų rodyklių tipai C++

C++ siūlo tris pagrindinius išmaniųjų rodyklių tipus, kurių kiekvienas turi savo unikalias savybes ir naudojimo atvejus:

std::unique_ptr: Išskirtinė nuosavybė

std::unique_ptr reiškia išskirtinę dinamiškai paskirstyto objekto nuosavybę. Tik vienas unique_ptr gali rodyti į tam tikrą objektą bet kuriuo metu. Kai unique_ptr išeina iš apimties srities, jo valdomas objektas automatiškai ištrinamas. Dėl to unique_ptr idealiai tinka scenarijams, kai vienas subjektas turėtų būti atsakingas už objekto gyvavimo ciklą.

Pavyzdys: std::unique_ptr naudojimas


#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "MyClass sukurta su reikšme: " << value_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass sunaikinta su reikšme: " << value_ << std::endl;
    }

    int getValue() const { return value_; }

private:
    int value_;
};

int main() {
    std::unique_ptr<MyClass> ptr(new MyClass(10)); // Sukurti unique_ptr

    if (ptr) { // Patikrinti, ar rodyklė yra galiojanti
        std::cout << "Reikšmė: " << ptr->getValue() << std::endl;
    }

    // Kai ptr išeina iš apimties srities, MyClass objektas yra automatiškai ištrinamas
    return 0;
}

Pagrindinės std::unique_ptr savybės:

Pavyzdys: std::move naudojimas su std::unique_ptr


#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1(new int(42));
    std::unique_ptr<int> ptr2 = std::move(ptr1); // Perduoti nuosavybę ptr2

    if (ptr1) {
        std::cout << "ptr1 vis dar galiojantis" << std::endl; // Tai nebus įvykdyta
    } else {
        std::cout << "ptr1 dabar yra null" << std::endl; // Tai bus įvykdyta
    }

    if (ptr2) {
        std::cout << "Reikšmė, į kurią rodo ptr2: " << *ptr2 << std::endl; // Išvestis: Reikšmė, į kurią rodo ptr2: 42
    }

    return 0;
}

Pavyzdys: Pasirinktinių naikintojų naudojimas su std::unique_ptr


#include <iostream>
#include <memory>

// Pasirinktinis naikintojas failų deskriptoriams
struct FileDeleter {
    void operator()(FILE* file) const {
        if (file) {
            fclose(file);
            std::cout << "Failas uždarytas." << std::endl;
        }
    }
};

int main() {
    // Atidaryti failą
    FILE* file = fopen("example.txt", "w");
    if (!file) {
        std::cerr << "Klaida atidarant failą." << std::endl;
        return 1;
    }

    // Sukurti unique_ptr su pasirinktiniu naikintoju
    std::unique_ptr<FILE, FileDeleter> filePtr(file);

    // Įrašyti į failą (pasirinktinai)
    fprintf(filePtr.get(), "Sveikas, pasauli!\n");

    // Kai filePtr išeis iš apimties srities, failas bus automatiškai uždarytas
    return 0;
}

std::shared_ptr: Dalinama nuosavybė

std::shared_ptr leidžia bendrai valdyti dinamiškai paskirstytą objektą. Keli shared_ptr egzemplioriai gali rodyti į tą patį objektą, o objektas ištrinamas tik tada, kai paskutinis į jį rodantis shared_ptr išeina iš apimties srities. Tai pasiekiama naudojant nuorodų skaičiavimą, kai kiekvienas shared_ptr padidina skaitiklį, kai jis sukuriamas ar kopijuojamas, ir sumažina skaitiklį, kai jis sunaikinamas.

Pavyzdys: std::shared_ptr naudojimas


#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1(new int(100));
    std::cout << "Nuorodų skaičius: " << ptr1.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 1

    std::shared_ptr<int> ptr2 = ptr1; // Kopijuoti shared_ptr
    std::cout << "Nuorodų skaičius: " << ptr1.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 2
    std::cout << "Nuorodų skaičius: " << ptr2.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 2

    {
        std::shared_ptr<int> ptr3 = ptr1; // Kopijuoti shared_ptr apimties srityje
        std::cout << "Nuorodų skaičius: " << ptr1.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 3
    } // ptr3 išeina iš apimties srities, nuorodų skaičius sumažėja

    std::cout << "Nuorodų skaičius: " << ptr1.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 2

    ptr1.reset(); // Atlaisvinti nuosavybę
    std::cout << "Nuorodų skaičius: " << ptr2.use_count() << std::endl; // Išvestis: Nuorodų skaičius: 1

    ptr2.reset(); // Atlaisvinti nuosavybę, objektas dabar yra ištrintas

    return 0;
}

Pagrindinės std::shared_ptr savybės:

Svarbūs aspektai naudojant std::shared_ptr:

std::weak_ptr: Nuosavybės neturintis stebėtojas

std::weak_ptr suteikia nuosavybės neturinčią nuorodą į objektą, kurį valdo shared_ptr. Jis nedalyvauja nuorodų skaičiavimo mechanizme, o tai reiškia, kad jis neužkerta kelio objekto ištrynimui, kai visi shared_ptr egzemplioriai išeina iš apimties srities. weak_ptr yra naudingas stebint objektą neprisiimant nuosavybės, ypač norint nutraukti ciklinius priklausomybes.

Pavyzdys: std::weak_ptr naudojimas ciklinių priklausomybių nutraukimui


#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b;
    ~A() { std::cout << "A sunaikintas" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a; // Naudojant weak_ptr, kad išvengti ciklinės priklausomybės
    ~B() { std::cout << "B sunaikintas" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b = b;
    b->a = a;

    // Be weak_ptr, A ir B niekada nebūtų sunaikinti dėl ciklinės priklausomybės
    return 0;
} // A ir B yra sunaikinami teisingai

Pavyzdys: std::weak_ptr naudojimas objekto galiojimo patikrinimui


#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(123);
    std::weak_ptr<int> weakPtr = sharedPtr;

    // Patikrinti, ar objektas vis dar egzistuoja
    if (auto observedPtr = weakPtr.lock()) { // lock() grąžina shared_ptr, jei objektas egzistuoja
        std::cout << "Objektas egzistuoja: " << *observedPtr << std::endl; // Išvestis: Objektas egzistuoja: 123
    }

    sharedPtr.reset(); // Atlaisvinti nuosavybę

    // Patikrinti dar kartą, kai sharedPtr buvo nustatytas iš naujo
    if (auto observedPtr = weakPtr.lock()) {
        std::cout << "Objektas egzistuoja: " << *observedPtr << std::endl; // Tai nebus įvykdyta
    } else {
        std::cout << "Objektas buvo sunaikintas." << std::endl; // Išvestis: Objektas buvo sunaikintas.
    }

    return 0;
}

Pagrindinės std::weak_ptr savybės:

Tinkamos išmaniosios rodyklės pasirinkimas

Tinkamos išmaniosios rodyklės pasirinkimas priklauso nuo nuosavybės semantikos, kurią norite įgyvendinti:

Geriausios praktikos naudojant išmaniąsias rodykles

Norėdami maksimaliai išnaudoti išmaniųjų rodyklių teikiamą naudą ir išvengti dažnų klaidų, laikykitės šių geriausių praktikų:

Pavyzdys: std::make_unique ir std::make_shared naudojimas


#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "MyClass sukurta su reikšme: " << value_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass sunaikinta su reikšme: " << value_ << std::endl;
    }

    int getValue() const { return value_; }

private:
    int value_;
};

int main() {
    // Naudoti std::make_unique
    std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>(50);
    std::cout << "Unique pointer reikšmė: " << uniquePtr->getValue() << std::endl;

    // Naudoti std::make_shared
    std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>(100);
    std::cout << "Shared pointer reikšmė: " << sharedPtr->getValue() << std::endl;

    return 0;
}

Išmaniosios rodyklės ir saugumas išimčių atveju

Išmaniosios rodyklės ženkliai prisideda prie saugumo išimčių atveju. Automatiškai valdydamos dinamiškai paskirstytų objektų gyvavimo ciklą, jos užtikrina, kad atmintis bus atlaisvinta net ir išmetus išimtį. Tai apsaugo nuo atminties nuotėkių ir padeda palaikyti jūsų programos vientisumą.

Apsvarstykite šį pavyzdį, kaip galima nutekinti atmintį naudojant neapdorotas rodykles:


#include <iostream>

void processData() {
    int* data = new int[100]; // Paskirstyti atmintį

    // Atlikti kai kurias operacijas, kurios gali išmesti išimtį
    try {
        // ... kodas, galintis išmesti išimtį ...
        throw std::runtime_error("Kažkas nutiko ne taip!"); // Išimties pavyzdys
    } catch (...) {
        delete[] data; // Atlaisvinti atmintį catch bloke
        throw; // Persviesti išimtį
    }

    delete[] data; // Atlaisvinti atmintį (pasiekiama tik jei išimtis neišmesta)
}

Jei išimtis išmetama try bloke *prieš* pirmąjį delete[] data; sakinį, data skirta atmintis nutekės. Naudojant išmaniąsias rodykles, to galima išvengti:


#include <iostream>
#include <memory>

void processData() {
    std::unique_ptr<int[]> data(new int[100]); // Paskirstyti atmintį naudojant išmaniąją rodyklę

    // Atlikti kai kurias operacijas, kurios gali išmesti išimtį
    try {
        // ... kodas, galintis išmesti išimtį ...
        throw std::runtime_error("Kažkas nutiko ne taip!"); // Išimties pavyzdys
    } catch (...) {
        throw; // Persviesti išimtį
    }

    // Nereikia aiškiai naikinti duomenų; unique_ptr tai padarys automatiškai
}

Šiame patobulintame pavyzdyje unique_ptr automatiškai valdo data skirtą atmintį. Jei išmetama išimtis, unique_ptr destruktorius bus iškviestas, kai valomas dėklas (stack unwinding), užtikrinant, kad atmintis būtų atlaisvinta, nepriklausomai nuo to, ar išimtis yra pagaunama, ar persviečiama.

Išvada

Išmaniosios rodyklės yra pagrindiniai įrankiai rašant saugų, efektyvų ir prižiūrimą C++ kodą. Automatizuodamos atminties valdymą ir laikydamosi RAII principo, jos pašalina dažnas klaidas, susijusias su neapdorotomis rodyklėmis, ir prisideda prie patikimesnių programų kūrimo. Suprasti skirtingus išmaniųjų rodyklių tipus ir jų tinkamus naudojimo atvejus yra būtina kiekvienam C++ programuotojui. Prisitaikydami prie išmaniųjų rodyklių ir laikydamiesi geriausių praktikų, galite žymiai sumažinti atminties nuotėkius, kabančias rodykles ir kitas su atmintimi susijusias klaidas, kas lemia patikimesnę ir saugesnę programinę įrangą.

Nuo startuolių Silicio slėnyje, kurie naudoja šiuolaikinį C++ didelio našumo skaičiavimams, iki pasaulinių įmonių, kuriančių gyvybiškai svarbias sistemas, išmaniosios rodyklės yra visuotinai taikomos. Nesvarbu, ar kuriate įterptąsias sistemas daiktų internetui, ar plėtojate pažangias finansines programas, išmaniųjų rodyklių įvaldymas yra pagrindinis įgūdis kiekvienam C++ programuotojui, siekiančiam meistriškumo.

Papildoma literatūra