[06-3] C++에서의 static

김민성·2022년 7월 26일
post-thumbnail

C언어에서의 static은 C++에서도 그대로 통용된다. 그러나 C++에서는 멤버변수와 멤버함수에 static 선언을 추가할 수 있다.

C언어에서 이야기한 static

C언어에서 공부한 static의 개념을 정리해보자.

  • 전역변수에 선언된 static의 의미
    ->선언된 파일 내에서만 참조를 허용하겠다는 의미
  • 함수 내에 선언된 static의 의미
    ->한번만 초기화되고, 지역변수와 달리 함수를 빠져나가도 소멸되지 않는다.

그럼 이 중에서 함수 내에 선언된 static의 의미를 확인할 수 있는 간단한 예제를 하나 보자.

CComStatic.cpp

#include <iostream>
using namespace std;

void Counter()
{
	static int cnt;
	cnt++;
	cout<<"Current cnt: "<<cnt<<endl;
}
	
int main(void)
{
	for(int i=0; i<10; i++)
		Counter();
	return 0;
}
Current cnt: 1
Current cnt: 2
Current cnt: 3
Current cnt: 4
Current cnt: 5
Current cnt: 6
Current cnt: 7
Current cnt: 8
Current cnt: 9
Current cnt: 10

static 변수는 전역변수와 마찬가지로 초기화하지 않으면 0으로 초기화된다. 그리고 이 문장은 딱 한번 실행이 된다. 즉, cnt는 Counter 함수가 호출될 때마다 새롭게 할당되는 지역변수가 아니다.

전역변수가 필요한 상황

NeedGlobal.cpp

#include <iostream>
using namespace std;

int simObjCnt=0;
int cmxObjCnt=0;

class SoSimple
{
public:
	SoSimple()
	{
		simObjCnt++;
		cout<<simObjCnt<<"번째 SoSimple 객체"<<endl;
	}
};

class SoComplex
{
public:
	SoComplex()
	{
		cmxObjCnt++;
		cout<<cmxObjCnt<<"번째 SoComplex 객체"<<endl;
	}
	SoComplex(SoComplex &copy)
	{
		cmxObjCnt++;
		cout<<cmxObjCnt<<"번째 SoComplex 객체"<<endl;
	}
};

int main(void)
{
	SoSimple sim1;
	SoSimple sim2;

	SoComplex com1;
	SoComplex com2=com1;
	SoComplex();
	return 0;
}
1번째 SoSimple 객체
2번째 SoSimple 객체
1번째 SoComplex 객체
2번째 SoComplex 객체
3번째 SoComplex 객체

위 예제를 보면 다음 사실을 알 수 있다.

  • simObjCnt는 SoSimple 클래스를 위한 전역변수이다.
  • cmxObjCnt는 SoComplex 클래스를 위한 전역변수이다.

즉, simObjCnt는 SoSimple 객체들이 공유하는 변수이다. 그리고 cmxObjCnt는 SoComplex 객체들이 공유하는 변수이다. 그런데 이 둘은 모두 전역변수이기에 이러한 제한을 지켜 줄만한 아무런 장치도 존재하지 않는다(어디서든 접근이 가능하므로). 따라서 문제를 일으킬 소지가 매우 높다.

그러나 simObjCnt를 SoSimple 클래스의 static 멤버로, cmxObjCnt를 SoComplex 클래스의 static 멤버로 선언하면, 문제의 소지를 없앨 수 있다.

static 멤버변수(클래스 변수)

static 멤버변수는 '클래스 변수'라고도 한다. 일반적인 멤버변수와 달리 클래스당 하나씩만 생성되기 때문이다. 다음은 전역변수 simObjCnt를 SoSimple 클래스의 static 변수로 선언한 예이다.

생성자에서 초기화 시켜주는 것은 말이 안된다. 왜냐하면, 초기화 시켜줄 수는 있으나, 객체가 생성될 때마다 초기화 될 것이다.

따라서 마지막 문장처럼 별도의 초기화 문법을 마련해 주어야 한다.

static 변수 simObjCnt는 SoSimple 객체가 생성될 때마다 함께 생성되어 객체별로 유지되는 변수가 아니다. 객체를 생성하건 생성하지 않건, 메모리 공간에 딱 하나만 할당되어 공유되는 변수이다. 예를 들어 다음과 같이 총 3개의 SoSimple 객체를 생성하게 되면,

다음과 같이 sim1, sim2, sim3 객체가 static 변수 simObjCnt를 공유하는 구조가 된다.

때문에 sim1, sim2, sim3 객체의 멤버함수에서는 simObjCnt에 멤버변수에 접근하듯이 접근이 가능하다. 하지만 그렇다고 해서 객체 내에 simObjCnt가 존재하는 것은 아니다. 이 변수는 객체 외부에 있다. 다만 객체에게 멤버변수처럼 접근할 수 있는 권한을 줬을 뿐이다.

이렇듯 SoSimple 클래스 안에 선언된 static 변수는 모든 SoSimple 객체가 공유하는 구조이다. 그리고 생성 및 소멸의 시점도 전역변수와 동일하다.

따라서 이를 이용하면 앞서 보인 예제 NeedGlobal.cpp를 다음과 같이 보다 안정적으로 재 구현할 수 있다.

StaticMember.cpp

#include <iostream>
using namespace std;

class SoSimple
{
private:
	static int simObjCnt;
public:
	SoSimple()
	{
		simObjCnt++;
		cout<<simObjCnt<<"��° SoSimple ��ü"<<endl;
	}
};
int SoSimple::simObjCnt=0;

class SoComplex
{
private:
	static int cmxObjCnt;
public:
	SoComplex()
	{
		cmxObjCnt++;
		cout<<cmxObjCnt<<"��° SoComplex ��ü"<<endl;
	}
	SoComplex(SoComplex &copy)
	{
		cmxObjCnt++;
		cout<<cmxObjCnt<<"��° SoComplex ��ü"<<endl;
	}
};
int SoComplex::cmxObjCnt=0;

int main(void)
{
	SoSimple sim1;
	SoSimple sim2;

	SoComplex cmx1;
	SoComplex cmx2=cmx1;
	SoComplex();
	return 0;
}
1번째 SoSimple 객체
2번째 SoSimple 객체
1번째 SoComplex 객체
2번째 SoComplex 객체
3번째 SoComplex 객체

static 멤버변수의 또 다른 접근방법

사실 static 멤버변수는 어디서든 접근이 가능한 변수이다. static 멤버가 private로 선언되면, 해당 클래스의 객체들만 접근이 가능하지만, public으로 선언되면, 클래스의 이름 또는 객체의 이름을 통해 어디서든 접근이 가능하다. 이와 관련해 예제를 보자. 이 예제는 static 멤버변수가 객체 내에 존재하지 않는다느 사실도 더불어서 증명하고 있다.

PublicStaticMember.cpp

#include <iostream>
using namespace std;

class SoSimple
{
public:
	static int simObjCnt;
public:
	SoSimple()
	{
		simObjCnt++;
	}
};
int SoSimple::simObjCnt=0; 
//static 멤버변수는 항상 이렇게 초기화

int main(void)
{
	cout<<SoSimple::simObjCnt<<"번째 SoSimple 객체"<<endl;
	SoSimple sim1;
	SoSimple sim2;

	cout<<SoSimple::simObjCnt<<"번째 SoSimple 객체"<<endl;
	cout<<sim1.simObjCnt<<"번째 SoSimple 객체"<<endl;
	cout<<sim2.simObjCnt<<"번째 SoSimple 객체"<<endl;
	return 0;
}
0번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체

실행결과를 통해서도 알 수 있듯이, 다음 세 문장은 동일한 변수에 접근해서 동일한 출력을 보이게 된다.

cout<<SoSimple::simObjCnt;
cout<<sim1.simObjCnt;
cout<<sim2.simObjCnt;

그런데 이 중에서 두 번째, 세 번째 문장은 sim1의 멤버변수와 sim2의 멤버변수에 접근하는 것과 같은 오해를 불러일으킨다. 따라서 public static 멤버에 접근할 때에는 첫 번째 문장에서 보이듯이 클래스의 이름을 이용해서 접근하는 것이 좋다.

static 멤버함수

static 멤버함수 역시 그 특성이 static 멤버변수와 동일하다.

  • 선언된 클래스의 모든 객체가 공유한다.
  • public으로 선언이 되면, 클래스의 이름을 이용해서 호출이 가능하다.
  • 객체의 멤버로 존재하는 것이 아니다.

여기서 가장 주목할 것은 객체의 멤버로 존재하지 않는다는 사실이다. 때문에 다음 코드는 컴파일 에러를 일으킨다.

static 함수가 멤버변수에 접근하는 것은 논리적으로 말이 안된다.

static 함수는 객체 내에 존재하는 함수가 아니기 때문에 멤버변수나 멤버함수에 접근이 불가능하다.
static 함수는 static 변수에만 접근이 가능하고, static 함수만 호출 가능하다.

const static 멤버

const 멤버변수(상수)의 초기화는 이니셜라이저를 통해야만 한다. 그러나 const static으로 선언되는 멤버변수(상수)는 다음과 같이 선언과 동시에 초기화가 가능하다.

ConstStaticMember.cpp

#include <iostream>
using namespace std;

class CountryArea
{
public:
	const static int RUSSIA			=1707540;
	const static int CANADA			=998467;
	const static int CHINA			=957290;
	const static int SOUTH_KOREA	=9922;
};

int main(void)
{
	cout<<"러시아 면적: "<<CountryArea::RUSSIA<<"km^2"<<endl;
	cout<<"캐나다 면적: "<<CountryArea::CANADA<<"km^2"<<endl;
	cout<<"중국 면적: "<<CountryArea::CHINA<<"km^2"<<endl;
	cout<<"한국 면적: "<<CountryArea::SOUTH_KOREA<<"km^2"<<endl;
	return 0;
}
러시아 면적: 1707540km^2
캐나다 면적: 998467km^2
중국 면적: 957290km^2
한국 면적: 9922km^2

const 멤버변수는 선언과 동시에 초기화가 불가능하며, 이니셜라이저를 통해 초기화를 해야하지만,

const static 멤버변수는 클래스가 정의될 때 지정된 값이 유지되는 상수이기 때문에, 위 예제에서 보이는 바와 같이 초기화가 가능하도록 정의하고 있다.

키워드 mutable

mutable이라는 키워드는 사용의 빈도수가 낮은, 아니 가급적 사용의 빈도수를 낮춰야 하는 키워드이다.

mutable 키워드의 의미는,

"const 함수 내에서의 값의 변경을 예외적으로 허용한다."

이다.

Mutable.cpp

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	mutable int num2; 
    // const 함수에 대해 예외를 둔다!
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{  }
	void ShowSimpleData() const
	{
		cout<<num1<<", "<<num2<<endl;
	}
	void CopyToNum2() const
	{
		num2=num1;
	}
};

int main(void)
{
	SoSimple sm(1, 2);
	sm.ShowSimpleData();
	sm.CopyToNum2();
	sm.ShowSimpleData();
	return 0;
}
1, 2
1, 1

위의 예제를 살펴보면,

mutable int num2; 

이는 mutable 키워드를 씀으로써 const 함수에 대해 초기화가 가능하도록 예외를 둘 수 있다.

void CopyToNum2() const
{
	num2=num1;
}

이렇게 const 함수 내에서 mutable로 선언된 num2의 값을 변경시키는 것을 볼 수 있다.

이렇게 const 함수 내에서 값을 변경시키는 유용한 키워드일 수 있으나, mutable의 과도한 사용은 C++에 있어서 그 중요성을 인정받은 키워드인 const의 선언을 의미 없에 만들어버리므로, mutable의 사용은 가급적 자제하는 것이 좋겠다.

profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글