매개 변수 또는 반환 값으로 C 구조체를 초기화해야합니까? [닫은]


33

내가 일하는 회사는 다음과 같은 초기화 기능을 통해 모든 데이터 구조를 초기화하고 있습니다.

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

나는 다음과 같이 내 구조체를 초기화하려고 시도했다.

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

다른 것보다 장점이 있습니까?
어느 것을해야합니까? 더 나은 방법으로 어떻게 정당화 할 수 있습니까?


4
@gnat 구조체 초기화에 관한 명백한 질문입니다. 이 스레드는이 특정 디자인 결정에 적용되는 것과 동일한 근거를 구현합니다.
Trevor Hickey

2
@Jefffrey 우리는 C에 있으므로 실제로 메소드를 가질 수 없습니다. 항상 직접적인 값 세트는 아닙니다. 때로는 구조체를 초기화하는 것은 값을 얻는 것입니다 (어쨌든). 구조체를 초기화하는 논리를 수행합니다.
Trevor Hickey

1
@JacquesB I got ""당신이 만드는 모든 컴포넌트는 다른 컴포넌트와 다를 것입니다. 구조체를 위해 다른 곳에서 Initialize () 함수가 사용되었습니다. 기술적으로 말하자면, 생성자를 호출하는 것은 잘못된 것입니다. "
Trevor Hickey

1
@TrevorHickey InitializeFoo()는 생성자입니다. C ++ 생성자와의 유일한 차이점은 this포인터가 암시 적이 아니라 명시 적으로 전달된다는 것입니다. 컴파일 된 코드 InitializeFoo()와 해당 C ++ Foo::Foo()는 정확히 동일합니다.
cmaster

2
더 나은 옵션 : C ++ 대신 C 사용을 중지하십시오. 오토 윈.
Thomas Eding

답변:


25

두 번째 접근 방식에서는 절대 초기화되지 않은 Foo가 없습니다. 모든 구조를 한곳에 두는 것이 더 합리적이고 명백한 장소입니다.

그러나 ... 첫 번째 방법은 그렇게 나쁘지 않으며 많은 분야에서 자주 사용됩니다 (1 차 방법과 같은 속성 주입 또는 2 차와 같은 생성자 주입 중 가장 좋은 의존성 주입 방법에 대한 토론도 있습니다) . 둘 다 잘못이 아닙니다.

따라서 어느 쪽도 잘못이 아니고 회사의 나머지 부분이 접근법 # 1을 사용하는 경우 기존 코드베이스에 적합해야하며 새로운 패턴을 도입하여 엉망으로 만들지 않아야합니다. 이것은 실제로 여기서 가장 중요한 요소이며, 새로운 친구들과 즐겁게 놀고 다른 일을하는 특별한 눈송이가되지 마십시오.


좋아, 합리적으로 보인다. 어떤 종류의 입력을 초기화했는지 알지 못하고 객체를 초기화하면 혼란을 초래할 것이라는 인상을 받았습니다. 예측 가능하고 테스트 가능한 코드를 생성하기 위해 데이터 입력 / 데이터 출력 개념을 따르려고했습니다. 다른 방법으로 내 구조체의 소스 파일이 초기화를 수행하기 위해 추가 종속성이 필요했기 때문에 커플 링이 증가하는 것처럼 보였습니다. 그래도 한 가지 방법이 다른 방법보다 선호되지 않는 한 보트를 흔들고 싶지 않습니다.
Trevor Hickey

4
@TrevorHickey : 실제로 나는 당신이주는 예제들 사이에 두 가지 주요 차이점 이 있다고 말할 것입니다 . (2) 초기화 매개 변수는 함수로 전달되고 다른 매개 변수는 암시 적입니다. (2)에 대해 더 묻는 것 같지만 여기에 대한 답변은 (1)에 집중하고 있습니다. 나는 대부분의 사람들이 명시 적 매개 변수와 포인터를 사용하여 두 가지의 하이브리드를 권장 할 것으로 생각합니다.void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
첫 번째 접근 방식은 어떻게 "초기화" Foo구조로 이어질 까요? 첫 번째 방법은 한 곳에서 모든 초기화를 수행합니다. (또는 초기화 되지 않은Foo 구조체를 "반 초기화 됨"으로 고려하고 있습니까?)
jamesdlin

1
Foo가 생성되고 InitialiseFoo가 실수로 누락 된 경우 @ jamesdlin. 긴 설명을 입력하지 않고 2 단계 초기화를 설명하는 것은 단지 그림 일뿐입니다. 경험이 풍부한 개발자 유형의 사람들이 이해할 것이라고 생각했습니다.
gbjbaanb

22

두 방법 모두 초기화 코드를 단일 함수 호출로 묶습니다. 여태까지는 그런대로 잘됐다.

그러나 두 번째 방법에는 두 가지 문제가 있습니다.

  1. 두 번째 것은 실제로 결과 객체를 구성하지 않고 스택의 다른 객체를 초기화 한 다음 최종 객체로 복사합니다. 이것이 두 번째 접근법이 약간 열등한 것으로 보는 이유입니다. 이 외부 사본으로 인해 푸시 백이 발생했을 수 있습니다.

    당신이 클래스 파생 때 심지어 더 나쁜 Derived에서을 Foo(구조체는 주로 C에서 객체 지향에 사용되는) : 두 번째 방법으로, 함수가 ConstructDerived()호출 것 ConstructFoo(), 그 결과 임시 복사본 Fooa의 슈퍼 클래스 슬롯에 걸쳐 객체를 Derived객체; Derived객체 의 초기화를 완료하십시오 . 결과 객체를 반환 할 때 다시 복사해야합니다. 세 번째 레이어를 추가하면 모든 것이 완전히 어리 석습니다.

  2. 두 번째 접근 방식을 사용하면 ConstructClass()기능이 건설중인 객체의 주소에 액세스 할 수 없습니다. 따라서 콜백을 위해 객체를 다른 객체에 등록해야 할 때 필요하므로 생성 중에 객체를 연결할 수 없습니다.


마지막으로, 모두 structs가 본격적인 수업이 아닙니다 . 일부 structs는 이러한 변수 값에 대한 내부 제한없이 여러 변수를 효과적으로 묶습니다. typedef struct Point { int x, y; } Point;이것의 좋은 예가 될 것입니다. 이러한 경우 초기화 기능을 사용하는 것은 과도한 것으로 보입니다. 이 경우 복합 리터럴 구문이 편리 할 수 ​​있습니다 (C99 임).

Point = { .x = 7, .y = 9 };

또는

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}


5
컴파일러가 사본을 제거해도 사본을 기록한 사실을 완화하지는 않습니다. C에서는 불필요한 연산을 작성하고이를 수정하기 위해 컴파일러에 의존하는 것은 나쁜 스타일로 간주됩니다. 이것은 컴파일러가 이론적으로 중첩 된 템플릿으로 남은 모든 균열을 제거 할 수 있음을 증명할 수 있다는 것을 자랑스럽게 생각하는 C ++과 다릅니다. C에서는 사람들이 기계가 실행해야 할 코드를 정확하게 작성하려고합니다. 어쨌든 액세스 할 수없는 주소에 대한 요점이 남아 있으므로 복사 제거는 도움이되지 않습니다.
cmaster

3
컴파일러를 사용하는 사람은 작성하는 코드가 컴파일러에 의해 변환 될 것으로 예상해야합니다. 그들이 하드웨어 C 인터프리터를 실행하지 않는 한, 그들이 작성하는 코드는 달리 믿기 쉽더라도 실행하는 코드가 아닙니다. 컴파일러를 이해하면 제거를 이해하고 int x = 3;문자열 x을 바이너리에 저장 하지 않는 것과 다르지 않습니다 . 주소와 상속 포인트가 좋습니다. 가정 된 소실 실패는 어리 석다.
Yakk

@ 약 : 역사적으로 C는 시스템 프로그래밍을위한 고급 어셈블리 언어의 형태로 개발되었습니다. 그 이후로 그 정체성은 점점 더 어두워졌습니다. 어떤 사람들은이 언어가 최적화 된 응용 프로그램 언어가되기를 원하지만 더 나은 형태의 고급 어셈블리 언어가 나오지 않기 때문에 C가 여전히 후자의 역할을 수행해야합니다. 최소한의 최적화로 컴파일 할 때조차도 잘 작성된 프로그램 코드가 적어도 괜찮게 작동해야한다는 생각에는 아무런 문제가 없습니다.하지만 실제로 작동하려면 C가 오랫동안 부족한 것들을 추가해야합니다.
supercat

@Yakk : 예를 들어, 컴파일러에게 "다음 코드는 코드를 확장하는 동안 레지스터에 안전하게 보관할 수 있습니다"라는 지시문이 있고 unsigned char최적화를 허용하지 않는 유형을 블록 복사하는 수단 이 있습니다. 엄격한 앨리어싱 규칙은 불충분 한 동시에 프로그래머의 기대를 더 명확하게합니다.
supercat

1

구조의 내용과 사용중인 특정 컴파일러에 따라 두 가지 접근 방식이 더 빠를 수 있습니다. 일반적인 패턴은 특정 기준을 충족하는 구조가 레지스터로 반환 될 수 있다는 것입니다. 다른 구조 유형을 리턴하는 함수의 경우 호출자는 임시 구조를위한 공간을 어딘가에 (일반적으로 스택에) 할당하고 주소를 "숨겨진"매개 변수로 전달해야합니다. 함수의 반환 값이 외부 코드에 의해 주소가 유지되지 않는 로컬 변수에 직접 저장되는 경우 일부 컴파일러는 해당 변수의 주소를 직접 전달할 수 있습니다.

구조 유형이 함수 리턴을 갖는 레지스터 (예를 들어, 하나의 기계어보다 크거나 정확히 두 개의 기계어를 채우는 것)에 반환되어야하는 특정 구현의 요구 사항을 충족하는 경우 구조는 구조의 주소를 전달하는 것보다 빠를 수 있습니다. 변수의 주소를 외부 코드에 노출하면 복사본을 유지할 수 있으므로 유용한 최적화가 불가능할 수 있습니다. 형식이 이러한 요구 사항을 충족하지 않으면 구조체를 반환하는 함수에 대해 생성 된 코드는 대상 포인터를 허용하는 함수의 코드와 유사합니다. 포인터를 사용하는 양식의 경우 호출 코드가 더 빠를 수 있지만 해당 양식은 일부 최적화 기회를 잃습니다.

너무 나쁘다 C는 함수가 전달 된 포인터 (C ++ 참조와 비슷한 의미)의 사본을 유지하는 것이 금지되어 있다고 말하는 수단을 제공하지 않습니다. 왜냐하면 제한된 포인터를 전달하면 전달의 직접적인 성능 이점을 얻을 수 있기 때문입니다. 기존 객체에 대한 포인터이지만 동시에 변수의 주소를 "노출 된"것으로 간주하도록 컴파일러에 요구하는 의미 론적 비용을 피하십시오.


3
마지막으로 : C ++에는 참조로 전달 된 포인터의 복사본을 유지하지 못하도록 함수가 없습니다.이 함수는 단순히 객체의 주소를 취할 수 있습니다. 참조를 사용하여 해당 참조가 포함 된 다른 개체를 생성 할 수도 있습니다 (알몸 포인터가 생성되지 않음). 그럼에도 불구하고, 포인터 사본 또는 객체의 참조는 그들이 가리키는 객체보다 수명이 길어 매달려있는 포인터 / 참조를 생성 할 수 있습니다. 따라서 참조 안전에 대한 요점은 꽤 음소거입니다.
cmaster

@cmaster : 임시 저장소에 포인터를 전달하여 구조를 반환하는 플랫폼에서 C 컴파일러는 해당 저장소의 주소에 액세스하는 방법으로 호출 된 함수를 제공하지 않습니다. C ++에서는 참조로 전달 된 변수의 주소를 얻을 수 있지만 호출자가 전달 된 항목의 수명을 보장하지 않으면 (이 경우 일반적으로 포인터를 전달한 경우) 정의되지 않은 동작이 발생할 수 있습니다.
supercat

1

"output-parameter"스타일을 선호하는 한 가지 주장은 함수가 오류 코드를 반환 할 수 있다는 것입니다.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

관련 구조체 세트를 고려할 때 초기화가 실패하면 일관성을 위해 모든 매개 변수를 매개 변수 외부 스타일로 사용하는 것이 좋습니다.


1
하나만 설정할 수 있지만 errno.
중복 제거기

0

나는 건설 인수가 제공되는 방식의 불일치가 아니라 출력 매개 변수를 통한 초기화 대 반환을 통한 초기화에 중점을두고 있다고 가정합니다.

첫 번째 접근 방식은 Foo불투명 할 수 있으며 (현재 사용하는 방식이 아님에도 불구하고) 일반적으로 장기적인 유지 관리에 바람직합니다. 예를 들어, Foo초기화하지 않고 불투명 한 구조체 를 할당하는 함수를 고려할 수 있습니다. 또는 Foo이전에 다른 값 으로 초기화 된 구조체 를 다시 초기화해야 할 수도 있습니다 .


Downvoter, 설명해야합니까? 내가 실제로 말한 것이 맞습니까?
jamesdlin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.