AcceleratedC++/Chapter12 AcceleratedC++/Chapter14


1. Chapter 13 Using inheritance and dynamic binding

13장에서는 4장에서 만들었던 성적 계산 프로그램을 학부생, 대학원생에 대해서 동작하도록 기능을 확장하는 프로그램을 통해서 상속과 다형성(동적바인딩)의 개념을 배운다.
(9.6절에 있는 기존의 프로그램을 이용한다.)

1.1. 13.1 Inheritance

상속(inheritance)
몇 가지 추가사항을 제외하면 한클래스와 다른 클래스가 동일한 경우가 많다는 데에 착안해서 나온 개념이다.
이 프로그램의 경우 기존 객체와 다른 부분은 동일하지만 대학원생의 성적을 다루는 경우에는 논문과 관련된 점수가 포함된다는 가정을 하고 만들어진다.

대학원생과 학부생의 성적의 공통적 요소만을 표현한 Core Class
~cpp 
class Core {
public:
	Core();
	Core(std::istream&);
	std::string name() const;
	std::istream& read(std::istream&);
	double grade() const;
private:
	std::istream& read_common(std::istream&);
	std::string n;
	double midterm, final;
	std::vector<double> homework;
};

대학원생에 관련된 점을 추가한 Grad class
~cpp 
class Grad:public Core {	// 구현(implementation)의 일부가 아닌 인터페이스(interface)의 일부로서 상속받는다는 것을 나타냄.
public:				// Core의 public요소를 그대로 public 요소로 받는다.
	Grad();
	Grad(std::istream&);
	double grade() const;
	std::istream& read(std::istream&);
private:
	double thesis;		// 논문관련 점수를 저장하는 멤버변수
}
Grad 클래스는 Core로 부터 파생되었다(Derived from), 상속받았다(inherits from), 혹은 Core는 Grad의 base class 이다 라는 표현을 사용한다.
상속받은 클래스는 그 부모클래스의 생성자, 소멸자, 대입연산자를 제외한 그외의 모든 클래스의 요소를 물려받는다.
또한 파생 클래스는 부모 클래스의 메소드를 재정의 하여서 자신에게 맞도록 수정하여 동작하는 것을 허용한다.

1.1.1. 13.1.1 보호정책(protection)에 대해서 다시 살펴보기

private 보호 레이블로 지정된 멤버는 그 클래스 자체, friend 함수를 통해서만 직접적으로 접근이 가능하다. 이 경우 상속된 클래스에서는 부모 클래스의 private 멤버로의 접근이 필요한데 이럴때 protected라는 키워드를 사용하면 좋다.
protected 레이블로 지정된 멤버들은 자식 클래스에서 직접적인 접근이 가능다. 그러나 클래스의 외부에서는 접근이 안되기 때문에 캡슐화의 장점을 유지시킬 수 있다.
~cpp 
class Core {
public:
	Core();
	Core(std::istream&);
	std::string name() const;
	double grade() const;
	std::istream& read(std::istream&);
protected:
	std::istream& read_common(std::istream&);
	double midterm, final;
	std::vector<double>homework;
private:
	std::string n;
 

1.1.2. 13.1.2 연산

구현해야할 부분.
Core, Grad의 생성자 4가지. 각기의 클래스에 따라 다르게 행동하게 되는 read, grade 함수. Core 클래스의 name, read-common 함수.
Core class의 기본 구현
~cpp 
string Core::name() const { return n; }

double Core::grade() const {
	return ::grade(midterm. final, homework);
}

istream& Core::read_common(istream& in) {
	in>>n>>midterm>>final;
	return in;
}

istream& Core::read(istream& in) {
	read_common(in);
	read_hw(in, homework);
	return in;
}
 
Grad::read 함수의 오버로딩
~cpp 
istream& Grad::read(istream& in) {
	read_common(in);
	in >> thesis;
	read_hw(in, homework);
	return in;
}
 
상기의 클래스는 Grad의 멤버 함수로 부모 클래스의 read_common, read_hw의 함수를 그대로 상속받았다는 것을 가정한다.
이를 명시적으로 표현하면 다음과 같이 표현할 수 있다.

~cpp 
istream& Grad::read(istream& in) {
	Core::read_common(in);
	in >> thesis;	// thesis는 Core가 아니라 Grad의 멤버 변수이므로 범위 지정 연산자를 사용해서는 안된다.
	Core::read_hw(in, Core::homework);
	return in;
}
 

thesis가 적용된 점수를 리턴하는 Grad::grade() 함수
~cpp 
double Grad::grade() const {
	return min(Core::grade(), thesis);		// min()은 <algorithm>에 정의된 함수이다.
}
 
Core::grade()를 사용하지 않고 grade()를 사용하게 되면 Grade:grade()를재귀적으로 호출하여 어떤 결과를 리턴할지 예상하지 못한다.

1.1.3. 13.1.2 상속 및 생성자

파생 클래스의 생성단계
* 전체 객체에 대한 공간을 할당
* 기본 클래스 생성자 호출, 기본클래스 공간 초기화
* 생성자의 초기설정자( ): { 사이에 존재하는 것들 )를 이용해서 파생클래스의 멤버 초기화
* 파생 클래스의 생성자의 본체를 실행한다.

~cpp 
class Core {
public:
	Core():midterm(0), final(0) {}
	Core(std::istream& is) { read(is); }
};

class Grad:public Core {
public:
	Grad():thesis(0) {}
	Grad(std::istream& is) { read(is); }
};
 
Grad의 생성자는 Core의 생성자가 midterm, final을 초기화 한다는 가정하에서 thesis만을 초기화하고 있습니다. 이러한 초기화는 암묵적으로 행하여진다.
마찬가지로 Grad(std::istream&)을 이용해서 객체를 초기화할 때에도 부모 객체의 디폴트 생성자로 먼저 기존의 부분을 초기화하고, Grad::read(istream&)를 통해서 각 요소의 값을 초기화하게 된다.

1.2. 13.2 Polymorphism and virtual functions

~cpp 
bool compare(const Core& c1, const Core& c2) {
	return c1.name() < c2.name();
}
상기의 함수는 sort에 의해서 각 요소의 판단식으로 사용되는 함수이다. 이 함수는 부모객체인 Core 객체 뿐만아니라, 자식 객체인 Grad객체도 대입하여 사용하는 것이 가능하다.
~cpp 
Grad g(cin);
Core c(cin);
compare(g, c);		// Grad, Core레코드를 비교한다.
Grad 클래스가 사용가능한 이유
비록 함수가 요구하는 인자값은 Core 클래스 이지만 Grad는 Core를 기반으로해서 파생된 클래스이기 때문에 이 경우 name();를 호출하게 되면 g 객체의 Core::name() 부분이 호출된다.
다시 말해서 Grad가 Core의 자식 클래스 이므로 Grad객체를 통해서 Core클래스의 함수를 바인딩시켜 사용하는 것이 가능하다는 뜻이다. (대신에 이 함수의 안에서는 Grad의 Core 의 요소들만을 취한다.)

1.2.1. 13.2.1 객체의 타입을 모르는 상태에서 값을 얻기

만약 이름이 아니라 최종 성적을 가지고 비교를 하고 싶을 경우를 다루게 된다.

grade와 유사한 기능을 하는 compare_grade 함수
~cpp 
bool compare_grade(const Core& c1, const Core& c2) {
	return c1.grade() < c2.grade();
}
 
만약 위 함수에 인자로 전달된 객체가 Grad객체라면 그 객체에서 호출되는 grade는 Core::grade() 이어서는 안된다. 그렇게 호출될 경우 논문 점수가 적용되지 않은 성적를 리턴하기 때문이다. 따라서 Grad::grade() 의 함수를 호출해야 할 것이다.
이런 행동을 함수에 전달되는 현재의 인자가 무엇이냐에 따라서 다르게 결정되어야 하므로 실행시(runtime)에 결정되어야 할 것이다.

동적바인딩을 위한 virtual키워드
~cpp 
class Core {
public:
	virtual double grade() const;	// virtual 이 추가됨.
};
 
virtual 키워드로 지정된 함수는 실제로 함수가 호출될때 그 객체의 이름 범위에 존재하는 함수를 호출하는 것이 가능하다.

1.2.2. 13.2.2 동적 바인딩(Dynamic binding)

동적 바인딩이 수행되기 위해서는 전달인자로 전달된 값이 포인터, 참조이어야 가능하다.
~cpp 
bool compare_grades(Core c1, Core c2) {
	return c1.grade() < c2.grade();
}
 
상기의 경우 Grad 객체를 인자로 전달할 경우 Grad객체의 Core객체의 요소만 복사되어 함수의 인자로 전달되기 때문에 Core::grade()가 호출되어서 정적바인딩(static binding)이 수행된다.

만약 일반형의 변수로 virtual함수를 호출하면 객체가 항상 한가지 타입으로만 존재하기 때문에 정적바인딩이 된다. 그러나 포인터, 참조형의 경우 이렇게 할때에는 동적바인딩으로 동작하게 된다. 포인터의 형과 실제 포인터가 가리키는 데이터형이 다른일이 생기기 때문이다. 따라서 이 경우 virtual 멤버함수는 런타임에 동적으로 바인딩 된다.
대신 virtual 키워드로 지정된 함수는 파생 클래스에서 반드시 재정의 되어야한다는 특징이 있다.

다형성(polymorphism)
기본 타입에 대한 포인터나 레퍼런스가 필요한 곳에 파생 타입을 사용할 수 있다는 개념. 하나의 타입을 통해서 여러 함수들 중 하나를 선택하여 호출할 수 있다.

1.2.3. 13.2.2 요약


~cpp 
#ifndef GUARD_Core_h
#define GUARD_Core_h

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

class Core {
public:
	Core(): midterm(0), final(0) { }
	Core(std::istream& is) { read(is); }

	std::string name() const;

	// as defined in 13.1.2/230
	virtual std::istream& read(std::istream&);		// virtual 키워드로 정의된 함수는 반드시 재정의되어야한다.
	virtual double grade() const;

protected:
	// accessible to derived classes
	std::istream& read_common(std::istream&);
	double midterm, final;
	std::vector<double> homework;

private:
	// accessible only to `Core'
	std::string n;
};

class Grad: public Core {
public:
	Grad(): thesis(0) { }
	Grad(std::istream& is) { read(is); }

	// as defined in 13.1.2/230; Note: `grade' and `read' are `virtual' by inheritance
	double grade() const;
	std::istream& read(std::istream&);
private:
	double thesis;
};

bool compare(const Core&, const Core&);
#endif
 

1.3. 13.3 Using inheritance to solve our problem

만들어진 클래스를 이용해서 성적을 입력받고 보고서를 출력하는 프로그램 (Core)
~cpp 
int main() {
	vector<Core> students;
	Core record;		// Core의 일반형
	string::size_type maxlen = 0;

	while(record.read(cin)) {
		maxlen = max(maxlen, record.name().size());
		students.push_back(record);
	}

	sort(students.begin(), students.end(), compare);

	for (vector<Core>::size_type i = 0; i != students.size(); ++i) {
		cout<<students[i].name()
			<<string(maxlen + 1 - students[i].name.size(), ' ');
		try {
			double final_grade = students[i].grade();
			streamsize prec = cout.precision();
			cout<<setprecision(3)<<final_grade
				<<setprecision(prec)<<endl;
		} catch (domain_error e) {
			cout<<e.what()<<endl;
		}
	}
	return 0;
}

만들어진 클래스를 이용해서 성적을 입력받고 보고서를 출력하는 프로그램 (Grad)
~cpp 
int main() {
	vector<Grad> students;
	Grad record;		// Grad의 일반형
	string::size_type maxlen = 0;

	while(record.read(cin)) {
		maxlen = max(maxlen, record.name().size());
		students.push_back(record);
	}

	sort(students.begin(), students.end(), compare);

	for (vector<Grad>::size_type i = 0; i != students.size(); ++i) {
		cout<<students[i].name()
			<<string(maxlen + 1 - students[i].name.size(), ' ');
		try {
			double final_grade = students[i].grade();
			streamsize prec = cout.precision();
			cout<<setprecision(3)<<final_grade
				<<setprecision(prec)<<endl;
		} catch (domain_error e) {
			cout<<e.what()<<endl;
		}
	}
	return 0;
}
입력을 받는 record가 일반형이기 때문에 위에 2개의 프로그램은 각기 Core, Grad에 대해서 정적으로 바인딩된다.
이렇게 프로그램이 작성되면 동일한 기능을 하는 프로그램을 자료형에 따라서 2가지로 따로 작성해야한다.

단일프로그램을 작성하기위해서 타입 의존성을 제거해야할 부분
* 읽어들일 요소들을 저장하는 vector의 정의
* 레코드를 읽어들일 임시 지역 변수의 정의
* read 함수
* grade 함수
마지막 2가지 문제는 virtual로 정의된 멤버함수를 통해서 해결. 처음의 2가지를 해결하는 방법은 2가지가 존재하며 13.3~13.4절에 걸쳐서 설명한다.

1.3.1. 13.3.1 알지 못하는(가상적으로) 타입에 대한 컨테이너

앞의 예에서처럼 vector<Core>의 컨테이너를 설정하게 되면 컨테이너 안에 저장되는 객체가 Core의 객체가 되므로 정적으로 바인딩된다. 이를 해결하기 위해서는 vector<Core*>를 통해서 객체를 동적으로 할당하고 관리하도록 하면, 서로 다른 타입의 객체를 저장하는 것도 가능하고 프로그램의 다른 부분에서 다형성의 이점을 이용하는 것도 가능하다.
~cpp 
int main() {
	vector<Core*> students;

	Core* record;
	while (record->read(cin)) {
	//...
	}
}
 
위의 프로그램은 할당되지 않은 Core 공간에 값을 대입하려하기 때문에 에러를 발생시킨다. 프로그래머가 객체에 필요한 공간을 직접관리. 읽어들이는 레코드의 종류를 판단해야함.

포인터로 이루어진 객체를 비교하기 위해서 작성한 함수
~cpp  
bool compare_Core_ptrs(const Core* cp1, const Core* cp2) {
	return compare(*cp1, *cp2);
}
 
인자를 전달하면서 생기는 모호함을 피하기 위해서 compare 라는 이름대신에 compare_Core_ptrs를 사용하여 컴파일러가 명시적으로 이 함수를 사용하도록 한다.

~cpp 
// main_core.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <iomanip>

#include <ios>

#include <iostream>
#include <stdexcept>

#include "Core.h"

using std::cout;
using std::cin;
using std::domain_error;
using std::endl;
using std::setprecision;
using std::setw;
using std::streamsize;
using std::sort;
using std::string;
using std::vector;

using std::max;

// this code almost works; see 13.3.2/242
int main()
{
	vector<Core*> students;         // store pointers, not objects
	Core* record;                   // temporary must be a pointer as well
	char ch;
	string::size_type maxlen = 0;

	// read and store the data
	while (cin >> ch) {
		if (ch == 'U')
			record = new Core;      // allocate a `Core' object
		else
			record = new Grad;      // allocate a `Grad' object
		record->read(cin);          // `virtual' call
		maxlen = max(maxlen, record->name().size());// dereference
		students.push_back(record);
	}

	// pass the version of `compare' that works on pointers
	sort(students.begin(), students.end(), compare_Core_ptrs);

	// write the names and grades
	for (vector<Core*>::size_type i = 0;
	     i != students.size(); ++i) {
		// `students[i]' is a pointer that we dereference to call the functions
		cout << students[i]->name()
		     << string(maxlen + 1 - students[i]->name().size(), ' ');
		try {
			double final_grade = students[i]->grade();
			streamsize prec = cout.precision();
			cout << setprecision(3) << final_grade
			     << setprecision(prec) << endl;

		} catch (domain_error e) {
			cout << e.what() << endl;
		}
		delete students[i];        // free the object allocated when reading
	}
	return 0;
}
 
입력과 출력의 각 부분에서 그리고 컨테이너의 요소를 사용할때 포인터를 이용함으로해서 프로그램이 동적바인딩을 이용해서 상당히 간결해진 것을 확인할 수 있다.

1.3.2. 13.3.2 가상 소멸자(virtual destructor)

상기와 같은 구조에서는 studentsi의 타입이 Core* 이기 때문에 메모리 해제시에 Core버전의 소멸자가 호출되고 Grad 부분은 해제되지 않는다.
소멸자 역시도 virtual 함수로 만들어서 대상 타입에 따라 다른 동작성을 보장하는 것이 가능하다.
~cpp 
class Core {
public:
	virtual ~Core() { }
	//이전과 동일
};
 
상기와 같이 빈 소멸자를 사용하는 것은 흔한 경우이다. 기본 타입의 소멸자를 virtual 로 만듦으로서 파생 클래스에서 발생하는 기타 요소들을 해제해야할 경우가 많기 때문이다.

1.4. 13.4 A simple handle class

상기와 같은 방식으로 포인터를 이용해서 프로그램을 작성하게 되면 프로그래머가 메모리를 직접적으로 관리를 해야하기 때문에 여러가지 버그를 만드는 문제점을 가지고 있다.

핸들 클래스(handle class)
특정 형의 포인터를 캡슐화시킨 인터페이스를 제공해서, 프로그래머에게 포인터가 보이지 않도록 하는 방법을 제공한다.

~cpp 
//Student_info.h
#ifndef GUARD_Student_info_h
#define GUARD_Student_info_h

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

#include "Core.h"

class Student_info {
public:
	// constructors and copy control
	Student_info(): cp(0) { }
	Student_info(std::istream& is): cp(0) { read(is); }
	Student_info(const Student_info&);
	Student_info& operator=(const Student_info&);
	~Student_info() { delete cp; }

	// operations
	std::istream& read(std::istream&);

	std::string name() const {
		if (cp) return cp->name();
		else throw std::runtime_error("uninitialized Student");
	}
	double grade() const {
		if (cp) return cp->grade();
		else throw std::runtime_error("uninitialized Student");
	}

	static bool compare(const Student_info& s1,		
	                    const Student_info& s2) {		// 정적 멤버 함수의 선언
		return s1.name() < s2.name();
	}

private:
	Core* cp;
};
#endif
 
Core*를 Student_info를 통해서 Wrapping 시킴으로써 프로그래머는 실제로르 Student_info가 2가지 종류의 객체를 다룰 수 있지만 실제의 내부 구현은 알지 못하게 된다.
정적멤버함수(static member fuction)
일반 멤버 함수와는 달리 그 클래스 타입의 객체에 대해 작업을 수행하는 것이 아니고, 클래스 안에서는 클래스 객체의 정적 데이터 멤버만을 다루는 것이 가능하다.
장점으로는 임의 범위를 그 클래스의 범위만큼을 가지기 때문에 클래스 내부에서 사용될때 ::compare와 혼동될 염려가 없다는 것이다.

1.4.1. 13.4.1 핸들 읽기


~cpp 
istream& Student_info::read(istream& is) {
	delete cp;		// 언어적으로 널포인터를 해제하는 것은 문제가 없으므로 체크하는 코드를 넣지 않아도 된다.
	char ch;
	is>>ch;

	if(ch == 'U') {
		cp = new Core(is);
	} else {
		cp = new Grad(is);
	}

	return is;
}
 
우선은 첫번째 인자를 받아서 생성할 객체의 타입을 결정하고, 결정된 타입의 객체를 생성시킨다.

1.4.2. 13.4.2 핸들 객체 복사

현재 상태의 클래스로는 복사생성자가 필요한 곳에서 과연 cp에 할당된 객체가 Grad인지 Core인지를 확인할 방법이 없다.
이를 해결하는 것은 복사 생성자를 virtual 로 할당하고 이러한 일을 컴파일러에게 위임시키는 방법이다.
~cpp 
class Core {
	friend class Student_info;
protected:
	virtual Core* clone() const { return new Core(*this); }
};

class Grad {
protected:
	Grad* clone() const { return new Grad(*this); }		// 본래 virtual 함수에서는 기본클래스와 파생클래스에서
								// 오버로드한느 함수의 파라메터 명세, 리턴형이 동일해야하지만
								// 포인터형인 경우에는 파생 클래스의 포인터를 사용하는 것이 가능하다.
};
 
friend class Student_info; 를 사용함으로해서 Student_info의 모든 멤버함수는 Core의 protected, private에 접근하는 것이 가능하다.
Student_info 클래스에서 Grad::clone()를 직접적으로 호출하는 경우가 없기 때문에 friend로 선언하지 않아도 무관하다.

~cpp 
Student_info::Student_info(const Student_info& s) : cp(0) {
	if (s.cp) cp = s.cp->clone();
}

Student_info& Student_info::operator=(const Student_info& s) {
	if (&s != this) {
		delete cp;
		if (s.cp)
			cp = s.cp->clone();
		else
			cp = 0;
	{
	return *this;
}
 

1.5. 13.5 Using the handle class

~cpp 
//main_perfect.cpp
#include <algorithm>
#include <iomanip>

#include <ios>

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

#include "Student_info.h"

using std::cin;
using std::cout;
using std::domain_error;
using std::endl;
using std::setprecision;
using std::setw;
using std::sort;
using std::streamsize;
using std::string;
using std::vector;

using std::max;

int main()
{
	vector<Student_info> students;
	Student_info record;
	string::size_type maxlen = 0;

	// read and store the data
	while (record.read(cin)) {
		maxlen = max(maxlen, record.name().size());
		students.push_back(record);
	}

	// alphabetize the student records
	sort(students.begin(), students.end(), Student_info::compare);

	// write the names and grades
	for (vector<Student_info>::size_type i = 0;
	     i != students.size(); ++i) {
		cout << students[i].name()
		     << string(maxlen + 1 - students[i].name().size(), ' ');
		try {
			double final_grade = students[i].grade();
			streamsize prec = cout.precision();
			cout << setprecision(3) << final_grade
			     << setprecision(prec) << endl;
		} catch (domain_error e) {
			cout << e.what() << endl;
		}
	}
	return 0;
}
전체적으로 쉬운 소스이므로 생략, 모르면 책을 참조.

1.6. 13.6 Subtleties

1.6.1. 13.6.1 상속 및 컨테이너

vector<Core>가 그 파생형을 담을 수 없다는 것에 유의하라. (일견 될 것 처럼 보이지만 안된다)
~cpp 
 vector<Core> students;
 Grad g(cin);
 students.push_back(g);
 
유효한 표현이기는 하지만 g의 Core클래스에서 정의된 부분 만이 저장이 된다.
원하는 방식대로 동작하는 것인지 아닌지는 프로그래머의 의도에 달려있다.

1.6.2. 13.6.2 어떤 함수를 원하나요?

만약 부모 클래스에 있는 메소드와 메소드 명은 갖지만 파라메터나 타입이 같이 않으면 메소드는 완전히 다른 함수로 인식되어 작동한다.
~cpp 
 //만약 r이 Core의 객체이고 Core::regrade(double)는 인자로 받은 것을 final에 기록한다.  
 //Grad::regrade(double, double) 인자로 받은값으로 final, thesis를 할당
 r.regrade(100);		// 작동
 r.regrade(100, 100);	// 컴파일 오류
 

~cpp 
 //만약 r이 Grad의 객체이고 Core::regrade(double)는 인자로 받은 것을 final에 기록한다.  
 //Grad::regrade(double, double) 인자로 받은값으로 final, thesis를 할당
 r.regrade(100);		// 컴파일 오류. Grad::compare 를 기대하기 때문에 이런 문제가 발생한다. 
 r.Core::regrade(100);	// 작동. 범위 연산자를 이용해서 명시적으로 호출하는 것은 허용한다.
 r.regrade(100, 100);	// 작동.
 

~cpp 
 virtual void Core::regrade(double d, double =0) { final = d; }
 
만약 이런 함수를 virtual 로 정의하고 싶다면 사용하지 않는 인자를 디폴트 인자로 지정해서 만들면 된다.

Retrieved from http://wiki.zeropage.org/wiki.php/AcceleratedC++/Chapter13
last modified 2021-02-07 05:22:24