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 : 8Przy 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, 21 lutego 2016; 20:17