Postaram się odpowiedzieć w miarę przekrojowo, trochę zachodząc też na stylistykę. Ogólnie parametry funkcji można podzielić na dwa rodzaje, parametry które nie zmieniają swojej wartości po skończeniu działania funkcji oraz parametry które zmieniają swoją wartość.
Przy parametrach nie zmieniających swojej wartości masz dwie możliwości. Możesz przekazać parametr robiąc kopię:
int fun(Object o);
wtedy cały obiekt jest kopiowany, korzystając z jego konstruktora kopiującego. Jeżeli nie chcesz robić kopii, wtedy używa się stałych referencji w postaci:
int fun(const Object& o);
wtedy kopia się nie robi, a nadal nie modyfikujesz obiektu. Ogólnie pierwsza wersja jest stosowana gdy obiekty są małe (np. typy wbudowane, można przyjąć, że do około 4 słów), lub zwracasz w tej funkcji nowy obiekt będący zmodyfikowanym parametrem jak parametr powinien być w dalszej części programu (po wywołaniu funkcji) niemodyfikowany:
Object fun(Object o){
o.modify();
return o;
}
Jest to czytelniejsze niż alternatywne:
Object fun(const Object& o){
Object temp=o;
temp.modify();
return temp;
}
Wydaje się, że takie rozwiązanie może powodować spadek wydajności (kopiowanie jako parametr, następnie kolejne kopiowanie przy zwracaniu), ale kompilator te wszystkie kopiowania może zoptymalizować, problem ten zupełnie znika z wprowadzonym w najnowszym standardzie konstruktorem przenoszącym.
W przypadku, gdy zmienna powinna zostać zmieniona w funkcji także są dwie możliwości. Pierwszą i według mnie lepszą jest przekazywanie przez wskaźnik. Obiekt przekazujesz przez jawne wyłuskanie adresu za pomocą operatora &. Wtedy deklaracja i wywołanie funkcji wyglądają tak:
void fun(Object * o){
o->modify();
Object& o2=*o;
o2.modify2();
}
fun(&obj);
Alternatywnie możesz przekazywać obiekt przez referencję, wywołanie i deklaracja funkcji wyglądają wtedy tak:
void fun(Object& o){
o.modify();
}
fun(obj);
Może się wydawać, że takie rozwiązanie jest prostsze, znajdzie ono też zwolenników wśród ludzi, którzy spędzili dużo czasu nad debugowaniem scanfa (BTW tam to nie jest problem z przekazywaniem przez adres, tylko z brakiem kontroli typów). Główną wadą tego rozwiązania jest jednak to, że w miejscu wywołania funkcji nie wiesz, które zmienne uległy zmianie. Wymaga to konsultowania się z deklaracją funkcji lub pamiętania jej przy każdej pracy z kodem, który używa tej konwencji. Dla porównania powiedzmy że mamy dwie funkcje:
void fun1(int, int, int&);
void fun2(int, int, int*);
Wtedy wywołania wyglądają tak:
fun1(a,b,c);
fun2(a,b,&c);
Trzymając się tej konwencji "przez wskaźnik" ułatwiasz pracę innym programistom i zmniejszasz komplikację programu. Wspomniałem o metodzie przekazywania parametrów przez referencję, bo jest jedno miejsce gdzie się ich używa, a mianowicie przeciążanie operatorów. Przykładowo jeżeli przeciążasz operator << dla strumienia i swojego obiektu, to musisz użyć referencji. Przeciążanie operatorów było w ogóle powodem wprowadzenia referencji do języka.
Dodam jeszcze kilka słów o kolejności parametrów. Ogólnie przyjęte jest umieszczanie parametrów nie modyfikowanych na początku, a zmienianych na końcu (jak w przykładzie porównującym referencje ze wskaźnikami). Niekiedy można spotkać funkcje, które jako pierwszy element przyjmują obiekt zmieniany. Jest to konwencja w miarę częsta, tylko że poprawna dla języka C, nie dla C++. Używa się tego głównie przy programowaniu obiektowym w C. Przykładem tego rozwiązania może być np. fprintf, którego deklaracja wygląda tak:
int fprintf ( FILE * stream, const char * format, ... );
którą można luźno przetłumaczyć na wywołanie funkcji klasy file na obiekcie stream z dalszymi parametrami. Nie ma potrzeby stosowania takiego rozwiązania w C++, ponieważ można jawnie zrobić obiekt.
Ostatnią rzeczą o której chce wspomnieć jest radzenie sobie z kontenerami. Powiedzmy, że chcesz zrobić rodzinę funkcji na vector<int>, np. następujące funkcje: dodającą liczbę do każdego elementu, mnożących każdy element przez liczbę, połączenie dwóch poprzednich. Wtedy, w ramach konsekwencji wydaje się, że najprostszym rozwiązaniem jest napisanie tego w taki sposób:
typedef std::vector<int> VecInt;
void vecAdd(VecInt& vi, int add);
void vecMul(VecInt& vi, int mul);
void vecAddAndMul(VecInt& vi, int add, int mul);
co przeczy wcześniej wysuniętym przeze mnie tezom (gdybym miał zrobić dokładnie takie funkcję, zrobiłbym to właśnie tak). Tylko, że tak nie powinno się używać kontenerów w C++. W języku w pełni obiektowym, żeby zrobić te funkcję ogólne dla różnych kontenerów, można by zrobić funkcję (czy bardziej metodę), która pobiera jako parametr klasę bazową wszystkich kontenerów. W bibliotece standardowej C++ kontenery nie posiadają jawnej klasy bazowej (nie mówię o szczegółach implementacji, tylko o standardzie). Destruktory kontenerów są niewirtualne, co powoduje, że dziedziczenie po nich jest bardzo nie zalecane. Więc jak to zrobić ładnie? Biblioteka standardowa podsuwa rozwiązanie - iteratory. W std są one używane w powiązaniu z szablonami, ale nic nie stoi na przeszkodzie, żeby używać ich bez znajomości tego konceptu (choć nie zrobisz wtedy funkcji ogólnych dla różnych kontenerów). Iteratory są przekazywane przez wartość, czyli można być zgodnym ze wcześniej podanymi zasadami, zachowując czytelność jak w przykładzie powyżej. Wymienione funkcje, z implementacją wyglądały by tak:
typedef std::vector<int> VecInt;
typedef std::vector<int>::iterator VecIntIt;
void vecAdd(VecIntIt beg, VecIntIt end, int add){
while (beg!=end){
*beg += add;
++beg;
}
}
void vecMul(VecIntIt beg, VecIntIt end, int mul){
while(beg!=end){
*beg++ *= mul;
}
}
void vecAddAndMul(VecIntIt beg, VecIntIt end, int add, int mul){
while (beg!=end){
*beg = (*beg+add)*mul;
++beg;
}
}
No to trochę się rozpisałem, mam nadzieje, że rozjaśniło to parę spraw.