is-a 관계
is-a 관계는 파생 클래스에 있어서 상속의 조건인데 is-a 관계의 의미인 ~은 ~이다라는 관계를 성립하자는 것입니다.
public 상속을 사용할 때는 is-a관계가 성립되도록 만들어야 합니다.
예를 들어서 "사과는 과일이다"은 성립합니다. 사과는 과일이니까요.
틀린 예로 "과일은 사과이다."가 있겠습니다. 문장 그대로 과일이라는 단어는 사과를 의미하지 않으니까요.
그래서 Apple 클래스는 파생클래스로 Fruit 클래스를 상속할 수 있는 것입니다.
is-a관계여야만 하는 이유
여러가지 과일의 대한 클래스를 만들 때, 기본적으로 그 사과들이 가지고 있는 공통적인 특징을 추상화한 클래스가 기초 클래스고 상속받은 여러가지 과일 클래스가 파생 클래스입니다.
이처럼 범위가 좁아지면서 기능이 기초 클래스보단 많아집니다.
기능이 많아지면 C++의 상속적 특성과 일치합니다.
그래서 is-a관계를 성립시키는 것이 상속을 하는 조건에서 반드시 필요로 합니다.
has-a 관계
has-a관계의 의미는 ~가 ~을 소유한다라는 의미인데,결론만 말하면
클래스의 데이터 멤버로 객체를 포함시키는게 좋습니다.
has-a관계가 성립되면 상속도 가능하지만 1순위로 is-a관계를 지키는 것이 좋습니다.
(애초에 소유하다는 포함하다라고 해석할 수 있으니 상속보다는 클래스의 데이터 멤버로 객체를 포함시키는 게 좋습니다.)
예를 들어서 "나(사람)는 사과를 갖고 있다."는 is-a관계에 성립되지 않고 has-a관계에 성립됩니다. 갖고 있다라는것은 뭐가 뭐를 포함하다는 의미도 되니 상속보다는 데이터 멤버가 더 어울립니다.
has-a 관계(~가~를 가지고 있다.)에서 모델링 하는 방법은 두 가지가 있는데 그중에 한 가지는 일반적인 방법으로, 컴포지션(컨테인먼트)을 사용하는 것입니다.
(나머지 한 가지는 private 상속을 사용하는 방법입니다.)
즉 다른 클래스의 객체들을 멤버로 가지는 클래스를 만드는 겁니다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <valarray>
using namespace std;
class Person {
private:
int x=0;
int y=0;
char * name = 0;
public:
Person(const char * p_name) {
name = new char[strlen(p_name) + 1];
strcpy(name, p_name);
}
Person(const Person & per) { //복사 생성자
name = new char[strlen(per.name) + 1];
strcpy(name, per.name);
x = per.x;
y = per.y;
}
Person & operator=(const Person & per) { //기초 클래스 대입 연산자 오버로딩
if (this == &per) {
return *this;
}
delete[] name;
name = new char[strlen(per.name) + 1];
strcpy(name, per.name);
x = per.x;
y = per.y;
return *this;
}
virtual void Show() {
cout << "이름:" << name << endl;
}
virtual ~Person() { //파괴자
delete[] name;
}
};
class Student : public Person {
private:
char * aff_school = 0;
valarray<double> scores; //내포된객체
//국어,영어,수학점수만
public:
Student(const char * p_name, const char * p_aff_school) : Person(p_name){
aff_school = new char(strlen(p_aff_school) + 1);
strcpy(aff_school, p_aff_school);
scores = valarray<double>(3); //3개짜리 배열 생성
}
Student(const Student & stu) : Person(stu) { //복사 생성자
aff_school = new char[strlen(stu.aff_school) + 1];
strcpy(aff_school, stu.aff_school);
}
Student & operator=(const Student & stu) { //파생 클래스 대입 연산자 오버로딩
if (this == &stu) {
return *this;
}
delete[] aff_school;
Person::operator=(stu); //명시적 대입 연산자 호출로 기초 클래스 부분을 복사함
aff_school = new char[strlen(stu.aff_school) + 1];
strcpy(aff_school, stu.aff_school);
return *this;
}
void Input(double sub1, double sub2, double sub3) {
scores[0] = sub1; //국어
scores[1] = sub2; //영어
scores[2] = sub3; //수학
}
void Show() {
cout << "소속 학교 명:" << aff_school << endl;
Person::Show();
cout << "국어 점수:" << scores[0] << endl;
cout << "영어 점수:" << scores[1] << endl;
cout << "수학 점수:" << scores[2] << endl;
}
~Student() { //파괴자
delete[] aff_school;
}
};
int main(void) {
Person per = Person("철수");
Student stu = Student("민지", "마포고");
stu.Input(50, 60, 70);
stu.Show();
}
(Student 클래스, 그 중에서도 42번째 줄만 보셔도 됩니다.)
valarray라는 클래스를 내포된 객체(scores)로 선언을 했는데 초기화 방식은 멤버 초기자 리스트나 생성자 내에서 해도 됩니다.
아니면 C++11에서 부터 가능한 In-Class 방식으로 해도 됩니다.
만약 멤버 초기자 리스트를 사용하지 않았을 경우에는 C++은 어떤 객체의 나머지 부분이 생성되기 전에 모든 멤버 객체들이 먼저 생성되어야 한다고 요구하기 때문에 멤버 객체 클래스를 위해 정의된 디폴트 생성자를 사용합니다.
이 scores라는 객체 멤버는 학생의 국어,영어,수학 점수를 저장하기 위해 존재합니다.(성적표)
성적표와 학생은 is-a관계보단 has-a관계에 가까우므로 모델링하는 방법 중 하나인 컴포지션을 사용하는게 맞습니다.
내포된 객체의 인터페이스 사용
scores.size() //원소 개수를 리턴
일반적인 클래스 멤버 함수 사용이랑 별 다를 게 없습니다.
댓글