개체지향 프로그래밍3

상속

  • 부모 클래스의 속성과 기능을 자식 클래스에서 사용하는 방식으로 다음과 같이 사용할 수 있다.

    • 자식 클래스는 멤버 변수 및 메서드를 추가할 수 있다.

// Animal.h
class Animal
{
public:
    Animal(int age);
private:
    int mAge;
};

// Cat.h
class Cat : public Animal
{
public:
    Cat(int age, const char* name);
private:
    char* name;
};

// Cat.cpp
// 부모의 생성자를 초기화하고 생성자 구현부에서 자식에 추가된 부분만 신경쓴다.
Cat::Cat(int age, const char* name)
    :Animal(age)
{
    size_t size = strlen(name) + 1;
    mName = new char[size];
    strcpy(mName, name);
}
  • 위 클래스를 자바에서 사용한다면 다음과 같이 사용할 수 있다.

  • Java 와 달리 상속 시 베이스 클래스 멤버의 접근 수준을 결정할 수 있다. -> 부모와 자식 클래스 중 제한 수준이 더 높은 것으로 사용하게 된다. -> 대부분 public 을 사용하게 된다.

    • public

    • private

    • protected

상속 시 메모리는 어떻게 작동할까?

  • 무조건 적으로 상속 받은 부모가 먼저 호출되고 그 다음 자식이 호출된다. -> 부모와 자식 간의 메모리는 연속적이고, 언제나 부모 멤버변수의 메모리가 먼저 사용된다.

생성자 호출 순서, 소멸자 호출 순서

  • 부모 클래스의 생성자가 먼저 호출되고, 그 다음으로 자식 클래스의 생성자가 호출된다.

  • 부모 클래스의 특정 생성자 호출 시, 초기화 리스트를 사용해야 한다! -> const, & 키워드가 붙은 변수들은 생성과 동시에 초기화가 되어야 하기 때문이다!

  • 호출 사례.1 -> 매개변수 없는 생성자가 있는 베이스 클래스

    • 자식 생성자에서 부모생성자를 호출하지 않는다면, 암시적으로 Animal() 과 같이 부모의 기본생성자를 호출하게 된다.

  • 호출 사례.2 -> 매개변수 없는 생성자가 없는 베이스 클래스

    • 다음의 경우 부모 생성자에 기본생성자가 존재하지 않기 때문에, 컴파일 에러가 발생한다!

  • 소멸자는 어떤 순서로 작동할까?

    • 자식의 소멸자가 호출되고, 부모의 소멸자가 호출된다! (생성자의 순서와 반대이다!)

다형성(Polymorphism)

  • 다형성 이전에 멤버 함수는 메모리 어디에 위치하고 있을까?

  • 멤버 함수도 메모리 어딘가에 위치하고 있다. -> 모든 것이 메모리 어디엔가 위치해야만 한다.

  • 그런데 각 개체마다 멤버 함수의 메모리가 잡혀 있을까? -> 같은 클래스를 사용해 개체를 만들 시 두 개체 함수의 동작은 완전하게 일치한다.

  • 때문에, 멤버 함수는 컴파일 시에 딱 한번만 메모리에 "할당" 된다!

  • 함수 오버라이딩?

    • 부모의 함수를 자식이 새롭게 정의하는 문법으로 다음 코드를 통해서 확인해보자.

  • 그렇다면 위와 같이 오버라이딩된 함수를 호출할 때 Java 와는 무엇이 다를까?

    • Java : 실제 개체의 함수를 호출한다. (동적바인딩)

    • C++ : 변수의 자료형을 따라 함수를 호출한다. (정적바인딩)

정적 바인딩 (컴파일타임)

  • 왜 위 코드와 같이 동작이 일어날까? -> C++ 에서는 정적 바인딩을 사용하기 때문이다!

  • 정적 바인딩

    • 변수의 자료형을 따라서 호출하는 형태를 의미한다.

동적 바인딩 (런타임)

  • 그렇다면, Java와 같이 실제 사용하는 개체의 함수를 사용하고 싶다면 어떻게 할까? -> 가상 함수를 만들면 된다.

  • 다음과 같이 virtual 키워드를 붙여서 실제 개체의 함수를 사용하겠다고 선언을 하면 된다! (Java 와 같은 방법 : Java 모든 함수가 기본적으로 가상 함수이다!) -> Java 에서 정적 바인딩을 하고 싶다면 프로그래머는 final 키워드를 쓸 수 있다! -> 이것은 정적 바인딩(오버라이딩을 하지 않겠다!) 을 하겠다는 것을 의미하고, -> 가상 함수는 비 가상 함수보다 언제나 느리다! (가상 함수는 실제 개체를 따라가 호출해야 하기 때문에, 느리다..)

  • virtual 키워드를 붙여주는 것은 가상 테이블에 가상 함수의 주소를 추가하겠다는 뜻이다.

  • C++ 에서 virtual 키워드를 생략하면 정말 개판난다!

  • 이러한 방식을 실행 중에 어떤 함수를 설정할지 결정한다고 해서 동적 바인딩이라고 부른다. -> 당연히 정적 바인딩보다 느리다!

  • 동적 바인딩은 어떻게 실행될까?

    • 가상 테이블이라는 별도의 장치를 메모리에 할당해서 가상 함수를 찾게 된다. -> 다형성(virtual 키워드)을 사용하지 않는다면, 가상테이블이 생성되지 않는다.

    • 가상 테이블에는 해당 개체의 가상 함수의 주소를 가리키는 정보들이 들어있다!

    • 그렇다면, 가상 테이블은 클래스마다 있을까? 개체마다 있을까? -> 가상 테이블은 클래스마다 한개씩 존재한다! -> 개체를 생성할 때, 해당 클래스의 가상 테이블 주소가 함께 저장된다!

정적 바인딩과 동적 바인딩의 메모리를 사용하는 방식을 살펴보자!

  • 정적 바인딩은 -> 컴파일 타임에 이루어진다.

  • 동적 바인딩은 -> 런타임에 이루어진다.

  • 동적 바인딩이 다형성의 핵심이지만, 런타임에 가상 테이블을 통해 멤버 함수를 호출하기에 느릴 수 밖에 없다! -> 때로는 속도보다 설계(다형적인 구조)가 더욱 중요할 때도 있다.

가상 소멸자

  • 다음의 코드를 보고 생각을 해보자 -> 얼핏 보면 문제가 없어보인다.

  • 하지만, 다음의 코드로 변경한다면 어떻게 될까? -> 기본적으로 C++ 코드는 정적 바인딩을 사용하기 때문에, Cat 의 메모리를 해제하지 않고 Animal 의 메모리만 해제하는, 메모리 누수가 발생하게 된다.

  • 때문에, 소멸자에서도 동적 바인딩을 사용할 수 있도록 다음과 같이 virtual 키워드를 사용해야 한다!

  • 이전에 설명했듯 동적 바인딩은 가상테이블을 사용한다고 말했었는데, 아래와 같이 myCat 을 delete 하는 순간 Cat 클래스의 가상테이블에서 소멸자의 주소를 찾아내 소멸자를 호출하게 된다.

  • 가상 소멸자는 모든 클래스마다 하는 것을 권고한다. 하지만 다음과 같은 질문이 있을 수 있다.

    • 근데, 가상함수가 느린데,, 가상 소멸자가 있는 클래스를 상속받지 않아도 모든 클래스마다 가상 소멸자를 호출하라고?

  • 모든 클래스를 다른사람에 의해 내가 의도한대로 사용하지 않을 수 있다! -> 다른 개발자가 가상 소멸자를 사용하지 않으려고 한 클래스를 상속받아 다형성을 구현한다면, 메모리 누수가 생겨버릴 수 있다. -> 이러한 예기치 못한 상황이 있기 때문에, 우리는 안전한 코딩 습관이 중요하다.

다중 상속

  • Java 에서 지원하지 않는 기능 중 하나로 다음과 같이 다중의 부모 클래스를 상속받을 수 있다.

  • 그렇다면 어느 부모의 생성자가 먼저 호출될까?

    • 답은 간단하다! 초기화 리스트 순서와 상관 없이, 자식 클래스에서 등장한 순서대로 호출된다.

  • 이쯤 되어서 생각해볼 수 있는 부분은, Java 처럼 super() 키워드를 제공하지 않는 이유는 무엇일까? -> 다중 상속이 가능하기에, 컴파일러가 어떤 부모를 호출할지 알 수 없다!

  • 문제점1 - 중복 메서드 호출 문제 -> 상속 받은 두개의 클래스에서 같은 함수를 정의하고 있을 때, 사용하는 개체는 어떤 함수를 호출해야 하는지 모호하다..

    • 때문에, 다음 코드와 같이 어떤 부모 클래스를 사용하는지 명시적으로 사용해주어야 한다. -> 하지만 코드가 너무 더럽기에,, 좋은 해결책으로 생각하기 어렵다..

  • 문제점2 - 다이아몬드 상속 -> 다음과 같이 부모들이 같이 부모를 상속 받고 있을때, 부모들은 같은 클래스를 상속받지만, 메모리는 중복으로 사용하게 되어 효율적이지 못한 사용을 하게 된다.

  • 다음과 같이 virtual 키워드를 사용해 동적 바인딩처럼 사용할 수는 있지만, 객체지향을 생각했을 때 처음부터 같이 부모들을 사용하는 부모클래스들을 다중상속 받을 것이라는 설계를 하기란 불가능에 가깝다.

  • 다음의 방법은 임시 방편으로 생각해야지 근원적으로 객체지향의 원칙을 지키는 방법이라고 보기에는 무리가 있다.

  • 다중 상속은 다음과 같은 문제가 있기에,, 권장되지 않는다! -> 인터페이스를 사용하는 것을 권장한다!!

추상클래스

  • 추상클래스란 구체적인 함수의 구현이 안되어 있는 클래스를 의미한다. -> 다음 코드를 살펴보자!

  • 다음과 같이 부모 클래스에서 virtual 키워드와 = 0; 을 통해서 함수를 구현하지 않겠다고 선언한 후, 상속받은 클래스에서 해당 함수를 구현해주는 형태면 된다. -> Java 에서 abstract 를 생각하면 된다.

  • 다음과 같이 구현체가 없이 인터페이스만 있는 함수를 순수 가상함수(Pure Virtual Function) 이라고 부른다. -> 자식 클래스가 구현체를 만들어주어야 한다! -> 자식 클래스가 구현해주지 않는다면 컴파일에러가 발생한다!

  • 추가적으로 순수가상함수를 가지고 있는 함수를 추상클래스라고 부른다. -> 추상클래스는 개체를 만들 수 없다..(함수의 구현체가 없는 불완전한 클래스이기 때문)

인터페이스

  • 인터페이스는 기본적으로 순수가상함수만으로 이루어진 클래스를 의미한다. -> Java 에서는 interface 키워드를 지원하지만 C++ 에서는 기본적으로 지원하지 않기 때문에, class 키워드를 사용하고 다음과 같이 사용해야 한다. -> 인터페이스를 정의할 때 I 를 붙여 이름을 짓는것이 일반적이다.

  • 인터페이스는 함수만을 정의하기 때문에, 동작을 정의한다고 생각할 수 있다!

  • 인터페이스를 생각해보면 동적 바인딩만 사용하기 때문에, 다중상속 시 문제가 되었던, 중복 메모리 사용의 문제를 해결할 수 있는 아주 나이스한 방법이다.

Last updated