[전공]

[명품 C++] 08 상속

danhan 2022. 6. 6. 13:54

출처 :

  • 명품 C++ Programming (저자 황기태)
  • 객체지향프로그래밍
  • 블뎁블뎁

C++ 에서 상속 inheritance

  • 클래스 사이의 상속, 객체 사이에는 상속 관계 없음
  • 기본 클래스의 속성과 기능을 파생 클래스에 물려줌
    • base class - derived class
    • 기본 클래스의 속성과 기능 + 자신만의 속성과 기능
  • 파생 클래스로 갈수록 클래스의 개념이 쿠체화
  • 다중 상속으로 클래스의 재활용성을 높임
    • 자바는 다중 상속이 없고 인터페이스 개념이 있음

상속의 목적 및 장점

  1. 간결한 클래스 작성
  2. 클래스 간의 계층적 분류 및 관리의 용이함
    • 상속은 클래스들의 구조적 관계 파악이 용이 하다
  3. 클래스 재사용과 확장을 통한 소프트웨어 생산성 향상
    • 앞으로 있을 상속에 대비해 클래스를 객체 지향적으로 설계 해야함
    • 다형성 - function overriding

상속 선언

// 파생클래스명:상속접근지정 기본클래스명
class Student: public Person {
	...
}

point 클래스를 상속받는 ColorPoint 클래스 만들기

#include <iostream>
#include <string>
using namespace std;

class Point {
	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();
};

void ColorPoint::showColorPoint() {
	cout << color << ";";
	// 파생클래스에서 기본 클래스 멤버 호출
	showPoint();         
	// x, y에 접근가능하다? No! private 멤버 접근 불가능
}

int main() {
	Point p;
	ColorPoint cp;
	// 파생클래스에서 기본클래스 멤버함수 호출
	cp.set(3,4);         
	cp.setColor("Red");
	cp.showColorPoint();

업 캐스팅 up-casting

  • 파생 클래스 포인터가 기본 클래스 포인터에 치환되는것
int main() {
	ColorPoint cp;
	ColorPoint * pDer = &cp;
	// ColorPoint의 주소를 담고 있지만 Point로만 봄
	// ColorPoint 멤버에 접근 불가능
	Point * pBase = pDer;

	pDer->set(3,4);
	pBase->showPoint();
	pDer->setColor("Red");
	pDer->showColorPoint();
	// 컴파일 오류 - 업캐스팅한 pBase로 파생클래스 ColorPoint에 접근불가능
	pBase->showColorPoint();

다운 캐스팅 down-casting

  • 기본 클래스의 포인터가 파생 클래스의 포인터에 치환되는것
int main() {
	ColorPoint cp;
	ColorPoint * pDer;
	Point * pBase = &cp;

	// 다운캐스팅 - ColorPoint 포인터에 넣기 위해 강제 타입 변환
	pDer = (ColorPoint *) pBase;
	PDer->setColor("Red");    // 정상 컴파일
	pDer->shokwColorPoint();  // 정상 컴파일
	// pDer로 private멤버인 color에는 접근 불가능

접근 지정자

  • private 멤버
    • 선언된 클래스 내에서만 접근가능
    • 파생 클래스에서도 기본 클래스의 private 멤버 직접 접근 불가
  • public 멤버
    • 선언된 클래스나 외부 어떤 클래스, 모든 외부 함수에 접근 허용
    • 파생 클래스에서 기본 클래스의 public 멤버 접근 가능
  • protected 멤버
    • 선언된 클래스에서 접근 가능
    • “파생 클래스에서만” 접근 허용

protected 멤버에 대한 접근

#include <iostream>
#include <string>
using namespace std;

class Point {
protected:
	int x, y; //한 점 (x,y) 좌표값
public:
	void set(int x, int y);
	void showPoint();
};

void Point::set(int x, int y) {
	this->x = x;
	this->y = y;
}

void Point::showPoint() {
	cout << "(" << x << "," << y << ")" << endl;
}

class ColorPoint : public Point {
	string color;
public:
	void setColor(string color);
	void showColorPoint();
	bool equals(ColorPoint p);
};

void ColorPoint::setColor(string color) {
	this->color = color;
}

void ColorPoint::showColorPoint() {
	cout << color << ":";
	showPoint(); // Point 클래스의 showPoint() 호출
}

bool ColorPoint::equals(ColorPoint p) {
	if(x == p.x && y == p.y && color == p.color) // ①
		return true;
	else
		return false;
}

int main() {
	Point p;
	p.set(2,3);
	p.x = 5;       // protected 접근불가능
	p.y = 5;       // protected 접근불가능
	p.showPoint(); 

	ColorPoint cp; // 파생 클래스의 객체 생성
	cp.x = 10;     // main은 클래스의 외부 protected 접근불가능
	cp.y = 10;     // protected 접근불가능
	cp.set(3,4);
	cp.setColor("Red");
	cp.showColorPoint();
}

상속 관계의 생성자와 소멸자 실행

  • 파생 클래스의 객체가 생성될 때 파생 클래스의 생성자와 기본클래스의 생성자가 모두 실행된다.
  • 기본 클래스의 생성자가 먼저 실행된 후 파생 클래스의 생성자가 실행된다.
  • 파생 클래스의 소멸자가 먼저 실행된 후 기본 클래스의 소멸자가 실행된다.
class A {
public:
	A() { cout << "생성자 A" << endl; }
	~A() { cout << "소멸자 A" << endl; }
};

class B : public A {
public:
	B() { cout << "생성자 B" << endl; }
	~B() { cout << "소멸자 B" << endl; }
};

class C : public B {
public:
	// 묵시적으로 기본 클래스의 기본 생성자를 호출하도록 컴파일됨.
	C() { cout << "생성자 C" << endl; }
	~C() { cout << "소멸자 C"<< endl; }
};

int main() {
	C c;
	return 0;
}

// 생성자A 생성자B 생성자C 소멸자C 소멸자B 소멸자A
// 생성은 기본 클래스부터! 소멸은 생성의 역순

기본 클래스에 기본 생성자가 없는경우

  • 파생 클래스에 “기본 생성자”가 있으면 기본 클래스에도 “기본 생성자”가 있어야 한다.
class A {
public:
	// 파생 클래스에서 기본 생성자를 호출했는데
	// 기본 생성자가 없음 - 컴파일 오류!
	A(int x) {
		cout << "매개변수생성자 A" << x << endl;
	}
};

class B : public A {
public:
	B() { 
		// 기본 생성자 A() 호출하도록 컴파일됨
		cout << "생성자 B" << endl;
	}
};

매개 변수를 가진 파생 클래스의 생성자는 묵시적으로 기본 클래스의 기본 생성자 선택

  • 파생 클래스의 매개 변수를 가진 생성자가 묵시적으로 기본 클래스의 기본 생성자 호출
  • 매개변수가 있는 생성자를 호출해도 별다른 지시가 없으면 A의 기본 생성자 호출
  • 컴파일러가 묵시적으로 B() : A() 로 기본 생성자를 위임생성자로 삽입함
class A {
public:
	A() { cout << "생성자 A" << endl; }
	A(int x) { cout << "매개변수생성자 A" << x << endl; }
};

class B : public A {
public:
	// A() 호출하도록 컴파일됨
	B() { cout << "생성자 B" << endl }
	// A() 호출하도록 컴파일됨
	// 매개변수가 있는 생성자를 호출해도 별다른 지시가 없으면 A의 기본 생성자 호출
	B(int x) { cout << "매개변수생성자 B" << x << endl; }
};

파생 클래스의 생성자에서 “명시적으로” 기본 클래스의 생성자 선택

  • 위임 생성자 사용 B(int x): A(x + 3)
class A {
public:
	A() { cout << "생성자 A" << endl; }
	A(int x) { cout << "매개변수생성자 A" << x << endl; }
};

class B : public A {
public:
	// A() 호출하도록 컴파일됨
	B() { cout << "생성자 B" << endl }
	// 파생 클래스의 생성자가 명시적으로 기본 클래스의 생성자를 선택하여 호출함
	B(int x): A(x + 3) {
		cout << "매개변수생성자 B" << x << endl;
	}
};

TV, WideTV, SmartTV 생성자 매개 변수 전달

#include <iostream>
#include <string>
using namespace std;

class TV {
	int size;
public:
	TV() { size = 20; }
	TV(int size) { this->size = size; }
	int getSize() { return size; }
};

class WideTV : public TV {
	bool videoIn;
public:
	WideTV(int size, bool videoIn) : TV(size) {
	this->videoIn = videoIn;
	}
	bool getVideoIn() { return videoIn; }
};

class SmartTV : public WideTV {
	string ipAddr;
public:
	// SmartTV 생성자가 호출되면 TV - WideTV - SmartTV 순으로 생성자 실행
	SmartTV(string ipAddr, int size) : WideTV(size, true) {
	this->ipAddr = ipAddr;
	}
		string getIpAddr() { return ipAddr; }
};

int main() {
	// 32 인치 크기에 "192.0.0.1"의 인터넷 주소를 가지는 스마트 TV 객체 생성
	SmartTV htv("192.0.0.1", 32);
	cout << "size=" << htv.getSize() << endl;
	cout << "videoIn=" << boolalpha << htv.getVideoIn() << endl;
	cout << "IP="htv.getIpAddr() << endl;
}

상속 지정 (중요!!!)

  • 상속 선언 시 public, private, protected 중 하나 지정
  • 기본 클래스의 멤버의 접근 속성을 어떻게 계승할 지 지정
    • public - 기본 클래스의 protected, public 멤버 속성을 그대로 계승
    • private - 기본 클래스의 protected, public 멤버 속성을 그대로 계승
      • A가 B를 private 상속 - B내부에서는 A의 protected, public 그대로 사용가능
      • 단, B를 상속받은 C에서는 A에 접근 불가능
      • 왜? A의 protected, public이 B에서는 private이 되어버려서
    • protected - 기본 클래스의 protected, public 멤버를 protected로 계승

private 상속 사례

class Base {
	int a;
protected:
	void setA(int a) { this->a = a; }
public:
	void showA() { cout << a; }
};

class Derived : private Base {
	int b;
protected:
	void setB(int b) { this->b = b; }
public:
	void showB() { cout << b; }
};

int main() {
	Derived x;
	x.a = 5;    // 기본 클래스 private 접근불가능
	x.setA(10); // private으로 계승하여 외부에서 접근불가능
	x.showA();  // private으로 계승하여 외부에서 접근불가능 
	x.b = 10;   // private은 외부 접근불가능
	x.setB(10); // protected는 외부 접근 불가능
	x.showB();  // 가능
}

protected 상속 사례

class Base {
	int a;
protected:
	void setA(int a) { this->a = a; }
public:
	void showA() { cout << a; }
};

class Derived : protected Base {
	int b;
protected:
	void setB(int b) { this->b = b; }
public:
	void showB() { cout << b; }
};

int main() {
	Derived x;
	x.a = 5;    // 기본 클래스 private 접근불가능
	x.setA(10); // protected로 계승하여 외부에서 접근불가능
	x.showA();  // protected로 계승하여 외부에서 접근불가능 
	x.b = 10;   // private은 외부 접근불가능
	x.setB(10); // protected는 외부 접근 불가능
	x.showB();  // 가능
}

상속이 중첩될 때 접근 지정 사례 (시험 문제!)

class Base {
	int a;
protected:
	void setA(int a) { this->a = a; }
public:
	void showA() { cout << a; }
};

class Derived : private Base {
	int b;
protected:
	void setB(int b) { this->b = b; }
public:
	void showB() {
		setA(5); // protectd이므로 파생 클래스내에서는 접근가능
		showA(); // public이므로 접근간으
		cout << b;
	}
};

class GrandDerived : private Derived {
	int c;
protected:
	void setAB(int x) {
	setA(x); // Derived에서 private이므로 접근불가능
	showA(); // Derived에서 private이므로 접근불가능
	setB(x); // 아직은 public이므로 클래스 내에서는 접근가능
	}
};

다중 상속 선언 및 멤버 호출 (시험문제)

class Adder {
protected:
	int add(int a, int b) { return a+b; }
};

class Subtractor {
protected:
	int minus(int a, int b) { return a-b; }
};

// 다중 상속 - 상속받고자 하는 기본 클래스를 나열한다.
class Calculator : public Adder, public Subtractor {
public:
	int calc(char op, int a, int b);
};

//캡슐화
int Calculator::calc(char op, int a, int b) {
	int res=0;
	switch(op) {
		// protected이므로 파생 클래스에서 접근가능
		case '+' : res = add(a, b); break;
		case '-' : res = minus(a, b); break;
	}
	return res;
}

int main() {
	Calculator handCalculator;
	// 객체 외부에서는 add(), minus()에 직접 접근 불가능
	// Calculator를 통해서 add(), minus()에 접근 = 캡슐화
	cout << "2 + 4 = "
	<< handCalculator.calc('+', 2, 4) << endl;
	cout << "100 - 8 = "
	<< handCalculator.calc('-', 100, 8) << endl;
}

다중 상속의 문제점 - 기본 클래스 멤버의 중복 상속

  • Base의 멤버가 이중으로 객체에 삽입되는 문제
  • 동일한 x를 접근하는 프로그램이 서로 다른 x에 접근하는 결과를 낳게되어 실행 오류

가상 상속

  • 다중 상속으로 인한 기본 클래스 멤버의 중복 상속 해결
  • 파생 클래스의 선언문에서 기본 클래스 앞에 virtual 선언
  • 파생 클래스의 객체가 생성될 때 기본 클래스의 멤버는 오직 한번만 생성
  • 기본 클래스이 멤버가 중복하여 생성되는 것을 방지
class In : virtual public BaseIO