7 minute read

🏫Chapters 8 : Class

📖Classes

  • 변수에는 local과 global이 있다.
  • 그러나 global var은 두가지 측면에서 나쁘다.
    하나는 메모리를 프로그램 시작부터 종료까지 붙잡고 있기 때문이고,
    두번째는 global var은 모든 곳에서 접근이 가능하기 때문에 값에 대한 안정성이 떨어진다.
  • global이 가지는 장점은 일종의 쉐어되는 메모리라는 것이다. 파라미터와 리턴을 굳이 통하지 않고 쉐어되는 메모리가 있다는 측면은 편리한 것이 확실하다.

  • 변수와 달리, function을 생각해보면 scope 면에서 글로벌 func밖에는 없다. function은 변경의 대상은 아니지만 변경의 수단이 될 수 있다.
    그렇기 때문에 모든 function이 모든 곳에서 공개되어 있다는 것은 간접적으로 변수에 대한 상태변경을 일으킬 수 있는 위험이 있기 때문에 안정성이 역시나 떨어진다.
    함수와 변수 사이에 일종의 벽이 필요하다. 서로간의 접근이 필요한 애들끼리 모아두고 서로에 대해 칸막이를 두자는 개념이다.(은닉)

    이 칸막이, 벽의 이름이 클래스이다.

  • 기본적으로 데이터간의 쉐어 및 접근, 함수간의 호출이 필요하다는 것은 서로 무언가의 연결점이 있기 때문이다. 남남이 이메일 번호를 공유할 필요 없듯이, 서로 데이터를 공유할 필요가 있다는 것은 둘 사이의 관계성이 존재한다는 것이다. 연결점이 있는 변수와 함수들끼리 묶고, 그 외에 관계성이 없는 것들과는 모르는 것이 낫다는 것이다. 얘는 왜 비밀번호를 1234를 쓸까. 어느날 로그인 했을때 되고, 이메일은,,? 이러면서 무언가의 실수를 일으킬 가능성을 소지한다. 따라서 차라리 접근할 필요가 없는 것들끼리는 아예 모르는 것이 약이라는 것이다.

  • 우리가 원하는 바는 글로벌 변수와 함수를 C처럼 다 보이게 하지 말고 클래스로 묶고, 각각의 클래스는 서로 볼 수 없게 만드는 것이 1차 원칙이다.

이것을 Encapsulization 캡슐화 라고 한다

  • 이러한 특성을 통해 안정성을 높일 수 있다.

  • 이때, 서로 거들떠도 보지 않을 거면 왜 굳이 같은 프로그램에 있어야 할까?
    넓게 보면 같은 프로그램에 있는 클래스 사이에 무언가의 연결점이 있기 때문일 것이다.
    즉, 역설적으로 클래스는 기본적인 상호연관성을 열 필요가 있다.
    따라서 최소한의 공개 원칙이 있다.

  • 최소한의 공개 원칙은 연결점이 있는 것에 한해 최소한만 외부에서 볼 수 있도록 해준다.

    public, private(default)


📖Members and member access

  • Ex
        class X {
            public:
            int m; // data member
            int mf(int v) { int old = m; m=v; return old; } // function member
        };

        X var; // var is a variable of type X 
        var.m = 7; // access var’s data member m
        int x = var.mf(9);// call var’s member function mf()
  • public으로 설정되어 있기 때문에
  • 클래스 외부에서 .을 통해 멤버들을 부를 수 있다.
  • x는 하나의 인스턴스인 것이고 각각의 인스턴스는 별도로 멤버변수를 갖는다.
        #include<iostream>
        using namespace std;

        class Sample{
            int m;

            public: 
                int n;
        };

        int main(void){
            Sample x, y;
            x.n = 1;
            y.n = 10;
            x.m = 2;        // m은 private이기 때문에 접근할 수 없어 컴파일에러 발생한다

            cout << "x.n = " << x.n;
            cout << "y.n = " << y.n;
        }
  • 각각의 인스턴스의 프로퍼티는 따로 메모리에 할당된다. 즉, x.n != y.n
  • private m을 사용하고 싶다면 액세스함수를 만든다.

        class Sample{
            int m;
    
            public: 
                int n;
                void setM(int x){
                    m = x;
                }
                int getM(){
                    return m;
                }
        };
    
    • 위의 멤버함수는 public으로 선언되었기 때문에 외부에서 접근이 가능하다. 이러한 액세스함수를 통해 private한 멤버변수에 접근할 수 있다.
    • 더 권장되는 방식은 변수는 private으로 선언하고 함수를 통해 접근하는 방식이다.
      한 번 더 검수하는 과정을 거치는 셈이므로 감시할 수 있는 수단이 되고, 아래와 같이 안정성을 높일 수 있다.
        class Error{};
        class Sample{
            int m;  // default = private
    
            public: 
                int n;
                void setM(int x){
                    if(x < 0) throw Error();
                    m = x;
                }
                int getM(){
                    return m;
                }
        };
    


📖Constructor

  • Constructor 생성자
    • 이름은 반드시 클래스의 이름과 동일해야한다.
    • 클래스를 최초로 정의될 때 자동으로 호출되는 녀석이다. 즉, 객체 생성 시 따로 명시적으로 호출하지 않아도 자동으로 호출된다.

    • 이 때, 다형성을 생각해보자. 생성자가 하나만 있으리라는 법은 없다.
          class Sample{
              int m;  // default = private
              int n;
    
              public: 
                  void Sample(){          // default 생성자
                      cout << "Sample() is called" << endl;
                  }
    
                  void Sample(int i){     // 다형성. 인자에 의해 알아서 생성자가 호출된다
                      cout << "Sample(int i) is called" << endl;
                  }
    
                  void setM(int x){
                      if(x < 0) throw Error();
                      m = x;
                  }
    
                  int getM(){
                      return m;
                  }
          };
    
          int main(void){
              Sample x, y;
              Sample z(100);
    
          }
    
          /*
          결과
          Sample() is called
          Sample() is called
          Sample(int i) is called
          */
    
    • 이 때 생성자를 만들지 않았을 경우, 컴파일러가 알아서 디폴트 생성자를 만든다.
      그리고, 이 생성자는 텅 비어있기 때문에 여기에 인자를 전달하면 에러가 난다. 디폴트 생성자에는 파라미터를 받지 않기 때문이다.


📖Classes

Ex

```cpp
    // simple Date (control access) 
    class Date {
        int y,m,d; // year, month, day
        
        public:
            Date(int y, int m, int d); // constructor: check for valid date and initialize
            // access functions:
            void add_day(int n); // increase the Date by n days
            int month() { return m; } 
            int day() { return d; }
            int year() { return y; }
    };

    // …
    Date my_birthday {1950, 12, 30}; // ok
    cout << my_birthday.month() << endl; // we can read 
    my_birthday.m = 14; // error: Date::m is private
```  

+ 멤버함수는 각각의 클래스가 공유한다. 값이 바뀌거나 그러지 않기 때문에 함수의 메모리 주소를 모두 공유한다.  
  그러나 변수는 각각의 객체마다 값이 달라질 수 있기 때문에 따로 저장한다.  
  따라서 메모리에서 함수는 고려되지 않고 변수만 고려한다. 함수의 메모리는 멤버변수만큼 크기를 갖는다.

파일 분리

```cpp
    class Sample{
        int m;  // default = private
        int n;

        public: 
            void Sample();
            void Sample(int i);
            void setM(int x);
            int getM();
    };

    void Sample::Sample(){           // Sample 클래스의 생성자를 정의하는 것을 알려줌
        cout << "Sample() is called" << endl;
    }

    void Sample::Sample(int i){   
        cout << "Sample(int i) is called" << endl;
    }

    void Sample::setM(int x){       // Sample 클래스의 멤버함수임을 알려줌
        if(x < 0) throw Error();
            m = x;
    }

    int Sample::getM(){
        return m;
    }
```

+ 이 방법이 권장사항이다
+ C는 멤버함수에 대한 선언만 클래스 안에 명시하고 정의는 그 밖에 명시하는 것이 권장사항인데, 그 이유는 C가 오픈소스의 문화를 모를 때 만들어졌기 때문이다.  
  남들에게 재산과 마찬가지인 멤버함수를 어떻게 구현했는지 쉽게 보여주지 않고, 그 결과를 low-level 언어로 보여주겠다는 것이다.  
  즉, 보여주고 싶은 선언과 보여주고 싶지 않은 정의를 분리하는 습관이 깃들어져 있다. 따라서 선언은 `.h`로 정의는 `.cpp`로 분리한다.

```cpp
    // Sample.cpp
    #include"Sample.h"
    
    void Sample::Sample(){         
        cout << "Sample() is called" << endl;
    }

    void Sample::Sample(int i){   
        cout << "Sample(int i) is called" << endl;
    }

    void Sample::setM(int x){      
        if(x < 0) throw Error();
            m = x;
    }

    int Sample::getM(){
        return m;
    }

    // Sample.h 선언만 있는 헤더파일만 공개한다
    class Sample{
        private:
            int m; 
            int n;

        public: 
            void Sample();
            void Sample(int i);
            void setM(int x);
            int getM();
    };
``` 


private, public

  • private에는 멤버변수를 넣어놓는다.
  • public에는 멤버변수에 접근할 수 있는 액세스함수를 만들어 놓는 것을 권장한다.


📖Destructor 소멸자

  • ~클래스이름(); –> ~Sample();
  • 메모리에서 사라질 때 호출됨
    Sample::~Sample(){  
        cout << m << " destructor is called!" << endl;
    }
  • 직접적으로 함수를 호출하는 것이 아니기 때문에 파라미터를 결정할 수 없음
    class Sample{
        private:
            int m; 
            int n;

        public: 
            void Sample();
            void Sample(int i);
            void setM(int x);
            int getM();
            void ~Sample();
    };

    int main(void){
        Sample x;
        Sample *w;
        Sample y2(x);
        w = new Sample();       // 포인터, 동적할당 받음, 힙에 저장

        {
            delete w;           // java는 문법적으로 소멸자가 없지만, C++은 delete가 있으므로 소멸자가 있다.
            
            /* java는 가비지컬렉터가 알아서 메모리를 해제해주고 관리하기 때문.
               프로그래머가 메모리를 직접 할당하고 해제하는 것을 허락해줄 경우, 
               메모리 릭leak이 잘 발생하기 때문에 자바는 프로그래머에게 결정권을 주지 않는다. 
               따라서 new로 heap에 할당한 메모리는 delete가 명시되면 소멸자를 호출하며 메모리 해제.
               stack에 할당된 메모리는 그 변수를 감싼 중괄호가 끝날 때, 소멸자를 호출하며 메모리 해제. 
            */

            // 포인터 변수 선언에 대해서는 생성자가 호출되지 않는다. 포인터는 단순히 주소를 저장하는 변수이기 때문.

            Sample y;
            x.setM(2);
            y.setM(20);

            {
                Sample z
                cout<<"x.m=" << x.getM() << endl;
                cout<<"y.m=" << x.getM() << endl;
                cout<<"z.m=" << x.getM() << endl;
                cout<<"y2.m=" << x.getM() << endl;
                cout<<"z.m=" << z.sizeof(() << endl;
                }
        }
    }

    /*result
    x constructor is called!
    w constructor is called!
    w destructor is called!
    y constructor is called!
    z constructor is called!
    z destructor is called!
    y destructor is called!
    z destructor is called!
    */
  • w는 원래 포인터 변수. w는 로컬변수이기 때문에 자신을 감싸고 있는 가까운 중괄호를 만나면 사라짐. 그러나 포인터변수가 가리키는 데이터가 저장되어있는 곳은 힙이다. 따라서 delete가 없으면 포인터변수는 중괄호를 만나면 사라지게 될 때, 그 포인터 변수가 가리키는 힙에 할당되어 있는 데이터는 그대로 남아있게 되고, 그러나 힙에 존재하는 그 메모리를 가리킬 변수가 없음으로써 가리킬 방법이 사라지고, 힙에 할당된 데이터가 불릴 수도 없고 사용될 수도 없고 할당만 되어있는 이것이 메모리 릭 leak이다.

  • 자기의 멤버 변수 중 포인터가 있는 경우, 소멸자에서 delete 해줘야 한다.

  • 위와 같은 마무리가 C++에서는 책임이 프로그래머에게 있으므로 뒤처리를 확실히 해주어야한다.


📖Copy Constructor 복사생성자

  • 나 자신을 파라미터로 받는 것은 디폴트로 자동 생성해줌
    Sample::Sample(Sample &s){
        m = s.m + 1000;
    }
  • 클래스 내부이기 때문에 private에 직접 접근 가능

  • Sample y2 = y1; Assignment
    • 모든 사용자정의에 대해 연산자 오버로딩을 자동으로 해줌. 멤버변수를 복사해준다.
    • 최초로 정의될 때 한 번 호출되는 복사생성자와 달리 시도때도 없이 불릴 수 있다.
  • 얕은 복사/ 깊은 복사


📖Initializer

  • 선언과 동시에 초기화하는 방법으로 속도가 좀 더 빠르다. 성능이 좀 더 좋아진다.


📖this

    Sample::Sample(int m){
        m = m;           // 둘 다 로컬변수 m을 가리키게 된다
        this->m = m;     // this는 자신을 가리키는 포인터

        // 로컬에 대한 멤벼변수는 . 스택, 포인터 변수는 -> 힙
    }
  • 자바에서는 primitive타입, 클래스 오브젝트는 무조건 힙에 저장하기 때문에 new를 써주어야 한다. 그래서 자바는 다른 언어에 비해 상대적으로 힙 메모리 관리가 굉장히 중요하다. 선택권이 없어 무조건 힙으로 보내기 때문에 .->를 구분할 필요가 없다. 굳이 구별하지 않고 모두 점으로 쓴다. primitive 타입을 억지로 힙에 보내기 위해서 감싸고 있는 Wrapper클래스를 만들어 사용한다.

  • this는 포인터이기 때문에 힙이라고 생각.


📖Const

    Sample::Sample(const Sample &a){
        m = a.getM() + 100;        // 에러
    }

    int getM() const {              // 여기에도 const 선언을 해주어야한다.
        return m;
    }
  • 위 코드에서 컴파일 에러가 뜨는 이유는 a에 const 선언이 되어 있으므로 a의 값이 바뀔 수 없는 상황에서,
    호출된 getM에서 a의 값이 바뀔 수도 있다고 컴파일러가 판단하기 때문이다. 간접적 변화도 차단하는 셈이다.
    따라서 getM에도 const 선언을 해주어야한다.

  • 즉, const로 선언된 함수만 호출할 수 있다.

What makes a good interface?

  • 귀차니즘이 발동하면 public으로 많이 열게 되는데, 이는 바람직하지 않다.
    필요한 최소한만큼만 public으로 열어야 한다.

  • 타입의 문제나 const의 정확성을 유지해야한다.

  • 자동으로 만들어주는 함수 몇가지가 있는데, 생성자, 소멸자, 복사생성자, copy assignment