Proven C BookEnglish GitHub

55 이름의 세계 — 네 이름 공간과 세 축

먼저 알아야 할 것

24장 함수 선언과 정의 · 스코프 — 이름이 보이는 범위
54장 여러 파일 — 분할과 링크 · 연결 — 이름이 파일 경계를 넘는가

돌아보기

54장에서 이름에는 스코프 말고 연결이라는 성질이 또 있다고 했다. 그러면 다음 코드는 왜 컴파일되는가? struct nodenode가 같은 철자인데도 서로 부딪히지 않는다.

typedef struct node node;

답. 스코프도 연결도 아닌 세 번째 성질이 있기 때문이다 — 이름 공간(namespace) (name space). struct 뒤에 오는 이름과 그냥 쓰이는 이름은 컴파일러가 서로 다른 목록에서 찾는다. 같은 철자라도 찾는 목록이 다르니 충돌하지 않는다.

이 장은 그 목록이 몇 개이고 무엇인지, 그리고 스코프·연결과 어떻게 다른지를 정면으로 다룬다.

이 장의 필요성과 맥락

파일을 쪼개자마자 이름이 부딪히기 시작한다. 그래서 이름 공간을 54장 바로 뒤에 둔다 — 한 파일 안에서는 스코프만 알면 됐지만, 경계를 넘는 순간 연결이라는 축이 하나 더 생기기 때문이다. 축이 셋으로 늘어난 지금이 그 셋을 한 번에 세울 자리다.

이 장이 끝나면

「C에는 이름 공간이 없다」는 말을 자주 듣는다. 절반만 맞는 말이다 — 표준은 이름 공간을 네 개 정의한다. 없는 것은 사용자가 새로 만드는 이름 공간이다. 이 장에서 그 넷을 세우고, 스코프·연결과 함께 세 축으로 묶고, 가림과 예약 이름까지 정리한다. 다음 장이 그 위에서 「그래서 충돌을 어떻게 막는가」를 다룬다.

이 장에서 답할 질문

  1. 매크로는 어느 이름 공간에 사는가?
  2. 태그와 멤버에는 왜 연결이 없는가?
  3. 그러면 -Wshadow를 늘 켜야 하는가?

55.1 「C에는 이름 공간이 없다」는 말의 진실

이 말은 널리 쓰이고, 널리 오해를 부른다. 표준의 문장은 반대에 가깝다.

표준 §6.2.3 은 「한 번역 단위 안에서 같은 식별자의 선언이 여럿 보이면 문법적 문맥이 그것들을 가른다. 따라서 식별자의 범주마다 별개의 이름 공간 (separate name spaces)이 있다」고 적는다.

즉 C에는 이름 공간이 있다. 없는 것은 namespace app { … }처럼 프로그래머가 새 마당을 파는 기능이다. 이 구별을 흐리면 다음 두 문장이 둘 다 사실이라는 것을 설명할 수 없다.

흔한 오해. “C에는 이름 공간 개념 자체가 없다”

표준이 §6.2.3 에서 네 개를 명시한다. 정확한 말은 「사용자가 정의하는 이름 공간이 없다」이다.

이 오해는 실무에서 값을 치른다. 「없다」고 알고 있으면 struct stat과 함수 stat이 공존하는 POSIX 헤더를 읽을 때 무슨 일이 벌어지는지 설명하지 못하고, 열거 상수가 왜 변수와 충돌하는지도 설명하지 못한다. 「넷이 있고, 내가 새로 만들 수는 없다」가 정확한 길잡이다.

55.2 네 이름 공간

표준이 가르는 넷은 이렇다.

이름 공간여기 사는 이름어느 문법 자리에서 찾는가
① 레이블goto의 표적goto 뒤, 그리고 문장 앞의 이름:
② 태그struct·union·enum 뒤의 이름struct/union/enum 키워드 바로 뒤
③ 멤버구조체·공용체의 멤버 — 타입마다 따로.->
④ 보통 식별자변수·함수·typedef 이름·열거 상수·매개변수그 밖의 모든 자리

표 55.1

세 가지를 특히 새겨 둔다.

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++ 는 이 자리를 어떻게 풀었는가.