Przekazywanie tablicy jako argumentu do funkcji w C

86

Napisałem funkcję zawierającą tablicę jako argument i wywołałem ją, przekazując wartość tablicy w następujący sposób.

void arraytest(int a[])
{
    // changed the array a
    a[0]=a[0]+a[1];
    a[1]=a[0]-a[1];
    a[0]=a[0]-a[1];
}

void main()
{
    int arr[]={1,2};
    printf("%d \t %d",arr[0],arr[1]);
    arraytest(arr);
    printf("\n After calling fun arr contains: %d\t %d",arr[0],arr[1]);
}

Odkryłem, że chociaż wywołuję arraytest()funkcję, przekazując wartości, oryginalna kopia int arr[]została zmieniona.

Czy możesz wyjaśnić dlaczego?

Mohan Mahajan
źródło
1
Przekazujesz tablicę przez odniesienie, ale modyfikujesz jej zawartość - stąd dlaczego widzisz zmianę w danych
Shaun Wilde
main()musi wrócić int.
underscore_d

Odpowiedzi:

137

Podczas przekazywania tablicy jako parametru this

void arraytest(int a[])

oznacza dokładnie to samo co

void arraytest(int *a)

więc modyfikując wartości w głównym.

Z powodów historycznych tablice nie są obywatelami pierwszej kategorii i nie można ich przekazywać według wartości.

Bo Persson
źródło
3
Który zapis jest lepszy w jakich okolicznościach?
Ramon Martinez
20
@Ramon - użyłbym drugiej opcji, ponieważ wydaje się mniej zagmatwana i lepiej wskazuje, że nie dostajesz kopii tablicy.
Bo Persson
2
Czy możesz wyjaśnić „przyczyny historyczne”? Przypuszczam, że przekazywanie wartości wymagałoby kopii, a więc marnowanie pamięci ... dzięki
Jacquelyn.Marquardt
5
@lucapozzobon - pierwotnie C nie miał żadnej przekazywanej wartości, z wyjątkiem pojedynczych wartości. To się structzmieniło dopiero po dodaniu do języka. A potem uznano, że jest za późno na zmianę reguł dla tablic. Było już 10 użytkowników. :-)
Bo Persson
1
... oznacza dokładnie to samo co void arraytest(int a[1000])itd. itp. Rozszerzona odpowiedź tutaj: stackoverflow.com/a/51527502/4561887 .
Gabriel Staples,
9

Jeśli chcesz przekazać jednowymiarową tablicę jako argument w funkcji , musisz zadeklarować parametr formalny na jeden z następujących trzech sposobów, a wszystkie trzy metody deklaracji dają podobne wyniki, ponieważ każda z nich mówi kompilatorowi, że zmierza wskaźnik liczby całkowitej do odbioru .

int func(int arr[], ...){
    .
    .
    .
}

int func(int arr[SIZE], ...){
    .
    .
    .
}

int func(int* arr, ...){
    .
    .
    .
}

Więc modyfikujesz oryginalne wartości.

Dzięki !!!

Monirul Islam Milon
źródło
Szukałem twojego drugiego przykładu, czy możesz wyjaśnić, jakie są zalety każdej metody?
Puck
8

Nie przekazujesz tablicy jako kopii. Jest to tylko wskaźnik wskazujący na adres, pod którym w pamięci znajduje się pierwszy element tablicy.

fyr
źródło
7

Przekazujesz adres pierwszego elementu tablicy

ob_dev
źródło
7

Tablice w języku C są w większości przypadków konwertowane na wskaźnik do pierwszego elementu samej tablicy. Bardziej szczegółowo, tablice przekazywane do funkcji są zawsze konwertowane na wskaźniki.

Oto cytat z K & R2nd :

Kiedy nazwa tablicy jest przekazywana do funkcji, przekazywana jest lokalizacja elementu początkowego. W wywołanej funkcji ten argument jest zmienną lokalną, a więc parametr nazwy tablicy jest wskaźnikiem, czyli zmienną zawierającą adres.

Pisanie:

void arraytest(int a[])

ma takie samo znaczenie jak pisanie:

void arraytest(int *a)

Więc pomimo tego, że nie piszesz tego wprost, dzieje się tak, gdy przekazujesz wskaźnik, a więc modyfikujesz wartości w main.

Po więcej, naprawdę polecam to przeczytać .

Ponadto możesz znaleźć inne odpowiedzi na SO tutaj

granmirupa
źródło
6

Przekazujesz wartość lokalizacji pamięci pierwszego elementu tablicy.

Dlatego kiedy zaczynasz modyfikować tablicę wewnątrz funkcji, modyfikujesz oryginalną tablicę.

Pamiętaj, że a[1]jest *(a+1).

Alex
źródło
1
Przypuszczam, że brakuje () dla * a + 1 powinno być * (a + 1)
ShinTakezou,
@Shin Dzięki, minęło trochę czasu, odkąd grałem z C.
alex
6

W C, z wyjątkiem kilku specjalnych przypadków, odwołanie do tablicy zawsze „rozpada się” na wskaźnik do pierwszego elementu tablicy. Dlatego nie jest możliwe przekazanie tablicy „według wartości”. Tablica w wywołaniu funkcji zostanie przekazana do funkcji jako wskaźnik, co jest analogiczne do przekazania tablicy przez referencję.

EDYCJA: Istnieją trzy takie specjalne przypadki, w których tablica nie rozpada się na wskaźnik do jej pierwszego elementu:

  1. sizeof ato nie to samo co sizeof (&a[0]).
  2. &ato nie to samo co &(&a[0])(i nie to samo co &a[0]).
  3. char b[] = "foo"to nie to samo co char b[] = &("foo").
Thom Smith
źródło
Jeśli przekażę tablicę do funkcji. Załóżmy na przykład, że stworzyłem tablicę int a[10]i przypisałem losową wartość każdemu elementowi. Teraz, jeśli przekażę tę tablicę do funkcji za pomocą int y[]lub int y[10]lub, int *ya następnie w tej funkcji, której używam, sizeof(y)będzie odpowiedź, że wskaźnik bajtów został przydzielony. Tak więc w tym przypadku zaniknie jako wskaźnik, byłoby pomocne, gdybyś to również dołączył. Zobacz ten postimg.org/image/prhleuezd
Suraj Jain
Jeśli użyję sizeofoperacji w funkcji w pierwotnie zdefiniowanej przez nas tablicy, to rozpadnie się jako tablica, ale jeśli przekażę inną funkcję, wtedy sizeofoperator use rozpadnie się jako wskaźnik.
Suraj Jain
Wiem, że to jest stare. Dwa pytania, jeśli ktoś to zobaczy :) 1. @ThomSmith napisał, że &ato nie to samo, co &a[0]kiedy ajest tablicą. Jak to? W moim programie testowym oba pokazują, że są takie same, zarówno w funkcji, w której zadeklarowano tablicę, jak i po przekazaniu do innej funkcji. 2. Autor pisze, że „ char b[] = "foo"to nie to samo co char b[] = &("foo")”. Dla mnie ta ostatnia nawet się nie kompiluje. Czy to tylko ja?
Aviv Cohn
6

Przekazywanie tablicy wielowymiarowej jako argumentu do funkcji. Przekazanie jednej tablicy dim jako argumentu jest mniej lub bardziej trywialne. Przyjrzyjmy się bardziej interesującemu przypadkowi przekazywania tablicy 2 dim. W C nie możesz użyć wskaźnika do konstrukcji wskaźnika ( int **) zamiast 2 dim array. Zróbmy przykład:

void assignZeros(int(*arr)[5], const int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 5; j++) {
            *(*(arr + i) + j) = 0;
            // or equivalent assignment
            arr[i][j] = 0;
        }
    }

Tutaj określiłem funkcję, która jako pierwszy argument przyjmuje wskaźnik do tablicy 5 liczb całkowitych. Jako argument mogę podać dowolną 2 dim tablicę, która ma 5 kolumn:

int arr1[1][5]
int arr1[2][5]
...
int arr1[20][5]
...

Możesz wpaść na pomysł zdefiniowania bardziej ogólnej funkcji, która może akceptować dowolną 2 tablice dim i zmienić sygnaturę funkcji w następujący sposób:

void assignZeros(int ** arr, const int rows, const int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            *(*(arr + i) + j) = 0;
        }
    }
}

Ten kod zostałby skompilowany, ale podczas próby przypisania wartości w taki sam sposób, jak w przypadku pierwszej funkcji wystąpi błąd w czasie wykonywania. Więc w C wielowymiarowe tablice nie są tym samym, co wskaźniki do wskaźników ... do wskaźników. An int(*arr)[5]jest wskaźnikiem do tablicy składającej się z 5 elementów, an int(*arr)[6] jest wskaźnikiem do tablicy składającej się z 6 elementów, a są one wskaźnikami do różnych typów!

Jak zdefiniować argumenty funkcji dla wyższych wymiarów? Proste, po prostu podążamy za wzorem! Oto ta sama funkcja dostosowana do przyjęcia tablicy 3 wymiarów:

void assignZeros2(int(*arr)[4][5], const int dim1, const int dim2, const int dim3) {
    for (int i = 0; i < dim1; i++) {
        for (int j = 0; j < dim2; j++) {
            for (int k = 0; k < dim3; k++) {
                *(*(*(arr + i) + j) + k) = 0;
                // or equivalent assignment
                arr[i][j][k] = 0;
            }
        }
    }
}

Jak można się spodziewać, może przyjąć jako argument dowolne 3 ciemne tablice, które mają w drugim wymiarze 4 elementy, aw trzecim wymiarze 5 elementów. Wszystko takie byłoby w porządku:

arr[1][4][5]
arr[2][4][5]
...
arr[10][4][5]
...

Ale musimy określić wszystkie wymiary, aż do pierwszego.

Andrushenko Alexander
źródło
5

Standardowe użycie tablicy w C z naturalnym zanikaniem typu z tablicy na ptr

@Bo Persson słusznie stwierdza w swej wielkiej odpowiedź tutaj :

Podczas przekazywania tablicy jako parametru this

void arraytest(int a[])

oznacza dokładnie to samo co

void arraytest(int *a)

Dodam jednak również, że powyższe dwie formy również:

  1. znaczy dokładnie to samo co

     void arraytest(int a[0])
    
  2. co oznacza dokładnie to samo co

     void arraytest(int a[1])
    
  3. co oznacza dokładnie to samo co

     void arraytest(int a[2])
    
  4. co oznacza dokładnie to samo co

     void arraytest(int a[1000])
    
  5. itp.

W każdym z powyższych przykładów tablic typ parametru wejściowego rozpada się na anint * i można go wywołać bez ostrzeżeń i błędów, nawet przy -Wall -Wextra -Werrorwłączonych opcjach kompilacji (zobacz moje repozytorium, aby uzyskać szczegółowe informacje na temat tych 3 opcji kompilacji), na przykład to:

int array1[2];
int * array2 = array1;

// works fine because `array1` automatically decays from an array type
// to `int *`
arraytest(array1);
// works fine because `array2` is already an `int *` 
arraytest(array2);

W rzeczywistości, „wielkość” wartość ( [0], [1], [2], [1000], itd.) Wewnątrz parametru tablicy tu widocznie tylko w celach estetycznych / self-dokumentacyjne, a może być dowolną liczbą całkowitą dodatnią ( size_ttyp I myślę) chcesz!

W praktyce jednak należy go użyć do określenia minimalnego rozmiaru tablicy, jaką ma otrzymać funkcja, aby podczas pisania kodu można było łatwo śledzić i weryfikować. MISRA-C-2012 Standard ( kupić / pobrać 236-PG 2012 wersja PDF standardem dla £ 15.00 tutaj ) idzie tak daleko, aby państwa (podkreślenie dodane):

Reguła 17.5 Argument funkcji odpowiadający parametrowi zadeklarowanemu jako typ tablicowy powinien mieć odpowiednią liczbę elementów.

...

Jeśli parametr jest zadeklarowany jako tablica o określonym rozmiarze, odpowiedni argument w każdym wywołaniu funkcji powinien wskazywać na obiekt, który ma co najmniej tyle elementów, co tablica.

...

Użycie deklaratora tablicy dla parametru funkcji określa interfejs funkcji wyraźniej niż użycie wskaźnika. Minimalna liczba elementów oczekiwana przez funkcję jest wyraźnie określona, ​​podczas gdy nie jest to możliwe w przypadku wskaźnika.

Innymi słowy, zalecają użycie jawnego formatu rozmiaru, mimo że standard C technicznie go nie wymusza - pomaga to przynajmniej wyjaśnić Tobie jako programistom i innym korzystającym z kodu, jakiego rozmiaru tablica oczekuje funkcja do przejścia.


Wymuszenie bezpieczeństwa typu na tablicach w C

Jak @Winger Sendon zwraca uwagę w komentarzu pod moją odpowiedź, możemy zmusić C traktować tablicę typ być różny w zależności od matrycy wielkości !

Po pierwsze, musisz rozpoznać, że w moim przykładzie powyżej, używając int array1[2];następującego: arraytest(array1);powoduje, array1że automatycznie rozpada się na int *. JEDNAK, jeśli zamiast tego weźmiesz adres array1 i zadzwonisz arraytest(&array1), uzyskasz zupełnie inne zachowanie! Teraz NIE rozpada się na int *! Zamiast tego typ &array1is int (*)[2], co oznacza „wskaźnik do tablicy o rozmiarze 2 typu int” lub „wskaźnik do tablicy o rozmiarze 2 typu int” . Możesz więc WYMUSIĆ C, aby sprawdzić bezpieczeństwo typów w tablicy, na przykład:

void arraytest(int (*a)[2])
{
    // my function here
}

Ta składnia jest trudna do odczytania, ale podobna do tej ze wskaźnika funkcji . Narzędzie online, cdecl , mówi nam, że int (*a)[2]oznacza to: „zadeklaruj jako wskaźnik do tablicy 2 int” (wskaźnik do tablicy 2 ints). NIE myl tego z wersją bez nawiasów OUT:, int * a[2]co oznacza: „zadeklaruj jako tablicę 2 wskaźnika do int” (tablica 2 wskaźników do int).

Teraz ta funkcja WYMAGA wywołania jej za pomocą operatora adresu ( &) w ten sposób, używając jako parametru wejściowego WSKAŹNIKA NA TABLICĘ O PRAWIDŁOWYM ROZMIARZE !:

int array1[2];

// ok, since the type of `array1` is `int (*)[2]` (ptr to array of 
// 2 ints)
arraytest(&array1); // you must use the & operator here to prevent
                    // `array1` from otherwise automatically decaying
                    // into `int *`, which is the WRONG input type here!

To jednak spowoduje ostrzeżenie:

int array1[2];

// WARNING! Wrong type since the type of `array1` decays to `int *`:
//      main.c:32:15: warning: passing argument 1 of ‘arraytest’ from 
//      incompatible pointer type [-Wincompatible-pointer-types]                                                            
//      main.c:22:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
arraytest(array1); // (missing & operator)

Możesz przetestować ten kod tutaj .

Aby zmusić kompilator C do zmiany tego ostrzeżenia w błąd, tak aby zawsze MUSISZ wywoływać arraytest(&array1);używając tylko tablicy wejściowej o odpowiednim rozmiarze i typie ( int array1[2];w tym przypadku), dodaj -Werrordo opcji kompilacji. Jeśli uruchamiasz powyższy kod testowy na onlinegdb.com, zrób to, klikając ikonę koła zębatego w prawym górnym rogu i kliknij „Dodatkowe flagi kompilatora”, aby wpisać tę opcję. Teraz to ostrzeżenie:

main.c:34:15: warning: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Wincompatible-pointer-types]                                                            
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’    

zamieni się w ten błąd kompilacji:

main.c: In function ‘main’:
main.c:34:15: error: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Werror=incompatible-pointer-types]
     arraytest(array1); // warning!
               ^~~~~~
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
 void arraytest(int (*a)[2])
      ^~~~~~~~~
cc1: all warnings being treated as errors

Zwróć uwagę, że możesz również utworzyć wskaźniki „bezpieczne dla typów” do tablic o danym rozmiarze, na przykład:

int array[2];
// "type safe" ptr to array of size 2 of int:
int (*array_p)[2] = &array;

... ale niekoniecznie to polecam, ponieważ przypomina mi to wiele wybryków C ++ używanych do wymuszania wszędzie bezpieczeństwa typów, przy wyjątkowo wysokich kosztach złożoności składni języka, szczegółowości i trudności w projektowaniu kodu, a których nie lubię i narzekałem już wiele razy (np .: zobacz „Moje myśli o C ++” tutaj ).


Dodatkowe testy i eksperymenty znajdują się w linku poniżej.

Bibliografia

Zobacz linki powyżej. Również:

  1. Moje eksperymenty z kodem online: https://onlinegdb.com/B1RsrBDFD
Gabriel Staples
źródło
2
void arraytest(int (*a)[1000])jest lepsze, ponieważ kompilator wyświetli błąd, jeśli rozmiar jest nieprawidłowy.
Skrzydłowy Sendon
@WingerSendon, wiedziałem, że są pewne subtelności, które muszę tutaj zweryfikować, a składnia jest myląca (podobnie jak składnia funkcji ptr jest myląca), więc nie spieszyłem się i w końcu zaktualizowałem moją odpowiedź dużą nową sekcją zatytułowaną Forcing type safety on arrays in C, obejmującą twoje punkt.
Gabriel Staples
0

Tablice są zawsze przekazywane przez odwołanie, jeśli używasz a[]lub *a:

int* printSquares(int a[], int size, int e[]) {   
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}

int* printSquares(int *a, int size, int e[]) {
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}
anil karikatti
źródło
Głosuję za tym. Nie jestem pewien, dlaczego jest odrzucany.
Gabriel Staples