Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variadické funkce

Variadické funkce jsou funkce s proměnným počtem parametrů. Variadické funkce mají povinné argumenty, těch je v každém zavolání stejný počet, a volitelné argumenty, jejichž počet se může mezi voláními může lišit. Každá variadická funkce musí mít alespoň jeden povinný argument.

Zjištění počtu a typu volitelných argumentů uvnitř funkce se neděje automaticky, řeší se konvencí. Existují v zásadě tři přístupy.

  • Předpokládáme pevný typ volitelných argumentů, v povinném argumentu předáváme jejich počet.

    //hlavicka
    double sum_of_doubles(int count, ...);  // hlavicka
    
    // volani
    sum_of_doubles(3, 1.2, 2.2, 3.3);  // vysledek je 6.6 
    sum_of_doubles(2, 2.1, 3.3);       // vysledek je 5.4
    
  • Předpokládáme pevný typ argumentů, ale poslední nepovinný argument má speciální hodnotu, například 0.

    //hlavicka
    int sum_of_ints(int first, ...);  // hlavicka
    
    // volani
    sum_of_ints(1, 2, 3, 0);  // vysledek je 6
    sum_of_ints(2, 2, 0);     // vysledek je 4
    
  • Informaci o typech i počtu volitelných argumentů předáme pomocí povinných argumentů. Tento přístup používá například funkce printf.

K nepovinným argumentům přistupujeme pomocí maker z stdarg.h.

  • typ va_list pro udržování nezpracovaných argumentů,
  • va_start nastaví va_list na všechny volitelné argumenty,
  • va_arg vrátí hodnotu prvního argumentu ve va_list, potřebuje znát typ, první argument odstraní z va_list.
  • va_end ukončí práci s nepovinnými argumenty.

Podrobnosti v následujících příkladech.

double sum_of_doubles(int n, ...) 
{
	va_list args;
	double ret = 0;
	va_start(args, n); // tady se musi predat va_list a posledni povinny argument 
	
	for (int i = 0; i < n; i += 1) 
	{
		ret += va_arg(args, double); // tady se musi predat va_list a typ argumentu
	}
	
	va_end(args);  // ukonceni prace s argumenty
	return ret;
}

//
// nutno volat aspon dvema argumenty !!!
// jinak UB
//
int sum_of_ints(int first, ...) 
{
	va_list args;
	int ret = first;
	va_start(args, first);

	while (1) 
	{
		int val = va_arg(args, int);
		if (!val) break;
		ret += val;
	}
	
	va_end(args);
	return ret;
}

void almost_printf(char *format, ...) 
{
	va_list args;
	int index = 0;
	va_start(args, format);
	
	while(format[index]) 
	{
		switch(format[index]) 
		{
			case 'i':
				printf("%i ", va_arg(args,int));
				break;
			case 'd':
				printf("%f ", va_arg(args,double));
				break;
		}
		index += 1;
	}

	va_end(args);
}

Při předávání variadických argumentů dochází k automatické konverzi typů: float je konvertován na double, u celočíselných typů dochází k celočíselné promoci.

Pointery na funkce

S pomocí pointerů na funkce lze v omezené míře pracovat s funkcemi vyššího řádu. Pointer na funkci sebou nese i typové informace: typ návratové hodnoty a počet a typy argumentů. Zápis tohoto typu podobný jako zápis hlavičky funkce bez jmen argumentů, pouze jméno funkce nahradíme (*).

int (*)(int*, int)   // typ pointer na fci, ktera vraci int, 
	                 // jeji argumenty jsou typu int* a int.

Při definici proměnné se identifikátor píše dovnitř závorky za *.

int (*fce_ptr)(int*, int);   // definice pointeru 

Předchozí syntax je trochu nešikovná, proto se často používá typedef.

typedef int (*p_fun)(int* , int); // typ se jmenuje p_fun
p_fun ptr;   // definice promenne

nebo alternativně

typedef int fun_type(int *, int); // definujeme typ pro funkci, nikoliv pointer na funkci
fun_type* ptr;

S pointery na funkce nelze provádět aritmetiku, ani aplikovat operátory adresy a dereference (resp, tyto operátory nedělají nic). Můžeme ovšem funkci volat, syntax volání je přitom stejná jako u běžného volání funkce.

Podobně jako u polí, jméno funkce degeneruje na pointer na funkci.

typedef int p_fun(int* , int);

int array_sum(int *array, int n) {
	int r = 0;
	for (int i = 0; i < n; i += 1) {
		r += array[i];
	}
	return r;
}

int array_product(int *array, int n) {
	int r = 1;
	for (int i = 0; i < n; i += 1) {
		r *= array[i];
	}
	return r;	
}

int main() {
	int a[] = { 1, 2, 3, 4, 5 };
	int n = sizeof(a)/sizeof(*a);
	
	p_fun* reduce = array_sum;      // reduce ukazuje na funkci array_sum
	printf("%i\n", reduce(a, n));   // vytiskne 15, vola array_sum
	reduce = array_product;
	printf("%i\n", reduce(a, n));   // vytiskne 120, vola array_product
	return 0;
}

S pointery na funkce lze pracovat jako s jinými typy v C. Lze tvořit jejich pole, předávat je do funkcí jako argumenty, mohou být návratovými hodnotami funkcí atd.

Pointer na funkci může být neplatný, v tom případě vede pokus o volání k nedefinovanému chování. Pointeru na funkci je možné přiřadit 0 a pointery na funkce lze porovnávat.

Jedno z využití pointerů na funkce je tvorba analogie generických funkcí známých z jiných jazyků. Ideou je, že funkci naprogramujeme jednou a ona poté pracuje pro více typů. Ukážeme si to na příkladu binárního vyhledávání.

typedef int compare_fn(void *, void *);  // argumenty jsou void*, aby byla funkce obecna

int compare_int(void *a, void *b) 
{ 
    int *aa = a; // tuto funkci budeme volat pouze s int*,
    int *bb = b; // muzeme bezpecne pretypovat

    if (*aa < *bb) return -1;
    if (*bb < *aa) return 1;

    return 0;
}

int compare_long(void *a, void *b) 
{
    long* aa = a;
    long* bb = b;

    if (*aa < *bb) return -1;
    if (*bb < *aa) return 1;

    return 0;
}

//
//  genericka funkce pro binarni vyhledavani
//

int binary_search(void *array,              // pole, ve kterem vyhledavame
                  void *key,                // ukazatel na hodnotu klice
                  size_t n,                 // pocet prvku pole
                  size_t element_size,      // velikost jednoho prvku v bajtech
                  compare_fn* cmp)          // funkce pro porovnani dvou prvku
{

	unsigned char *bytes = array;

	size_t lo = 0;
	size_t hi = n;          // polouzavreny interval [lo, hi)

	while (lo < hi)
	{
		size_t mid = lo + (hi - lo) / 2;

		int cmp_result = cmp(key, bytes + mid * element_size);

		if (cmp_result == 0)     { return (int)mid; }
		else if (cmp_result > 0) { lo = mid + 1; }
		else                     { hi = mid;     }
	}

	return -1;
}

//
//  pro jednotlive typy vytvorem funkce tvorici rozhrani
//
int binary_search_int(int* array, int key, size_t n) 
{
	return binary_search(array, &key, n, sizeof(int), compare_int);
}

int binary_search_long(long* array, long key, size_t n) 
{
	return binary_search(array, &key, n, sizeof(long), compare_long);
}

int main() 
{
	int array_int[20];
	long array_long[20];

	for(int i = 0; i < 20; i += 1) 
	{
		array_int[i] = i;
		array_long[i] = i;
	}

	int key_int_ok = 10;
	int key_int_fail = 30;

	long key_long_ok = 5;
	long key_long_fail = -3;

	printf("index of %i in array_int is %i\n",
		    key_int_ok,
		    binary_search_int(array_int, key_int_ok, 20));

	printf("index of %i in array_int is %i\n",
		   key_int_fail,
           binary_search_int(array_int, key_int_fail, 20));

	printf("index of %li in array_long is %i\n",
           key_long_ok,
           binary_search_long(array_long, key_long_ok, 20));

	printf("index of %li in array_long is %i\n",
           key_long_fail,
           binary_search_long(array_long, key_long_fail, 20));
		 
	return 0;
}

Úkoly

  1. Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.

  2. Napište funkci long double prumer(char* format, ...), která výpočítá aritmetický průměr ze zadaných hodnot různých datových typů. Typy předávaných hodnot jsou určeny pomocí parametru format, který může tvořit libovolná posloupnost znaků odpovídající typům následujících parametrů - i pro typ int, d pro typ double a l pro long double.

  3. Napište funkci double* map(double (*fce)(double), double* input, int len), která na hodnoty pole input aplikuje funkci fce a vrátí pole výsledných hodnot. Velikost pole input parametrem len.

  4. Napište funkci double akumulator(double (*fce)(double, double), double numbers[], int len), která zpracuje pomocí předané funkce fce hodnoty z pole numbers, jehož velikost je dána parametrem len. Můžete předpokládat, že len je vždy alespoň 2.

  5. S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.

  6. Do následujícího kódu doprogramujte generickou funkci pro třídění a s její pomocí funkce pro třídění zlomků a unsigned. (Můžete si doprogramovat libovolné pomocné funkce.) Je zakázáno použít třídicí funkci ze standardní knihovny.

    typedef struct
    {
        int numerator;
        int denominator;
    } Fraction;
    
    typedef int compare_fn(void*, void*);
    
    int compare_fraction(void *a, void *b)
    {
        // TODO
    }
    
    int compare_unsigned(void *a, void *b)
    {
        // TODO
    }
    
    void generic_sort(void *array, size_t n, size_t element_size, compare_fn* cmp)
    {
        // TODO
    }
    
    void fraction_sort(Fraction *array, int n)
    {
        // TODO
    }
    
    void unsigned_sort(unsigned int* array, int n)
    {
        // TODO
    }
    
  7. Naprogramujte vlastní verzi funkce printf, která s podporou tiskových direktiv %i, %f a %z, kde poslední direktiva vede k tisku zlomku. Při řešení lze využít funkci printf.

  8. Naprogramujte jednoduchou kalkulačku, která operaci vybírá indexem do pole pointerů na funkce, nikoliv pomocí switch nebo řetězu if.

    typedef enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_COUNT } Operation;
    
    double apply(Operation op, double a, double b);
    

    Funkci apply napište tak, aby obsahovala pole OP_COUNT pointerů na funkce a operaci provedla jediným zavoláním. Ošetřete dělení nulou. Porovnejte výsledný kód s variantou napsanou pomocí switch a rozmyslete si, kdy je která přehlednější.

  9. Naprogramujte funkci

    int filter(int *input, int n, int *output, int (*predicate)(int));
    

    která do pole output zkopíruje ty prvky input, pro které predicate vrátí nenulovou hodnotu, a vrátí jejich počet. Otestujte ji s predikáty pro sudost, prvočíselnost a zápornost.

  10. Naprogramujte funkci

    typedef double binary_op(double, double);
    
    binary_op *get_operation(char symbol);
    

    která pro znak '+', '-', '*' a '/' vrátí pointer na odpovídající funkci a pro jiný znak vrátí 0. Volající musí návratovou hodnotu zkontrolovat, než ji zavolá.

Řešení úkolů

Úkol 1

Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.

typedef struct
{
	double re;
	double im;
} Complex;

//
// prvni pristup: pocet volitelnych argumentu je v povinnem argumentu
//
Complex complex_sum(int count, ...)
{
	Complex ret = { 0, 0 };
	va_list args;
	va_start(args, count);

	for (int i = 0; i < count; i += 1)
	{
		Complex c = va_arg(args, Complex);
		ret.re += c.re;
		ret.im += c.im;
	}

	va_end(args);
	return ret;
}

Volání využívá složených literálů:

Complex s = complex_sum(3, (Complex){1,2}, (Complex){3,-1}, (Complex){0.5,0.5});
// s je 4.5 + 1.5i

Strukturu lze variadickou funkcí předat, protože se na ni nevztahují automatické konverze — do va_arg se předá přesně ten typ, který jsme poslali. U typů menších než int nebo u float by to takto nefungovalo, viz konec sekce o variadických funkcích.

Kdyby uživatel předal jiný počet argumentů, než slibuje count, šlo by o nedefinované chování. Variadická funkce nemá jak si to ověřit.

Úkol 2

Napište funkci long double prumer(char *format, ...), kde i značí int, d značí double a l značí long double.

long double prumer(char *format, ...)
{
	long double sum = 0;
	int         n   = 0;

	va_list args;
	va_start(args, format);

	for (int i = 0; format[i]; i += 1)
	{
		switch (format[i])
		{
			case 'i': sum += (long double)va_arg(args, int);    n += 1; break;
			case 'd': sum += (long double)va_arg(args, double); n += 1; break;
			case 'l': sum += va_arg(args, long double);         n += 1; break;
			default: break;     // neznamy znak ignorujeme
		}
	}

	va_end(args);
	return n ? sum / n : 0;
}

// prumer("idl", 2, 3.0, (long double)4.0)  ->  3.0

Typ ve va_arg musí přesně odpovídat tomu, co bylo předáno. Kdybychom u 'l' napsali va_arg(args, double), přečetli bychom nesprávný počet bajtů a všechny další argumenty by se rozsypaly.

Při volání je literál 4.0 typu double, nikoliv long double. Pro direktivu 'l' je potřeba psát 4.0L nebo přetypovat.

Úkol 3

Napište funkci double *map(double (*fce)(double), double *input, int len).

double *map(double (*fce)(double), double *input, int len)
{
	double *out = malloc((size_t)len * sizeof(double));
	assert(out);

	for (int i = 0; i < len; i += 1) { out[i] = fce(input[i]); }

	return out;
}

// pouziti
static double twice(double x) { return 2 * x; }

double in[] = { 1, 2, 3 };
double *out = map(twice, in, 3);     // { 2, 4, 6 }
free(out);                           // uvolnuje volajici!

Zápis double (*fce)(double) je pointer na funkci, která bere i vrací double. Závorky kolem *fce jsou nutné — double *fce(double) by byla deklarace funkce vracející double*.

Při volání se píše jenom jméno funkce, bez &: jméno funkce degeneruje na pointer stejně jako jméno pole.

Funkce alokuje nové pole, takže ho volající musí uvolnit. Alternativou by bylo nechat cílové pole předat jako parametr.

Úkol 4

Napište funkci double akumulator(double (*fce)(double, double), double numbers[], int len).

double akumulator(double (*fce)(double, double), double numbers[], int len)
{
	double acc = numbers[0];
	for (int i = 1; i < len; i += 1) { acc = fce(acc, numbers[i]); }
	return acc;
}

static double add(double a, double b)  { return a + b; }
static double maxd(double a, double b) { return a > b ? a : b; }

double nums[] = { 3, 1, 4, 1, 5 };
akumulator(add,  nums, 5);    // 14
akumulator(maxd, nums, 5);    // 5

Jde o klasickou operaci fold: první prvek je počáteční hodnota a funkce se postupně aplikuje na dosavadní výsledek a další prvek. Právě proto zadání říká, že len je aspoň 2 — pro prázdné pole bychom neměli co vrátit a museli bychom počáteční hodnotu dostat jako další parametr.

Úkol 5

S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.

Generická funkce binary_search z výkladu zůstává beze změny. Stačí dodat porovnávací funkci a obal:

int compare_fraction(void *a, void *b)
{
	Fraction *x = a;
	Fraction *y = b;

	//
	// x/xd < y/yd  prave kdyz  x*yd < y*xd  (kladne jmenovatele)
	// nasobime v long, aby soucin nepretekl
	//
	long left  = (long)x->numerator * y->denominator;
	long right = (long)y->numerator * x->denominator;

	return (left > right) - (left < right);
}

int binary_search_fraction(Fraction *array, Fraction key, size_t n)
{
	return binary_search(array, &key, n, sizeof(Fraction), compare_fraction);
}

Zlomky nelze porovnávat po položkách; musíme je převést na společného jmenovatele. Křížové násobení je přesnější než převod na double, který by u velkých čísel ztrácel přesnost.

Výraz (left > right) - (left < right) vrací −1, 0 nebo 1 bez větvení. Zápis return left - right; by byl chybný: rozdíl může přetéct.

Přetypování na long před násobením je podstatné. (long)(x->numerator* y->denominator) by nepomohlo — součin by se spočítal v int a přetekl by ještě před konverzí.

Klíč se předává adresou (&key), protože binary_search pracuje s void*. Parametr key je lokální kopie, takže je to bezpečné.

Úkol 6

Doprogramujte generickou funkci pro třídění a s její pomocí funkce pro třídění zlomků a unsigned.

void generic_sort(void *array, size_t n, size_t element_size, compare_fn *cmp)
{
	unsigned char *bytes = array;

	//
	// jeden prvek si musime umet odlozit, velikost zname az za behu
	//
	unsigned char *tmp = malloc(element_size);
	assert(tmp);

	//
	// insertion sort
	//
	for (size_t i = 1; i < n; i += 1)
	{
		memcpy(tmp, bytes + i * element_size, element_size);

		size_t j = i;
		while (j > 0 && cmp(bytes + (j-1) * element_size, tmp) > 0)
		{
			memcpy(bytes + j * element_size, bytes + (j-1) * element_size, element_size);
			j -= 1;
		}

		memcpy(bytes + j * element_size, tmp, element_size);
	}

	free(tmp);
}

int compare_unsigned(void *a, void *b)
{
	unsigned int x = *(unsigned int*)a;
	unsigned int y = *(unsigned int*)b;
	return (x > y) - (x < y);
}

void fraction_sort(Fraction *array, int n)
{
	generic_sort(array, (size_t)n, sizeof(Fraction), compare_fraction);
}

void unsigned_sort(unsigned int *array, int n)
{
	generic_sort(array, (size_t)n, sizeof(unsigned int), compare_unsigned);
}

Protože velikost prvku známe až za běhu, nemůžeme si odkládací proměnnou deklarovat — musíme ji alokovat. Prohazování prvků probíhá přes memcpy nad bajty.

U compare_unsigned nelze psát return x - y;. Odčítání dvou unsigned int je modulární, takže pro x < y vyjde obrovské kladné číslo a třídění by bylo úplně špatně.

Poznámka k typu compare_fn. V zadání je definován jako typ funkce, nikoliv pointeru:

typedef int compare_fn(void*, void*);   // typ funkce
compare_fn *cmp;                        // proto se pise hvezdicka

Ve výkladu se objevuje i varianta typedef int (*compare_fn)(void*, void*), kde už je pointer součástí typu a píše se pak jenom compare_fn cmp. Obojí je správně, jenom je potřeba vědět, který zápis zrovna používáme.

Úkol 7

Naprogramujte vlastní verzi printf s podporou direktiv %i, %f a %z, kde poslední vede k tisku zlomku.

typedef struct { int numerator; int denominator; } Fraction;

void my_printf(char *format, ...)
{
	va_list args;
	va_start(args, format);

	for (int i = 0; format[i]; i += 1)
	{
		if (format[i] != '%') { putchar(format[i]); continue; }

		i += 1;    // preskocime '%'
		switch (format[i])
		{
			case 'i':
				printf("%i", va_arg(args, int));
				break;

			case 'f':
				printf("%f", va_arg(args, double));
				break;

			case 'z':
			{
				Fraction f = va_arg(args, Fraction);
				printf("%i/%i", f.numerator, f.denominator);
				break;
			}

			case '%':
				putchar('%');
				break;

			case 0:
				//
				// format koncil znakem '%', vratime se
				// aby vnejsi cyklus korektne skoncil
				//
				i -= 1;
				break;

			default:
				putchar('%');
				putchar(format[i]);
				break;
		}
	}
	va_end(args);
}

Direktivy %i a %f jenom předáme skutečnému printf, jak zadání dovoluje. Zajímavé je %z, kde si strukturu vytáhneme a naformátujeme sami.

Případ case 0 je snadné přehlédnout: pokud řetězec končí osamoceným %, zvýšili jsme i za konec řetězce a bez ošetření bychom četli mimo pole.

Větev pro %f je dobrou ilustrací automatických konverzí: i kdyby volající předal float, přijde do funkce jako double, takže va_arg(args, double) je správně a va_arg(args, float) by bylo špatně.

Úkol 8

Naprogramujte kalkulačku, která operaci vybírá indexem do pole pointerů na funkce, nikoliv pomocí switch nebo řetězu if.

static double op_add(double a, double b) { return a + b; }
static double op_sub(double a, double b) { return a - b; }
static double op_mul(double a, double b) { return a * b; }
static double op_div(double a, double b) { return b == 0.0 ? 0.0 : a / b; }

double apply(Operation op, double a, double b)
{
	//
	// poradi prvku MUSI odpovidat poradi konstant ve vyctu
	//
	static double (*table[OP_COUNT])(double, double) = { op_add, op_sub, op_mul, op_div };

	assert(op >= 0 && op < OP_COUNT);
	return table[op](a, b);
}

Zápis double (*table[OP_COUNT])(double, double) se čte zevnitř ven: table je pole OP_COUNT pointerů na funkci beroucí dva double a vracející double. Závorky kolem *table[...] jsou nutné.

Pole je static, aby se neinicializovalo znovu při každém volání. Jeho velikost je OP_COUNT, takže po přidání nové operace do výčtu překladač ohlásí, že v inicializátoru chybí prvek.

Dělení nulou ošetřujeme uvnitř op_div. Celočíselné dělení nulou je nedefinované chování; u double bychom sice dostali inf, ale vracet nulu je tu čitelnější.

Kontrola assert je podstatná: op je typu Operation, ale nic nebrání někomu přiřadit do něj (Operation)99 a index mimo pole by byl nedefinované chování.

Varianta se switch by byla zhruba stejně dlouhá a pro čtyři operace možná i čitelnější. Tabulka vyhrává tehdy, když je operací hodně, když se sestavuje za běhu, nebo když se má dát rozšířit bez zásahu do apply.

Úkol 9

Naprogramujte funkci filter s predikátem předaným jako pointer na funkci.

int filter(int *input, int n, int *output, int (*predicate)(int))
{
	int count = 0;
	for (int i = 0; i < n; i += 1)
	{
		if (predicate(input[i]))
		{
			output[count] = input[i];
			count += 1;
		}
	}
	return count;
}

Predikáty pro otestování:

static int is_even(int x)     { return x % 2 == 0; }
static int is_negative(int x) { return x < 0; }

static int is_prime(int x)
{
	if (x < 2) { return 0; }
	for (int d = 2; d * d <= x; d += 1) { if (x % d == 0) { return 0; } }
	return 1;
}

Index i prochází vstup, count ukazuje na první volné místo ve výstupu — a protože se zvyšuje jenom při úspěchu, jsou výsledky ve výstupním poli za sebou bez mezer.

Zápis if (predicate(...)) stačí; porovnávat s nulou není potřeba, protože zadání mluví o „nenulové hodnotě“, což je přesně to, co if testuje.

Alokaci výstupního pole necháváme na volajícím, takže funkce sama nealokuje ani neuvolňuje. Volající musí zajistit, že se do output vejde až n prvků.

Úkol 10

Naprogramujte funkci get_operation, která vrací pointer na funkci.

typedef double binary_op(double, double);

binary_op *get_operation(char symbol)
{
	switch (symbol)
	{
		case '+': return op_add;
		case '-': return op_sub;
		case '*': return op_mul;
		case '/': return op_div;
		default:  return 0;
	}
}

Použití — návratovou hodnotu je nutné zkontrolovat, než ji zavoláme:

binary_op *f = get_operation('*');
if (f) { printf("%f\n", f(6, 7)); }     // vytiskne 42.000000
else   { fprintf(stderr, "neznamy operator\n"); }

Všimněte si, jak se typedef vyplatil. Bez něj by hlavička vypadala

double (*get_operation(char symbol))(double, double);

což je zápis, který se čte velmi obtížně: get_operation je funkce beroucí char a vracející pointer na funkci dvou double vracející double. S typedef je z toho čitelné binary_op *get_operation(char).

Zde je binary_op typ funkce, nikoliv pointeru — proto se u návratového typu píše hvězdička. Kapitola ukazuje i druhou možnost, kde je pointer součástí typu:

typedef double (*binary_op_ptr)(double, double);
binary_op_ptr get_operation(char symbol);    // uz bez hvezdicky

Obojí je správně, jen je potřeba vědět, kterou variantu zrovna používáme.

Vracet 0 pro neznámý znak je tatáž konvence jako u neplatného pointeru na data: nulový pointer na funkci je vždy neplatný a odpovídá nepravdě, takže se dá testovat přímo v if. Zavolat ho by bylo nedefinované chování — proto ta kontrola.