danbibibi
article thumbnail

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
profile

danbibibi

@danbibibi

꿈을 꾸는 시간은 멈춰 있는 것이 아냐 두려워하지 마 멈추지 마 푸른 꿈속으로