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:
- Atminties nuotėkiai: Nepavykus atlaisvinti atminties, kai ji nebėra reikalinga.
- Kabančios rodyklės: Rodyklės, kurios rodo į jau atlaisvintą atmintį.
- Dvigubas atlaisvinimas: Bandymas atlaisvinti tą patį atminties bloką du kartus.
Š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
std::shared_ptr
std::weak_ptr
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:
- Kopijavimo draudimas:
unique_ptr
negali būti kopijuojamas, taip užkertant kelią kelioms rodyklėms turėti tą patį objektą. Tai užtikrina išskirtinę nuosavybę. - Perkėlimo semantika:
unique_ptr
gali būti perkeltas naudojantstd::move
, perduodant nuosavybę iš vienounique_ptr
kitam. - Pasirinktiniai naikintojai (deleters): Galite nurodyti pasirinktinę naikintojo funkciją, kuri bus iškviesta, kai
unique_ptr
išeis iš apimties srities, leidžiant valdyti ne tik dinamiškai paskirstytą atmintį, bet ir kitus išteklius (pvz., failų deskriptorius, tinklo lizdus).
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:
- Dalinama nuosavybė: Keli
shared_ptr
egzemplioriai gali rodyti į tą patį objektą. - Nuorodų skaičiavimas: Valdo objekto gyvavimo ciklą stebėdamas į jį rodančių
shared_ptr
egzempliorių skaičių. - Automatinis ištrynimas: Objektas automatiškai ištrinamas, kai paskutinis
shared_ptr
išeina iš apimties srities. - Saugumas gijose (Thread Safety): Nuorodų skaitiklio atnaujinimai yra saugūs gijose, leidžiantys naudoti
shared_ptr
daugiagijėse aplinkose. Tačiau prieiga prie paties rodomo objekto nėra saugi gijose ir reikalauja išorinės sinchronizacijos. - Pasirinktiniai naikintojai: Palaiko pasirinktinius naikintojus, panašiai kaip
unique_ptr
.
Svarbūs aspektai naudojant std::shared_ptr
:
- Ciklinės priklausomybės: Būkite atsargūs dėl ciklinių priklausomybių, kai du ar daugiau objektų rodo vienas į kitą naudodami
shared_ptr
. Tai gali sukelti atminties nuotėkius, nes nuorodų skaitiklis niekada nepasieks nulio.std::weak_ptr
gali būti naudojamas šiems ciklams nutraukti. - Našumo pridėtinės išlaidos: Nuorodų skaičiavimas sukelia tam tikras našumo pridėtines išlaidas, palyginti su neapdorotomis rodyklėmis arba
unique_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:
- Neturi nuosavybės: Nedalyvauja nuorodų skaičiavime.
- Stebėtojas: Leidžia stebėti objektą neprisiimant nuosavybės.
- Ciklinių priklausomybių nutraukimas: Naudinga nutraukiant ciklinius priklausomybes tarp objektų, valdomų
shared_ptr
. - Objekto galiojimo tikrinimas: Galima naudoti patikrinti, ar objektas vis dar egzistuoja, naudojant
lock()
metodą, kuris grąžinashared_ptr
, jei objektas gyvas, arba nulinįshared_ptr
, jei jis buvo sunaikintas.
Tinkamos išmaniosios rodyklės pasirinkimas
Tinkamos išmaniosios rodyklės pasirinkimas priklauso nuo nuosavybės semantikos, kurią norite įgyvendinti:
unique_ptr
: Naudokite, kai norite išskirtinės objekto nuosavybės. Tai yra efektyviausia išmanioji rodyklė ir jai turėtų būti teikiama pirmenybė, kai įmanoma.shared_ptr
: Naudokite, kai keli subjektai turi dalintis objekto nuosavybe. Atsižvelkite į galimas ciklinių priklausomybių ir našumo pridėtines išlaidas.weak_ptr
: Naudokite, kai reikia stebėti objektą, valdomąshared_ptr
, neprisiimant nuosavybės, ypač norint nutraukti ciklinius priklausomybes arba patikrinti objekto galiojimą.
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ų:
- Teikite pirmenybę
std::make_unique
irstd::make_shared
: Šios funkcijos užtikrina saugumą išimčių atveju ir gali pagerinti našumą, paskirstydamos valdymo bloką ir objektą vienu atminties paskirstymu. - Venkite neapdorotų rodyklių: Sumažinkite neapdorotų rodyklių naudojimą savo kode. Naudokite išmaniąsias rodykles dinamiškai paskirstytų objektų gyvavimo ciklo valdymui, kai tik įmanoma.
- Iš karto inicializuokite išmaniąsias rodykles: Inicializuokite išmaniąsias rodykles iškart po jų deklaravimo, kad išvengtumėte neinicijuotų rodyklių problemų.
- Atsižvelkite į ciklinius priklausomybes: Naudokite
weak_ptr
, kad nutrauktumėte ciklinius priklausomybes tarp objektų, valdomųshared_ptr
. - Venkite perduoti neapdorotas rodykles funkcijoms, kurios perima nuosavybę: Perduokite išmaniąsias rodykles pagal vertę arba pagal nuorodą, kad išvengtumėte atsitiktinių nuosavybės perdavimų ar dvigubo ištrynimo problemų.
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
- cppreference.com: https://en.cppreference.com/w/cpp/memory
- Effective Modern C++ by Scott Meyers
- C++ Primer by Stanley B. Lippman, Josée Lajoie, and Barbara E. Moo