본문 바로가기
Programming/C++

C++ 언어 기초 (11) - 상속; 기초/파생클래스, 접근 제어, final, name binding

by 롱일스 2020. 9. 3.
반응형

▣ 기초 클래스와 파생 클래스

 ●  클래스의 상속 (inheritance)

  ▷ 공통적인 멤버를 포함하는 유사한 유형의 클래스

  ▷ 일반화와 특수화를 통한 클래스 계층구조 설계

    공통적인 멤버로 구성된 클래스를 기초 클래스, 기초 클래스로부터 상속받는 클래스를 파생 클래스라고 한다.

    코드의 중복을 방지할 수 있다.

 ●  파생 클래스 선언

  ▷ 파생 클래스 선언 형식

class DClassName : visibilitySpec BClassName {
visibilitySpec_1:
   데이터 멤버 또는 멤버함수 리스트;
visibilitySpec_2:
   데이터 멤버 또는 멤버함수 리스트;
   ...
};

// DClassName: 파생 클래스 이름
// BClassName: 기초 클래스 이름
// visibilitySpec: 가시성 지시어

 

 ●  Person 클래스와 Student 클래스

  ▷ Person 클래스

  • 사람을 나타내는 클래스로 사람 객체는 '이름'을 가지고, 이름을 지정하거나 이름을 알릴 수 있다.
Person
-name:  string
+setName(n:string) : void
+getName( ) : string
+print( ) : void

Person.h

#ifndef PERSON_H_INCLUDED
#define PERSON_H_INCLUDED
#include <iostream>
#include <string>
using namespace std;

class Person{
    string name;
public:
    void setName(const string& n) {name = n;}
    string getName() const {return name;}
    void print() const {cout << name;}
};
#endif

 

  ▷ Student 클래스

  • 학생을 나타내는 클래스로 학생 객체는 사람의 기능을 '상속'받으면서 학교 이름을 지정하거나 저장된 학교 이름을 알리는 기능이 있다. 
Student
-school : string
+setSchool (s : string) : void
+getSchool ( ) : string
+print ( ) : void

파생클래스에서 기초클래스의 멤버를 재정의하는 것을 overriding이라고 한다. 여기는 print ( ) 함수를 Student 클래스에서 재정의하였다. 

Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <iostream>
#include <string>
#include "Person.h"

class Student : public Person {
    string school;
public:
    void setSchool(string s) {school = s;}
    string getSchool() const {return school;}
    void print() const { //멤버함수 재정의! overriding
        Person::print();
        cout << " goes to " << school;
    }
};
#endif

name이 Person.h에서 private하게 선언되었기 때문에 Student 클래스에서 사용할 수 없기 때문에 기초클래스의 멤버를 Person::print( )와 같이 사용해서 name을 출력받는다.

  ▷Person & Student 클래스 사용 (main.cpp)

#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;

int main()
{
    Person KIM;				//기초 클래스의 객체 선언
    KIM.setName("Kim");		//기초 클래스의 함수 호출
    Student RM;				//파생 클래스의 객체 선언
    RM.setName("RapMonster");//기초 클래스의 함수 호출
    RM.setSchool("Ilsan");	//파생 클래스의 함수 호출
    KIM.print();			//기초 클래스의 함수 호출
    cout << endl;
    RM.print();				//파생 클래스의 함수 호출
    cout << endl;		
    RM.Person::print();		//기초 클래스의 함수 호출
    cout << endl;
    return 0;
}

 

  ▷출력결과

 

 ●  클래스 계층

   클래스 계층 구조는 "트리 구조"와 "그래프 구조"로 나뉜다.

  ▷ 트리 구조

   가장 일반적인 형태지만, 부모 클래스가 둘 이상인 경우에는 사용하기 어렵다.

  ▷ 그래프 구조

    부모 클래스가 둘 이상인 경우에 사용하기 좋은 구조다.

 

 

▣ 파생 클래스의 생성자 및 소멸자

 ●  생성자 선언 형식

DClassName(fParameterList) : BClassName(bArgsList)
{
	... // 파생 클래스 생성자에서 추가되는 항목
}

// DClassName: 파생 클래스 생성자 - 파생 클래스 이름 사용
// BClassName: 기초 클래스 생성자 - 기초 클래스 이름 사용
// fParameterList: 파생 클래스 생성자 형식 매개변수 목록
// bArgsList: 기초 클래스 생성자에 전달할 인수 목록

 

 ●  생성자 및 소멸자의 실행 순서

  ▷ 생성자: 기초 클래스 생성자 --> 파생 클래스 생성자

  • 파생 클래스는 기초 클래스의 내용을 바탕으로 하고 있기 때문에 위 순서로 생성자가 동작한다. 

  ▷ 소멸자: 파생 클래스 생성자 --> 기초 클래스 생성

  • 기초 클래스의 속성이 제거되기 전에 이를 활용할 가능성이 있는 파생 클래스 객체를 제거해야 한다.

 

 ● 예시 - Person 클래스와 Student 클래스

Person
- name : string
+ Person ( n : string )
+ ~Person ( )
+ getName ( ) : string
+ print ( ) : void
Student
- school :  string
+ Student ( s: string )
+ ~Student ( )
+ getSchool ( ) : string
+ print ( ) : void

 

  ▷ Person.h

  • name 객체를 string으로 만들었기 때문에 소멸자 선언안해도 알아서 소멸되기 때문에 굳이 소멸자를 선언할 필요는 없다. (배열도 아니고..)
#ifndef PERSON_H_INCLUDED
#define PERSON_H_INCLUDED
#include <iostream>
#include <string>
using namespace std;

class Person{
    string name;
public:
    Person(const string& n){
        cout << "Person's constructor" << endl;
        name = n;
    }
    ~Person(){
        cout << "Person's destructor" << endl;
    }
    string getName() const {return name;}
    void print() const {cout << name;}
};
#endif

 

  ▷ Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <iostream>
#include <string>
#include "Person.h"

class Student : public Person {
    string school;
public:
    Student(const string& n, const string& s) : Person(n) {
        cout << "Student's constructor" << endl;
        school = s;
    }
    ~Student(){
        cout << "Student's destructor" << endl;
    }
    string getSchool() const {return school;}
    void print() const { //멤버함수 재정의! overriding
        Person::print();
        cout << " goes to " << school;
    }
};
#endif
  • Student(const string& n, const string& s) : Person(n)--> name에 n을 넣어야 하는데 Person클래스의 private한 value인 name에 접근할 수 없으니 Person의 생성자를 호출해서 Student 내의 Person의 name에 값이 전달된다.

  ▷ main.cpp

#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;

int main()
{
    Student RM("Namjoon", "Ilsan");
    cout << RM.getName() << " goes to "
         << RM.getSchool() << endl;
    return 0;
}

  ▷ 출력결과

출력 결과를 보면, 

  1. 기초 생성자 
  2. 파생 생성자
  3. 출력
  4. 파생 소멸자
  5. 기초 소멸자

순서로 동작하는 것을 확인할 수 있다.

 

 

▣ 접근 제어

 ●  가시성

  ▷ 가시성 지시어

가시성 지시어 공개 범위
private
(default)
- 소속 클래스의 멤버함수
- 친구 클래스의 멤버함수 및 친구함수
protected - 소속 클래스의 멤버함수
- 친구 클래스의 멤버함수 및 친구 함수
- 파생 클래스의 멤버함수
- 파생 클래스의 친구 클래스의 멤버함수 및 친구함수
public - 전 범위
  • 클래스의 경우에는 default가 private이고 구조체(struct)에서는 public이 default다.

 ●  가시성의 상속

  ▷ 기초클래스로부터 상속받은 멤버의 가시성

class DClassName: visibilitySpec BClassName {
   ....
};
  • visibilitySpec : 기초 클래스로부터 상속된 멤버가 파생 클래스의 멤버로서 가지게 되는 가시성을 제어한다.
    - private, protected, public을 가질 수 있다.
  • visibilitySpec에 지시된 것이 가시성의 상한이 되도록 제한된다.

  ▷ 기초클래스로부터 상속받은 멤버의 가시성

가시성 상속 지시어 B의 public 멤버는?  B의 protected 멤버는?
class D1 : private B{ }; D1의 private 멤버 D1의 private 멤버
class D2 : protected B{ }; D2의 protected 멤버 D2의 protected 멤버
class D3 : public B{ }; D3의 public 멤버 D3의 protected 멤버

 

  ▷ 예시 1. 파생클래스에서 기초 클래스를 public으로 상속받을 때

  • 기초 클래스 (Mom.h)
class Mom {
    int a;
protected:
    int b;
public:
    int c;
    int geta() const
        {return a;}
    void set(int x, int y, int z)
        {a = x; b = y; c = z;}
};

 

  • 파생 클래스 (Son.h)
class Son : public Mom {
public:
    int sum() const
        {return a + b + c;}   // a가 Mom 클래스에서 private하기 때문에 오류가 발생한다.
    void printbc() const
        {cout << b << ' ' << c;}
};

  -->  a가 Mom 클래스에서 private하기 때문에 오류가 발생한다.

  --> 아래와 같이 기초 클래스의 public한 멤버함수를 이용해서 데이터를 이용해야 한다.

class Son : public Mom {
public:
    int sum() const
        {return geta() + b + c;}
    void printbc() const
        {cout << b << ' ' << c;}
};

 

  • 실행 코드 (main.cpp)
int main() {
    Son id;
    id.a = 1;   // a: private
    id.b = 2;   // b: protected
    id.c = 3;   // c: public
    return 0;
}

위의 출력결과와 같이 오류가 발생한다. public으로 접근 가능한 id.c = 3; 명령만 수행 가능하고 나머지는 접근 권한이 없어서 오류가 발생한다.

 

  ▷ 예시 1. 파생클래스에서 기초 클래스를 protected로 상속받을 때

   기초 클래스는 위의 Mom.h로 동일하다고 하고 파생클래스 Son.h를 아래와 같이 변경하였을 때 어떤 변화가 있는지 살펴보겠다.

  • 파생 클래스 (Son.h)
class Son : protected Mom {
public:
    int sum() const
        {return geta() + b + c;}   // a: private, b&c: protected
    void printbc() const
        {cout << b << ' ' << c;}
};
  • 실행 코드 (main.cpp)

위와 동일하게 main.cpp를 실행하면  a는 private, b와c:는 protected가 되므로 모든 문장에서 오류가 발생하는 것을 볼 수 있다.

 

▣ 참고 사항

 ●  final 클래스란?

  ▷ 더 이상 상속 클래스를 정의할 수 없도록 만드는 방법이다.

  •   final로 선언하면 더 이상 파생 클래스를 정의할 수 없다.
class A {...};
class B : public A {...};
class C final : public B {...};
class D : public C {...};   // 에러!!
  • final은 키워드가 아니라 식별자(identifier)다.
  • 그래서 final 클래스 지정과 같이 특별히 정해진 위치에 사용하지 않는 경우 외에는 final 단어를 변수의 이름으로도 사용할 수는 있다. 권장하지는 않는다.

 

 ● 이름 은폐, name hiding

  ▷ 어떠한 영역에서 그 영역을 내포하는 영역에 선언된 이름을 다시 선언하면 바깥 영역의 이름이 은폐된다.

  ▷ 예시 1

#include <iostream>
using namespace std;

void f(int x){
    cout << "f(int x) --> " << x << endl;
}

void f(double x){
    cout << "f(double x) --> " << x << endl;
}

int main(){
    void f(int x);
    f(10);
    f(20.0);
    ::f(32.1);
}
  • main() 함수 내에서 함수 f(int x)를 다시 선언했기 때문에 f(20.0);에서 함수 f는 main() 함수 내에서 선언됐던 f(int x)에 의해 묵시적 형변환이 일어나서 20.0을 20으로 정수형으로 변환한 후 f(int x)함수에 의해 출력된다. main()함수 바깥에서 선언했던 f(double x)를 사용하고 싶으면 전역함수를 의미하는 ::를 함수 f 앞에 붙여서 ::f(32.1);과 같이 사용해야 f(double x) 함수를 호출할 수 있다.

  ▷ 예시 2

#include <iostream>
using namespace std;

void f(int x){
    cout << "f(int x) --> " << x << endl;
}

void f(const char* x){
    cout << "f(const char* x) --> " << x << endl;
}

int main(){
    void f(int x);
    f(10);
    f("abc"); // 에러
    ::f("abc");
}
  • f("abc")는 f(int x)를 호출할 수도 없는 형태이기 때문에 오류가 발생한다. 만약 ::f("abc");로 함수를 호출하면 아래와 같이 정상 출력되는 것을 확인할 수 있다.

 

  ▷ 클래스 계층구조에서 이름 은폐

  • 파생 클래스에서 기초 클래스에 선언된 이름을 다시 선언하면, 기초 클래스에 선언됐던 이름이 은폐된다.
#include <iostream>
using namespace std;

class A{
public:
    void f(double x){cout << "B::f() --> " << x << endl;}
};

class B : public A{
public:
    void f(int x){cout << "A::f() --> " << x << endl;}
};

int main(){
    B sonB;
    sonB.f(20.0);
    sonB.f(10);
    sonB.A::f(32.1);
}

 

  • 출력결과

728x90
반응형