Zdarzają się funkcje, które nie mają w naturalny sposób określonej, z góry znanej liczby argumentów. Jako przykład możemy wyobrazić sobie funkcję wyznaczająca maksimum z dwóch, trzech, czterech... liczb, albo drukującą wartości pewnej nieustalonej z góry ilości argumentów. W C/C++ istnieje sposób na tworzenie tego rodzaju funkcji, choć jest to nieco zawiłe i trudne w użyciu: można zastosować funkcje o zmiennej liczbie argumentów (ang. variable-length argument list). W C++ zwykle lepiej i prościej zastosować opisany dalej mechanizm przeciążania funkcji lub narzędzia z nowego standardu (np. tak zwane krotki lub listy inicjujące).
Funkcje tego typu deklarujemy ze znakiem wielokropka (' ...') w miejsce parametrów, których liczby nie specyfikujemy. Przed wielokropkiem wymienione są wszystkie „normalne”, obowiązkowe parametry. Na przykład deklaracja
int fun(char* ...);deklaruje funkcję, którą można wywołać z obowiązkowym pierwszym argumentem (typu char*) i dowolną liczbą dodatkowych argumentów. Tę samą deklarację można też zapisać z przecinkiem po ostatnim obowiązkowym argumencie:
int fun(char*, ...);Jak napisać treść funkcji aby prawidłowo odczytać w niej wszystkie przekazane argumenty? Przede wszystkim należy dołączyć plik nagłówkowy cstdarg a następnie, wewnątrz funkcji:
Jako przykład rozpatrzmy program:
1. #include <iostream>
2. #include <cstdarg>
3. using namespace std;
4.
5. void typy(const char typ[] ...);
6.
7. int main() {
8. typy("SxS", "Jan", 0, "Maria");
9. typy("issD", 17, "Jan", "Maria", 1.);
10. typy("iDdsiI", 17, 19.5, 1.5, "OK", -1, 8);
11. }
12.
13. void typy(const char typ[] ...) {
14. int i = 0, integ;
15. char c, *strin;
16. double doubl;
17.
18. va_list ap;
19.
20. va_start(ap,typ);
21.
22. while ( (c = typ[i++]) != '\0') { ➊
23. switch (c) {
24. case 'i':
25. case 'I':
26. integ = va_arg(ap,int);
27. cout << "Liczba int : " << integ << endl;
28. break;
29. case 'd':
30. case 'D':
31. doubl = va_arg(ap,double);
32. cout << "Liczba double: " << doubl << endl;
33. break;
34. case 's':
35. case 'S':
36. strin = va_arg(ap,char*);
37. cout << "Napis : " << strin << endl;
38. break;
39. default:
40. cout << "Nielegalny kod typu!!!!!" << endl;
41. goto KONIEC;
42. }
43. }
44. KONIEC:
45. cout << endl;
46.
47. va_end(ap); ➋
48. }
Pierwszym, obowiązkowym argumentem funkcji typy jest napis, czyli tablica znaków, z których ostatni jest znakiem ' \0'. Kolejne znaki tego napisu określają typy kolejnych argumentów wywoływanej funkcji: 'd' lub 'D' — typ double, 'i' lub 'I' — typ int, 's' lub 'S' — typ char*, czyli wskaźnik do napisu. W ciele funkcji, po wykonaniu kroków 1 i 2 z podanego powyżej schematu postępowania, odczytujemy w pętli while kolejne argumenty. Najpierw (➊) z napisu typ odczytujemy kolejny znak (aż będzie nim znak ' \0'). Znak ten określa typ kolejnego argumentu do wczytania. Znając ten typ, za pomocą instrukcji switch przechodzimy do wczytywania kolejnego argumentu: wczytujemy go do zmiennej roboczej o odpowiednim typie za pomocą wywołania funkcji va_arg (patrz punkt 3 schematu). Jeśli odczytany znak nie odpowiada żadnemu ze spodziewanych typów, wypisywany jest komunikat i za pomocą instrukcji goto przerywane jest wykonywanie zarówno bloku switch, jak i pętli while. Tym niemniej funkcja va_end musi nawet wtedy być wywołana (➋), aby umożliwić „posprzątanie” stosu i kontynuowanie programu. Przykład działania tego programu podany jest poniżej:
Napis : Jan
Nielegalny kod typu!!!
Liczba int : 17
Napis : Jan
Napis : Maria
Liczba double: 1
Liczba int : 17
Liczba double: 19.5
Liczba double: 1.5
Napis : OK
Liczba int : -1
Liczba int : 8
Przy pierwszym wołaniu powstaje błąd, gdyż użyta jest
w napisie
typ
litera 'x', która nie odpowiada żadnemu
typowi. Funkcja kończy swoje działanie, ale porządkuje stos
i dalszy przebieg programu może być prawidłowy.
Ponieważ w deklaracji i definicji funkcji nie jest określony typ parametrów, wyłączona zostaje kontrola zgodności typów dla jej wywołań. Aby uprościć stosowanie funkcji o zmiennej liczbie parametrów, „krótkie” wartości całkowite awansowane są do typu int, a wartości float do typu double. Na przykład w drugim wywołaniu funkcji typy jednym z argumentówr jest '1.'. Gdybyśmy opuścili kropkę dziesiętną, literał odpowiadałby wartości całkowitej 1 i zostałby przekazany jako czterobajtowa wartość typu int. Ta zostałaby następnie odczytana jako double, a więc wartość ośmiobajtowa, co doprowadziłoby do trudno wykrywalnego błędu w programie.
Stosować funkcje o zmiennej liczbie argumentów należy tylko wtedy, gdy naprawdę są potrzebne i robić to trzeba bardzo ostrożnie.
W nowym standardzie C++11 istnieją inne, lepsze metody do przekazywania nieokreślonej z góry liczby danych do funkcji, o których powiemy w dalszej części.
T.R. Werner, 23 lutego 2022; 19:40