
static 멤버 변수
global variable과 static local variable은 둘 다 static duration 생명주기를 가진다, 즉 프로그램이 시작할때 생성되고 종료할때 소멸되며 그리고 이러한 변수들은 { }스코프를 벗어나도 그 값을 유지한다
int Foo()
{
static int a{};
return ++a;
}
int main()
{
std::cout << Foo() << std::endl;
std::cout << Foo() << std::endl;
std::cout << Foo() << std::endl;
return 0;
}
일반 local variable이라면 함수가 호출될때마다 0으로 초기화되어 1, 1, 1이 나왔겠지만 static local varaible이기 때문에 값을 유지하여 1, 2, 3이 호출된다
클래스 타입은 이러한 static 키워드에 두 가지 추가 용도를 제공한다, 그 용도가 바로 static member variable과 static member function이다
우선 static member variable부터 정리해보자
클래스의 static member variable은 해당 멤버 변수를 static(정적)으로 만들 수 있다, 그 말은 곧 일반 멤버 변수와 달리 static member variable은 해당 클래스 타입의 모든 객체에 공유된다는 말이다
struct Foo
{
static int value; //static멤버변수 선언
};
int Foo::value{ 10 }; //static멤버변수 초기화
int main()
{
Foo f1{};
Foo f2{};
f1.value = 200;
std::cout << f2.value << std::endl;
return 0;
}
static멤버 변수를 변경했기 때문에 분명 f1객체의 value를 변경했는데 f2의 value도 200으로 변경된걸 확인할 수 있다
여기서 중요한 점은 static member는 클래스 객체와 연관되지 않는다는 점이다, static member는 클래스 타입의 객체가 하나도 인스턴스화 되지 않아도 존재한다 (static 값은 프로그램 시작 시 생성, 종료 시 소멸되기 때문에 일반 멤버처럼 클래스 객체와 연관되지 않음)
본질적으로 static member는 클래스의 { } 영역에 존재하는 전역 변수이다
따라서 다음과 같이 접근이 가능하다
Foo::value = 100;
std::cout << Foo::value << std::endl;
(객체가 아닌 클래스 이름::을 통해 참조)
위에서 설명한대로 전혀 인스턴스화 하지 않고도 접근하여 사용이 가능하다 (이런 방식으로 사용하는걸 권장한다)
static member variable 초기화
클래스 내부에 static 멤버변수를 선언할 때 우리는 전방 선언과 비슷하게 컴파일러에게 이러한 static 멤버 변수의 존재를 알리는것이다 (정의하는게 아님)
본질적으로 class { }영역에 존재하는 전역변수이기 때문에 클래스 외부의 global scope에서 static 멤버 변수를 정의해야 한다
struct Foo
{
static int value; //static멤버변수 선언 (클래스 내부 스코프에서 선언만)
};
int Foo::value{ 10 }; //static멤버변수 초기화 (클래스 외부 스코프에서 정의)
기본적으로 초기화 값이 없다면 0으로 초기화 된다
또한 static 멤버의 정의는 접근 제어의 영향을 받지 않는다, 클래스 내부에서 private이나 protected아래에 선언되어도 값을 정의하고 초기화가 가능하다 (왜냐하면 정의는 접근으로 간주하지 않음)
클래스가 .h에 정의된 경우 static member의 정의는 cpp에서 한다, 혹은 static member를 inline으로 선언하고 바로 정의할 수 있다
inline이 아니라면 static member의 정의를 .h에 하면 안된다 (해당 헤더파일이 두 번 이상 include되면 정의가 중복되어 ODR에 위배되고 Linker error가 발생한다)
하지만 template class라면 static member의 정의는 일반적으로 .h의 template class정의 아래에서 한다 (이러한 정의는 암시적으로 inline이기에 ODR에 위배되지 않는다)
template <typename T>
class Foo
{
public:
static int temp;
};
template <typename T>
int Foo<T>::temp{ 0 };
(template class가 아닐때) static member변수가 static const 정수타입이라면 클래스 정의 내부에서 초기화될 수 있다
class Foo
{
public:
static const int temp{}; //const가 아니라면 compile error
};
float이어도 불가능하다, 이는 const int가 컴파일 타임 상수로 사용될 수 있기 때문에 코드에서 해당 부분을 치환할 수 있기 때문에다, 따라서 변수가 곧 값으로 치환되기 때문에 ODR예외가 된다 (float은 컴파일 타임 상수로 사용이 불가능 함)
C++17부터 static member는 inline변수가 될 수 있다, inline변수는 정의 중복이 가능하기 때문에 클래스 내부 .h에서 정의가 가능하다
class Foo
{
public:
static inline int value{ 10 };
};
(inline으로 하면 const가 아니라도 클래스 내부에서 정의가 가능함)
constexpr 멤버는 C++17부터 암시적으로 inline이기 때문에 static constexpr 멤버도 inline을 명시적으로 사용하지 않고 클래스 내부에서 정의가 가능하다
class Foo
{
public:
static constexpr int value{ 100 };
static constexpr std::string_view svalue{ "Hello" };
};
static member를 constexpr이나 inline으로 만들어 클래스 정의 내에서 초기화 하는걸 권정한다
그렇다면 이런 static 멤버 변수를 어디 보통 사용할까? 클래스의 모든 인스턴스에 고유한 ID를 할당할 수 있다
class Foo
{
public:
Foo()
: id{ idGen++ }
{
}
int GetId() { return id; }
private:
static inline int idGen{ 1 };
int id{};
};
int main()
{
Foo f1{};
Foo f2{};
Foo f3{};
std::cout << f1.GetId() << std::endl;
std::cout << f2.GetId() << std::endl;
std::cout << f3.GetId() << std::endl;
return 0;
}
이전 객체보다 1이 더 증가한 id로 세팅된다 (1, 2, 3출력)
static멤버는 auto로 초기화 값으로부터 타입 추론이 가능하고 CTAD로 클래스 템플릿 인수 추론을 할 수 있다 하지만 non-static멤버는 auto와 CTAD를 사용할 수 없다
(non-static 멤버는 모호하거나 직관적이지 않은 결과 초래가 가능하기 때문, 생성자 멤버 리스트 초기화에서 기존 멤버 변수 초기값과 다른 타입이 들어오게 되면 타입이 모호해지기 때문에 허용하지 않는다)
class Foo
{
public:
auto test{ 100 }; //error
static inline auto test1{ 200 }; //ok
std::vector vec{ 1, 2, 3 }; //error
};
마지막으로 static 멤버 변수는 lookup table을 활용할 때 유용하다, 미리 계산된 값을 저장하는데 사용되는 배열과 같은 데이터를 static으로 만들면 모든 객체에 대해 단 하나만 존재하기 때문에 메모리 절약에 도움이 된다
static 멤버 함수
static 멤버 변수는 public:에 있을때 클래스명::변수;로 접근이 가능하다
Foo::test = 200;
그렇다면 private:에 있을때는 어떻게 접근할까?
일반적으로 public:에 getter를 만들어서 해당 멤버 함수로 접근하지만 그렇게 되면 클래스 타입 객체를 인스턴스화 해야 한다
인스턴스화 하지 않고 가져오려면 static 멤버 함수로 getter를 만들어 가져와야 한다
class Foo
{
public:
static int GetIdGen() { return idGen; } //static member function
private:
static inline int idGen{ 100 }; //static member variable
};
int main()
{
Foo::GetIdGen();
return 0;
}
static 멤버 함수도 특정 객체와 연관되지 않기때문에 static 멤버 변수와 마찬가지로 클래스명::으로 호출이 가능하다
(특정 객체와 연관되지 않기 때문에 static 멤버 함수에는 this포인터가 존재하지 않는다)
this포인터는 항상 멤버 함수를 호출한 객체를 가리킨다, 하지만 static 멤버 함수는 객체와 연관되지 않기 때문에 this포인터가 필요 없는것이다
또한 static 멤버 함수는 다른 static 멤버(함수, 변수)에 접근이 가능하지만 non-static 멤버에는 접근이 불가능하다 (마찬가지로 non-static은 클래스 객체에 속해야 하지만 static 멤버 함수는 클래스 객체와 연관되지 않기 때문)
static 멤버 함수도 class 외부에서 정의될 수 있다
class Foo
{
public:
static int GetIdGen();
private:
static inline int idGen{ 100 };
};
int Foo::GetIdGen() //static 키워드 사용 안함
{
return idGen;
}
기본적으로 클래스 정의 내부에서 정의된 멤버 함수는 암시적으로 inline이다, 하지만 클래스 외부에서 정의된 멤버 함수는 inline이 아니다 (inline 키워드로 inline으로 만들 수 있음)
따라서 .h에 정의된 static 멤버 함수는 해당 .h가 여러곳에서 include되어 ODR규칙을 위배하지 않도록 inline으로 만들어야 한다
class Foo
{
public:
static inline int GetIdGen();
private:
static inline int idGen{ 100 };
};
inline int Foo::GetIdGen() //ODR위배되지 않음
{
return idGen;
}
모든 멤버가 static인 클래스는 순수 정적 클래스(pure static class)라고 부른다, 이러한 순수 정적 클래스는 유용하게 사용되지만 단점도 존재한다
순수 정적 클래스 vs namespace
순수 정적 클래스와 namespace는 유사한 부분이 많다, 둘다 scope내에서 static한 생존기간을 가진 변수와 함수를 정의할 수 있게 해준다 하지만 순수 정적 클래스는 클래스기 때문에 접근 지정자를 가지지만 namespace는 그렇지 않다는 점이다
일반적으로 static member가 있거나 접근 지정자가 필요한 경우에는 클래스를 권장하고 그렇지 않다면 namespace를 권장한다
C++은 static 생성자를 지원하지 않는다, 일반적으로 생성자를 이용하여 일반 변수를 초기화하기 때문에 static생성자가 있어서 static 멤버 변수를 초기화 할 수 있을것 같지만 그렇지 않다
struct FooStruct
{
char a{};
char b{};
};
class Foo
{
private:
static inline FooStruct fs{ 'a', 'b' }; //정의 동시에 static 멤버 변수 초기화
static inline int idGen{ 100 };
};
만약 static멤버 변수를 초기화하는데 일반적인 값이 아니고 코드가 필요하다면 헬퍼 함수를 만들어 초기화 해줘야 할 수 있다
struct FooStruct
{
char a{};
char b{};
};
class Foo
{
private:
static FooStruct genFooStruct() //static 멤버 변수를 초기화하기 위한 helper함수
{
FooStruct tempfs{};
tempfs.a = 'a';
tempfs.b = 'b';
//원하는 코드로 초기화 시키면 됨
return tempfs;
};
static inline FooStruct fs{ genFooStruct() }; //함수로 static 멤버 변수 초기화하기
};
friend 비멤버 함수
friend 키워드를 사용하면 컴파일러에게 다른 클래스나 함수가 friend임을 알릴 수 있다, 여기서 friend란 다른 클래스의 private, protected 멤버에 대한 완전한 접근 권한이 부여된 클래스 혹은 함수를 말한다
다음 코드를 보면서 정리해보자

현재 test라는 멤버 변수는 Foo class의 private에 있기 때문에 다른곳에서 접근이 불가능해 compile error가 발생한걸 확인할 수 있다
이때 해당 함수를 friend로 만든다면 해당 함수에서 접근이 가능해진다
class Foo
{
private:
int test{ 100 };
friend void print(const Foo& InFoo);
};
void print(const Foo& InFoo) //print()를 friend로 지정했기 때문에 private멤버인 test에 접근이 가능하다
{
std::cout << InFoo.test << std::endl;
}
int main()
{
Foo f1{};
print(f1);
return 0;
}
클래스 내부에 friend함수를 정의해도 그 함수는 비멤버 함수가 된다
class Foo
{
private:
int test{ 100 };
friend void print(const Foo& InFoo)
{
std::cout << InFoo.test << std::endl;
}
};
int main()
{
Foo f1{};
print(f1); //비멤버 함수가 되었기 때문에 객체를 통해 호출하지 않아도 사용이 가능하다
return 0;
}
함수는 동시에 여러개의 class의 friend가 될 수 있다
class Archer;
class Knight
{
public:
friend void print(const Knight& InKnight, const Archer& InArcher);
private:
int Sword{};
};
class Archer
{
public:
friend void print(const Knight& InKnight, const Archer& InArcher);
private:
int bow{};
};
//Archer, Knight class의 print()가 동시에 friend로 적용되어 둘다 private에 접근이 가능함
void print(const Knight& InKnight, const Archer& InArcher)
{
std::cout << InKnight.Sword << InArcher.bow << std::endl;
}
int main()
{
Knight k1{};
Archer a1{};
print(k1, a1);
return 0;
}
그렇다면 이러한 friend가 데이터 은닉 원칙을 위반하지는 않을까?
결론적으로는 그렇지 않다, 결국 class 자기 자신이 필요에 의해 private에 접근을 허용할 함수를 지정하는것이기 때문이다
friend는 클래스의 구현에 직접 접근이 가능하기 때문에 클래스 구현의 변경은 곧 friend의 변경도 필요하게 만든다, 따라서 너무 많은 friend가 클래스에 존재하게 되면 나중에 수정이 많이 필요할 수 있다
friend class
특정 class를 friend로 지정하게 되면 그 class에서 자기자신 class의 private: protected:멤버에 접근이 가능하게 할 수 있다
class Archer;
class Knight
{
public:
private:
friend class Archer; //Archer클래스를 friend로 지정
int Sword{};
};
class Archer
{
public:
void foo(const Knight& InKnight)
{
InKnight.Sword; //Knight클래스에서 Archer클래스를 friend로 지정했으니 Archer클래스에서 Knight의 private: protected:의 멤버에 접근이 가능하다
}
private:
int bow{};
};
friend이더라도 this포인터에 접근은 불가능하다

Archer 클래스가 Knight 클래스의 friend라고해서 Knight 클래스가 Archer의 friend인건 아니다
class A가 B의 friend이고 B가 C의 friend라고 해서 A가 C의 friend는 아니다
friend는 상속되지 않는다, A가 B의 friend이고 A를 상속받은 클래스 C는 B의 friend가 아니다
friend 선언은 전방선언을 포함한다, 따라서 미리 friend가 될 클래스의 전방선언은 필요없다
friend 멤버 함수
클래스 전체를 friend로 만드는 대신 단일 멤버 함수만 friend로 만들 수 있다
단순하게 다음과 같은 코드로 작성하면 컴파일 에러가 발생한다

왜냐하면 컴파일러가 friend 멤버 함수의 클래스에 대한 전체 정의를 알아야 하기 때문이다
(전방선언만으로는 부족)
그렇다면 클래스 정의 순서를 변경하면 어떨까?

이제는 Knight 클래스를 컴파일러가 알지 못하기 때문에 에러가 발생한다
결국 단순히 클래스 정의 순서만 변경하는 방식으로는 불가능하다
사용하는 클래스 전방선언을 하고 멤버 함수의 정의를 분리하는 방법이 있다

이제 class Knight로 전방선언을 했기 때문에 Archer 클래스에서 Knight 타입을 사용할 수 있고 Archer::TestArchor()를 Knight class 밑에서 정의했기 때문에 에러가 발생하지 않는다
또한 friend void Archer::TestArcher()로 Archer클래스의 TestArcher함수를 friend로 지정했기 때문에 Knight클래스의 private: protected: 멤버에 접근이 가능하다
이러한 경우는 같은 파일에서 여러개의 class를 정의했기 때문에 발생한다, 각각 다른 .h, .cpp에서 클래스를 선언하고 정의한 뒤 .h를 #include하면 깔끔하게 처리가 가능하다
//Archer.h
class Knight;
class Archer
{
public:
void TestArcher(const Knight& InKnight);
private:
int bow{};
};
//Archer.cpp
#include "Knight.h"
void Archer::TestArcher(const Knight& InKnight)
{
std::cout << InKnight.Sword << std::endl; //Knight클래스에서 Archer::TestArcher가 friend멤버 함수로 지정되었기 때문에 Sword에 접근이 가능하다
}
//Knight.h
#include "Archer.h"
class Knight
{
public:
private:
friend void Archer::TestArcher(const Knight& InKnight);
int Sword{};
};
//main.cpp
#include "Archer.h"
#include "Knight.h"
int main()
{
Archer A1{};
Knight K1{};
A1.TestArcher(K1);
return 0;
}
참조 한정자
데이터 멤버에 대한 참조를 반환하는 멤버함수에서, 암시적 객체가 rvalue일때 데이터 멤버에 대한 참조를 반환하는 함수를 호출하는건 위험하다
class Foo
{
public:
Foo(std::string InName) : name{ InName }
{
}
const std::string& GetName() { return name; }
private:
std::string name{};
};
Foo CreateFoo(std::string InName)
{
Foo f1{ InName };
return f1;
}
int main()
{
const Foo& foo = CreateFoo("Kelvin");
foo.GetName();
return 0;
}
C++표준에 따르면 임시객체가 const lvalue참조 혹은 rvalue참조에 직접 바인딩(&&)될 경우 임시 객체의 수명은 참조의 수명과 같아지도록 연장된다, 따라서 위 코드는 dangling 참조가 발생하지 않는다 (일반 lvalue참조는 임시객체를 참조할 수 없다)
하지만 다음은 다르다
int main()
{
const std::string& refName{ CreateFoo("Kelvin").GetName() };
std::cout << refName << std::endl;
}
이 코드는 dangling 참조를 발생시켜 undefined behavior를 발생시킬 수 있다 (CreateFoo("Kelvin")으로 생성된 임시객체는 해당 라인에서 생성되고 바로 소멸되기 때문, 직접 바인딩이 아님)
만약 GetName()이 값을 반환한다면 암시적 객체가 rvalue일때 안전하지만 암시적 객체가 lvalue라면 불필요한 복사가 발생한다
그렇다면 GetName()이 const&를 반환한다면 위와 같은 케이스로 undefined behavior가 발생할 수 있다
C++11에서 참조 한정자 (ref-qualifier)를 도입하여 해당 멤버함수가 lvalue 암시적 객체에서 호출되는지 아니면 rvalue 암시적 객체에서 호출되는지에 따라 멤버 함수를 오버로딩 할 수 있게 되었다
다음과 같이 사용할 수 있다
class Foo
{
public:
Foo(std::string InName) : name{ InName }
{
}
const std::string& GetName()&
{
return name;
}
std::string GetName()&&
{
return name;
}
private:
std::string name{};
};
Foo CreateFoo(std::string InName)
{
Foo f1{ InName };
return f1;
}
int main()
{
Foo f1{ "Kelvin" };
std::cout << f1.GetName() << std::endl; //멤버 함수를 호출하는 암시적 객체가 lvalue이기 때문에 참조 반환을 호출한다
std::cout << CreateFoo("Kelvin").GetName() << std::endl; //멤버 함수를 호출하는 암시적 객체가 rvalue이기 때문에 값 반환 &&함수를 호출한다
return 0;
}
멤버 함수를 호출하는 암시적 객체가 lvalue라면 참조를 반환하는 &함수를 호출하고, rvalue라면 값을 반환하는 &&를 호출하게 오버로딩 한 코드이다
(선택적으로 함수를 호출함으로서 안전함, 성능 향상을 챙길 수 있다)
이때 &만 정의하고 &&는 정의하지 않으면 rvalue 암시적 객체로부터는 멤버 함수 호출이 불가능하다 (compile error)
여기서 주의할 점이 있다
//X
std::string GetName()
{
return name;
}
std::string GetName()&
{
return name;
}
//O
std::string GetName()&
{
return name;
}
std::string GetName()&&
{
return name;
}
std::string GetName() const&
{
return name;
}
&&버전이 없어도 rvalue 암시적 객체로부터 호출이 가능하다
std::string GetName() && = delete;
하지만 참조 한정자는 사용을 권장하지 않는다
또한 많은 개발자들이 잘 모르기 때문에 비효율적일 수 있으며 표준 라이브러리에서 이 기능을 사용하지 않는다
왠만하면 임시객체의 멤버함수 반환값을 즉시 사용하고 캐싱해서 사용하지 않는 방식으로 작업하는게 좋다