Klasa <vector> jako argument funkcji

0

Witam, chciałbym zrobić tak że przekazuje jako parametr funkcji tablicę utworzoną za pomocą klasy <vector> a następnie 2 warianty

  1. zmieniamy jakąś wartość w tej tablicy (wiadomo jak) i zwracamy ją - przypisując w main() do innej tablicy vectorowej (nie ważne jaki ma to sens, po prostu chcę aby to działało. W pseudokodzie wygląda to tak:
foo(vector tablica)
{
    //operacja na tablica
    return tablica;
}

int main()
{
   vector tablica1, tablica 2;
   tablica2 = foo(tablica1);
}

Tyle że problem stanowią gwiazdki i te '&' - gdzie to i jak umieścić? Bo moje kombinacje niestety wypluwają errory. Ze zwykłymi tablicami jakoś idzie ale z tymi vectorowymi już nie bardzo.

  1. Podobnie tyle że przekazujemy tablice, zmieniamy ja tak aby i poza funkcją foo była zmieniona (to się chyba nazywa przez referencję) i nic nie zwracamy.

Głównie chodzi mi pokazanie na przykładzie kodu jak to może wyglądać i wyjaśnienie kiedy w argumentach funkcji piszemy np.

int foo(vector <int*> costam)
//a kiedy
int foo(vector <int> &costam)
//a kiedy jeszcze
int * foo(//powyzsze ) 

Pozdrawiam

1

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.

0

Owszem trochę rozjaśniło parę rzeczy ale nie do końca znalazłem odpowiedzi na wszystkie pytania. Tak czy siak dostaniesz ode mnie + jak już temat będzie do zamknięcia. Takie iteratory to jeszcze nie moja liga, poza tym może nie dokładnie przeczytałem ale użyłeś tam czegoś takiego jak np

void vecMul(VecInt& vi, int mul);

a wcześniej napisałeś że raczej powinno się robić tak

void vecMul(VecInt* vi, int mul);

bo wtedy
[quote]
ułatwiasz pracę innym programistom i zmniejszasz komplikację programu
[/quote]

Ale tak BTW, to jak jest z takimi tablicami, przecież tam chyba nie trzeba dodawać & przy wywołaniu bo sama nazwa tablicy jest jej adresem. I tu przy takich wektorach jest już sieka, jak wywołać taką funkcję, jak ją zwrócić do innego wektora? (na podstawie mojego przypadku np)

0

Po prostu użyłem tego idiomu "obiektowość w C", wtedy jest jasne, że te funkcje zachowują się analogicznie jak metody, więc w takim przypadku można by użyć referencji jak już musisz tak robić (upraszcza to sam kod funkcji, nie musisz ubierać tego w referencję (vector<int>& viref=*vi), ani pisać potworków w stylu (*vi)[idx]). W tym przypadku co opisałeś (zwracanie nowego zmodyfikowanego wektora) to zrobiłbym taką funkcję:

VecInt getBetterVector(VecInt vi){
   for (VecIntIt it=vi.begin(); it!=vi.end(); ++it){
      *it *= 2;
   }
   return vi;
}
VecInt org;
//...
VecInt better = getBetterVector(org);
    
0

Nom ok i czemu teraz te 2 gwiazdki mają być? Utworzyliśmy teraz kopię obiektu tak? W tej funkcji. Bo gwiazdki można użyć tylko chyba na adresie żeby odwołać się do true zmiennej,

0

Iterator to coś takiego jak wskaźnik, ma przeładowany operator *. Powinieneś się z nimi zapoznać jak najszybciej, bo tracisz praktycznie całą bibliotekę standardową. Gdybym w tej funkcji używał size(), wtedy wyglądałaby ona tak:

VecInt getBetterVector(VecInt vi){
   for (size_t i = 0; i<vi.size(); ++i){
      vi[i] *= 2;
   }
   return vi;
}
1
MateuszS napisał(a)

Takie iteratory to jeszcze nie moja liga

Nikt nie każe Ci pisać iteratorów, masz je tylko wykorzystać.
Takie podejście to poważny błąd - iteratory to jeden z podstawowych konceptów w bibliotece standardowej. Skoro chcesz pisać w C++ to się ucz C++ a nie C w kompilatorze C++.

0

No dobrze, ale nie mogę tak od środka zaczynać. Póki co mam problem z takimi rzeczami jak przekazywanie tablic do funkcji - a konkretnie wektorów. Jak widać różni się to znacząco od przesyłania zwykłej tablicy. Ledwo wczoraj zacząłem wektory ku radzie z tego forum (bardzo wygodne narzędzie - dzięki). Myślę że najpierw muszę się nauczyć robić to nawet za pomocą C w kompilatorze C++ bo na egzaminie pewnie taka metoda będzie obowiązywać. Nie da się tego przykładu zrobić tak jak napisałem w pierwszym poście w pseudokodzie? Wzbogacając go o gwiazdki wskaźnikowe w odpowiednich miejscach? Wiem że to jest uwstecznienie, że to nie C++ ale jak już pisałem - po kolei.

Podsumowując. & - adres zmiennej ale przy tablicy już chyba nie musimy tego pisać. Za to przy argumencie funckji dajemy * i po typie funkcji dajemy * jeżeli chcemy zwrócić wskaźnik na wektor tak?

1 użytkowników online, w tym zalogowanych: 0, gości: 1