Zwróć „strukturę” z funkcji w C


171

Dzisiaj uczyłem kilku przyjaciół, jak używać C structs. Jeden z nich zapytał, czy możesz zwrócić a structz funkcji, na co odpowiedziałem: „Nie! Zamiast tego zwróciłbyś wskaźniki do dynamicznie mallocedytowanych structs”.

Pochodząc od kogoś, kto zajmuje się głównie C ++, spodziewałem się, że nie będę w stanie zwrócić structs przez wartości. W C ++ możesz przeciążać operator =for swoje obiekty i ma sens posiadanie funkcji zwracającej obiekt według wartości. Jednak w C nie masz tej opcji, więc pomyślałem, co właściwie robi kompilator. Rozważ następujące:

struct MyObj{
    double x, y;
};

struct MyObj foo(){
    struct MyObj a;

    a.x = 10;
    a.y = 10;

    return a;
}        

int main () {

    struct MyObj a;

    a = foo();    // This DOES work
    struct b = a; // This does not work

    return 0;
}    

Rozumiem, dlaczego struct b = a;nie powinno działać - nie możesz przeciążać operator =swojego typu danych. Jak to się a = foo();dobrze komponuje? Czy to oznacza coś innego niż struct b = a;? Może pytanie, które należy zadać, brzmi: co dokładnie robi returnstwierdzenie w połączeniu ze =znakiem?

[edytuj]: Ok, właśnie wskazano mi struct b = ato błąd składniowy - to prawda i jestem idiotą! Ale to sprawia, że ​​jest to jeszcze bardziej skomplikowane! Używanie struct MyObj b = arzeczywiście działa! Czego tu brakuje?


23
struct b = a;jest błędem składniowym. A co jeśli spróbujesz struct MyObj b = a;?
Greg Hewgill

2
@GregHewgill: Masz całkowitą rację. Dość ciekawe jednak struct MyObj b = a;wydaje się, że działa :)
mmirzadeh

Odpowiedzi:


199

Możesz =bez problemu zwrócić strukturę z funkcji (lub użyć operatora). To dobrze zdefiniowana część języka. Jedynym problemem struct b = ajest to, że nie podałeś pełnego typu. struct MyObj b = abędzie działać dobrze. Możesz również przekazywać struktury do funkcji - struktura jest dokładnie taka sama, jak każdy typ wbudowany na potrzeby przekazywania parametrów, zwracania wartości i przypisywania.

Oto prosty program demonstracyjny, który wykonuje wszystkie trzy - przekazuje strukturę jako parametr, zwraca strukturę z funkcji i używa struktur w instrukcjach przypisania:

#include <stdio.h>

struct a {
   int i;
};

struct a f(struct a x)
{
   struct a r = x;
   return r;
}

int main(void)
{
   struct a x = { 12 };
   struct a y = f(x);
   printf("%d\n", y.i);
   return 0;
}

Następny przykład jest prawie taki sam, ale używa typu wbudowanego intdo celów demonstracyjnych. Oba programy zachowują się tak samo w odniesieniu do przekazywania wartości przy przekazywaniu parametrów, przypisywaniu itp .:

#include <stdio.h>

int f(int x) 
{
  int r = x;
  return r;
}

int main(void)
{
  int x = 12;
  int y = f(x);
  printf("%d\n", y);
  return 0;
}

14
To całkiem interesujące. Zawsze miałem wrażenie, że potrzebujesz do tego wskazówek. Myliłem się :)
mmirzadeh

8
Na pewno nie potrzebujesz wskazówek. To powiedziawszy, przez większość czasu chciałbyś ich używać - niejawne kopie pamięci, które mają miejsce w postaci rozrzuconych struktur według wartości, mogą być prawdziwą stratą cykli procesora, nie wspominając o przepustowości pamięci.
Carl Norum

10
@CarlNorum jak duża musi być struktura, aby kopia była droższa niż darmowe malloc +?
josefx

7
@josefx, pojedyncza kopia? Prawdopodobnie ogromny. Rzecz w tym, że normalnie, jeśli przekazujesz struktury według wartości, często je kopiujesz . W każdym razie nie jest to takie proste. Możesz omijać struktury lokalne lub globalne, w takim przypadku ich koszt alokacji jest prawie bezpłatny.
Carl Norum

7
Potrzebujesz wskaźników i alokacji pamięci dla zwracanej wartości poza treścią funkcji, gdy tylko ilość pamięci przydzielonej dla wartości nie jest znana w czasie kompilacji. Jest przeznaczony dla struktur, więc funkcje w C nie mają problemu z ich zwróceniem.
reinierpost

33

Podczas wykonywania wywołania takiego jak a = foo();kompilator może umieścić adres struktury wynikowej na stosie i przekazać go jako „ukryty” wskaźnik do foo()funkcji. Skutecznie mogłoby to stać się czymś takim:

void foo(MyObj *r) {
    struct MyObj a;
    // ...
    *r = a;
}

foo(&a);

Jednak dokładna implementacja tego zależy od kompilatora i / lub platformy. Jak zauważa Carl Norum, jeśli struktura jest wystarczająco mała, może nawet zostać przekazana w całości w rejestrze.


11
To jest całkowicie zależne od implementacji. Na przykład armcc przekaże wystarczająco małe struktury w rejestrach zwykłego przekazywania parametrów (lub wartości zwracanych).
Carl Norum

Czy nie byłoby to zwrócenie wskaźnika do zmiennej lokalnej? Pamięć dla zwróconej struktury nie może być częścią fooramki stosu. Musi być w miejscu, które przetrwa po powrocie foo.
Anders Abel

@AndersAbel: Myślę, że Greg ma na myśli to, że kompilator bierze wskaźnik do zmiennej w funkcji głównej i przekazuje go do funkcji foo. Wewnątrz funkcji foopo prostu wykonujesz zadanie
mmirzadeh

4
@AndersAbel: *r = ana końcu wykonałby (efektywnie) kopię zmiennej lokalnej do zmiennej wywołującego. Mówię „efektywnie”, ponieważ kompilator może zaimplementować RVO i acałkowicie wyeliminować zmienną lokalną .
Greg Hewgill

3
Chociaż nie daje to bezpośredniej odpowiedzi na pytanie, jest to powód, dla którego wiele osób c return structtrafia tutaj za pośrednictwem google : wiedzą, że w cdecl eaxjest zwracana wartość i że struktury ogólnie nie mieszczą się w środku eax. To jest to, czego szukałem.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

14

struct bLinia nie działa, ponieważ jest to błąd składni. Jeśli rozszerzysz go, aby uwzględnić typ, będzie działał dobrze

struct MyObj b = a;  // Runs fine

To, co robi tutaj C, to zasadniczo przejście memcpyod struktury źródłowej do celu. Dotyczy to zarówno przypisywania, jak i zwracania structwartości (i tak naprawdę każdej innej wartości w C)


+1, w rzeczywistości, wiele kompilatorów faktycznie wyemituje memcpyw tym przypadku dosłowne wywołanie - przynajmniej jeśli struktura jest dość duża.
Carl Norum

Czyli podczas inicjalizacji typu danych funkcja memcpy działa?
bhuwansahni

1
@bhuwansahni Nie jestem do końca pewien, o co pytasz. Czy mógłbyś trochę rozwinąć?
JaredPar

4
@JaredPar - kompilatory często dosłownie zadzwoń na memcpyfunkcję sytuacji struktury. Możesz zrobić szybki program testowy i na przykład zobaczyć, jak robi to GCC. W przypadku typów wbudowanych, które się nie zdarzają - nie są wystarczająco duże, aby wywołać tego rodzaju optymalizację.
Carl Norum

3
Zdecydowanie jest to możliwe - projekt, nad którym pracuję, nie ma memcpyzdefiniowanego symbolu, więc często napotykamy błędy konsolidatora „niezdefiniowany symbol”, gdy kompilator zdecyduje się wypluć go samodzielnie.
Carl Norum

9

tak, jest możliwe, że możemy również przekazać strukturę i zwrócić strukturę. Miałeś rację, ale w rzeczywistości nie przekazałeś typu danych, który powinien być taki jak ta struktura MyObj b = a.

Właściwie dowiedziałem się również, kiedy próbowałem znaleźć lepsze rozwiązanie, aby zwrócić więcej niż jedną wartość funkcji bez użycia wskaźnika lub zmiennej globalnej.

Poniżej znajduje się przykład tego samego, który oblicza odchylenie uczniów od średniej.

#include<stdio.h>
struct marks{
    int maths;
    int physics;
    int chem;
};

struct marks deviation(struct marks student1 , struct marks student2 );

int main(){

    struct marks student;
    student.maths= 87;
    student.chem = 67;
    student.physics=96;

    struct marks avg;
    avg.maths= 55;
    avg.chem = 45;
    avg.physics=34;
    //struct marks dev;
    struct marks dev= deviation(student, avg );
    printf("%d %d %d" ,dev.maths,dev.chem,dev.physics);

    return 0;
 }

struct marks deviation(struct marks student , struct marks student2 ){
    struct marks dev;

    dev.maths = student.maths-student2.maths;
    dev.chem = student.chem-student2.chem;
    dev.physics = student.physics-student2.physics; 

    return dev;
}

5

O ile dobrze pamiętam, pierwsze wersje języka C pozwalały zwracać tylko wartość, która mieściła się w rejestrze procesora, co oznacza, że ​​można było zwracać tylko wskaźnik do struktury. To samo ograniczenie dotyczy argumentów funkcji.

Nowsze wersje pozwalają na przekazywanie większych obiektów danych, takich jak struktury. Myślę, że ta funkcja była już powszechna w latach osiemdziesiątych lub wczesnych dziewięćdziesiątych.

Jednak tablice można nadal przekazywać i zwracać tylko jako wskaźniki.


Możesz zwrócić tablicę według wartości, jeśli umieścisz ją wewnątrz struktury. To, czego nie możesz zwrócić przez wartość, to tablica o zmiennej długości.
han

1
Tak, mogę umieścić tablicę wewnątrz struktury, ale nie mogę np. Napisać typedef char arr [100]; arr foo () {...} Nie można zwrócić tablicy, nawet jeśli znany jest rozmiar.
Giorgio

Czy przeciwnik mógłby wyjaśnić powód głosowania przeciw? Jeśli moja odpowiedź zawiera nieprawidłowe informacje, chętnie to naprawię.
Giorgio,

4

Ty można przypisać przypisać struktury w C a = b; jest prawidłowa składnia.

Po prostu zostawiłeś część typu - znacznik struct - w linii, która nie działa.


4

Nie ma problemu z przekazywaniem struktury z powrotem. Zostanie przekazana według wartości

Ale co, jeśli struktura zawiera dowolny element członkowski, który ma adres zmiennej lokalnej

struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

Teraz tutaj e1.name zawiera adres pamięci lokalny dla funkcji get (). Gdy funkcja get () powróci, lokalny adres nazwy zostałby zwolniony. Tak więc w dzwoniącym, jeśli spróbujemy uzyskać dostęp do tego adresu, może to spowodować błąd segmentacji, ponieważ próbujemy zwolnić adres. To jest złe..

Gdzie jako e1.id będzie całkowicie poprawne, ponieważ jego wartość zostanie skopiowana do e2.id

Dlatego powinniśmy zawsze starać się unikać zwracania lokalnych adresów pamięci funkcji.

Wszystko, co zostało pobrane, można zwrócić w dowolnym momencie


2
struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

działa dobrze z nowszymi wersjami kompilatorów. Podobnie jak id, zawartość nazwy zostaje skopiowana do przypisanej zmiennej strukturalnej.


1
Jeszcze prościej: struct emp get () {return {100, "john"}; }
Chris Reid

1

struct var e2 adres przekazany jako arg do wywoływanego stosu i tam przypisane wartości. W rzeczywistości get () zwraca adres e2 w eax reg. Działa to jak wywołanie przez odniesienie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.