58 이름이 쓰일 수 있는 범위 — 스코프
먼저 알아야 할 것
돌아보기
25장에서 「블록 안에서 선언된 이름은 그 블록 안에서만 보인다」고 했다. 그렇다면 이 코드에서 n 이라는 이름은 몇 개인가?
for (int n = 0; n < 3; n++) { }
for (int n = 10; n > 8; n--) { }답. 둘이다. 그리고 서로 아무 관계가 없다. 이름이 같다고 같은 것이 아니라, 같은 범위 안에서 같은 이름일 때만 같은 것이다. 이 장은 그 「범위」를 끝까지 따진다 — 범위가 몇 종류인지, 어디서 시작해서 어디서 끝나는지, 그리고 범위가 겹칠 때 무슨 일이 벌어지는지.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 왜 레이블만 예외인가?
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 int n;— 스코프는 블록, 수명은 정적, 연결은 없음 - 파일 수준의
static int n;— 스코프는 파일, 수명은 정적, 연결은 내부 - 파일 수준의
int n;— 스코프는 파일, 수명은 정적, 연결은 외부 - 함수 안의
int n;— 스코프는 블록, 수명은 자동, 연결은 없음
흔한 오해. 이름이 안 보이면 물건도 없다
아니다. 함수 안의 static 변수는 함수를 나가는 순간 이름이 보이지 않지만 물건은 그대로 있고, 다음에 들어오면 그 값이 남아 있다. 반대로 주소를 넘겨 두면 이름이 닿지 않는 곳에서도 물건을 계속 만질 수 있다 — 포인터가 하는 일이 정확히 그것이다.
★ 그래서 위험한 조합은 「이름은 사라졌는데 물건도 사라진」 자리다. 블록을 나간 지역 변수의 주소를 계속 들고 있는 것이 그 대표다(45장).
이제 이름이 어디서 통하는지 정확히 말할 수 있다. 다음 장은 다른 질문이다 — 같은 자리, 같은 글자인데도 서로 부딪히지 않는 이름들이 있다. C 에는 이름이 사는 마당이 넷 있기 때문이다.