C++를 쓰다 보면 static이라는 키워드가 여기저기 등장한다.
문맥에 따라 의미가 달라지기 때문에 헷갈리기 쉽다.
이 글에서는 static을 중심으로, 함께 알아두면 좋은 저장소 관련 개념들을 정리한다.
변수의 생명주기(Storage Duration) 개요
C++에서 모든 변수는 언제 메모리에 올라가고, 언제 해제되는가에 따라 분류된다.
| 종류 | 생성 시점 | 소멸 시점 | 예시 |
|---|---|---|---|
| 자동(Automatic) | 선언 시점 | 블록 종료 시 | 지역 변수 |
| 정적(Static) | 프로그램 시작 (또는 첫 사용) | 프로그램 종료 | static 변수, 전역 변수 |
| 동적(Dynamic) | new 호출 시 |
delete 호출 시 |
힙 할당 객체 |
| 스레드(Thread) | 스레드 시작 시 | 스레드 종료 시 | thread_local 변수 |
static 키워드 — 4가지 용법
static은 쓰이는 위치에 따라 의미가 완전히 다르다.
지역 변수에 static — 값이 유지되는 지역 변수
함수 안에서 선언하지만, 함수가 끝나도 값이 사라지지 않는다.
다음 호출 때도 이전 값을 그대로 기억한다.
int counter() {
static int count = 0; // 최초 1회만 초기화, 이후 유지
return ++count;
}
int main() {
std::cout << counter() << "\n"; // 1
std::cout << counter() << "\n"; // 2
std::cout << counter() << "\n"; // 3
}
활용: 호출 횟수 카운터, 캐시, 싱글톤 인스턴스 보관
전역/파일 범위 변수에 static — 파일 내부로 숨기기
전역 변수 앞에 static을 붙이면 해당 .cpp 파일 안에서만 접근 가능하다.
다른 파일에서 extern으로 참조할 수 없다.
// file_a.cpp
static int hiddenValue = 42; // 이 파일에서만 접근 가능
int publicValue = 100; // 다른 파일에서도 접근 가능
// file_b.cpp
extern int publicValue; // OK
extern int hiddenValue; // 링크 에러! 접근 불가
활용: 파일 내부 구현 세부사항을 외부에 노출하지 않을 때 (캡슐화)
클래스 멤버 변수에 static — 모든 객체가 공유하는 변수
인스턴스(객체)가 몇 개 만들어지든 딱 하나의 변수를 모두 공유한다.
class Player {
public:
Player() { ++totalCount; }
~Player() { --totalCount; }
static int totalCount; // 모든 Player 객체가 공유
};
int Player::totalCount = 0; // 클래스 외부에서 정의 필수
int main() {
Player p1, p2, p3;
std::cout << Player::totalCount << "\n"; // 3
{
Player p4;
std::cout << Player::totalCount << "\n"; // 4
} // p4 소멸
std::cout << Player::totalCount << "\n"; // 3
}
활용: 객체 생성 횟수 추적, 공유 설정값, 싱글톤 인스턴스 보관
클래스 멤버 함수에 static — 인스턴스 없이 호출 가능한 함수
static 멤버 함수는 this 포인터가 없다.
즉, 객체를 만들지 않아도 클래스 이름으로 직접 호출할 수 있다.
class MathHelper {
public:
static int add(int a, int b) {
return a + b;
}
static double pi() {
return 3.14159265;
}
};
int main() {
// 객체 생성 없이 바로 호출
std::cout << MathHelper::add(3, 4) << "\n"; // 7
std::cout << MathHelper::pi() << "\n"; // 3.14159...
}
활용: 유틸리티 함수, 팩토리 메서드, getInstance() (싱글톤)
extern — 다른 파일의 변수/함수 참조
extern은 "이 변수/함수는 다른 곳에 정의되어 있다" 고 컴파일러에게 알리는 선언이다.
// config.cpp
int maxRetryCount = 5; // 여기서 정의(definition)
void initConfig() { /*...*/ }
// main.cpp
extern int maxRetryCount; // 선언만 (메모리 할당 없음)
extern void initConfig();
int main() {
initConfig();
std::cout << maxRetryCount << "\n"; // 5
}
헤더 파일 패턴 (권장)
// config.h
extern int maxRetryCount; // 선언
// config.cpp
#include "config.h"
int maxRetryCount = 5; // 정의
// main.cpp
#include "config.h"
// maxRetryCount 사용 가능
⚠️ extern을 남발하면 전역 상태가 복잡해진다. C++17의 inline 변수가 더 나은 대안인 경우가 많다.
thread_local — 스레드별 독립 변수
thread_local로 선언된 변수는 스레드마다 별도의 복사본을 가진다.
전역처럼 보이지만, 각 스레드가 자신만의 값을 독립적으로 관리한다.
#include <thread>
#include <iostream>
thread_local int threadId = 0; // 스레드마다 독립적
void worker(int id) {
threadId = id; // 이 스레드의 threadId만 변경
std::cout << "Thread " << threadId << " running\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
std::thread t3(worker, 3);
t1.join(); t2.join(); t3.join();
// 출력 순서는 다를 수 있지만, 각자 자신의 threadId를 가짐
}
static vs thread_local 비교
전역 static: [ 스레드1 ]──┐
├──▶ [ 공유 변수 1개 ] (동기화 필요!)
[ 스레드2 ]──┘
thread_local: [ 스레드1 ] ──▶ [ 변수 복사본 A ]
[ 스레드2 ] ──▶ [ 변수 복사본 B ] (동기화 불필요)
[ 스레드3 ] ──▶ [ 변수 복사본 C ]
활용: 스레드별 에러 코드, 스레드별 로컬 버퍼, 재진입(Reentrant) 함수 구현
const / constexpr — 컴파일 타임 상수
const — 런타임 상수
값이 초기화 후 변경 불가. 런타임에 결정될 수도 있다.
const int bufferSize = 1024; // 컴파일 타임 상수
const int inputSize = getUserInput(); // 런타임 상수도 가능
bufferSize = 2048; // 에러: 변경 불가
constexpr — 컴파일 타임 보장 상수 (C++11~)
반드시 컴파일 타임에 값이 결정된다. 함수에도 적용 가능.
constexpr int MAX_SIZE = 256;
constexpr int square(int x) { return x * x; }
int arr[square(4)]; // OK: 컴파일 타임에 16으로 결정
static const vs static constexpr
class Config {
public:
static const int VERSION = 1; // 런타임 상수 (ODR 주의)
static constexpr int MAX_CONN = 100; // 컴파일 타임 상수 (권장)
static constexpr double TIMEOUT = 5.0; // C++11~
};
규칙: 컴파일 타임에 알 수 있는 상수는 constexpr을 쓰자. 더 안전하고 최적화도 잘 된다.
inline 변수 (C++17)
C++17 이전에는 헤더 파일에 전역 변수를 정의하면 ODR(One Definition Rule) 위반이 발생했다.
// C++17 이전 — 헤더에 변수 정의 불가
// header.h
int globalVar = 0; // 여러 .cpp에서 include하면 중복 정의 에러!
C++17부터 inline 변수로 해결:
// C++17 — header.h
inline int globalVar = 0; // OK: 여러 번 include해도 하나로 취급
inline const std::string APP_NAME = "MyApp"; // OK
extern 패턴을 대체하는 현대적 방법
// C++17 이전 (extern 패턴)
// config.h → extern int maxRetry;
// config.cpp → int maxRetry = 5;
// C++17 이후 (inline 변수)
// config.h → inline int maxRetry = 5; // 헤더 하나로 끝
개념 비교 정리표
| 키워드 | 저장 위치 | 생명주기 | 범위(Scope) | 주요 용도 |
|---|---|---|---|---|
static (지역) |
데이터 세그먼트 | 프로그램 전체 | 선언된 블록 | 값 유지 지역 변수 |
static (파일) |
데이터 세그먼트 | 프로그램 전체 | 해당 파일만 | 파일 내부 은닉 |
static (멤버) |
데이터 세그먼트 | 프로그램 전체 | 클래스 | 공유 멤버 변수/함수 |
extern |
— (선언만) | — | 전역 | 파일 간 변수 공유 |
thread_local |
스레드 스택/TLS | 스레드 생존기간 | 선언 위치 | 스레드 독립 변수 |
const |
상황에 따라 | 상황에 따라 | 선언 위치 | 변경 불가 값 |
constexpr |
대부분 코드 세그먼트 | 컴파일 타임 | 선언 위치 | 컴파일 타임 상수 |
inline 변수 |
데이터 세그먼트 | 프로그램 전체 | 전역 | 헤더 전역 변수 (C++17) |
마무리: 언제 무엇을 쓸까?
값이 컴파일 타임에 정해지는 상수?
└▶ constexpr
런타임 상수 (변경 불가)?
└▶ const
함수 호출 간 값을 유지하고 싶다?
└▶ static 지역 변수
클래스 모든 객체가 공유하는 데이터?
└▶ static 멤버 변수
파일 내부에서만 쓰는 전역 변수/함수?
└▶ static (파일 범위)
멀티스레드에서 각 스레드가 독립된 변수?
└▶ thread_local
헤더에 전역 변수를 정의하고 싶다? (C++17)
└▶ inline 변수
C++의 static은 문맥에 따라 의미가 달라지는 대표적인 키워드다.
처음엔 헷갈리더라도 각 용법을 하나씩 익혀두면, 코드의 의도를 훨씬 명확하게 표현할 수 있다.
'언어 > C, C++' 카테고리의 다른 글
| C++ 스마트 포인터 (0) | 2026.08.10 |
|---|---|
| [C/C++] 공백을 포함한 파일을 문자열 변수에 쓰기 (0) | 2024.05.27 |
| CMake, CMakeLists.txt 정리 (0) | 2024.02.19 |
| make, Makefile 정리 (0) | 2024.02.13 |
| [C++] stringstream (0) | 2024.01.02 |
