C/C++ (35) - Reference, funkce

Ukážeme si, co to jsou reference a jak se liší od ukazatelů. V druhé části dílu probereme dvě metody, které umožňují volat funkce se stejným identifikátorem, ale s různou sadou parametrů.

16.2.2006 08:00 | Jan Němec | přečteno 36679×

Reference

Jazyk C zná běžné typy a ukazatele na ně. Parametry předávané odkazem, běžně užívané například v Pascalu, v C neexistují. Pokud chceme předat do funkce jako parametr proměnnou a tuto proměnnou (nikoli její kopii na zásobníku) ve funkci modifikovat, musíme použít ukazatel. To s sebou přináší určitá úskalí, neboť je jen na zodpovědnosti programátora ohlídat si, aby byl ukazatel inicializovaný, navíc práce s běžnou proměnnou je díky odlišné syntaxi přeci jen o něco pohodlnější. C++ se snaží problém řešit pomocí referencí. Referenci je nejjednodušší si představit jako neměnný inicializovaný ukazatel se syntaxí běžné proměnné. Lze ji používat jako každý jiný typ při definici proměnných a parametrů i návratové hodnoty funkcí. Reference se definuje stejně jako ukazatel, jen místo znaku * napíšeme &. Nejjednodušší příklad je asi prohození hodnot dvou proměnných.

#include <stdio.h>

// Nefunkční řešení - programátor si neuvědomil, že se vytvářejí kopie
// proměnných. Původní proměnné zůstanou nezměněné.

void prohodNefunguje(int a, int b) {
   int c = a;
   a = b;
   b = c;
}

// Řešení ve stylu C++ (v C neprojde překladem) s použitím referencí
void prohodCPlusPlus(int &a, int &b) {
   int c = a;
   a = b;
   b = c;
}

// Řešení ve stylu C (lze použít i v C++) s použitím ukazatelů
void prohodC(int *a, int *b) {
   int c = *a;
   *a = *b;
   *b = c;
}

int main(void) {
  int x = 1;
  int y = 0;

  printf("x = %i y = %i\n", x, y);
  
  prohodNefunguje(x, y);
  printf("x = %i y = %i\n", x, y);

  prohodCPlusPlus(x, y);
  printf("x = %i y = %i\n", x, y);

  prohodC(&x, &y);
  printf("x = %i y = %i\n", x, y);

  return 0;
}

Jak použití referencí, tak i ukazatelů má v tomto případě své výhody i nevýhody. U ukazatele nemá programátor nikdy jistotu, že je správně inicializovaný, a ani test na NULL neodhalí všechny chyby. Rovněž zápis implementace funkce je trochu krkolomnější. V případě referencí sice klesá (nikoli na nulu!) riziko paměťových chyb, ale programátor zase nepozná na první pohled v místě volání, zda funkce může své parametry modifikovat. V duchu jazyka C++ je v tomto případě užití referencí.

Občas se také hodí reference jako proměnná, například pokud budeme opakovaně přistupovat do pole na stejný index.

int pole[20];

// definice s inicializací
int &pole5 = pole[5];

// totéž jako pole[5] = 1;
pole5 = 1;

Důležité je, že reference musí být inicializovaná již v místě definice a není možné později znovu určit nebo změnit, na kterou proměnnou reference odkazuje. To je podstatná změno oproti ukazatelům, která činí používání referencí o něco bezpečnější.

int pole[20];

// definice bez inicializace - u reference nelze
int *pole5;

// (pravděpodobně) paměťová chyba, u reference nelze tak snadno
*pole5 = 0;

// inicializace
pole5 = pole + 5;

// totéž jako pole[5] = 1;
*pole5 = 1;

// změna ukazatele, u reference nelze
pole5++;

// totéž jako pole[6] = 2;
*pole5 = 2;

Referenci lze použít také jako návratovou hodnotu funkce. V implementaci funkce vrátíme nějakou l-hodnotu, která zůstane platná i po opuštění těla funkce, ne tedy například lokální proměnnou. Ve volajícím kódu lze návratovou hodnotu použít jako běžnou l-hodnotu, takže výsledek funkce lze použít i na levé straně přiřazení.

int data[10];

int & vektor(int index) {
  // Tady můžeme ošetřit meze polí.
  return data[index];
}

int main(void) {
  vektor(5) = 7;
  vektor(3) = vektor(5);
  return 0;
}

Kdybychom se pokusili ve funkci vektor vrátit například číselnou konstantu, došlo by k chybě při překladu funkce vektor. Stejně tak by selhal překlad main, kdybychom návratový typ funkce vektor deklarovali jako běžný int a nikoli referenci.

Je zřejmé, že reference jako návratová hodnota umožní programátorovi implementovat s poměrně jednoduchou syntaxí například bezpečné pole s hlídáním mezí nebo automatickou alokací a podobně. Standardní knihovna C++ skutečně takové nástroje nabízí.

Reference mají oproti ukazatelům ještě některá omezení, která jsme dosud nezmínili. Především nelze definovat ukazatel ani referenci na referenci, zatímco ukazatel na ukazatel se běžně používá a má rozumný smysl. Rovněž nelze definovat referenci na void ani pole referencí.

Z pohledu konzervativního céčkaře přemýšlejícího na úrovni polidštěného asembleru nejspíš reference zůstane bude jen zakukleným ukazatelem s omezenými možnostmi použití, který jen ztěžuje čitelnost programu. Pohled objektově orientovaného programátora, fanouška C++ a především jeho standardní knihovny, bude nejspíš zcela opačný.

Přetěžování funkcí

C++ umožňuje definovat více funkcí se stejným jménem, pokud se odlišují svými parametry. V místě volání překladač automaticky vybere funkci, která s použitím případných implicitních konverzí vyhovuje sadě parametrů. Je-li takových funkcí více, vybere tu, kde jsou konverze nejjednodušší. Teprve pokud ani zde není funkce určená jednoznačně, dojde k chybě. Norma C++ výběr popisuje poměrně striktně, nicméně spíše než detailní studium normy bych doporučil psát kód tak, aby výběr funkce nezávisel například na tom, zda je nějaký parametr typu const char * nebo char *.

#include <stdio.h>

void vypis(int i) {
  printf("%i\n", i);
}

void vypis(const char *s) {
  puts(s);
}

int main(void) {
  vypis(1);
  vypis("text");
  return 0;
}

Implicitní hodnoty parametrů

V C++ je možné definovat implicitní hodnotu jednoho nebo několika posledních parametrů funkce. Pokud nejdou uvedeny, překladač je automaticky doplní na implicitní hodnotu.

#include <stdio.h>
#include <math.h>

double logaritmus(double x, double zaklad = 10) {
  return log10(x) / log10(zaklad);
}

void vypis(const char *s, FILE *f = stdout) {
  fputs(s, f);
}


int main(void) {
  char s[64];

  sprintf(s, "%f, %f\n", logaritmus(100), logaritmus(100, 100));
  vypis(s);
  vypis(s, stderr);
  return 0;
}

Pokud má funkce uvedenou hlavičku v .h souboru, uvádí se hodnota implicitního parametru pouze tam.

// soubor.h
// ...
double logaritmus(double x, double zaklad = 10);
// ...

Implementace je potom již stejná, jako kdyby byly oba parametry povinné.

// soubor.cpp
// ...
double logaritmus(double x, double zaklad) {
  return log10(x) / log10(zaklad);
}
// ...

Je to logické, neboť implicitní parametr se je implementován na úrovni volaného kódu.

Nepoužité parametry

V matematice, stejně jako v C++ lze a občas i má rozumný smysl definovat funkci, která na nějakém svém parametru nezávisí. V obou případech je dobré zdůraznit, že opravdu víme, co děláme a že ten nevyužitý parametr opravdu potřebujeme. Pokud to neuděláme, hrozí v matematice zmatení kolegů a v C++ varování překladače o zbytečném parametru. V C++ stačí neuvést jméno parametru.

void funkce(int) {
}

Nevyužitý parametr má smysl například pokud potřebujeme rozlišit přetíženou funkci nebo pokud předáváme nějaké knihovně ukazatel na naší funkci zpětného volání, která má (pro naše potřeby) zbytečně obecný prototyp.

Domácí úkoly

Pokračování příště

Příště se podíváme na prostory jmen.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1101