본문 바로가기
개인공부/C++ 기초플러스

C++ Primer 14

by 하고싶은건많은놈 2023. 2. 21.

학습목표

  • has-a 관계
  • 객체 멤버를 가지는 클래스(컨테인먼트)
  • valarray 템플릿 클래스
  • private 상속과 protected 상속
  • 다중 상속
  • 가상 기초 클래스
  • 클래스 템플릿 만들기
  • 클래스 템플릿 사용하기
  • 템플릿 특수화

C++은 코드의 재활용성을 높이는 것을 추구함

  • 이를 위해 클래스에 속하는 객체를 클래스 멤버로 사용하는 방법을 사용함
    이 방법을 컨테인먼트(containment) / 컴포지션(composition) / 레이어링(layering) 이라고 부름
  • 또는 private 상속이나 protected 상속을 사용함
  • 위 방법들은 일반적으로 has-a 관계를 나타냄

14.1 객체 멤버를 가지는 클래스

라이브러리가 이미 적당한 클래스를 제공하고 있을 경우 그것을 사용하는 것이 훨씬 더 간단함

valarray 클래스 : 훑어보기

valarray 헤더 파일이 valarray 클래스를 지원

  • 수치 값(또는 비슷한 속성을 가진 클래스)들을 다루는 클래스
  • 배열에 들어있는 내용물들의 합, 최대값, 최소값 등의 동작들을 지원
  • 여러 데이터형을 다루기 위해 템플릿 클래스로 정의되어있음

템플릿 클래스의 객체 선언시에는 구체적인 데이터형을 지정해야함
valarray<int> q_values;, valarray<double> weights;와 같은 형태로 지정

valarray 클래스에서 생성 가능한 배열의 형태

  • 크기가 0인 배열
  • 주어진 크기의 빈 배열
  • 모든 원소들이 동일한 값으로 초기화된 배열
  • 일반 배열의 값들을 사용하여 초기화된 배열
  • C++11에서는 valarray<int> v = {20, 32, 17, 9}; 형태의 초기자 리스트를 사용할 수 있음

valarray에서 사용 가능한 메소드들

  • operator[]() : 개별 원소에 대한 접근 제공
  • size() : 원소들의 개수 리턴
  • sum() : 원소들의 합 리턴
  • max() / min() : 최대 / 최소 원소 리턴

Student 클래스 설계

string 객체를 사용하여 이름을, valarray<double> 객체를 사용하여 성적표를 나타냄
학생과 이름, 학생과 성적표는 is-a 모델이 아닌 has-a 관계이기 때문에 다중 public 상속을 사용하는건 바람직하지 않음
일반적으로 C++에서 has-a 관계의 모델링에는 컴포지션(컨테인먼트)를 사용
즉, 다른 클래스의 객체들을 멤버로 가지는 클래스를 생성함

class Student
{
	private:
		string name;
		valarray<double> scores;
		...
};

데이터 멤버들이 private으로 선언되어있으므로 Student 클래스의 멤버 함수들이 stringvalarray<double> 클래스의 public 인터페이스를 사용하여 name 객체 및 scores 객체에 접근하여 변경시킬 수 있음
외부에서는 Student 클래스의 public 인터페이스를 사용해야만 namescores에 접근할 수 있음
이와같이 Student 클래스는 멤버 객체들의 구현은 획득하지만 인터페이스는 상속하지 않음


Student 클래스 예제

// studentc.h

#ifndef STUDENTC_H_
#define STUDENTC_H_
#include <iostream>
#include <string>
#include <valarray>

class Student
{
	private:
		typedef std::valarray<double> ArrayDb;
		std::string name;
		ArrayDb scores;
		std::ostream & arr_out(std::ostream & os) const;
	public:
		Student() : name("Null Student"), scores() {}
		explicit Student(const std::string & s) : name(s), scores() {}
		explicit Student(int n) : name("Nully"), scores(n) {}
		Student(const std::string & s, int n) : name(s), scores(n) {}
		Student(const std::string & s, const ArrayDb & a) : name(s), scores(a) {}
		Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {}
		~Student() {}
		double Average() const;
		const std::string & Name() const;
		double & operator[](int i);
		double operator[](int i) const;
		friend std::istream & operator>>(std::istream & is, Student & stu);
		friend std::istream & getline(std::istream & is, Student & stu);
		friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};

# endif

explict 키워드를 사용함으로써 해당 생성자가 암시적 변환 함수로 기능하는 것을 방지함
즉, Student doh("Homer", 10); doh = 5;에서 doh = 5;Student(5) 생성자 호출을 사용하여 5를 임시 객체로 변환한 후 대입 연산으로 원래의 doh에 대입되는 것을 막을 수 있음

상속되는 객체의 경우 멤버 초기자 리스트에서 클래스 이름을 사용하여 특정한 기초 클래스 생성자를 호출함
멤버 객체의 경우에는 생성자들이 멤버 초기자 리스트에서 멤버 이름을 사용함

  • 멤버 초기자 리스트 문법을 사용하지 않을 경우 멤버 객체 클래스를 위해 정의된 디폴트 생성자가 사용됨
  • 멤버 초기자 리스트에 나열된 순으로 초기화되는 대신 해당 멤버들이 선언된 순서대로 초기화됨
// studentc.cpp

#include "studentc.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;

double Student::Average() const
{
	if (scores.size() > 0)
		return scores.sum() / scores.size();
	else
		return 0;
}

const string & Student::Name() const
{
	return name;
}

double & Student::operator[](int i)
{
	return scores[i];
}

double Student::operator[](int i) const
{
	return scores[i];
}

ostream & Student::arr_out(ostream & os) const
{
	int i;
	int lim = scores.size();
	if (lim > 0)
	{
		for (i = 0; i < lim; i++)
		{
			os << scores[i] << " ";
			if (i % 5 == 4)
				os << endl;
		}
		if (i % 5 != 0)
			os << endl;
	}
	else
		os << " 빈 배열 ";
	return os;
}

istream & operator>>(istream & is, Student & stu)
{
	is >> stu.name;
	return is;
}

istream & getline(istream & is, Student & stu)
{
	getline(is, stu.name);
	return is;
}

ostream & operator<<(ostream & os, const Student & stu)
{
	os << stu.name << "학생의 성적표 : \n";
	stu.arr_out(os);
	return os;
}

내포된 객체의 인터페이스를 클래스 메소드 안에서 사용할 수 있음

  • Average() 메소드에서 scoresvalarray 객체이기 때문에 valarray 클래스의 size()sum() 메소드를 사용할 수 있음
  • operator<<() 프렌드 함수에서 stu.namestring 객체로써 operator<<(ostream &, const string &) 함수를 호출함
// use_stuc.cpp

#include <iostream>
#include "studentc.h"
using std::cin;
using std::cout;
using std::endl;

void set(Student & sa, int n);

const int pupils = 3;
const int quizzes = 5;

int main()
{
	Student ada[pupils] = {Student(quizzes), Student(quizzes), Student(quizzes)};
	
	int i;
	for (i = 0; i < pupils; ++i)
		set(ada[i], quizzes);
	cout << "\n학생 리스트 : \n";
	for (i = 0; i < pupils; ++i)
		cout << ada[i].Name() << endl;
	cout << "\n시험 결과 : ";
	for (i = 0; i < pupils; ++i)
	{
		cout << endl << ada[i];
		cout << "평균 : " << ada[i].Average() << endl;
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

void set(Student & sa, int n)
{
	cout << "학생의 이름을 입력하십시오 : ";
	getline(cin, sa);
	cout << n << "회에 걸친 시험 점수를 모두 입력하십시오 : \n";
	for (int i = 0; i < n; i++)
		cin >> sa[i];
	while (cin.get() != '\n')
		continue;
}


14.2 private 상속

is-a 관계인 public 상속에서는 기초 클래스의 public 메소드가 파생 클래스의 public 메소드가 되며, 이에따라 파생 클래스는 기초 클래스의 인터페이스를 상속함

반면 has-a 관계인 private 상속에서는 기초 클래스의 public 메소드가 파생 클래스의 private 메소드가 되며, 파생 클래스가 기초 클래스의 인터페이스를 상속하지 않음
즉, 기초 클래스의 메소드들이 파생 객체의 public 인터페이스가 되지 않는 대신 파생 클래스의 멤버 함수들 안에서 해당 메소드들을 사용할 수 있음

  • private 상속에서는 클래스가 구현을 상속
  • private 상속은 객체를 이름없는 상속된 객체로 클래스에 추가함
  • 컨테인먼트와 동일한 기능을 제공(구현 획득, 인터페이스 미획득)

Student 클래스 예제(새 버전)

접근 제한자 생략시 디폴트인 private로 상속됨
기초 클래스가 하나 이상일 경우 다중 상속(multiple inheritance, MI)라고 부름

// studenti.h

#ifndef STUDENTI_H_
#define STUDENTI_H_
#include <iostream>
#include <string>
#include <valarray>

class Student : private std::string, private std::valarray<double>
{
	private:
		typedef std::valarray<double> ArrayDb;
		std::ostream & arr_out(std::ostream & os) const;
	public:
		Student() : std::string("Null Student"), ArrayDb() {}
		explicit Student(const std::string & s) : std::string(s), ArrayDb() {}
		explicit Student(int n) : std::string(s), ArrayDb() {}
		Student(const std::string & s, int n) : std::string(s), ArrayDb(n) {}
		Student(const std::string & s, const ArrayDb & a) : std::string(s), ArrayDb(a) {}
		Student(const char * str, const double * pd, int n) : std::string(str), ArrayDb(pd, n) {}
		~Student() {}
		double Average() const;
		const std::string & Name() const;
		double & operator[](int i);
		double operator[](int i) const;
		friend std::istream & operator>>(std::istream & is, Student & stu);
		friend std::istream & getline(std::istream & is, Student & stu);
		friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};

# endif

상속되는 두 기초 클래스에서 필요한 데이터 멤버들을 제공하기 때문에 private 데이터를 가질 필요가 없음
멤버 초기자 리스트 문법에서 멤버 이름 대신 클래스 이름을 사용하여 생성자들을 식별함

// studenti.cpp

#include "studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;

double Student::Average() const
{
	if (ArrayDb::size() > 0)
		return ArrayDb::sum() / ArrayDb::size();
	else
		return 0;
}

const string & Student::Name() const
{
	return (const string &) *this;
}

double & Student::operator[](int i)
{
	return ArrayDb::operator[](i);
}

double Student::operator[](int i) const
{
	return ArrayDb::operator[](i);
}

ostream & Student::arr_out(ostream & os) const
{
	int i;
	int lim = ArrayDb::size();
	if (lim > 0)
	{
		for (i = 0; i < lim; i++)
		{
			os << ArrayDb::operator[](i) << " ";
			if (i % 5 == 4)
				os << endl;
		}
		if (i % 5 != 0)
			os << endl;
	}
	else
		os << " 빈 배열 ";
	return os;
}

istream & operator>>(istream & is, Student & stu)
{
	is >> (string &)stu;
	return is;
}

istream & getline(istream & is, Student & stu)
{
	getline(is, (string &)stu);
	return is;
}

ostream & operator<<(ostream & os, const Student & stu)
{
	os << (const string &)stu << "학생의 성적표 : \n";
	stu.arr_out(os);
	return os;
}

private 상속에서는 클래스 이름과 사용 범위 결정 연산자를 사용하여 기초 클래스의 메소드를 호출함
private 상속에서는 기초 클래스의 객체가 이름을 가지지 않기 때문에, 이에 접근하기 위해서는 데이터형 변환을 사용해야함
이 때 생성자 호출로 새로운 객체가 생성되는 것을 피하려면 참조를 사용한 데이터형 변환이 이루어져야함
private 상속에서 명시적인 데이터형 변환이 없으면 파생 클래스에 대한 참조나 포인터를 기초 클래스에 대한 참조나 포인터에 대입할 수 없음

use_stui.cppuse_stuc.cpp와 동일한 것을 사용


컨테인먼트와 private 상속

대부분의 C++ 프로그래머는 private 상속보다 컨테인먼트를 선호함

  • 컨테인먼트가 사용하기 더 쉬움
  • 하나 이상의 기초 클래스로부터 클래스 상속시 문제가 발생할 수 있음
    • 별개의 기초 클래스들이 하나의 조상 클래스를 공유하고있을 때의 문제
    • 별개의 기초 클래스들이 같은 이름을 가진 메소드를 사용하고있을 때의 문제 등
  • 컨테인먼트 사용시 같은 클래스의 종속 객체를 하나 이상 내포시킬 수 있음

단, protected 멤버가 있는 클래스의 경우 컨테인먼트를 사용하여 다른 클래스에 내포시킬 경우 새 클래스는 바깥 세계의 일부가되므로 protected 멤버에 접근할 수 없음
반면 private 상속을 사용하면 protected 멤버에 접근할 수 있음
가상 함수를 재정의할 필요가 있을 때에도 private 상속을 사용해야함


protected 상속

protected 상속에서는 기초 클래스의 public 멤버와 protected 멤버가 파생 클래스의 protected 멤버가 됨

private 상속에서는 파생 클래스로부터 파생된 3세대 클래스가 기초 클래스 인터페이스의 내부적인 사용권을 얻지 못함
이는 2세대 클래스에서 1세대 클래스의 public 메소드들이 private이 되고, 3세대에서는 2세대의 private 멤버와 메소드에 직접 접근할 수 없기 때문
반면 protected 상속에서는 public 메소드들이 2세대에서 protected가 되므로 3세대에서도 접근이 가능함


using을 사용하여 접근 다시 정의하기

어떤 특정 기초 클래스 메소드를 파생 클래스에서 public으로 사용할 수 있게 하고싶을 경우

  • 기초 클래스 메소드를 사용하는 파생 클래스 메소드를 직접 정의
double Student::sum() const
{
	return std::valarray<double>::sum();
}
  • using 선언을 사용하여 파생 클래스에서 사용할 특정 기초 클래스 멤버를 지정
    • 이 때 파생 클래스가 private 파생이어도 상관 없음
    • using 선언은 소괄호, 함수 시그니처, 리턴형을 제외하고 멤버 이름만 사용
    • 상속에만 적용 가능(컨테인먼트에는 적용 불가능)
class Student : private std::string, private std::valarray<double>
{
	...
	public:
		using std::valarray<double>::min;
		using std::valarray<double>::max;
		...
};
  • private에 파생된 클래스에 기초 클래스 메소드들을 다시 선언
    • 단, 구식이므로 using을 사용할 것을 권장
class Student : private std::string, private std::valarray<double>
{
	public:
		std::valarray<double>::operator[];
		...
};


14.3 다중 상속

다중 상속은 직계 인접한 기초 클래스를 하나 이상 가지는 클래스를 서술함

  • public 다중 상속은 단일 상속과 마찬가지로 is-a 관계를 나타내야함
    이 때 디폴트가 private 파생이기 때문에 public 키워드로 기초 클래스들을 각각 제한해주어야함
  • private, protected 다중 상속은 has-a 관계를 나타낼 수 있음
// worker0.h

#ifndef WORKER0_H_
#define WORKER0_H_
#include <string>

class Worker
{
	private:
		std::string fullname;
		long id;
	public:
		Worker() : fullname("no name"), id(0L) {}
		Worker(const std::string & s, long n) : fullname(s), id(n) {}
		virtual ~Worker() = 0;
		virtual void Set();
		virtual void Show() const;
};

class Waiter : public Worker
{
	private:
		int panache;
	public :
		Waiter() : Worker(), panache(0) {}
		Waiter(const std::string & s, long n, int p = 0)
			: Worker(s, n), panache(p) {}
		Waiter(const Worker & wk, int p = 0)
			: Worker(wk), panache(p) {}
		void Set();
		void Show() const;
};

class Singer : public Worker
{
	protected:
		enum {other, alto, contralto, soprano, bass, baritone, tenor};
		enum {Vtypes = 7};
	private:
		static char *pv[Vtypes];
		int voice;
	public:
		Singer() : Worker(), voice(other) {}
		Singer(const std::string & s, long n, int v = other)
			: Worker(s, n), voice(v) {}
		Singer(const Worker & wk, int v = other)
			: Worker(wk), voice(v) {}
		void Set();
		void Show() const;
};

# endif
// worker0.cpp

#include "worker0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;

Worker::~Worker() {}

void Worker::Set()
{
	cout << "사원 이름을 입력하십시오 : ";
	getline(cin, fullname);
	cout << "사원 번호를 입력하십시오 : ";
	cin >> id;
	while (cin.get() != '\n')
		continue;
}

void Worker::Show() const
{
	cout << "사원 이름 : " << fullname << "\n";
	cout << "사원 번호 : " << id << "\n";
}

void Waiter::Set()
{
	Worker::Set();
	cout << "웨이터 등급을 입력하십시오 : ";
	cin >> panache;
	while (cin.get() != '\n')
		continue;
}

void Waiter::Show() const
{
	cout << "직종 : 웨이터\n";
	Worker::Show();
	cout << "웨이터 등급 : " << panache << "\n";
}

char * Singer::pv[] = {"other", "alto", "contralto",
					"soprano", "bass", "baritone", "tenor"};

void Singer::Set()
{
	Worker::Set();
	cout << "가수의 목소리 유형 번호를 입력하십시오::\n";
	int i;
	for (i = 0; i < Vtypes; i++)
	{
		cout << i << " : " << pv[i] << " ";
		if (i % 4 == 3)
			cout << endl;
	}
	if (i % 4 != 0)
		cout << endl;
	while (cin >> voice && (voice < 0 || voice >= Vtypes))
		cout << "0보다 크거나 같고 " << Vtypes << "보다 작은 값을 입력하세요." << endl;
	while (cin.get() != '\n')
		continue;
}

void Singer::Show() const
{
	cout << "직종 : 가수\n";
	Worker::Show();
	cout << "목소리 유형 : " << pv[voice] << endl;
}

사원 수는 몇 명인가?

SingerWaiter로부터 SingingWatier 클래스를 public 다중 상속받을 경우

  • SingingWatier 클래스는 각각의 부모 클래스로부터 Worker 성분을 하나씩 상속받음
  • 이 경우 파생 클래스 객체의 주소를 기초 클래스 포인터에 대입할 때 선택할 수 있는 주소가 두개이기 때문에 모호해짐
  • 강제 데이터형 변환을 사용하여 어느 객체를 지정할 것인지 선택할 수 있으나, 이는 다형(polymorphism)을 어렵게 만듬

이와같이 두 개의 같은 객체 복사본을 가질 필요가 없으므로 C++에서는 가상 기초 클래스를 사용함

  • class Singer : virtual public Worker {...};의 형식으로 사용
  • virtual 키워드와 public 키워드의 순서는 상관없음
  • 가상 기초 클래스 사용시 복사본을 갖는 대신 하나의 객체를 공유하게됨

가상 기초 클래스는 가상 함수와 큰 연관이 없으며, virtual이라는 키워드는 단순히 오버로딩되어 사용됨
기초 클래스를 가상으로 만들시 프로그램의 추가 작업이 요구되고, 하나의 기초 클래스 객체에 대해 여러 복사본을 원하는 경우가 있으므로 가상 기초 클래스를 다중 상속의 표준으로 삼지 않음

가상 기초 클래스는 클래스 생성자에 대한 새로운 접근 방식을 요구함

  • 기초 클래스가 가상일 경우 중간 클래스를 통해 기초 클래스에 자동으로 정보를 전달되어 중복으로 충돌할 수 있음
  • 따라서 컴파일러는 기초 클래스의 생성자에 이를 전달하지 않고, 대신 디폴트 생성자를 사용함
  • 기초 클래스에서 디폴트 생성자가 아닌 다른 생성자를 사용하기 원한다면 아래와 같이 기초 생성자를 명시적으로 호출해야함
SingingWatier(const Worker & wk, int p = 0, int v = Singer::other)
	: Worker(wk), Watier(wk, p), Singer(wk, v) {}

어느 메소드를 사용하는가?

상속받은 클래스들에 같은 메소드가 존재할 경우, 모호한 함수 호출이 발생한다는 문제가 생김

  • 사용 범위 결정 연산자를 사용하면 문제를 해결할 수 있음
  • 상속받은 클래스에 해당 메소드를 재정의함으로써 문제를 해결할 수 있음
    • 단, 이 경우에는 점층적 접근 방식 대신 모듈 접근 방식을 사용해야함
    • 즉, 기초 클래스와 부모 클래스의 성분을 나타내는 메소드들을 따로 제공한 뒤 현재 클래스에서 해당 성분들을 결합함
    • 이 때 상위 클래스의 메소드들이 private일 경우에는 접근할 수 없으므로 protected로 만드는 것이 유용함
// workermi.h

#ifndef WORKERMI_H_
#define WORKERMI_H_
#include <string>

class Worker
{
	private:
		std::string fullname;
		long id;
	protected:
		virtual void Data() const;
		virtual void Get();
	public:
		Worker() : fullname("no name"), id(0L) {}
		Worker(const std::string & s, long n) : fullname(s), id(n) {}
		virtual ~Worker() = 0;
		virtual void Set() = 0;
		virtual void Show() const = 0;
};

class Waiter : virtual public Worker
{
	private:
		int panache;
	protected:
		void Data() const;
		void Get();
	public :
		Waiter() : Worker(), panache(0) {}
		Waiter(const std::string & s, long n, int p = 0)
			: Worker(s, n), panache(p) {}
		Waiter(const Worker & wk, int p = 0)
			: Worker(wk), panache(p) {}
		void Set();
		void Show() const;
};

class Singer : virtual public Worker
{
	protected:
		enum {other, alto, contralto, soprano, bass, baritone, tenor};
		enum {Vtypes = 7};
		void Data() const;
		void Get();
	private:
		static char *pv[Vtypes];
		int voice;
	public:
		Singer() : Worker(), voice(other) {}
		Singer(const std::string & s, long n, int v = other)
			: Worker(s, n), voice(v) {}
		Singer(const Worker & wk, int v = other)
			: Worker(wk), voice(v) {}
		void Set();
		void Show() const;
};

class SingingWaiter : public Singer, public Waiter
{
	protected:
		void Data() const;
		void Get();
	public:
		SingingWaiter() {}
		SingingWaiter(const std::string & s, long n, int p = 0, int v = other)
			: Worker(s, n), Waiter(s, n, p), Singer(s, n, v) {}
		SingingWaiter(const Worker & wk, int p = 0, int v = other)
			: Worker(wk), Waiter(wk, p), Singer(wk, v) {}
		SingingWaiter(const Singer & wt, int p = 0)
			: Worker(wt), Waiter(wt, p), Singer(wt) {}
		void Set();
		void Show() const;
};

#endif
// workermi.cpp

#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;

Worker::~Worker() {}

void Worker::Data() const
{
	cout << "사원 이름 : " << fullname << endl;
	cout << "사원 번호 : " << id << endl;
}

void Worker::Get()
{
	getline(cin, fullname);
	cout << "사원 번호를 입력하십시오 : ";
	cin >> id;
	while (cin.get() != '\n')
		continue;
}

void Waiter::Set()
{
	cout << "웨이터의 이름을 입력하십시오 : ";
	Worker::Get();
	Get();
}

void Waiter::Show() const
{
	cout << "직종 : 웨이터 \n";
	Worker::Data();
	Data();
}

void Waiter::Data() const
{
	cout << "웨이터 등급 : " << panache << endl;
}

void Waiter::Get()
{
	cout << "웨이터 등급을 입력하십시오 : ";
	cin >> panache;
	while (cin.get() != '\n')
		continue;
}

char * Singer::pv[] = {"other", "alto", "contralto",
					"soprano", "bass", "baritone", "tenor"};

void Singer::Set()
{
	cout << "가수의 이름을 입력하십시오 : ";
	Worker::Get();
	Get();
}

void Singer::Show() const
{
	cout << "직종 : 가수\n";
	Worker::Data();
	Data();
}

void Singer::Data() const
{
	cout << "목소리 유형 : " << pv[voice] << endl;
}

void Singer::Get()
{
	cout << "가수의 목소리 유형 번호를 입력하십시오::\n";
	int i;
	for (i = 0; i < Vtypes; i++)
	{
		cout << i << " : " << pv[i] << " ";
		if (i % 4 == 3)
			cout << endl;
	}
	if (i % 4 != 0)
		cout << endl;
	while (cin >> voice && (voice < 0 || voice >= Vtypes))
		cout << "0보다 크거나 같고 " << Vtypes << "보다 작은 값을 입력하세요." << endl;
	while (cin.get() != '\n')
		continue;
}

void SingingWaiter::Data() const
{
	Waiter::Data();
	Singer::Data();
}

void SingingWaiter::Get()
{
	Waiter::Get();
	Singer::Get();
}

void SingingWaiter::Set()
{
	cout << "가수 겸 웨이터의 이름을 입력하십시오 : ";
	Worker::Get();
	Get();
}

void SingingWaiter::Show() const
{
	cout << "직종 : 가수 겸 웨이터\n";
	Worker::Data();
	Data();
}
// workmi.cpp

#include <iostream>
#include <cstring>
#include "workermi.h"

const int SIZE = 5;

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;
	using std::strchr;

	Worker * lolas[SIZE];

	int ct;
	for (ct = 0; ct < SIZE; ct++)
	{
		char choice;
		cout << "직종을 입력하십시오 : \n"
			 << "w : 웨이터		s : 가수		"
			 << "t : 가수 겸 웨이터		q : 종료\n";
		cin >> choice;
		while (strchr("wstq", choice) == NULL)
		{
			cout << "w, s, t, q 중에서 하나를 선택하십시오 : ";
			cin >> choice;
		}
		if (choice == 'q')
			break;
		switch(choice)
		{
			case 'w': lolas[ct] = new Waiter;
				break;
			case 's': lolas[ct] = new Singer;
				break;
			case 't': lolas[ct] = new SingingWaiter;
				break;
		}
		cin.get();
		lolas[ct]->Set();
	}
	
	cout << "\n사원 현황은 다음과 같습니다 : \n";
	int i;
	for (i = 0; i < ct; i++)
	{
		cout << endl;
		lolas[i]->Show();
	}
	for (i = 0; i < ct; i++)
		delete lolas[i];
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

하나의 공통 조상을 공유하는 다중 상속을 사용하려면 가상 기초 클래스를 도입해야함

  • 생성자에서 멤버 초기자 리스트의 규칙을 변경
  • 다중 상속을 고려하지 않고 작성한 클래스의 코딩을 변경

가상 기초 클래스와 가상이 아닌 기초 클래스가 혼합되어있을 경우 가상으로 파생시킨 조상들에 대해서 종속 개체 하나, 가상이 아닌 조상들에 대해서는 각각 별개의 종속 객체들을 내포하게됨

가상 기초 클래스를 사용한 경우, 하나의 이름이 다른 이름들보다 비교 우위(dominance)를 가진다면 제한자가 없어도 모호함 없이 사용될 수 있음

  • 파생 클래스에 있는 이름은 조상 클래스에 있는 동일한 이름보다 비교 우위를 가짐
  • 이 때 접근 규칙은 고려하지 않음

다중 상속 요약

가상 기초 클래스를 사용하지 않는 다중 상속의 경우

  • 파생 클래스가 서로 다른 기초 클래스로부터 이름이 같은 멤버를 상속받는다면 두 멤버를 구분하기 위해 클래스 제한자를 사용하여야 함
  • 파생 클래스는 기초 클래스의 가상이 아닌 인스턴스마다 하나씩 클래스 객체를 상속받음

가상 기초 클래스를 사용한 다중 상속의 경우

  • 키워드 virtual을 사용하여 가상 기초 클래스를 나타냄
  • 하나의 가상 기초 클래스의 여러 인스턴스로부터 상속을 받는다 하더라도 하나의 기초 클래스 객체만 상속받게됨
  • 단, 파생 클래스의 생성자들이 간접적인 가상 기초 클래스 생성자들을 직접 호출하게 해야함
  • 이름의 모호함은 비교 우위 규칙을 통해 해결됨


14.4 클래스 템플릿

클래스 템플릿은 포괄적인 클래스 선언을 생성하기 위해 사용됨

  • 템플릿은 매개변수화(parameterized)되는 데이터형을 제공함
  • 즉, 클래스나 함수에 데이터형 이름을 매개변수로 넘겨줄 수 있음
  • C++ 표준 템플릿 라이브러리(Standard Template LIbrary, STL)에서 컨테이너 클래스들을 위한 템플릿 구현을 제공함

클래스 템플릿 정의

템플릿 클래스는 template <class Type> 형식을 사용함

  • template 키워드로 템플릿을 정의
  • class 키워드는 데이터형을 값으로 받아들이는 어떤 변수의 데이터형 이름의 역할을 함
  • 단, Type으로 이름을 나타내는 변수가 반드시 클래스일 필요는 없음
  • 따라서 혼동을 피하기 위해 class 대신 typename 키워드를 사용할 수 있음
  • 클래스 선언 안에서 메소드 정의시(인라인 정의) 템플릿 제한자와 클래스 제한자를 생략할 수 있음
// stacktp.h

#ifndef STACKTP_H_
#define STACKTP_H_

template <class Type>
class Stack
{
	private:
		enum {MAX = 10};
		Type items[MAX];
		int top;
	public:
		Stack();
		bool isempty();
		bool isfull();
		bool push(const Type & item);
		bool pop(Type & item);
};

template <class Type>
Stack<Type>::Stack()
{
	top = 0;
}

template <class Type>
bool Stack<Type>::isempty()
{
	return top == 0;
}

template <class Type>
bool Stack<Type>::isfull()
{
	return top == MAX;
}

template <class Type>
bool Stack<Type>::push(const Type & item)
{
	if (top < MAX)
	{
		items[top++] = item;
		return true;
	}
	else
		return false;
}

template <class Type>
bool Stack<Type>::pop(Type & item)
{
	if (top > 0)
	{
		item = items[--top];
		return true;
	}
	else
		return false;
}

#endif

템플릿은 클래스와 멤버 함수 정의를 생성하는 방법을 컴파일러에게 알려주는 지시문임
템플릿의 특별한 현실화를 구체화(instantiation) 또는 특수화(specialization)이라고 함
템플릿은 함수가 아니기 때문에 개별적으로 컴파일할 수 없으며, 특별한 구체화 요구들과 함께 결합하여 사용해야함
이를 위해 모든 템플릿 관련 정보를 헤더 파일에 집어넣고 템플릿 사용시 해당 헤더 파일을 포함시키는 방법을 사용할 수 있음


템플릿 클래스 사용

템플릿 클래스 생성을 위해서는 구체화가 필요하고, 따라서 템플릿 클래스형의 객체를 선언할 때 포괄적인 데이터형 이름을 구체적인 데이터형으로 대체해야함
즉, Stack<int> kernels;, Stack<string> colonels;와 같은 형태를 사용

Type과 같은 포괄적인 데이터형 식별자를 데이터형 매개변수(type parameter)라고 함
일반적인 함수 템플릿은 함수 매개변수의 데이터를 사용해 컴파일러가 어떤 종류의 함수를 생성할 것인지 결정하는 반면, 템플릿 클래스에서는 사용하려는 데이터형을 명시적으로 제공해야함

// stacktem.cpp

#include "stacktp.h"
#include <iostream>
#include <string>
#include <cctype>
using std::cout;
using std::cin;

int main()
{
	Stack<std::string> st;
	char ch;
	std::string po;
	cout << "주문서를 추가하려면 A, 주문서를 처리하려면 P, \n"
		 << "종료하려면 Q를 입력하십시오. \n";
	while (cin >> ch && std::toupper(ch) != 'Q')
	{
		while (cin.get() != '\n')
			continue;
		if (!std::isalpha(ch))
		{
			cout << '\a';
			continue;
		}
		switch (ch)
		{
			case 'A':
			case 'a': cout << "추가할 주문서의 번호를 입력하십시오 : ";
					cin >> po;
					if (st.isfull())
						cout << "스택이 가득 차 있습니다.\n";
					else
						st.push(po);
					break;
			case 'P':
			case 'p': if (st.isempty())
						cout << "스택이 비어있습니다.\n";
					else
					{
						st.pop(po);
						cout << "#" << po << " 주문서를 처리했습니다.\n";
						break;
					}
		}
			cout << "주문서를 추가하려면 A, 주문서를 처리하려면 P, \n"
						<< "종료하려면 Q를 입력하십시오. \n";
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

템플릿 클래스 자세히 들여다보기

stacktem.cpp에서 string 객체 대신 char형을 지시하는 포인터를 사용하게 할 경우

  • Stack<char *> st;, char *po : po가 가리키는 공간이 할당되지 않았기 때문에 cin >> po가 정상 작동하지 않음 '
  • Stack<char *> st;, char po[40] : 참조 변수 item은 Lvalue를 참조해야하며, 배열을 참조할 수 있다 하더라도 배열 이름에 대입할 수는 없음
  • Stack<char *> st;, char * po = new char[40] : po 변수는 하나 뿐이기 때문에 항상 같은 메모리를 지시하기 때문에 스택에서도 항상 마지막으로 읽어들인 문자열을 참조하게됨

따라서 포인터들의 배열을 제공하여 포인터들의 스택을 사용할 수 있음

// stacktp1.h

#ifndef STACKTP1_H_
#define STACKTP1_H_

template <class Type>
class Stack
{
	private:
		enum {MAX = 10};
		int stacksize;
		Type * items;
		int top;
	public:
		explicit Stack(int ss = SIZE);
		Stack(const Stack & st);
		~Stack() { delete [] items; }
		bool isempty() { return top == 0; }
		bool isfull() { return top == stacksize; }
		bool push(const Type & item);
		bool pop(Type & item);
		Stack & operator=(const Stack & st);
};

template <class Type>
Stack<Type>::Stack(int ss) : stacksize(ss), top(0)
{
	items = new Type[stacksize];
}

template <class Type>
Stack<Type>::Stack(const Stack & st)
{
	stacksize = st.stacksize;
	top = st.top;
	items = new type[stacksize];
	for (int i = 0; i < top; i++)
		items[i] = st.items[i];
}

template <class Type>
bool Stack<Type>::push(const Type & item)
{
	if (top < stacksize)
	{
		items[top++] = item;
		return true;
	}
	else
		return false;
}

template <class Type>
bool Stack<Type>::pop(Type & item)
{
	if (top > 0)
	{
		item = items[--top];
		return true;
	}
	else
		return false;
}

template <class Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st)
{
	if (this == &st)
		return *this;
	delete [] items;
	stacksize = st.stacksize;
	top = st.top;
	items = new Type[stacksize];
	for (int i = 0; i < top; i++)
		items[i] = st.items[i];
	return *this;
}

#endif

대입 연산자 함수의 리턴형을 원형에서는 Stack에 대한 참조로, 실제 템플릿 함수 정의에서는 Stack<Type>에 대한 참조로 선언함

  • 이 때 StackStack<Type>의 약식 표기로 해당 클래스 사용 범위 안에서만 사용할 수 있음
  • 즉, 템플릿 선언 및 템플릿 함수 정의의 내부에서 Stack을 사용할 수 있음
  • 단, 클래스 바깥에서는 완전한 형식인 Stack<Type>을 사용해야함
// stkoptr1.cpp

#include "stacktp1.h"
#include <iostream>
#include <cstdlib>
#include <ctime>

const int Num = 10;

int main()
{
	std::srand(std::time(0));
	std::cout << "스택의 크기를 입력하십시오 : ";
	int stacksize;
	std::cin >> stacksize;
	Stack<const char *> st(stacksize);

	const char * in[Num] = {
		" 1 : 노미호", " 2 : 주리혜",
		" 3 : 이몽룡", " 4 : 성춘향",
		" 5 : 이수일", " 6 : 심순애",
		" 7 : 박문수", " 7 : 홍길동",
		" 9 : 김두한", "10 : 하야시"
	};

	const char * out[Num];

	int processed = 0;
	int nextin = 0;
	while (processed < Num)
	{
		if (st.isempty())
			st.push(in[nextin++]);
		else if (st.isfull())
			st.pop(out[processed++]);
		else if (std::rand() % 2 && nextin < Num)
			st.push(in[nextin++]);
		else
			st.pop(out[processed++]);
	}
	for (int i = 0; i < Num; i++)
		std::cout << out[i] << std::endl;

	std::cout << "프로그램을 종료합니다.\n";
	return 0;
}

배열 템플릿 예제와 데이터형이 아닌 매개변수

// arraytp.h

#ifndef ARRAYTP_H_
#define ARRAYTP_H_

#include <iostream>
#include <cstdlib>

template <class T, int n>
class ArrayTP
{
	private:
		T ar[n];
	public:
		ArrayTP() {}
		explicit ArrayTP(const T & v);
		virtual T & operator[](int i);
		virtual T operator[](int i) const;
};

template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v)
{
	for (int i = 0; i < n; i++)
		ar[i] =  v;
}

template <class T, int n>
T & ArrayTP<T,n>::operator[](int i)
{
	if (i < 0 || i >= n)
	{
		std::cerr << "배열의 경계를 벗어나는 에러 : "
				  << i << "--> 잘못된 인덱스입니다.\n";
		std::exit(EXIT_FAILURE);
	}
	return ar[i];
}

template <class T, int n>
T ArrayTP<T,n>::operator[](int i) const
{
	if (i < 0 || i >= n)
	{
		std::cerr << "배열의 경계를 벗어나는 에러 : "
				  << i << "--> 잘못된 인덱스입니다.\n";
		std::exit(EXIT_FAILURE);	
	}
	return ar[i];
}

#endif

template <class T, int n> 에서 T는 데이터형 매개변수 또는 데이터형 인수(arguments)로 인식되며, n은 int형으로 인식됨
이 때 n과 같은 종류의 매개변수를 데이터형이 아닌 매개변수(non-type argument) 또는 수식 매개변수(expression argument)라고 함

  • 수식 매개변수에는 정수형, 열거형, 참조, 포인터가 올 수 있음
  • 템플릿 코드는 수식 매개변수의 값을 변경하거나 주소를 얻을 수 없음
  • 템플릿 구체화시 수식 매개변수에 사용되는 값은 상수 수식이어야함

생성자 접근 방식은 newdelete에 의해 관리되는 힙 메모리를 사용
반면 배열의 크기를 수식 매개변수로 결정할 경우 스택 메모리를 사용하기 때문에 크기가 작은 배열들을 많이 사용하는 경우 더 빠른 실행 속도를 제공
단, 각 배열 크기가 자신만의 템플릿을 각각 생성한다는 단점이 있음


템플릿의 융통성

template <typename T>
class Array
{
	...
};

template <typename Type>
class GrowArray : public Array<Type> {...}; // 상속

template <typename Tp>
class Stack
{
	Array<Tp> ar; // 성분으로 사용
	...
};

Array <Stack<int>> asi; // 배열으로 사용

템플릿 클래스는 기초 클래스 및 성분 클래스의 역할을 할 수 있음
템플릿 클래스 자체가 다른 템플릿들의 데이터형 매개변수가 될 수도 있음

// twod.cpp

#include "arraytp.h"
#include <iostream>

int main()
{
	using std::cout;
	using std::endl;
	ArrayTP<int, 10> sums;
	ArrayTP<double, 10> aves;
	ArrayTP<ArrayTP<int, 5>, 10> twodee;

	int i, j;

	for (i = 0; i < 10; i++)
	{
		sums[i] = 0;
		for (j = 0; j < 5; j++)
		{
			twodee[i][j] = (i + 1) * (j + 1);
			sums[i] += twodee[i][j];
		}
		aves[i] = (double) sums[i] / 10;
	}
	for (i = 0; i < 10; i++)
	{
		for (j = 0; j < 5; j++)
		{
			cout.width(2);
			cout << twodee[i][j] << ' ';
		}
		cout << ": 합계 = ";
		cout.width(3);
		cout << sums[i] << ", 평균 = " << aves[i] << endl;
	}

	cout << "프로그램을 종료합니다.\n";
	return 0;
}

위와 같이 템플릿을 재귀적으로 사용할 수도 있음

// pairs.cpp

#include <iostream>
#include <string>

template <class T1, class T2>
class Pair
{
	private:
		T1 a;
		T2 b;
	public:
		T1 & first();
		T2 & second();
		T1 first() const { return a; }
		T2 second() const { return b; }
		Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) {}
		Pair() {}
};

template<class T1, class T2>
T1 & Pair<T1, T2>::first()
{
	return a;
}

template<class T1, class T2>
T2 & Pair<T1, T2>::second()
{
	return b;
}

int main()
{
	using std::cout;
	using std::endl;
	using std::string;
	Pair<string, int> ratings[4] = 
	{
		Pair<string, int>("이동갈비", 5),
		Pair<string, int>("태릉갈비", 4),
		Pair<string, int>("수원갈비", 5),
		Pair<string, int>("LA갈비", 3)
	};

	int joints = sizeof(ratings) / sizeof(Pair<string, int>);
	cout << "등급 : \t 갈비 종류\n";
	for (int i = 0; i < joints; i++)
		cout << ratings[i].second() << " :\t "
			 << ratings[i].first() << endl;
	cout << "조정된 등급 : \n";
	ratings[3].first() = "LA떡갈비";
	ratings[3].second() = 6;
	cout << ratings[3].second() << " :\t "
		 << ratings[3].first() << endl;
	return 0;
}

위와 같이 하나 이상의 데이터형 매개변수를 사용하는 템플릿을 만들 수도 있음

클래스 템플릿은 데이터형 매개변수들에 디폴트 값을 제공할 수 있음

  • template <class T1, class T2 = int> class Topo {...};와 같은 형태로 사용
  • 표준 템플릿 라이브러리는 클래스를 디폴트 데이터형으로 하여 이 기능을 자주 사용함
  • 단, 함수 템플릿에서는 데이터형 메개변수에 디폴트 값을 제공할 수 없음
  • 그러나 데이터형이 아닌 매개변수에 대해서는 클래스 템플릿과 함수 템플릿 모두 디폴트 값을 제공할 수 있음

템플릿 특수화

클래스 템플릿은 함수 템플릿과 마찬가지로 암시적 구체화, 명시적 구체화, 명시적 특수화를 사용할 수 있음

암시적 구체화(implicit instantiation)의 경우 해당 템플릿 클래스의 객체가 요구될 때 컴파일러가 생성함

template 키워드를 사용하여 클래스를 선언하면서 사용하려는 데이터형을 나타냈을 경우 컴파일러는 명시적 구체화(explicit instantiation)를 생성함
이 때 클래스 선언은 템플릿 정의와 동일한 이름 공간 안에 있어야하며, 클래스의 객체가 아직 생성되거나 언급되지 않았더라도 클래스 정의를 생성함

템플릿이 특정형에 맞게 구체화될 때 조금 다르게 행동하도록 수정해야하는 경우 명시적 특수화(explicit specialization)를 생성함
구체화 요구가 발생했을 때 특수화된 템플릿과 포괄적인 템플릿이 모두 적합하다면 컴파일러는 특수화된 버전을 우선적으로 사용함
특수화된 클래스 템플릿은 template <>class Classname<specialized-type-name> {...};의 형식을 사용함

C++에서는 데이터형 매개변수 중 어느 하나에 구체적인 데이터형을 제공하는 부분적인 특수화(partial specialization)을 허용함

  • 포괄적 템플릿 : template <class T1, class T2> class Pair {...};
  • 부분적 특수화 : template <class T1> class Pair<T1, int> {...};
  • 완전한 명시적 특수화 : template <> class Pair<int, int> {...};

여러가지 중에서 하나를 선택해야할시 컴파일러는 가장 특수화된 템플릿을 사용함
template <class t>가 포괄적인 버전일 때, template <class T*>와 같이 포인터들을 위한 특별한 버전을 제공함으로써 기존의 템플릿을 부분적으로 특수화시킬 수도 있음


멤버 템플릿

// tempmemb.cpp

#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class beta
{
	private:
		template <typename V>
		class hold
		{
			private:
				V val;
			public:
				hold(V v = 0) : val(v) {}
				void show() const { cout << val << endl; }
				V Value() const { return val; }
		};
	hold<T> q;
	hold<int> n;
	public:
		beta(T t, int i) : q(t), n(i) {}
		template <typename U>
		U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }
		void Show() const { q.show(); n.show(); }
};

int main()
{
	beta<double> guy(3.5, 3);
	cout << "T가 double로 설정되었습니다.\n";
	guy.Show();
	cout << "V가 double인 T로 설정되었고, 그 다음에 V가 int로 설정되었습니다.\n";
	cout << guy.blab(10, 2.3) << endl;
	cout << "U가 int로 설정되었습니다. \n";
	cout << guy.blab(10.0, 2.3) << endl;
	cout << "U가 double로 설정되었습니다. \n";
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

blab() 메소드의 U 데이터형은 메소드가 호출될 때의 매개변수에 값에 의해 암시적으로 결정되며, T 데이터형은 객체의 구체화 데이터형에 의해 결정됨

template <typename T>
class beta
{
	private:
		template <typename V>
		class hold;
		hold<T> q;
		hold<int> n;
	public:
		beta(T t, int i) : q(t), n(i) {}
		template<typename U>
		U blab(U u, T t);
		void Show() const { q.show(); n.show(); }
};

template <typename T>
	template <typename V>
	class beta<T>::hold
	{
		private:
			V val;
		public:
			hold(V v = 0) : val(v) {}
			void show() const { std::cout << val << std::endl; }
			V Value() const { return val; }
	};

template <typename T>
	template <typename U>
	U beta<T>::blab(U u, T t)
	{
		return (n.Value() + q.Value()) * u / t;
	}

위와 같이 beta 템플릿 안에 클래스와 메소드를 선언만 해둔 채로 템플릿 바깥에 정의할 수도 있음
이 때 템플릿들이 내포되어있기 때문에 template <typename T, typename V> 위와 같이 분리한 문법을 사용하여야함
멤버 템플릿이 beta<T> 클래스의 멤버라는 것을 알려주기 위해 사용 범위 결정 연산자를 사용함


매개변수 템플릿

// tempparm.cpp

#include <iostream>
#include "stacktp.h"

template <template <typename T> class Thing>
class Crab
{
	private:
		Thing<int> s1;
		Thing<double> s2;
	public:
		Crab() {};
		bool push(int a, double x) { return s1.push(a) && s2.push(x); }
		bool pop(int & a, double & x) { return s1.pop(a) && s2.pop(x); }
};

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	Crab<Stack> nebula;

	int ni;
	double nb;
	cout << "4 3.5와 같이 int double 쌍을 입력하세요(끝내려면 0 0) : \n";
	while (cin >> ni >> nb && ni > 0 && nb > 0)
	{
		if (!nebula.push(ni, nb))
			break;
	}
	
	while (nebula.pop(ni, nb))
		cout << ni << ", " << nb << endl;
	cout << "프로그램을 종료합니다.\n";

	return 0;
}

stacktp.h에 정의된 StackCrab 클래스의 템플릿 매개변수로 선언한 Thing과 선언이 일치하기 때문에 사용할 수 있음

template <template <typename T> class Thing, typename U, typename V>와 같이 템플릿 매개변수를 일반 매개변수와 혼합하여 사용할 수도 있음


템플릿 클래스와 프렌드 함수

템플릿 클래스 선언도 프렌드를 가질 수 있음

  • 템플릿이 아닌 프렌드
  • 바운드 템플릿 프렌드 : 클래스가 구체화될 때 클래스의 데이터형에 의해 프렌드의 데이터형이 결정
  • 언바운드 템플릿 프렌드 : 프렌드의 모든 특수화가 그 클래스의 각 특수화에 대해 프렌드

템플릿이 아닌 프렌드 함수

template <class T>
class HasFriend
{
	friend void counts();
	friend void report(HasFriend<T> &);
	...
};

counts() 함수는 템플릿의 가능한 모든 구체화들에 대해 프렌드가 됨
counts() 함수는 멤버 함수가 아니기 때문에 객체에 의해 호출되지 않음

  • 전역 객체에 접근
  • 전역 포인터를 사용하여 전역이 아닌 객체에 접근
  • 템플릿 클래스의 static 데이터 멤버에 접근
// frnd2tmp.cpp

#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class HasFriend
{
	private:
		T item;
		static int ct;
	public:
		HasFriend(const T & i) : item(i) { ct++; }
		~HasFriend() { ct--; }
		friend void counts();
		friend void report(HasFriend<T> &);
};

template <typename T>
int HasFriend<T>::ct = 0;

void counts()
{
	cout << "int 카운트 : " << HasFriend<int>::ct << "; ";
	cout << "double 카운트 : " << HasFriend<double>::ct << endl;
}

void report(HasFriend<int> & hf)
{
	cout << "HasFriend<int> : " << hf.item << endl;
}

void report(HasFriend<double> & hf)
{
	cout << "HasFriend<double> : " << hf.item << endl;
}

int main()
{
	cout << "객체가 선언되지 않았다 : ";
	counts();
	HasFriend<int> hfi1(10);
	cout << "hfi1 선언 후 : ";
	counts();
	HasFriend<int> hfi2(20);
	cout << "hfi2 선언 후 : ";
	counts();
	HasFriend<double> hfdb(10.5);
	cout << "hfdb 선언 후 : ";
	counts();
	report(hfi1);
	report(hfi2);
	report(hfdb);

	return 0;
}

프렌드 함수의 매개변수로 템플릿을 사용하기 위해서는 명시적 특수화를 정의해주어야함

바운드 템플릿 프렌드 함수

  1. 클래스 정의 앞에 템플릿 함수를 각각 선언
template <typename T> void counts();
template <typename T> void report(T &);
  1. 해당 함수 안에서 다시 템플릿들을 프렌드로 선언
template <typename TT>
class HasFriendT
{
	...
	friend void counts<TT>();
	friend void report<>(HasFriendT<TT> &);
};
  1. 프렌드들을 위한 정의를 제공
// tmp2tmp.cpp

#include <iostream>
using std::cout;
using std::endl;

template <typename T> void counts();
template <typename T> void report(T &);

template <typename TT>
class HasFriendT
{
	private:
		TT item;
		static int ct;
	public:
		HasFriendT(const TT & i) : item(i) { ct++; }
		~HasFriendT() { ct--; }
		friend void counts<TT>();
		friend void report<>(HasFriendT<TT> &);
};

template <typename T>
int HasFriendT<T>::ct = 0;

template <typename T>
void counts()
{
	cout << "템플릿 크기 : " << sizeof(HasFriendT<T>) << "; ";
	cout << "템플릿 counts() : " << HasFriendT<T>::ct << endl;
}

template <typename T>
void report(T & hf)
{
	cout << hf.item << endl;
}

int main()
{
	counts<int>();
	HasFriendT<int> hfi1(10);
	HasFriendT<int> hfi2(20);
	HasFriendT<double> hfdb(10.5);

	report(hfi1);
	report(hfi2);
	report(hfdb);
	cout << "counts<int>() 출력 : \n";
	counts<int>();
	cout << "counts<double>() 출력 : \n";
	counts<double>();

	return 0;
}

언바운드 템플릿 프렌드 함수

// manyfrnd.cpp

#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class ManyFriend
{
	private:
		T item;
	public:
		ManyFriend(const T & i) : item(i) {}
		template <typename C, typename D> friend void show2(C &, D &);
};

template <typename C, typename D> void show2(C & c, D & d)
{
	cout << c.item << ", " << d.item << endl;
}

int main()
{
	ManyFriend<int> hfi1(10);
	ManyFriend<int> hfi2(20);
	ManyFriend<double> hfdb(10.5);
	cout << "hfi1, hfi2 : ";
	show2(hfi1, hfi2);
	cout << "hfdb, hfi2 : ";
	show2(hfdb, hfi2);

	return 0;
}

템플릿을 클래스 안에 선언하여 언바운드 프렌드 함수를 생성 가능함
이 때 모든 함수 특수화는 모든 클래스 특수화에 대해 프렌드가 됨


템플릿 별칭(C++11)

typedef std::array<double, 12> arrd;
typedef std::array<int, 12> arri;`
typedef std::array<std::string, 12> arrst;
arrd gallons;
arri days;
arrst months;

템플릿 특수화를 위한 별칭을 만들기 위해 typedef를 사용할 수 있음

template<typename T>
	using arrtype = std::array<T, 12>;

arrtype<double> gallons;
arrtype<int> days;
arrtype<std::string> months;

arrtype을 데이터형으로 사용할 수 있는 템플릿 별칭으로 설정함
즉, arrtype<T>std::array<T,12> 자료형을 의미함

typedef const char*pc1;
using pc2 = const char*;
typedef const int*(*pa1)[10];
using pa2 = const int*(*)[10];

C++11에서는 using 구문을 비템플릿으로 확장하여 typedef와 동일한 기능을 하도록 함
이는 데이터형의 이름과 정보를 명확하게 분리하고 있어 읽기 편함



연습문제

  1. a) public 상속
    b) private 상속
    c) public 상속
    d) private 상속
    e) 각각 public, private 상속
  2. class Frabjous
    {
    	private:
    		char fab[20];
    	public:
    		Frabjous(const char * s = "C++") : fab(s) {}
    		virtual void tell() { cout << fab; }
    };
    
    class Gloam
    {
    	private:
    		int glip;
    		Frabjous fb;
    	public:
    		Gloam(int g = 0, const char * s = "C++");
    		Gloam(int g, const Frabjous & f);
    		void tell();
    };
    
    Gloam::Gloam(int g, const char * s) : glip(g), fb(s) {}
    Gloam::Gloam(int g, const Frabjous & f) : glip(g), fb(f) {}
    Gloam::tell()
    {
    	std::cout << glip << std::endl;
    	fb.tell();
    }
  3. class Frabjous
    {
    	private:
    		char fab[20];
    	public:
    		Frabjous(const char * s = "C++") : fab(s) {}
    		virtual void tell() { cout << fab; }
    };
    
    class Gloam : private Frabjous
    {
    	private:
    		int glip;
    	public:
    		Gloam(int g = 0, const char * s = "C++");
    		Gloam(int g, const Frabjous & f);
    		void tell();
    };
    
    Gloam::Gloam(int g, const char * s) : glip(g), Frabjous(s) {}
    Gloam::Gloam(int g, const Frabjous & f) : glip(g), Frabjous(f) {}
    Gloam::tell()
    {
    	std::cout << glip << std::endl;
    	Frabjous::tell();
    }
  4. class Stack<Worker *>
    {
    	private:
    		enum {MAX = 10};
    		Worker * items[MAX];
    		int top;
    	public:
    		Stack();
    		bool isempty();
    		bool isfull();
    		bool push(const Worker * & item);
    		bool pop(Worker * & item);
    };
  5. ArrayTP<string> sa;
    Stack<ArrayTP<double>> stk_arr_db;
    ArrayTP<Stack<Worker*>> arr_stk_worker;
  6. 어떤 클래스의 부모들이 공통으로 가지는 조상 클래스를 가상 기초 클래스로 설정한 경우 해당 클래스가 부모들로부터 상속을 받았을 때 조상 클래스의 멤버가 중복되지 않음

'개인공부 > C++ 기초플러스' 카테고리의 다른 글

C++ Primer 15  (0) 2023.02.21
C++ Primer 14 Exercise  (0) 2023.02.21
C++ Primer 13 Exercise  (0) 2023.02.19
C++ Primer 13  (0) 2023.02.19
C++ Primer 12 Exercise  (0) 2023.02.19

댓글