C++에서 static을 처음 배우면 조금 혼란스럽다.
함수 안에서도 사용할 수 있고, 전역 변수에도 붙일 수 있으며, 클래스의 멤버 변수와 멤버 함수에도 붙일 수 있기 때문이다.
static int Value;
class Cat
{
static int Count;
static void PrintCount();
};
모두 같은 static이라는 키워드를 사용하지만 의미가 완전히 동일한 것은 아니다.
특히 static = 프로그램이 끝날 때까지 살아 있는 변수라고만 기억하면 파일 범위 static이나 정적 멤버 함수를 설명하기 어려워진다.
그래서 이번에는 static을 어디에 사용했는가를 기준으로 나누어 정리해봤다.
변수의 수명과 범위
변수를 이해할 때는 수명(storage duration)과 범위(scope)를 구분해서 보는 것이 중요하다.
C에서 공부했던 내용을 단순화하면 다음처럼 볼 수 있다.
| 종류 | 수명 | 접근 가능한 범위 |
| 일반 지역 변수 | 함수 실행 동안 | 해당 블록 |
| 정적 지역 변수 | 프로그램 실행 동안 | 해당 블록 |
| 전역 변수 | 프로그램 실행 동안 | 선언을 볼 수 있는 영역 |
| 파일 범위 static 변수 | 프로그램 실행 동안 | 해당 번역 단위 |
여기서 중요한 점은 수명과 범위는 서로 다른 개념이라는 것이다.
예를 들어 정적 지역 변수는 함수가 끝난 뒤에도 객체 자체는 살아 있지만, 그 이름은 함수 밖에서 사용할 수 없다.
void Foo()
{
static int Count = 0;
}
Count의 수명은 길지만 scope는 여전히 Foo() 내부다.
함수 내부의 static
함수 내부에서 static 지역 변수를 선언하면 해당 객체는 static storage duration, 즉 정적 저장 기간을 가진다.
선언과 동시에 초기화하면, 초기화는 한 번만 이루어진다!
함수 내부의 정적 지역 변수는 해당 선언에 처음 도달했을 때 초기화되고, 이후 함수가 다시 호출되어도 다시 초기화되지 않는다.
C++11부터는 함수 지역 정적 변수의 초기화가 스레드 안전하게 한 번만 수행되도록 언어 차원에서 보장된다.
#include <iostream>
void CreateCanCoke()
{
static int CreatedCanCokeCount = 0; // 선언과 동시에 초기화
++CreatedCanCokeCount;
std::cout
<< "CreatedCanCokeCount: "
<< CreatedCanCokeCount
<< '\n';
}
int main()
{
CreateCanCoke(); //1
CreateCanCoke(); //2
CreateCanCoke(); //3
}
실행 결과는 다음과 같다.
CreatedCanCokeCount: 1
CreatedCanCokeCount: 2
CreatedCanCokeCount: 3
일반 지역 변수였다면 함수가 호출될 때마다 새로 만들어졌을 것이다.
void CreateCanCoke()
{
int CreatedCanCokeCount = 0;
++CreatedCanCokeCount;
}
이 경우 호출할 때마다 다시 0에서 시작한다.
(+) 선언과 동시에 초기화 하지 않으면, 이후 코드가 매번 실행되어 의도한 동작이 안 될 수도 있다는 점 참고!
#include <iostream>
void CreateCanCoke()
{
static int CreatedCanCokeCount; // 선언과 동시에 초기화 하기 않으면
CreatedCanCokeCount = 0;
++CreatedCanCokeCount;
std::cout
<< "CreatedCanCokeCount: "
<< CreatedCanCokeCount
<< '\n';
}
int main()
{
CreateCanCoke(); // 1
CreateCanCoke(); // 1
CreateCanCoke(); // 1
}
데이터 섹션이라고 외우지 않기
정적 변수에 대해 흔히
static 변수는 데이터 영역에 저장된다.
라고 설명한다.
static의 본질은 "데이터 영역에 저장된다"가 아니라 "정적 저장 기간을 가진다"는 것이다.
.data나 .bss에 저장되는 것은 일반적인 컴파일 환경에서 그 규칙을 구현하는 대표적인 방법이다.
따라서 개념적으로는
일반 지역 변수
→ 함수 호출 동안 살아 있음
→ automatic storage duration
→ 일반적으로 스택을 사용
static 지역 변수
→ 프로그램 종료까지 살아 있음
→ static storage duration
→ 일반적으로 .data / .bss 등의 정적 영역 사용
으로 이해하는 것이 더 정확하다.
클래스의 static 멤버 변수 - 정적 멤버 변수
클래스 안에서도 멤버 변수에 static을 붙일 수 있다.
class Cat
{
public:
Cat();
private:
static float TailLength;
};
일반 멤버 변수는 객체마다 하나씩 존재한다.
class Cat
{
float Weight;
};
Cat A;
Cat B;
Cat C;
라면 Weight는 각각의 객체 A, B, C가 자기 값을 가진다.
하지만 정적 멤버 변수는 다르다.
class Cat
{
static float TailLength;
};
Cat 객체가 몇 개 만들어지든 TailLength는 객체마다 복제되는 멤버가 아니다.
Cat A
Cat B
Cat C
↓
Cat::TailLength 하나를 공유
따라서 정적 멤버 변수는 특정 객체가 아니라 클래스 자체에 속하는 상태를 표현할 때 사용할 수 있다.
예를 들어 지금까지 생성된 객체 수를 기록할 수 있다.
class Cat
{
public:
Cat();
static int GetCount();
private:
static int Count;
};
int Cat::Count = 0;
Cat::Cat()
{
++Count;
}
int Cat::GetCount()
{
return Count;
}
Cat A;
Cat B;
Cat C;
std::cout << Cat::GetCount();
결과는 3이 된다.
정적 멤버 변수의 정의
전통적인 C++ 코드에서는 클래스 안에 정적 멤버 변수를 선언한 뒤 cpp 파일에서 별도로 정의한다.
// Cat.h
#pragma once
class Cat
{
private:
static float TailLength; // 정적 멤버의 선언
};
// Cat.cpp
#include "Cat.h"
float Cat::TailLength = 5.0f;
클래스 안의
static float TailLength;
는 정적 멤버의 선언이다.
실제 정의는
float Cat::TailLength = 5.0f;
처럼 클래스 밖에 작성한다.
C++17에서는 inline static도 사용할 수 있다
정적 멤버 변수는 반드시 cpp 파일에서 초기화해야 한다.
라고 외우면 현대 C++에서는 정확하지 않다.
C++17부터는 inline static 멤버를 사용할 수 있다.
class Cat
{
private:
inline static float TailLength = 5.0f;
};
이 경우 헤더에서 바로 정의하고 초기화할 수 있다.
여러 cpp 파일에서 이 헤더를 include해도 inline 변수에 대한 ODR 규칙 덕분에 중복 정의 문제가 발생하지 않는다.
따라서 다음 두 방식을 구분해두면 된다.
// 전통적인 방식
// Cat.h
class Cat
{
static float TailLength; // 선언만 - 헤더에서 초기화할 경우 중복 정의 에러 발생
};
// Cat.cpp
float Cat::TailLength = 5.0f;
또는 C++17 이후에는
class Cat
{
// inline static 멤버로 헤더에서 바로 정의하고 초기화 가능
inline static float TailLength = 5.0f;
};
처럼 작성할 수 있다.
생성자에서 정적 멤버를 초기화하지 않는 이유
정적 멤버 변수는 일반 멤버 변수와 달리 생성자 초기화 리스트에서 초기화하는 대상이 아니다.
// Cat.h
class Cat
{
public:
Cat(float InWeight)
: Weight(InWeight) // 초기화
{
TailLength = 5; // 대입 - 문법적으로 가능
// 하지만 객체 생성 때마다 공유 변수에 대입됨
}
private:
float Weight;
static float TailLength;
};
Weight는 각각의 Cat 객체가 자기 값을 가지므로 객체 생성 과정에서 초기화하는 것이 자연스럽다.
객체마다 만들어지는 값이 아니기 때문에 객체 생성자의 멤버 초기화 리스트에서 초기화하는 개념과 맞지 않는다.
(생성자 초기화 리스트는 기본적으로 지금 생성 중인 객체의 구성 요소(subobject)를 초기화하는 곳이다.)
생성자 본문에서 정적 멤버에 값을 대입하는 것은 가능하지만, 이는 초기화가 아니라 객체가 생성될 때마다 수행되는 일반적인 대입이다.
// Cat.cpp
float Cat::TailLength = 5.0f; // 반드시 Cat::
클래스 밖에서 초기화해줄 경우, 반드시 Cat:: 을 붙여 초기화해주어야 한다.
그렇지 않으면 Cat의 정적 멤버를 정의하는 것이 아니라 별개의 전역 변수를 정의하는 것이 된다.
static과 접근 지정자는 별개의 개념이다
#include <iostream>
using namespace std;
class Cat
{
public:
// static 이라고 해서 자동으로 클래스 내부에서만 접근 가능한 것은 아니다.
inline static int PublicCount = 0;
private:
inline static int CatCount = 5;
};
int main()
{
// public static 변수 - 클래스 외부에서도 직접 접근하고 수정 가능
Cat::PublicCount = 10;
// private static 변수
//Cat::CatCount = 10; // 컴파일 에러
}
static 멤버 함수
멤버 함수에도 static을 붙일 수 있다.
class Math
{
public:
static float Square(float Value)
{
return Value * Value;
}
};
[특징 1] 정적 멤버 함수는 객체가 없어도 호출할 수 있다.
float Result = Math::Square(12.0f);
[특징 2] 정적 멤버 함수에는 특정 객체가 없으므로 this 포인터도 없다.
일반 멤버 함수의 예시를 보자.
#include <iostream>
class Cat
{
public:
Cat(int InAge)
: Age(InAge)
{
}
void PrintAge()
{
std::cout << this->Age << '\n';
}
private:
int Age;
};
int main()
{
Cat CatA(3);
CatA.PrintAge(); // 3
return 0;
}
PrintAge() 내부의 this는 개념적으로 &CatA 를 가리킨다.
반면, 정적 멤버 함수에는 특정 객체가 없으므로 this 포인터도 없다.
class Cat
{
public:
static void StaticFunction()
{
// std::cout << this->Age; // ❌ this가 없음
}
private:
int Age = 3;
};
따라서 정적 멤버 함수는 특정 객체의 상태와 관계없이 클래스라는 논리적 범위에 묶고 싶은 기능을 표현할 때 사용할 수 있다.
static 멤버 함수가 일반 멤버 변수에 접근할 수 없는 이유
다음 클래스를 생각해보자.
class Cat
{
public:
void NormalFunction();
static void StaticFunction();
private:
int Age = 3;
static int Count;
};
일반 멤버 함수에서는 둘 다 접근할 수 있다.
void Cat::NormalFunction()
{
Age = 5;
Count = 10;
}
왜 가능할까?
일반 멤버 함수에는 this가 있기 때문이다.
this->Age
라고 생각할 수 있다.
어떤 객체의 Age인지 명확하다. (특정 가능)
정적 멤버 변수 Count 역시 클래스 전체에 하나뿐이므로 접근할 수 있다. (특정 가능)
반면 정적 멤버 함수는 다르다.
void Cat::StaticFunction()
{
Age = 5; // 오류
Count = 10; // 가능
}
StaticFunction()에는 this가 없다.
따라서
Age = 5;
라고 작성하면 컴파일러 입장에서는 이런 문제가 생긴다.
어느 Cat의 Age를 말하는 것인가?
A.Age?
B.Age?
C.Age?
특정할 수 없다.
반면 Count는 클래스 전체에 하나다.
Cat::Count
이므로 어떤 객체를 선택할 필요가 없다.
예시 코드
왜 GetCatCount()도 static인가?
여기서 CatCount는 특정 Cat 객체의 상태가 아니다.
따라서 굳이 특정 객체를 통해 물어볼 필요 없이, 클래스 자체에게 현재 고양이가 몇 마리인지 묻는 것이 의미상 자연스럽다.
#include <iostream>
using namespace std;
class Cat
{
public:
// static 이라고 해서 자동으로 클래스 내부에서만 접근 가능한 것은 아니다.
inline static int PublicCount = 0;
static int GetCatCount()
{
return CatCount; // 클래스 내부니까 Cat:: 없어도 됨
}
private:
inline static int CatCount = 5;
};
int main()
{
// public static 변수 - 클래스 외부에서도 직접 접근하고 수정 가능
Cat::PublicCount = 10;
++Cat::PublicCount;
cout << Cat::PublicCount << "\n"; // 11
// private static 변수
//Cat::CatCount = 10; // 컴파일 에러
int temp = Cat::GetCatCount();
cout << temp<< "\n"; // 5
}
멤버와 static 멤버의 접근 관계
정리하면 다음과 같다.
| 호출 위치 | 일반 멤버 변수 | static 멤버 변수 |
| 일반 멤버 함수 | 해당 객체(this)의 멤버 변수이므로 특정 가능 | 클래스 단위로 한 개만 존재하므로 특정 가능 |
| static 멤버 함수 | 직접 접근 불가능 | 어떤 객체를 선택할 필요가 없기 때문에 가능 |
핵심은 static이라는 문법 자체보다 현재 함수에 특정 객체를 나타내는 this가 존재하는가이다.
일반 멤버 함수
→ this 있음
→ 특정 객체의 일반 멤버 접근 가능
static 멤버 함수
→ this 없음
→ 특정 객체의 일반 멤버를 알 수 없음
물론 정적 멤버 함수에 객체를 직접 전달한다면 그 객체의 멤버에는 접근할 수 있다.
class Cat
{
public:
static void PrintAge(const Cat& Target)
{
std::cout << Target.Age;
}
private:
int Age = 3;
};
문제는 정적 멤버 함수가 멤버에 절대 접근할 수 없다는 것이 아니라, 암시적인 대상 객체인 this가 없다는 것이다.
static 함수만 모은 클래스
다음처럼 특정 객체를 만들 필요가 없는 함수들을 하나의 클래스에 모으는 코드도 작성할 수 있다.
class MyMath
{
public:
static float Square(float Value)
{
return Value * Value;
}
static float Power(float Value, float Exponent)
{
return std::pow(Value, Exponent);
}
private:
MyMath() = delete;
};
사용할 때는 객체를 만들지 않는다.
float A = MyMath::Square(3.0f);
float B = MyMath::Power(2.0f, 3.0f);
이런 클래스는 사실상 클래스 이름을 namespace처럼 사용하여 관련 함수를 묶은 형태에 가깝다.
다만 C++에서는 객체 상태가 전혀 필요 없는 단순 함수 그룹이라면 클래스만이 정답은 아니다.
namespace MyMath
{
float Square(float Value)
{
return Value * Value;
}
}
처럼 namespace를 사용할 수도 있다.
따라서
객체의 개념이 필요한가?
클래스의 private 상태와 함께 관리되어야 하는가?
단순한 함수 그룹인가?
를 보고 클래스와 namespace 선택하는 것이 더 자연스럽다.
static 멤버 함수의 필요성
객체는 필요 없지만 클래스의 캡슐화 경계 안에 있어야 할 때 정적 멤버 함수가 특히 의미가 있다.
- Player::Create() 같은 private 생성자를 사용하는 Factory (객체 없이 생성 필요한 경우)
- GameManager::GetInstance() 같은 Singleton 접근 함수
- Enemy::GetCount() 같은 클래스 공유 상태 접근 함수
정리
static은 사용되는 위치에 따라 의미가 달라진다.
함수 내부 static
→ 함수가 끝나도 객체의 수명을 유지한다.
파일 범위 static
→ 이름을 해당 번역 단위 내부로 제한한다.
static 멤버 변수
→ 객체마다 존재하지 않고 클래스 단위로 공유한다.
static 멤버 함수
→ 특정 객체 없이 호출하며 this 포인터가 없다.
처음에는 static을 단순히 "오래 살아 있는 변수" 정도로 이해했는데, 사용 위치별로 나누어보니 수명, 링키지, 객체 소속 여부라는 서로 다른 문제를 하나의 키워드가 다루고 있다는 점이 핵심이었다.
'C++ > C++ 문법과 핵심 개념' 카테고리의 다른 글
| C++ extern과 파일 범위 static — 다른 cpp 파일의 변수는 어떻게 연결될까? (0) | 2026.08.27 |
|---|---|
| C++ 동적 메모리 할당은 왜 필요할까? - Stack과 Heap (0) | 2026.08.27 |
| C++ 멤버 접근 제어 방법 - Getter/Setter, friend, using (1) | 2026.08.26 |
| C++ Struct와 Class의 차이, 서로 상속할 수 있을까? (0) | 2026.08.26 |
| [C++ 실수 자료형 3] 부동소수점 오차와 대응 방법 (0) | 2026.08.06 |