개체지향 프로그래밍3
상속
부모 클래스의 속성과 기능을 자식 클래스에서 사용하는 방식으로 다음과 같이 사용할 수 있다.
자식 클래스는 멤버 변수 및 메서드를 추가할 수 있다.
위 클래스를 자바에서 사용한다면 다음과 같이 사용할 수 있다.
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