55 이름의 세계 — 네 이름 공간과 세 축
먼저 알아야 할 것
돌아보기
54장에서 이름에는 스코프 말고 연결이라는 성질이 또 있다고 했다. 그러면 다음 코드는 왜 컴파일되는가? struct node와 node가 같은 철자인데도 서로 부딪히지 않는다.
typedef struct node node;답. 스코프도 연결도 아닌 세 번째 성질이 있기 때문이다 — 이름 공간(namespace) (name space). struct 뒤에 오는 이름과 그냥 쓰이는 이름은 컴파일러가 서로 다른 목록에서 찾는다. 같은 철자라도 찾는 목록이 다르니 충돌하지 않는다.
이 장은 그 목록이 몇 개이고 무엇인지, 그리고 스코프·연결과 어떻게 다른지를 정면으로 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 매크로는 어느 이름 공간에 사는가?
- 태그와 멤버에는 왜 연결이 없는가?
- 그러면
-Wshadow를 늘 켜야 하는가?
55.1 「C에는 이름 공간이 없다」는 말의 진실
이 말은 널리 쓰이고, 널리 오해를 부른다. 표준의 문장은 반대에 가깝다.
표준 §6.2.3 은 「한 번역 단위 안에서 같은 식별자의 선언이 여럿 보이면 문법적 문맥이 그것들을 가른다. 따라서 식별자의 범주마다 별개의 이름 공간 (separate name spaces)이 있다」고 적는다.
즉 C에는 이름 공간이 있다. 없는 것은 namespace app { … }처럼 프로그래머가 새 마당을 파는 기능이다. 이 구별을 흐리면 다음 두 문장이 둘 다 사실이라는 것을 설명할 수 없다.
typedef struct node node;는 합법이다 — 서로 다른 이름 공간이니까.- 두 라이브러리가
init이라는 함수를 각각 내보내면 링크가 깨진다 — 둘 다 같은 이름 공간에 있으니까.
흔한 오해. “C에는 이름 공간 개념 자체가 없다”
표준이 §6.2.3 에서 네 개를 명시한다. 정확한 말은 「사용자가 정의하는 이름 공간이 없다」이다.
이 오해는 실무에서 값을 치른다. 「없다」고 알고 있으면 struct stat과 함수 stat이 공존하는 POSIX 헤더를 읽을 때 무슨 일이 벌어지는지 설명하지 못하고, 열거 상수가 왜 변수와 충돌하는지도 설명하지 못한다. 「넷이 있고, 내가 새로 만들 수는 없다」가 정확한 길잡이다.
55.2 네 이름 공간
표준이 가르는 넷은 이렇다.
| 이름 공간 | 여기 사는 이름 | 어느 문법 자리에서 찾는가 |
|---|---|---|
| ① 레이블 | goto의 표적 | goto 뒤, 그리고 문장 앞의 이름: |
| ② 태그 | struct·union·enum 뒤의 이름 | struct/union/enum 키워드 바로 뒤 |
| ③ 멤버 | 구조체·공용체의 멤버 — 타입마다 따로 | .과 -> 뒤 |
| ④ 보통 식별자 | 변수·함수·typedef 이름·열거 상수·매개변수 | 그 밖의 모든 자리 |
표 55.1
세 가지를 특히 새겨 둔다.
- 멤버는 타입마다 마당이 따로다.
struct point의x와struct vec의x는 아무 관계도 없다. 구조체가 많아도 멤버 이름이 짧을 수 있는 이유다. - 태그 마당은 하나다.
struct·union·enum이 셋이서 한 마당을 나눠 쓴다. 그래서enum status가 있으면struct status는 만들 수 없다. - 열거 상수는 태그가 아니라 ④에 산다. 이것이 실무에서 가장 자주 부딪히는 자리다.
examples/ch55/four_spaces.c
/* 같은 철자 x 를 네 이름 공간에 동시에 둔다 — 표준 §6.2.3.
합법이고 컴파일된다. 읽기 좋은가는 별개의 문제다. */
#include <stdio.h>
/* ② 태그 이름 공간 — struct/union/enum 뒤에 오는 이름 */
struct x {
int x; /* ③ 멤버 이름 공간 — struct x 만의 마당 */
int y;
};
/* 다른 구조체의 멤버는 또 다른 마당이라 같은 철자를 다시 써도 된다 */
struct point { int x; int y; };
/* ④ 보통 식별자 — 변수·함수·typedef 이름·열거 상수가 모두 여기에 산다.
태그 x 와 철자가 같지만 다른 이름 공간이라 공존한다. */
typedef struct x x;
static int show(x v, struct point p)
{
/* ① 레이블 이름 공간 — goto 의 표적. 이것도 x 라 지을 수 있다 */
if (v.x < 0) goto x;
printf("struct x : x=%d y=%d\n", v.x, v.y);
printf("struct point: x=%d y=%d\n", p.x, p.y);
return 0;
x: /* 레이블 x */
puts("negative, so we came to label x");
return 1;
}
int main(void)
{
/* 네 이름 공간에 x 가 하나씩 있는 상태에서 전부 쓴다 */
x a = { .x = 10, .y = 20 }; /* typedef 이름 x */
struct x b = { .x = -1, .y = 0 }; /* 태그 x */
struct point p = { .x = 3, .y = 4 };
puts("[the same spelling x lives in four name spaces at once]");
puts(" ① label x ② tag struct x ③ member x ④ typedef name x");
puts("");
(void)show(a, p);
(void)show(b, p);
/* 이름을 찾는 자리(문법적 문맥)가 이름 공간을 고른다.
struct 뒤 → 태그, . 뒤 → 멤버, goto 뒤 → 레이블, 그 밖 → 보통 식별자. */
printf("\nsizeof(x) = %zu, sizeof(struct x) = %zu (the same type)\n",
sizeof(x), sizeof(struct x));
return 0;
}
실행 결과
[the same spelling x lives in four name spaces at once]
① label x ② tag struct x ③ member x ④ typedef name x
struct x : x=10 y=20
struct point: x=3 y=4
negative, so we came to label x
sizeof(x) = 8, sizeof(struct x) = 8 (the same type)
같은 철자 x가 네 마당에 동시에 살아도 컴파일되고 돌아간다. 컴파일러가 문법적 문맥으로 어느 마당을 뒤질지 고르기 때문이다 — goto 뒤면 레이블, struct 뒤면 태그, . 뒤면 멤버, 그 밖이면 보통 식별자.
물론 이렇게 쓰라는 뜻이 아니다. 언어가 허용하는 범위와 사람이 읽을 수 있는 범위는 다르고, 이 예제는 전자를 보이려고 일부러 극단으로 간 것이다.
문. 매크로는 어느 이름 공간에 사는가?
답. 어디에도 살지 않는다. 매크로는 언어의 이름이 아니라 전처리기의 이름이고, 전처리기는 C의 문법을 모른다(57장). 그래서 매크로 이름은 네 마당을 전부 무시하고 토큰 열을 갈아치운다.
#define max 100
struct max { int x; }; // struct 뒤인데도 100 으로 치환된다 → 오류이름 공간이 지켜 주지 못하는 유일한 자리라서, 매크로 이름을 대문자로 짓는 관행이 그토록 굳어진 것이다. 문법이 못 지키는 담장을 표기법으로 대신 세운다.
55.3 세 축 — 스코프·연결·이름 공간
이제 셋을 나란히 놓는다. 헷갈리는 이유는 셋이 같은 이름에 동시에 붙는 성질인데도 서로 독립적이기 때문이다.
| 축 | 무엇을 정하는가 | 값 | 무엇으로 정해지는가 |
|---|---|---|---|
| 스코프(24장) | 어디서 보이는가 | 블록 / 파일 / 함수 / 함수 원형 | 선언을 어디에 썼는가 |
| 연결(54장) | 다른 번역 단위의 같은 철자와 같은 것인가 | 외부 / 내부 / 없음 | static·extern과 선언 위치 |
| 이름 공간(이 장) | 어느 목록에서 찾는가 | 레이블 / 태그 / 멤버 / 보통 | 문법적 자리 |
표 55.2
세 축이 직교한다는 것을 한 줄로 확인해 둔다 — 파일 수준의 static struct node 태그는 「파일 스코프 + 연결 없음(태그에는 연결이 없다) + 태그 이름 공간」이고, 그 안의 멤버 next는 「그 구조체 안에서만 + 연결 없음 + 멤버 이름 공간」이다.
문. 태그와 멤버에는 왜 연결이 없는가?
답. 연결은 링커가 이름을 잇는 일에 대한 성질인데, 태그와 멤버는 링커에 도달하지 않기 때문이다. 타입의 이름과 그 속의 칸 이름은 컴파일이 끝나면 사라진다 — 기계어에 남는 것은 오프셋(47장)이지 이름이 아니다.
그래서 두 파일이 struct point를 각각 선언해도 링크는 멀쩡하다. 대신 두 선언이 다르면 아무도 말해 주지 않는다는 위험이 생긴다(54장의 헤더 규율이 그래서 중요하다).
55.4 태그가 따로 사는 덕분에 생긴 관행
태그와 보통 식별자가 다른 마당이라는 사실이 C 코드의 겉모습을 크게 바꿔 놓았다.
examples/ch55/tag_typedef.c
/* 태그와 typedef 가 갈리는 자리, 그리고 열거 상수가 '보통 식별자'라서
생기는 충돌. */
#include <stdio.h>
/* 태그 이름 공간과 보통 식별자 이름 공간이 달라서
"같은 철자"로 태그와 typedef 이름을 둘 다 만들 수 있다 */
typedef struct node {
int value;
struct node *next; /* 태그가 있어야 자기 자신을 가리킬 수 있다 */
} node;
/* 열거 상수는 태그가 아니라 *보통 식별자* 다.
그래서 아래 red 는 int 변수 red 와 같은 마당에 산다 — 충돌한다. */
enum color { red, green, blue };
/* 이렇게 쓰면 컴파일 오류다(주석으로만 보인다):
int red; // error: 'red' redeclared as different kind of symbol
그래서 실무는 열거 상수에 접두어를 붙인다. */
enum status { STATUS_OK, STATUS_BUSY, STATUS_FAIL };
/* 반대로 태그는 보통 식별자와 절대 충돌하지 않는다.
단, struct/union/enum 은 태그 마당을 *셋이서 함께 쓴다* — 그래서
struct status { int code; };
는 위의 enum status 와 충돌한다("defined as wrong kind of tag").
태그 마당은 넷 중 하나이지, 키워드마다 하나가 아니다. */
struct handle { int code; }; /* 다른 철자를 쓴다 */
/* 반면 보통 식별자 마당은 태그와 완전히 별개다 — 같은 철자를 써도 된다 */
static int status = 42; /* enum status 태그와 공존한다 */
static const char *name_of(enum color c)
{
switch (c) {
case red: return "red";
case green: return "green";
case blue: return "blue";
}
return "?";
}
int main(void)
{
node b = { .value = 2, .next = nullptr };
node a = { .value = 1, .next = &b };
puts("[tags and typedef names are different name spaces, so the spelling can be shared]");
for (node *p = &a; p; p = p->next)
printf(" node %d\n", p->value);
puts("\n[enumeration constants are ordinary identifiers - the same space as variables]");
printf(" enum color: %s %s %s\n", name_of(red), name_of(green), name_of(blue));
puts(" which is why int red; is a compile error - hence the habit of prefixes (STATUS_OK)");
puts("\n[struct, union and enum share one tag space - there is only one]");
struct handle h = { .code = 7 };
enum status e = STATUS_BUSY;
printf(" struct handle.code = %d, enum status = %d\n", h.code, (int)e);
puts(" you cannot declare struct status - enum status already took that tag");
puts("\n[but ordinary identifiers live in a different space from tags]");
printf(" the variable status = %d lives beside the tag status\n", status);
return 0;
}
실행 결과
[tags and typedef names are different name spaces, so the spelling can be shared]
node 1
node 2
[enumeration constants are ordinary identifiers - the same space as variables]
enum color: red green blue
which is why int red; is a compile error - hence the habit of prefixes (STATUS_OK)
[struct, union and enum share one tag space - there is only one]
struct handle.code = 7, enum status = 1
you cannot declare struct status - enum status already took that tag
[but ordinary identifiers live in a different space from tags]
the variable status = 42 lives beside the tag status
55.4.1 typedef struct node node;
가장 흔한 관용구다. 태그 node와 typedef 이름 node가 다른 마당이니 같은 철자를 쓸 수 있고, 그 덕에 쓰는 쪽은 struct를 매번 적지 않아도 된다. 자기 자신을 가리키는 포인터에는 태그가 반드시 필요하다는 점도 함께 본다 — struct node *next;를 쓸 시점에는 typedef 이름이 아직 완성되지 않았다.
55.4.2 그런데 typedef를 쓰지 말라는 규약도 있다
리눅스 커널의 코딩 스타일이 대표적이다. 근거는 정보의 은폐다 — node n;만 보면 그것이 구조체인지 포인터인지 정수인지 알 수 없지만, struct node n;은 읽는 즉시 드러난다. 큰 코드를 여럿이 읽을 때는 타이핑 몇 글자보다 그 정보가 값지다는 판단이다.
| 방침 | 근거 | 이런 곳에서 쓴다 |
|---|---|---|
| 태그와 typedef 를 같은 철자로 | 쓰는 쪽이 짧아진다 | 공개 라이브러리 API (SQLite·SDL 등) |
typedef를 쓰지 않는다 | struct가 보이면 정체가 드러난다 | 리눅스 커널, 커널 계열 프로젝트 |
불투명 타입만 typedef | 속을 감출 때는 감추고, 열 때는 연다 | 핸들을 넘기는 API (FILE *가 그 원형) |
표 55.3
셋 중 무엇이 옳은지는 정해져 있지 않다. 정하고 지키는 것이 옳다.
55.5 열거 상수는 보통 식별자다
enum color { red, green, blue };를 쓰면 red·green·blue는 태그 마당이 아니라 ④ 보통 식별자 마당에 들어간다. 즉 변수·함수와 같은 마당이다.
enum color { red, green, blue };
int red; // 오류: 'red' redeclared as different kind of symbol그래서 실무의 규약은 열거 상수에 접두어를 붙이는 것이다 — STATUS_OK, GTK_ALIGN_FILL, SDL_QUIT. 이름이 길어 보여도, 마당이 하나뿐인 세계에서는 그것이 담장이다.
플랫폼 노트. C++ 의 enum class
C++11 은 이 자리에서 갈라섰다. enum class color { red };로 선언하면 상수가 바깥 마당으로 새지 않고 color::red로만 쓰인다 — 「열거체마다 마당을 하나」 라는 뜻이다.
C23 은 열거체의 밑타입을 지정하는 문법(enum color : unsigned char { … })을 들여왔지만, 상수가 사는 마당은 그대로 두었다. 두 기능은 이름이 비슷해 보여도 푸는 문제가 다르다 — 하나는 크기, 하나는 이름이다.
55.6 가림(shadowing)
안쪽 스코프에 같은 철자를 다시 선언하면 바깥 것이 가려진다. 합법이고, 가끔은 유용하며, 자주 사고의 자리다.
int count = 1; // 전역
static int f(int count) { // 매개변수가 전역을 가린다
{ int x = count; { int x = 2; return x; } } // 안쪽 x 가 바깥 x 를 가린다
}문제는 경고가 기본으로 켜져 있지 않다는 것이다.
| 옵션 | 실측 결과(GCC 14) |
|---|---|
-Wall -Wextra | 가림은 한 건도 잡지 않는다(쓰이지 않는 변수만 지적) |
-Wshadow | 세 건 모두 잡는다 — 전역을 가리는 매개변수, 그리고 두 겹의 지역 |
표 55.4
문. 그러면 -Wshadow를 늘 켜야 하는가?
답. 새 코드라면 켜는 쪽을 권한다. 오래된 코드에 처음 켜면 경고가 쏟아지는데, 그때 쓰는 절충이 있다.
- GCC/Clang 의
-Wshadow=local(지역이 지역을 가리는 것만) 또는-Wshadow=compatible-local(타입까지 같을 때만). - 전역을 가리는 경우만 잡고 싶다면
-Wshadow=global.
근본 처방은 따로 있다 — 전역을 줄이는 것이다. 가릴 전역이 없으면 가림 사고도 없다. 다음 장의 「내보낼 것만 내보낸다」가 같은 처방의 다른 얼굴이다.
반례. 루프 변수를 안쪽에서 다시 선언하기
for (int i = 0; i < n; i++) {
for (int i = 0; i < m; i++) { /* 바깥 i 를 잃었다 */ }
}컴파일은 된다. 그러나 안쪽 블록에서 바깥 i를 참조할 방법이 없고, 나중에 누군가 안쪽 루프의 몸통을 함수로 빼낼 때 조용히 뜻이 바뀐다. -Wshadow가 잡아 주는 대표적인 무늬다.
55.7 써서는 안 되는 이름 — 예약된 마당
이름 공간이 넷뿐인 세계에서, 표준 라이브러리와 구현은 미리 자리를 예약해 두었다. 그 자리를 침범하면 오늘은 돌아가도 다음 버전에서 깨진다.
| 무엇이 예약되어 있나 | 범위 | 예 |
|---|---|---|
| 밑줄 + 대문자, 밑줄 두 개 | 모든 용도로 언제나 | _Value, __x, _Atomic |
| 밑줄 하나 + 소문자 | 파일 스코프의 보통 식별자와 태그 | _helper(전역이면 안 됨) |
str·mem·wcs + 소문자 | <string.h> 계열이 가져간다 | strdup, memcpy2 |
is·to + 소문자 | <ctype.h> 계열 | isodd, tolower2 |
E + 대문자·숫자 | <errno.h> | EMYERROR |
LC_ + 대문자 | <locale.h> | LC_MINE |
SIG·SIG_ + 대문자 | <signal.h> | SIGMINE |
PRI·SCN + 소문자 | <inttypes.h> | PRIxmine |
_t 접미사 | POSIX 가 예약(표준 C 아님) | mytype_t — 회색지대(12장) |
표 55.5
근거는 표준 §7.1.3 이고, 더 자세한 사연은 82장에서 다시 본다.
실제 사례. _t 접미사는 왜 회색지대인가
size_t·uint32_t처럼 표준 타입이 _t로 끝나니 자기 타입에도 붙이고 싶어진다. 그런데 표준 C는 _t를 예약하지 않았고, POSIX는 예약했다.
그래서 상황이 이렇게 갈린다. 순수 표준 C 프로그램에서 mytype_t는 합법이다. 같은 코드가 POSIX 시스템의 헤더와 만나면, 언젠가 POSIX가 그 이름을 쓰기 시작할 때 부딪힐 수 있다 — 실제로 그런 충돌이 여러 번 있었다.
12장의 사다리대로 하면 판단은 간단해진다. 혼자 쓰는 코드면 주석 한 줄로 족하고, 여러 플랫폼으로 나가는 라이브러리라면 접두어를 앞에 붙여 prov_str_t처럼 자기 마당 안에서 쓴다. 큰 프로젝트들이 대체로 후자를 택한 이유다.
이름의 규칙을 알았다. 그런데 규칙을 다 지켜도 두 라이브러리가 같은 철자를 내보내면 부딪힌다 — C에는 새 마당을 팔 방법이 없으니까. 다음 장이 그 문제다: 충돌을 어떻게 막고, C++ 는 이 자리를 어떻게 풀었는가.