Menu

C 언어 스코프: 블록, 함수, 파일 스코프 완전 정리

C 변수가 어디서 보이고 얼마나 오래 사는지 - 블록 스코프, 함수 매개변수, 파일 스코프 전역 변수, 호출 사이에 살아남는 static 지역 변수, static 함수, 섀도잉, 그리고 전역 변수가 문제를 일으키는 이유.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

모든 변수에 대한 두 가지 질문

C의 모든 변수는 별개의 두 성질을 가지고 있고, 이 둘을 헷갈리는 것이 이 페이지에 나오는 놀라움 대부분의 원인입니다.

  • 스코프 - 이름을 어디서 쓸 수 있는가. 컴파일 타임의 질문입니다.
  • 수명 - 저장 공간이 얼마나 오래 존재하는가. 런타임의 질문입니다.

보통은 함께 움직이지만(평범한 지역 변수는 자기 블록 안에서 보이고 딱 그만큼 삽니다), static이 둘을 떼어 놓습니다. 그래서 여기서 이해할 가치가 있는 키워드입니다.

블록 스코프

중괄호 안에 선언된 변수는 선언 지점부터 닫는 중괄호까지 보이고, 그 밖에서는 보이지 않습니다.

안쪽 블록은 자기가 그 안에 중첩되어 있으므로 outer를 볼 수 있습니다. 바깥쪽 블록은 inner를 볼 수 없습니다. 이는 모든 중괄호 쌍 - 함수 본문, if 본문, 반복문 본문, 또는 위처럼 맨 블록 - 에 적용됩니다.

반복문이 이를 구체적으로 보여 줍니다. for 헤더에 선언된 카운터는 반복문의 것입니다.

for (int i = 0; i < 5; i++) {
    /* 여기서 i가 산다 */
}
/* 여기서 i는 존재하지 않는다 */

이것이 바로 바라는 바입니다. 같은 함수 안의 두 반복문이 각각 i를 간섭 없이 쓸 수 있고, 반복문이 끝난 뒤 실수로 카운터를 읽을 수도 없습니다. 나중에 값이 필요하다면 - 탐색이 멈춘 인덱스 같은 것 - 반복문 앞에서 선언하세요.

함수 매개변수는 함수 본문의 스코프를 가지므로, 본문 맨 위에 선언된 지역 변수처럼 행동합니다.

C99는 블록의 맨 위뿐 아니라 어디서나 선언을 허용합니다. 변수는 처음 필요한 자리에서 선언하세요. 스코프가 좁은 변수는 틀릴 여지가 적은 변수입니다.

수명: 자동 저장 공간

평범한 지역 변수는 자동 저장 공간을 가집니다. 제어가 그 블록에 들어올 때 생겨나고 떠날 때 없어집니다. 그 메모리는 스택에 있습니다.

1을 세 번 출력합니다. 각 호출은 0으로 초기화된 완전히 새로운 count를 얻고, 반환할 때 버립니다. 재귀 함수의 각 프레임이 모든 지역 변수의 자기 사본을 가지는 이유이기도 합니다.

분명히 말해 둘 만한 결과가 둘 있습니다. 초기화하지 않은 지역 변수는 0이 아니라 쓰레기 값을 담습니다. 스택에 있던 바이트가 무엇이든 그대로입니다. 그리고 지역 변수의 포인터를 반환하는 것은 미정의 동작입니다. 함수가 반환하는 순간 그 저장 공간이 사라지기 때문입니다.

/* 버그: buf는 반환 후에 존재하지 않는다 */
char *broken(void) {
    char buf[64] = "hello";
    return buf;
}

static 지역 변수: 호출 사이의 기억

지역 변수에 static을 붙이면 수명이 프로그램 전체가 되고, 스코프는 그대로 남습니다.

이제 카운터가 1, 2, 3을 출력합니다. = 0은 각 호출이 아니라 main이 시작하기 전에 한 번 실행됩니다.

static 지역 변수의 규칙 셋.

  • 초기화자는 상수 식이어야 합니다. 런타임이 아니라 프로그램 시작 시점에 적용되기 때문입니다.
  • 초기화자가 없으면 static은 0으로 초기화됩니다(쓰레기 값을 담는 자동 지역 변수와 다릅니다).
  • 변수는 여전히 함수에 속한 비공개입니다. 다른 어떤 코드도 그 이름을 부를 수 없습니다.

마지막 항목이 매력의 전부입니다. 함수가 무엇이든 수정할 수 있는 전역 변수를 노출하지 않고도 호출 사이에 무언가를 기억할 수 있습니다. 호출 카운터, 첫 사용 때 만들어 두는 조회 테이블 캐시, 일회성 초기화 플래그에 쓰세요.

대가는 함수가 더 이상 입력에서 출력으로의 순수한 사상이 아니게 된다는 것입니다. 같은 인자가 다른 답을 줄 수 있어서 테스트하기 어려워지고, 보호 없이 여러 스레드에서 동시에 호출하면 안전하지 않습니다. static 지역 변수는 편의가 아니라 의도적으로 쓰세요.

파일 스코프: 전역 변수

모든 함수 바깥에 선언된 변수는 파일 스코프를 가집니다. 선언 지점부터 파일 끝까지 그 아래의 모든 함수에서 보이고, 수명은 프로그램 전체입니다.

전역 변수는 기본적으로 0으로 초기화되므로, = 0이 없어도 totalOperations0에서 시작합니다.

기본적으로 전역 변수는 외부 링키지도 가집니다. 같은 프로그램의 다른 .c 파일이 extern으로 선언해 닿을 수 있습니다.

/* stats.c 안 */
int totalOperations = 0;          /* 정의 - 프로그램에 정확히 하나 */

/* main.c 안 */
extern int totalOperations;       /* 선언: "어딘가에 존재한다" */

extern 선언은 헤더 파일에 넣어 모든 사용자가 같은 것을 보게 하세요. 비대칭을 눈여겨보세요. extern 선언은 변수가 존재한다고 말할 뿐이고, 정확히 하나의 .c 파일이 실제로 정의해야 합니다.

전역 변수가 무는 이유

전역 변수는 데이터를 공유하는 가장 쉬운 방법이자, 범위를 좁힐 수 없는 버그를 만드는 가장 쉬운 방법입니다. 구체적인 문제가 넷 있습니다.

무엇이든 바꿀 수 있습니다. totalOperations가 틀린 값을 담고 있을 때, 범인은 어느 파일의 어느 줄이든 될 수 있습니다. 매개변수라면 용의자는 눈에 보이는 호출 지점들뿐입니다.

함수를 테스트할 수 없게 됩니다. 전역 변수를 읽는 함수는 따로 떼어 호출할 수 없습니다. 먼저 전역 상태를 차려 놓고, 나중에 되돌리는 것도 잊지 말아야 합니다.

이름이 프로그램 전체를 차지합니다. countbuffer라는 전역 변수는 결국 다른 누군가의 것과 충돌합니다.

초기화식이 컴파일 타임 상수여야 합니다. C에서 전역 변수는 상수 표현식으로만 초기화할 수 있습니다. int limit = readConfig();는 컴파일되지 않고, 파일 스코프의 int b = a * 2;도 마찬가지입니다. 계산해서 얻는 값은 어떤 설정 함수가 실행 시점에 대입해야 하며, 그 호출이 돌기 전에 그 전역 변수를 건드리는 코드는 조용히 0을 보게 됩니다.

대안은 거의 항상 있습니다. 값을 매개변수로 넘기고, 결과를 반환하고, 연관된 상태를 구조체로 묶어 명시적으로 주고받으세요. 진짜 예외도 있습니다. 프로그램 전체 설정 객체나 로깅 핸들 같은 것들인데, 그조차도 접근자 함수와 함께 파일 스코프의 static으로 두는 편이 낫습니다. 다음 절의 내용입니다.

파일 스코프의 static: 비공개

전역 변수나 함수에 붙은 static은 지역 변수에 붙었을 때와 완전히 다른 것을 뜻합니다. 내부 링키지입니다. 이름이 자기 .c 파일에만 속하게 되고 다른 어디서도 닿을 수 없습니다.

/* counter.c */
#include "counter.h"

static int count = 0;           /* 이 파일에만 속한다 - 다른 파일은 건드릴 수 없다 */

static void validate(void) {    /* 비공개 헬퍼, 인터페이스의 일부가 아니다 */
    if (count < 0) count = 0;
}

void increment(void) {          /* 공개: counter.h에 선언되어 있다 */
    count++;
    validate();
}

int getCount(void) {            /* 공개 */
    return count;
}

다른 파일은 incrementgetCount를 호출할 수 있지만 count를 볼 수 없고 validate를 호출할 수 없습니다. 링커가 그 이름들을 해결해 주지 않습니다. 그것이 C의 모듈 시스템입니다. 공개 함수를 선언하는 헤더와, 나머지 전부에 붙는 static이 전부입니다.

깔끔함 말고도 실질적인 이점이 둘 있습니다. 두 파일이 각각 static void validate(void)를 가져도 충돌하지 않습니다. 함수가 공개였다면 불가능했을 일입니다. 그리고 컴파일러는 static 함수에 파일 밖의 호출자가 없음을 알기에 더 적극적으로 인라인하거나 쓰이지 않는다고 경고할 수 있습니다.

그래서 이 키워드는 어디에 나타나느냐에 따라 뚜렷이 다른 두 의미를 가집니다.

지역 변수에 붙은 static   ->  수명이 영구가 된다 (스코프는 그대로)
파일 스코프의 static      ->  링키지가 내부가 된다 (수명은 그대로)

섀도잉

바깥 스코프에 이미 있는 이름을 안쪽 스코프에 선언하면 바깥 이름을 가립니다(섀도잉). 안쪽 스코프 안에서 그 이름은 새 변수를 가리키고 바깥 것에는 닿을 수 없습니다.

이것은 합법이고 가끔은 의도적입니다. 더 자주는 사고이고, 비싼 사고입니다. 전역을 갱신하려던 함수가 같은 이름의 지역 변수를 갱신해 버리고 전역은 결코 바뀌지 않습니다. 지역 변수가 매개변수를 가릴 때도 같은 일이 벌어져, 입력에 하려던 대입이 아무 데도 가지 않습니다.

gcc -Wshadow가 모든 경우를 보고합니다. -Wall에는 들어 있지 않으니 따로 켜세요.

gcc -Wall -Wextra -Wshadow program.c -o program

이 문제를 아예 피하는 습관은 타입이 아니라 역할로 이름 짓는 것입니다. totalScoreitemScore는 서로 가릴 수 없지만, 둘 다 n이라 불리는 변수는 언젠가 그렇게 됩니다.

빠른 참조

블록 안에 선언             블록 스코프, 자동 수명, 초기화 안 하면 쓰레기 값
함수 매개변수              함수 본문의 블록 스코프, 인자의 사본
함수 안의 static           블록 스코프, 프로그램 수명, 0으로 초기화, 호출 사이 유지
모든 함수 바깥에 선언       파일 스코프, 프로그램 수명, 0으로 초기화, 다른 파일에서 보임
모든 함수 바깥의 static     파일 스코프, 프로그램 수명, 이 .c 파일에만 속함
extern 선언                다른 파일에 정의된 변수를 가리킴

자주 묻는 질문

C에서 스코프란 무엇인가요?

이름이 보이는 코드의 영역입니다. 블록(중괄호 한 쌍) 안에 선언된 변수는 그 블록 안에서만 보이는데, 이것이 블록 스코프입니다. 모든 함수 바깥에 선언된 변수는 파일 스코프를 가지며 선언 지점부터 파일 끝까지 보입니다.

C에서 static은 지역 변수에 무슨 일을 하나요?

스코프는 바꾸지 않고 변수의 수명을 바꿉니다. static 지역 변수는 한 번 만들어지고 한 번 초기화되며 호출 사이에 값을 유지하지만, 여전히 그 함수 안에서만 보입니다. 전역 변수를 쓰지 않고 함수에 기억을 주는 방법입니다.

C에서 static과 전역 변수의 차이는 무엇인가요?

그냥 전역 변수는 프로그램의 모든 파일에서 보이고, 다른 파일이 extern으로 닿을 수 있습니다. 파일 스코프의 static 변수나 함수는 그 하나의 .c 파일에만 속하며, 다른 파일이 링크할 수 없습니다. 파일 스코프의 static숨김에 대한 것이고, 지역 변수의 static수명에 대한 것입니다.

C에서 전역 변수가 왜 나쁜가요?

어떤 함수든 바꿀 수 있어서 버그가 프로그램 어디서든 생길 수 있고, 전역 변수를 읽는 함수는 따로 떼어 테스트할 수 없으며, 이름이 프로그램 전체를 차지해 충돌을 부릅니다. 대신 값을 매개변수로 넘기고 결과를 반환하세요. 공유 상태가 정말 필요하면 파일 스코프의 static으로 만들어 한 파일만 건드릴 수 있게 하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기