지난 포스팅에서는 has-a 관계를 구현하기 위한 컴포지션(composition) 또는 레이어링(layering)이란 기법에 대해 소개했었다. 하지만 has-a 관계를 표현하기 위한 방법은 이 뿐만이 아니다. 이번에는 또 다른 방식으로 has-a 관계를 표현할 수 있는 private 상속에 대해 소개하고자 한다. 덧붙여서 protected 상속과 기초클래스의 메서드를 using 키워드를 이용해 멤버 함수로서 기능하게 만드는 방법까지 소개하고 정리하고자 한다.

 


목차

  • private 상속, using 멤버 함수
  • protected 상속, private 상속과의 차이점

 

 


private 상속

C++에서는 has-a 관계를 구현하는 또 다른 방법으로 private 상속을 제공하고 있다. 기초클래스로부터 파생클래스로 private 상속이 될 경우 기초클래스의 public과 protected 멤버는 파생클래스의 private 멤버로 변환되어 상속된다. 즉, 기초클래스의 public 멤버들이 파생클래스의 public 인터페이스가 되지 않고 private 멤버가 된다는 이야기다.

 

일반적으로 public 상속에서는 기초클래스의 public 멤버, 즉 기초클래스의 인터페이스가 파생클래스의 인터페이스로 넘겨줌으로서 is-a관계를 구현한다. 반면에 private 상속은 기초클래스의 인터페이스를 private 형태로 상속하므로 인터페이스를 넘겨주지 않는다. 파생클래스가 기초클래스를 소유하고 있을 뿐이다. 이건 has-a 관계를 의미한다.

 

 다음은 표준 라이브러리의 string과 valarray 클래스를 private 상속으로 활용해서 구현한 클래스이다.

#ifndef __TEST_CLASS_H__
#define __TEST_CLASS_H__

#include <iostream>
#include <string>
#include <valarray>

class Student : private std::string, private std::valarray<double>
{
private:
	typedef std::valarray<double>      m_scores;
	typedef std::string                m_name;
	std::ostream& printScore(std::ostream& os) const;
public:
	Student() : m_name("Null"), m_scores() {}
	explicit Student(const char* str) : m_name(str), m_scores() {}
	explicit Student(int n) : m_name("Null"), m_scores(n) {}
	explicit Student(const char* str, const double pd, int n) : m_name(str), m_scores(pd, n) {}
	~Student() {}

	using::std::valarray<double>::size;
	using::std::valarray<double>::operator[];
	using::std::valarray<double>::operator=;

	friend std::ostream& operator<<(std::ostream& os, const Student& t);
};
#endif

 

  • typedef std::valarray<double> m_scores;
    typedef 를 통해 valarray<double> 자료형의 이름을 m_scores로 바꾼다.
  • typedef std::string m_name;
    typedef 를 통해 string 자료형의 이름을 m_name로 바꾼다.
  • std::ostream&  printScore(std::ostream& os) const;
    m_scores(valarray) 에 담긴 점수를 5열로 나누어 줄력한다.
  • Student() : m_name("Null"), m_scores() {}
    이니셜라이징을 통해 이름과 형태가 없는 클래스들을 초기화한다.
  • explicit Student(const char* str) : m_name(str), m_scores() {}
    explicit로 암시적 형변환을 제한하고 m_name(string) 클래스에 값을 초기화한다.
  • using::std::valarray::size, using::std::valarray<double>::operator[];
    private 상속이 되었으므로 using을 이용해 관련 멤버 함수들에 접근이 가능하다.

 

 

여기서 생성자에 explicit를 사용하는 이유프로그래머의 실수를 미리 차단하기 위해서이다. 만약 프로그래머가 student[5] = 3.0; 가 아닌 student = 3.0; 으로 잘못 적을 경우 원치않는 상황이 발생할 수 있으므로 기능적으로 제약을 건다. 그리고 using::std::valarray<double>::size 와 같은 형태는 private 상속으로 기초클래스 string과 valarray에 대한 기능을 모두 가지게 됐으므로 사용할 수 있는 문법이다. (using을 통해 기초클래스들의 멤버 함수를 자신의 멤버 함수처럼 기능하게 한다.)

 

이것이 private 상속의 장점이자 핵심이라고 볼 수 있다. private 상속을 사용하면 기초클래스의 virtual 함수를 재정의 하거나 protected 멤버에 접근이 가능해진다는 점을 이용한 것이다. 하지만 안정성과 사용 난이도의 문제로 has-a 관계를 구현할 땐 컴포지션을 이용하는게 일반적이다.

 

아래는 위 헤더파일의 정의 부분과 main 함수의 전문이다.

// testClass.cpp
#include "testClass.h"
std::ostream& Student::printScore(std::ostream& os) const
{
	int i;
	int size = m_scores::size();
	if (size > 0)
	{
		for (i = 0; i < size; i++)
		{
			os << m_scores::operator[](i) << ' ';
			if (i % 5 == 4)
				os << std::endl;
		}
		if (i % 5 != 0)
			os << std::endl;
	}
	else
		os << "빈 배열";
	return os;
}

std::ostream& operator<<(std::ostream& os, const Student& t)
{
	// 이름 출력
	os << (const std::string&)t << std::endl;  // 기초클래스의 형태로 명시적 형변환
	// 점수 출력
	t.printScore(os);                          // private 멤버 함수 이용
	
	return os;
}

 

// main function
#include <iostream>
#include "testClass.h"

int main(void)
{
	using namespace std;
	Student A("Fubao", 0, 5);    // 0점으로 5과목 초기화
	cout << A << endl << endl << endl;

	for (int i = 0; i < A.size(); i++)    // 과목마다 점수 입력
	{
		A[i] = (i + 10) * 2;
	}

	cout << A << endl << endl << endl;
	return 0;
}

 

여기서 입출력 연산자 오버로딩을 살펴보자.

std::ostream& operator<<(std::ostream& os, const Student& t)
{
	// 이름 출력
	os << (const std::string&)t << std::endl;  // 기초클래스의 형태로 명시적 형변환
	// 점수 출력
	t.printScore(os);                          // private 멤버 함수 이용
	
	return os;
}

 

이름을 출력하는 구문에서 Student t를 string으로 명시적 형변환하는걸 볼 수 있다. 이렇게 하는 이유는 Student 클래스는 string과 valarray를 다중상속받고 있기 때문에 os가 t를 매개변수로 취하는데 있어서 모호함이 발생하게 된다. 그래서 파생클래스는 기초클래스로 업캐스팅(up casting)이 가능하다는 점을 이용해서 t를 string으로 명시적 형변환 해주고 모호한 문제를 해결해주었다.

 

 

C++에서의 제한

프로그래머가 코드 작성 방식에 제한을 걸 수 있는 다양한 방법들이 C++에 존재한다. 이를테면 암시적 형변환을 차단하는 explicit, 데이터를 변경시킬 권한이 있는 멤버 함수의 사용을 제한하는 const, 그 밖에도 여러가지가 있다. 이렇게 많은 제한들이 존재하는 이유는, 실행 시에 일어나는 에러보다도 컴파일 단계에서 일어나는 에러가 여러모로 낫기 때문이다.

 


 

protected 상속

protected 상속은 private 상속과 마찬가지로 파생클래스의 인터페이스로서 기능하지 않는다. 둘 다 기능상 동일해보여서 똑같은거 아니야? 라고 생각할 수도 있겠지만 둘의 차이점파생클래스에서 또 다른 클래스를 상속할 때 나타난다. 

 

  • A의 기초클래스로부터 private 상속이 된 파생클래스 B에서 다시 상속된 파생클래스 C

B는 A클래스의 protected와 public 함수에 대해서 접근 권한을 얻게되지만, private 상속을 받은 멤버와 함수들은 B클래스에서 private가 되므로 C클래스에서는 B클래스의 private 멤버에 접근할 수 없게된다.

 

  • A의 기초클래스로부터 protected 상속이 된 파생클래스 B에서 다시 상속된 파생클래스 C

B는 A클래스의 protected와 public 함수에 대해서 접근 권한을 얻게된다, protected 상속을 받은 멤버와 함수들은 B클래스에서 protected가 되므로,C클래스에서도 B클래스의 protected 멤버에 접근할 수 있다..