문맥에 따라 두 가지 전혀 다른 의미를 갖는 키워드.
① 전역/함수 수준 — 링크 범위를 파일 안으로 제한
// sensor.c
static int calibration = 0; // 이 파일에서만 보인다
static void reset_adc(void) { … } // 이 파일에서만 호출 가능
내부 링키지(internal linkage) 를 갖게 되어 다른 .c 파일에서
extern으로도 접근할 수 없다.
왜 중요한가
- ① 이름 충돌 방지 — 여러 파일에 같은 이름의 헬퍼 함수를 둘 수 있다
- ② 캡슐화 — 모듈의 내부 구현을 감춘다 (C에서 private 을 흉내내는 방법)
- ③ 최적화 기회 — 컴파일러가 "이 함수는 밖에서 안 부른다"를 알아
- 인라인하거나 아예 제거할 수 있다
임베디드에서 ③이 실질적 이득이다. 코드 크기가 줄어든다.
② 함수 내부 지역 변수 — 생명주기를 프로그램 전체로
void counter(void) {
static int count = 0; // 함수를 나가도 값이 유지된다
count++;
}
| 일반 지역 변수 | static 지역 변수 | |
|---|---|---|
| 위치 | 스택 | .data / .bss |
| 생명 | 함수 실행 중 | 프로그램 전체 |
| 초기화 | 호출할 때마다 | 최초 한 번(사실은 프로그램 시작 시) |
| 범위 | 함수 안 | 함수 안 (그대로) |
"범위는 지역, 생명은 전역" 이 정확한 표현이다.
초기화 시점의 오해
static int x = 5; // 함수가 처음 불릴 때 5를 대입하는 것이 아니다
프로그램 시작 전 startup 코드가 .data 섹션을 플래시에서 RAM으로
복사할 때 이미 5가 들어간다. 런타임 비용이 0이다.
초기값이 0이거나 없으면 .bss에 들어가 startup 코드가 0으로 채운다.
임베디드에서의 활용
대용량 버퍼를 스택 대신 static으로
void process(void) {
uint8_t buf[4096]; // ✗ 스택에 4KB — 스택 오버플로 위험
}
void process(void) {
static uint8_t buf[4096]; // ✓ .bss 에 배치. 스택 안전
}
MCU의 스택은 보통 1~8KB로 매우 작다. 큰 배열을 지역 변수로 잡는 것이
스택 오버플로의 대표 원인이다. static으로 옮기면 링커가 컴파일 시점에
RAM 크기를 계산해 주므로 오버플로 위험이 사라진다.
static의 함정 — 재진입성이 깨진다
char *int_to_str(int n) {
static char buf[12]; // 함수를 나가도 유효 — 편리해 보이지만
sprintf(buf, "%d", n);
return buf;
}
두 번 부르면 앞의 결과가 덮인다.
printf("%s %s", int_to_str(1), int_to_str(2)); // "2 2" 가 나올 수 있다
더 위험한 것은 ISR과 메인 루프가 같은 함수를 부르는 경우다.
메인이 실행 중일 때 인터럽트가 들어와 같은 buf를 덮어쓰면
메인의 결과가 조용히 손상된다.
표준 라이브러리의 strtok, asctime, rand 가 이 문제를 갖고 있어
재진입 버전(strtok_r 등)이 따로 존재한다.
→ ISR에서 부를 수 있는 함수에는 static 지역 변수를 두지 않는다.
C++에서 추가되는 의미
class Sensor {
static int count; // 클래스 변수 — 모든 인스턴스가 공유
static void reset(); // 정적 멤버 함수 — this 가 없다
};
임베디드 C++에서는 정적 멤버 함수가 ISR 콜백으로 등록 가능하다는 점이
실용적으로 중요하다(일반 멤버 함수는 this 때문에 함수 포인터로 못 넘긴다).