본문 바로가기

Effective C++/6. 상속, 그리고 객체 지향 설계

(9)
항목 40: 다중 상속은 심사숙고해서 사용하자 C++에서 다중 상속(multiple inheritance: MI)에 관한 견해를 살펴보면, 크게 두 가지 진영으로 나뉜다. 단일 상속(single inheritance: SI)이 좋다면 다중 상속은 더 좋을 것이다. 단일 상속은 좋지만 다중 상속은 골칫거리밖에 안된다. 일단 이 두 가지 견해가 어떤 이야기인지 살펴보자. '다중 상속'하면 바로 떠올라야 하는 사실 중 하나는, 둘 이상의 기본 클래스로부터 똑같은 이름(이를테면 함수, typedef 등)을 물려받을 가능성이 생긴다는 점이다. 즉 다중 상속으로 인해 모호성이 생긴다는 것이다. 여기서 잘 보면, checkOut() 호출부에서 모호성이 발생하고 있는데, 사실 두 checkOut() 중에서 파생 클래스가 접근할 수 있는 함수가 딱 정해져 있다.(E..
항목 39: private 상속은 심사숙고해서 구사하자 C++은 public 상속을 is-a 관계로 나타낸다.(항목 32 참고) Student가 Person으로부터 public 상속으로 파생된 형태의 클래스 계통이 주어졌다고 가정하면, 함수 호출을 성공시키기 위해 컴파일러가 Student를 Person으로 암시적 변환을 수행하는 예제를 통해 is-a 관계를 설명했었다. 이 예제를 private 상속으로 살짝 바꿔보자. private 상속은 분명 is-a 관계를 뜻하지 않는다. 그럼 뭘까? 일단 private 상속의 동작 규칙부터 알아보자. private 상속의 동작 규칙 파생 클래스 객체(Student)를 기본 클래스(Person)으로 변환하지 않는다. 기본 클래스로부터 물려받은 맴버는 파생 클래스에서 모조리 private 맴버가 된다. 즉 기본 클래스에서 ..
항목 38: "has-a(…는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(…는...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 합성(composition) : 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계 포함된 객체들을 모아서 이들을 포함한 다른 객체들을 합성한다 라는 뜻이다. 예를 보자. Person 객체는 string, Address, PhoneNumber 객체로 이루어져 있다. 개발자들 사이에선 이러한 합성 관계를 합성(composition) 레이러링(layering) 포함(containment) 통합(aggregation) 내장(embedding) 등 다양한 이름으로 부른다 public 상속 "is-a(…는...의 일종이다.)" (항목 32 참고) 객체 합성 "has-a(...는...를 가짐)" "is-implemented-in-terms-of(…는 ...를 써서 ..
항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 C++에서 상속받을 수 있는 함수의 종류 비가상 함수 : 절대로 재정의하면 안된다. 가상 함수 : '기본 매개변수 값을 가진 가상 함수를 상속하는 경우' 재정의하면 안된다. 왜 안될까? 가상 함수는 동적으로 바인딩(지연 바인딩,late binding)되고, 기본 매개변수 값은 정적으로 바인딩(선행 바인딩,early binding),되기 때문이다. 객체의 정적 타입(static type) : 우리가 작성한 선언문을 통해 그 객체가 갖는 타입 어떤 클래스 계통을 예로 들어보자. ps, pc, pr은 모두 Shape*로 선언되어 있기 때문에, 정적타입은 그냥 모두 Shape*이다. 하지만 이들이 진짜로 가르키는 대상은 모두 다르다. 객체의 동적 타입(dynamic type) : 현재 그 객체가 진짜로 무엇이냐..
항목 36: 상속받은 비가상 함수를 파생 클래스에 재정의하는 것은 절대 금물! D라는 이름의 클래스가 B라는 이름의 클래스로부터 public 상속에 의해 파생되었고, B 클래스에는 mf()라는 public 맴버 함수가 정의되어 있다고 가정해보자. 여기서의 pB->mf() 이 당연히 이런식으로 동작할 것이라고 예상할 수 있다. 왜? 둘 다 같은 객체인 x에 대해 mf()를 호출하고 있기 때문이다. 하지만 당연하지 않을 수도 있다는 것이 문제이다. 특히 mf()가 비가상 함수이고 D 클래스가 자체적으로 mf() 함수를 또 정의하고 있는 경우 아래 코드와 같은 황당한 일이 벌어진다. 이렇게 같은 객체에서 같은 함수를 호출하는데, 실제로는 다른 함수를 호출하는 이유는, 비가상 함수는 정적 바인딩(static binding)으로 묶이기 때문이다.(항목 37 참고) => 즉 pB가 가르키는 ..
항목 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 우리는 지금 비디오 게임 개발팀에서 일을 하고 있다. 맡은 일은 게임에 등장하는 각종 캐릭터를 클래스로 설계하는 직업이다. 근데 게임이 막 치고받고 싸우는 게임이라, 체력바가 남아나질 않는다. 그래서 healthValue() 라는 맴버 함수를 제공하기로 한다. 체력이 어떻게 계산되는지는 캐릭마다 다르기 때문에, 이 함수를 가상 함수로 선언하기로 한다. healthValue()가 순수가상함수는 아니기 때문에, 체력치를 계산하는 기본 알고리즘(기본 구현)이 제공된다는 사실을 알 수 있다. (항목 34 참고) 너무나 당연한 설계라 오히려 맥이 빠진다. 이것 말고 다른 설계도 있지 않을까? 비가상 인터페이스 관용구를 통한 템플릿 메서드 패턴 이 이야기는 "가상 함수는 반드시 private 맴버로 두어야 한다"고..
항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자 (public)상속 이라는 개념은 사실 2가지로 나뉜다. 인터페이스 상속 vs (함수)구현 상속 이 둘의 차이는 함수 선언과 함수 정의의 차이와 맥을 같이한다고 보면 된다. 클래스 설계자의 입장에서 맴버 함수의 인터페이스(선언)만을 파생클래스에 상속받고 싶은 경우 함수의 인터페이스 구현을 모두 상속받고 싶은 경우 상속받은 구현이 오버라이드가 가능하게 만들고 싶은 경우 어떤 것도 오버라이드할 수 없도록 막고 싶은 경우 이러저러한 선택사항이 나올 수 있는데, 이러한 선택사항들 사이의 차이점을 명확하게 몸으로 느끼는 것이 중요하다. 그래서 예를 들어보면, 그래픽 응용프로그램에 쓰이는 기하학적 도형을 나타내는 클래스 계통구조를 놓고 한번 생각해보자. Shape는 추상 클래스이다. 왜냐하면 맴버 함수인 draw(..
항목 33: 상속된 이름을 숨기는 일은 피하자 상속된 이름 => 진짜 상속이 아니라, 유효범위에 관련된 이야기이다. 여기서 cin 은 전역변수 x가 아닌, 지역변수 x를 참조한다. 왜냐하면 안쪽 유효범위에 있는 이름이 바깥쪽 유효범위에 있는 이름을 가리기 때문이다. 컴파일러가 someFunc()의 유효범위 안에서 x라는 이름을 만나면 자신이 처리하고 있는 유효범위(지역 유효범위, local scope)안에서 같은 이름을 가진 것을 찾는다. 지역 유효범위 안에 찾는 이름이 있으면 더 이상 탐색하지 않는다. 지역 유효범위 안에 찾는 이름이 없으면 전역 유효범위(global scope) 안에서 같은 이름을 가진 것을 찾는다. => 이름 탐색에 type은 관계가 없다!! => 겹치는 이름들의 type이 같든 안같든, 현재 중요한 것은 x라는 이름의 doub..
항목 32: public 상속 모형은 반드시 "is-a(…는 ...의 일종이다)"를 따르도록 만들자 public 상속은 "is-a(…는 ...의 일종이다.)" 를 의미한다. 이 말은 꼭 기억하도록 하자. 만약 Derived 클래스를 Base 클래스로 부터 public 상속을 통해 파생시켰다면, 이것은 컴파일러에게 다음과 같이 말한 것이다. "Derived 타입으로 만들어진 모든 객체는 또한 Base 타입의 객체이지만, 그 반대는 되지 않는다" 즉 Base는 Derived 보다 더 일반적인 개념을 나타내며, Derived는 Base보다 더 특수한 개념을 나타낸다. 밴다이어그램으로 표현하면 다음과 같은 의미일 것이다. C++는 public 상속을 이렇게 해석하도록 문법적으로 지원하고 있다. 위 예제는 public 상속에서만 통한다. public 상속 = is-a 관계 자연어에 낚인 케이스이다. "새는 날 ..