상속 (Inheritance) 는 객체 지향 언어의 가장 중요한 특성 중 하나입니다. 상속을 통해 소프트웨어의 재사용이 가능해지고, 동적 바인딩을 통해 객체 지향 프로그래밍을 할 수 있고 계층적으로 프로그램을 관리할 수 있습니다. C++ 에서의 상속은 두 클래스 사이의 부모-자식(기본-파생) 상속 관계를 의미합니다.
상속의 정의는 위 링크를 참조해주세요. 예제 코드 위주로 상속을 알아보겠습니다.
#include <iostream>
#include <string>
using namespace std;
class Point {
int x, y;
public:
// void set(int x = 0, int y = 0) : x(x), y(y) {}
void set(int x, int y) { this->x = x; this->y = y; }
void showPoint() { cout << "(" << x << "," << y << ")" << endl; }
};
class ColorPoint : public Point {
string color;
public:
void setColor(string color) { this->color = color; }
void showColorPrint(); // Point의 showPoint() 호출
};
void ColorPoint::showColorPrint() {
cout << color << ":";
showPoint();
}
int main() {
Point p; // 없어도 수행된다. 파생클래스 객체에서 기본 클래스 객체 생성하지 않고 접근 가능
ColorPoint cp;
cp.set(3, 4); // 기본 클래스 멤버 호출
cp.setColor("Red"); // 파생 클래스 멤버 호출
cp.showColorPrint(); // 파생 클래스 멤버 호출
}
// 출력 : Red(3,4)
점의 좌표를 나타내는 Point 라는 기본 클래스(부모 클래스)가 있고, 그를 상속 받은 ColorPoint 라는 파생 클래스(자식 클래스)가 있습니다. 위 예시를 통해 클래스를 상속받는 방법에 대해 알 수 있습니다.
class ColorPoint : public Point {}
class 기본클래스(부모클래스) : 접근지시자 파생클래스(자식클래스) {}
아래와 같은 문법으로 상속 받는 파생 클래스를 생성할 수 있습니다. 이 때 접근지시자는 클래스 내와 동일하게 public, private, protected 3가지를 사용할 수 있는데 이는 맨 아래에서 다시 설명할 예정이니 우선은 public 으로 사용하겠습니다.
파생 클래스는 기본 클래스의 멤버 변수와 멤버 함수를 모두 상속받습니다. 파생 클래스 객체 내에도 분명히 x와 y private 변수가 있습니다. 그러나, 파생 클래스에서 바로 기본 클래스의 private 멤버로 접근할 수 있을까요? 정답은 접근 할 수 없다. 입니다. 파생 클래스를 통해 생성한 객체에서는 기본 클래스의 private 멤버 변수에 직접 접근할 수 없고, 기본 클래스의 public 멤버 함수 등을 통해 접근해야만 합니다.
업 캐스팅 / 다운 캐스팅 ( Up-casting / Down-Casting )
파생 클래스(자식 클래스)는 상속을 통해 기본 클래스를 확장합니다. 파생 클래스의 객체에는 기본 클래스에서 선언된 멤버들 + 파생 클래스에서 선언한 멤버들 이 모두 존재하기 때문에, 파생 클래스 객체를 기본 클래스의 포인터나 파생 클래스의 포인터로 모두 가리킬 수 있습니다.
말이 좀 어려운 것 같은데, 파생 클래스란 ( 기본 클래스 + 파생 클래스 ) 가 되고, 따라서 ( 기본 클래스 포인터 or 파생 클래스 포인터 ) 로 접근이 된다는 소리입니다. 그러면 각각의 포인터로 각각의 객체에 접근 하는 경우는 총 4가지가 됩니다.
1. 기본 클래스 포인터 → 기본 클래스 객체
2. 파생 클래스 포인터 =(명시적 형 변환)기본 클래스 포인터 ( 다운 캐스팅 )
3. 기본 클래스 포인터 = 파생 클래스 포인터 ( 업 캐스팅 )
4. 파생 클래스 포인터 → 파생 클래스 객체
원래 포인터가 가르켜야할 객체를 가리키는 것에는 따로 이름이 있지 않습니다. int 형 포인터가 int 를 가리키는 건 이상한게 아니잖아요? int 형 포인터로 char 형을 가리키는 건 안됩니다. 그런데 상속 관계에 있는 클래스에는 이런 현상들이 가능하다는 것이 바로 캐스팅입니다.
이런 걸 왜 하느냐는 차후에 배울 overriding 이라는 개념 때문입니다.
업 캐스팅
업 캐스팅이란 파생 클래스의 객체를 기본 클래스의 포인터로 가리키는 것입니다. 업 캐스팅은 파생 클래스의 객체를 기본 클래스의 객체처럼 다룰 수 있게 만들어줍니다.
int main(){
ColorPoint cp; // 파생 클래스 객체
ColorPoint* pDer = & cp; // 파생클래스 객체 포인터 선언 및 초기화
Point* pBase = pDer; // 업 캐스팅. 기본 클래스 객체 포인터 선언 및 파생클래스 객체 주소로 초기화
pDer->set(3, 4); // 파생 클래스 포인터로 파생 클래스 객체 접근
pBase->showPoint(); // 기본 클래스 포인터로 파생 클래스 객체 접근
pDer->setColor("Red"); // 파생 클래스 포인터로 파생 클래스 객체 접근
pDer->showColorPoint(); // 파생 클래스 포인터로 파생 클래스 객체 접근
// pBase->showColorPoint(); // 컴파일 오류.
// 기본 클래스 포인터로는 파생 클래스 객체 중 기본 클래스의 public 멤버만 접근 가능
}
이 때 기본 클래스의 포인터로 접근이 가능한 범위는 파생클래스의 전체 멤버가 아닌, 오직 기본 클래스의 public 에 해당하는 멤버에만 접근할 수 있고, 파생 클래스의 포인터로는 기본 클래스의 private 멤버를 제외한 나머지를 모두 접근할 수 있습니다. 마치 파생 클래스를 기본 클래스처럼 사용하는 것이 업 캐스팅입니다. 전체 코드 및 출력은 아래와 같습니다.
// 업 캐스팅 (기본 클래스 포인터로 파생 클래스 객체)
#include <iostream>
#include <string>
using namespace std;
class Point {
protected:
int x, y;
public:
void set(int x, int y) { this->x = x; this->y = y; }
void showPoint() { cout << "(" << x << "," << y << ")" << endl; }
};
class ColorPoint : public Point {
string color;
public:
void setColor(string color) { this->color = color; }
void showColorPoint(); // Point의 showPoint() 호출
};
void ColorPoint::showColorPoint() {
cout << color << ":";
showPoint();
}
int main() {
ColorPoint cp; // 파생 클래스 객체
ColorPoint* pDer = &cp; // 파생클래스 객체 포인터 선언 및 초기화
Point* pBase = pDer; // 업 캐스팅. 기본 클래스 객체 포인터 선언 및 파생클래스 객체 주소로 초기화
pDer->set(3, 4); // 파생 클래스 포인터로 파생 클래스 객체 접근
pBase->showPoint(); // 기본 클래스 포인터로 파생 클래스 객체 접근
pDer->setColor("Red"); // 파생 클래스 포인터로 파생 클래스 객체 접근
pDer->showColorPoint(); // 파생 클래스 포인터로 파생 클래스 객체 접근
// pBase->showColorPoint(); // 컴파일 오류.
// 기본 클래스 포인터로는 파생 클래스 객체 중 기본 클래스의 public 멤버만 접근 가능
}
이 때 이해가 좀 어려울 수 있습니다. ColorPoint 객체는 ColorPoint 클래스 타입인데 어떻게 Point 타입 포인터로 이를 접근할 수가 있는거지? 그 답은 ColorPoint 객체는 ColorPoint 타입이기도 하지만, 상속 받은 Point 타입이기도 하기 때문입니다. 다형성을 이용해 코드 재사용성을 높이기 위해 업 캐스팅을 사용하는데요, 지금은 이런 게 있구나 정도로 넘어가시고 추후 오버라이딩과 동적 바인딩에서 이 업 캐스팅이 많이 활용되게 됩니다. 이 때 업 캐스팅한 기본 클래스의 포인터로는 기본 클래스의 멤버에만 접근할 수 있습니다. 노란색 부분이 다운 캐스팅이 필요한 이유입니다.
다운 캐스팅
업 캐스팅은 기본 클래스의 포인터로 파생 클래스를 접근하는 것을 말했죠. 그 반대로 생각해보면 파생 클래스의 포인터로 기본 클래스의 객체를 가리키는 것이 다운 캐스팅이 될 것 같지만 실제로는 조금 다릅니다.
다운 캐스팅이란 엄밀하게는 기본 클래스 포인터가 가리키는 객체를 파생 클래스의 포인터로 가리키는 것 을 말합니다. 다른 말로 다운 캐스팅은 기본 클래스에서 파생 클래스로의 형 변환을 의미합니다. 하지만 위 정의는 너무 이해하기가 어렵죠. 다운 캐스팅이란, 업 캐스팅했던 파생 클래스 객체를 다시 파생 클래스로 되돌리는 작업 이라고 생각하셔도 좋습니다.
다운 캐스팅은 필수적으로 명시적 형 변환이 필요합니다. 파생클래스객체 → 기본클래스객체 형태의 업 캐스팅은 어차피 상위 클래스가 하위 클래스를 가리키는 것이기 때문에 컴파일러가 암묵적인 형 변환을 해줍니다. 그러나 다운 캐스팅은 수 많은 파생클래스 중에서 해당 객체가 어떤 타입인지 명시를 해주지 않는다면 에러를 발생시킬 수 있기 때문입니다.
예를 들어 "휴대폰" 이라는 기본 클래스로 2개의 파생 클래스를 만들었다고 생각해봅시다. 하나의 파생 클래스는 "갤럭시" 이고 하나의 파생 클래스는 "아이폰" 이에요. 내가 갤럭시 폰이 있고 아이폰이 있는데, 이를 전부 휴대폰 이라고 생각하겠다 라는 것이 업 캐스팅입니다. 이 때 우리가 휴대폰을 다시 "갤럭시"나 "아이폰"으로 돌리는 것이 다운 캐스팅이라고 생각할 수 있는데, 이 때의 작업을 다운 캐스팅이라 합니다. 다운 캐스팅을 뭐로 하느냐에 따라 기종이 다른 것이 되겠죠?
갤럭시나, 아이폰이나 일단 둘 다 휴대폰 이라는 큰 틀에 속해있습니다. 갤럭시 → 휴대폰, 아이폰 → 휴대폰으로 올리는 것이 업 캐스팅이고 그 반대로 휴대폰→갤럭시 or 휴대폰→아이폰 이 다운 캐스팅인데, 이를 위해서는 무엇에 대한 다운 캐스팅인지 명시해야할 필요가 있다는 것이죠.
업 캐스팅을 하게 되면, 파생 클래스의 멤버는 사용하지 못하고 기본 클래스에 정의된 멤버만 호출할 수 있습니다. 그런데 파생 클래스의 고유 기능을 써야할 때가 있겠죠? 그럴 때 잠시 다운캐스팅을 해서
기본클래스포인터->파생클래스객체 의 형태에서 파생클래스포인터->파생클래스객체 형태로 변환을 해주는 겁니다.
class Base {
public:
int baseVar;
};
class Derived : public Base {
public:
int derivedVar;
};
이런 코드가 있다고 가정할 때, 기본 클래스 포인터를 사용하여 파생 클래스의 객체를 가리키는 것은 오직 "기본 클래스에 선언된" baseVar 에만 접근가능하고, 아래의 derivedVar 에는 접근할 수 없습니다.
int main() {
ColorPoint cp;
ColorPoint* pDer;
Point* pBase = &cp; // 업캐스팅
pBase->set(3, 4);
pBase->showPoint();
pDer = (ColorPoint*)pBase; // 다운 캐스팅
pDer->setColor("Red");
}
위 같은 과정을 통해 다운 캐스팅이 일어납니다. 다운 캐스팅을 하기 전에는 파생클래스의 멤버인 setColor 함수를 호출할 수 없지만, 다운캐스팅을 통해 파생클래스 고유의 기능을 사용할 수 있는 것이죠.
상속의 종류(public, protected, private)
상속을 받을 때도 접근 지정자를 통해 상속을 합니다. 이 때 public, protected, private 을 모두 사용할 수 있습니다.
class Derived : public Base { ... }
class Derived : protected Base { ... }
class Derived : private Base { ... }
public 상속을 사용하면 Base 에 선언된 멤버들의 접근 지정을 그대로 유지한 채 Derived 의 멤버로 확장됩니다. public 은 public 으로, protected 는 protected 로, private 은 private 으로 그대로 확장됩니다.
protected 상속을 사용하면 Base 에 선언된 멤버들의 접근 지정 중 public 이 있다면 protected 접근 지정으로 변경되어 Derived 의 멤버로 확장됩니다.
private 상속을 사용하면 Base에 선언된 멤버들을 모두 privte 접근지정으로 변경하여 Derived의 멤버로 확장합니다.
즉 상속에 있어서 접근 지정을 사용하면 상속 받는 멤버들의 최소 범위? 를 그 접근 지시자로 맞춰주는 것이고, 아니면 그대로 가져오게 됩니다.
'C++ > C++ 기초' 카테고리의 다른 글
[C++] 파생 클래스와 기본 클래스의 생성자/소멸자 호출 관계 + 가상 소멸자 (0) | 2023.07.17 |
---|---|
[C++] 가상 함수와 오버라이딩 ( Virtual function and overriding ) (0) | 2023.07.17 |
[C++] 연산자 중복 (Operator overloading) + 프렌드(friend) 개념 (0) | 2023.07.13 |
[C++] 함수 중복과 디폴트 매개변수 (Function overloading and default parameter) (0) | 2023.07.11 |
[C++] static 멤버 / const 멤버 (0) | 2023.07.10 |