read

열네번째 열혈강의


MAC

꾀 많은 시간이 지났다. 학교에서 시험도 쳤고, 이 외의 맨토역할도 진행이 되었고, 이전부터 하던 프로젝트도 계속 하고있다. 오랜만에 C++을 다시 하게되었는 데, 그동안에 맥북도 사게되어 지금은 처음으로 맥북으로 마크다운 문서를 적어보고 있다.

아마 비슷하겠지만 이전과는 조금 다른 문서 형태가 될지도 모르겠다.(조금 더 문법적으로 잘 맞춰서 적어보도록 하겠다.)

그럼 이전에 하던 것에 이어서 static 부터 시작해보자.


static


C언어에서부터 우리는 static을 사용해 왔다. 이는 C++에서 고스란히 이어진다. 그러나 C++에서는 멤버변수와 멤버함수에 static 선언을 추가할 수 있게 되었다. 이것의 의미부터 차근히 진행 해보도록 하자.


C 에서의 static


한 번 우리가 얼마나 알고 있는 지 정리를 해보자.

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

그럼 이제 함수 내 선언된 static에 대해 예시를 보도록 하자.

#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;
}

이에 대한 실행 결과는 매우 간단히 1부터 10까지 출력된다. 더이상의 설명은 필요 없다고 느낀다.


전역변수가 필요한 상황


C++ 에서의 static이 지니는 의미를 보기전에, 또 예제를 한번 보자. 이 예제는 객체가 생성될 때마다 메세지를 출력하는 클래스다.

#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;
}

여기서 이제 갯수가 나올 것이라는 걸 유추할 수 있을 것이다.

출력은 당연히 SoSimple 2번째 까지와 SoComplex 3번째 까지 출력이 된다. 즉, 전역변수를 볼 때, simObjCnt는 SoSimple 객체들이 공유하는 변수이다.

이처럼 cmxObjCnt는 SoComplex 객체들이 공유하는 변수이다.

그러나 문제점은 둘다 전역으로 선언헀으므로 이들을 지켜 줄만 한 것들이 존재하지 않는다.(너무 쉽게 접근이 된다.)

그럼 쉽게 생각할 수 있을 것이다. 각자, 만들어 주면 된다.


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


static 멤버변수는 클래스 변수 라고도 한다. 일반적인 멤버변수와는 달리 클래스당 하나씩만 생성되기 때문이다. 그럼 예시를 한번 보도록 하자.

 class SoSimple{
  private:
    static int simObjCnt;
  public:
    SoSimple(){
      simObjCnt++;
      cout << cmxObjCnt << "번째 SoComplex 객체" << endl;
    }
 };
 int SoSimple::simObjCnt = 0;

위 코드에 선언된 static 변수 simObjCnt 는 SoSimple 객체가 생성될 때마다 함께 생성되어 객체 별로 유지 하지 않는다. 객체가 생성되건 말건, 메모리 공간에 딱 하나만 할당이 되어서 공유한다. 다음을 보자

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

위와 같을 때 이 3 객체는 하나의 simObjCnt를 공유하는 형태다.

따라서 각 객체가 마치 멤버변수에 접근하듯 접근이 가능하다. 그러나 객체 내에 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 cmx1;
  SoComplex cmx2 = cmx1;
  SoComplex();
  return 0;
}

위 예제에서 보면 static 변수의 초기화 방법이 나오는데 이는 잠시 후 설명하겠다.

이렇듯 마치 멤버변수에 대한 접근을 하듯이 static 변수에 접근하고 있고, 사용하고 있음을 알 수 있다. 또한, 다른 객체에서의 접근을 허용하지 않게 되어 있기 때문에, 위험성도 떨어진다.

그럼 static 변수를 생성자에서 초기화하면 안되는 이유 를 알아보자. 만약 SoSimple의 생성자를 다음과 같이 정의하면 변수 simObjCnt는 객체가 생성될 때마다 초기화가 된다.

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

그 이유는 알다시피 객체가 생성될 때 동시에 생성되는 변수가 아니라 이미 메모리 공간에 할당이 이뤄진 변수이기 때문이다. 따라서 static 멤버변수의 초기화는 따로 정의되어 있다.

int SoSimple::simObjCnt = 0;

이는 SoSimple 클래스의 static 멤버변수 simObjCnt가 메모리 공간에 저장될 때 0으로 초기화 하라 는 뜻이다.


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


MAC

사실 static 멤버변수는 어디서든 접근 가능한 변수다. 앞서 언급한 예제처럼 static 멤버가 private로 선언되면 해당 클래스의 객체들만 접근이 가능하지만, public으로 선언되면 클래스의 이름 또는 객체의 이름을 통해 어디서든 접근이 된다.

그럼 다음 예제를 한 번 보자. 이 예제는 static 멤버변수가 객체 내에 존재하지 않는다 는 사실도 증명한다.

#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 << sim1.simObjCnt << "번째 SoSimple 객체" << endl;
  return 0;
}

이처럼 위에 main에서의 출력이 어떻게 되는 지 보면, 3문장에서 동일 출력이 나온다. 그런데 이중 sim1과 sim2의 출력에서는 마치 멤버변수를 접근하듯 오해를 불러일으킨다. 따라서 public static에 대한 접근은 첫번째 문장처럼 클래스명으로 접근 하는게 좋다.


static 멤버함수


코드가 많아지고 길이가 길어지다보니 포스팅의 길이도 점점 늘어가는게 보인다.

그럼 계속해서 static 멤버함수를 알아보자. 역시나 그 특성이 static 멤버변수와 동일하다. 따라서 다음 특성이 그대로 적용된다.

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

여기서 특히 주목할 것은 객체의 멤버로 존재하지 않는다 는 것이다.

class SoSimple{
  private:
    int num1;
    static int num2;
  public:
    SoSimple(int n): num1(n){

    }
    static void Adder(int n){
        num1 += n;
        num2 += n;
    }
};
int SoSimple::num2 = 0;

이 예제는 컴파일 에러 가 일어난다. static 멤버함수인 Adder에서 멤버변수인 num1에 접근하는 것이 왜? 잘못된 것인지는 논리적으로도 이해가 가능하다.

  • 객체의 멤버가 아닌데, 어떻게 멤버변수에 접근을 하겠는가?
  • 객체생성 이전에도 호출이 가능하다. 그런데 어떻게 멤버변수에 접근이 가능하곘는가?
  • 멤버변수에 접근을 한다고 치자, 그렇다면 어떤 객체의 멤버변수에 접근을 해야겠는가?

즉, 위 내용들을 다시 말해보면

static 멤버함수 내에서는 static 멤버변수와 static 멤버함수만 호출이 가능하다.

그리고 이런 특성을 지닌 static 멤버변수와 static 멤버함수를 잘 활용하면 대부분의 전역변수와 전역함수를 대체할 수 있다.


const static 멤버


예전에 보았듯이, 클래스 내에 선언된 const 멤버변수(상수)의 초기화는 이니셜라이저를 통해야만 한다. (도서 상으로 chapter 4 내용이다.) 다시 한 번 해당 부분을 말하자면 const 변수는 선언과 동시에 초기화 해야 하기 때문에, 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" << endl;
  cout << "캐나다 면적 : " << CountryArea::CANADA << "km" << endl;
  cout << "중국 면적 : " << CountryArea::CHINA << "km" << endl;
  cout << "한국 면적 : " << CountryArea::SOUTH_KOREA << "km" << endl;
  return 0;
 }

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


키워드 mutable


앞서서 우리는 const(상수화)와 explicit(묵시적 변환 제어)를 공부하였다. 이 둘은 나름의 의미가 있고, 또 매우 유용한 키워드 들이다. 그런데 이번에 설명하는 mutable이라는 키워드는 매우 빈도가 낮고, 심지어 가급적 빈도를 낮추어야 하는 키워드 이다.

먼저 이 의미부터 알아보자.

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

이어서 에제를 함께 보자.

#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

이 나온다. 여기서 CopyToNum2 함수와 mutable로 선언된 변수 num2를 보면서 다음처럼 말 할지도 모른다.

“num2가 mutable로 선언되고, CopyToNum2 함수가 const로 선언되었으니, 이 함수 내에서 실수로 num1의 값이 변경되진 않곘구나!”

즉, num1 = num2 와 같이 위 예제와 반대인 상황을 방지한다는 측면에서 좋게 말할 수도 있는데, 이것은 너무 확대 해석한 것이라고 본다. 만약 그렇다면, 아마 C++ 에서 거의 모든 함수는 const로 선언되고, 멤버변수중 상당수는 mutable이 선언되어야 할 것이다.

즉 반드시 예외적이고 제한적인 상황에서 사용해야 하는 키워드라고 할 수 있고, 이게 허용되는 순간 상당히 중요하게 여겨지는 const는 무용지물이 되어 버린다.


이렇게 마무리를 짓고 다음 포스팅에는 OOP 단계별 프로젝트 4단계와 다음 Chapter 7 내용인 상속으로 넘어가 보도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview