Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

SYDev

C++ Chapter 06-3 : C++에서의 static 본문

Programming Lang/C++

C++ Chapter 06-3 : C++에서의 static

시데브 2023. 7. 17. 20:09

C언어에서의 static

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

 이 중에서 함수 내에 선언된 static과 관련된 예제를 살펴보자.

#include <iostream>
using namespace std;

void Counter()
{
    static int cnt; // static 변수는 전역변수와 마찬가지로 초기화하지 않을 시에 0으로 초기화된다.
    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 변수의 선언문은 딱 한 번만 실행되며, 함수가 호출될 때마다 새롭게 할당되는 지역변수가 아니다.

 

전역변수가 필요한 상황

 C++에서의 static을 설명하기에 앞서, 전역변수가 필요한 상황의 예시를 한 번 살펴보자.

#include <iostream>
using namespace std;

int simObjCnt = 0; // SoSimple 객체 수를 세기 위한 전역변수
int cmxObjCnt = 0; // SoComplex 객체 수를 세기 위한 전역변수

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 클래스를 위한 전역변수이다.

 

 해당 변수들은 각각 SoSimple, SoComplex의 객체들끼리 공유하는 변수이다. 그러나, 전역변수로 선언하면 클래스 객체끼리만 사용하도록 하는 제한을 걸지 못한다. 이런 문제를 해결해주는 것이 바로 static 멤버변수이다.

 

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

 static 멤버변수는 일반적인 멤버변수들과 달리 클래스 당 한 번만 선언되기 때문에 클래스 변수라고도 불린다.

 

 위 simObjCnt를 클래스 SoSimple의 static 멤버변수로 선언한 간단한 형태는 다음과 같다.

class SoSimple
{
private:
	static int simObjCnt;		// static 멤버변수, 클래스 변수
public:
    SoSimple()
    {
        simObjCnt++;
        cout<<simObjCnt<<"번째 SoSimple 객체"<<endl;
    }
};
int SoSimple::simObjCnt = 0;	// static 멤버변수의 초기화(이후 설명)

  위 예제에서 선언된 static 변수 simObjCnt는 객체가 생성될 때마다 함께 생성되어 객체 별로 유지되는 변수가 아니다. 메모리 공간에 딱 하나만 할당이 되어서 공유되는 변수이다. 예를 들어 클래스 SoSimple에 sim1, sim2, sim3 이라는 객체가 존재하면 해당 객체들은 변수 simObjCnt를 공유하는 구조가 된다.

 

각 객체의 멤버함수에서는 클래스 변수에 다른 멤버 변수 접근하듯이 접근할 수 있다. 하지만 그렇다고 객체 내에 해당 클래스 변수가 존재하는 것은 아니고, 객체 외부에 존재한다.클래스 변수는 생성 및 소멸 시기도 전역변수와 동일하다.

 

 그렇다면 이제 앞서 보인 예제를 안정적으로 재구현해보자.    

#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 com1;
    SoComplex com2 = com1;
    SoComplex();

    return 0;
}
1번째 SoSimple 객체
2번째 SoSimple 객체
1번째 SoComplex 객체
2번째 SoComplex 객체
3번째 SoComplex 객체

 위 예제에서는 static 변수를 생성자가 아닌 클래스 외부에서 초기화하고 있다. 그렇다면 그 이유는 무엇일까? 예를 들어서 SoSimple의 생성자 정의가 다음과 같다고 가정해보자.

SoSimple()
{
    simObjCnt = 0;
    simObjCnt++;
    cout<<simObjCnt<<"번째 SoSimple 객체"<<endl;
}

  그렇다면 변수 simObjCnt는 객체가 생성될 때마다 0으로 초기화될 것이다. 왜냐하면 변수 simObjCnt는 객체에 따라서 새로 생성되는 변수가 아닌, 이미 메모리 공간 할당이 이뤄진 변수이기 때문이다. 따라서 static 멤버변수 초기화 문법은 다음과 같은 방법으로 클래스 외부에서 진행한다.

int SoSimple::simObjCnt = 0;

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

 사실 static 멤버 변수는 private이 아닌 public으로 선언되면 클래스의 이름 또는 객체의 이름을 통해서 어디서든 접근이 가능하다.

 

 이와 관련하여 예제를 살펴보자. 

#include <iostream>
using namespace std;

class SoSimple
{
public:
    static int simObjCnt;
public: // 불필요하지만 변수와 함수의 구분 목적으로 삽입하기도 함
    SoSimple()
    {
        simObjCnt++;
    }
};
int SoSimple::simObjCnt = 0;

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 객체

 실행 결과에서 알 수 있듯이 main 함수의 return을 제외한 마지막 3문장은 같은 동일한 변수에 접근하고 있기 때문에 같은 출력값을 보인다. 또한, sim1.simObjCnt와 같은 문장은 객체의 멤버변수에 접근하는 듯한 오해를 불러일으킬 수 있기 때문에 가능한 클래스 이름으로 static 멤버변수에 접근하는 것이 좋다.

 

static 멤버함수

 static 멤버함수는 앞서 설명한 static 멤버변수와 특성이 다음과 같이 동일하다.

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

 여기서 우리는 마지막 특징에 주목할 필요가 있다. 객체의 멤버로 존재하지 않는 함수이기 때문에 객체의 멤버변수에도 접근하지 못한다. 따라서 static 멤버함수의 정의에는 static 멤버변수, static 멤버함수만 호출될 수 있다. 

 

 이렇게 정의된 static 멤버변수, static 멤버함수를 잘만 이용하면 거의 모든 경우에서 전역변수, 전역함수를 대체할 수 있다.

 

const static 멤버

 chapter 4에서 우리는 클래스 내부에 선언된 const 멤버변수의 초기화는 이니셜라이저를 통해야만 가능하다는 것을 배웠다. 그러나, const static으로 선언되는 멤버변수는 선언과 동시에 초기화가 가능하다.

 

 이와 관련된 예제를 살펴보자.

#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

 

키워드 mutable

 키워드 mutableconst 함수 내에서 값의 변경을 예외적으로 허용한다는 의미를 지니고 있다.

 

 이와 관련된 다음 예제를 살펴보자.

#include <iostream>
using namespace std;

class SoSimple
{
private:
    int num1;
    mutable int num2;
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

 위에서 num2는 mutable로 선언되었기 때문에 const 함수 내부에서도 값의 변경이 가능해진다.

 

 그러나 mutable 역시 예외를 허용하는 키워드이므로(const의 선언을 의미 없게 만들어버림) 제한적으로, 매우 예외적인 상황에서만 사용하는 것이 좋다.

 


출처 : 윤성우, <윤성우의 열혈 C++ 프로그래밍>, 오렌지미디어, 2010.05.12