C++의 상속 개념 및 사용법에 관해 정리한다.


개요

기존에 정의한 클래스를 재활용하기 위한 방법

클래스의 공통되는 부분을 Base 클래스로 추상화 하고, 특징을 지닌 Derived 클래스를 정의할 수 있음

. B 클래스가 A 클래스를 상속할 경우

​ : B 클래스는 “A 클래스에 선언되어 있는 멤버 + B 클래스에 선언된 멤버” 를 지님

. 상속되는 클래스 : Super class or Base class

. 상속받는 클래스 : Sub class or Derived class

1
						class Base : public Derived
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <cstring>

using std::endl;
using std::cout;

class Person
{
    int age;
    char name[20];

    public:
        int GetAge() const {
            return age;
        }
        const char* GetName() const {
            return name;
        }
        Person(int _age=1, char* _name="none"){
            age = _age;
            strcpy(name, _name);
        }    
};

class Student: public Person    // Person 클래스를 상속
{
    char major[20];

    public:
        Student(char* _major){
            strcpy(major, _major);
        }
        const char* GetMajor() const {
            return major;            
        }
        void ShowData() const {
            cout << "이름 :" << GetName() << endl;
            cout << "나이 :" << GetAge() << endl;
            cout << "전공 :" << GetMajor() << endl;
        }
};

int main(void)
{
    Student Lee("Computer");
    Lee.ShowData();

    return 0;
}

5

. Student 객체가 처음 생성 되었을 때, 모습

_ 1

생성 및 소멸

. 생성 과정

  1. 메모리 공간 할당, 상속되는 클래스를 감안하여 메모리가 할당됨

    _ 2

  2. Derived 클래스 생성자 호출

    _ 3

    이 때, BBB(init j) 생성자가 호출만 될 뿐, 함수의 몸체( { … } )부분은 실행되지 않음

    상속하고 있는 AAA의 클래스 생성자가 우선 실행 된 후, 실행되어야 하기 때문임

    별 다른 선언(상속하고 있는 AAA 클래스의 어떤 생성자 호출할지 여부)가 없으면, Void 생성자 호출

    상속하고 있는 클래스의 특정 생성자 호출 시, 멤버 이니셜라이저 활용

  3. Base 클래스 생성자 실행

    1
    2
    3
    4
    5
    
    // 상속하고 있는 AAA 클래스의 어떠한 생성자를 호출해야 한다는 선언이 존재하지 않음
    // 따라서, AAA 클래스(Base 클래스의) void 생성자가 호출 및 실행 됨
    BBB(int j){			
       cout << " BBB(int j) call!" << endl;
    }
    
    1
    2
    3
    
    BBB(int j) : AAA(j) {				// j 인자를 받을 수 있는 AAA 클래스의 생성자를 호출!
       cout << " BBB(int j) call!" << endl; 
    }
    

    _ 4

  4. Derived 클래스 생성자 실행

    _ 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <cstring>

using std::endl;
using std::cout;

class AAA{
    public:
        AAA(){
            cout << "AAA() call!" << endl;
        }
        AAA(int i){
            cout << "AAA(int i) call!" << endl;
        }    
};

class BBB : public AAA{
    public:
        BBB(){
            cout << "BBB() call!" << endl;
        }
        BBB(int j){
            cout << "BBB(int j) call!" << endl;
        }    
};

int main(void)
{
    cout << "객체 1 생성" << endl;
    BBB bbb1;		// BBB 생성자 호출 -> AAA 생성자(void) 호출/실행 -> BBB 생성자 실행

    cout << "객체 2 생성" << endl;
    BBB bbb2(10);	// BBB 생성자 호출 -> AAA 생성자(int) 호출/실행 -> BBB 생성자 실행

    return 0;
}

6

상속 클래스 객체 생성에 따른 초기화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#i nclude <cstring>
using std::endl;
using std::cout;

class Person
{
    int age;
    char name[20];

    public:
        int GetAge() const {
            return age;
        }
        const char* GetName() const {
            return name;
        }
        Person(int _age=1, char* _name="none"){
            age = _age;
            strcpy(name, _name);
        }    
};

class Student: public Person
{
    char major[20];

    public:
        Student(int _age, char* _name, char* _major) : Person(_age, _name){
            // age = _age;          // compile error
            // strcpy(name, _name); // compile error
            strcpy(major, _major);
        }
        const char* GetMajor() const {
            return major;            
        }
        void ShowData() const {
            cout << "이름 :" << GetName() << endl;
            cout << "나이 :" << GetAge() << endl;
            cout << "전공 :" << GetMajor() << endl;
        }
};

int main(void)
{
    Student Lee(30, "Lee Yuna", "Computer");
    Lee.ShowData();

    return 0;
}

7

. 위 예제에서, compile error 로 주석 처리된 부분을 활성화 하기 위해서는, Person 클래스의 age,

​ name을 public 멤버로 설정해야함 → 정보은닉 문제 발생

. 때문에, 예제에서 처럼, : Person(_age, _name) 을 통해 생성자 명시적 호출

1
2
3
        Student(int _age, char* _name, char* _major) : Person(_age, _name){
            strcpy(major, _major);
        }

. 멤버변수 접근은 부모 클래스의 함수를 통해 수행하도록 함

1
2
3
4
5
6
        int GetAge() const {
            return age;
        }
        const char* GetName() const {
            return name;
        }

. 소멸 과정

  1. Derived 클래스 소멸자 실행
  2. Base 클래스 소멸자 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <cstring>

using std::endl;
using std::cout;

class AAA{
    public:
        AAA(){
            cout << "AAA() call!" << endl;
        }
        ~AAA(){
            cout << " ~AAA() call!" << endl;
        }
 
};

class BBB : public AAA{
    public:
        BBB(){
            cout << "BBB() call!" << endl;
        }
        ~BBB(){
            cout << " ~BBB() call!" << endl;
        }    
};

int main(void)
{    
    BBB bbb;
    return 0;
}

8

. 상속받은 클래스에 대한 객체 소멸 시, 상속하는 클래스의 소멸자 호출!

protected 멤버

protected 멤버는 외부에서 볼 때 private, 상속 관계에서는 public

protected 멤버는 상속 관계에서만 접근을 허용함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <cstring>

using std::endl;
using std::cout;

class AAA{
    private:
        int a;
    protected:
        int b; 
};

class BBB : public AAA{
    public:
        void SetData(){
            // a = 10; // private member, Compile Error
            b = 20; // protected member, working
        }    
};

int main(void)
{    
    AAA aaa;
    // aaa.a = 10; // private member, Compile Error
    // aaa.b = 20; // protected member, Compile Error

    BBB bbb;
    bbb.SetData();   

    return 0;
}

상속 형태

_ 6

. 각각의 상속은 본인보다 접근 권한이 넓은 것을 본인하고 동일하게 맞춤

상속 조건

IS-A 관계

_ 7

HAS-A 관계

_ 8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using std::endl;
using std::cout;

class Cudgel{
    public:
        void Swing(){ cout << "Swing a cudgel!" << endl; }    
};

class Police : public Cudgel
{
    public:
        void UseWeapon(){ Swing(); }
};

int main()
{
    Police p1;
    p1.UseWeapon();
    return 0;
}

9

HAS-A 관계와 유사한 역할을 하는 포함관계-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using std::endl;
using std::cout;

class Cudgel{
    public:
        void Swing(){ cout << "Swing a cudgel!" << endl; }    
};

class Police
{
    Cudgel cud;     // 클래스 객체를 멤버화
    public:
        void UseWeapon(){ cud.Swing(); }
};

int main()
{
    Police p1;
    p1.UseWeapon();
    return 0;
}

10

_ 9

HAS-A 관계와 유사한 역할을 하는 포함관계-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using std::endl;
using std::cout;

class Cudgel{
    public:
        void Swing(){ cout << "Swing a cudgel!" << endl; }    
};

class Police
{
    Cudgel* cud;     // 클래스 객체를 멤버화
    public:
        Police(){
            cud = new Cudgel;            
        }
        ~Police(){
            delete cud;
        }
        void UseWeapon(){ cud->Swing(); }
};

int main()
{
    Police p1;
    p1.UseWeapon();
    return 0;
}

11

_ 10

상속된 객체와 포인터

객체 포인터

해당 클래스의 객체 주소 값 뿐만 아니라, 이 클래스를 상속받는 Derived 클래스의 객체 주소 값도 저장 가능함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using std::endl;
using std::cout;

class Person{
    public:
        void Sleep(){
            cout << " Sleep " << endl;
        }
};

class Student : public Person{
    public:
        void Study(){
            cout << " Study " << endl;
        }
};

class PartTimeStd : public Student {
    public:
        void Work(){
            cout << " Work " << endl;
        }
};

int main(void){
    // ( Person* ) 는 객체 포인터로,
    // Person 클래스를 상속받는 클래스의 객체 주소 값도 저장할 수 있다
    Person* p1 = new Person;
    Person* p2 = new Student;
    Person* p3 = new PartTimeStd;

    p1->Sleep();
    p2->Sleep();
    p3->Sleep();
    
    return 0;
}

1

. 각 클래스의 관계

_ 1

객체 포인터 권한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using std::endl;
using std::cout;

class Person{
    public:
        void Sleep(){
            cout << " Sleep " << endl;
        }
};

class Student : public Person{
    public:
        void Study(){
            cout << " Study " << endl;
        }
};

class PartTimeStd : public Student {
    public:
        void Work(){
            cout << " Work " << endl;
        }
};

int main(void){
    Person* p3 = new PartTimeStd;

    p3->Sleep();

    // p3->Study(); // Error 원인
    // p3->Work(); // Error 원인
    
    return 0;
}

_1111

A 클래스의 객체 포인터는 A 클래스 타입 내에 선언된 멤버에만 접근 가능함

A 클래스를 상속받은 B 클래스의 경우, A 클래스 타입의 객체 포인터로 지정을 하였어도, A 클래스 타입 내에 선언된 멤버에만 접근이 가능하다

상속된 객체와 참조 관계

객체 레퍼런스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using std::endl;
using std::cout;

class Person{
    public:
        void Sleep(){
            cout << " Sleep " << endl;
        }
};

class Student : public Person{
    public:
        void Study(){
            cout << " Study " << endl;
        }
};

class PartTimeStd : public Student {
    public:
        void Work(){
            cout << " Work " << endl;
        }
};

int main(void){
    PartTimeStd p;
    Student& ref1 = p;
    Person& ref2 = p;

    p.Sleep();
    ref1.Sleep();
    ref2.Sleep();
    
    return 0;
}

2

. 레퍼런스는 이름을 하나 더 부여하는 것으로, 위 예제에서의 p 객체는 3개의 이름을 가지게 됨

. 레퍼런스를 통해 객체와 해당 클래스를 상속하는 Derived 객체도 참조가 가능함

객체 레퍼런스 권한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using std::endl;
using std::cout;

class Person{
    public:
        void Sleep(){
            cout << " Sleep " << endl;
        }
};

class Student : public Person{
    public:
        void Study(){
            cout << " Study " << endl;
        }
};

class PartTimeStd : public Student {
    public:
        void Work(){
            cout << " Work " << endl;
        }
};

int main(void){
    PartTimeStd p;
    p.Sleep();
    p.Study();
    p.Work();

    Person& ref = p;
    ref.Sleep();
    // ref.Study();    // Error
    // ref.Work();     // Error
    
    return 0;
}

. 위 예제의 ref.Study(), ref.Work() 의 경우 Person 클래스는 멤버로 Study와 Work를 지니지 않기 때문에 컴파일

​ 오류를 발생시킨다.

결론
1
2
3
4
5
6
1. PartTimeStd p;
   - PartTimeStd 상속받은 모든 클래스에 접근 가능(, public, protected)
2. PartTimeStd * p;
   - PartTimeStd Class에만 접근가능
3. PartTimeStd & p;
   - PartTimeStd Class에만 접근가능

정적, 동적 바인딩 (Static / Dynamic Binding)

정적 바인딩은 컴파일 시, 호출될 함수가 정해지도록 하는 방법

Base b; b.func(); 의 형태로 사용됨

동적 바인딩은 함수 호출 시, 상황에 따라 실제 호출되는 함수가 달라지는 방법

*Base b = new xxx; b->func(); 의 형태로 사용됨

오버라이딩 (Overriding)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using std::endl;
using std::cout;

class Base{
    public:
        void func(){			// Dervied의 func()에 의해 오버라이딩 됨
            cout << "BASE" << endl;
        }
};

class Dervied : public Base {
    public:
        void func(){			// Base의 func()을 오버라이딩 함
            cout << "Dervied" << endl;
        }
};

int main(void)
{
    Dervied d;
    d.func();

    return 0;
}

3

_ 2

Base 클래스에 선언된 형태의 함수를 Derived 클래스에서 다시 선언하는 것

이 때, Base 클래스의 함수는 가려지게 된다(Hiding)

오버라이딩 된 함수 호출

포인터 활용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using std::endl;
using std::cout;

class Base{
    public:
        void func(){
            cout << "BASE" << endl;
        }
};

class Dervied : public Base{
    public:
        void func(){
            cout << "Dervied" << endl;
        }
};

int main(void)
{
    // Dervied d;
    // d.func();

    Dervied* d = new Dervied;
    d->func();

    Base* b = d;	// Base 객체 포인터로, Derived 객체 포인터 d 의 값을 받음
    b->func();
  	
  	// 즉, 선언된 객체 포인터 타입에 따라 접근할 수 있는 함수가 정해짐
  
    delete b;
    
    return 0;
}

4

. Dervied 객체를 Dervied 포인터로 접근할 경우, Dervied 클래스 내에 선언된 멤버 함수만 접근할 수 있음

_ 3

. Dervied 객체를 Base 포인터로 접근할 경우 (Dervied는 Base 클래스를 상속받은 클래스이기 때문에, Base 포인터로 Dervied 객체 주소를 받을 수 있음) Base 클래스 내에 선언된 멤버 함수를 접근할 수 있음

. 이는, Derived 객체를 포인터 입장에서는 Base 객체로 판단하기 때문임

_ 4

범위 지정 연산자 활용

. 방법 1은 자주 쓰이는 방법이나, 방법 2는 클래스 디자인의 실패로 간주되어 자주 쓰이지 않는다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using std::endl;
using std::cout;

class Base{
    public:
        virtual void func(){
            cout << "BASE" << endl;
        }
};

class Dervied : public Base{
    public:
        void func(){ 
            Base::func();       // 방법 1
            cout << "Dervied" << endl;
        }
};

int main(void)
{    
    Base* b = new Dervied;
    cout << "첫 번째 시도입니다." << endl;
    b->func(); 

    cout << "두 번째 시도입니다." << endl;
    b->Base::func();            // 방법 2

    delete b;
        
    return 0;
}

멤버 함수의 가상 (Virtual) 선언

virtual 키워드를 함수 앞에 선언

해당 함수는 dynamic binding

만들어진 객체의 접근 형태에 따라, 사용할 함수를 선택하여 접근할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using std::endl;
using std::cout;

class Base{
    public:
        virtual void func(){
            cout << "BASE" << endl;
        }
};

class Dervied : public Base{
    public:
        void func(){
            cout << "Dervied" << endl;
        }
};

int main(void)
{
    // Dervied d;
    // d.func();

    Dervied* d = new Dervied;
    d->func();

    Base* b = d;
    b->func();

    Base * k = new Base;
    k->func();

    delete b;
    delete k;    
    
    return 0;
}

2

. Base 포인터로 접근하더라도, Base 클래스 내의 func()은 virtual 선언이 되어 있어, Base 클래스를 상속받은 Dervied 클래스의 func()이 호출되게 됨

_ 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using std::endl;
using std::cout;

class Base{
    public:
        virtual void func(){
            cout << "BASE" << endl;
        }
};

class FDervied : public Base{
    public:
        void func(){        // virtual void func();
            cout << "FDervied" << endl;
        }
};

class SDervied : public FDervied{
    public:
        void func(){        // virtual void func();
            cout << "SDervied" << endl;
        }
};

int main(void)
{    
    FDervied* fd = new SDervied;
    fd->func();

    Base* b = fd;
    b->func();

    delete b;
    
    return 0;
}

6

. 가상화 (virtual) 선언된 함수를 오버라이딩 하면, 최종적으로 오버라이딩한 함수만 Call을 받을 수 있게 됨

_ 6

_ 7

Virtual 소멸자

virtual 키워드는 가상함수, 소멸자 에 사용됨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <cstring>

using std::endl;
using std::cout;

class Base{
    char* str1;
    public:
        Base(char* _str1){
            str1 = new char[strlen(_str1)+1];
            strcpy(str1, _str1);
        }
  		~Base(){		// 소멸 시, 메모리 유출(누수) 발생 가능성 존재
        // virtual ~Base()	의 형태로 선언해야 함
            cout << " ~Base() call! " << endl;
            delete []str1;
        }
        virtual void ShowString(){
            cout << str1 << ' ';
        }
};

class Dervied : public Base {
    char* str2;
    public:
        Dervied(char* _str1, char* _str2) : Base(_str1){
            str2 = new char[strlen(_str2) + 1];
            strcpy(str2, _str2);
        }
        ~Dervied(){
            cout << " ~Dervied() call! " << endl;
            delete []str2;
        }
        virtual void ShowString(){
            Base::ShowString();
            cout << str2 << endl;
        }
};

int main()
{
    Base * b = new Dervied( "Hello", "Asia");
    Dervied * d = new Dervied( "Hello", "World");

    b->ShowString();		// Base의 ShowString()는 Virtual 이기 때문에, Dervied의 함수 호출
    d->ShowString();

    cout << "=============== 객체 소멸 =================" << endl;
    delete b;
    delete d;

    return 0;
}

8

. 실행 결과를 살펴보면, Dervied 객체는 2번 생성되었음에도 불구하고, 소멸자는 한 번만 호출되었음

. 포인터 b가 가리키는 객체는 Dervied 객체이지만, Base 타입의 포인터로 가리키고 있기 때문에, 컴파일러는 Base 객체로 인식함. 따라서 Dervied 객체의 소멸자는 호출되지 않음

_ 8

_ 9

해결 방법
1
2
3
4
        virtual ~Base(){
            cout << " ~Base() call! " << endl;
            delete []str1;
        }

9

  1. Base 클래스의 소멸자를 호출
  2. Base 클래스의 소멸자가 virtual 이기 때문에 Derived 클래스의 소멸자를 호출함
  3. Derived 클래스의 소멸자는 Base 클래스를 상속하고 있기 때문에, Base 클래스의 소멸자를 재호출

_ 10