C로 구조체를 반환하는 많은 함수가 실제로 구조체에 대한 포인터를 반환하는 이유는 무엇입니까?


49

return함수 설명 에서 전체 구조를 반환하는 대신 구조체에 대한 포인터를 반환하면 어떤 이점이 있습니까?

나는 같은 fopen저수준 함수와 같은 함수에 대해 이야기하고 있지만 아마도 구조에 대한 포인터를 반환하는 더 높은 수준의 함수가있을 것입니다.

나는 이것이 프로그래밍 문제가 아니라 디자인 선택에 더 가깝다고 믿으며 두 방법의 장단점에 대해 더 알고 싶습니다.

내가 구조체에 대한 포인터를 반환하는 것이 유리하다고 생각한 이유 중 하나는 포인터를 반환하여 함수가 실패한 경우 더 쉽게 알 수 있기 때문 NULL입니다.

전체 구조를 반환하는 NULL것이 더 어려울 것입니다. 이것이 유효한 이유입니까?


9
@ JohnR.Strohm 나는 그것을 시도하고 실제로 작동합니다. 함수는 구조체를 반환 할 수 있습니다. 그래서 그 이유는 무엇입니까?
yoyo_fun

27
사전 표준화 C에서는 구조체를 복사하거나 값으로 전달할 수 없었습니다. C 표준 라이브러리에는 그 당시에는 그렇게 쓰여지지 않은 많은 유인물 gets()이 있습니다. 예를 들어 완전히 잘못 설계된 기능이 제거 되기까지 C11까지 걸렸습니다 . 일부 프로그래머는 여전히 구조체 복사에 대한 혐오감을 가지고 있으며, 오래된 습관은 열심히 죽습니다.
amon

26
FILE*사실상 불투명 한 핸들입니다. 사용자 코드는 내부 구조가 무엇인지 신경 쓰지 않아야합니다.
코드 InChaos

3
가비지 콜렉션이있는 경우 참조로 리턴은 합리적인 기본값입니다.
Idan Arye

6
@ JohnR.Strohm 프로필의 "매우 시니어"는 1989 년 이전으로 되돌아 간 것 같습니다. ;-)-ANSI C가 K & R C가하지 않은 것을 허용했을 때 : 할당, 구조 전달 및 반환 값의 구조 복사. K & R의 원래 책은 실제로 명시 적으로 언급했다. (나는 역설적이다) : "구조를 가지고 정확히 두 가지를 할 수 있고, 주소를 가지고 와서 & 회원에게 접근 할 수 있습니다 .."
피터-복원 모니카

답변:


62

타입 fopen의 인스턴스 대신에 리턴 포인터 와 같은 함수가 작동하는 몇 가지 실용적인 이유 가 struct있습니다.

  1. 사용자에게 struct유형 표현을 숨기려고합니다 .
  2. 객체를 동적으로 할당하고 있습니다.
  3. 다중 참조를 통해 객체의 단일 인스턴스를 참조하고 있습니다.

과 같은 유형의 경우 유형 FILE *표현의 세부 사항을 사용자에게 노출하지 않기 때문입니다. FILE *객체는 불투명 한 핸들 역할을하며 해당 핸들을 다양한 I / O 루틴에 전달하기 FILE만합니다. A와 구현 struct형, 그것은하지 않습니다 ) 될 수 있습니다.

따라서 불완전한 struct 유형을 헤더에 노출시킬 수 있습니다 .

typedef struct __some_internal_stream_implementation FILE;

불완전한 유형의 인스턴스는 선언 할 수 없지만 포인터를 선언 할 수 있습니다. 그래서 나는를 만들 수 있습니다 FILE *및 그것을 통해 지정 fopen, freopen등,하지만 난 직접가 가리키는 객체를 조작 할 수 없습니다.

그것은 것 또한 가능성 fopen함수가 할당되어 FILE사용, 동적 객체 malloc또는 유사한. 이 경우 포인터를 반환하는 것이 좋습니다.

마지막으로, struct객체 에 어떤 종류의 상태를 저장하는 것이 가능하며 , 그 상태를 여러 곳에서 사용할 수있게해야합니다. struct유형의 인스턴스를 반환 한 경우 해당 인스턴스는 메모리에서 서로 다른 객체가되어 결국 동기화되지 않습니다. 단일 객체에 대한 포인터를 반환하면 모든 사람이 동일한 객체를 참조합니다.


31
포인터를 불투명 한 유형으로 사용하면 얻을 수있는 특별한 이점은 구조 자체가 라이브러리 버전간에 변경 될 수 있으며 호출자를 다시 컴파일 할 필요가 없다는 것입니다.
Barmar

6
@Barmar : 사실 ABI 안정성은 C 큰 판매 지점이며 불투명 포인터가 없으면 안정적이지 않습니다.
Matthieu M.

38

"구조를 돌려주는"방법에는 두 가지가 있습니다. 데이터 사본을 리턴하거나 참조 (포인터)를 리턴 할 수 있습니다. 일반적으로 몇 가지 이유로 포인터를 반환 (및 일반적으로 전달)하는 것이 좋습니다.

먼저, 구조를 복사하는 것은 포인터를 복사하는 것보다 훨씬 많은 CPU 시간이 걸립니다. 이것이 코드에서 자주하는 일이면 눈에 띄는 성능 차이가 발생할 수 있습니다.

둘째, 포인터를 몇 번이나 복사하더라도 메모리의 동일한 구조를 계속 가리키고 있습니다. 그것에 대한 모든 수정 사항은 동일한 구조에 반영됩니다. 그러나 구조 자체를 복사 한 다음 수정하면 해당 사본 에만 변경 사항이 표시됩니다 . 다른 사본을 보유한 코드는 변경 사항을 볼 수 없습니다. 때때로, 매우 드물게, 이것은 당신이 원하는 것이지만, 대부분 그렇지 않습니다. 잘못하면 버그를 일으킬 수 있습니다.


54
포인터로 돌아 오는 단점 : 이제 해당 객체의 소유권을 추적하고 가능하게해야합니다. 또한 포인터 간접 복사는 빠른 복사보다 비용이 많이들 수 있습니다. 여기에는 많은 변수가 있으므로 포인터를 사용하는 것이 보편적으로 좋지 않습니다.
amon

17
또한 요즘 포인터는 대부분의 데스크톱 및 서버 플랫폼에서 64 비트입니다. 나는 경력에서 64 비트에 맞는 몇 가지 구조체를 보았습니다. 따라서 포인터를 복사하는 것이 구조체를 복사하는 것보다 비용이 적게 든다고 항상 말할 수는 없습니다 .
Solomon Slow

37
이것은 대부분 좋은 대답이지만, 때로는 그 부분에 대해 동의하지 않습니다. 매우 드물게, 이것은 당신이 원하는 것입니다. 그러나 대부분 그렇지 않습니다 . 포인터를 반환하면 여러 종류의 원치 않는 부작용이 생길 수 있으며 포인터의 소유권을 잘못 얻는 여러 가지 불쾌한 방법이 있습니다. CPU 시간이 그다지 중요하지 않은 경우, 사본 변형을 선호합니다. 옵션 인 경우 오류 가 훨씬 적습니다.
Doc Brown

6
이것은 실제로 외부 API에만 적용됩니다. 내부 함수의 경우, 지난 수십 년 동안 거의 유능한 컴파일러조차도 포인터를 추가 인수로 사용하여 객체를 직접 작성하기 위해 큰 구조체를 반환하는 함수를 다시 작성합니다. 불변과 변이에 대한 논쟁은 종종 충분히 이루어졌지만, 불변의 데이터 구조가 당신이 원하는 것이 거의 결코 아니라는 주장은 사실이 아니라는 데 모두 동의 할 수 있습니다.
Voo

6
당신은 또한 포인터를위한 전문가로서 컴파일 파이어 월을 언급 할 수 있습니다. 광범위하게 공유 된 헤더를 가진 큰 프로그램에서 불완전한 유형의 함수는 구현 세부 사항이 변경 될 때마다 다시 컴파일 할 필요가 없습니다. 더 나은 컴파일 동작은 실제로 인터페이스와 구현이 분리 될 때 달성되는 캡슐화의 부작용입니다. 값으로 리턴 (및 전달, 지정)하려면 구현 정보가 필요합니다.
피터-복원 모니카

12

다른 답변 외에도 값 으로 작은 struct 값을 반환하는 것이 좋습니다. 예를 들어, 한 쌍의 데이터와 이와 관련된 일부 오류 (또는 성공) 코드를 반환 할 수 있습니다.

예를 들어, fopen하나의 데이터 (열린 FILE*) 만 반환 하고 오류가 발생하면 errno의사 전역 변수를 통해 오류 코드를 제공합니다 . 그러나 핸들과 오류 코드 (파일 핸들이 인 경우 설정 됨) struct의 두 멤버 중 하나를 리턴하는 것이 좋습니다 . 역사적 이유로 그렇지 않습니다 (그리고 오늘날 매크로 인 글로벌을 통해 오류가보고됩니다 ).FILE*NULLerrno

것을 알 이동 언어는 두 가지를 반환하는 좋은 표기법 (또는 몇) 값이 있습니다.

또한 Linux / x86-64에서 ABI 및 호출 규칙 ( x86-psABI 페이지 참조) struct은 두 개의 스칼라 멤버 중 하나 (예 : 포인터 및 정수 또는 두 개의 포인터 또는 두 개의 정수)가 두 개의 레지스터를 통해 리턴되도록 지정합니다. (그리고 이것은 매우 효율적이며 메모리를 통해 가지 않습니다).

따라서 새로운 C 코드에서 작은 C를 반환하면 struct더 읽기 쉽고 스레드 친화적이며 효율적입니다.


사실 작은 구조체가된다 포장rdx:rax. 따라서 (예를 들어, shift / or와 함께) struct foo { int a,b; };포장되어 반환 rax되며 shift / mov로 포장을 풀어야합니다. 다음은 Godbolt에 대한 예입니다 . 그러나 x86은 높은 비트에 신경 쓰지 않고 32 비트 연산에 32 비트의 64 비트 레지스터를 사용할 수 있으므로 항상 너무 나쁘지만 2 레지스터 구조체에 대부분 2 레지스터를 사용하는 것보다 확실히 나쁩니다.
Peter Cordes

관련 : bugs.llvm.org/show_bug.cgi?id=34840은의 std::optional<int> 상위 절반에서 부울을 반환 rax하므로로 테스트하려면 64 비트 마스크 상수가 필요합니다 test. 또는을 사용할 수 있습니다 bt. 그러나 호출자와 호출 수신자 dl는 "비공개"기능을 위해 수행해야하는 컴파일러 를 사용 하는 것과 비교할 때 짜증이납니다 . 또한 관련 : libstdc ++ std::optional<T>는 T 일지라도 사소하게 복사 할 수 없으므로 숨겨진 포인터를 통해 항상 반환됩니다 : stackoverflow.com/questions/46544019/… . (libc ++는 사소하게 복사 가능합니다)
Peter Cordes

@PeterCordes : 당신의 관련 일이없는 C C ++입니다
실레 Starynkevitch

죄송합니다. 그럼 같은 일이 적용됩니다 정확히struct { int a; _Bool b; };발신자가 하찮게 - 복사 가능한 C ++ 구조체가 C. 같은 ABI 사용하기 때문에, 부울을 테스트하고 싶다면, C에서
피터 코르

1
전형적인 예div_t div()
Chux

6

당신은 올바른 길을 가고 있습니다

언급 한 두 가지 이유가 모두 유효합니다.

내가 구조체에 대한 포인터를 반환하는 것이 유리하다고 생각한 이유 중 하나는 NULL 포인터를 반환하여 함수가 실패하면 더 쉽게 알 수 있기 때문입니다.

NULL 인 FULL 구조를 반환하면 더 어렵거나 덜 효율적입니다. 이것이 유효한 이유입니까?

메모리 어딘가에 텍스처가 있고 프로그램의 여러 곳에서 해당 텍스처를 참조하려는 경우; 참조 할 때마다 사본을 만드는 것이 현명하지 않습니다. 대신, 텍스처를 참조하기 위해 포인터를 단순히지나 가면 프로그램이 훨씬 빠르게 실행됩니다.

가장 큰 이유는 동적 메모리 할당입니다. 종종 프로그램이 컴파일 될 때 특정 데이터 구조에 필요한 메모리 양이 정확히 확실하지 않은 경우가 있습니다. 이러한 상황이 발생하면 런타임에 사용해야 할 메모리 양이 결정됩니다. 'malloc'을 사용하여 메모리를 요청한 다음 'free'사용이 끝나면 메모리를 비울 수 있습니다.

이에 대한 좋은 예는 사용자가 지정한 파일을 읽는 것입니다. 이 경우 프로그램을 컴파일 할 때 파일이 얼마나 큰지 알 수 없습니다. 프로그램이 실제로 실행될 때 필요한 메모리 양만 파악할 수 있습니다.

malloc 및 free return 포인터 모두 메모리의 위치를 ​​가리 킵니다. 따라서 동적 메모리 할당을 사용하는 함수는 메모리에서 구조를 만든 위치에 대한 포인터를 반환합니다.

또한 주석에서 함수에서 구조체를 반환 할 수 있는지에 대한 질문이 있음을 알았습니다. 당신은 실제로 이것을 할 수 있습니다. 다음이 작동해야합니다.

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

구조체 유형이 이미 정의되어있는 경우 특정 변수에 필요한 메모리 양을 어떻게 알 수 있습니까?
yoyo_fun

9
@JenniferAnderson C에는 불완전한 유형의 개념이 있습니다. 유형 이름을 선언 할 수는 있지만 아직 정의 할 수 없으므로 크기를 사용할 수 없습니다. 해당 유형의 변수를 선언 할 수 없지만 해당 유형에 대한 포인터 를 선언 할 수 있습니다 ( 예 :) struct incomplete* foo(void). 그렇게하면 헤더에 함수를 선언 할 수 있지만 C 파일 내에서만 구조체를 정의 할 수 있으므로 캡슐화가 가능합니다.
amon

@amon 이것이 함수 헤더 (프로토 타입 / 서명)를 선언하기 전에 함수 헤더 (프로토 타입 / 서명)를 선언하는 방법입니다. 그리고 C의 구조와 노동 조합에 같은 일을 할 수
yoyo_fun

@JenniferAnderson 헤더 파일에 함수 프로토 타입 (본문이없는 함수)을 선언 한 다음 컴파일러는 인수를 정렬하는 방법과 인수를 수락하는 방법을 알아야하기 때문에 함수의 본문을 알지 않고도 다른 코드에서 해당 함수를 호출 할 수 있습니다 반환 값. 프로그램을 링크 할 때 실제로 함수 정의 (예 : 본문) 를 알아야 하지만 한 번만 처리하면됩니다. 단순하지 않은 유형을 사용하는 경우 해당 유형의 구조도 알아야하지만 포인터의 크기는 종종 같으며 프로토 타입 사용에는 중요하지 않습니다.
simpleuser

6

FILE*클라이언트 코드에 관한 한 a 와 같은 것은 실제로 구조를 가리키는 포인터가 아니라 파일과 같은 다른 엔티티와 관련된 불투명 식별자의 형태입니다 . 프로그램이을 호출 fopen하면 일반적으로 반환 된 구조의 내용을 신경 쓰지 않습니다. 관심 fread할 것은 다른 함수와 관련하여 필요한 모든 작업을 수행한다는 것입니다.

표준 라이브러리가 FILE*해당 파일 내의 현재 읽기 위치에 대한 정보를 유지 하는 경우 호출하면 fread해당 정보를 업데이트 할 수 있어야합니다. fread포인터 를 받으면 FILE쉽게 할 수 있습니다. fread대신을 받으면 호출자가 보유한 객체 FILE를 업데이트 할 방법이 없습니다 FILE.


3

정보 숨기기

함수의 return 문에서 전체 구조를 반환하는 대신 구조체에 대한 포인터를 반환하면 어떤 이점이 있습니까?

가장 일반적인 것은 정보 숨기기 입니다. C는 struct개인의 필드를 만들 수있는 능력을 가지고 있지 않으며, 액세스 할 수있는 방법을 제공 하지는 않습니다 .

따라서 개발자가와 같이 포인트의 내용을보고 조작 할 수 FILE없도록하려면, 포인터를 포인트 크기가 불투명 한 포인터로 포인터를 처리하여 정의에 노출되지 않도록하는 유일한 방법이 있습니다. 외부 세계에 대한 정의는 알려져 있지 않다. 그러면의 정의는 FILE정의와 같은 정의를 요구하는 오퍼레이션을 구현하는 사용자에게만 표시되는 fopen반면, 구조 선언 만 공개 헤더에 표시됩니다.

이진 호환성

구조 정의를 숨기면 dylib API에서 이진 호환성을 유지할 수있는 호흡 공간을 제공 할 수 있습니다. 이를 통해 라이브러리 구현자는 라이브러리를 사용하는 사용자와 바이너리 호환성을 유지하면서 불투명 구조의 필드를 변경할 수 있습니다. 코드의 특성상 구조의 크기 나 필드가 아니라 구조로 수행 할 수있는 작업 만 알면되기 때문입니다. 그렇습니다.

예를 들어, 오늘날 Windows 95 시대에 만들어진 일부 고대 프로그램을 실제로 실행할 수 있습니다 (항상 완벽하지는 않지만 놀랍게도 많은 것이 여전히 작동합니다). 고대 바이너리에 대한 일부 코드는 Windows 95 시대에서 크기와 내용이 변경된 구조에 대해 불투명 한 포인터를 사용했을 가능성이 있습니다. 그러나 프로그램은 이러한 구조의 내용에 노출되지 않았기 때문에 새로운 버전의 창에서 계속 작동합니다. 이진 호환성이 중요한 라이브러리에서 작업 할 때 일반적으로 클라이언트에 노출되지 않는 항목은 이전 버전과의 호환성을 유지하면서 변경 될 수 있습니다.

능률

NULL 인 전체 구조를 반환하는 것은 더 어렵거나 덜 효율적입니다. 이것이 유효한 이유입니까?

일반적으로 malloc이미 할당 된 가변 크기 할당 자 풀링 메모리보다는 고정 크기 크기보다 고정 된 크기의 메모리 할당자가 씬보다 훨씬 덜 일반적으로 사용되지 않는 한 유형에 실제로 적합하고 스택에 할당 될 수 있다고 가정하면 일반적으로 효율성이 떨어집니다 . 이 경우 라이브러리 개발자가와 관련된 불변 (개념적 보증)을 유지할 수있게하는 것이 안전 트레이드 오프입니다 FILE.

적어도 성능 관점에서 fopen포인터를 반환하는 것은 정당한 이유가 아닙니다. 반환되는 유일한 이유 NULL는 파일을 열지 못했기 때문입니다. 그것은 모든 일반적인 실행 경로를 느리게하는 대신에 탁월한 시나리오를 최적화하는 것입니다. 경우에 따라 디자인을 좀 더 직관적으로 만들어서 NULL일부 사후 조건에서 반환 될 수 있도록 포인터를 반환하도록하는 유효한 생산성 이유가있을 수 있습니다 .

파일 작업의 경우 파일 작업 자체에 비해 오버 헤드가 상대적으로 매우 작 fclose으므로 수동 으로 피할 수 없습니다. 따라서 힙 할당을 피하기 위해 파일 작업 자체의 상대적 비용을 감안할 때 정의를 노출하고 FILE값으로 반환 fopen하거나 성능을 크게 향상 시켜 클라이언트를 자원 확보 (닫기) 번거 로움을 덜 수있는 것은 아닙니다. .

핫스팟 및 수정

그러나 다른 경우에는 핫스팟이 malloc있고 불필요한 캐시 캐시 누락이 있는 레거시 코드베이스에서 많은 낭비적인 C 코드를 프로파일 링했습니다. 이 방법은 불투명 포인터로 너무 자주 사용하고 너무 많은 것을 힙에 불필요하게 할당 한 결과 큰 루프.

내가 대신 사용하는 다른 방법은 클라이언트가 변조를 의도하지 않더라도 명명 규칙 표준을 사용하여 다른 사람이 필드를 만지면 안된다는 의사 소통을 통해 구조 정의를 공개하는 것입니다.

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

미래에 바이너리 호환성 문제가 있다면 미래의 목적을 위해 여분의 공간을 여분으로 예약하는 것이 충분하다는 것을 알았습니다.

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

예약 된 공간은 약간 낭비 적이지만 나중에 Foo라이브러리를 사용하는 바이너리를 깨지 않고 더 많은 데이터를 추가해야 할 경우 생명을 구할 수 있습니다 .

내 의견으로는 정보 숨기기 및 이진 호환성은 일반적으로 가변 길이 구조체 이외의 구조의 힙 할당 만 허용하는 유일한 이유입니다 (항상 필요하거나 클라이언트가 할당 해야하는 경우 적어도 사용하기가 조금 어색합니다) VLS를 할당하기 위해 VLA 방식으로 스택의 메모리). 큰 구조체조차도 소프트웨어가 스택의 핫 메모리로 훨씬 더 많이 작동한다는 것을 의미한다면 값으로 반환하는 것이 더 저렴합니다. 그리고 그들이 창조 할 때 가치로 돌아 오는 것이 더 저렴하지 않더라도 간단하게 이것을 할 수 있습니다.

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

Foo불필요한 사본을 만들지 않고 스택에서 초기화 합니다. 또는 클라이언트가 Foo어떤 이유로 원하는 경우 힙에 자유롭게 할당 할 수도 있습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.