return
에서 전체 구조를 반환하는 것과 반대로 구조에 대한 포인터를 반환하는 이점은 무엇입니까? 함수 설명?
fopen
및 기타 저수준 함수와 같은 함수에 대해 이야기하고 있지만 구조에 대한 포인터를 반환하는 상위 수준 함수도있을 수 있습니다.
나는 이것이 단순한 프로그래밍 질문 이라기보다는 디자인 선택에 더 가깝다고 생각하며 두 가지 방법의 장단점에 대해 더 많이 알고 싶습니다.
구조에 대한 포인터를 반환하는 것이 유리하다고 생각한 이유는 NULL
포인터를 반환하여 함수가 실패했는지 더 쉽게 알 수 있기 때문입니다.
NULL
인 전체 구조를 반환하는 것은 제가 생각하기에 더 어렵거나 덜 효율적입니다. 이것이 타당한 이유입니까?
댓글
답변
거기 fopen
와 같은 함수가 struct
유형의 인스턴스 대신 포인터를 반환하는 몇 가지 실용적인 이유는 다음과 같습니다.
- 사용자로부터
struct
유형의 표현을 숨기고 싶습니다. - 객체를 동적으로 할당합니다.
- 여러 참조를 통해 개체의 단일 인스턴스를 참조합니다.
FILE *
와 같은 유형의 경우 사용자에게 유형 표현의 세부 정보를 노출하려는 경우-FILE *
개체 ct는 불투명 핸들 역할을하며 해당 핸들을 다양한 I / O 루틴에 전달하기 만하면됩니다 (FILE
는 종종 struct
유형이 있을 필요가 없습니다 ).
따라서 어딘가에 헤더에 불완전한 struct
유형을 노출 할 수 있습니다.
typedef struct __some_internal_stream_implementation FILE;
불완전한 유형의 인스턴스를 선언 할 수는 없지만 이에 대한 포인터를 선언 할 수 있습니다. 따라서 FILE *
를 만들고 fopen
, freopen
등을 통해 할당 할 수 있습니다. , 그러나 나는 그것이 가리키는 객체를 직접 조작 할 수 없습니다.
또한 fopen
함수가 개체는 malloc
또는 이와 유사한 방식을 사용합니다. 이 경우 포인터를 반환하는 것이 좋습니다.
마지막으로 struct
객체에 어떤 종류의 상태를 저장할 수 있으며 여러 다른 위치에서 해당 상태를 사용할 수 있도록해야합니다. struct
유형의 인스턴스를 반환하면 해당 인스턴스는 서로 메모리에있는 별도의 개체가되어 결국 동기화되지 않습니다. 단일 개체에 대한 포인터를 반환함으로써 모든 사람이 동일한 개체를 참조하게됩니다.
설명
- 포인터를 불투명 한 유형은 구조 자체가 라이브러리 버전간에 변경 될 수 있으며 호출자를 다시 컴파일 할 필요가 ‘ 필요하지 않습니다.
- @Barmar : 실제로 ABI 안정성은 C의 엄청난 판매 포인트이며 불투명 한 포인터 없이는 안정적이지 않습니다.
답변
“구조를 반환 “하는 두 가지 방법이 있습니다. 데이터 사본을 반환하거나 데이터에 대한 참조 (포인터)를 반환 할 수 있습니다.일반적으로 몇 가지 이유로 포인터를 반환 (일반적으로 전달)하는 것이 선호됩니다.
첫째, 구조체를 복사하는 것은 포인터를 복사하는 것보다 훨씬 많은 CPU 시간을 필요로합니다. 코드가 자주 수행되므로 눈에 띄는 성능 차이가 발생할 수 있습니다.
둘째, 포인터를 몇 번 복사하더라도 여전히 메모리에서 동일한 구조를 가리 킵니다. 그것에 대한 모든 수정 사항은 동일한 구조에 반영됩니다. 그러나 구조 자체를 복사 한 다음 수정하면 변경 사항이 해당 사본 에만 표시됩니다. 다른 사본을 보유한 코드는 변경 사항을 볼 수 없습니다. 때로는 매우 드물게 원하는 것이지만 대부분의 경우 그렇지 않으며 잘못하면 버그가 발생할 수 있습니다.
댓글
- 포인터로 반환 할 때의 단점 : 이제 ‘ 해당 개체의 소유권을 추적해야하며 가능합니다. 무료입니다. 또한 포인터 간접은 빠른 복사보다 비용이 더 많이들 수 있습니다. 여기에는 많은 변수가 있으므로 포인터를 사용하는 것이 보편적으로 더 나은 것은 아닙니다.
- 또한 요즘 포인터는 대부분의 데스크톱 및 서버 플랫폼에서 64 비트입니다. 저는 ‘ 내 경력에서 64 비트에 맞는 구조체를 몇 개 이상 보았습니다. 따라서 ‘ 포인터를 복사하는 것이 구조체를 복사하는 것보다 비용이 적게 든다고 항상 말할 수 없습니다.
- 대부분 좋은 대답입니다. ,하지만 때로는 아주 드물게 원하는 것이지만 대부분의 경우에는 ‘ 아니요 부분에 대해 동의하지 않습니다. 그 반대입니다. 포인터를 반환하면 여러 종류의 원치 않는 부작용과 포인터의 소유권을 잘못 가져 오는 여러 가지 불쾌한 방법이 허용됩니다. CPU 시간이 그다지 중요하지 않은 경우 복사 변형을 선호합니다. 옵션 인 경우 오류 발생 가능성이 훨씬 적습니다.
- 이 점에 유의해야합니다. 외부 API에만 적용됩니다. 내부 함수의 경우 지난 수십 년 동안 거의 유능한 컴파일러는 큰 구조체를 반환하는 함수를 다시 작성하여 포인터를 추가 인수로 취하고 거기에 직접 개체를 구성합니다. 불변과 불변의 논쟁은 충분히 자주 이루어졌지만 불변 데이터 구조가 당신이 원하는 것이 거의 없다는 주장이 사실이 아니라는 데 모두 동의 할 수 있다고 생각합니다.
- 컴파일 방화벽에 대해서도 언급 할 수 있습니다. 포인터에 대한 전문가로서. 널리 공유되는 헤더가있는 대형 프로그램에서 함수가있는 불완전한 유형은 구현 세부 사항이 변경 될 때마다 재 컴파일 할 필요가 없습니다. 더 나은 컴파일 동작은 실제로 인터페이스와 구현이 분리 될 때 달성되는 캡슐화의 부작용입니다. 값으로 반환 (전달, 할당)하려면 구현 정보가 필요합니다.
Answer
다른 답변 외에도 , 때로는 작은 struct
값을 반환하는 것이 가치가 있습니다. 예를 들어 한 쌍의 데이터와 이와 관련된 오류 (또는 성공) 코드를 반환 할 수 있습니다.
예를 들어 fopen
는 하나의 데이터 (열린 FILE*
) 및 오류 발생시 errno
의사 전역 변수. 그러나 두 구성원의 struct
를 반환하는 것이 더 나을 것입니다. FILE*
핸들과 오류 코드 (다음 경우에 설정 됨) 파일 핸들은 NULL
입니다. 역사적 이유로 그렇지 않습니다 (그리고 오류는 현재 매크로 인 errno
전역을 통해보고됩니다).
Go 언어 에는 두 개 (또는 몇 개) 값을 반환하는 멋진 표기법이 있습니다.
또한 Linux / x86-64에서 ABI 및 호출 규칙 ( x86-psABI 페이지 참조)은 struct
는 두 개의 레지스터를 통해 반환됩니다 (매우 효율적이며 메모리를 통과하지 않음).
따라서 새 C 코드에서 작은 C struct
를 반환하면 더 읽기 쉽고 스레드 친화적이며 효율적일 수 있습니다.
댓글
- 실제로 작은 구조체는
rdx:rax
에 포장 됩니다. 따라서struct foo { int a,b; };
는rax
(예 : shift / or 사용)에 압축되어 반환되며 shift / mov로 압축을 풀어야합니다. 여기 ‘ Godbolt의 예 가 있습니다. 그러나 x86은 32 비트 연산에 64 비트 레지스터의 하위 32 비트를 사용할 수 있으므로 상위 비트를 신경 쓰지 않고 ‘ 항상 너무 나쁘지만 사용하는 것보다 확실히 나쁩니다. 2는 2 인조 구조체에 대해 대부분의 경우 등록합니다. - 관련 : bugs.llvm.org/show_bug.cgi? id = 34840
std::optional<int>
는rax
의 위쪽 절반에있는 부울을 반환하므로 64 비트 마스크가 필요합니다. 상수를 사용하여test
로 테스트합니다. 또는bt
를 사용할 수 있습니다. 그러나 컴파일러가 ” private ”
를 사용하는 것과 비교할 때 호출자와 수신자에게 짜증이납니다. 함수. 관련 항목 : libstdc ++ ‘의std::optional<T>
는 ‘ T가 있어도 간단하게 복사 할 수 없습니다. 이므로 항상 숨겨진 포인터를 통해 반환됩니다. stackoverflow.com/questions/46544019/ … . (libc ++ ‘ s는 간단하게 복사 할 수 있습니다.)
struct { int a; _Bool b; };
에 적용됩니다. 사소하게 복사 할 수있는 C ++ 구조체는 다음과 같은 ABI를 사용하기 때문입니다. C. div_t div()
답변
당신은 올바른 길을 가고 있습니다
당신이 언급 한 두 가지 이유가 모두 타당합니다.
이유 중 하나입니다. 구조에 대한 포인터를 반환하는 것이 장점이라고 생각하면 함수가 NULL 포인터를 반환하여 실패한 경우 더 쉽게 알 수 있다는 것입니다.
NULL 인 FULL 구조를 반환하는 것이 더 어려울 것입니다. 또는 덜 효율적입니다. 이것이 유효한 이유입니까?
메모리 어딘가에 텍스처 (예 : 텍스처)가 있고 해당 텍스처를 여러 위치에서 참조하려는 경우 프로그램; 참조하고 싶을 때마다 복사본을 만드는 것은 현명하지 않습니다. 대신 단순히 텍스처를 참조하기 위해 포인터를 넘기면 프로그램이 훨씬 빠르게 실행됩니다.
가장 큰 이유는 동적 메모리 할당입니다. 종종 프로그램을 컴파일 할 때 특정 데이터 구조에 필요한 메모리 양을 정확히 알지 못합니다.이 경우 사용해야하는 메모리 양은 런타임에 결정됩니다. malloc을 사용하여 메모리를 요청한 다음 free사용이 끝나면 해제합니다.
좋은 예는 사용자가 지정한 파일에서 읽는 것입니다. 프로그램을 컴파일 할 때 파일의 크기를 알 수 있습니다. 프로그램이 실제로 실행 중일 때 필요한 메모리 양만 알 수 있습니다.
malloc과 메모리 위치에 대한 자유 리턴 포인터. 따라서 함수 동적 메모리 할당을 사용하는 것은 메모리에서 구조를 생성 한 위치에 대한 포인터를 반환합니다.
또한 댓글에서 함수에서 구조체를 반환 할 수 있는지에 대한 질문이 있음을 확인했습니다. 당신은 정말로 이것을 할 수 있습니다. 다음이 작동합니다.
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; }
댓글
- 메모리 양을 알 수없는 방법 이미 구조체 유형을 정의한 경우 특정 변수가 필요합니까?
- @JenniferAnderson C에는 불완전한 유형의 개념이 있습니다. 유형 이름은 선언 할 수 있지만 아직 정의되지 않았으므로 ‘의 크기를 사용할 수 없습니다. 해당 유형의 변수를 선언 할 수는 없지만 해당 유형에 대한 포인터 를 선언 할 수 있습니다.
struct incomplete* foo(void)
. 이렇게하면 헤더에서 함수를 선언 할 수 있지만 C 파일 내에서 구조체 만 정의하여 캡슐화를 허용합니다. - @amon 따라서 함수 헤더 (프로토 타입 / 서명)를 선언하기 전에 선언하는 방법입니다. 작업은 실제로 C에서 수행됩니까? 그리고 C
- @JenniferAnderson의 구조체와 공용체에 동일한 작업을 수행 할 수 있습니다. 헤더 파일에서 함수 prototypes (본문이없는 함수)를 선언 한 다음 해당 함수를 호출 할 수 있습니다. 다른 코드에서는 함수의 본문을 알지 못합니다. 컴파일러는 인수를 배열하는 방법과 반환 값을받는 방법 만 알면되기 때문입니다. 프로그램을 연결할 때 실제로 함수 정의 (즉, 본문)를 알아야하지만 한 번만 처리하면됩니다. 단순하지 않은 유형을 사용하는 경우 해당 유형도 ‘의 구조를 알아야하지만 포인터는 종종 같은 크기이고 그렇지 않습니다. ‘ 시제품 ‘ 사용에는 중요하지 않습니다.
답변
FILE*
와 같은 것은 “클라이언트 코드에 관한 한 구조에 대한 포인터가 아니라 일부 와 관련된 불투명 식별자의 한 형태입니다. 파일과 같은 기타 엔티티입니다. 프로그램이 fopen
를 호출 할 때 일반적으로 반환 된 구조의 내용에 대해 신경 쓰지 않습니다. fread
와 같은 다른 함수는 필요한 모든 작업을 수행합니다.
표준 라이브러리가 예를 들어 FILE*
정보 내에 보관되는 경우 해당 파일 내의 현재 읽기 위치, fread
에 대한 호출은 해당 정보를 업데이트 할 수 있어야합니다. fread
가 FILE
에 대한 포인터를 받으면 쉽게 할 수 있습니다. fread
가 대신 FILE
를 수신했다면 FILE
개체를 업데이트 할 방법이 없습니다. 발신자가 보유합니다.
응답
정보 숨기기
return 문에서 전체 구조를 반환하는 것과 반대로 구조에 대한 포인터를 반환하는 이점은 무엇입니까? 기능?
가장 일반적인 것은 정보 숨김 입니다. 예를 들어 C는 struct
의 필드를 비공개로 만들 수있는 기능이 없으며 여기에 액세스하는 방법을 제공합니다.
강제적으로 원하는 경우 개발자가 FILE
와 같은 pointee의 내용을보고 변조하지 못하도록 방지 한 다음 유일한 방법은 포인터를 처리하여 해당 정의에 노출되지 않도록하는 것입니다. 뾰족한 크기와 정의가 외부 세계에 알려지지 않은 불투명합니다. 그러면 FILE
의 정의는 , 구조 선언 만 공개 헤더에 표시됩니다.
바이너리 호환성
구조 정의를 숨기면 dylib API에서 바이너리 호환성을 유지할 수있는 숨결 공간을 제공하는 데 도움이 될 수 있습니다. 라이브러리 구현자가 불투명 한 구조체의 필드를 변경할 수 있습니다. 라이브러리를 사용하는 사람들과 바이너리 호환성을 깨지 않고 코드의 특성상 구조가 얼마나 크거나 어떤 필드가 있는지가 아니라 구조로 무엇을 할 수 있는지 알면되기 때문입니다.
As an 예를 들어, 오늘날 Windows 95 시대에 구축 된 고대 프로그램을 실제로 실행할 수 있습니다 (항상 완벽하지는 않지만 놀랍게도 많은 프로그램이 여전히 작동합니다). 고대 바이너리의 코드 중 일부는 Windows 95 시대에서 크기와 내용이 변경된 구조에 대한 불투명 포인터를 사용했을 가능성이 있습니다. 그러나 프로그램은 이러한 구조의 내용에 노출되지 않았기 때문에 새 버전의 창에서 계속 작동합니다. 바이너리 호환성이 중요한 라이브러리에서 작업 할 때 클라이언트가 노출되지 않은 항목은 일반적으로 깨지지 않고 변경 될 수 있습니다. 이전 버전과의 호환성.
효율성
NULL 인 전체 구조를 반환하는 것은 제가 생각하기에 더 어렵거나 덜 효율적입니다. 이것이 타당한 이유입니까?
일반적으로 훨씬 적지 않는 한 유형이 실제로 적합하고 스택에 할당 될 수 있다고 가정하는 것은 일반적으로 덜 효율적입니다. 이미 할당 된 가변 크기 할당 자 풀링 메모리와 같이 malloc
이면에서 사용되는 일반화 된 메모리 할당 자입니다.이 경우에는 대부분의 아마도 라이브러리 개발자가 FILE
와 관련된 불변 (개념적 보증)을 유지할 수 있도록 허용합니다.
적어도 성능 관점에서 볼 때 타당한 이유는 아닙니다. fopen
가 포인터를 반환하도록 만드는 것은 “NULL
를 반환하는 유일한 이유는 파일을 열지 못했기 때문입니다. 이는 모든 일반적인 실행 경로를 늦추는 대가로 예외적 인 시나리오를 최적화하는 것입니다. 일부 사후 조건에서 NULL
가 반환 될 수 있도록 포인터를 반환하도록 설계를보다 간단하게 만드는 유효한 생산성 이유가있을 수 있습니다.
파일 작업의 경우 오버 헤드는 파일 작업 자체에 비해 상대적으로 매우 사소하며 fclose
수동 작업은 피할 수 없습니다. 따라서 FILE
의 정의를 노출하고 또는 힙 할당을 피하기 위해 파일 작업 자체의 상대적 비용을 고려할 때 성능이 크게 향상 될 수 있습니다.
핫스팟 및 수정
다른 경우에는 malloc
불투명 포인터와 함께이 방법을 너무 자주 사용하고 힙에 불필요하게 너무 많은 것을 할당하기 때문에 불필요한 강제 캐시 누락이 발생합니다.
대신 내가 사용하는 또 다른 방법은 구조 정의를 노출하는 것입니다. 클라이언트가 조작 할 의도가없는 경우에도 이름 지정 규칙 표준을 사용하여 다른 사람이 필드를 건드리지 말아야 함을 알립니다.
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; };
예약 된 공간은 약간 낭비이지만 나중에 라이브러리를 사용하는 바이너리를 깨지 않고.
내 의견으로는 정보 숨김 과 바이너리 호환성은 일반적으로 힙 할당 만 허용하는 적절한 이유입니다. 가변 길이 구조체 이외의 구조체 (항상 필요하거나 클라이언트가 VLA fash에서 스택에 메모리를 할당해야하는 경우 사용하는 것이 적어도 약간 어색합니다. VLS를 할당하는 이온). 큰 구조체조차도 소프트웨어가 스택의 핫 메모리로 훨씬 더 많이 작동하는 경우 값으로 반환하는 것이 더 저렴합니다. 생성시 가치로 반환하는 것이 더 저렴하지 않더라도 다음과 같이 간단하게 수행 할 수 있습니다.
int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); }
… 중복 복사 가능성없이 스택에서 가져옵니다. 또는 클라이언트는 원하는 경우 힙에 Foo
를 자유롭게 할당 할 수 있습니다.
gets()
함수가 제거 되려면 C11까지 걸렸습니다. 일부 프로그래머는 여전히 구조체 복사를 혐오하고 오래된 습관은 힘들어집니다.FILE*
는 사실상 불투명 한 핸들입니다. 사용자 코드는 내부 구조가 무엇인지 신경 쓰지 않아야합니다.&
로 주소를 가져오고 . ”