C++문법(자료형) - 5. Class

Ui Jin·2021년 9월 28일
0

C++ Grammar

목록 보기
11/13

객체지향 프로그래밍

객체지향 프로그래밍이란 무엇일까요?

제가 지금 공부하고 있는 한빛미디어의 뇌를 자극하는 C++에서는 "객체를 기본단위로 해서 만들기"라고 소개하고 있습니다. 쉽게 말하면, 음... 예를들어 건물을 지을 때 현대 사회에서 망치와 못 이 두 도구만을 가지고 집을 짓는 사람이 있을까요? 네, 특별한 경우를 제외하고는 뭐 거의 없다고 봐야겠죠. 여기서 이러한 도구들을 '변수', 그리고 이 도구를 다루는 기술을 '함수'라고 생각하면 되겠습니다. 반면에 이 도구들을 이용해서 우리가 원하는 포크레인(굴삭기) 같은 건설장비들을 만든 것을 '클래스', '객체' 라고 생각해 봅시다.

즉, 이러한 장비들을 이용해서 프로그래밍을 하자는것이 "객체지향 프로그래밍"이라고 생각합시다.

그렇다면, 클래스와 객체가 다른것은 무었일까요?

우선 우리가 이 포크레인(굴삭기)를 사용하기 위해서는 이것을 만들어야겠죠, 이 기계들을 만들기 위해서는 설계도면이 필요할 것입니다. 이 설계도면을 바로 클래스라고 하고, 이 설계도면을 통해 만든 "실제 제품", "실제 예시"가 객체가 되는 것입니다.

객체지향 프로그래밍의 장점

  1. 캡슐화: 위에서 설명한 것처럼 어떤 기능을 하게하는 데이터 및 함수를 하나의 단위로 묶을 수 있게 됩니다.

  2. 정보은닉: 위에서 클래스로 만든 객체는 그 내부의 데이터 및 함수에게 접근권한을 줄 수 있는 기능이 있습니다.
    (자세한 내용은 후에 알아봅시다.)

  3. 상속: 이미 만들어진 클래스를 이용해 새로운 클래스를 만들 수 있는, 즉 재사용할 수 있는 방법이 있습니다.
    (자세한 내용은 후에 알아봅시다.)

  4. 다형성: Overloading과 Overriding을 통해 보이는 모습은 하나지만 실질적으로는 여러 기능을 수행할 수 있게 됩니다.


사용자 지정 자료형

위에서 설명한 클래스를 다르게 생각해 보면, 우리가 자료형을 새로 만들었다고 생각해 볼 수 있습니다. 따라서 클래스를 "사용자 지정 자료형"이라고도 합니다.

자 그럼 이제 우리가 만들 기계에 대한 설계도면, 즉 클래스를 만드는 방법에 대해 알아봅시다.


Class, 클래스 - 정의

1) 기본적인 틀

class Excavators
{

};
  • 위와 같이 class키워드 뒤에 그 클래스의 이름을 넣어주어 정의합니다.

  • 이 틀 안에 아래의 구성요소들을 넣어 정의해주게 됩니다.

2) 구성요소

2-1) 멤버 변수

멤버 변수는 클래스에 속해있는 변수를 의미합니다.

class Excavators
{
    int a;		// 일반변수
    point b;	// 멤버 변수
}
  • 변수, 구조체, 객체등 모든 변수를 멤버로 포함시킬 수 있습니다.

2-2) 멤버 함수

기본적인 방법

class Excavators
{
    int a;
    point b;

    void print() {
        cout<< a << b << endl;
    }
}
  • 일반 함수와 같이 클래스의 안에 정의하면 되겠습니다.

우리가 일반 함수를 작성할 때 그 함수의 길이가 길어지고 함수의 양이 많아졌을 때 가독성을 위해서 어떻게 했었죠?

네, 멤버함수도 Prototype으로 먼저 선언한 후에 뒤에서 정의하는 것이 가능합니다.

prototype을 이용하는 방법

class Excavators
{
    int a;
    point b;

    void print();
}

void Excavators::print() {
    cout << a << b << endl;
}
  • 중간에 보이는 ::는 범위 지정 연산자로, Point 클래스에 속하고 있는 print()함수임을 알려주고 있는 것입니다.

    (만약 void print()와 같이 그냥 적어주면 일반 함수를 정의하는 것으로 인식하겠죠?)

(참고)
(뒤쪽에 보시면 이 둘의 차이점이 있다는 것을 알 수 있답니다)


특별한 멤버함수

  • 생성자

    생성자는 객체를 생성할 때 가장 먼저, 단 한번 호출 되는 함수를 의미합니다.

    객체를 생성할 때 가장 먼저 호출 된다는 것을 곰곰히 생각해 봅시다. 생성자는 어떤 역할을 하게 될까요? 객체를 생성할 때 해주어야 할 일들을 정의해 주면 되겠죠?

    우선 생성자의 형태는 반환값: 없음, 이름: 클래스 이름으로 정해야 한다고 규칙이 정해져 있습니다.

    이를 이용하면 다음과 같은 생성자를 만들 수 있습니다.

    • 일반 생성자: Excavators(int x, int y);:

      class Excavators
      {
        int a;
        point b;
      
        Excavators(int x, int y);
      };
      
      Excavators::Excavators(int x, int y) {
        a = x;
        b = y;
      }
    • 복사 생성자: Excavators(const Excavators& ref);

      class Excavators
      {
         int a;
         point b;
      
         Excavators(int x, int y);
         Excavators(const Excavators& ref);
      
         void print();
      }
      
      Excavators::Excavators(int x, int y) {
         a = x;
         b = y;
      }
      Excavators::Excavators(const Excavators& ref) {
         a = ref.a
         b = ref.b
      }
      void Excavators::print() {
         cout << a << b << endl;
      }

위와같이 인자의 수를 조절하거나 그 종류를 다르게 해서 여러개의 생성자를 만드는 것이 가능합니다. 즉, 생성자 또한 오버로딩이 가능합니다.

  • 소멸자

    소멸자는 반대로 객체가 소멸할때 자동으로 호출되는 함수입니다.

    객체가 소멸하기 전에 꼭 해주어야 하는 일에는 어떤것이 있을 까요? 바로 메모리를 정리하는 것입니다. 즉, 멤버 변수에서 동적 메모리 할당을 통해 변수를 선언해 주었다면 여기서 그 메모리를 해제해야 하는 것입니다.

    • 소멸자: ~Excavators();

      소멸자의 형태도 위와같이 정해져 있습니다. 즉, 클래스와 같은 이름을 갖고 ~(틸다)를 붙여서 표현합니다.

       class Excavators
      {
         int a;
         point b;
         double* ptr;
      
         Excavators(int x, int y);
         Excavators(const Excavators& ref);
         ~Excavators();
      
         void print();
      }
      
      Excavators::Excavators(int x, int y) {
         a = x;
         b = y;
         ptr = new double[10]
      }
      Excavators::~Excavators(); {
         delete[] ptr;
         ptr = NULL;
      }
      void Excavators::print() {
         cout << a << b << endl;
      }

2-3) 접근 제어

클래스에서 말하는 접근 제어란 클래스로 객체를 만들었을 때 어떤 멤버를 외부에 보이게 할 것인가를 지정하는 작업을 말합니다.

여기서 '외부'라는 단어를 잘 기억합시다. 즉, 클래스의 내부에서는 접근이 가능하다는 것입니다. 따라서 외부에 접근이 금지된 멤버라도, 외부에서 접근할 수 있는 멤버 함수를 통해서는 접근할 수 있다는 것입니다.

class에서 지원하는 접근 권한에는 3가지가 있습니다.

  • public

    public:키워드의 밑에 있는변수와 함수들은 외부에서도 접근이 가능 합니다.

    (이 공간에는 주로 함수들이 위치하게 됩니다.)

    • 생성자와 소멸자
      만약 생성자를 private:에 선언하게 된다면 외부 함수인 main()함수에서 해당 클래스에 대해 생성할 권한이 없게 됩니다. 따라서 생성자나 소멸자는 public:으로 설정해주도록 합시다.

      class Excavators
      {
          private:
              int part1, part2;
              double stick;
          public:
              Excavators();
              ~Excavators();
              void SetPart1(int v1) { part1 = v1; }
              void SetPart2(int v2) { part2 = v2; }
              int GetPart1() { return part1; }
              int GetPart2() { return part2; }
      };
    • 접근자
      만약 외부에서 멤버 변수에 접근하도록 만들어야 한다면 이 공간에 따로 접근하는 함수를 만들어 줍시다.
      (이것을 접근자 혹은 "Setter""getter" 라고 합니다.)

      class Excavators
      {
          private:
              int part1, part2;
              double stick;
          public:
              void SetPart1(int v1) { part1 = v1; }
              void SetPart2(int v2) { part2 = v2; }
              int GetPart1() { return part1; }
              int GetPart2() { return part2; }
      };

      (참고)
      만약 원한다면 Setter에서 멤버 변수에 넣을 수 있는 값을 제한하여 원하지 않는 값이 들어가는 것을 막을 수 있습니다.

  • private

    private: 키워드 밑에 있는 멤버는 클래스 외부에서는 접근이 불가능해집니다.(ex. main()함수나 다른 함수들에서)

    (이 공간에는 주로 멤버 변수가 들어갑니다.)

    • 멤버변수

      class Excavators
      {
          private:
              int part1, part2;
              double stick;
      }

      (참고)
      클래스가 잘 작동하도록 하기 위해서는 멤버 변수에 대한 컨트롤이 중요합니다. 따라서 외부에서는 조작할 수 없도록 지정해 줍시다.

  • protected

    protected: 키워드 밑의 멤버는 외부에서는 접근이 불가능해 집니다.

    단, 이 클래스를 상속받은 클래스에서의 접근은 가능합니다.

위에서 설명한 예시 외에는 필요에 따라 각 함수에 접근 권한을 설정 해 줍시다.

2-4) Static(정적멤버)

정적 멤버란 모든 객체가 공유하고 있는 멤버를 의미합니다. 즉 클래스로 객체를 만들었을 때 그 하나의 객체에만 속해있지 않고, 클래스 자체에 속해있는 멤버를 의미합니다.

따라서 정적 멤버에는 주로 클래스 자체에 관련된 정보같은 것을 넣어주게 됩니다.
(예를들면 지금까지 이 클래스로 생성한 객체의 수를 저장할 수 있겠죠...)

선언

보통의 변수 앞에 static키워드를 붙여서 선언할 수 있습니다.

static멤버는 객체에 속하는 요소가 아니기 때문에, 초기화를 할 때에는 클래스 외부에서 별도로 다루어 주어야 합니다.

class Excavators
{
public:
  static int Excavators_num;
  static void printNum();

  Excavators();
  ~Excavators();
  void SetPart1(int v1) { part1 = v1; }
  void SetPart2(int v2) { part2 = v2; }
  int GetPart1() { return part1; }
  int GetPart2() { return part2; }       

private:
  int part1, part2;
  double stick;
};

int Excavators::Excavators_num = 0; 
void Excavators::printNum(){ cout << num; }
int main() {
    Excavators::printNum();
}

즉, 위와같이 값을 넣어줄 때나, 아래와 같이 호출할 때, 어느 클래스에 속한 static멤버인지 명시해 주어야 합니다.

당연한 이야기 이겠지만, static멤버에서는 일반멤버에 접근할 수 없습니다. (public:이든 아니든 상관 없습니다.)

(static에서는 일반 멤버에 접근할 때 어떤 객체의 일반 멤버에 접근할 지 모르겠죠?)


Object, 객체 - 선언(생성)

일반 자료형과 같이 클래스 이름변수 이름을 통해 생성합니다. 아 혹시 아까 위에서 class는 설계도이고, 객체는 그 실제 제품이라고 했던거 기억 나시나요? 네 여기서 이렇게 class를 통해 변수를 선언해 주면 이 변수가 바로 객체가 되는것입니다.

선언

1) 객체 생성(stack)

생성자를 이용하지 않는 방법

Excavators num1;

생성자를 이용하지 않을 경우, 자동으로 디폴트 생성자가 호출됩니다.

디폴트 생성자는, 파라미터로 아무 값도 전달해주지 않는 생성자를 의미합니다.

( 정의할 때 디폴트 생성자를 정의해 주지 않았다고 하더라도 내장되어 있는, 아무 기능도 하지 않는 디폴트 생성자를 호출하게 됩니다.)


생성자를 이용해 생성하는 방법

Excavators num2(1, 2);

생성자를 이용해 객체를 생성할 경우, 객체를 만들 때 해야할 일들을 하면서 생성할 수 있습니다.

(생성자 안에서 객체의 초기화 리스트를 이용하면 초기화가 필요한 곳에 초기화 할 수 있습니다.)


복사하여 생성하는 방법

  • 복사 생성자가 없을 때

    Excavators num3 = num2

    이 경우는 (자동으로 생성된)복사생성자가 호출되어 num2의 멤버변수 전체가 1:1로 num3에 복사되어 생성되게 됩니다.

    (대입연산, num3 = num2와 같이 같은 클래스 변수의 객체를 대입 할 때에는 멤버 변수끼리의 복사가 일어납니다)

  • 복사 생성자를 이용할 때
    Excavators num3(num2)
    이 경우에는 Excavatorclass를 정의할 때 만들어 놓은 복사 생성자를 호출하여 생성하게 됩니다. 즉, 그 생성자 안에 다른 일들을 적어놓을 경우 멤버변수의 1:1복사 말고도 다른 기능을 하도록 만들어 줄 수 있습니다.

2) 배열 생성

클래스도 타입(자료형)의 한 종류이기 때문에 배열을 만들 수 있습니다.

Excavators arr1[3]

Excavators arr2[2] = {Excavators(1, 2), Excavators(4, 5)}
  • arr1의 경우
    클래스의 생성자를 입력해주지 않았기 때문에 디폴트 생성자가 자동으로 호출되게 됩니다.

  • arr2의 경우
    클래스의 생성자를 입력해주어 원하는 생성자를 호출 할 수 있게 됩니다.

3) 포인터 생성

객체를 가리키는 포인터 또한 생성이 가능합니다.

Excavators num1();

Excavators* ptr = num1;

(참고)
이때 이 포인터가 가리키는 곳은 해당 객체가 확보한 공간의 시작 주소일 것입니다.

4) 동적 생성

생성자를 통해 객체를 동적으로 생성하는 것 또한 가능합니다.

Excavators* ptr = new Excavators();

// Excavators* : 해당 객체를 가리킬 수 있는 포인터 
// new : 동적으로 생성
// Excavators() : 해당 클래스의 생성자

우리가 실제로 사용할 객체들은 보통 그 크기가 매우 큽니다. 따라서 이 방법을 주로 사용하게 된다고 합니다.

(참고)
동적생성: 해당 대상을 현재 process의 stack이 아닌 heap에 생성하는 것.


접근

객체 내부의 public 함수들에 접근하기 위한 방법에는 2가지가 있습니다.

1) .(dot)연산자

Excavators num1();

num1.setpart1();
  • 객체를 통해 멤버에 접근할 때 사용하는 방법입니다.

2) ->(arrow)연산자

Excavators* ptr = new Excavators(1, 2);

ptr -> setpart1();
  • 포인터를 통해 멤버에 접근할 때 사용하는 방법입니다.

(참고)
ptr: 포인터 변수에 담긴 주소값.

*ptr: 해당 주소에 담긴 변수

ptr->: 해당 주소에 담긴 객체의 멤버

3) this 포인터

객체가 생성될 때 this라고 하는 포인터도 자동으로 생성이 됩니다. 이 포인터는 그 객체의 시작 주소값을 가지고 있는 포인터 입니다. 이를 이용하면 클래스 안에서 어떤 함수나 변수를 부를 때 헷갈리지 않도록 명시할 수 있습니다.

이를 이용하면 다음의 문제점을 해결 할 수 있습니다.

class Excavators
{
    private:
        int part1, part2;
        double stick;
    public:
       void set(int part1) { part1 = part1}
}

이경우 part1이 멤버 변수의 것인지, 파라미터의 것인지 헷갈리게 됩니다. 따라서 다음과 같이 코드를 고쳐 봅시다.

class Excavators
{
    private:
        int part1, part2;
        double stick;
    public:
       void set(int part1) { this->part1 = part1}
}

또 후에 배우는 상속의 문제에 있어서도 부모클래스의 함수를 불러야 하는지 자식 클래스의 함수를 불러야 하는지에 대해 헷갈릴 수 있습니다. 이때도 이 방법을 사용하면 되겠습니다.


초기화

코드를 구현해 보시면 알겠지만, 객체의 멤버를 초기화 하는 곳은 생성자의 함수 내부가 아닙니다. 즉, 아래와 같은 코드는 초기화를 하는 코드가 아닙니다.

  Excavators::Excavators(int x, int y) {
     int a = x;
     int b = y;
}

위의 코드의 { }(중괄호)의 내부는 단지, 생성한 후에 바로 할 행동들을 적어놓은 곳이라고 보시면 되겠습니다. 그렇다면 객체를 초기화 할 때는 어떻게 해야 할까요?

생성자의 초기화 리스트

Excavators::Excavators(int x, int y) 
: a(x), b(y)
{
     cout << 'initiator~!' << endl;
}

위와같이 생성자를 구현하기 전에 :(콜론)으로 연결해 주고 멤버변수 (초기화값)의 형태로 ,(콤마)로 구분하여 넣어주어야 합니다.


즉, 위와 같이 생성자를 실행할 때, 초기화 리스트의 값으로 모든 멤버 변수들의 값을 초기화 한 후에 추가적으로 해야할 일들이 적힌 { }안의 내용을 실행하게 됩니다.


중요: 초기화리스트를 사용해야만 한 경우

1) const멤버 변수, 레퍼런스 멤버변수

const& (Reference)로 선언된 변수들은 항상 초기화를 해야했다는 것이 기억 나시나요?

이 멤버 변수들은 반드시 생성자의 초기화 리스트를 통해서 초기화 해줍시다.

2) 객체인 멤버변수
어떤 클래스의 멤버 변수가 객체인 경우가 있을 것입니다. 이때에도 이 초기화 리스트를 통해 생성해 주어야 합니다.

우선 초기화 리스트를 사용하지 않을경우를 살펴봅시다

//-----------------point class---------------------//
class point
{
public:
    point(_x, _y){
        x = _x;
        y = _y;
    }
private:
    int x, y;
};

// ----------------Excavators class------------------//
class Excavators
{
public:
    Excavators(int x, int y);
private:
    point abc;
};

Excavators::Excavators(int _x, int _y)
{
    abc(_x)
}

위와 같은 코드에서 객체는 생성되기 전에 모든 멤버 변수에 대한 공간을 확보하게 됩니다. 즉, 이때 어떤 객체(Excavators) 안에 객체인 멤버변수(point)가 있을 때, point abc생성 -> Excavators 객체생성의 순으로 생성되어야 합니다.

즉, 초기화 리스트를 사용하지 않고 { }안에서 대입해줄 경우,
우선 컴퓨터는 초기화 리스트에 point의 생성자가 없으므로 자동으로 디폴트 생성자를 부르게 됩니다. 그리고 나서 { }안에서 이미 생성된 객체(abc)에 abc(_x)라는 코드를 행하게 되어 오류를 일으킵니다.

이러한 경우 abc._x와 같은 코드로 하나씩 대입해 주는 방법도 있지만 이는 우리의 목표와는 다른 방향이므로 객체는 초기화 리스트를 통해 생성해 줍시다.

결론
1) 초기화 리스트
const, &(레퍼런스 변수), 객체인 멤버변수는 반드시 초기화리스트를 통해 초기화 해 줍시다.

2) 생성자의 호출 순서
멤버변수의 생성자 -> 해당 클래스의 생성자

3) 소멸자의 호출 순서
해당 클래스의 소멸자 -> 멤버 변수의 소멸자


상속

여기서는 상속에 대해서 배워 보겠습니다.
우선 상속과 포함이란 이미 만들어진 클래스를 재사용하는 방법을 의미합니다. 우리는 이 상속을 통해서 서로 포함관계에 있는 클래스를 만들 수 있는데, 이를 활용하면 많은 시간을 단축할 수 있다는 장점이 있습니다.

예를 들면 "건설 장비"라는 클래스가 있고 "타워크레인"과 "굴삭기"라는 클래스가 있다고 생각해 봅시다. 또 이 "건설 장비" 클래스에는 건설에 필요한 도구들이 담겨 있을 때 이를 재사용해서 "타워크레인" 이라는 클래스와 "굴삭기"라는 클래스를 만드는 방법을 우리는 배우게 될 것입니다.

기본조건

우선 상속을 진행하기 위한 클래스의 조건을 알아봅시다. 상속은 자식클래스와 부모 클래스가 IS - A 조건을 만족해야 합니다.

IS - A 조건이란?
포함관계가 되는 조건을 말합니다. 즉, AVANTE IS CAR와 같이 자식 클래스가 부모 클래스에 포함되는 관계를 말합니다.
(여기서 AVANTE는 자식클래스, CAR는 부모 클래스가 되겠죠)

3-1) 상속방법

class towercrain : public machine
{
public:
    towercrain();
    ~towercrain();
}
  • 위와 같이 class 클래스이름:상속형식 상속받을 클래스 형식으로 써 주면 됩니다.

3-2) 상속 형식

우선 접근범위의 크기는 위에서 본것과 같이 public > protected > private 순으로 크게 됩니다.

  • public 상속

    접근 범위가 public보다 큰것들은 public으로 상속받게 됩니다. 즉,

    public -> public
    protected -> protected
    private -> private

    위와 같이 상속을 받게 됩니다.

(참고)
C++을 사용할 때 대부분의 경우 이 방식으로 상속을 진행하는게 좋습니다.

  • protected 상속

    접근 범위가 protected보다 큰것들은 protected로 상속받게 됩니다. 즉,

    public -> protected
    protected -> protected
    private -> private

    위와 같이 상속을 받게 됩니다.

  • private 상속

    접근 범위가 private보다 큰것들은 private로 상속받게 됩니다. 즉,

    public -> private
    protected -> private
    private -> private

    위와 같이 상속을 받게 됩니다.

(참고)
상속 형식을 아무것도 정해주지 않을 경우 자동으로 private방법으로 상속하게 됩니다.

3-3) 상속 내용

  • 기본

    상속을 통하면 자식클래스는 부모 클래스의 모든 멤버변수와 멤버 함수를 물려받게 됩니다.

    이때, 모든 멤버들을 물려받긴 했지만 물려받을 때 정해진 권한에 의해 접근여부가 결정되는 것입니다.

    즉, 예를들어 최종적으로 private로 상속받았을 경우 자식클래스는 그 대상을 직접적으로 이용할 수는 없지만 가지고 있다는 것입니다.

  • 오버라이딩(재정의)

    상속 받을 때, 부모 클래스의 함수를 다시 정의하는 것을 의미합니다.
    class base
    {
    public:
       void printName() {cout << "base" << endl; }
    };
    class derived : public base
    {
    public:
       void printName() {cout << "derived" << endl; }
    }
    위와 같이 부모 클래스에서 물려받았던 함수이지만 자식이 다시 새롭게 정의하는것을 오버 라이딩이라고 합니다.
  • 새로운 멤버

    class base
    {
    public:
       void printName() {cout << "base" << endl; }
    };
    class derived : public base
    {
    public:
       void printName() {cout << "derived" << endl; }
       void printing() {cout << "New member" << endl; }
    private:
       int a
    }
    위와 같이 부모 클래스에 없던 새로운 함수나 멤버 변수를 생성하는 것이 가능합니다.

(참고)
오버로딩
오버 로딩은 같은 이름을 가졌지만 파라미터로 인해 그 함수의 종류가 다른, 명백히 다른 함수를 정의하는 것입니다.

오버라이딩
오버 라이딩은 부모 클래스에서 이미 정의된 종류까지도 모두 같은 함수를 다시 정의하는것을 의미합니다.

3-4) 부모와 자식의 관계

생성 순서

당연히 자식이 있기 이전에 부모가 있여야겠죠? 따라서 자식 객체를 만들게 되면 우선 부모 클래스의 생성자를 호출해야 합니다..

class base
{
public:
   base(int _x) { a = _x;}
private:
   int a;
};
class derived : public base
{
public:
   derived(int _x, int _y) 
       :base(_x)
   { b =_y; }
private:
   int b
}

위와 같이 상속받은 클래스에서 부모 클래스의 생성자를 통해 부모 클래스의 멤버를 초기화 해 주고, 나머지 멤버 변수를 초기화 해주도록 합시다.

(잊지맙시다.)
정의한 derived클래스를 초기화 할 때 부모 클래스의 멤버가 보이지 않아서 부모클래스의 멤버의 존재를 까먹는 경우가 있었습니다.

(참고)
생성자의 호출 순서: 부모 -> 자식
소멸자의 호출 순서: 자식 -> 부모


대입

부모 객체와 자식 객체간의 대입관계는 어떻게 될까요?

1) 대입연산

     base = derived; // 가능
     (부모객체에 자식객체를 대입하는 연산입니다.)
    derived = base; // 불가능
     (자식 객체에 부모 객체를 대입하는 연산입니다.)
  • 위의 경우

    부모 객체에 존재하는 멤버만 자식 객체의 멤버에서 대입하면 되므로 가능합니다.

  • 아래의 경우

    자식 객체의 멤버에는 부모 객체에는 존재하지 않는 요소들이 있어 이것들에 대한 대입이 불가능합니다.

2) 포인터

     base* ptr = derived; // 가능
     (부모포인터로 자식 객체를 가리키는 연산입니다.)
     `derived* ptr = base; // 불가능`
      *(자식포인터로 부모 객체를 가리키는 연산입니다.)*

(우선 포인터는 "위치정보"+"크기정보" 를 가져야 한다는 것을 생각해 봅시다.)

  • 위의 경우

    크기 정보의 경우 base의 크기가 더 작으므로 derived객체의 범위를 벗어나지 않습니다.

    즉, 이 포인터의 크기인 base의 크기 만큼의 derived객체를 이 포인터로 관할 하게 됩니다.

  • 아래의 경우

    크기정보의 경우 derived의 크기가 더 크므로 base객체의 범위를 벗어납니다.

    즉, 이 포인터가 관할하는 구역에는 "건들여서는 안되는 주소"가 포함되고 있으므로 포인터 지정이 불가능합니다.

3) 별명

레퍼런스도 포인터와 마찬가지로 자식객체의 레퍼런스의 경우 부모객체에서 접근 불가능한 공간이 발생하므로

derived& ref = base; //불가능

해당 코드는 사용이 불가합니다.

(참고)


중요
위와 같이 부모 포인터로 자식 객체를 가리키는 경우

실제 객체가 무엇이던 상관없이,
포인터의 타입을 기준으로

호출될 함수가 결정됩니다.

즉, 오버 라이딩한 함수의 경우라도 부모 클래스의 함수를 호출한다는 것입니다.



상속의 활용-다형성

문제상황

1) 상속을 통해 많은 자식클래스들을 만들었다고 생각해봅시다.

2) 이 클래스들을 다루기 위해서 부모 클래스의 포인터로 자식 클래스들을 가리키게 만들고 이 포인터로 자식 객체를 다루고자 합니다.

3) 이때 포인터의 타입을 기준으로 호출될 함수가 결정되므로 자식 객체를 다루는데 문제가 발생합니다.

ptr[0] -> play();	// base::play() 실행
ptr[1] -> play();	// base::play() 실행

...

(위의 대입: 포인터부분에서 배웠던 것 참고)

가상함수

가상함수는 해당 함수가 오버라이딩 되었을 때, 그 함수를 없는 것으로 생각한다... 라는 정도로 생각하시면 될 것 같습니다.

즉, 부모 클래스로 가리키고 있는 자식 클래스에서 해당 함수에 접근할 때, 부모 클래스의 함수가 아닌 자식 클래스의 함수를 호출하게 됩니다.

  • 가상함수 설정

    1) virtual키워드를 해당 함수 앞에 붙여줍니다.

    2) 중요: 가상함수가 존재하는 클래스의 소멸자는 반드시 가상함수로 만들어 주어야 합니다.

class base
{
public:
    base();
    virtual ~base();
    virtual void play() const;
};

base::base() { }
void base::play() const{ cout << "base"<< endl; }
class derived : public base
{
public:
    derived();
    virtual ~derived();
    virtual void play() const;
};

derived::derived() { }
void derived::play() const{cout << "derived" << endl; }

(참고)
1. virtual 키워드는 프로토 타입에만 붙여주면 됩니다.
2. 부모클래스에서 가상함수였던 함수는 자식클래스에서 자동으로 가상함수가 됩니다.
하지만 자식클래스에서도 virtual 키워드를 붙여주는 것이 관례입니다.

  • 순수 가상함수

    1) 순수 가상함수란?
    함수의 정의조차 필요하지 않는 함수임을 의미합니다. 즉, 자식 클래스에서 반드시 오버라이딩(재정의)해야 하는 함수임을 알려주는 것입니다.

    2)virtual키워드와 const = 0; 을 붙이면 순수 가상함수가 됩니다.

class base
{
public:
    base();
    virtual ~base();
    virtual void play() const = 0;
};

base::base() { }
class derived : public base
{
public:
    derived();
    virtual ~derived();
    virtual void play() const;
};

derived::derived() { }
void derived::play() const{cout << "derived" << endl; }

(참고)
이 순수 가상함수를 가진 클래스를 추상 클래스라고 부릅니다.

이때 이 클래스로 객체를 만드는 것은 불가능합니다.

다형성이란?

생물학과 화학에서 사용하는 용어로, 동일한 /물질 이면서 그 형태나 성질이 다른 것을 의미한다고 합니다.

즉, 우리는 다음처럼 상속관계에 있는 두 클래스가 있을 때,

class base
{
public:
    base();
    virtual ~base();
    virtual void play() const;
};

base::base() { }
void base::play() const{ cout << "base"<< endl; }
class derived : public base
{
public:
    derived();
    virtual ~derived();
    virtual void play() const;
};

derived::derived() { }
void derived::play() const{cout << "derived" << endl; }

아래와 같이, 동일한 포인터로, 같은 함수를 불렀지만

int main() {
    base* ptr[2];
    
    ptr[0] = new base();
    ptr[1] = new derived();
    
    ptr[0] -> play();    // base 출력
    ptr[1] -> play();    // derived 출력
}

서로다른 결과를 가지도록 하는것을 "다형성"이라고 부르는 것입니다.

(참고)
오버로딩도 다형성의 한 종류라고 볼 수 있습니다.
하나의 이름을 사용하지만, 인자를 다르게 하여 서로 다른 결과를 가지도록 하기 때문입니다.


주의점

1. 객체 파라미터

어떤 함수에 객체가 파라미터로 넘겨질 경우,

call by reference

방법으로 해당 함수에 넘겨주는 것이 좋습니다.

객체는 그 크기가 매우 크기 때문에 call by value로 가져올경우 시간상이나 메모리상으로 매우 비효율 적이기 때문입니다.

class point
{
    int x, y
};

void print(const point& x);   //point를 사용해야하는 함수

이때 넘긴 객체의 데이터가 변경되지 않기를 원한다면 const를 활용해 줍시다

2. 대입과 생성

생성자는 객체를 생성할 때 단 한번 호출된다는 것이 기억 나시나요?
즉, 객체를 생성하는 것과 대입하는 것은 완전히 다른 일이라는 것입니다.

  • Excavators num1(num2)는 생성자를 호출하는 것이고

  • num1 = num2는 대입, 즉 멤버 변수의 1:1 대입을 의미합니다.
    (Excavators num1, num2가 이미 선언되어 있는 상태)

3. Shallow copy와 Deep copy

  • 상황
    포인터가 있는 클래스를 복사하고자 한다.

  • Shallow copy (위험)

    class base
    {
    public:
      base(int x) {
          ptr = new int[x];
      }
      ~base() {
          delete ptr;
      }
    private:
      int* ptr;
    };
    int main() {
        base a(12);
        base b = a;
    }

    위와 같이 복사 생성자를 통해 대입할 경우 abptr에 담긴 주소를 복사하게 되어 해당 주소를 공유하게 됩니다.

    이때 a와 b가 소멸될 경우 소멸자에 의해 해당 주소의 공간을 중복 해제하게 됩니다.

    따라서 위와같이 코드를 작성하면 매우 위험한 코드가 되겠습니다.

  • Deep copy

    class base
    {
    public:
      base(int x) {
          ptr = new int[x];
      }
      ~base() {
          delete ptr;
      }
      
      base(const base& s) {
          ptr = new int[sizeof(s.ptr)/sizeof(int)] 
      }
    private:
      int* ptr;
    };
    int main() {
        base a(12);
        base b = a;
    }

    따라서 멤버 변수에 포인터가 존재할 경우

    위와 같이 복사 생성자를 재정의 하고, 포인터에는 무작정 대입이 아닌, 그 사이즈를 구해서 그 만큼의 길이를 새로 할당하는 코드를 짜주도록 합시다.

4. inline 함수

우리가 멤버 함수를 정의할때의 방법은 class안에 정의하는 방법과 class 외부에 정의하는방법 이렇게 2가지가 있었습니다.

이때 class의 내부에 정의할 때에는 그 멤버 함수는 inline함수로 자동으로 선언이 됩니다. 반면에 class의 외부에 정의할 때에는 inline함수가 아닌 호출 함수로 정의되게 됩니다.

따라서 외부에 정의하였을 때 inline함수로 만들어주고 싶을 때에는 inline키워드를 정의 맨 앞쪽에 적어주어 표시합시다.

5. const객체, const함수

일반 변수에만 const가 존재하는 것이 아닙니다. class에서는 객체와 함수에도 const속성을 부여할 수 있습니다.

  • const함수
    함수 또한 일반 변수의 const와 같이 이 멤버 함수가 멤버 변수의 값을 변경하지 않을 때 const속성을 부여해 줄 수 있습니다.

    • 방법
      void print() const;
      void Excavators::print(){ cout << x << endl; }

      void print() const{ cout << x << endl; }

      다음과 같이 함수의 prototype에, 또는 함수 설명시작 전에 붙여줍시다.

  • const객체
    const로 선언된 객체는 해당 멤버 변수에 접근해서 값을 바꿀 수 없게 됩니다. 또, const로 선언된 함수가 아닐 경우 호출이 불가능 합니다.

    즉, const Excavators num1(1,1)로 객체를 선언 하면

    • num1.Getpart1();

    • num1.x = 1

      위와 같이 const가 아닌 함수를 호출하거나 멤버 변수를 변경할 경우 오류를 일으키게 됩니다.
      (const로 선언하지 않았다면 값을 변경하지 않는 함수라고 하더라도 호출이 불가능합니다.)

즉, const로 선언된 객체를 위해 가능한한 멤버 함수를const로 선언해 주도록 합시다.

5. 헤더파일의 구성

class를 헤더파일과 구현파일로 나눌 때에는 다음과 같은 조건을 만족해야 합니다.

헤더파일

  • class 정의
  • inline으로 선언된 멤버함수

구현파일

  • class의 멤버함수 정의
  • static 멤버함수/변수의 정의

7. 상속- 이름의 충돌

A <- B <- C
위와 같은 상속관계를 가지는 클래스 A,B,C가 있다고 생각해 봅시다.

A클래스와 B클래스에서 오버라이딩 한 함수play()가 있습니다.

이 때 C로 만든 객체에서 play()함수를 부를 경우 어떤 함수를 호출해야 할지 모르기 때문에 오류를 일으킵니다.

즉, 이 경우 함수 호출시 A::play()B::play()같이 직접 정해주어야 합니다.

8. HAS - A와 IS - A

  • HAS - A
    어떤 객체가 다른 객체를 멤버 변수로 가지는 것을 의미합니다
  • IS - A
    상속관계에 있는 두 클래스를 의미합니다.
profile
github로 이전 중... (https://uijinee.github.io/)

0개의 댓글