Search
Duplicate
🤔

[C] Identifier와 Name Space

간단소개
C에도 name space가 있다는 것을 알고 계셨나요? C언어의 식별자와 네임스코프에 대해 알아봤습니다.
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
C
Scrap
태그
C99
9 more properties

[C] Identifier와 Name space

Code 1.
#include <stdio.h> int main(void) { int a = 42; printf("%d\\n", a); int a = -42; printf("%d\\n", a); }
C
복사
Code 2.
#include <stdio.h> int a = 42; int main(void) { printf("%d\\n", a); int a = -42; printf("%d\\n", a); }
C
복사
Code 3.
#include <stdio.h> int a = 0; int main(void) { int a = 42; printf("%d\\n", a); { int a = -42; printf("%d\\n", a); } printf("%d\\n", a); }
C
복사
Code 4.
#include <stdio.h> int main(void) { struct s { int s; }; struct s s = {.s = 42}; printf("%d\\n", s.s); }
C
복사
Code 5.
#include <stdio.h> int main(void) { typedef struct s { int s; } s; s s = {.s = 42}; printf("%d\\n", s.s); }
C
복사
위 코드들은 정상 작동할까? 컴파일되지 않으면 그 이유는 무엇이고, 잘 작동한다면 그 이유는 무엇일까?
해답은 C언어의 Identifier(식별자), 그리고 Name Space에 있다.
Note) 이 글은 C99 표준(ISO/IEC 9899)을 바탕으로 설명합니다. 가장 최신 표준인 C23(ISO/IEC 9899:2023)도 추가되는 요소들(constexpr 등)에 대한 설명이 추가될 뿐, 기본 개념은 다르지 않습니다.

1. Identifier

C언어 표준은 Identifier를 다음과 같의 정의한다. (C99 6.2.1 Clause 1)
An identifier can denote an object; a function; a tag or a member of a structure, union, or enumeration; a typedef name; a label name; a macro name; or a macro parameter. (...)
식별자(Identifier)는 다음을 가리킨다.
객체(object)
함수
구조체(structure), 공용체(union), 열거체(enumeration)의 태그나 멤버
typedef
label
macro name, macro parameter
여기서 말하는 객체(object)는, C++ 등의 객체 지향 언어에서 말하는 객체와는 다른 개념이다. C언어 표준은 객체에 대해 다음과 같의 정의한다. (C99 3.14)
region of data storage in the execution environment, the contents of which can represent values
실행 환경에서 값을 저장할 수 있는 data storage의 영역
즉, 변수이다.
이어서 오는 부연설명에서, 전처리기(preprocessor)에 의해 치환되는 매크로에 대해서는 논의하지 않는다고 나와 있다. 이 글에서도 앞으로의 논의에서 매크로는 다루지 않는다.
또한, 6.2.1의 3절에서 라벨에 대해 다룬다. 라벨은 goto statement에서만 사용되고, function scope만 가지기 때문에 이 글에서 다루지 않겠다.
같은 구절에서, 식별자의 사용에 대해 다루고 있다.
The same identifier can denote different entities at different points in the program
같은 식별자라도 실행되는 지점에 따라 다른 요소들을 가리키고 있을 수 있다.
이 글의 주제이다. 같은 이름의 식별자라도 가리키는 요소가 달라질 수 있기 때문에, 어떨 때는 컴파일 오류를 발생시키기도 하고, 어떨 때는 예상과는 다른 요소를 가리킨다. 식별자가 어떻게 다뤄지는지 알기 위해 표준을 계속 읽어보자. (C99 6.2.1 Clause 2)
For each different entity that an identifier designates, the identifier is visible (i.e., can be used) only within a region of program text called its scope. Different entities designated by the same identifier either have different scopes, or are in different name spaces. There are four kinds of scopes: function, file, block, and function prototype. (...)
식별자가 가리키는 요소에 대해, 그 식별자는 Scope라고 불리는 프로그램의 특정 영역에서만 그 요소를 가리키고 있다(visible). 같은 식별자가 기리키는 다른 요소들은, 다른 스코프에 있거나 다른 name space에 있다.
Scope에는 네 종류가 있다:
함수
파일
블록
함수 프로토타입
즉, scope에 따라 같은 식별자라도 가리키고 있는 요소가 다른 것이다. 이에 대한 내용을 보자. (C99 6.2.1 Clause 4)
Every other identifier has scope determined by the placement of its declaration (in a declarator or type specifier). (...) If an identifier designates two different entities in the same name space, the scopes might overlap. If so, the scope of one entity (the inner scope) will be a strict subset of the scope of the other entity (the outer scope). Within the inner scope, the identifier designates the entity declared in the inner scope; the entity declared in the outer scope is hidden (and not visible) within the inner scope.
모든 식별자는 식별자의 선언 위치에 따라 스코프가 결정된다. (...)
만약 같은 name space에서 식별자가 다른 요소를 가리키고 있다면, 스코프는 중첩될 수 있다. 스코프가 중첩되었다면, 안쪽 스코프(inner scope)가 가리키는 요소는 는 바깥쪽 스코프(outer scope)에서 가리키는 요소에 대해 strict subset이 된다. 식별자는 안쪽 스코프에서 가리키는 요소로 지정된다. 바깥쪽 스코프에서 선언된 요소는 가려지게 된다.
실행 흐름 순으로 따져보면, 더 나중에 선언된 식별자가 우선한다는 뜻으로도 이해할 수 있다. 바깥 스코프에서 선언한 식별자 이름 그대로 안쪽 스코프에서 다른 요소를 선언한다면 바깥쪽 스코프에서 정의한 식별자가 가려진다. 따라서 이 스코프에서는 식별자가 다른 의미를 갖게 된다.
단, 이 식별자가 가리키는 요소(entitiy)는 사라지는게(freed, deleted) 아니라 단순히 가려진다(hidden). 이 스코프에서 빠져나오게 되면 다시 원래의 식별자가 가리키는 요소를 가리키게 되며, 식별자가 가려졌더라도 포인터 등의 방식을 사용해서 여전히 그 요소를 사용할 수 있다.
#undef 지시어는 매크로에 대해서만 사용할 수 있기 때문에 스코프에서 가려진 식별자는 (스코프가 끝나기 전까지) 다른 의미를 가지게 된다.
생략된 내용에서는 선언의 위치별로 어떤 스코프를 갖게 되는지에 대해 설명되어 있다. (이때 식별자는 어떤 요소들을 선언할 때 정의되기 때문에, 이 스코프들은 lifetime과도 밀접한 연관이 있다.)
file scope
block scope
function prototype scope
이 구절은 Code 1~3의 동작을 설명한다.
Code 1: C언어는 같은 스코프(와 namespace)에서 식별자를 재선언하는 것을 허용하지 않는다. main함수의 스코프 내에서 이미 a라는 식별자가 선언되었는데 재선언하려고 하니 컴파일 오류가 발생한다.
Code 2: file scope를 갖는 전역변수 a가 main함수 실행 이전에 선언되어 있다. main함수의 첫 줄에서 a라는 식별자는 42라는 값을 갖는 전역변수를 가리키고 있다. 그러나 두 번째 줄에서 block scope를 갖는 지역변수 a를 선언했다. 안쪽 스코프가 우선하기 때문에 이 시점부터 a라는 식별자는 -42라는 값을 갖는 지역변수를 가리키게 된다. 따라서 첫 출력은 전역변수를 가리키므로 42, 두 번째 출력은 지역변수를 가리키므로 -42이다.
Code 3: 이 코드에서는 식별자 a가 서로 다른 세 개의 요소를 가리킨다. main함수의 실행 직후 식별자 a는 바깥쪽 스코프(file scope)에서 선언된 전역변수(0)이 아니라, 지역변수(42)를 가리킨다. 따라서 첫 출력은 42이다. Compound Statement는 하나의 블록을 만들어 독자적인 스코프를 만들게 된다. 이 블록에서 식별자 a는 함수의 스코프에서 선언된 지역변수(42)가 아니라 블록 내에서 만들어진 변수(-42)를 가리킨다. 따라서 두 번째 출력은 -42이다. 이때, 이 블록이 끝나면 식별자 a는 다시 가려졌던 지역변수(42)를 가리킨다. 따라서 세 번째 출력은 42이다.

2. Name Space

그런데, 스코프에 대한 내용만으로는 Code 4의 동작을 설명할 수 없다. 같은 블록 스코프 안에서 s라는 식별자가 연달아 사용되며 서로 다른 요소들을 선언하고 있지만, 컴파일 에러가 발생하지 않는다.
6.2.1의 2절을 다시 읽어보자.
Different entities designated by the same identifier either have different scopes, or are in different name spaces.
식별자를 구분하는 데에는 Scope와 Name Space가 동시에 사용된다. C++처럼 프로그래머가 명시적으로 namespace를 선언하고 사용할 수는 없지만, C에서도 암묵적인 namespace가 존재한다. name space에 대한 설명은 C99 6.2.3에 나와있다.
If more than one declaration of a particular identifier is visible at any point in a translation unit, the syntactic context disambiguates uses that refer to different entities.
Thus, there are separate name spaces for various categories of identifiers, as follows: — label names (disambiguated by the syntax of the label declaration and use); — the tags of structures, unions, and enumerations (disambiguated by following any of the keywords struct, union, or enum); — the members of structures or unions; each structure or union has a separate name space for its members (disambiguated by the type of the expression used to access the member via the . or -> operator); — all other identifiers, called ordinary identifiers (...)
만약 번역기(어셈블러)가 특정 식별자의 선언을 만나게 되면, 문법적인 맥락이 어떤 요소를 가리키고 있는지 해석한다.
따라서, 다양한 식별자에 대해 별도의 name space가 존재한다. 다음 name space가 정의된다.
label (라벨 선언 문법과 사용에 의해 해석됨)
구조체, 공용체, 열거체의 태그 (struct, union, enum 키워드에 의해 해석됨)
구조체나 고용체의 멤버 (. 또는 > 연산자에 의해 해석됨)
다른 모든 식별자들
일반적으로 사용하는 식별자들(변수, 함수), 구조체/공용체/열거체의 태그, 구조체/공용체의 멤버, 점프 라벨은 서로 다른 네임스페이스에 저장된다.(단, 구조체/공용체/열거체 태그는 같은 네임스페이스 사용) 따라서 같은 스코프이더라도, 다른 네임스페이스에 있으면 식별자의 재정의(redifinition) 문제가 발생하지 않는다.
재미있는 점은 구조체나 공용체의 태그마다 멤버의 네임스페이스가 생긴다는 점이다. 덕분에 서로 다른 구조체가 같은 멤버변수 이름을 가지고 있어도, 충돌하지 않는다. 만약 멤버의 네임스페이스가 공유되었다면 다음 코드는 컴파일 에러를 발생시켰을 것이다.
struct s1 { int var; }; struct s2 { int var };
C
복사
그러나 위 코드를 컴파일하면 두 개의 네임스페이스가 생성된다. 따라서 같은 멤버 변수의 이름을 가지고 있더라도 다른 네임스페이스이기 때문에 "같은 스코프와 네임스페이스에서 동일한 식별자를 사용할 수 없다"다른 규칙에 어긋나지 않는다.
네임스페이스에 대한 정의로, Code 4,5를 설명할 수 있다.
Code 4
구조체의 네임스페이스: s(struct)
struct s의 네임스페이스: s(멤버변수 int s)
일반 네임스페이스: s(struct s 자료형의 지역변수 struct s s)
같은 식별자들이지만 서로 다른 네임스페이스에 있기 때문에 충돌하지 않는다
Code 5
구조체의 네임스페이스: s(struct)
struct s의 네임스페이스: s(멤버변수 int s)
일반 네임스페이스: s(struct s 자료형의 지역변수 struct s s) vs s(typedef로 선언한 struct s의 별칭)
일반 네임스페이스에서 식별자가 충돌하기 때문에 컴파일 에러가 발생한다