Proven C Book←↑→

58 이름이 쓰일 수 있는 범위 — 스코프

먼저 알아야 할 것

25장 함수 선언과 정의 · 블록과 매개변수
45장 수명과 저장 기간 · 이름이 아니라 물건이 사는 기간
56장 여러 파일로 나누어 짓기 · 연결 — 이름이 파일 경계를 넘는가

돌아보기

25장에서 「블록 안에서 선언된 이름은 그 블록 안에서만 보인다」고 했다. 그렇다면 이 코드에서 n 이라는 이름은 몇 개인가?

for (int n = 0; n < 3; n++) { }
for (int n = 10; n > 8; n--) { }

답. 둘이다. 그리고 서로 아무 관계가 없다. 이름이 같다고 같은 것이 아니라, 같은 범위 안에서 같은 이름일 때만 같은 것이다. 이 장은 그 「범위」를 끝까지 따진다 — 범위가 몇 종류인지, 어디서 시작해서 어디서 끝나는지, 그리고 범위가 겹칠 때 무슨 일이 벌어지는지.

이 장의 필요성과 맥락

스코프는 지금까지 여러 장에 나뉘어 조금씩 나왔다 — 함수에서 블록으로, 루프에서 첫 칸의 변수로, 여러 파일에서 연결로. 각각의 자리에서는 그때 필요한 만큼만 말할 수 있었다. 이제 이름의 세계로 들어가기 직전에, 흩어져 있던 것을 한자리에 모아 정확하게 정리할 때다. 다음 장의 이름 공간도, 그다음 장의 이름 충돌도 이 장의 낱말 위에서 이야기된다.

이 장이 끝나면

표준이 정한 네 가지 스코프를 하나씩 확인하고, 스코프가 어느 지점에서 시작하는지를 따진다(등호 앞이 아니다). 가림(shadowing)이 무엇이며 왜 위험하고 왜 금지되지 않는지, 레이블이라는 예외, 그리고 전처리기가 스코프를 모른다는 결정적인 대비까지. 마지막으로 스코프·수명·연결 셋을 나란히 세운다 — 자주 뒤섞이는 세 낱말이다.

이 장에서 답할 질문

  1. 왜 레이블만 예외인가?

58.1 스코프란 무엇인가 — 「보이는 곳」이 아니라 「그 이름이 그것을 뜻하는 곳」#

스코프(scope)를 「이름이 보이는 범위」라고 배웠다. 한 걸음 더 정확히 말하면 이렇다 — 어떤 식별자가 특정한 것을 가리키도록 정해져 있는 프로그램의 구간이다. 같은 글자가 다른 구간에서는 다른 것을 뜻할 수 있고, 아무것도 뜻하지 않을 수도 있다.

그래서 물어야 할 것은 「이 이름이 존재하는가」가 아니라 「여기서 이 이름은 무엇을 뜻하는가」다. 이 차이가 이 장 전체를 관통한다.

스코프는 이름의 성질이다. 물건의 성질이 아니다. 이름이 안 보이는 곳에서도 물건은 멀쩡히 살아 있을 수 있고(45장), 반대로 이름이 보이는데 물건은 아직 없거나 이미 없을 수도 있다.

58.2 네 가지 스코프#

표준은 스코프를 넷으로 나눈다(C23 §6.2.1). 실무에서 셋은 자주 만나고, 하나는 알아 두지 않으면 오해를 부른다.

스코프어디서부터어디까지무엇이 여기 속하나
파일 스코프선언한 지점그 번역 단위의 끝함수 밖에서 선언한 모든 것
블록 스코프선언한 지점그 블록의 닫는 중괄호지역 변수, 매개변수, for 첫 칸
함수 원형 스코프매개변수 이름원형의 닫는 괄호원형에 적은 매개변수 이름
함수 스코프함수 어디서든함수 전체레이블뿐

표 58.1 — 표준이 정한 네 가지 스코프

examples/ch56/scopes.c

/* 이름이 보이는 네 가지 범위 --- 표준이 정한 그대로. */
#include <stdio.h>

/* ① 파일 스코프: 여기서부터 이 파일 끝까지 보인다 */
static int file_level = 1;

/* ② 함수 원형 스코프: 매개변수 이름 rows·cols 는 이 괄호가 닫히면 사라진다.
      이름이 사라져도 쓸모는 있다 --- 뒤 매개변수의 크기를 이 이름으로 적는다. */
void print_grid(int rows, int cols, int grid[rows][cols]);

/* ③ 함수 스코프: 레이블은 함수 전체에서 보인다. 블록 안에 적어도 그렇다. */
static int find_first_negative(const int *a, int n)
{
    for (int i = 0; i < n; i++) {
        if (a[i] < 0) {
            goto found;          /* 아래 블록 안의 레이블로 뛴다 */
        }
    }
    return -1;
    {
    found:                       /* 블록 안에 있어도 함수 어디서나 보인다 */
        return 0;
    }
}

void print_grid(int rows, int cols, int grid[rows][cols])
{
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) printf(" %d", grid[r][c]);
        putchar('\n');
    }
}

int main(void)
{
    /* ④ 블록 스코프: 이 중괄호 안에서만 */
    int block_level = 2;
    {
        int inner = 3;
        printf("inner block sees: file=%d block=%d inner=%d\n",
               file_level, block_level, inner);
    }
    /* 여기서 inner 는 이미 이름이 아니다 */

    int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
    puts("\nthe grid, sized by names from the prototype scope:");
    print_grid(2, 3, grid);

    int values[] = {5, 7, -2, 9};
    printf("\nfirst negative found: %s\n",
           find_first_negative(values, 4) == 0 ? "yes" : "no");
    return 0;
}

실행 결과

inner block sees: file=1 block=2 inner=3

the grid, sized by names from the prototype scope:
 1 2 3
 4 5 6

first negative found: yes

시연이 넷을 한 번에 보인다. 눈여겨볼 것은 아래의 둘이다.

함수 원형 스코프는 이름이 곧 사라지는데도 쓸모가 있다. 원형에 적은 매개변수 이름은 괄호가 닫히는 순간 사라진다 — 그래서 void f(int n); 과 void f(int); 는 완전히 같은 선언이다. 그런데도 이름을 적는 이유가 둘이다. 사람이 읽기 위해서이고, 뒤 매개변수의 크기를 그 이름으로 적기 위해서다.

void print_grid(int rows, int cols, int grid[rows][cols]);

rows와 cols는 셋째 매개변수의 모양을 적는 데 쓰인 뒤 사라진다. 이름이 없으면 쓸 수 없는 표기다(40장).

레이블만 함수 스코프다. 시연의 found: 는 안쪽 블록에 적혀 있는데도 함수 어디서나 보인다. goto가 블록 경계를 넘어 뛸 수 있는 근거가 이것이다. 그리고 이것은 레이블만의 특권이다 — 변수는 절대 이렇게 동작하지 않는다.

문. 왜 레이블만 예외인가?

답. 레이블은 값을 갖지 않기 때문이다. 변수 이름이 블록에 갇히는 진짜 이유는 그 이름이 가리키는 물건이 블록과 함께 생겼다 사라지기 때문인데, 레이블이 가리키는 것은 코드의 한 지점이라 생기고 사라질 것이 없다. 그래서 가둘 이유도 없다.

★ 다만 「뛸 수 있다」와 「뛰어도 좋다」는 다르다. 블록 안으로 뛰어들면 그 블록의 변수 선언을 건너뛰게 되고, 가변 길이 배열이 있는 블록으로 뛰어드는 것은 아예 금지되어 있다.

58.3 스코프는 어디서 시작하는가 — 등호 앞이다#

이름을 선언하면 그 이름의 스코프는 선언자가 끝나는 지점부터 시작한다. 초기화를 적는 = 보다 앞이다. 사소해 보이지만 결과가 크다.

examples/ch56/scope_start.c

/* 스코프는 '선언자가 끝난 지점'부터 시작한다 --- 등호 앞이 아니라. */
#include <stdio.h>

typedef int meter;          /* 파일 스코프의 타입 이름 */

int main(void)
{
    /* i 의 스코프는 = 를 만나기 *전에* 이미 시작했다.
       그래서 sizeof(i) 는 방금 선언한 그 i 를 가리킨다(값은 읽지 않는다). */
    int i = sizeof(i);
    printf("int i = sizeof(i);  -> %d\n", i);

    /* 자기 자신의 주소를 담는 포인터도 같은 이치로 성립한다 */
    void *self = &self;
    printf("void *self = &self; -> self == &self is %s\n",
           self == (void *)&self ? "true" : "false");

    {
        /* 이 자리에서 meter 는 타입 이름이다. 그러나 아래 한 줄이 지나면
           이 블록에서 meter 는 *변수*다 --- 선언자가 끝난 지점부터. */
        meter meter = 42;
        printf("meter meter = 42;   -> %d  (the type name is now shadowed)\n", meter);
    }

    /* 블록을 벗어나면 meter 는 다시 타입 이름이다 */
    meter distance = 7;
    printf("outside the block, meter is a type again -> %d\n", distance);

    /* for 의 첫 칸에 선언한 이름은 루프가 끝나면 사라진다 */
    for (int n = 0; n < 3; n++) { /* n 은 여기서만 */ }
    /* 여기서 n 은 이름이 아니다 --- 같은 이름을 다시 써도 서로 무관하다 */
    for (int n = 10; n > 8; n--) { /* 앞의 n 과 아무 관계가 없다 */ }
    puts("\ntwo loops used the name n; neither knows the other");
    return 0;
}

실행 결과

int i = sizeof(i);  -> 4
void *self = &self; -> self == &self is true
meter meter = 42;   -> 42  (the type name is now shadowed)
outside the block, meter is a type again -> 7

two loops used the name n; neither knows the other

int i = sizeof(i); 가 통과하는 이유가 그것이다. = 오른쪽에 이르렀을 때 i는 이미 「방금 선언한 그 int」를 뜻한다. sizeof는 값을 읽지 않고 크기만 재므로 아무 문제도 없다. void *self = &self; 도 마찬가지로 온전하다 — 주소를 얻는 데 값은 필요 없다.

반례. 같은 규칙의 어두운 쪽

int x = 10;
{
    int x = x + 1;    /* ← 바깥 x 가 아니다. 자기 자신이다. */
}

안쪽 x의 스코프는 = 앞에서 이미 시작했으므로, 오른쪽의 x는 바깥의 10이 아니라 아직 값이 없는 자기 자신이다. 값을 읽었으니 미정의 동작이다 (54장). 컴파일러는 대개 -Winit-self 나 -Wuninitialized 로 일러 주지만, 경고를 켜 두지 않으면 조용히 지나간다.

처방은 규칙을 외우는 것이 아니라 이름을 다르게 짓는 것이다.

타입 이름도 같은 규칙을 따른다. 시연의 meter meter = 42; 가 그 증거다 — 앞의 meter는 아직 타입 이름이고, 선언자가 끝난 뒤부터 meter는 그 블록에서 변수다.

58.4 가림 — 안쪽이 바깥쪽을 덮는다#

스코프는 겹칠 수 있다. 안쪽 블록에서 바깥과 같은 이름을 선언하면, 그 블록 안에서 그 이름은 안쪽 것을 뜻한다. 이것을 가림(shadowing)이라 한다.

examples/ch56/shadow.c

/* 가림(shadowing), 그리고 전처리기는 스코프를 모른다는 것. */
#include <stdio.h>

int count = 100;                 /* 파일 스코프 */

static void layers(void)
{
    printf("file scope        : count = %d\n", count);
    int count = 10;              /* 파일 스코프의 count 를 가린다 */
    printf("function body     : count = %d\n", count);
    {
        int count = 1;           /* 다시 가린다 */
        printf("inner block       : count = %d\n", count);
    }
    printf("back in the body  : count = %d\n", count);
    /* 가려진 이름은 사라진 것이 아니다 --- 다른 이름으로는 여전히 닿는다 */
}

static void define_inside(void)
{
    /* 이 지시는 '함수 안' 이라는 자리와 아무 상관이 없다.
       전처리는 컴파일 전에 끝나고, 전처리기는 블록을 모른다. */
#define LIMIT 5
    printf("\ninside the function : LIMIT = %d\n", LIMIT);
}

static void far_away(void)
{
    /* 다른 함수인데도 LIMIT 이 그대로 살아 있다 --- 스코프가 아니라
       '그 지점부터 파일 끝까지'이기 때문이다. */
    printf("another function    : LIMIT = %d\n", LIMIT);
}

int main(void)
{
    layers();
    define_inside();
    far_away();
    printf("\nthe file-scope count is still %d\n", count);
    return 0;
}

실행 결과

file scope        : count = 100
function body     : count = 10
inner block       : count = 1
back in the body  : count = 10

inside the function : LIMIT = 5
another function    : LIMIT = 5

the file-scope count is still 100

가려진 이름은 사라지지 않는다. 잠시 다른 이름에 자리를 내주었을 뿐이고, 블록을 나오면 그대로 돌아온다. 시연의 마지막 줄이 그것을 확인해 준다.

문제는 이것이 합법이라는 데 있다. C 는 가림을 금지하지 않는다 — 그럴 만한 이유가 있다. 만약 금지한다면, 남이 만든 헤더에 어떤 전역 이름이 있느냐에 따라 내 지역 변수 이름이 컴파일 오류가 되어 버린다. 즉 금지하면 이름의 캡슐화가 깨진다.

그래서 규율은 언어가 아니라 도구와 습관 쪽에 있다.

수단무엇을 해 주는가
-Wshadow바깥 이름을 가리는 선언마다 경고한다
좁게 선언하기쓰는 자리에서 선언하면 가릴 것 자체가 줄어든다
이름 짓기바깥은 길게(config_limit), 안쪽은 짧게(i, n)

표 58.2 — 가림을 다루는 실무 규율

-Wshadow 가 내는 말은 이렇게 생겼다.

warning: declaration of 'count' shadows a global declaration [-Wshadow]

★ 다만 이 경고는 기본으로 켜져 있지 않고, 켜면 남의 헤더에서까지 시끄러워지는 일이 잦다. 그래서 「전부 켜고 견디기」보다 내 코드에만 켜기가 현실적인 타협이다.

58.5 전처리기는 스코프를 모른다#

이 장에서 가장 자주 사람을 놀래는 대비가 이것이다. 시연의 #define LIMIT 5 는 함수 안에 적혀 있는데도 그 함수 안에서만 유효하지 않다. 다른 함수에서도 그대로 살아 있다.

이유는 간단하다. 61장에서 볼 그 층은 컴파일 전에 끝나고, 전처리기는 블록도 함수도 모른다. 매크로에 있는 것은 스코프가 아니라 구간이다 — 정의한 지점부터 #undef 하거나 파일이 끝날 때까지.

C 의 이름매크로 이름
범위를 정하는 것블록 구조정의한 지점부터 파일 끝까지
안쪽에서 가리기된다안 된다 — 겹치면 그냥 갈아치운다
끝내는 법블록을 닫는다#undef

표 58.3 — 이름을 만드는 두 층의 규칙이 다르다

★ 그래서 매크로 이름을 대문자로 쓰는 관행은 취향이 아니라 안전 장치다. 스코프의 보호를 받지 못하는 이름이니, 눈에 띄게 만들어 사람이 피하게 하는 것이다.

58.6 스코프·수명·연결 — 자주 뒤섞이는 셋#

마지막으로 셋을 나란히 세운다. 같은 static 이라는 낱말이 자리에 따라 다른 뜻이 되는 바람에 특히 헷갈리는 자리다.

축묻는 것정하는 것
스코프여기서 이 이름은 무엇을 뜻하는가블록 구조 (이 장)
수명그 물건은 언제부터 언제까지 있는가저장 기간 (45장)
연결다른 번역 단위의 같은 이름과 같은 것인가static·extern (56장)

표 58.4 — 이름과 물건에 대한 세 가지 질문

세 축은 서로 독립이다. 그래서 이런 조합이 다 성립한다.

흔한 오해. 이름이 안 보이면 물건도 없다

아니다. 함수 안의 static 변수는 함수를 나가는 순간 이름이 보이지 않지만 물건은 그대로 있고, 다음에 들어오면 그 값이 남아 있다. 반대로 주소를 넘겨 두면 이름이 닿지 않는 곳에서도 물건을 계속 만질 수 있다 — 포인터가 하는 일이 정확히 그것이다.

★ 그래서 위험한 조합은 「이름은 사라졌는데 물건도 사라진」 자리다. 블록을 나간 지역 변수의 주소를 계속 들고 있는 것이 그 대표다(45장).

이제 이름이 어디서 통하는지 정확히 말할 수 있다. 다음 장은 다른 질문이다 — 같은 자리, 같은 글자인데도 서로 부딪히지 않는 이름들이 있다. C 에는 이름이 사는 마당이 넷 있기 때문이다.