Reference

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

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 & ref;  //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í.

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

V tomto případě můžeme tvrdit, že ref je jen jakýsi náhradní název pro proměnnou cislo. Pokud ref změníme, změní se i cislo, pokud změníme cislo, změní se i ref. Stejně tak vrátí vždy stejnou hodnotu.

...
ref = 10;  //přiřazení čísla 10 referenci ref... změní i cislo na 10
cout<<cislo;  //vypíše 10
cislo = -5;  //přiřazení čísla -5 proměnné cislo
cout<<ref;  //ref se odkazuje na cislo, vypíše se -5

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, ke které pak program přistupuje jako k proměnné či objektu typu, kterého je reference.

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

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

int a = 100;
int* ptr = &a;
int* & ref = ptr;
cout<<ref;  //vypíše adresu proměnné a, respektive obsah pointeru ptr
cout<<*ref;  //vypíše hodnotu proměnné a, respektive dereferencovaný obsah pointeru ptr, tedy 100

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. Můžeme k jejich předání použít pointer, pak ovšem musíme neustále myslet na to, že je potřeba použít dereferenci nebo přemýšlet nad tím, zda použít operátor přímé či nepřímé příslušnosti. 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

Předávání parametrů referencí má ještě jednu, mnohdy velkou výhodu. Tento parametr se totiž nemusí kopírovat do nově vytvořené proměnné či objektu, ale funkci pouze řekne, kam k němu má přistupovat. U obyčejných proměnných na tom běžně nezáleží, co když ale budeme mít objekt třídy zabírající v paměti několik kilobytů. Uvažujme následující třídu:

class HugeArray
{
  private:
    int array[100000];  //třída obsahuje pole o 100 000 prvcích typu int
  ...
};

Řekněme, že chceme objekt takovéto třídy předat parametrem nějaké funkci.

void funkce(HugeArray hArr){...}  //funkce přebírá jako parametr objekt třídy HugeArray
...
HugeArray arr;
funkce(arr);  //kopíruje celý objekt arr do parametru funkce

Při volání této funkce, se všech 100 000 prvků pole array bude kopírovat z předávaného objektu do objektu, který je vytvořen jako parametr funkce. To je ovšem dosti neefektivní řešení. Pokud je jisté, že se objekt z parametru uvnitř funkce nijak nezmění (tedy nic do něj nezapisujeme, pouze z něj čteme), pak můžeme předat pouze odkaz na tento objekt, což bude mnohonásobně rychlejší. Je dobré uvést klíčové slovo const, aby bylo jasné, že funkce objekt nezmění, a pokud omylem ano, kompilátor zahlásí chybu.

void funkce(const HugeArray & hArr){...}  //funkce přebírá jako parametr referenci na const HugeArray
...
HugeArray arr;
funkce(arr);  //inicializuje referenci v parametru funkce objektem arr

Toto řešení je mnohem efektivnější. Program nemusí kopírovat celý objekt, pouze předá referenci na něj.

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;
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 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, nyní se ref na tuto proměnnou odkazuje
funkce() = 50;  //i toto je možné, nastaví hodnotu proměnné a na 50

Pokud by naše funkce nevracela referenci, ale hodnotu (int funkce()), poslední dva výrazy by nebylo možné vykonat. Funkce si při vracení hodnoty dočasně alokuje část paměti, do které uloží návratovou hodnotu, pokud není tato hodnota dále nikam přiřazena, program ji okamžitě dealokuje, pokud ano, program zkopíruje tuto část paměti tam, kam má a následné ji dealokuje. Tak jako tak, tato paměť je po zavolání funkce okamžitě dealokována, nemůžeme tedy 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. Berme opět v úvahu naší třídu HugeArray.

HugeArray funkce(HugeArray & arr)  //vytvoření funkce vracející objekt třídy HugeArray
{
  ...  //nějaký kód
  return arr;
}
...
HugeArray hArray;  //deklarace objektu třídy HugeArray
funkce(hArray);  //volání funkce
HugeArray obj = funkce(hArray);  //inicializace obejktu, objektem vráceným funkcí

Tento kód bude opět značně neefektivní. Funkce totiž vrací hodnotu objektu hArray, na nějž přebírá referenci v parametru. Při volání funkce samostatně se objekt musí celý zkopírovat do pomocné paměti, kterou si funkce vytvoří pro návratovou hodnotu, a to i přes to, že se tato paměť dále nikam nekopíruje a ihned zanikne. Při druhém volání, kde je vráceným objektem funkce inicializován další, nově vytvořený objekt, se návratová hodnota kopíruje dvakrát. Jedou z hArray do pomocné paměti, podruhé z této paměti do nově vytvořeného objektu. Tomuto zbytečnému kopírování můžeme snadno zabránit vrácením reference. Pokud nechceme, aby byl objekt přes volání funkce změněn, nebo jím byla inicializována nějaká další reference, přes kterou by bylo možné objekt změnit, uvedeme před názvem datového typu klíčové slovo const.

const HugeArray & funkce(HugeArray & arr)  //vytvoření funkce vracející referenci na const HugeArray
{
  ...  //nějaký kód
  return arr;
}
HugeArray hArray;  //deklarace objektu třídy HugeArray
funkce(hArray);  //volání funkce
HugeArray obj = funkce(hArray);  //inicializace obejktu, objektem vráceným funkcí

Zde se při samostatném volání objekt hArray nekopíruje vůbec nikam a při druhém volání, kde se tímto objektem inicializuje nový objekt, se hArray kopíruje pouze jednou. Funkce totiž vrátí pouze referenci na objekt a přes tuto referenci se pak data objektu hArray zkopírují rovnou do nově vytvořeného objektu. Toto je samozřejmě znatelně efektivnější než předchozí kód.

Nakonec jedna důležitá poznámka: Nikdy nevracejte referenci na data deklarovaná nestaticky uvnitř funkce, tato data po skončení funkce již neexistují.


 

  Aktivity (3)

Článek pro vás napsal Lukáš Hruda (Luckin)
Avatar
...

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 


Miniatura
Všechny články v sekci
Pokročilé konstrukce v C++
Miniatura
Následující článek
Parametry a ukazatele na funkce v C++

 

 

Komentáře

Avatar
Samuel Bachar:

Čauko super článok na osvieženie znalostí :) . Ale zaujal ma jeden problem a to jest :

HugeArray hArray;
HugeArray obj = funkce(hArray); // Tato inicializácia objektu prostredníctvom referencie objektu ktoru vracia funkce mi nefunguje . Výpiše mi nasledovné .

 
Odpovědět 27.10.2015 21:22
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na Samuel Bachar
Martin Dráb:

Ahoj,

příčinou tvého problému se zdá býti přetečení zásobníku. Na zásobníku se alokují lokální proměnné, což v tvém případě jsou hArray, obj a při volání funkce funkce to bude ještě proměnná arr. Každá z těchto proměnných je definovaná +- jako pole 100000 intů, takže každá zabírá cca 390 KB (400 000 B) zásobníku. Celkem tedy něco přes 1 MB. Nevím, na jakém OS jsi to zkoušel, ale Windows standardně dávají vláknu zásobník o velikosti 1 MB, a proto přeteče. Zkus zvýšit velikost zásobníku na 2 či 4 MB a mělo by to fungovat.

Odpovědět  +2 28.10.2015 1:59
2 + 2 = 5 for extremely large values of 2
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovídá na Martin Dráb
David Novák:

Cokoliv většího by mělo být realizované na hromadě - navyšování velikosti zásobníku sice pomůže, ale není to řešení. Pokud by najednou potřeboval ještě větší pole, znovu se dostane do problému.

Samuel: Použij malloc pro takové velké pole ;)

Odpovědět  +2 28.10.2015 10:15
Chyba je mezi klávesnicí a židlí.
Avatar
Odpovídá na David Novák
Lukáš Hruda (Luckin):

Kdyz uz, tak C++ ma operator new[], malloc je Ceckova funkce.
Stacilo by ale proste zmensit velikost toho pole o jednu nulu, kdyz jsem ten tutorial psal, tak jsem na zasobnik nepomyslel.

 
Odpovědět  +2 28.10.2015 10:40
Avatar
Samuel Bachar:

Ďakujem za odpovede urobím si všetky možné verzie riešenia .

 
Odpovědět 28.10.2015 11:00
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na David Novák
Martin Dráb:

Cokoliv většího by mělo být realizované na hromadě - navyšování velikosti zásobníku sice pomůže, ale není to řešení. Pokud by najednou potřeboval ještě větší pole, znovu se dostane do problému.

To máš samozřejmě pravdu. Jenom to byl nejsnadnější způsob, jak ten příklad rozchodit.

Odpovědět  +2 28.10.2015 12:51
2 + 2 = 5 for extremely large values of 2
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovídá na Lukáš Hruda (Luckin)
David Novák:

Jasné.. :D Zvyk je železná košile... Ale ono to new je stejně jen zabalený malloc..

Odpovědět 28.10.2015 15:11
Chyba je mezi klávesnicí a židlí.
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na David Novák
Martin Dráb:

Jasné.. :D Zvyk je železná košile... Ale ono to new je stejně jen zabalený malloc..

Ano, v MSVS je to krásně vidět. Jen to tam navíc přidává vyhazování výjimky, pokud to není std::nothrow.

P. S. Malloc je pro Windows zakuklený HeapAlloc, resp. je to v podstatě 1:1 bez ničeho přídavného :-).

Odpovědět  +1 28.10.2015 17:34
2 + 2 = 5 for extremely large values of 2
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 8 zpráv z 8.