Jak przekazać obiekty do funkcji w C ++?

249

Jestem nowy w programowaniu w C ++, ale mam doświadczenie w Javie. Potrzebuję wskazówek, jak przekazywać obiekty do funkcji w C ++.

Czy muszę przekazywać wskaźniki, referencje lub wartości inne niż wskaźnik i inne? Pamiętam, że w Javie nie ma takich problemów, ponieważ przekazujemy tylko zmienną, która zawiera odniesienie do obiektów.

Byłoby wspaniale, gdybyś mógł również wyjaśnić, gdzie użyć każdej z tych opcji.

Rakesh K
źródło
6
Z jakiej książki uczysz się C ++?
17
Ta książka jest zdecydowanie nie polecam. Idź do C ++ Primer autorstwa Stan Lippman.
Prasoon Saurav
23
Cóż, jest twój problem. Schildt to w zasadzie cr * p - get Accelerated C ++ firmy Koenig & Moo.
9
Zastanawiam się, jak nikt nie wspomniał o języku programowania C ++ autorstwa Bjarne Stroustrup. Bjarne Stroustrup jest twórcą C ++. Naprawdę dobra książka do nauki języka C ++.
George
15
@George: TC ++ PL nie jest dla początkujących, ale jest uważany za Biblię dla C ++.
XD

Odpowiedzi:

277

Praktyczne zasady dla C ++ 11:

Przekaż wartość , z wyjątkiem kiedy

  1. nie potrzebujesz prawa własności do obiektu, wystarczy prosty alias, w którym to przypadku przekażesz przez constodniesienie ,
  2. musisz mutować obiekt, w takim przypadku użyj pass przez constodwołanie inne niż wartość ,
  3. przekazujesz obiekty klas pochodnych jako klasy bazowe, w takim przypadku musisz przekazać referencję . (Użyj poprzednich reguł, aby określić, czy przekazać przez constodniesienie, czy nie.)

Ominięcie wskaźnika praktycznie nigdy nie jest zalecane. Parametry opcjonalne najlepiej wyrażać jako std::optional( boost::optionaldla starszych bibliotek std), a aliasing jest wykonywany dobrze przez odniesienie.

Semantyka ruchów w C ++ 11 sprawia, że ​​przekazywanie i zwracanie wartości jest o wiele bardziej atrakcyjne, nawet w przypadku złożonych obiektów.


Praktyczne zasady dla C ++ 03:

Przekaż argumenty przez constodniesienie , z wyjątkiem kiedy

  1. należy je zmienić wewnątrz funkcji i takie zmiany powinny zostać odzwierciedlone na zewnątrz, w takim przypadku przechodzisz przez brak constodniesienia
  2. funkcja powinna być możliwa do wywołania bez żadnego argumentu, w takim przypadku należy przekazać wskaźnik, aby użytkownicy mogli przekazać NULL/ 0/ nullptrzamiast tego; zastosuj poprzednią regułę, aby ustalić, czy powinieneś przekazać wskaźnik do constargumentu
  3. są to typy wbudowane, które można przekazać kopią
  4. należy je zmienić wewnątrz funkcji i takie zmiany nie powinny być odzwierciedlone na zewnątrz, w takim przypadku można przekazać przez kopiowanie (alternatywą byłoby przejście zgodnie z poprzednimi zasadami i wykonanie kopii wewnątrz funkcji)

(tutaj „przekazanie przez wartość” nazywa się „przekazanie przez kopię”, ponieważ przekazywanie przez wartość zawsze tworzy kopię w C ++ 03)


Jest coś więcej, ale te zasady dla kilku początkujących zaprowadzą cię dość daleko.

sbi
źródło
17
+1 - Chciałbym również zauważyć, że niektórzy (np. Google) uważają, że obiekty, które zostaną zmienione w ramach funkcji, powinny być przekazywane za pomocą wskaźnika zamiast odwołania non-const. Rozumowanie polega na tym, że gdy adres obiektu jest przekazywany do funkcji, bardziej oczywiste jest, że wspomniana funkcja może go zmienić. Przykład: W przypadku odniesień wywołanie to foo (bar); czy referencja jest stała, czy nie, za pomocą wskaźnika jest to foo (& bar); i jest bardziej widoczne, że foo jest przekazywany jako obiekt zmienny.
RC.
19
@RC Nadal nie mówi, czy wskaźnik jest stały, czy nie. Wytyczne Google przyniosły wiele błędów w różnych społecznościach internetowych C ++ - słusznie, IMHO.
14
Podczas gdy w innych kontekstach Google może być liderem, w C ++ ich styl nie jest tak dobry.
David Rodríguez - dribeas
4
@ArunSaha: Jako przewodnik w czystym stylu, Stroustrup ma przewodnik opracowany dla firmy z branży lotniczej. Przeglądałem przewodnik Google i nie podobało mi się to z kilku powodów. Sutter & Alexandrescu C ++ Standardy kodowania to świetna książka do przeczytania i można uzyskać sporo dobrych porad, ale tak naprawdę nie jest to przewodnik po stylu . Nie znam żadnego automatycznego sprawdzania stylu , innego niż ludzie i zdrowy rozsądek.
David Rodríguez - dribeas
3
@ anon Otrzymujesz jednak gwarancję, że jeśli argument nie zostanie przekazany przez wskaźnik, NIE zostanie zmieniony. To dość cenne IMHO, w przeciwnym razie, próbując prześledzić, co dzieje się ze zmienną w funkcji, musisz zbadać pliki nagłówkowe wszystkich przekazanych funkcji, aby ustalić, czy została zmieniona. W ten sposób wystarczy spojrzeć na te, które zostały przekazane za pomocą wskaźnika.
smehmood,
107

Istnieją pewne różnice w konwencjach wywoływania w C ++ i Javie. W języku C ++ technicznie rzecz biorąc istnieją tylko dwie konwencje: pass-by-value i pass-by-reference, z pewną literaturą zawierającą trzecią konwencję pass-by-point (czyli faktycznie pass-by-value typu wskaźnika). Ponadto możesz dodać ciąg do typu argumentu, poprawiając semantykę.

Przekaż przez odniesienie

Przekazywanie przez referencję oznacza, że ​​funkcja odbierze koncepcyjnie instancję obiektu, a nie jej kopię. Odwołanie jest koncepcyjnie aliasem obiektu użytego w kontekście wywołującym i nie może mieć wartości null. Wszystkie operacje wykonywane wewnątrz funkcji dotyczą obiektu poza funkcją. Ta konwencja nie jest dostępna w Javie ani C.

Przekaż według wartości (i wskaźnika po)

Kompilator wygeneruje kopię obiektu w kontekście wywołującym i użyje tej kopii wewnątrz funkcji. Wszystkie operacje wykonywane wewnątrz funkcji są wykonywane na kopii, a nie na elemencie zewnętrznym. Jest to konwencja dla pierwotnych typów w Javie.

Specjalną jego wersją jest przekazywanie wskaźnika (adresu obiektu) do funkcji. Funkcja odbiera wskaźnik, a wszelkie operacje zastosowane do samego wskaźnika są stosowane do kopiowania (wskaźnik), z drugiej strony operacje zastosowane do wyłuskowanego wskaźnika będą miały zastosowanie do instancji obiektu w tej lokalizacji pamięci, więc funkcja może powodować działania niepożądane. Efekt użycia parametru pass-by-value wskaźnika do obiektu pozwoli funkcji wewnętrznej zmodyfikować wartości zewnętrzne, tak jak w przypadku pass-by-referencji, a także pozwoli na opcjonalne wartości (pass wskaźnik zerowy).

Jest to konwencja stosowana w C, gdy funkcja musi zmodyfikować zmienną zewnętrzną, a konwencja stosowana w Javie z typami referencji: referencja jest kopiowana, ale obiekt referencyjny jest taki sam: zmiany referencji / wskaźnika nie są widoczne na zewnątrz funkcja, ale zmiany we wskazanej pamięci są.

Dodanie stałej do równania

W C ++ można przypisywać stałość obiektom podczas definiowania zmiennych, wskaźników i referencji na różnych poziomach. Możesz zadeklarować zmienną jako stałą, możesz zadeklarować odwołanie do stałej instancji i możesz zdefiniować wszystkie wskaźniki do stałych obiektów, stałe wskaźniki do zmiennych obiektów i stałe wskaźniki do stałych elementów. I odwrotnie, w Javie można zdefiniować tylko jeden poziom stałości (słowo kluczowe końcowe): poziom zmiennej (instancja dla typów prymitywnych, referencja dla typów referencyjnych), ale nie można zdefiniować referencji do niezmiennego elementu (chyba że sama klasa jest niezmienny).

Jest to szeroko stosowane w konwencjach wywoływania C ++. Gdy obiekty są małe, możesz przekazać obiekt według wartości. Kompilator wygeneruje kopię, ale ta kopia nie jest kosztowną operacją. W przypadku dowolnego innego typu, jeśli funkcja nie zmieni obiektu, możesz przekazać odwołanie do stałej instancji (zwykle nazywanej stałą referencją) typu. To nie skopiuje obiektu, ale przekaże go do funkcji. Ale jednocześnie kompilator zagwarantuje, że obiekt nie zostanie zmieniony wewnątrz funkcji.

Reguły kciuka

Oto kilka podstawowych zasad, których należy przestrzegać:

  • Preferuj wartość przekazywaną dla typów pierwotnych
  • Preferuj przekazywanie przez odniesienie z odniesieniami do stałej dla innych typów
  • Jeśli funkcja musi zmodyfikować argument, użyj parametru pass-by-reference
  • Jeśli argument jest opcjonalny, użyj parametru pass-by-pointer (do stałej, jeśli wartość opcjonalna nie powinna być modyfikowana)

Istnieją inne niewielkie odstępstwa od tych zasad, z których pierwszą jest obsługa własności obiektu. Gdy obiekt jest dynamicznie przydzielany do nowego, należy go cofnąć przy pomocy delete (lub jego [] wersji). Obiekt lub funkcja odpowiedzialna za zniszczenie obiektu jest uważana za właściciela zasobu. Kiedy dynamicznie alokowany obiekt jest tworzony w kawałku kodu, ale własność jest przenoszona na inny element, zwykle odbywa się to za pomocą semantyki pass-by-point lub, jeśli to możliwe, za pomocą inteligentnych wskaźników.

Dygresja

Ważne jest podkreślenie znaczenia różnicy między odniesieniami do C ++ i Java. W C ++ referencje są koncepcyjnie wystąpieniem obiektu, a nie akcesorium do niego. Najprostszym przykładem jest implementacja funkcji wymiany:

// C++
class Type; // defined somewhere before, with the appropriate operations
void swap( Type & a, Type & b ) {
   Type tmp = a;
   a = b;
   b = tmp;
}
int main() {
   Type a, b;
   Type old_a = a, old_b = b;
   swap( a, b );
   assert( a == old_b );
   assert( b == old_a ); 
}

Powyższa funkcja zamiany zmienia oba argumenty za pomocą referencji. Najbliższy kod w Javie:

public class C {
   // ...
   public static void swap( C a, C b ) {
      C tmp = a;
      a = b;
      b = tmp;
   }
   public static void main( String args[] ) {
      C a = new C();
      C b = new C();
      C old_a = a;
      C old_b = b;
      swap( a, b ); 
      // a and b remain unchanged a==old_a, and b==old_b
   }
}

Wersja kodu Java modyfikuje wewnętrznie kopie referencji, ale nie modyfikuje zewnętrznych obiektów zewnętrznie. Odwołania Java to wskaźniki C bez arytmetyki wskaźników, które są przekazywane wartościom do funkcji.

David Rodríguez - dribeas
źródło
4
@ david-rodriguez-dribeas Lubię reguły sekcji kciuka, szczególnie „Wolę przekazywanie wartości dla typów pierwotnych”
yadab
Według mnie jest to o wiele lepsza odpowiedź na pytanie.
unrealsoul007
22

Jest kilka przypadków do rozważenia.

Zmodyfikowany parametr (parametry „out” i „in / out”)

void modifies(T &param);
// vs
void modifies(T *param);

Ten przypadek dotyczy głównie stylu: czy chcesz, aby kod wyglądał jak call (obj) lub call (& obj) ? Istnieją jednak dwa punkty, w których różnica ma znaczenie: przypadek opcjonalny poniżej i chcesz użyć odniesienia podczas przeciążania operatorów.

... i opcjonalnie

void modifies(T *param=0);  // default value optional, too
// vs
void modifies();
void modifies(T &param);

Parametr niezmodyfikowany

void uses(T const &param);
// vs
void uses(T param);

To ciekawy przypadek. Ogólna zasada jest taka, że ​​typy „tanie do kopiowania” są przekazywane przez wartość - są to zazwyczaj małe typy (ale nie zawsze) - podczas gdy inne są przekazywane przez const ref. Jeśli jednak musisz wykonać kopię w ramach funkcji, powinieneś przekazać wartość . (Tak, to ujawnia trochę szczegółów implementacji. C'est le C ++. )

... i opcjonalnie

void uses(T const *param=0);  // default value optional, too
// vs
void uses();
void uses(T const &param);  // or optional(T param)

Jest tu najmniejsza różnica między wszystkimi sytuacjami, więc wybierz to, co czyni twoje życie najłatwiejszym.

Stała wartość jest szczegółem implementacji

void f(T);
void f(T const);

Te deklaracje są dokładnie taką samą funkcją! Przekazując wartość, const jest jedynie szczegółem implementacji. Wypróbuj to:

void f(int);
void f(int const) { /* implements above function, not an overload */ }

typedef void NC(int);       // typedefing function types
typedef void C(int const);

NC *nc = &f;  // nc is a function pointer
C *c = nc;    // C and NC are identical types

źródło
3
+1 Nie wiedziałem, constże jestem implementacją, kiedy przekazuję wartość.
balki
20

Przekaż według wartości:

void func (vector v)

Przekazuj zmienne według wartości, gdy funkcja wymaga całkowitej izolacji od środowiska, tj. Aby zapobiec modyfikowaniu oryginalnej zmiennej przez funkcję, a także aby zapobiec modyfikowaniu jej wartości przez inne wątki podczas wykonywania funkcji.

Minusem są cykle procesora i dodatkowa pamięć poświęcona na skopiowanie obiektu.

Pomiń const const:

void func (const vector& v);

Ten formularz emuluje zachowanie typu „pass-by-value” podczas usuwania narzutu związanego z kopiowaniem. Funkcja uzyskuje dostęp do odczytu oryginalnego obiektu, ale nie może modyfikować jego wartości.

Minusem jest bezpieczeństwo wątków: wszelkie zmiany dokonane w oryginalnym obiekcie przez inny wątek pojawią się wewnątrz funkcji podczas jej wykonywania.

Przekaż odniesienie non-const:

void func (vector& v)

Użyj tego, gdy funkcja musi zapisać pewną wartość do zmiennej, która ostatecznie zostanie wykorzystana przez program wywołujący.

Podobnie jak w przypadku stałej referencji, nie jest to bezpieczne dla wątków.

Pomiń wskaźnik const:

void func (const vector* vp);

Funkcjonalnie taki sam, jak przekazanie przez const-referen, z wyjątkiem innej składni, plus fakt, że funkcja wywołująca może przekazać wskaźnik NULL, wskazując, że nie ma prawidłowych danych do przekazania.

Nie jest bezpieczny dla wątków.

Pomiń wskaźnik non-const:

void func (vector* vp);

Podobne do odwołania non-const. Program wywołujący zwykle ustawia zmienną na NULL, gdy funkcja nie powinna zapisywać wartości. Ta konwencja jest widoczna w wielu interfejsach API glibc. Przykład:

void func (string* str, /* ... */) {
    if (str != NULL) {
        *str = some_value; // assign to *str only if it's non-null
    }
}

Podobnie jak wszystkie przekazywane przez referencję / wskaźnik, nie są bezpieczne dla wątków.

nav
źródło
0

Ponieważ nikt o tym nie wspomniał, dodaję go, kiedy przekazujesz obiekt do funkcji w c ++, domyślny konstruktor kopiowania obiektu jest wywoływany, jeśli nie masz takiego, który tworzy klon obiektu, a następnie przekazujesz go do metody, więc kiedy zmieniasz wartości obiektu, które będą odzwierciedlać kopię obiektu zamiast oryginalnego obiektu, jest to problem w c ++, więc jeśli uczynisz wszystkie atrybuty klasy wskaźnikami, wówczas konstruktorzy kopiowania skopiują adresy atrybuty wskaźnika, więc gdy metoda wywołuje obiekt, który manipuluje wartościami przechowywanymi w adresach atrybutów wskaźnika, zmiany odzwierciedlają się również w oryginalnym obiekcie, który jest przekazywany jako parametr, dzięki czemu może zachowywać się tak samo jak Java, ale nie zapomnij, że cała twoja klasa atrybuty muszą być wskaźnikami, należy również zmienić wartości wskaźników,będzie wyjaśnione z wyjaśnieniem kodu.

Class CPlusPlusJavaFunctionality {
    public:
       CPlusPlusJavaFunctionality(){
         attribute = new int;
         *attribute = value;
       }

       void setValue(int value){
           *attribute = value;
       }

       void getValue(){
          return *attribute;
       }

       ~ CPlusPlusJavaFuncitonality(){
          delete(attribute);
       }

    private:
       int *attribute;
}

void changeObjectAttribute(CPlusPlusJavaFunctionality obj, int value){
   int* prt = obj.attribute;
   *ptr = value;
}

int main(){

   CPlusPlusJavaFunctionality obj;

   obj.setValue(10);

   cout<< obj.getValue();  //output: 10

   changeObjectAttribute(obj, 15);

   cout<< obj.getValue();  //output: 15
}

Ale to nie jest dobry pomysł, ponieważ skończysz pisać dużo kodu zawierającego wskaźniki, które są podatne na wycieki pamięci i nie zapomnij wywołać destruktorów. Aby uniknąć tego c ++, mają konstruktory kopiowania, w których utworzysz nową pamięć, gdy obiekty zawierające wskaźniki zostaną przekazane do argumentów funkcji, które przestaną manipulować danymi innych obiektów, Java przekazuje wartość, a wartość jest referencją, więc nie wymaga konstruktorów kopiowania.

Murali Krish
źródło
-1

Istnieją trzy metody przekazywania obiektu do funkcji jako parametru:

  1. Przekaż przez odniesienie
  2. przekazać wartość
  3. dodanie stałej w parametrze

Przejdź przez następujący przykład:

class Sample
{
public:
    int *ptr;
    int mVar;

    Sample(int i)
    {
        mVar = 4;
        ptr = new int(i);
    }

    ~Sample()
    {
        delete ptr;
    }

    void PrintVal()
    {
        cout << "The value of the pointer is " << *ptr << endl
             << "The value of the variable is " << mVar;
   }
};

void SomeFunc(Sample x)
{
cout << "Say i am in someFunc " << endl;
}


int main()
{

  Sample s1= 10;
  SomeFunc(s1);
  s1.PrintVal();
  char ch;
  cin >> ch;
}

Wynik:

Powiedz, że jestem w someFunc
Wartość wskaźnika to -17891602
Wartość zmiennej wynosi 4

amerr
źródło
Są tylko 2 metody (pierwsze 2, o których wspomniałeś). Nie mam pojęcia, co masz na myśli przez „stałą przekazywania w parametrze”
MM
-1

Poniżej przedstawiono sposoby przekazywania argumentów / parametrów do działania w C ++.

1. według wartości.

// passing parameters by value . . .

void foo(int x) 
{
    x = 6;  
}

2. przez odniesienie.

// passing parameters by reference . . .

void foo(const int &x) // x is a const reference
{
    x = 6;  
}

// passing parameters by const reference . . .

void foo(const int &x) // x is a const reference
{
    x = 6;  // compile error: a const reference cannot have its value changed!
}

3. według obiektu.

class abc
{
    display()
    {
        cout<<"Class abc";
    }
}


// pass object by value
void show(abc S)
{
    cout<<S.display();
}

// pass object by reference
void show(abc& S)
{
    cout<<S.display();
}
Yogeesh HT
źródło
1
„omijanie obiektu” nie jest rzeczą. Jest tylko przekazywanie przez wartość i przekazywanie przez odniesienie. Twój „przypadek 3” faktycznie pokazuje jeden przypadek przekazania według wartości i jeden przypadek przekazania przez odniesienie.
MM