저장 클래스 지정자(Storage-class specifiers)
저장 클래스 지정자(Storage-class specifiers)
객체가 언제 메모리에 살아 있고, 다른 소스 파일에서 참조할 수 있는지를 선언으로 결정하고 싶을 때 쓰는 게 저장 클래스 지정자예요. 변수를 static으로 둘지, extern으로 둘지 정하는 그 지정자들이에요.
출처: cppreference
본문
저장 클래스 지정자는 객체와 함수의 저장 기간(storage duration)과 링키지(linkage)를 지정해요:
-
auto- 자동 저장 기간, 링키지 없음 -
register- 자동 저장 기간, 링키지 없음; 이 변수의 주소를 취할 수 없음 -
static- 정적 저장 기간, 내부 링키지(블록 범위에서는 제외) -
extern- 정적 저장 기간, 외부 링키지(이미 내부로 선언된 경우는 제외) -
_Thread_local(C23 이전)thread_local(C23부터) - 스레드 저장 기간 (C11부터)
설명(Explanation)
저장 클래스 지정자는 선언과 복합 리터럴 표현식(C23부터)에 나타나요. 하나의 지정자만 쓸 수 있고, 단 _Thread_local(C23 이전)thread_local(C23부터)은 링키지를 조정하기 위해 static이나 extern과 결합될 수 있어요(C11부터). 저장 클래스 지정자는 그것들이 선언하는 이름의 두 가지 독립적인 속성, 즉 저장 기간과 링키지를 결정해요.
auto지정자는 블록 범위에서 선언된 객체에만(함수 매개변수 목록 제외) 쓸 수 있어요. 자동 저장 기간과 링키지 없음을 나타내는데, 이는 이런 종류의 선언의 기본값이에요.register지정자는 함수 매개변수 목록을 포함해 블록 범위에서 선언된 객체에만 쓸 수 있어요. 자동 저장 기간과 링키지 없음(이런 선언의 기본값)을 나타내지만, 추가로 가능하면 이 변수의 값을 CPU 레지스터에 저장하라고 최적화 프로그램에 힌트를 줘요. 최적화가 실제로 일어나든 말든,register로 선언된 변수는 주소 연산자(&)의 인자로 쓸 수 없고,_Alignas(C23 이전)alignas(C23부터)(C11부터)를 쓸 수 없으며,register배열은 포인터로 변환될 수 없어요.static지정자는 정적 저장 기간(_Thread_local(C23 이전)thread_local(C23부터)과 결합된 경우 제외)(C11부터)과 내부 링키지(블록 범위에서 쓰면 제외)를 모두 지정해요. 파일 범위의 함수와 파일 및 블록 범위의 변수에 쓸 수 있지만, 함수 매개변수 목록에는 쓸 수 없어요.extern지정자는 정적 저장 기간(_Thread_local(C23 이전)thread_local(C23부터)과 결합된 경우 제외)(C11부터)과 외부 링키지를 지정해요. 파일과 블록 범위(함수 매개변수 목록 제외)의 함수 및 객체 선언에 쓸 수 있어요.extern이 이미 내부 링키지로 선언된 식별자의 재선언에 나타나면, 링키지는 내부로 유지돼요. 그 외의 경우(이전 선언이 외부, 링키지 없음이거나, 범위에 없으면) 링키지는 외부예요._Thread_local(C23 이전)thread_local(C23부터)은 스레드 저장 기간을 나타내요. 함수 선언에는 쓸 수 없어요. 객체의 선언에 쓰면, 같은 객체의 모든 선언에 반드시 있어야 해요. 블록 범위 선언에 쓰면 링키지를 결정하기 위해 반드시static이나extern과 결합해야 해요. (C11부터)
저장 클래스 지정자를 제공하지 않으면 기본값은 다음과 같아요:
- 모든 함수에
extern - 파일 범위의 객체에
extern - 블록 범위의 객체에
auto
저장 클래스 지정자로 선언된 struct나 union에 대해, 저장 기간(링키지는 아님)은 그 멤버들에게도 재귀적으로 적용돼요.
블록 범위의 함수 선언은 extern을 쓰거나 아무것도 쓰지 않을 수 있어요. 파일 범위의 함수 선언은 extern이나 static을 쓸 수 있어요.
함수 매개변수는 register 외의 저장 클래스 지정자를 쓸 수 없어요. static이 배열 타입의 함수 매개변수에서 특별한 의미를 가진다는 점에 유의하세요.
저장 기간(Storage duration)
모든 객체는 저장 기간이라는 속성을 가지며, 이는 객체의 수명(lifetime)을 제한해요. C에는 네 종류의 저장 기간이 있어요:
- 자동 저장 기간(automatic storage duration). 객체가 선언된 블록에 들어갈 때 저장 공간이 할당되고, 어떤 방법으로든(goto, return, 끝에 도달) 그 블록을 빠져나갈 때 해제돼요. 예외로 VLA가 있어요. VLA의 저장 공간은 블록 진입이 아니라 선언이 실행될 때 할당되고, 블록을 빠져나갈 때가 아니라 선언이 범위를 벗어날 때 해제돼요(C99부터). 블록이 재귀적으로 들어가면 모든 재귀 단계마다 새 할당이 수행돼요. 모든 함수 매개변수와 비-static 블록 범위 객체, 그리고 블록 범위에서 쓰인 복합 리터럴(C23 이전)이 이 저장 기간을 가져요.
- 정적 저장 기간(static storage duration). 저장 기간은 프로그램 실행 전체이고, 객체에 저장된 값은
main이전에 단 한 번만 초기화돼요.static으로 선언된 모든 객체와,_Thread_local(C23 이전)thread_local(C23부터)(C11부터)로 선언되지 않은 내부 또는 외부 링키지를 가진 모든 객체가 이 저장 기간을 가져요. - 스레드 저장 기간(thread storage duration). 저장 기간은 객체가 만들어진 스레드의 실행 전체이고, 객체에 저장된 값은 스레드가 시작될 때 초기화돼요. 각 스레드는 자기만의 별개의 객체를 가져요. 이 객체에 접근하는 표현식을 실행하는 스레드가 그 초기화를 실행한 스레드가 아니면, 동작은 구현이 정의(implementation-defined)예요.
_Thread_local(C23 이전)thread_local(C23부터)로 선언된 모든 객체가 이 저장 기간을 가져요. (C11부터) - 할당 저장 기간(allocated storage duration). 동적 메모리 할당 함수를 사용해 요청 시 저장 공간을 할당하고 해제해요.
링키지(Linkage)
링키지는 식별자(변수 또는 함수)가 다른 범위에서 참조될 수 있는 능력을 말해요. 같은 식별자를 가진 변수나 함수가 여러 범위에 선언되어 있는데 그 모든 범위에서 참조될 수 없다면, 변수의 여러 인스턴스가 생성돼요. 다음 링키지가 인식돼요:
- 링키지 없음(no linkage). 변수나 함수는 그것이 있는 범위(블록 범위)에서만 참조될 수 있어요.
extern으로 선언되지 않은 모든 블록 범위 변수, 모든 함수 매개변수, 그리고 함수나 변수가 아닌 모든 식별자가 이 링키지를 가져요. - 내부 링키지(internal linkage). 변수나 함수는 현재 번역 단위의 모든 범위에서 참조될 수 있어요.
static또는constexpr(C23부터)로 선언된 모든 파일 범위 변수와,static으로 선언된 모든 파일 범위 함수(static 함수 선언은 파일 범위에서만 허용돼요)가 이 링키지를 가져요. - 외부 링키지(external linkage). 변수나 함수는 전체 프로그램의 다른 어떤 번역 단위에서도 참조될 수 있어요.
static이나constexpr(C23부터)로 선언되지 않은 모든 파일 범위 변수,static으로 선언되지 않은 모든 파일 범위 함수 선언, 모든 블록 범위 함수 선언, 그리고 추가로 그 시점에 내부 링키지의 이전 선언이 보이지 않는 한extern으로 선언된 모든 변수나 함수가 이 링키지를 가져요.
같은 식별자가 같은 번역 단위에서 내부와 외부 링키지 모두로 나타나면 동작은 정의되지 않아요. 이는 가정 정의(tentative definitions)를 쓸 때 발생할 수 있어요.
링키지와 라이브러리(Linkage and libraries)
외부 링키지를 가진 선언은 보통 헤더 파일에서 제공되어, 그 파일을 #include하는 모든 번역 단위가 다른 곳에서 정의된 같은 식별자를 참조할 수 있게 해요.
헤더 파일에 나타난 내부 링키지를 가진 어떤 선언도, 그 파일을 포함하는 각 번역 단위에서 별개이고 분리된 객체를 만들어요.
라이브러리 인터페이스, 헤더 파일 "flib.h":
#ifndef FLIB_H
#define FLIB_H
void f(void); // function declaration with external linkage
extern int state; // variable declaration with external linkage
static const int size = 5; // definition of a read-only variable with internal linkage
enum { MAX = 10 }; // constant definition
inline int sum (int a, int b) { return a + b; } // inline function definition
#endif // FLIB_H
라이브러리 구현, 소스 파일 "flib.c":
#include "flib.h"
static void local_f(int s) {} // definition with internal linkage (only used in this file)
static int local_state; // definition with internal linkage (only used in this file)
int state; // definition with external linkage (used by main.c)
void f(void) { local_f(state); } // definition with external linkage (used by main.c)
애플리케이션 코드, 소스 파일 "main.c":
#include "flib.h"
int main(void)
{
int x[MAX] = {size}; // uses the constant and the read-only variable
state = 7; // modifies state in flib.c
f(); // calls f() in flib.c
}
flib.h의 extern int state;는 외부 링키지를 가진 선언이라서, main.c의 state = 7;이 flib.c의 실제 정의를 건드려요. 반면 static const int size는 헤더에 있지만 내부 링키지라서, 이 헤더를 포함하는 main.c와 flib.c가 각자 별개의 size 객체를 가지게 돼요.
키워드(Keywords)
auto, register, static, extern, _Thread_local, thread_local
주의할 점(Notes)
키워드 _Thread_local은 보통 <threads.h> 헤더에 정의된 편의 매크로 thread_local을 통해 써요. (C23 이전)
typedef와 constexpr(C23부터) 지정자는 C 언어 문법에서 공식적으로 저장 클래스 지정자로 나열되지만, 저장 공간은 지정하지 않아요. (C23부터)
auto 지정자는 타입 추론에도 쓰여요. (C23부터)
const이고 extern이 아닌 파일 범위 이름은 C에서 외부 링키지를 가져요(모든 파일 범위 선언의 기본값처럼) 하지만 C++에서는 내부 링키지를 가져요.
예제(Example)
#include <stdio.h>
#include <stdlib.h>
// static storage duration
int A;
int main(void)
{
printf("&A = %p\n", (void*)&A);
// automatic storage duration
int A = 1; // hides global A
printf("&A = %p\n", (void*)&A);
// allocated storage duration
int* ptr_1 = malloc(sizeof(int)); // start allocated storage duration
printf("address of int in allocated memory = %p\n", (void*)ptr_1);
free(ptr_1); // stop allocated storage duration
}
가능한 출력:
&A = 0x600ae4
&A = 0x7ffefb064f5c
address of int in allocated memory = 0x1f28c30
출력에서 주소가 모두 다른 걸 확인할 수 있어요. 파일 범위의 A는 정적 저장 기간을 가져서 데이터 세그먼트 주소(0x600ae4 계열)에 있고, main 안의 int A는 자동 저장 기간이라 스택 주소에 있으며, malloc으로 잡은 메모리는 힙 주소에 있어요.
더 알아보기
- extern 및 가정 정의: 외부 정의와 가정 정의가 실제로 어떻게 이뤄지는지.
- constexpr(C23): 컴파일 타임 상수를 만드는 지정자.
- 동적 메모리 할당(malloc, free): 할당 저장 기간을 만드는 함수.