Víme vše o operátorech new a delete? Použití nového odstranění k implementaci polí

Jak víte, v jazyce C se funkce malloc() a free() používají k dynamickému přidělování a uvolňování paměti. C++ však obsahuje dva operátory, které provádějí alokaci paměti a dealokaci efektivněji a jednodušeji. Tyto operátory jsou nové a smazané. Jejich obecná podoba je:

ukazatel proměnná = nový typ proměnné;

odstranit proměnnou ukazatele;

Zde je ukazatel-proměnná ukazatel typu proměnná-typ. Operátor new alokuje paměť pro uložení hodnoty typu variable_type a vrátí její adresu. Jakýkoli typ dat lze uložit s novými. Operátor delete uvolní paměť, na kterou ukazuje pointer_variable.

Pokud nelze operaci alokace paměti provést, operátor new vyvolá výjimku typu xalloc. Pokud program tuto výjimku nezachytí, jeho provádění bude ukončeno. Zatímco toto výchozí chování je uspokojivé pro krátké programy, skutečné aplikační programy obvykle potřebují zachytit výjimku a náležitě ji zpracovat. Chcete-li zachytit tuto výjimku, musíte zahrnout hlavičkový soubor exception.h.

Operátor delete by se měl používat pouze pro ukazatele na paměť přidělenou pomocí operátoru new. Použití operátoru delete s jinými typy adres může způsobit vážné problémy.

Použití new má oproti malloc() řadu výhod. Nejprve nový operátor automaticky vypočítá velikost požadované paměti. Není potřeba používat operátor sizeof(). Ještě důležitější je, že zabráníte náhodnému přidělení nesprávného množství paměti. Za druhé, operátor new automaticky vrátí ukazatel na požadovaný typ, takže není potřeba používat operátor konverze typu. Za třetí, jak bude krátce popsáno, je možné inicializovat objekt pomocí operátoru new. Nakonec je možné přetížit operátor new a operátor delete globálně nebo ve vztahu k třídě, která se vytváří.

Níže je uveden jednoduchý příklad použití operátorů new a delete. Všimněte si použití bloku try/catch ke sledování chyb alokace paměti.

#zahrnout
#zahrnout
int main()
{
int *p;
zkus (
p = new int; // přidělení paměti pro int
) catch (xalloc xa) (
cout<< "Allocation failure.\n";
návrat 1;
}
*p = 20; // přiřazení tohoto paměťového místa hodnotě 20
cout<< *р; // демонстрация работы путем вывода значения
odstranit p; // uvolnění paměti
návrat 0;
}

Tento program přiřadí proměnné p adresu bloku paměti dostatečně velké, aby pojal celé číslo. Dále je této paměti přiřazena hodnota a obsah paměti se zobrazí na obrazovce. Nakonec se dynamicky alokovaná paměť uvolní.

Jak bylo uvedeno, paměť můžete inicializovat pomocí operátoru new. Chcete-li to provést, musíte zadat inicializační hodnotu v závorkách za názvem typu. Například v následujícím příkladu je paměť, na kterou ukazuje p, inicializována na 99:

#zahrnout
#zahrnout
int main()
{
int *p;
zkus (
p = new int(99); // inicializace 99
) catch (xalloc xa) (
cout<< "Allocation failure.\n";
návrat 1;
}
cout<< *p;
odstranit p;
návrat 0;
}

Pro alokaci polí můžete použít new. Obecný tvar pro jednorozměrné pole je:

variabilní_ukazatel = nový typ_proměnné [velikost];

Velikost zde určuje počet prvků v poli. Při umísťování pole je třeba pamatovat na důležité omezení: nelze jej inicializovat.

Chcete-li uvolnit dynamicky alokované pole, musíte použít následující formulář operátoru delete:

odstranit proměnnou ukazatele;

Zde závorky informují operátora delete, aby uvolnil paměť přidělenou pro pole.

Následující program alokuje paměť pro pole 10 prvků float. Prvky pole jsou přiřazeny hodnoty od 100 do 109 a poté se obsah pole vytiskne na obrazovku:

#zahrnout
#zahrnout
int main()
{
plovák *p;
int i;
zkus (
p = nový plovák; // získání desátého prvku pole
) catch(xalloc xa) (
cout<< "Allocation failure.\n";
návrat 1;
}
// přiřazení hodnot od 100 do 109
pro (i=0; i<10; i + +) p[i] = 100.00 + i;
// vypíše obsah pole
pro (i=0; i<10; i++) cout << p[i] << " ";
odstranit p; // smazání celého pole
návrat 0;
}

Pole a ukazatele spolu ve skutečnosti úzce souvisí. Název pole je konstantní ukazatel, jehož hodnota je adresa prvního prvku pole (&arr). Název pole tedy může být inicializátor ukazatele, na který se budou vztahovat všechna pravidla aritmetiky adres související s ukazateli. Příklad programu:
Program 11.1

#zahrnout pomocí jmenného prostoru std; int main() ( const int k = 10; int arr[k]; int *p = arr; // ukazatel ukazuje na první prvek pole for (int i = 0; i< 10; i++){ *p = i; p++; // указатель указывает на следующий элемент } p = arr; // возвращаем указатель на первый элемент for (int i = 0; i < 10; i++){ cout << *p++ << " "; } cout << endl; // аналогично: for (int i = 0; i < 10; i++){ cout << *(arr + i) << " "; } cout << endl; p = arr; // выводим адреса элементов: for (int i = 0; i < 10; i++){ cout << "arr[" << i << "] => " << p++ << endl; } return 0; }

Výstup programu:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 arr => 0xbffc8f00 arr => 0xbffc8f04 arr => 0xbffc8f08 arr => 0xbrr0x8f0c arr = 0xbrr0x8f0c arr 4 arr => 0xbffc8f18 arr = > 0xbffc8f1c arr => 0xbffc8f20 arr => 0xbffc8f24

Výraz arr[i] – přístup k prvku indexem odpovídá výrazu *(arr + i) , který se nazývá ukazatel-offset(řádek 22). Tento výraz jasněji ilustruje, jak C++ skutečně pracuje s prvky pole. Proměnná čítače i udává, kolik prvků je třeba odsadit od prvního prvku. Řádek 17 vypíše hodnotu prvku pole po dereferencování ukazatele.

Co znamená výraz *p++? Operátor * má nižší prioritu, zatímco přírůstek postfixu je asociativní zleva doprava. Proto se v tomto složitém výrazu nejprve provede nepřímé adresování (přístup k hodnotě prvku pole) a poté se ukazatel zvýší. Jinak by tento výraz mohl být reprezentován jako: cout Poznámka. Operátor sizeof() použitý na název pole vrátí velikost celého pole (ne prvního prvku).
Poznámka. Operátor adresy (&) se používá pro prvky pole stejným způsobem jako pro běžné proměnné (prvky pole se někdy nazývají indexované proměnné). Například &arr . Proto můžete vždy získat ukazatel na jakýkoli prvek pole. Operace &arr (kde arr je název pole) však vrátí adresu celého pole a například operace (&arr + 1) bude znamenat krok velikosti pole, tedy získání ukazatele na pole. prvek vedle posledního.

Výhody použití ukazatelů při práci s prvky pole

Podívejme se na dva příklady programů, které vedou ke stejnému výsledku: nové hodnoty od 0 do 1999999 jsou přiřazeny prvkům pole a jsou výstupem.
Program 11.2

#zahrnout pomocí jmenného prostoru std; int main() ( const int n = 2000000; int mass[n] (); for (int i = 0; i< n; i++) { mass[i] = i; cout << mass[i]; } return 0; }

Program 11.3

#zahrnout pomocí jmenného prostoru std; int main() ( konst int n = 2000000; int hmotnost[n] (); int *p = hmotnost; for (int i = 0; i< n; i++) { *p = i; cout << *p++; } return 0; }

Program 11.3 poběží rychleji než program 11.2 (s rostoucím počtem prvků bude program 11.3 efektivnější)! Důvodem je, že Program 11.2 pokaždé přepočítá umístění (adresu) aktuálního prvku pole vzhledem k prvnímu (11.2, řádky 12 a 13). V programu 11.3 je adresa prvního prvku přístupná jednou při inicializaci ukazatele (11.3, řádek 11).

Pole mimo hranice

Všimněme si dalšího důležitého aspektu práce s C-polemi v C++. Není k dispozici v C++ sledování souladu s hranicemi C-pole. Že. Odpovědnost za pozorování způsobu zpracování prvků v rámci pole leží zcela na vývojáři algoritmu. Podívejme se na příklad.
Program 11.4

#zahrnout #zahrnout #zahrnout pomocí jmenného prostoru std; int main() ( int mas; default_random_engine rnd(time(0)); uniform_int_distribution < 10; i++) mas[i] = d(rnd); cout << "Элементы массива:" << endl; for (int i = 0; i < 10; i++) cout << mas[i] << endl; return 0; }

Program vypíše něco takového:

Prvky pole: 21 58 38 91 23 5 38 -1219324996 -1074960992 0

V programu 11.4 došlo k záměrné chybě. Kompilátor ale neoznámí chybu: pole má deklarováno pět prvků, ale cykly předpokládají, že prvků je 10! Výsledkem je, že pouze pět prvků bude správně inicializováno (je možné další poškození dat) a budou zobrazeny spolu s „odpadem“. C++ poskytuje možnost řídit hranice pomocí funkcí knihovny begin() a end() (musíte zahrnout hlavičkový soubor iterátoru). Úprava programu 11.4
Program 11.5

#zahrnout #zahrnout #zahrnout #zahrnout pomocí jmenného prostoru std; int main() ( int mas; int *first = begin(mas); int *last = end(mas); default_random_engine rnd(time(0)); uniform_int_distribution d(10,99);<< "Элементы массива:" << endl; while(first != last) { cout << *first++ << " "; } return 0; }

while(first != last) ( *first = d(rnd); first++; ) first = begin(mas);
cout

Funkce begin() a end() vrátí . Konceptu iterátorů se budeme věnovat později, ale prozatím řekneme, že se chovají jako ukazatele, které ukazují na první prvek (první) a prvek následující za posledním (poslední). V programu 11.5 jsme z důvodu kompaktnosti a pohodlí nahradili smyčku for smyčkou while (protože zde již nepotřebujeme čítač - používáme aritmetiku ukazatele). Máme-li dva ukazatele, můžeme snadno formulovat podmínku pro opuštění smyčky, protože v každém kroku smyčky se první ukazatel inkrementuje.

Dalším způsobem, jak učinit procházení prvků pole bezpečnější, je použití cyklu for založeného na rozsahu, který jsme zmínili v tématu ()
Operace nové a smazat

Než jste se seznámili s ukazateli, znali jste jediný způsob, jak zapsat proměnlivá data do paměti prostřednictvím proměnných. Proměnná je pojmenovaná oblast paměti. Paměťové bloky pro odpovídající proměnné jsou alokovány při spuštění programu a jsou používány až do jeho ukončení. Pomocí ukazatelů můžete vytvářet nepojmenované paměťové bloky určitého typu a velikosti (a také je uvolnit) za běhu samotného programu. To odhaluje pozoruhodnou vlastnost ukazatelů, která se nejvíce projevuje v objektově orientovaném programování při vytváření tříd.

Dynamická alokace paměti se provádí pomocí nové operace. Syntax:

datový_typ *název_ukazatele = nový datový_typ;

Pravá strana výrazu říká, že new požaduje blok paměti pro uložení dat typu int. Pokud je paměť nalezena, je vrácena adresa, která je přiřazena proměnné ukazatele typu int. Nyní můžete přistupovat pouze k dynamicky vytvořené paměti pomocí ukazatelů! Příklad práce s dynamickou pamětí je uveden v programu 3.
Program 11.6

#zahrnout pomocí jmenného prostoru std; int main() ( int *a = new int(5); int *b = new int(4); int *c = new int; *c = *a + *b; cout<< *c << endl; delete a; delete b; delete c; return 0; }

Po práci s alokovanou pamětí je nutné ji uvolnit (vrátit, zpřístupnit pro další data) pomocí operace delete. Řízení spotřeby paměti je důležitým aspektem vývoje aplikací. Chyby, kdy paměť není uvolněna, vedou k „ úniky paměti", což zase může způsobit zhroucení programu. Operaci odstranění lze použít na ukazatel null (nullptr) nebo na ukazatel vytvořený pomocí nového (tj. new a delete se používají ve dvojicích).

Dynamická pole

Dynamické pole je pole, jehož velikost je určena během provádění programu. Přísně vzato, pole C není v C++ dynamické. To znamená, že můžete určit pouze velikost pole a změna velikosti pole za běhu programu je stále nemožná. Chcete-li získat pole požadované velikosti, musíte alokovat paměť pro nové pole a zkopírovat do něj data z původního pole a poté uvolnit paměť dříve přidělenou pro původní pole. Skutečně dynamické pole v C++ je typ, na který se podíváme později. K alokaci paměti pro pole se používá nová operace. Syntaxe pro alokaci paměti pro pole je:
ukazatel = nový typ[velikost] . Například:

Int n = 10; int *arr = new int[n];

Paměť se uvolní pomocí operátoru delete:

Smazat arr;

V tomto případě není velikost pole specifikována.
Příklad programu. Vyplňte dynamické celé pole arr1 náhodnými čísly. Zobrazit zdrojové pole. Přepište všechny prvky s lichými pořadovými čísly (1, 3, ...) do nového dynamického celočíselného pole arr2. Vytiskněte obsah pole arr2 .
Program 11.7

#zahrnout #zahrnout #zahrnout pomocí jmenného prostoru std; int main() ( int n; cout<< "n = "; cin >>n; int *arr1 = new int[n];< n; i++) { arr1[i] = d(rnd); cout << arr1[i] << " "; } cout << endl; int *arr2 = new int; for (int i = 0; i < n / 2; i++) { arr2[i] = arr1; cout << arr2[i] << " "; } delete arr1; delete arr2; return 0; } n = 10 73 94 17 52 11 76 22 70 57 68 94 52 76 70 68

Víme, že v C++ je dvourozměrné pole polem polí. Pro vytvoření dvourozměrného dynamického pole je proto nutné alokovat paměť ve smyčce pro každé příchozí pole, přičemž bylo předem určeno počet polí, která mají být vytvořena. K tomuto účelu se používá ukazatel na ukazatel, jinými slovy, popis pole ukazatelů:

Int **arr = new int *[m];

kde m je počet takových polí (řádků dvourozměrného pole).
Příklad úkolu. Vyplňte náhodnými čísly a vytvořte výstup prvků dvourozměrného dynamického pole.
Program 11.8

#zahrnout #zahrnout #zahrnout #zahrnout pomocí jmenného prostoru std; int main() ( int n, m; default_random_engine rnd(time(0)); uniform_int_distribution d(10,99);<< "Введите количество строк:" << endl; cout << "m = "; cin >cout<< "введите количество столбцов:" << endl; cout << "n = "; cin >> m;< m; i++) { arr[i] = new int[n]; for (int j = 0; j < n; j++) { arr[i][j] = d(rnd); } } // вывод массива: for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { cout << arr[i][j] << setw(3); } cout << "\n"; } // освобождение памяти выделенной для каждой // строки: for (int i = 0; i < m; i++) delete arr[i]; // освобождение памяти выделенной под массив: delete arr; return 0; } Введите количество строк: m = 5 введите количество столбцов: n = 10 66 99 17 47 90 70 74 37 97 39 28 67 60 15 76 64 42 65 87 75 17 38 40 81 66 36 15 67 82 48 73 10 47 42 47 90 64 22 79 61 13 98 28 25 13 94 41 98 21 28

cout
  1. >n;
  2. int **arr = new int *[m];
  3. // vyplnění pole: for (int i = 0; i
  4. Otázky
  5. Jaký je vztah mezi ukazateli a poli?
  6. Proč je použití ukazatelů při iteraci přes prvky pole efektivnější než použití operace indexu?
Co je podstatou pojmu „únik paměti“?
Vyjmenujte způsoby, jak zabránit tomu, aby hranice pole překročily hranice?

Co je dynamické pole? Proč není pole C v C++ ze své podstaty dynamické?

Popište proces vytváření dynamického dvourozměrného pole

Prezentace na lekci

Domácí úkol
  1. Pomocí dynamických polí vyřešte následující problém: Je dáno celočíselné pole A o velikosti N. Přepište všechna sudá čísla z původního pole (ve stejném pořadí) do nového celočíselného pole B a vytiskněte velikost výsledného pole B a jeho obsah.
  2. Učebnice
  3. §62 (10) §40 (11)
  4. Literatura
  5. Lafore R. Objektově orientované programování v C++ (4. vydání). Petr: 2004

Prata, Stephene. Programovací jazyk C++. Přednášky a cvičení, 6. vyd.: Trans. z angličtiny - M.: LLC „I.D. William", 2012

Lippman B. Stanley, Josie Lajoie, Barbara E. Mu. Programovací jazyk C++. Základní kurz. Ed. 5. M: LLC "I.D. Williams", 2014
struct A ( std::string str; // Automatický objekt, je implicitně odstraněn v destruktoru A (který je generován // automaticky). Samotný řetězec buffer je dynamický objekt (*), bude explicitně // smazán v destruktor std::string, který bude implicitně volán v destruktoru A // (*) Pokud není řetězec příliš krátký, bude fungovat Small String Optimization a dynamický // buffer nebude vůbec přidělen ); void foo() ( std::vektor proti; // Automatický objekt, implicitně smazán při ukončení funkce. v.push_back(10); // Obsah vektoru - dynamický objekt (pole) - bude explicitně smazán v // destruktoru vektoru, který bude implicitně volán při ukončení funkce.
A a; // Automatický objekt třídy A, implicitně smazán při ukončení funkce.

A* pa = nové A; // Ukazatel pa je automatický objekt, který je implicitně odstraněn při ukončení funkce, // ale ukazuje na dynamický objekt třídy A, který musí být smazán explicitně.

odstranit pa; // Explicitně odstraní dynamický objekt.

auto upa = // Inteligentní ukazatel upa je automatický objekt, implicitně odstraněný při ukončení funkce, std::make_unique

(); // ale ukazuje na dynamický objekt třídy A, který bude explicitně odstraněn // v destruktoru inteligentních ukazatelů. )

Dynamické objekty jsou obvykle na hromadě, i když obecně tomu tak není. Automatické objekty mohou být umístěny buď na zásobníku nebo na hromadě. Ve výše uvedeném příkladu je automatický objekt upa->str na hromadě, protože je součástí dynamického objektu *upa. Tito. dynamické/automatické vlastnosti určují životnost, ale ne místo života objektu.

Od jejich vynálezu byly operátory new a delete nadměrně používány. Největší problémy se týkají operátoru delete:
  • Můžete úplně zapomenout na volání delete (únik paměti).
  • Můžete zapomenout na volání delete v případě výjimky nebo předčasného návratu z funkce (také únik paměti).
  • Smazat můžete volat dvakrát (dvojité smazání).
  • Je možné volat nesprávný tvar operátoru: delete místo delete nebo naopak (nedefinované chování).
  • Objekt můžete použít po zavolání delete (visící ukazatel).
Všechny tyto situace vedou v lepším případě k pádům programu a v horším případě k únikům paměti a nosním démonům.

Proto lidé již dlouho přišli na to, že skryjí operátor odstranění v hlubinách kontejnerů a chytrých ukazatelů, čímž jej odstraní z kódu klienta. S novým operátorem jsou však také spojeny problémy, ale řešení pro ně se neobjevily okamžitě a ve skutečnosti se mnoho vývojářů stále stydí tato řešení používat. Více si o tom povíme, až se dostaneme k funkcím.

Nyní přejdeme k případům použití pro nové a smazané . Dovolte mi připomenout, že se podíváme na několik scénářů a systematicky ukážeme, že ve většině z nich bude kód lepší, když opustíme používání new a delete .

Začněme něčím jednoduchým – dynamickými poli.

Dynamická pole

Dynamické pole je pole s prvky alokovanými v dynamické paměti. Je potřeba, pokud je velikost neznámá v době kompilace, nebo pokud je velikost dostatečně velká, takže nechceme alokovat pole na zásobníku, který má obvykle velmi omezenou velikost.

Pro alokaci dynamických polí poskytuje C++ nízkoúrovňovou vektorovou formu operátorů new a delete: new a delete . Jako příklad uvažujme nějakou funkci, která pracuje s externí vyrovnávací pamětí:
void DoWork(int* buffer, size_t bufSize);
Podobné funkce se často nacházejí v knihovnách s API v čistém C. Níže je příklad toho, jak by mohl vypadat kód, který jej používá. Toto je špatný kód, protože... explicitně používá delete a problémy s tím spojené jsme již popsali výše.
void Call(size_t n) ( int* p = new int[n]; DoWork(p, n); delete p; // Bad! )
Všechno je zde jednoduché a většina lidí ví, že pro takové účely v C++ byste měli použít standardní kontejner std::vector. Přidělí paměť v konstruktoru a uvolní ji v destruktoru. Navíc může ještě během života měnit svou velikost, ale to už je pro nás teď jedno. Pomocí vektoru by kód vypadal takto:
void Volání (velikost_t n) ( std::vektor v(n); // Lepší. DoWork(v.data(), v.size()); )
Vyřešíme tak všechny problémy spojené s voláním delete a kromě toho máme místo anonymního páru ukazatel + číslo explicitní kontejner s pohodlným rozhraním.

Zároveň žádné nové a smazat . Nebudu se o tomto scénáři více rozepisovat. Podle mých zkušeností už většina vývojářů ví, co v tomto případě dělat a proč.

* V C++ by takové rozhraní mělo být implementováno pomocí typu span . Poskytuje jednotné rozhraní kompatibilní s STL pro přístup k souvislým sekvencím prvků, aniž by to jakkoli ovlivnilo jejich životnost (nevlastnící sémantika).

** Vzhledem k tomu, že programátoři C++ čtou tento článek, jsem si jistý, že si někdo pomyslí: „Ha! std::vector ukládá až tři (!) ukazatele, když starý dobrý int* je podle definice pouze jeden ukazatel. Dochází k nadměrnému využití paměti a několika strojových instrukcí pro jejich inicializaci! To je nepřijatelné! Myers se k této vlastnosti programátorů C++ ve své zprávě výtečně vyjádřil Proč C++ pluje, když se Vasa potopí. Pokud je to pro vás opravdu problém, mohu doporučit std::unique_ptr a v budoucnu nám standard může poskytnout dynarray.

Dynamické objekty

Dynamické objekty se obvykle používají, když není možné svázat životnost objektu s určitým rozsahem. Pokud je to možné, měli byste pravděpodobně použít automatickou paměť (viz, proč byste neměli zneužívat dynamické objekty). Ale to je téma na samostatný článek.

Když je vytvořen dynamický objekt, musí ho někdo smazat a typy objektů lze rozdělit do dvou skupin: ty, které si proces jejich mazání nijak neuvědomují, a ty, které něco tuší. Řekneme, že první mají standardní model správy paměti a druhé mají nestandardní.

Typy se standardním modelem správy paměti zahrnují všechny standardní typy včetně kontejnerů. Ve skutečnosti kontejner spravuje paměť, kterou alokoval sám. Je mu jedno, kdo to vytvořil nebo jak to bude odstraněno.

Mezi typy s nestandardním modelem správy paměti patří například objekty Qt. Zde má každý objekt rodiče, který je zodpovědný za jeho odstranění. A objekt o tom ví, protože dědí z třídy QObject. Patří sem také typy s počtem odkazů, například ty, které jsou navrženy pro práci s boost::intrusive_ptr .

Jinými slovy, typ se standardním modelem správy paměti neposkytuje žádné další mechanismy pro správu své životnosti. To by mělo být řešeno výhradně na straně uživatele. Ale typ s nestandardním modelem takové mechanismy poskytuje. Například QObject má metody setParent() a children() a obsahuje seznam potomků a typ boost::intrusive_ptr spoléhá na funkce intrusive_ptr_add_ref a intrusive_ptr_release a obsahuje počítadlo referencí.

Pokud má typ objektu standardní model správy paměti, pak pro stručnost řekneme, že se jedná o objekt se standardní správou paměti. Podobně, pokud má typ objektu nestandardní model správy paměti, pak řekneme, že se jedná o objekt s nestandardní správou paměti.

Dále se podívejme na objekty obou modelů. Výhledově stojí za to říci, že u objektů se standardní správou paměti byste rozhodně neměli používat new a delete v klientském kódu a u objektů s nestandardní správou paměti záleží na konkrétním modelu.

* Některé výjimky: idiom pimpl; velmi velký objekt (například vyrovnávací paměť).

** Výjimkou je std::locale::facet (viz níže).

Dynamické objekty se standardní správou paměti

S těmi se v praxi setkáváme nejčastěji. A právě oni by se měli snažit využít v moderním C++, protože s nimi pracují standardní přístupy, používané zejména v chytrých ukazatelích.

Ve skutečnosti, chytré ukazatele, ano, jsou odpovědí. Měli by mít kontrolu nad životností dynamických objektů. V C++ jsou dva: std::shared_ptr a std::unique_ptr. Nebudeme zde zdůrazňovat std::weak_ptr, protože je to jen pomocník pro std::shared_ptr v určitých případech použití.

Pokud jde o std::auto_ptr, byl oficiálně odstraněn z C++ počínaje C++17. Odpočívej v pokoji!

Nebudu se zde zdržovat designem a používáním chytrých ukazatelů, protože... to je nad rámec článku. Dovolte mi, abych vám hned připomněl, že jsou dodávány s úžasnými funkcemi std::make_shared a std::make_unique a měly by být použity k vytváření chytrých ukazatelů.

Tito. místo tohoto:
std::unique_ptr cookie(new Cookie(těsto, cukr, skořice));
by se mělo psát takto:
auto cookie = std::make_unique (těsto, cukr, skořice);
Výhody make funkcí oproti explicitnímu vytváření chytrých ukazatelů krásně popisuje Herb Sutter ve svém GotW #89 a Scott Myers ve svém Effective Modern C++, Item 21. Nebudu se opakovat, ale jen krátce seznam bodů zde:

  • Pro obě make funkce:
    • Bezpečnost z hlediska výjimek.
    • Neexistuje žádný duplicitní název typu.
  • Pro std::make_shared:
    • Zvyšte produktivitu, protože řídicí blok je alokován vedle samotného objektu, což snižuje počet volání do správce paměti a zvyšuje lokalizaci dat. Optimalizace.
Funkce Make mají také řadu omezení, která jsou podrobně popsána ve stejných zdrojích:
  • Pro obě make funkce:
    • Nemůžete předat svůj vlastní mazač . To je celkem logické, protože interně, make funkce, podle definice, používat standardní new .
    • Nemůžete použít zpevněný inicializátor ani všechny další vymoženosti spojené s dokonalým předáváním (viz Effective Modern C++, položka 30).
  • Pro std::make_shared:
    • Potenciální spotřeba paměti pro velké objekty s dlouhodobými slabými referencemi (std::weak_pointer).
    • Problémy s operátory new a delete přepsanými na úrovni třídy.
    • Potenciální falešné sdílení mezi objektem a řídicím blokem (viz otázka na StackOverflow).
V praxi jsou tato omezení vzácná a nijak neubírají na výhodách. Ukázalo se, že inteligentní ukazatele před námi skryly volání k odstranění a funkce make před námi skryly volání nové. V důsledku toho jsme získali spolehlivější kód, který neobsahuje nové ani smazat .

Mimochodem, struktura funkcí make je vážně odhalena ve svých zprávách Stefana Lavaveyho (aka STL). Zde je výmluvný snímek z jeho zprávy Don't Help the Compiler:

Dynamické objekty s nestandardní správou paměti

Kromě standardního přístupu ke správě paměti pomocí chytrých ukazatelů existují i ​​další modely. Například referenční počítání a vztahy mezi rodiči a dětmi.

Dynamické objekty s počítáním referencí


Velmi běžná technika používaná v mnoha knihovnách. Vezměme si jako příklad knihovnu OpenSceneGraph. Je to otevřený multiplatformní 3D engine napsaný v C++ a OpenGL.

Většina tříd v něm dědí z třídy osg::Referenced, která interně provádí počítání odkazů. Metoda ref() zvyšuje čítač, metoda unref() snižuje čítač a odstraňuje objekt, když čítač dosáhne nuly.

Sada také obsahuje inteligentní ukazatel osg::ref_ptr , který volá metodu T::ref() na uloženém objektu v jeho konstruktoru a metodu T::unref() v jeho destruktoru. Stejný přístup se používá v boost::intrusive_ptr, pouze tam jsou externí funkce místo metod ref() a unref().

Podívejme se na část kódu, který je uveden v oficiálním OpenSceneGraph 3.0: Průvodce pro začátečníky:
osg::ref_ptr vertices = new osg::Vec3Array; // ... osg::ref_ptr normals = new osg::Vec3Array; // ... osg::ref_ptr geom = nové osg::Geometrie; geom->setVertexArray(vertices.get()); geom->
Velmi známé konstrukce jako osg::ref_ptr p = nové T . Přesně stejným způsobem, jakým se funkce std::make_unique a std::make_shared používají k vytvoření tříd std::unique_ptr a std::shared_ptr, můžeme napsat funkci osg::make_ref a vytvořit třídu osg::ref_ptr . To se provádí velmi jednoduše, analogicky s funkcí std::make_unique:
jmenný prostor osg (šablona osg::ref_ptr make_ref(Args&&... args) ( return new T(std::forward (args)...);
))
Pojďme přepsat tento kus kódu vyzbrojený naší novou funkcí: auto vertices = osg::make_ref (); // ... auto normals = osg::make_ref (); // ... auto geom = osg::make_ref
(); geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); //...

Změny jsou triviální a lze je snadno provést automaticky. Tímto jednoduchým způsobem získáme bezpečnost výjimek, žádné duplicitní názvy typů a vynikající shodu se standardním stylem. Volání delete již bylo skryto v metodě osg::Referenced::unref() a nyní jsme nové volání skryli ve funkci osg::make_ref.

Takže žádné nové a smazat .

* Technicky vzato, v tomto fragmentu nejsou žádné situace, které by byly z hlediska výjimek nebezpečné, ale ve složitějších konfiguracích nějaké být mohou.


Dynamické objekty pro nemodální dialogy v MFC

Podívejme se na příklad specifický pro knihovnu MFC. Toto je obal tříd C++ přes Windows API. Používá se ke zjednodušení vývoje GUI ve Windows.

V níže uvedeném příkladu se dialogové okno vytvoří kliknutím na tlačítko v metodě CMainFrame::OnBnClickedCreate() a odstraní se v přepsané metodě CMyDialog::PostNcDestroy().
void CMainFrame::OnBnClickedCreate() ( auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); ) class CMyDialog: public CDialog ( public: CMyDialog(CWnd* pParent); (Vytvořit(IDD_MY_MY) protected: void PostNcDestroy() override ( CDialog::PostNcDestroy(); delete this; ) );
Zde nemáme skrytý ani nový, ani odstraněný hovor. Existuje spousta způsobů, jak se střelit do nohy. Kromě obvyklých problémů s ukazateli můžete zapomenout přepsat metodu PostNcDestroy() v dialogu, což má za následek únik paměti. Když uvidíte volání na nový , možná budete chtít v určitém okamžiku zavolat smazání, což bude mít za následek dvojité smazání. Můžete omylem vytvořit objekt dialogu v automatické paměti, opět dostaneme dvojité smazání.

Pokusme se skrýt volání new a delete uvnitř mezitřídy CModelessDialog a továrny CreateModelessDialog, která bude zodpovědná za nemodální dialogy v naší aplikaci:
class CModelessDialog: public CDialog ( public: CModelessDialog(UINT nIDTemplate, CWnd* pParent) ( Create(nIDTemplate, pParent); ) protected: void PostNcDestroy() override ( CDialog::PostNcDestroy(); smazat toto; ) ); // Továrna na vytváření šablony modálních dialogů Derived* CreateModelessDialog(Args&&... args) ( // Místo static_assert v těle funkce můžeme použít std::enable_if v její hlavičce, což nám umožní použít SFINAE. // Ale protože další přetížení této funkce jsou neočekávané, zdá se rozumné použít jednodušší a vizuálnější řešení static_assert(std::is_base_of) ::value, "CreateModelessDialog by měl být volán pro potomky CModelessDialog"); auto* pDialog = new Derived(std::forward
(args)...);
pDialog->ShowWindow(SW_SHOW); return pDialog; )
Samozřejmě jsme tímto způsobem nevyřešili všechny problémy. Například objekt může být stále alokován na zásobníku a může být dvakrát odstraněn. Alokaci objektu na zásobníku můžete zabránit pouze úpravou samotné třídy objektu, například přidáním soukromého konstruktoru. Ale neexistuje způsob, jak to udělat ze základní třídy CModelessDialog. Třídu CMyDialog můžete samozřejmě úplně skrýt a vytvořit továrnu, která nebude založená na šablonách, ale bude klasičtější a bude přijímat nějaký identifikátor třídy. To vše je ale nad rámec článku.

Každopádně jsme usnadnili vytváření dialogu z klientského kódu a psaní nové třídy dialogů. A zároveň jsme z klientského kódu odstranili volání new a delete.

Dynamické objekty se vztahem rodič-dítě



Vyskytují se poměrně často, zejména v knihovnách pro vývoj GUI. Jako příklad uveďme Qt, známou knihovnu pro vývoj aplikací a uživatelského rozhraní.

Většina tříd dědí z QObject. Ukládá seznam dětí a maže je, když se sám smaže. Ukládá ukazatel na rodiče (může být null) a může během života změnit rodiče.

Vynikající příklad situace, kdy zbavování se nových a mazání nebude fungovat tak snadno. Knihovna byla navržena tak, aby tyto operátory mohly a měly být použity v mnoha případech. Navrhl jsem obal pro vytváření objektů s nenulovým rodičem, ale nápad nefungoval (viz diskuze v e-mailové konferenci Qt).

Takže nevím o dobrém způsobu, jak se zbavit nových a odstranit v Qt.

Dynamické objekty std::locale::facet


K řízení výstupu dat do streamů v C++ se používají objekty std::locale. Národní prostředí je sada aspektů, které určují způsob zobrazení určitých dat. Fazety mají svůj vlastní referenční čítač a při kopírování lokalit se fazety nekopírují, pouze se zkopíruje ukazatel a referenční čítač se zvýší.

Samotné národní prostředí je zodpovědné za odstranění faset, když počet odkazů dosáhne nuly, ale uživatel musí vytvořit fasety pomocí operátoru new (viz část Poznámky v popisu konstruktoru std::locale):
std::locale default; std::locale myLocale(výchozí, nové std::codecvt_utf8 );
Tento mechanismus byl implementován ještě před zavedením standardních chytrých ukazatelů a vyčnívá z obecných pravidel pro používání tříd ve standardní knihovně.

Můžete vytvořit jednoduchý obal, který vytvoří národní prostředí pro odstranění nového z kódu klienta. Jde však o poměrně známou výjimku z obecných pravidel a zahrádku k tomu snad ani nemá smysl dělat.

Závěr

Nejprve jsme se tedy podívali na scénáře, jako je vytváření dynamických polí a dynamických objektů se standardní správou paměti. Místo new a delete jsme použili standardní kontejnery a make funkce a získali jsme jednodušší a spolehlivější kód.

Poté jsme se podívali na řadu příkladů nestandardní správy paměti a viděli jsme, jak bychom mohli kód vylepšit odstraněním nových a odstraněním ve vhodných obalech. Našli jsme také příklad, kdy tento přístup nefunguje.

Ve většině případů však toto doporučení poskytuje vynikající výsledky a lze jej použít jako výchozí princip. Nyní můžeme zvážit, že pokud kód používá new nebo delete , jedná se o speciální případ, který vyžaduje zvláštní pozornost. Pokud tato volání vidíte v klientském kódu, zvažte, zda jsou skutečně oprávněná.

  • Vyhněte se použití nového a smazání v kódu. Představte si je jako nízkoúrovňové manuální operace správy haldy.
  • Pro dynamické datové struktury použijte standardní kontejnery.
  • Kdykoli je to možné, použijte k vytváření dynamických objektů funkce make.
  • Vytvořte obaly pro objekty s nestandardním paměťovým modelem.

Od autora

Osobně jsem se setkal s mnoha případy úniku paměti a pádů kvůli nadměrnému používání new a delete . Ano, většina tohoto kódu byla napsána před mnoha lety, ale pak s ním začnou pracovat mladí programátoři a myslí si, že tak by se to mělo psát.

Doufám, že tento článek poslouží jako praktický průvodce, ke kterému lze mladého vývojáře poslat, aby nezabloudil.

Před více než rokem jsem na toto téma přednesl prezentaci na konferenci C++ Russia. Po mém projevu se publikum rozdělilo na dvě skupiny: na ty, pro které bylo všechno samozřejmé, a na ty, kteří sami pro sebe udělali úžasný objev. Domnívám se, že konferencí se účastní spíše zkušenější vývojáři, takže i když se našlo mnoho lidí, kteří s těmito informacemi byli noví, doufám, že tento článek bude pro komunitu užitečný.

PS V průběhu diskuse o článku jsme s kolegy vedli celou debatu o tom, co je správné: „Myers“ nebo „Meyers“. Na jedné straně zní „Meyers“ ruským uším povědoměji a zdá se, že my sami jsme tak vždy mluvili. Na druhé straně se na wiki používá „Myers“. Pokud se podíváte na lokalizované knihy, pak je tam obecně spousta věcí: k těmto dvěma možnostem se přidává také „Meyers“. Na konferencích jiný Lidé zastupovat to různými způsoby. Nakonec my podařilo zjistit, že si říká „Myers“, pro který se rozhodli.

Odkazy

  1. Bylina Sutter Řešení GotW #89: Inteligentní ukazatele.
  2. Scott Meyers Efektivní moderní C++, bod 21, s. 139.
  3. Stephan T. Lavavej, Nepomáhat kompilátoru.
  4. Bjarne Stroustrup, Programovací jazyk C++, 11.2.1, str. 281.
  5. Pět populárních mýtů o C++., Část 2
  6. Michail Matrosov, C++ bez nového a smazání .

Štítky:

Přidejte značky

Komentáře 134

  • Konzultace

Ahoj! Níže budeme hovořit o známých operátorech nový A vymazat, respektive o tom, o čem se v knihách (alespoň v knihách pro začátečníky) nepíše.
K napsání tohoto článku mě přiměla běžná mylná představa o nový A vymazat, které neustále vídám na fórech a dokonce (!!!) v některých knihách.
Víme všichni, co to doopravdy je? nový A vymazat? Nebo si jen myslíme, že víme?
Tento článek vám pomůže na to přijít (no, ti, kteří vědí, mohou kritizovat :))

Poznámka: níže budeme hovořit výhradně o operátoru new, pro ostatní formy operátoru new a pro všechny formy operátoru delete platí také vše napsané níže a platí obdobně.

Začněme tedy tím, co obvykle píší v knihách pro začátečníky, když popisují nový(text byl vytažen z čistého nebe, ale je zcela pravdivý):

Operátor nový alokuje paměť větší nebo rovnou požadované velikosti a na rozdíl od funkcí jazyka C volá konstruktor(y) pro objekt(y), kterým je paměť alokována...můžete přetížit (někde píšou, aby implementovali) operátor nový aby vyhovoval vašim potřebám.

A například ukazují primitivní přetížení (implementaci) nového operátoru, jehož prototyp vypadá takto
void* operátor new (std::size_t size) throw (std::bad_alloc);

Čemu chcete věnovat pozornost:
1. Nikde se nesdílejí nový klíčové slovo Jazyk C++ a operátor nový všude se o nich mluví jako o jedné entitě.
2. Všude to píšou nový volá konstruktor(y) na objektu(ech).
První i druhý jsou běžné mylné představy.

Nespoléhejme ale na knihy pro začátečníky, vraťme se ke Standardu, konkrétně k části 5.3.4 a 18.6.1, ve které je téma tohoto článku skutečně odhaleno (nebo spíše mírně odhaleno).

5.3.4
Nový výraz se pokusí vytvořit objekt typu id (8.1) nebo id nového typu, na který je aplikován. /*další nás nezajímá*/
18.6.1
void* operátor new(std::size_t size) throw(std::bad_alloc);
Efekty: Alokační funkce volaná novým výrazem (5.3.4) k přidělení velikosti bajtů
úložiště vhodně zarovnané, aby reprezentovalo jakýkoli předmět této velikosti /*další nás nezajímá*/

Zde již vidíme, že v prvním případě nový označované jako výraz, a ve druhém je deklarováno jako operátor. A to jsou opravdu 2 různé entity!
Zkusme přijít na to, proč tomu tak je, k tomu budeme potřebovat výpisy sestav získané po kompilaci kódu pomocí nový. No a teď si povíme všechno popořadě.

nový-výraz je jazykový operátor, stejně jako -li, zatímco atd. (Ačkoli li, zatímco atd. jsou stále označovány jako prohlášení, ale zahoďme texty) To je. narazí-li na něj ve výpisu, kompilátor vygeneruje specifický kód odpovídající tomuto operátoru. Také nový- toto je jeden z klíčová slova jazyk C++, který opět potvrzuje svou shodnost s -li"ami, pro" ami atd. A operátor new() jde zase jednoduše o stejnojmennou funkci jazyka C++, jejíž chování lze přepsat. DŮLEŽITÉ - operátor new() NE volá konstruktor(y) pro objekt(y), pro které je alokována paměť. Jednoduše přidělí paměť požadované velikosti a je to. Jeho rozdíl od C funkcí je v tom, že může vyvolat výjimku a lze ji předefinovat, stejně jako vytvořit operátor pro samostatnou třídu, čímž ji předefinujete pouze pro tuto třídu (ostatní si pamatujte sami :)).
Ale nový-výraz pouze volá konstruktor(y) objektu(ů). I když by bylo správnější říci, že také nic nevolá jednoduše, když na něj narazí, kompilátor vygeneruje kód pro volání konstruktoru(ů).

Chcete-li obrázek dokončit, zvažte následující příklad:

#zahrnout třída Foo ( public: Foo() ( std::cout<< "Foo()" << std::endl; } }; int main () { Foo *bar = new Foo; }

Po provedení tohoto kódu se podle očekávání vytiskne „Foo()“. Pojďme zjistit proč, k tomu se musíte podívat do assembleru, který jsem pro pohodlí trochu okomentoval.
(kód pochází z kompilátoru cl použitého v MSVS 2012, i když většinou používám gcc, ale to je vedle)
/Foo *bar = new Foo; stisknout 1; velikost v bajtech pro operátora volání objektu Foo new (02013D4h) ; operátor hovoru nový pop ecx mov dword ptr ,eax ; napište ukazatel vrácený z new na bar a dword ptr ,0 cmp dword ptr ,0 ; zkontrolujeme, zda se 0 přihlásil do baru je main+69h (0204990h); je-li 0, pak zde necháme (třeba i z main nebo nějakému handleru, v tomto případě je to jedno) mov ecx,dword ptr ; umístit ukazatel na alokovanou paměť do ecx (MSVS to vždy předá ecx(rcx)) call Foo::Foo (02011DBh) ; a zavolejte konstruktor; žádný další zájem
Pro ty, kteří ničemu nerozuměli, zde je (téměř) analogie toho, co se stalo v pseudokódu podobném C (tj. není třeba zkoušet jej kompilovat :))
Foo *bar = operátor nový (1); // kde 1 je požadovaná velikost bar->Foo(); // volání konstruktoru

Výše uvedený kód potvrzuje vše napsané výše, konkrétně:
1. operátor (jazyk) nový A operátor new()- to NENÍ to samé.
2. operátor new() NEVOLÁ konstruktor(y)
3. volání konstruktoru (konstruktorů) je generováno kompilátorem, když na něj narazí v kódu klíčové slovo "nový"

Sečteno a podtrženo: Doufám, že vám tento článek pomohl pochopit rozdíl mezi nový-výraz A operátor new() nebo dokonce zjistit, že to (tento rozdíl) vůbec existuje, pokud by to někdo nevěděl.

P.S. operátor vymazat A operátor delete() mají podobný rozdíl, tak jsem na začátku článku řekl, že to nebudu popisovat. Myslím, že nyní chápete, proč jeho popis nedává smysl a můžete nezávisle zkontrolovat platnost toho, co bylo napsáno výše vymazat.

Aktualizovat:
Habrazhitel s přezdívkou khim V osobní korespondenci navrhl následující kód, který jasně demonstruje podstatu toho, co bylo napsáno výše.
#zahrnout class Test ( public: Test() ( std::cout<< "Test::Test()" << std::endl; } void* operator new (std::size_t size) throw (std::bad_alloc) { std::cout << "Test::operator new(" << size << ")" << std::endl; return::operator new(size); } }; int main() { Test *t = new Test(); void *p = Test::operator new(100); // 100 для различия в выводе }
Tento kód vypíše následující
Test::operátor nový(1) Test::Test() Test::operátor nový(100)
což se dá očekávat.

Nový operátor umožňuje alokovat paměť pro pole. Vrací se

ukazatel na první prvek pole v hranatých závorkách. Při přidělování paměti pro vícerozměrná pole musí být všechny rozměry kromě levého konstanty. První rozměr může být specifikován proměnnou, jejíž hodnota je uživateli známa v době, kdy je nový použit, například:

int *p=new int[k]; // chyba nelze převést z "int (*)" na "int *"

int (*p)=new int[k]; // správně

Při přidělování paměti objektu nebude jeho hodnota definována. Objektu však může být přidělena počáteční hodnota.

int *a = nový int (10234);

Tuto volbu nelze použít k inicializaci polí. Však

místo inicializační hodnoty můžete umístit seznam oddělený čárkami

hodnoty předané konstruktoru při alokaci paměti pro pole (mass

iv nové objekty určené uživatelem). Paměť pro řadu objektů

lze přidělit pouze v případě, že má odpovídající třída

existuje výchozí konstruktor.

matr())(; // výchozí konstruktor

matr(int i,float j): a(i),b(j) ()

(matr mt(3,.5);

matr *p1=nová matr; // true p1 - ukazatel na 2 objekty

matr *p2=nová matr (2,3.4); // nesprávné, inicializace není možná

matr *p3=nová matr (2,3.4); // true p3 – inicializovaný objekt

( int i; // datová složka třídy A

A()() // konstruktor třídy A

~A()() // destruktor třídy A

( A *a,*b; // popis ukazatelů na objekt třídy A

float *c,*d; // popis ukazatelů na prvky typu float

a=nové A; // alokace paměti pro jeden objekt třídy A

b=nové A; // alokace paměti pro pole objektů třídy A

c=nový plovák; // přidělení paměti pro jeden prvek float

d=nový plovák; // alokuje paměť pro pole prvků float

odstranit a; // uvolnění paměti obsazené jedním objektem

odstranit b; // uvolnění paměti obsazené polem objektů

odstranit c; // uvolnění paměti jednoho prvku float

smazat d; ) // uvolnění paměti pole prvků float

Organizace externího přístupu k místním součástem třídy (přítel)

Již jsme se seznámili se základním pravidlem OOP - data (interní

proměnné) objektu jsou chráněny před vnějšími vlivy a přístup k nim může být

získat pouze pomocí funkcí (metod) objektu. Ale jsou i takové případy

čaje, když potřebujeme organizovat přístup k objektovým datům bez použití

učení jeho rozhraní (funkcí). Samozřejmě můžete přidat novou veřejnou funkci

do třídy, abyste získali přímý přístup k interním proměnným. Nicméně, v

Ve většině případů rozhraní objektu implementuje určité operace a

nová funkce může být nadbytečná. Zároveň se někdy vyskytuje a

potřeba organizovat přímý přístup k interním (místním) datům

dva různé objekty z jedné funkce. Přitom v C++ jedna funkce nemůže

může být součástí dvou různých tříd.

Aby to bylo možné implementovat, byl v C++ zaveden specifikátor přátel. Pokud nějaké

funkce je definována jako přátelská funkce pro nějakou třídu, pak:

Není funkční komponent této třídy;

Má přístup ke všem komponentám této třídy (soukromé, veřejné a chráněné).

Níže je uveden příklad přístupu externí funkce

interní data třídy.

#zahrnout

pomocí jmenného prostoru std;

kls(int i,int J) : i(I),j(J) () // konstruktor

int max() (return i>j? i: j;) // funkce komponenty třídy kls

přítel double fun(int, kls&); // přítel prohlášení o externí funkci fun

double fun(int i, kls &x) // externí funkce

( return (double)i/x.i;

cout<< obj.max() << endl;

V C(C++) jsou známy tři způsoby předávání dat funkci: hodnotou

možné na některém stávajícím objektu. Lze rozlišit následující časy:

přítomnost odkazů a ukazatelů. Za prvé, nemožnost existence nuly

odkazy znamená, že jejich správnost není třeba kontrolovat. A když používáte ukazatel, musíte zkontrolovat, zda nemá hodnotu nula. Za druhé, ukazatele mohou ukazovat na různé objekty, ale odkaz vždy ukazuje na jeden objekt, který je zadán při jeho inicializaci. Pokud chcete povolit funkci měnit hodnoty

parametry mu předány, pak v jazyce C musí být deklarovány buď

globálně, nebo se s nimi práce ve funkcích provádí prostřednictvím předávaných

Obsahuje ukazatele na tyto proměnné. V C++ lze argumenty předávat funkci

rum je označen &.

void fun1(int,int);

void fun2(int &,int &);

( int i=1,j=2; // i a j jsou lokální parametry

cout<< "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

cout<< "\n i = "<


Nahoru