4. díl - Reference v C++

C a C++ C++ Pokročilé konstrukce v C++ Reference v C++

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

Dosud jsme si řekli o dvou "typech", kterými C++ disponuje. Byly to hodnoty (sem se řadí typy jako int, char, string apod.) a ukzatele. C++ na rozdíl od C zavádí i další skupinu typů - reference. Jedná se o takový přechod mezi ukazateli a hodnotami.

Reference, podobně jako pointer, slouží k odkazování se na data v paměti. Mezi pointerem a referencí jsou dva zásadní rozdíly. Za prvé, s referencí, na rozdíl od pointeru, zacházíme jako s klasickou proměnnou nebo objektem. Za druhé, reference odkazuje pořád na ta samá data, nikdy ji nemůžeme přesměrovat někam jinam do paměti. Dá se tedy říct, že reference je takový pohodlnější konstantní pointer. Deklaruje se použitím znaku & za datovým typem.

int &reference;  //pokus o deklaraci reference typu int

Toto nebude fungovat, protože reference musí vždy být inicializována při jejím vytvoření. To je dáno tím, že reference musí odkazovat na nějaké místo v paměti, ale dále se s ní pracuje stejnou syntaxí, s jakou pracujeme s klasickými typy.

int cislo = 50;
int &reference = cislo;  //deklarace reference odkazující na proměnnou cislo
...

V tomto případě můžeme tvrdit, že reference je jen jakýsi náhradní název pro proměnnou cislo. Pokud referenci změníme (nebo změníme hodnotu čísla), změní se obě hodnoty.

int cislo = 10;
int &reference = cislo;
cout << "Cislo: " << cislo << ", reference: " << reference << endl;
cislo++;
cout << "Po inkrementaci cisla" << endl;
cout << "Cislo: " << cislo << ", reference: " << reference << endl;
reference = reference + 4;
cout << "Po scitani s referenci" << endl;
cout << "Cislo: " << cislo << ", reference: " << reference << endl;
Konzolová aplikace
Cislo: 10, reference: 10
Po inkrementaci cisla
Cislo: 11, reference: 11
Po scitani s referenci
Cislo: 15, reference: 15

S referencí jsme se již setkali. Pomocí & jsme získávali adresu proměnné (tedy její referenci). Je tedy nutné rozlišovat zápis deklarace reference (int&) a získání reference (&promenna). Adresu, na kterou reference odkazuje již nelze později změnit, protože kompilátor netuší, zda chcete do proměnné uložit adresu nebo chcete změnit hodnotu proměnné. Následující případ se tedy nezkompiluje.

int promenna1 = 1;
int promenna2 = 2;
int& reference = promenna1;
reference = &promenna2;

Reference se nemusí odkazovat pouze na deklarované proměnné či objekty, ale i na data vytvořená operátorem new, nebo všeobecněji, na jakoukoliv část paměti. V principu bysme mohli referenci považovat za ukazatel s jednodušší syntaxí (protože se s ní zachází jako se základním typem, tj. nemusíme psát *).

int* ptr = new int;  //vytvoření paměti pro data typu int
int & ref = *ptr;    //ref se nyní odkazuje na tato data
ref = 50;            //můžeme s nimi zacházet jako s proměnnou
cout << "Hodnota ukazatele: " << *ptr << ", reference: " << ref << endl;

Můžeme vytvořit i referenci na pointer.

int* ptr = new int;
int* &ref = ptr;
*ref = 14;
cout << "Hodnota ukazatele: " << *ptr << "Hodnota reference: " << *ref << endl;

O tom, že reference odkazuje na stejný objekt se můžeme přesvědčit právě získáním adresy proměnné.

Pozn.: Hodnota se zřejmě bude lišit při každém spuštění programu, ale dvojice bude vždy stejná.

int promenna = 5;
int &reference = promenna;
cout << "Adresa promenne: " << &promenna << "Adresa reference: " << &reference << endl;
Konzolová aplikace
Adresa promenne: 0x7ffda12a67f4Adresa reference: 0x7ffda12a67f4

Reference jako parametr funkce

Čas od času potřebujeme, aby nějaká funkce pracovala s již vytvořenými daty - předanými parametry a ne pouze s jejich kopiemi. K jejich předání můžeme použít pointer, pak ovšem musíme neustále myslet na to, že je potřeba použít dereferenci. V tomto případě bývá pohodlnější použít referenci. Uvažujme například funkci, která má za úkol prohodit dvě hodnoty typu double. Pokud bychom použili pointery, funkce by vypadala takto:

void SwapFunction(double* a, double* b)
{
  double c = *a;  //přiřazení hodnoty na adrese, na kterou ukazuje pointer a, proměnné c
  *a = *b;  //přiřazení hodnoty na adrese, na kterou ukazuje pointer b, na místo v paměti, na které ukazuje pointer a
  *b = c;  //přiřazení hodnoty proměnné c, na místo v paměti, na které ukazuje pointer b
}
//...
double x = 0.4;
double y = 1.9;
SwapFunction(&x,&y);  //při volání funkce musíme za parametry dosadit adresy proměnných

Stejná funkce by ovšem vypadala poněkud lépe, pokud bychom místo pointerů použili reference.

void SwapFunction(double & a, double & b)
{
  double c = a;  //přiřazení hodnoty proměnné, na kterou se odkazuje reference a, proměnné c
  a = b;  //přiřazení hodnoty proměnné, na kterou se odkazuje reference b, proměnné, na kterou se odkazuje reference a
  b = c;  //přiřazení hodnoty proměnné c, proměnné, na kterou se odkazuje reference b
}
...
double x = 0.4;
double y = 1.9;
SwapFunction(x,y);  //při volání se reference inicializují proměnnými x a y

V předchozích dílech jsme si řekli, že parametry metod se překopírují do zásobníku a tím se předají volané funkci. To může být problém, pokud předávané objekty jsou velké. Zásobník má velikost v řádech jednotkách megabajtů, zatímco halda v řádech gigabajtů (vaše RAM paměť). Pokud se metod bude volat moc (tzv. zanoření), tak nám zásobník dojde a program spadne.

struct test {
        char polozky[1024*1024];
};
void metoda(int zanoreni, test x){
        cout << "Zanoreni: " << zanoreni << endl;
        metoda(zanoreni+1,x);
}
int main(){
        test a;
        metoda(0,a);
}
Konzolová aplikace
Zanoreni: 0
Zanoreni: 1
Zanoreni: 2
Zanoreni: 3
Zanoreni: 4
Neoprávněný přístup do paměti

Všimněte si, že jsme mohli zavolat pouze 5 funkcí. To je v běžném programu naprosto nedostatečné číslo. Tím, že objekt předáme jako referenci ušetříme místo na zásobníku. Na druhou stranu musíme počítat s tím, že nám hodnotu může funkce změnit.

void metoda(int zanoreni, test &x){
        cout << "Zanoreni: " << zanoreni << endl;
        metoda(zanoreni+1,x);
}
Konzolová aplikace
...
Zanoreni: 229937
Zanoreni: 229938
Zanoreni: 229939
Neoprávněný přístup do paměti

Rázem jsme se dostali na statisíce volání. Ve vyšších programovacích jazycích (Java, C#) se rozlišují hodnotové a referenční typy. Referenční typy jsou přesně to, co jsme si před chvílí ukázali a v těchto jazycích je to většina běžně používaných objektů.

Reference jako návratová hodnota funkce

Stejně tak jako můžeme referenci předat parametrem, může jí funkce i vrátit. Funkce nám jednoduše vrátí referenci na to, co uvedeme za klíčovým slovem return. Uvažujme například následující kód:

int a = 10;
int& funkce()  // vytvoření funkce vracející referenci typu int
{
  //... nějaký kód
  return a;  // funkce vrátí referenci na globální proměnnou a
}

int main()
{
        int num = funkce();    // proměnné num se pouze přiřadí hodnota proměnné a
        int & ref = funkce();  // reference ref je inicializována proměnnou a
                               // ref nyní odkazuje na a
        cout << "num: " << num << ", a: " << a << ", ref: " << ref << endl;
        funkce() = 50;         // i toto je možné, nastaví hodnotu proměnné a na 50
        cout << "num: " << num << ", a: " << a << ", ref: " << ref << endl;
}

Pokud by naše funkce nevracela referenci, ale hodnotu (int funkce()), poslední dva výrazy by nebylo možné vykonat. Pro běžné funkce vracjící hodnota (například int), je v paměti vyhrazena část místa, kde se návratová hodnota uloží. Pokud je hodnota použita, potom se zkopíruje a následně zahodí. Pokud návratová hodnota použita není, zahodí se rovnou. V obou případech je paměť uvolněna hned po skončení funkce a tak nemůžeme hodnotou v této části paměti inicializovat referenci, stejně tak nemůžeme do této části paměti přiřadit žádnou hodnotu. Naše funkce ovšem nevrací hodnotu, nýbrž referenci na již existující data, proto je možné touto funkcí inicializovat další referenci, nebo tato data přes vrácenou referenci funkce rovnou změnit. Často můžeme vrácení reference použít čistě z důvodu efektivity, tak jako tomu bylo u parametru.

Nakonec ještě jedna poznámka. Nikdy nevracejte referenci na lokální data. Ty jsou po ukončení funkce smazány a na jejich místě pravděpodobně budou neplatné data. Následující kód se zkompiluje, ale nebude fungovat správně.

int& soucet(int a, int b)
{
        int soucet = a+b;
        return soucet;
}

int main()
{
        int num = soucet(1,1);    // proměnné num se pouze přiřadí hodnota proměnné a
        cout << "Neplatné data: " << num << endl;
}

V příštím díle, Přetěžování a statika u funkcí, se můžete těšit na pokročilé témata spojené s funkcemi.


 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
1 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Miniatura
Předchozí článek
Aritmetika ukazatelů v C++
Miniatura
Všechny články v sekci
Pokročilé konstrukce C++
Miniatura
Následující článek
Přetěžování a statika u funkcí
Aktivity (5)

 

 

Komentáře

Avatar
dan.azo
Člen
Avatar
dan.azo:29. srpna 14:44

Mám trochu problém u posledního příkladu. Chápu, že se lokální proměnné smažou a pak se nutně musejí vracet neplatná data, proč se ale vrací správný součet, když už tam ta data nejsou ?

 
Odpovědět 29. srpna 14:44
Avatar
Luboš Satik Běhounek
Autoredaktor
Avatar
Odpovídá na dan.azo
Luboš Satik Běhounek:29. srpna 15:12

c++ paměť po uvolnění ani po alokaci běžně nemaže, takže pokud se ti tam nezapsalo mezi tím nic jinýho, tak tam pořád najdeš ty data :)

Odpovědět 29. srpna 15:12
:)
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.