development

C에서 구조체 또는 구조체에 대한 포인터를 반환할지 여부를 어떻게 선택합니까?

big-blog 2021. 1. 10. 19:50
반응형

C에서 구조체 또는 구조체에 대한 포인터를 반환할지 여부를 어떻게 선택합니까?


최근에 내 C 근육에 대해 작업하고 그와 함께 작업해온 많은 라이브러리를 살펴보면 좋은 방법이 무엇인지 확실히 알 수있었습니다. 내가 보지 못한 한 가지는 구조체를 반환하는 함수입니다.

something_t make_something() { ... }

내가 흡수 한 것에서 이것이 "올바른"방법입니다.

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

코드 스 니펫 2의 아키텍처는 스 니펫 1보다 훨씬 더 많이 사용됩니다. 이제 스 니펫 1 에서처럼 구조체를 직접 반환하는 이유는 무엇입니까? 두 옵션 중에서 선택할 때 어떤 차이점을 고려해야합니까?

또한이 옵션은 어떻게 비교됩니까?

void make_something(something_t *object)

something_t크기가 작을 (읽기 : 복사하는 것은 포인터를 복사하는 것만 큼 저렴합니다) 기본적으로 스택 할당을 원합니다.

something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

something_t크거나 당신이 원하는 힙 할당 :

something_t *make_something(void);

something_t *heap_thing = make_something();

의 크기에 관계없이 something_t할당 된 위치에 신경 쓰지 않는 경우 :

void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);

이것은 거의 항상 ABI 안정성에 관한 것입니다. 라이브러리 버전 간의 이진 안정성. 그렇지 않은 경우 동적 크기의 구조체를 사용하는 경우가 있습니다. 드물게 그것은 매우 큰 structs 또는 성능 에 관한 것입니다 .


struct힙에 를 할당 하고 반환하는 것이 값으로 반환하는 것만 큼 빠르다 는 것은 매우 드뭅니다 . struct거대 할 것이다.

실제로 속도는 값에 의한 반환이 아닌 포인터로 반환하는 기술 2의 이유가 아닙니다.

기술 2는 ABI 안정성을 위해 존재합니다. a가 struct있고 다음 버전의 라이브러리가 여기에 다른 20 개의 필드를 추가하는 경우 이전 버전의 라이브러리 소비자가 미리 구성된 포인터를 전달받은 경우 바이너리와 호환 됩니다. struct그들이 알고 있는 끝을 넘어서는 추가 데이터는 그들이 알 필요가없는 것입니다.

스택에 반환하면 호출자가 메모리를 할당하고 크기에 대해 동의해야합니다. 라이브러리가 마지막으로 다시 빌드 된 이후 업데이트 된 경우 스택을 폐기합니다.

기술 2는 또한 반환하는 포인터 앞뒤에 추가 데이터를 숨길 수 있도록 허용합니다 (구조체 끝에 데이터를 추가하는 버전이 변형 임). 가변 크기 배열로 구조를 종료하거나 포인터 앞에 추가 데이터를 추가하거나 둘 다 사용할 수 있습니다.

struct안정적인 ABI에서 스택 할당을 원하면 struct필요에 대해 말하는 거의 모든 함수가 버전 정보를 전달해야합니다.

그래서

something_t make_something(unsigned library_version) { ... }

반환 할 것으로 예상되는 library_version버전을 결정하기 위해 라이브러리에서 사용되는 위치 이며 조작하는 스택의 양을 변경합니다 . 표준 C로는 불가능하지만something_t

void make_something(something_t* here) { ... }

이다. 이 경우, something_t있을 수 있습니다 version첫 번째 요소로 필드 (또는 크기 필드), 당신은 그것을 호출하기 전에 채워 것을 요구한다 make_something.

a를 취하는 다른 라이브러리 코드 something_tversion필드를 쿼리하여 something_t작업중인 버전을 확인합니다 .


경험상 struct객체를 값으로 전달해서는 안됩니다 . 실제로 CPU가 단일 명령으로 처리 할 수있는 최대 크기보다 작거나 같으면 그렇게하는 것이 좋습니다. 그러나 스타일 적으로는 일반적으로 그런 경우에도 피합니다. 값으로 구조체를 전달하지 않으면 나중에 구조체에 멤버를 추가 할 수 있으며 성능에 영향을주지 않습니다.

이것이 void make_something(something_t *object)C에서 구조를 사용하는 가장 일반적인 방법 이라고 생각 합니다. 할당은 호출자에게 맡깁니다. 효율적이지만 예쁘지는 않습니다.

그러나 객체 지향 C 프로그램 something_t *make_something()포인터를 사용하도록하는 불투명 한 유형 개념으로 빌드 되었기 때문에 사용합니다. 반환 된 포인터가 동적 메모리 또는 다른 것을 가리키는 지 여부는 구현에 따라 다릅니다. 불투명 한 유형의 OO는 종종 더 복잡한 C 프로그램을 디자인하는 가장 우아하고 가장 좋은 방법 중 하나이지만 슬프게도 C 프로그래머가 그것에 대해 알고 / 관심하는 사람은 거의 없습니다.


첫 번째 접근 방식의 몇 가지 장점 :

  • 작성할 코드가 적습니다.
  • 여러 값을 반환하는 사용 사례에 대해 더 관용적입니다.
  • 동적 할당이없는 시스템에서 작동합니다.
  • 작거나 작은 물체의 경우 아마도 더 빠를 것입니다.
  • 을 잊어도 메모리 누수가 없습니다 free.

몇 가지 단점 :

  • 객체가 크면 (예 : 메가 바이트) 스택 오버플로가 발생하거나 컴파일러가이를 잘 최적화하지 않으면 느려질 수 있습니다.
  • May surprise people who learned C in the 1970s when this was not possible, and haven't kept up to date.
  • Does not work with objects that contain a pointer to a part of themself.

I'm somewhat surprised.

The difference is that example 1 creates a structure on the stack, example 2 creates it on the heap. In C, or C++ code which is effectively C, it's idiomatic and convenient to create most objects on the heap. In C++ it is not, mostly they go on the stack. The reason is that if you create an object on the stack, the destructor is called automatically, if you create it on the heap, it must be called explicitly.So it's a lot easier to ensure there are no memory leaks and to handle exceptions is everything goes on the stack. In C, the destructor must be called explictly anyway, and there's no concept of a special destructor function (you have destructors, of course, but they are just normal functions with names like destroy_myobject()).

Now the exception in C++ is for low-level container objects, e.g. vectors, trees, hash maps and so on. These do retain heap members, and they have destructors. Now most memory-heavy objects consist of a few immediate data members giving sizes, ids, tags and so on, and then the rest of the information in STL structures, maybe a vector of pixel data or a map of English word / value pairs. So most of the data is in fact on the heap, even in C++.

And modern C++ is designed so that this pattern

class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y; 
    std::string station_name; 
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

Will compile to pretty efficient code. The large observation member won't generate unnecessary makework copies.


Unlike some other languages (like Python), C does not have the concept of a tuple. For example, the following is legal in Python:

def foo():
    return 1,2

x,y = foo()
print x, y

The function foo returns two values as a tuple, which are assigned to x and y.

Since C doesn't have the concept of a tuple, it's inconvenient to return multiple values from a function. One way around this is to define a structure to hold the values, and then return the structure, like this:

typedef struct { int x, y; } stPoint;

stPoint foo( void )
{
    stPoint point = { 1, 2 };
    return point;
}

int main( void )
{
    stPoint point = foo();
    printf( "%d %d\n", point.x, point.y );
}

This is but one example where you might see a function return a structure.

ReferenceURL : https://stackoverflow.com/questions/40167559/in-c-how-would-i-choose-whether-to-return-a-struct-or-a-pointer-to-a-struct

반응형