C에서 변수 선언 배치


129

나는 C에서 모든 변수가 함수의 시작 부분에 선언되어야한다고 생각했습니다. C99에서 규칙은 C ++에서와 동일하지만 C89 / ANSI C에 대한 변수 선언 배치 규칙은 무엇입니까?

다음 코드는 gcc -std=c89및로 성공적으로 컴파일됩니다 gcc -ansi.

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

의 선언 안 csC89 / ANSI 모드에서 오류가 발생할?


54
참고 사항 : ansi C의 변수는 함수 시작 시가 아니라 블록 시작시 선언 할 필요가 없습니다. 따라서 for 루프 상단의 char c = ...는 ansi C에서 완전히 합법적입니다. 그러나 char * s는 그렇지 않습니다.
Jason Coco

답변:


149

GCC s가 C89 또는 ANSI 표준의 일부가 아니더라도 GNU 확장으로 선언을 허용하므로 성공적으로 컴파일됩니다 . 해당 표준을 엄격히 준수하려면 -pedantic플래그 를 전달해야합니다 .

블록 c시작시 선언은 { }C89 표준의 일부입니다. 블록은 기능 일 필요는 없습니다.


41
의 선언 만 s확장 (C89 관점에서)이라는 점에 주목할 가치가 있습니다. 선언은 cC89에서 완벽하게 합법적이며 확장이 필요하지 않습니다.
AnT

7
@AndreyT : 예, C에서 변수 선언은 함수 자체가 아니라 블록 의 시작이어야합니다 . 그러나 사람들은 블록의 기본 예이므로 블록을 기능과 혼동합니다.
legends2k

1
+39 표의 댓글을 답변으로 옮겼습니다.
MarcH

78

C89의 경우 스코프 블록 시작시 모든 변수를 선언해야합니다 .

따라서 char c선언은 for 루프 범위 블록의 맨 위에 있으므로 유효합니다. 그러나 char *s선언은 오류가되어야합니다.


2
꽤 맞습니다. {...}의 시작 부분에 변수를 선언 할 수 있습니다.
Artelius

5
@Artelius 정확하지 않습니다. curls가 블록의 일부인 경우에만 (구조 또는 공용체 선언 또는 브레이징 된 초기화의 일부가 아닌 경우)
Jens

헛소리를하기 위해서는 적어도 C 표준에 따라 잘못된 선언을해야합니다. 따라서의 오류 또는 경고 여야합니다 gcc. 즉, 프로그램이 컴플라이언스됨을 의미하도록 컴파일 될 수 있다고 믿지 마십시오.
jinawee

35

블록 맨 위에 변수 선언을 그룹화하는 것은 오래된 기본 C 컴파일러의 한계로 인해 레거시 일 가능성이 높습니다. 모든 현대 언어는 최신 시점에서 지역 변수 선언을 권장하고 때로는 시행하기도합니다. 실수로 임의의 값을 사용하는 위험을 제거하기 때문입니다. 선언과 초기화를 분리하면 가능한 경우 "const"(또는 "final")를 사용하지 못하게됩니다.

C ++은 불행히도 C와의 이전 버전과의 호환성을 위해 이전의 최고 선언 방법을 계속 수용합니다 (하나의 C 호환성은 다른 많은 것들 중에서 하나입니다 ...). 그러나 C ++은 그것을 멀리하려고합니다.

  • C ++ 참조의 디자인은 이러한 최상위 그룹을 허용하지도 않습니다.
  • C ++ 로컬 객체 의 선언과 초기화를 분리 하면 추가 생성자 비용을 지불하지 않아도됩니다. 인수없는 생성자가 없으면 다시 분리 할 수 ​​없습니다!

C99가 C를 같은 방향으로 움직이기 시작합니다.

로컬 변수가 선언 된 위치를 찾지 못할 경우 훨씬 큰 문제가 있음을 의미합니다. 둘러싸는 블록이 너무 길어서 분할되어야합니다.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions



블록 상단에 변수 선언을 적용하여 보안 허점을 만드는 방법도 참조하십시오. lwn.net/Articles/443037
MarcH

"C ++은 불행히도 C와의 하위 호환성을 위해 이전의 최고 선언 방식을 계속 받아들이고 있습니다."IMHO, 그것은 그것을하는 확실한 방법 일뿐입니다. 다른 언어는 항상 0으로 초기화하여이 문제를 "해결"합니다. Bzzt, 요청하면 논리 오류 만 숨 깁니다. 그리고 초기화를 위해 가능한 여러 위치가 있기 때문에 초기화하지 않고 선언을 해야하는 경우가 꽤 있습니다. 이것이 바로 C ++의 RAII가 엉덩이에 큰 고통을주는 이유입니다. 이제 이러한 경우를 위해 각 객체에 "유효한"초기화되지 않은 상태를 포함시켜야합니다.
Jo So

1
@JoSo : 초기화되지 않은 변수를 읽으면 임의의 효과가 발생하여 프로그래밍 오류가 일관된 값이나 결정적 오류를 발생시키는 것보다 쉽게 ​​감지 할 수 있다고 생각하는 이유가 혼란 스럽습니까? 초기화되지 않은 스토리지의 읽기가 변수가 가질 수있는 모든 비트 패턴과 일치하는 방식으로 작동한다고 보장 할 수 없으며, 이러한 프로그램도 일반적인 시간 및 인과 관계 법칙과 일치하는 방식으로 작동합니다. 같은 감안할 때 뭔가 int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat

1
@JoSo : 포인터, 특히에서 작업을 트랩하는 구현의 경우 null, 모든 비트 0은 종종 유용한 트랩 값입니다. 또한 변수의 기본값을 all-bits-zero로 명시 적으로 지정하는 언어에서는 해당 값에 의존하는 것이 오류가 아닙니다 . 컴파일러는 아직 "최적화"로 지나치게 별 나지 않지만 컴파일러 작성자는 점점 더 똑똑해 지려고 노력하고 있습니다. 의도적 인 의사 난수 변수로 변수를 초기화하는 컴파일러 옵션은 오류를 식별하는 데 유용 할 수 있지만 마지막 값을 유지하는 스토리지를 그대로두면 오류가 마스킹 될 수 있습니다.
supercat

22

구문 적 관점보다는 유지 관리 성 측면에서 최소한 세 가지 사고가 있습니다.

  1. 함수 시작 부분에 모든 변수를 선언하면 한 위치에 있고 포괄적 인 목록을 한눈에 볼 수 있습니다.

  2. 모든 변수를 처음 사용한 장소에 최대한 가깝게 선언하면 각 변수 가 필요한지 알 수 있습니다.

  3. 가장 안쪽 범위 블록의 시작 부분에 모든 변수를 선언하므로 가능한 한 빨리 범위를 벗어나 컴파일러가 메모리를 최적화하고 의도하지 않은 위치에서 실수로 변수를 사용하는지 알려줍니다.

나는 일반적으로 첫 번째 옵션을 선호합니다. 다른 사람들은 종종 선언을위한 코드를 찾아야합니다. 모든 변수를 미리 정의하면 디버거에서 쉽게 초기화하고 볼 수 있습니다.

때로는 더 작은 스코프 블록 내에서 변수를 선언하지만, 타당한 이유가 있습니다. 그 이유는 매우 적습니다. fork()자식 프로세스에서만 필요한 변수를 선언하기 위해 a 다음에 한 가지 예가 있습니다 . 나에게이 시각적 표시기는 그 목적을 알려주는 유용한 정보입니다.


27
옵션 2 또는 3을 사용하면 변수를 찾기가 더 쉽습니다. 함수가 너무 커서 변수 선언을 볼 수 없어서는 안되기 때문입니다.
Jonathan Leffler

8
옵션 3은 70 년대의 컴파일러를 사용하지 않는 한 문제가 아닙니다.
edgar.holleis

15
괜찮은 IDE를 사용했다면, 선언을 찾기위한 IDE 명령이 있어야하기 때문에 코드 헌팅을 할 필요가 없습니다. (Eclipse의 F3)
edgar.holleis

4
옵션 1에서 초기화를 보장하는 방법을 이해하지 못합니다. 블록에서 나중에 다른 함수를 호출하거나 계산을 수행하여 초기 값만 얻을 수있는 경우가 있습니다.
Plumenator

4
@Plumenator : 옵션 1은 초기화를 보장하지 않습니다. 선언시 "정확한"값으로 또는 후속 코드가 적절하게 설정되지 않은 경우 후속 코드가 중단되도록 보장하는 것으로 초기화 할 때 선택했습니다. "선택"이라고했는데,이 글을 쓴 후 기본 설정이 # 2로 바뀌었기 때문입니다. 아마도 지금은 C보다 Java를 더 많이 사용하고 있고 더 나은 개발 도구가 있기 때문입니다.
Adam Liss

6

다른 사람들이 언급했듯이, GC는 'pedantic'검사를 사용하지 않는 한 'C89'모드에 있더라도 이와 관련하여 (그리고 호출 된 인수에 따라 다른 컴파일러) 가능합니다. 솔직히 말하면, pedantic을 사용하지 않는 좋은 이유는 많지 않습니다. 고품질의 현대 코드는 항상 경고없이 컴파일해야합니다 (또는 컴파일러에게 의심스러운 특정 작업을 실수로 의심하는 경우는 거의 없습니다). 따라서 코드 설정을 사용하여 코드를 컴파일 할 수 없으면주의를 기울여야합니다.

C89는 변수가 각 범위 내에서 다른 문장보다 먼저 선언되어야하며, 이후 표준에서는 사용하기에 더 가까운 선언 (보다 직관적이고 효율적일 수 있음), 특히 'for'루프에서 루프 제어 변수의 동시 선언 및 초기화를 허용합니다.


0

언급 된 바와 같이, 여기에는 두 가지 사고 학교가 있습니다.

1) 연도가 1987 년이므로 모든 기능을 최상으로 선언하십시오.

2) 최초 사용에 가장 가깝고 가능한 가장 작은 범위에서 선언하십시오.

이것에 대한 나의 대답은 DO BOTH입니다! 설명하겠습니다 :

긴 기능의 경우 1) 리팩토링을 매우 어렵게 만듭니다. 개발자가 서브 루틴 개념에 반대하는 코드베이스에서 작업하는 경우 함수 시작시 50 개의 변수 선언이 있으며 그 중 일부는 for 루프의 "i"일 수 있습니다. 기능의 하단.

따라서 나는 이것으로부터 최고 -PTSD 선언을 개발했고 옵션 2)를 종교적으로하려고했다.

짧은 기능 하나 때문에 옵션 1로 돌아 왔습니다. 함수가 충분히 짧으면 지역 변수가 거의 없으며 함수가 짧기 때문에 함수의 맨 위에 놓으면 첫 번째 사용에 가깝습니다.

또한 맨 위에 선언하고 싶지만 초기화에 필요한 계산을 수행하지 않은 경우 "선언 및 NULL로 설정"의 반 패턴은 초기화해야하는 항목이 인수로 수신되기 때문에 해결되지 않습니다.

이제 내 생각은 함수의 맨 위에서 선언하고 가능한 한 처음 사용하는 것에 가깝다는 것입니다. 둘 다! 그리고이를 수행하는 방법은 잘 나누어 진 서브 루틴을 사용하는 것입니다.

그러나 긴 기능을 사용하는 경우 메소드를 추출하는 것이 더 쉽기 때문에 처음 사용하는 것에 가장 가까운 것을 넣으십시오.

내 레시피는 이것입니다. 모든 지역 변수의 경우 변수를 가져 와서 선언을 맨 아래로 이동하고 컴파일 한 다음 선언을 컴파일 오류 바로 앞으로 이동하십시오. 그것은 첫 번째 사용입니다. 모든 지역 변수에 대해이 작업을 수행하십시오.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

이제 선언 전에 시작되는 스코프 블록을 정의하고 프로그램이 컴파일 될 때까지 끝을 이동하십시오.

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

foo를 사용하는 코드가 더 있으므로 컴파일되지 않습니다. 우리는 컴파일러가 foo를 사용하지 않기 때문에 bar를 사용하는 코드를 통과 할 수 있음을 알 수 있습니다. 이 시점에서 두 가지 선택이 있습니다. 기계적인 방법은 컴파일 할 때까지 "}"를 아래쪽으로 옮기는 것이고, 다른 방법은 코드를 검사하고 순서를 다음과 같이 바꿀 수 있는지 확인하는 것입니다.

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

주문을 전환 할 수 있다면 임시 값의 수명이 단축되기 때문에 원하는 것일 수 있습니다.

주목해야 할 또 다른 사항은 foo의 값을 사용하는 코드 블록 사이에서 foo의 값을 유지해야하는지 또는 둘 다에서 다른 foo 일 수 있다는 것입니다. 예를 들어

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

이러한 상황은 내 절차보다 더 필요합니다. 개발자는 코드를 분석하여 수행 할 작업을 결정해야합니다.

그러나 첫 번째 단계는 첫 번째 사용법을 찾는 것입니다. 시각적으로 할 수는 있지만 때로는 선언을 삭제하고 컴파일하고 첫 번째 사용 위에 다시 넣는 것이 더 쉽습니다. 첫 번째 사용이 if 문 안에 있으면 그것을 넣고 컴파일하는지 확인하십시오. 그러면 컴파일러는 다른 용도를 식별합니다. 두 용도를 모두 포함하는 스코프 블록을 만드십시오.

이 기계적인 부분이 끝나면 데이터의 위치를 ​​분석하기가 더 쉬워집니다. 변수가 큰 범위 블록에서 사용되는 경우 상황을 분석하고 두 개의 다른 항목에 대해 동일한 변수를 사용하고 있는지 확인하십시오 (예 : "i"와 같이 두 개의 for 루프에 사용됨). 용도와 관련이없는 경우, 관련없는 용도마다 새 변수를 작성하십시오.


0

함수에서 맨 위 또는 "로컬"로 모든 변수를 선언해야합니다. 정답은:

사용하는 시스템 종류에 따라 다릅니다 .

1 / 내장형 시스템 (특히 비행기 또는 자동차와 같은 생활과 관련됨) : 동적 메모리 (예 : calloc, malloc, new ...)를 사용할 수 있습니다. 1000 명의 엔지니어와 함께 매우 큰 프로젝트를 수행한다고 가정 해보십시오. 새로운 동적 메모리를 할당하고 더 이상 사용하지 않는 경우 제거하지 않은 경우 어떻게합니까? 내장 시스템이 장시간 실행되면 스택 오버플로가 발생하고 소프트웨어가 손상됩니다. 품질을 확인하기 쉽지 않습니다 (가장 좋은 방법은 동적 메모리 금지).

비행기가 30 일 안에 운행되고 꺼지지 않으면 소프트웨어가 손상된 경우 (비행기가 여전히 공중에있을 때) 어떻게됩니까?

2 / 웹, PC와 같은 다른 시스템 (메모리 공간이 큼) :

사용하는 메모리를 최적화하려면 변수를 "로컬로"선언해야합니다. 이러한 시스템이 오랫동안 실행되고 스택 오버플로가 발생하는 경우 (다른 사람이 동적 메모리 제거를 잊었 기 때문에) PC를 재설정하는 간단한 작업 만 수행하십시오 .P 삶에 영향을 미치지 않습니다.


이것이 올바른지 확실하지 않습니다. 모든 지역 변수를 한곳에서 선언하면 메모리 누수를 감사하는 것이 더 쉽다고 말하고 있습니까? 사실 일지도 모르겠지만, 잘 모르겠습니다. 포인트 (2)에 관해서는 변수를 로컬로 선언하면 "메모리 사용을 최적화"한다고 말합니까? 이것은 이론적으로 가능합니다. 컴파일러는 메모리 사용을 최소화하기 위해 함수 과정에서 스택 프레임의 크기를 조정하도록 선택할 수는 있지만 그렇게하는 것은 알지 못합니다. 실제로 컴파일러는 모든 "로컬"선언을 "장면에서 함수 시작"으로 변환합니다.
QuinnFreedman

1 / 임베디드 시스템은 때때로 동적 메모리를 허용하지 않으므로 모든 변수를 함수 상단에 선언하면. 소스 코드가 빌드되면 프로그램을 실행하기 위해 스택에 필요한 바이트 수를 계산할 수 있습니다. 그러나 동적 메모리를 사용하면 컴파일러에서 동일한 작업을 수행 할 수 없습니다.
Dang_Ho

2 / 변수를 로컬로 선언하면 해당 변수는 "{}"열기 / 닫기 괄호 안에 만 있습니다. 따라서 변수가 "범위를 벗어난"경우 컴파일러는 변수 공간을 해제 할 수 있습니다. 함수 상단에 모든 것을 선언하는 것보다 낫습니다.
Dang_Ho

정적 메모리와 동적 메모리가 혼란 스럽다고 생각합니다. 정적 메모리는 스택에 할당됩니다. 함수에서 선언 된 모든 변수는 선언 된 위치에 관계없이 정적으로 할당됩니다. 동적 메모리는 다음과 같은 힙으로 할당됩니다 malloc(). 장치를 사용할 수없는 장치는 본 적이 없지만 임베디드 시스템에서 동적 할당을 피하는 것이 가장 좋습니다 ( 여기 참조 ). 그러나 함수에서 변수를 선언하는 위치와는 아무런 관련이 없습니다.
QuinnFreedman

1
이것이 합리적인 운영 방법이라는 데 동의하지만 실제로는 그렇지 않습니다. 다음은 예제와 매우 유사한 실제 어셈블리입니다. godbolt.org/z/mLhE9a . 보시다시피 11 행 에서 if 문 외부sub rsp, 1008전체 배열을위한 공간을 할당 하고 있습니다. 이것은 마찬가지입니다 및 I 시도 모든 버전 및 최적화 수준. clanggcc
QuinnFreedman

-1

명확한 설명을 위해 gcc 버전 4.7.0에 대한 설명서의 일부 문장을 인용하겠습니다.

"컴파일러는 'c90'또는 'c ++ 98'과 같은 몇 가지 기본 표준과 'gnu90'또는 'gnu ++ 98'과 같은 표준의 GNU 방언을 수용 할 수 있습니다. 기본 표준을 지정하여 컴파일러 예를 들어, '-std = c90'은 asm 및 typeof 키워드와 같이 ISO C90과 호환되지 않는 GCC의 특정 기능을 끕니다 (예 : '-std = c90'). ":: 표현식의 중간 용어를 생략하는 등 ISO C90에서 의미가없는 다른 GNU 확장"

귀하의 질문의 핵심은 "-std = c89"옵션을 사용하더라도 왜 gcc가 C89를 준수하지 않는 것입니다. gcc의 버전을 모르지만 큰 차이는 없을 것이라고 생각합니다. gcc 개발자는 "-std = c89"옵션은 C89와 모순되는 확장 기능이 꺼져 있다고 말합니다. 따라서 C89에서 의미가없는 일부 확장과는 아무런 관련이 없습니다. 변수 선언의 배치를 제한하지 않는 확장은 C89와 모순되지 않는 확장에 속합니다.

솔직히 말하면, 모든 사람들은 "-std = c89"옵션의 첫눈에 C89를 완전히 준수해야한다고 생각할 것입니다. 그러나 그렇지 않습니다. 처음에 모든 변수를 선언하는 것이 좋거나 나쁘다는 문제는 습관의 문제입니다.


준수는 확장을 허용하지 않는다는 의미는 아닙니다. 컴파일러가 유효한 프로그램을 컴파일하고 다른 프로그램에 필요한 진단을 생성하는 한 준수합니다.
Monica를 기억하십시오

@Marc Lehmann, 그렇습니다. "conform"이라는 단어가 컴파일러를 차별화하는 데 사용될 때 맞습니다. 그러나 "적합"이라는 단어가 일부 사용법을 설명하는 데 사용될 때 "사용이 표준을 준수하지 않습니다"라고 말할 수 있습니다. 그리고 모든 초보자는 표준을 준수하지 않는 사용법이 오류를 초래할 것이라는 의견을 가지고 있습니다.
junwanghe

그러나 @Marc Lehmann은 gcc가 C89 표준을 준수하지 않는 사용법을 볼 때 진단이 없습니다.
junwanghe

"gcc가 준수하지 않는다"고 주장하는 것이 "일부 사용자 프로그램이 준수하지 않습니다"와 동일하지 않기 때문에 귀하의 대답은 여전히 ​​틀립니다. 순응의 사용법은 간단하지 않습니다. 게다가, 내가 초보자 였을 때 나는 당신이 말하는 의견이 아니 었으므로, 그것은 또한 잘못된 것입니다. 마지막으로, 적합한 컴파일러가 비 적합 코드를 진단 할 필요는 없으며, 실제로는 구현이 불가능합니다.
Monica를 기억하십시오
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.