1. 상속
1. 부모 클래스, 자식 클래스
- 부모 클래스(Base 클래스=Parent 클래스=Super 클래스=Existing 클래스): 훨씬 더 추상화된 클래스
- 자식 클래스(Derived 클래스=Child 클래스=Sub 클래스): 부모 클래스로부터 파생되어 만들어진 새로운 클래스, 부모 클래스의 모든 멤버 데이터&멤버 함수를 가진다. 더 실체화된 클래스(specailized)
- 상속: '자식 클래스'가 '부모 클래스'를 '상속받았다' 고 표현함.
- "is-a" 관계: Derived 클래스 is a Base 클래스 (역은 성립 안됨) → 자식 클래스가 부모 클래스에 '포함 된다' (부분 집합과 유사한 개념, 자식 클래스는 부모 클래스의 모든 멤버 데이터&멤버 함수를 가지니까!)
- Derived 클래스의 객체는 Base 클래스 객체를 채울 수 있다.
- Base 클래스의 객체는 Derived 클래스 객체를 채울 수 없다.
- → Derived 클래스에서 추가로 만들어진 정보들을 Base클래스가 채울 수 없기 때문이다!!
2. 문법
-형태
class Base {
private:
public:
};
class Derived : public Base {
privatd:
public:
};
- Derived클래스는 상속받은 Base클래스의 멤버 데이터, 함수 이외에도 추가적인 데이터, 함수를 클래스 내부에 만들 수 있다.
- class [자식 클래스 이름] : 상속 타입 [부모 클래스 이름]
- 여러 개의 부모 클래스를 상속받을 수 있음 ( class Derived : public Base1, public Base2, public Base3 {}; )
-접근 타입
Base type (부모 클래스의 타입) | inheritance type (상속 타입) | Derived type (자식 클래스의 타입) |
private | 상관 없음 | 접근 불가 (Base의 다른 접근 가능한 =public 메소드를 통해 접근 가능) |
protected | 상관 없음 | Base의 private 부분을 상속 받았지만 Derived내에서 접근 가능, Base입장에서는 private과 동일함 |
public | private/protected | private |
public | public | public |
- protected는 온전히 Base 클래스에서 상속만을 위해 존재하고 사용되는 타입!!
- base, inheritance 타입을 어떻게 설정하느냐에 따라 derived 타입이 결정됨 (위 표 암기!)
-protected 타입
- 클래스 외부에서는 클래스의 protected로 선언된 멤버 데이터, 멤버 함수에 접근할 수 없음. 단, 파생 클래스(derived class) 제외!!
- derived 클래스 내에서는 base 클래스의 protected 정보들이 마치 자신의 private 정보처럼 동작한다.
- 외부에서는 감춰진 정보이다. (접근 x)
3. 재사용성 & 구체화 (Reusability & Specialization)
- 기존의 클래스(Base 클래스)를 이용해 더 추가적이고 향상된 동작을 하는 새 클래스를 생성할 수 있다. 공통된 것을 재사용한다는 점에서 효율적이다.
- 또한 파생 클래스(Derived 클래스)에서 코드를 복제하지 않고도 기존 클래스(Base 클래스)를 활용할 수 있다.
- 상속을 할 때 기존 클래스, Base 클래스의 소스 코드는 건드리면 안된다.
- Base 클래스는 보다 추상화된 것 → 더 구체적인 Derived 클래스로 구현
2. 오버라이딩 (Overriding)
1. 오버라이딩이란?
함수의 이름, 입력값은 같으나 기능(body)이 달라질 때 오버라이딩을 이용한다! (상속 관계에서만 적용되는 개념)
Derived 클래스의 함수가 기존 Base 클래스의 가상 함수(virtual function)를 오버라이드(=대체, 재정의) 하도록 지정한다.
*) 오버로딩(overloading) vs 오버라이딩(overriding) = 이름은 같지만 입력 파라미터가 다름 vs 이름, 입력값 같지만 기능은 다름
*) 오버로딩 복습: 동일한 이름의 함수 또는 연산자에 대해 둘 이상의 정의를 지정할 수 있다. (각각 함수 오버로딩, 연산자 오버로딩) 오버로드 된 선언은 두 선언이 서로 다른 입력 파라미터와 다른 형태의 구현을 갖는 것을 제외하고 동일한 범위에서 같은 이름으로 선언된(declared) 선언(declaration)이다. (영어 직역으로 인해 매끄럽지 못함,,,)
2. virtual, override 지정자
- virtual : Base클래스에 사용되는 지정자로, 추후 이 클래스를 상속받을 파생 클래스=Derived 클래스가 virtual 메소드(virtual 지정자가 붙은 base클래스의 메소드)의 동작을 사용자 정의할 수 있도록 나타낸다.
- override : Base클래스의 virtual 메소드를 Derived 클래스에서 동작을 새롭게 정의할 때 사용된다. 파생 클래스가 가상 메소드를 재정의하지 않는 경우 기존 Base 클래스의 메소드의 동작을 변경하지 않고 그대로 상속하게 된다.
*) 2-1.~2-2. example
#include <iostream>
#include <vector>
using namespace std;
class Base {
public: //base type
void print_base() {
cout << "Base" << endl;
}
virtual void print() { cout << "Base::print()" << endl; }
};
class Derived : public Base { //inheritance type
public: //derived type
void print_derived() {
cout << "Derived" << endl;
Base::print_base(); //Base에만 정의된 함수지만 상속받아서 쓸 수 있음!
}
void print() override { cout << "Derived::print()" << endl; }
};
3. 동적 바인딩(dynamic binding)
1. 정적 바인딩(static binding)
: 컴파일할 때 객체가 어떤 메소드를 호출할지 미리 결정된 것, 변경 x
*) 3-1. 정적 바인딩 example
#include <iostream>
#include <vector>
using namespace std;
class Base {
public: //base type
virtual void print() { cout << "Base::print()" << endl; }
};
class Derived : public Base { //inheritance type
public: //derived type
void print() override { cout << "Derived::print()" << endl; }
};
int main() {
Base base;
Derived derived;
base.print();
derived.print();
cout << endl;
return 0;
}
- base.print(); → "Base::print()" 출력
- derived.print(); → "Derived::print()" 출력
2. 동적 바인딩(dynamic binding)
- 런타임(run-time)시 호출할 메소드가 상황에 맞게 결정, 변경된다.
- 이 때 부모 클래스에서 virtual로 지정돼 자식 클래스에서 오버라이딩된 함수만 사용 가능함!! (derived 자체 메소드는 사용 불가)
- 동적 바인딩은, 객체들이 내부적으로 vtable을 운영하도록 함으로써 상황(포인터가 어떤 객체를 가리키게 아느냐)에 따라 다양한 메소드들이 호출되도록 허용하는 것이다!!!
-형태
부모 클래스의 포인터에(부모 클래스의 객체를 가리키는 포인터) 자식 클래스 객체의 주솟값을 할당할 수 있다.
ex) Base* pBase = &derived;
-핵심 키워드
virtual & override : 안 쓰면 동적 바인딩을 할 수 없음 & 좀 더 명시적인 표현을 위함이다.
ex) 키워드를 쓰지 않으면 Base* pBase 부분에서 이미 정적 바인딩이 일어남 → 부모 클래스의 주솟값만 저장돼 print함수가 어떤 동작을 할지 결정됨.(부모 클래스의 Base::print() 실행)
-응용
다형성(polymorphism) 을 이용해 다양한 자식 클래스를 사용할 수 있다.
상황에 맞게 객체를 사용할 수 있다.
ex) 직선 그리는 도구를 클릭하면 마우스 커서에는 직선을 그려주는 객체의 주솟값이 들어가게 됨 (부모 클래스 포인터 = single interface로 사용 = 마우스 커서 = 위의 예제에서 pBase) (직선을 그려주는 객체 = 자식 클래스의 객체)
*) 3-2. 동적 바인딩 example
#include <iostream>
#include <vector>
using namespace std;
class Base {
public: //base type
virtual void print() { cout << "Base::print()" << endl; }
};
class Derived : public Base { //inheritance type
public: //derived type
void print() override { cout << "Derived::print()" << endl; }
};
int main() {
Base base;
Derived derived;
Base* pBase = &derived;
pBase->print(); //1.
pBase = &base;
pBase->print(); //2.
cout << endl;
return 0;
}
- 1. print() 결과: pBase는 자식 객체 derived를 가리킴, 따라서 Derived::print() 가 출력됨
- 2. print() 결과: pBase는 부모 객체 base를 가리킴, 따라서 Base::print() 가 출력됨
4. 다형성(polymorphism)의 응용
1. 다형성이란?
= 여러가지 다른 타입의 객체들을 위한 단일 인터페이스(single interface)를 제공해주는 기법!!
= 부모 클래스의 포인터(single interface)가 다양한 자식 클래스의 객체들을 상황에 맞게 운영하도록 하는 기법!!
= single interface to entities of different types
2. 다형성의 구현 방법 (*핵심)
- 1. 상속해서 부모 클래스의 포인터에 자식 클래스의 주솟값 집어넣기
- 2. 오버라이딩된 함수에 virtual/override 키워드 적어주기
*) 4. 다형성의 응용 example
#include <iostream>
#include <vector>
using namespace std;
class Base {
public: //base type
virtual void print() { cout << "Base::print()" << endl; }
};
class Derived : public Base { //inheritance type
public: //derived type
void print() override { cout << "Derived::print()" << endl; }
};
int main() {
Base base;
Derived derived;
Base* pBase;
// vec[0]<-0x00, vec[1]<-0x10, vec[2]<-0x20
vector<Base*> vec{ new Base, new Derived, new Base }; //1.
pBase = new Derived;
vec.push_back(pBase); //vec[3]<-0x30 추가
for (auto& elem : vec)
elem->print(); //2.
for (auto& elem : vec)
delete elem; //3.
return 0;
}
- 1. Base() 생성사 호출 시: 멤버 변수 초기화는 new Base(3, 4) / new Base() / new Base 세 가지 타입 상관 x!!
- 2. vec의 elem: 벡터 vec의 원소들인 elem은 각각 Base*타입(= Base클래스 객체들을 가리키는 포인터)이므로 "."대신 "->"로 메소드 사용하기!!
- 3. elem해제: 벡터 vec는 주솟값을 저장하는 벡터이기 때문에 원소들이 new 키워드로 동적 할당 되었음, 사용 후 반드시 delete로 할당 해제 시켜주기!!
5. 완전한 다형성
1. 순수가상함수 (pure virtual function)
: single interface는 사실 객체화될 필요가 없음, 단순히 껍데기만 제공해줘도 상관 없다. (인터페이스만 제공)
ex) 앞선 예제에서 Base클래스가 인터페이스로만 동작하고 실제 객체로 동작하지 않아도 된다.
- 메소드들이 부모 클래스 안에서 실제로 구현되지 않아도 됨. (virtual 키워드가 붙어서 추후 자식 클래스에서 오버라이딩 될 함수들)
- 단지 포인터로써 다른 자식 클래스의 객체를 가리키는 인터페이스로만 동작한다.
- 따라서, 부모 클래스에서는 메소드를 가상 함수로 만들고 자식 클래스에서 오버라이딩 한다! (굳이 추후 자식 클래스에서 오버라이딩 돼 동작이 달라질 함수라면, 굳이 부모 클래스에서 구현하지 않는다는 느낌)
*) 5-1. 순수가상함수 example / 부모 클래스를 protected로 선언한 경우
class Text {
protected: //자식 클래스도 접근 가능
string text;
public:
Text(string _t) : text(_t) {}
virtual string get() = 0; //순수가상함수 (pure virtual ft)
virtual void append(string _extra) = 0; //순수가상함수
};
class FancyText : public Text{
private:
string left_brac;
string right_brac;
string connector;
public:
FancyText(string _t, string _lb, string _rb, string _con)
: text(_t), left_brac(_lb), right_brac(_rb), connector(_con) {}
string get() override { //순수가상함수 오버라이드
return left_brac + text + right_brac; //text에 왼/오 괄호 추가
}
void append(string _extra) override { text + connector + _extra; }
};
class FixedText : public Text {
public:
FixedText() : text("FIXED"){}
void append(string _extra) override {
// NO OPERATATION 아무기능x _extra를 추가해도 추가 안됨
}
string get() override { return Text::text; }
};
- Text 클래스 (=부모 클래스) 의 순수가상함수는 get, append 두 가지
- 함수의 구현부를 {} 로 감싸지 않고 =0; 으로 아예 구현하지 않음.
- FancyText, FixedText (=자식 클래스) 에서 get, append 함수를 실제로 구현함. (FixedText의 append처럼 아무 기능을 추가하지 않고 { } 로 비워둬도 됨)
*) 5-1. example에서 순수가상함수가 아닌 경우 / 부모 클래스를 private으로 선언한 경우
class Text {
private:
string text;
public:
Text(string _t) : text(_t) {}
virtual string get() { return text; }
virtual void append(string _extra) { text += _extra; }
};
class FancyText : public Text{
private: //상속->Text의 string text도 가짐, 부모class에서 private이라 접근불가
string left_brac;
string right_brac;
string connector;
public:
//1.
FancyText(string _t, string _lb, string _rb, string _con)
: Text::Text(_t), left_brac(_lb), right_brac(_rb), connector(_con) {}
string get() override //함수 오버라이드->동적바인딩 가능
{
//2.
return left_brac + Text::get() + right_brac;
}
//3.
void append(string _extra) override { Text::append(connector + _extra); }
};
class FixedText : public Text {
public:
FixedText() : Text::Text("FIXED"){}
void append(string _extra) override {
// NO OPERATATION
}
//4.
//string get()메소드는 override되지 않음
};
- 1. 자식 클래스의 생성자: Text클래스를 상속받았기 때문에 멤버 변수로 string text도 가지고있음, 그러나 부모 클래스에서 private으로 생성돼 자식 클래스의 생성자 안에서는 접근 불가함
- → but, 초기화 리스트(initialization list)에서는 부모 클래스의 생성자 호출 가능! ex) Text::Text(생성자 입력값)
- 2. 함수 오버라이딩 시: get함수를 자식 클래스에서 오버라이딩할 때 text변수에는 접근할 수 없음 (바로 left_brac + text + right_brac 으로 접근 x)
- → 범위 지정 연산자 '::'와 Text클래스의 get함수를 이용해 Text::get() 로 접근한다! (Text의 get함수는 text를 리턴하기 때문)
- 3. 2번과 마찬가지로 Text클래스로부터 상속받은 text를 바로 사용하지 못함
- → Text클래스의 append함수를 사용함!
- 4. FixedText클래스도 Text클래스를 상속받았기 때문에 virtual 메소드인 get을 가짐.
- → 그러나, 굳이 get을 override를 이용해 오버라이딩 하지 않음! 그냥 부모 클래스의 get 기능 그대로 상속받아 사용하게 됨
2. 추상 클래스 (abstract class)
: 순수가상함수를 1개 이상 포함하는 클래스를 추상 클래스라 한다.
: main함수에서 인스턴스화 해 객체로 사용하지 않고, 포인터로 사용하는 클래스!
: 자식 클래스의 객체를 가리키는 껍데기(인터페이스)로만 작동한다.
: 객체화될 수 없다 (Text t1("Plain"); 처럼 인스턴스화 불가)
: 추상 클래스를 부모로 삼은 자식 클래스는 반드시 순수가상함수를 override를 통해 오버라이딩 해줘야 함 (순수가상함수가 자식 클래스에서는 어떤 동작을 할지 명확히!)
*) 1.~5-2. 상속 example
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Text {
protected:
string text;
public:
Text(string _t) : text(_t) {}
virtual string get() = 0; //순수가상함수
virtual void append(string _extra) = 0; //순수가상함수
};
class FancyText : public Text{
private:
string left_brac;
string right_brac;
string connector;
public:
FancyText(string _t, string _lb, string _rb, string _con)
: text(_t), left_brac(_lb), right_brac(_rb), connector(_con) {}
string get() override {
return left_brac + text + right_brac;
}
void append(string _extra) override { text + connector + _extra; }
};
class FixedText : public Text {
public:
FixedText() : text("FIXED"){}
void append(string _extra) override {
// NO OPERATATION
}
string get() override { return Text::text; }
};
int main() {
Text t1("Plain");
t1.append("A");
cout << t1.get() << endl;
FancyText t2("Fancy", "<<", ">>", "***");
t2.append("A");
cout << t2.get() << endl;
FixedText t3;
t3.append("A");
cout << t3.get() << endl;
cout << endl;
// up-casting (casting=형변환)
t1 = t3; // base class <- derived class 는 가능
// t2 = t1; derived class <- base class 는 불가능
cout << t1.get() << endl;
cout << endl;
//다형성(polymorphism) 응용
vector<Text*> vec{ new Text("Plain"),
new FancyText("Fancy", "(", ")", "-"),
new FixedText()
};
Text* pText;
pText = new FancyText("Fancy2", "(", ")", "^");
vec.push_back(pText); //vec[3]으로 element추가
//pText에는 new FancyText(...)의주솟값이 들어있었고 그게 vec[3]에 저장됨
//->pText는 그 객체에 대한 주솟값을 더이상 갖고 있지 x도 됨
pText = new Text("Plain2");
vec.push_back(pText);
//내부적으로 모든 메소드가 overriding돼있음
//->벡터관리시 썼던 simplified for문 써서 각각의 객체들 사용 가능
for (auto& elem : vec)
elem->append("A"); //vec의 elem각각은 포인터기 때문에 .이 아니라 ->
for (auto& elem : vec)
cout << elem->get() << endl; //get도 모든 객체가 갖고있음
for (auto& elem : vec)
delete elem;
return 0;
}
- 업 캐스팅(up-casting): derived class → base class 는 가능 (is-a 관계!!, base클래스의 모든 정보를 derived클래스가 채울 수 있기 때문)
- 다운 캐스팅(down-casting): base class → derived class 는 불가능 (base클래스가 derived클래스의 모든 정보를 채울 수 없기 때문, derived에는 추가적인 데이터가 만들어질 수 있으므로)
- 벡터 vec: Text* 타입이지만 원소들은 Text, FancyText, FixedText 클래스의 객체로, 한 가지가 아닌 다양함 → 헤테로지니어스(heterogeneous)타입임!! 다형성(polymorphism)으로 인해 벡터가 내부적으로 다른 데이터 타입의 원소들을 가지게 됨. (원래 벡터는 호모지니어스-homogeneous 타입)
- Text* pText: pText는 '객체'가 아닌 객체를 '가리키는' = '주솟값'을 저장하는 포인터 '변수'!!
- 벡터 vec의 각 원소들: new로 동적 할당된 객체들의 주솟값(혹은 메모리 내부 주솟값을 저장한 포인터)이기 때문에 반드시 delete로 할당 해제 필수!!
6. .cpp파일과 .h(헤더)파일
main함수가 아래로 내려가지 않도록 클래스를 정리할 필요가 있다.
- 클래스 메소드의 선언부/구현부 분리
- 클래스 헤더파일(.h)과 cpp파일(.cpp)를 만들어 각각에 선언부/구현부 삽입 (ctrl X+V)
- 선언부 → .h파일 / 구현부 → .cpp파일
- 순수가상함수는 메소드의 선언부/구현부가 나눠져있지 않으므로 추상클래스의 메소드가 모두 순수가상함수라면 구현부가 들어갈 .cpp파일은 지워줘도 됨!
- 함수를 나눌 때 선언부에만 virtual, override키워드 / 구현부에는 적으면 안됨
*) 헤더파일 이름이 CText, CFancyText, CFixedText인 것, 클래스 이름은 그대로
*) 위 예제의 Text.h 파일 (선언부, virtual 키워드)
#pragma once
#include <iostream>
#include <string>
using namespace std;
//class용 전용 file
class Text {
protected:
string text;
public:
Text(string _t) : text(_t) {}
virtual string get() = 0;
virtual void append(string _extra) = 0;
};
- #pragma once: 라이브러리 include시 반드시 한 번만 해야 함, pragma once를 적어줘야 실수로 같은 헤더나 라이브러리를 두 번 include시 두 번째 include를 무시함
*) FancyText.h 파일 (선언부, override 키워드)
#pragma once
#include <iostream>
#include <string>
#include "CText.h" //반드시 부모class헤더파일 include
using namespace std;
class FancyText : public Text {
private:
string left_brac;
string right_brac;
string connector;
public:
FancyText(string _t, string _lb, string _rb, string _con);
string get() override;
void append(string _extra) override;
};
- 자식 클래스의 헤더 파일에서는 반드시 부모 클래스의 헤더파일을 include 해야 함!!
- <>는 빌트인, " "는 사용자 정의
*) FancyText.cpp 파일
#include "CFancyText.h"
FancyText::FancyText(string _t, string _lb, string _rb, string _con)
: Text::Text(_t), left_brac(_lb), right_brac(_rb), connector(_con) {}
string FancyText::get() {
return left_brac + Text::text + right_brac;
} //Text::는 안써도 되지만 상속받았단 명시적표현을 위해
void FancyText::append(string _extra) {
Text::text += connector + _extra;
}
- .cpp파일에서는 자기 자신 클래스의 헤더파일.h을 include 해야 함!!
- 함수의 구현부를 쓸 때 함수의 이름 앞에 '::'로 범위(자기 자신 클래스)를 지정해줘야 함!!
*) FixedText.h 파일 (선언부, override 키워드)
#pragma once
#include <iostream>
#include <string>
#include "CText.h"
using namespace std;
class FixedText : public Text {
public:
FixedText();
string get() override;
void append(string _extra) override;
};
*) FixedText.cpp 파일
#include "CFixedText.h"
FixedText::FixedText() : Text::Text("FIXED") {}
string FixedText::get() { return Text::text; }
void FixedText::append(string _extra) {
//NO OP
}
*)sample codes: https://github.com/halterman/CppBook-SourceCode
'코딩 > 객체지향프로그래밍' 카테고리의 다른 글
[C++] 클래스와 객체 (2) (0) | 2020.08.04 |
---|---|
[C++] 클래스와 객체 (1) (0) | 2020.08.02 |
[C++] 포인터, 벡터, 배열 (3) (0) | 2020.07.30 |
[C++] 포인터, 벡터, 배열 (2) (0) | 2020.07.29 |
[C++] 포인터, 벡터, 배열 (1) (0) | 2020.07.27 |