목차7개 항목
출처와 참고
팔만코딩경(42Seoul 커뮤니티 위키)의 「[C] Identifier와 Name Space」, 「외부 비노출 함수는 static 함수로 선언하는 것이 '권장'됨」, 「상수(constant)를 직접 정의해 가독성을 높이자」, 「C 코딩 스타일 가이드」 연재를 바탕으로 C 표준 문서와 함께 재구성한 노트다. 저장 클래스 지정자와 변수 선택 기준은 고려사이버대학교 「프로그래밍 기초와 실습(C언어)」(2024-2학기) 강의 내용으로 보강했다.
식별자
식별자(identifier)는 이름이다. C에서 이름은 객체(값을 저장하는 메모리 영역, 즉 변수), 함수, 구조체·공용체·열거형의 태그와 멤버, typedef 이름, 라벨, 매크로를 가리킨다. 같은 이름이 프로그램의 위치에 따라 다른 대상을 가리킬 수 있는데, 그 규칙을 세 가지 축이 정한다.
| 축 | 질문 | 결정하는 것 |
|---|---|---|
| 스코프(scope) | 이 이름이 어디서 보이나 | 선언의 위치 |
| 이름 공간(name space) | 같은 위치에서 어떤 종류의 이름인가 | 문법적 맥락 (struct 키워드, . 연산자 등) |
| 링키지(linkage) | 다른 파일의 같은 이름과 같은 대상인가 | static, extern과 선언 위치 |
스코프와 이름 가림
| 스코프 | 범위 |
|---|---|
| 블록 | { } 안에서 선언된 이름. 선언 지점부터 블록 끝까지 |
| 파일 | 모든 함수 바깥에서 선언된 이름. 선언 지점부터 번역 단위(전처리된 소스 파일) 끝까지 |
| 함수 | 라벨(goto의 대상)만 해당. 함수 전체 |
| 함수 원형 | 원형의 매개변수 이름. 원형 끝까지 |
스코프가 겹치면 안쪽 선언이 바깥 선언을 가린다(shadowing). 가려진 대상은 사라지지 않고, 안쪽 블록을 나오면 다시 보인다. 같은 스코프에서 같은 이름을 두 번 정의하는 것은 오류다.
int a = 0; /* 파일 스코프 */
int main(void)
{
printf("%d\n", a); /* 0: 전역 a */
int a = 42; /* 여기부터 전역 a를 가림 */
{
int a = -42; /* 블록 안에서 다시 가림 */
printf("%d\n", a); /* -42 */
}
printf("%d\n", a); /* 42 */
/* int a = 1; */ /* 같은 블록에서 재정의: 컴파일 오류 */
}이름 가림은 합법이지만 읽는 사람을 속이기 쉽다. -Wshadow로 경고를 켤 수 있다.
네 가지 이름 공간
C++처럼 namespace를 직접 만들 수는 없지만, C에도 암묵적인 이름 공간이 있다. 같은 스코프라도 이름 공간이 다르면 같은 이름이 충돌하지 않는다.
| 이름 공간 | 구별하는 방법 |
|---|---|
| 라벨 | goto와 label: 문법 |
태그 (struct·union·enum 공통) | 앞에 붙는 키워드 |
| 멤버 | 구조체·공용체마다 따로 있다. .이나 -> 앞의 타입으로 구별 |
| 일반 식별자 | 변수, 함수, typedef 이름, 열거 상수 |
struct s { int s; };
struct s s = { .s = 42 }; /* 태그 s, 멤버 s, 변수 s: 모두 다른 공간 → 정상 */
typedef struct t { int t; } t;
t t; /* typedef 이름 t와 변수 t가 둘 다 일반 식별자 → 오류 */멤버 이름 공간이 구조체마다 따로 있기 때문에 서로 다른 구조체가 같은 멤버 이름(size, next)을 가질 수 있다. typedef와 변수 이름이 부딪히지 않도록 t_list, list_t처럼 타입 이름에 접두사·접미사를 붙이는 관례가 여기서 나온다.
저장 기간
스코프가 "어디서 보이나"라면 저장 기간은 "언제까지 살아 있나"다.
| 저장 기간 | 대상 | 수명 |
|---|---|---|
| 자동 | 일반 지역 변수 | 블록에 들어가서 나올 때까지 (보통 스택) |
| 정적 | 전역 변수, static 지역 변수, 문자열 리터럴 | 프로그램 시작부터 끝까지. 초기값이 없으면 0으로 초기화 |
| 할당 | malloc으로 얻은 메모리 | free할 때까지 |
| 스레드 | thread_local(C23, 이전엔 _Thread_local) | 스레드가 끝날 때까지 |
함수 안의 static int count;는 블록 스코프라서 그 함수에서만 보이지만, 값은 호출 사이에 유지된다. 초기화식도 프로그램 시작 전에 한 번만 적용되므로, 함수가 다시 불려 static int count = 1; 줄을 지나도 1로 돌아가지 않는다. 호출 횟수 세기나 한 번만 할 초기화 표시에 쓴다.
변수의 저장 기간과 링키지는 선언 위치와 저장 클래스 지정자(storage-class specifier) 로 정해진다.
| 지정자 | 뜻 | 비고 |
|---|---|---|
auto | 자동 저장 기간. 블록 안 지역 변수의 기본값이라 쓸 일이 없다 | C23은 auto x = 1;처럼 타입 추론에도 쓴다(C++11의 auto와 비슷) |
register | 자주 쓰니 레지스터에 두라는 힌트 | 현대 컴파일러는 무시하고 스스로 배치한다. &로 주소를 얻을 수 없다는 제약만 남는다. C++17에서는 제거됐다 |
static | 지역 변수면 정적 저장 기간, 파일 스코프면 내부 링키지 | 아래 「링키지와 static」 |
extern | 다른 곳(보통 다른 파일)에 정의된 변수를 쓰겠다는 선언. 메모리를 잡지 않는다 | 정의는 한 파일에만 둔다 |
_Thread_local / thread_local | 스레드 저장 기간 | C23부터 thread_local이 키워드 |
volatile과 const는 저장 클래스가 아니라 타입 한정자(type qualifier) 다. volatile은 하드웨어 레지스터나 시그널 처리기처럼 프로그램 바깥에서 값이 바뀔 수 있는 변수에 붙여, 컴파일러가 읽기·쓰기를 레지스터에 캐시하거나 없애지 못하게 한다. 스레드 간 동기화 수단은 아니다(그건 _Atomic과 뮤텍스의 몫이다).
전역 변수는 어느 함수에서나 바꿀 수 있어 버그 추적이 어렵다. 기본은 자동 변수로 쓰고, 호출 사이에 값을 유지해야 하면 static 지역 변수, 여러 파일이 정말 공유해야 할 때만 extern 전역 변수를 쓴다. 일부 함수만 쓰는 데이터는 전역으로 두지 말고 인수로 넘긴다.
링키지와 static
여러 .c 파일을 각각 컴파일한 뒤 링커가 합칠 때, 파일 스코프 이름이 다른 파일의 같은 이름과 연결되는지를 링키지가 정한다.
| 링키지 | 선언 | 의미 |
|---|---|---|
| 외부(external) | 파일 스코프의 함수·변수 기본값, extern | 모든 번역 단위에서 같은 대상. 헤더에 원형을 두고 공유 |
| 내부(internal) | 파일 스코프에 static | 그 파일 안에서만 보인다 |
| 없음 | 지역 변수, 매개변수 | 링커가 모른다 |
파일 안에서만 쓰는 함수와 전역 변수는 static으로 선언하는 것이 좋은 습관이다.
- 다른 파일과 이름이 겹쳐 생기는 링크 오류(
duplicate symbol)를 막는다. - 읽는 사람이 "이 함수는 이 파일 밖에서 안 쓰인다"는 것을 바로 안다.
- 컴파일러가 호출하는 곳을 모두 알기 때문에 인라인이나 미사용 함수 제거 같은 최적화를 하기 쉽다.
static이라는 키워드가 지역 변수에서는 저장 기간을, 파일 스코프에서는 링키지를 바꾼다는 점이 헷갈림의 원인이다. 한 프로젝트 안에서는 "외부에 노출하지 않는 함수는 모두 static"처럼 규칙을 정해 일관되게 지키는 것이 중요하다. 접두사(ft_)로 이름 충돌을 피하는 규약도 같은 목적을 달성한다.
기호 상수: #define, enum, const
코드에 박힌 숫자(매직 넘버) 대신 이름 붙은 상수를 쓰면 의미가 드러나고 한곳에서 고칠 수 있다.
| 방법 | 특징 | 쓰는 곳 |
|---|---|---|
#define BUFFER_SIZE 42 | 전처리기의 텍스트 치환. 타입·스코프가 없고 디버거에 이름이 안 남는다 | 조건부 컴파일, -D로 바꿀 값, 헤더 가드 |
enum { MAX_FD = 1024 }; | 컴파일러가 아는 int 상수. 디버거에 이름이 보이고 배열 크기에 쓸 수 있다 | 서로 관련된 정수 상수 묶음, 상태 값 |
static const int max = 10; | 타입이 있는 읽기 전용 변수. C17까지는 상수식이 아니라 배열 크기·case 라벨에 못 쓴다 | 타입이 중요한 값, 실수 상수 |
constexpr int max = 10; (C23) | 진짜 컴파일 타임 상수 | C23을 쓸 수 있을 때 const를 대신 |
같은 값이라도 의미가 다르면 이름을 따로 짓는다. #define MAX_USERS 153과 #define ERR_READ 153은 값이 같아도 서로 대신 쓰면 안 된다. 이름은 값이 아니라 의도를 담는다.
정리
- 이름이 무엇을 가리키는지는 스코프(어디서), 이름 공간(무슨 종류), 링키지(다른 파일과 같은가)가 함께 정한다.
- 안쪽 선언은 바깥을 가린다. 태그·멤버·일반 식별자는 서로 충돌하지 않지만
typedef와 변수는 충돌한다. - 파일 밖으로 노출하지 않는 함수와 전역은
static으로 내부 링키지를 준다. - 상수는 관련된 정수면
enum, 컴파일 옵션이면#define, C23이면constexpr.
관련 노트: GCC 컴파일과 링크, C 사용자 정의 타입과 함수 포인터, C의 역사와 표준
원본 기록6
- Notion 원문 (새 창) 80000coding.notion.site/353fe98e21a54b1889f294192257551f
- Notion 원문 (새 창) 80000coding.notion.site/c5e5094f801e4dc493b45de9a61fdd11
- Notion 원문 (새 창) 80000coding.notion.site/083f8a82494543658797ac87745289b3
- Notion 원문 (새 창) 80000coding.notion.site/374b09d2f19a4ccc92dc22b38b73207a
- Notion 원문 (새 창) 80000coding.notion.site/46b05277e1cd476ca86a36581932b775
- 원문 (새 창) en.cppreference.com/w/c/language/scope