본문 바로가기

코딩/객체지향프로그래밍

[C++] 상속

출처: http://bravo.etoday.co.kr/view/atc_view.php?varAtcId=4606

 

 


 

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함수가 아래로 내려가지 않도록 클래스를 정리할 필요가 있다.

  1. 클래스 메소드의 선언부/구현부 분리
  2. 클래스 헤더파일(.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

반응형