상속 (Inheritance)

  • 클래스를 구현할 때, 다른 클래스나 객체에 구현되어 있는 동작을 유지하기 위해 그것을 기반으로 만드는 것.

  • 일반적으로는 기반이 되는 것을 부모(Parent) 클래스, 기반을 활용하여 만드는 클래스를 자식(Child) 클래스라고 함.

  • 상속을 하는 이유

    • 기반 클래스의 코드를 재활용 하기 위함

    • 다형성(Polymorphism)을 활용하기 위함.

  • 상속의 종류

    • 단일 상속(Single Inheritance): 하위 클래스에서 한 개의 상위 클래스로부터 상속 받는 것.

    • 다중 상속(Multiple Inheritance): 하위 클래스에서 두 개 이상의 상위 클래스로부터 상속 받는 것.

    • 다 단계 상속(Multilevel Inheritance): 하위 클래스에서 다른 하위 클래스로부터 상속 받는 것.

// Parent class
class FirstParent { }
class SecondParent { }

// Single Inheritance
class FirstChild : public FirstParent { }
class SecondChild : public SecondParent { }

// Multiple Inheritance
class ThirdChild : public FirstParent, pulbic SecondParent { }

// Multilevel Inheritance
class FirstDescendant : public FirstChild { }
class SecondDescendant : public SecondChild { }

// Multiple + Multilevel Inheritance
class ThirdDescendant : public FirstChild, public SecondChild { }
  • 상속 관계에서 하위 클래스는 상위 클래스의 멤버 변수들을 모두 갖기 때문에, 하위 클래스의 객체를 생성할 때 하위 클래스와 상위 클래스의 생성자가 모두 호출되어야 하며, 객체가 파괴될 때 상위 클래스와 하위 클래스의 소멸자가 모두 호출되어야 함.

    • 생성자 호출 순서: 상위 클래스 생성자 호출 --> 하위 클래스 생성자 호출

    • 소멸자 호출 순서: 하위 클래스 소멸자 호출 --> 상위 클래스 소멸자 호출

  • 하위 클래스들은 자신의 고유한 멤버 변수나 함수 뿐만 아니라 상위 클래스의 멤버 변수와 함수까지 가질 수 있음.

    • Programmer와 Designer가 Person을 상속받으면, 두 클래스 모두 Person의 멤버 변수 및 함수를 사용 가능.

#include <iostream>
using namespace std;

class Person {
private:
    string name;
protected:
    int age;
public:
    Person(string _name, int _age) : name(_name), age(_age) {
        cout << "Person Constructor." << endl;
    }
    Person(const Person& src) : name(src.name), age(src.age) { }
    ~Person() { cout << "Person Destructor." << endl; }

    void introduce() { cout << name << ": Hi!" << endl; }
    void setName(string _name) { name = _name; }
    string getName() const { return name; }
    void setAge(int _age) { age = _age; }
    int getAge() const { return age; }
};


class Programmer : public Person {
private:
    int numOfLang;
public:
    Programmer(string _name, int _age, int _numOfLang)
        : Person(_name, _age), numOfLang(_numOfLang) {
        cout << "Programmer Constructor." << endl;
    }
    ~Programmer() { cout << "Programmer Destructor." << endl; }
    void setNumOfLang(int _numOfLang) { numOfLang = _numOfLang; }
    int getNumOfLang() const { return numOfLang; }
};


class Designer : public Person {
private:
    int numOfTools;
public:
    Designer(string _name, int _age, int _numOfTools)
        : Person(_name, _age), numOfTools(_numOfTools) {
        cout << "Designer Constructor." << endl;
    }
    ~Designer() { cout << "Designer Destructor." << endl; }
    void setNumOfTools(int _numOfTools) { numOfTools = _numOfTools; }
    int getNumOfTools() const { return numOfTools; }
};


int main() {
    Programmer pr("ke2ek", 24, 4);
    Designer dg("2ekke2", 40, 3);
    pr.introduce();
    cout << "I'm " << pr.getAge() << " years old." << endl;
    dg.introduce();
    cout << "I'm " << dg.getAge() << " years old." << endl;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Person Constructor.         <---------- 중복 호출, virtual keyword 사용 시 한 번만 호출 가능
Designer Constructor.
ke2ek: Hi!
I'm 24 years old.
2ekke2: Hi!
I'm 40 years old.
Designer Destructor.
Person Destructor.
Programmer Destructor.
Person Destructor.         <---------- 중복 호출, virtual keyword 사용 시 한 번만 호출 가능

  • 상속의 접근 범위

    • 클래스의 멤버 변수나 함수에 접근 제한자를 두는 것처럼, 상속 관계 안에도 접근 제한자를 통해, 하위 클래스에서 상위 클래스의 멤버 변수의 접근 범위를 정할 수 있음.

    • 상속 관계 안에서의 접근 제한자에 따라 3가지의 상속 관계로 분류.

    • public 상속: 하위 클래스에서는 상위 클래스의 접근 제한자의 특성을 그대로 물려 받음.

    • protected 상속: 하위 클래스에서는 상위 클래스의 public 접근 제한자도 protected로 바꿔서 물려 받음. 따라서 외부에서는 하위 클래스의 객체로 상위 클래스의 public 멤버 함수를 호출할 수 없음.

    • private 상속: 하위 클래스에서는 상위 클래스의 모든 접근 제한자를 private으로 바꿔서 물려 받음. 따라서 하위 클래스 내부에서도 상위 클래스의 protected나 public 멤버 변수 및 함수를 사용할 수 없음.

 

함수 재정의 (Function Overriding)

  • 상속 관계일 때, 상위 클래스의 함수를 하위 클래스에서 재정의 하는 것을 의미.

  • 상위 클래스의 함수 이름, 반환형, 파라미터의 형태가 같은 함수를 동일하게 하위 클래스에서 정의.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }
    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() {
        cout << "Programmer Constructor." << endl;
    }
    ~Programmer() {
        cout << "Programmer Destructor." << endl;
    }
    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person p;
    p.doWork(); // Working ...
    Programmer pr;
    pr.doWork(); // Programming ...
    return 0;
}

 

Up/Down Casting

  • 하위 클래스의 객체를 상위 클래스의 객체에 대입할 대 발생하는 형 변환을 업 캐스팅(Up-casting)이라고 하며, 일반적으로 업 캐스팅을 하면 하위 클래스의 데이터가 손실될 수 있음.

    • 따라서 데이터 손실을 방지하기 위해 참조자 또는 포인터를 사용하여 업 캐스팅을 함.

    • 그대로 참조자나 포인터 변수를 사용하면 안되고 클래스 내부에서 virtual 키워드를 사용해야 함.

// the same as above

int main() {
    Programmer pr;
    Person p = pr;
    p.doWork(); // Working ...
    
    // better than previous
    Programmer pro;
    Person &pRef = pro; // reference
    pRef.doWork(); // Working ...
    
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork();; // Working ...
    
    // BUT, Data loss occurred still.
    // We need virtual keyword!
    return 0;
}
  • 상위 클래스의 객체를 하위 클래스의 객체에 대입할 때 발생하는 형 변환을 다운 캐스팅(Down-casting)이라고 하며, 일반적으로 상위 클래스의 객체를 하위 클래스 객체로 형 변환 하는 것은 상대적으로 메모리가 작은 상위 클래스 객체에서 메모리가 큰 하위 클래스의 객체로 변환하기 때문에 정상 동작을 보장하지 않음.

// the same as above

int main() {
    /* Error
    Person p;
    Programmer pr = p; 
    pr.doWork();
    */
    
    Person p;
    Programmer *pr = reinterpret_cast<Programmer*>(&p);
    pr->doWork();
    
    return 0;
}

 

가상 함수 (Virtual Function)

  • 부모 클래스의 멤버 함수 앞에 virtual 이라는 키워드를 붙이고, 자식 클래스에서 그 함수를 오버라이딩(Overidding)할 때, 포인터 또는 참조자 타입의 업 캐스팅(Up-casting)하여 객체를 사용하는 경우 자식 클래스의 함수를 호출함.

  • 업 캐스팅 시 virtual 키워드를 붙여주지 않으면 부모 클래스의 함수가 호출 되는데, 그 이유는 함수의 호출 위치를 컴파일 타임(early time)에 결정하기 때문.

  • virtual 키워드를 붙여 주면, 해당 함수의 호출 위치가 코드가 컴파일 되는 시점에 결정되는 것이 아니라 런타임(late time)에 결정되기 때문에 하위 클래스의 오버라이딩 된 함수를 호출하게 됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }
    
    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }
    
    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Programmer pro;
    Person &pRef = pro; // reference
    pRef.doWork(); // Programming ...
    
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork(); // Programming ...

    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...               <----- 재정의 된 함수 호출
Person Constructor.
Programmer Constructor.
Programming ...                <----- 재정의 된 함수 호출
Programmer Destructor.  <----- 레퍼런스 변수에 의해 호출된 소멸자
Person Destructor.           <----- 레퍼런스 변수에 의해 호출된 소멸자

  • 소멸자 역시 함수이므로 오버라이딩 된 소멸자가 호출되게 하려면 반드시 상위 클래스에 virtual 키워드를 붙여야 함.

    • 소멸자의 경우 제대로 처리하지 않으면 메모리 누스(Memory Leak)이 발생할 수 있으니 주의 깊게 다루어야 함.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...
Person Destructor.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    Person *pPtr = new Programmer; // pointer
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Programming ...
Programmer Destructor.
Person Destructor.

 

추상 클래스 (Abstract Class)

  • 순수 가상 함수(Pure Virtual Function)

    • 부모 클래스에서 상속받을 자식 클래스의 공통 기능의 정의만 선언해 놓고, 실제 구현은 자식 클래스에게 위임하기 위한 함수.

    • 문법: virtual  반환형  함수명(매개변수)  =  0;

  • 추상 클래스란 순수 가상 함수를 하나 이상 포함하는 클래스를 의미

  • 추상 클래스는 객체화(인스턴스화)를 할 수 없으며, 시도하게 될 경우 컴파일 오류가 발생함.

  • 상위 클래스에 대한 객체 생성 요구가 발생하지 않는 상황이라면 상위 클래스에 순수 가상 함수를 선언하여 추상 클래스로 만드는 것이 권장됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


int main() {
    // Error
    // Person p;
    Person *pPtr = new Programmer;
    pPtr->doWork(); // Programming ...
    delete pPtr;
    return 0;
}

 

가상 상속 (Virtual Inheritance)

  • 다중 상속(Multiple Inheritance)을 구현하다 보면, 다음과 같은 문제가 발생함.

    • Parent 클래스를 상속한 FirstChild와 SecondChild 클래스를 Descendant 클래스가 다중 상속하는 경우

    • Parent 클래스가 추상 클래스이기 때문에 FirstChild와 SecondChild 클래스의 멤버함수가 동일한 형태를 띈다면

    • Descendant 클래스에서 그 멤버 함수를 사용하지 않더라고 컴파일 오류가 발생함.

    • 또는 Descendant 클래스를 만들거나 파괴할 때, Parent 클래스의 생성자와 소멸자가 2번 호출됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }
};


class Designer : public Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

// the same as above

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


class Designer : public Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }

    void doWork() { cout << "Designing ..." << endl; }
};

// the same as above

[결과]

// the same as above

int main() {
    Freelancer f;
    //f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Person Constructor.
Designer Constructor.
Freelancer Constructor.
Freelancer Destructor.
Designer Destructor.
Person Destructor.
Programmer Destructor.
Person Destructor.

  • 상속 문법에서 접근 제한자 뒤에 virtual 키워드를 붙이면 위의 문제가 해결됨.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    void doWork() { cout << "Working ..." << endl; }
};


class Programmer : public virtual Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }
};


class Designer : public virtual Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Designer Constructor.
Freelancer Constructor.
Working ...
Freelancer Destructor.
Designer Destructor.
Programmer Destructor.
Person Destructor.

#include <iostream>
using namespace std;

class Person {
public:
    Person() { cout << "Person Constructor." << endl; }
    virtual ~Person() { cout << "Person Destructor." << endl; }

    virtual void doWork() = 0;
};


class Programmer : public virtual Person {
public:
    Programmer() : Person() { cout << "Programmer Constructor." << endl; }
    virtual ~Programmer() { cout << "Programmer Destructor." << endl; }

    void doWork() { cout << "Programming ..." << endl; }
};


class Designer : public virtual Person {
public:
    Designer() : Person() { cout << "Designer Constructor." << endl; }
    virtual ~Designer() { cout << "Designer Destructor." << endl; }

    void doWork() { cout << "Designing ..." << endl; }
};


class Freelancer : public Programmer, public Designer {
public:
    Freelancer() : Programmer(), Designer() {
        cout << "Freelancer Constructor." << endl;
    }
    ~Freelancer() { cout << "Freelancer Destructor." << endl; }

    // You must implement duplicate function overidding again.
    void doWork() { cout << "Working too much ..." << endl; }
};


int main() {
    Freelancer f;
    f.doWork();
    return 0;
}

[결과]

더보기

Person Constructor.
Programmer Constructor.
Designer Constructor.
Freelancer Constructor.
Working too much ...
Freelancer Destructor.
Designer Destructor.
Programmer Destructor.
Person Destructor.

 

다형성 (Polymorphism)

  • 하나의 모습으로 다양한 형태를 가질 수 있는 성질.

  • 클래스를 상속받아 사용하는 가장 큰 이유.

  • Person *pPtr = new Programmer; 또는 Person *pPtr = new Designer; 등 하나의 클래스 타입으로 다양한 형태를 가질 수 있다.

  • 특히, 함수의 매개변수로 클래스 타입을 받을 때, 여러 하위 클래스를 처리해야 한다면 다형성이 꼭 필요하다.

 

+ Recent posts