[C++] 템플릿 (함수/클래스)

2024. 4. 2. 10:15Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

템플릿 (Template)

▷ 전체 코드


템플릿 (Template)

template : 주형(용해된 금속을 주입하여 주물을 만드는 데 사용하는 )

• `<typename T>`에서 `T`는 템플릿 타입 매개변수로, 템플릿에 사용될 데이터 타입에 대한 자리 표시자(placeholder) 역할을 한다.

• 보통 `typename`과 `T`로 작성하지만 내 마음대로 작성해도 된다.

(ex. class로 적어도 되지만, 여기서 class는 우리가 알고 있는 class가 아니다.)

※ 함수 템플릿은 함수가 아니고, 클래스 템플릿은 클래스가 아니다.


함수 템플릿

함수를 찍어내는 템플릿(틀)
함수 오버로딩(함수명 동일하지만 인자 개수 또는 타입이 다른 게 가능)을 사용할 때 사용한다.
• 거의 기능이 유사한데 몇 가지 사항의 차이 때문에 똑같은 함수를 프로그래머가 타이핑 쳐야 하는 상황이 있다.

template<typename T>
T Add(T a, T b)
{
	return a + b;
}

int main()
{
    // Add() 함수 호출
	int i = Add<int>(10, 20);  // T 부분에 int가 들어간 함수를 요청한다.
	// 만약 윗 줄이 없다면?
		// 프로그램 내에서 Add() 함수는 존재하지 않게 된다.
	// 템플릿은 해당 typename 자리에 원하는 것을 요청했을 때 그 버전의 함수가 만들어진다.
	// 즉, 함수 틀을 만들고 틀로 찍어내지 않으면 그 함수는 사용하지 않았으므로 존재하지 않게 된다.
	// 함수를 여러 번 호출해도 함수가 여러 개 생기는 것은 아니다.
	// 위 예시의 경우 int 버전은 딱 하나만 만들어진다.
	Add<int>(100, 200);

	return 0;
}

※ 주의

`<자료형>` 부분을 생략해도 컴파일러가 알아서 추가해 주지만, 함수인지 함수 템플릿인지 애매하고 실수할 여지도 있기 때문에 명확하게 작성해 주는 것이 좋다.

i = Add(10, 20);  // 컴파일러가 알아서 반환 타입이 int인 것을 파악하고 T 부분에 int를 넣는다.
// ※ 이때 Add()는 함수가 아니라 함수 템플릿이다.

클래스 템플릿

클래스 틀을 만들고 찍어내는 것
• 함수 템플릿과 마찬가지로 몇 가지 사항만 다른 유사한 클래스를 여러 개 타이핑 쳐야 할 때 사용한다.
• 사용자가 저장하는 데이터 타입을 고르고 이를 저장시키는 클래스를 선언한다.
그렇게 만들어진 클래스의 객체를 사용한다.

• 클래스들을 만들 수 있는 근본적인 원형이다.
때문에 타입을 정해줘야 그런 버전의 클래스가 생기고 그 객체를 사용한다.

CArr.h

// 클래스 템플릿
template<typename T>
// T는 가변 배열에 저장될 데이터 타입이다.
class CArr
{
private:
	T* m_pData;  // 일단 템플릿 부분을 지우거나 주석 처리 후 <Ctrl + R + R>로 전체적으로 해당 이름을 바꾸자.
	int m_iCount;  // 현재 들어와있는 데이터 개수
	int m_iMaxCount;  // 최대 개수
	// ※ 무지성으로 다 T로 바꾸지 않게 주의하기

public:
	void push_back(const T& _Data);  // 데이터 추가
	// 데이터 단위가 크면 지역 변수에 복사하는 것 자체가 비효율적이다.
	// 그래서 T타입이 커질 것을 대비하여 주소를 주거나 참조를 하는 것이 좋다.
	// 원본은 수정하지 않고 참조만 받을 것이기 때문에 const T& 타입으로 했다.

	void resize(int _iResizeCount);  // 재할당 => 몇 칸으로 늘릴 것인지를 _iResizeCount로 받는다.
	// 데이터 타입과 무관한 가변 배열의 개수이기 때문에 int가 맞다.

	T& operator[] (int idx);

public:
	CArr();
	~CArr();
};

//----------CArr.cpp에 있던 내용은 CArr.h에 있어야 한다.----------//
// 생성자
template<typename T>
CArr<T>::CArr()  // CArr 클래스의 T 버전 안에 구현되어있는 생성자를 지칭한다.
	: m_pData(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	m_pData = new T[2];
}

// 소멸자
template<typename T>
CArr<T>::~CArr()
{
	delete[] m_pData;
}

 

main.cpp

int main()
{
    //CArr carr;  // 오류 => 더이상 클래스가 아닌 클래스 템플릿이기 때문이다.
	
	// int 버전
	CArr<int> icarr;
	icarr.push_back(10);
	icarr.push_back(20);
	icarr.push_back(30);

	int iData = icarr[1];
	icarr[1] = 100;

	// float 버전
	CArr<float> fcarr;
	fcarr.push_back(2.64f);
	fcarr.push_back(12.2f);
	fcarr.push_back(8.29f);

	float fData = fcarr[1];

	return 0;
}

`CArr.cpp` 코드를 `CArr.h`에 모두 옮기기

`CArr.cpp`에서 `CArr.h`로 옮길 때 `CArr.cpp` 파일에 구현한 모든 코드들은 `CArr.h`에 있어야 한다.

헤더 파일에는 실제 구현이 아닌 선언만 포함된다.

`main.cpp`에서 `<float>`를 사용하는 경우 `T`를 `float`로 바꾸면 된다.

하지만 `float`에 대한 구현은 아직 헤더 파일에 없다.

즉, `float`에 대한 함수는 구현되지 않았기 때문에 헤더 파일에도 없다.

`main.cpp`에서는 아직 구현되지 않았음에도 불구하고 `float`가 있는 버전이 존재한다고 헤더 파일에서 선언했기 때문에 오류를 생성하지 않는다.

헤더 파일을 옮기기 전에 cpp 파일에서 오류가 발생한 이유

`CArr.cpp` 파일에 템플릿 `<typename T>`은 실제 클래스가 아닌 원형들만 선언되어 있고 이를 무엇으로 만들어줘야 되는지에 관한 정보는 없다.

`float` 버전이 요청되었다는 사실을 cpp파일을 컴파일할 때는 컴파일러가 알지 못한다.

그래서 헤더 파일에 옮기기 전에 cpp 파일에서 오류가 잔뜩 있던 것이다.

 

※ 요약

• 헤더를 참조하는 사용자 입장에서 특정 버전을 요청했을 때 관련된 부분을 한 번에 만들어줄 수 있다.

 기존 클래스나 함수는 해당 cpp에서 즉시 사용될 것이기 때문에 컴파일이 진행된다.

• 반면 템플릿은 바로 사용되지 않고 타입을 정해준 버전이 나와줘야 만들어진다.

• 사용자 코드를 보는 시점에서 헤더 파일로 가봤자 실제 구현 부분은 없다.

• 따라서 템플릿은 헤더 안에 구현까지 되어있어야 된다.

CArr.h

#pragma once
class CArr  // (C는 class라는 의미로 사용했다.)
{
private:
	int* m_pInt;  // 주소값 (m = 멤버라는 의미로 사용했다.)
	int m_iCount;  // 현재 들어와있는 데이터 개수
	int m_iMaxCount;  // 최대 개수
	// CArr의 객체를 만들면 그 크기는 (64bit이기 때문에)int* 8byte + int 4byte + int 4byte = 총 16byte가 만들어질 것이다.

public:
	// 어떤 객체인지 알려줄 필요가 없다.
	// 애초에 멤버 함수는 객체를 통해서 호출되기 때문에 호출될 때 this로 객체의 주소가 넘어온다.
	// 가변 배열로 따지면 어차피 this에 해당하니까 tArr* _pArr 부분이 생략되는 것이다.
	void push_back(int _iData);  // 가변 배열로 따지면 데이터 추가 함수이다.

public:
	// public 필드에 생성자를 둔다.
		// 멤버 함수도 private으로 감추면 외부에 공개되지 않는다.
		// 클래스 객체를 만들면 컴파일러가 알아서 생성자를 호출하는데, 생성자가 비공개면 호출할 수가 없다.
	// 헤더 파일이기 때문에 구현은 하지 않고 cpp 파일에 구현해야 한다.
	CArr();  // 가변 배열로 따지면 => 배열 초기화 함수
	~CArr();  // 가변 배열로 따지면 => 배열 메모리 해제 함수
};

 

CArr.cpp

#include "CArr.h"

// CArr에 선언되어있는 생성자와 소멸자는 다음과 같이 정의하겠다.
// 생성자
	// 객체가 만들어지는 순간에 호출된다.
CArr::CArr()  // 범위 지정 연산자(::)로, 여러 범위에서 사용된 식별자를 구분할 때 사용한다.
	: m_pInt(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	m_pInt = new int[2];  // int 자료형 2개만큼 할당한다.
}

// 소멸자
	// 소멸자는 이 클래스의 객체가 해제되는 순간에 호출된다.
CArr::~CArr()
{
	delete[] m_pInt;
	// 메모리의 시작 주소로 접근해서 클래스 포인터가 각각의 객체마다 소멸자를 호출시킨다.
	// 할당한 끝부분에 마킹용으로 마감 비트값으로 동적 할당 끝을 알 수 있다. (ex. 문자열의 끝이 0인 것처럼)
}

CArr.h

 

#pragma once

#include <assert.h>

// 클래스 템플릿
template<typename T>
// T는 가변 배열에 저장될 데이터 타입이다.
class CArr
{
private:
	T* m_pData;  // 일단 템플릿 부분을 지우거나 주석 처리 후 <Ctrl + R + R>로 전체적으로 해당 이름을 바꾸자.
	int m_iCount;  // 현재 들어와있는 데이터 개수
	int m_iMaxCount;  // 최대 개수
	// ※ 무지성으로 다 T로 바꾸지 않게 주의하기

public:
	void push_back(const T& _Data);  // 데이터 추가
	// 데이터 단위가 크면 지역 변수에 복사하는 것 자체가 비효율적이다.
	// 그래서 T타입이 커질 것을 대비하여 주소를 주거나 참조를 하는 것이 좋다.
	// 원본은 수정하지 않고 참조만 받을 것이기 때문에 const T& 타입으로 했다.

	void resize(int _iResizeCount);  // 재할당 => 몇 칸으로 늘릴 것인지를 _iResizeCount로 받는다.
	// 데이터 타입과 무관한 가변 배열의 개수이기 때문에 int가 맞다.

	T& operator[] (int idx);

public:
	CArr();
	~CArr();
};

//----------CArr.cpp에 있던 내용은 CArr.h에 있어야 한다.----------//
// 생성자
template<typename T>
CArr<T>::CArr()  // CArr 클래스의 T 버전 안에 구현되어있는 생성자를 지칭한다.
	: m_pData(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	m_pData = new T[2];
}

// 소멸자
template<typename T>
CArr<T>::~CArr()
{
	delete[] m_pData;
}

template<typename T>
void CArr<T>::push_back(const T& _Data)
{
	int i = 0;

	if (m_iMaxCount <= m_iCount)
	{
		// 재할당
		resize(m_iMaxCount * 2);
	}

	// 데이터 추가
	m_pData[m_iCount++] = _Data;
}

template<typename T>
void CArr<T>::resize(int _iResizeCount)
{
	if (m_iMaxCount >= _iResizeCount)
	{
		assert(nullptr);
	}

	// 1. resize시킬 개수만큼 동적 할당한다.
	T* pNew = new T[_iResizeCount];

	// 2. 기존 공간에 있던 데이터들을 새로 할당한 공간으로 복사시킨다.
	for (int i = 0; i < m_iCount; ++i)
	{
		pNew[i] = m_pData[i];
	}

	// 3. 기존 공간은 메모리 해제
	delete[] m_pData;

	// 4. 배열이 새로 할당된 공간을 가리키게 한다.
	m_pData = pNew;

	// 5. iMaxCount 변경점 적용
	m_iMaxCount = _iResizeCount;
}

template<typename T>
T& CArr<T>::operator[](int idx)
{
	// m_pInt가 가리키고 있는 인덱스 첫 번째 데이터를 반환한다.
	return m_pData[idx];
}

▷ 전체 코드

Arr.h

#pragma once

// 가변 배열 자료형 tArr (int)
typedef struct _tagArr
{
	int* pInt;
	int iCount;
	int iMaxCount;
}tArr;

// 배열 초기화(초기값을 줄뿐 문법적으로 진짜 초기화는 아니다.) 함수
	// 문법적으로 초기화는 객체가 만들어짐과 동시에 값이 주어지는 것인데, 이미 구조체에서 객체가 만들어졌기 때문에 실제로 초기화가 아니다.
void InitArr(tArr* _pArr);

// 데이터 추가 함수
void PushBack(tArr* _pArr, int _iData);

// 공간 추가 확장 함수
void Reallocate(tArr* _pArr);

// 배열 메모리 해제 함수
void ReleaseArr(tArr* _pArr);

 

Arr.cpp

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

// 배열 초기화
void InitArr(tArr* _pArr)
{
	_pArr->pInt = new int[2];  // new[]를 사용하여 메모리 할당
	_pArr->iCount = 0;
	_pArr->iMaxCount = 2;
}

// 공간 추가 확장
void Reallocate(tArr* _pArr)
{
	// 1. 2배 더 큰 공간을 동적할당한다.
	int* pNew = new int[_pArr->iMaxCount * 2];

	// 2. 기존 공간에 있던 데이터들을 새로 할당한 공간으로 복사시킨다.
	for (int i = 0; i < _pArr->iCount; ++i)
	{
		pNew[i] = _pArr->pInt[i];
	}

	// 3. 기존 공간은 메모리 해제
	delete[] _pArr->pInt;

	// 4. 배열이 새로 할당된 공간을 가리키게 한다.
	_pArr->pInt = pNew;

	// 5. iMaxCount 변경점 적용
	_pArr->iMaxCount *= 2;
}

// 뒤에 데이터 추가
void PushBack(tArr* _pArr, int _iData)
{
	if (_pArr->iMaxCount <= _pArr->iCount)
	{
		// 재할당
		Reallocate(_pArr);
	}

	// 데이터 추가
	_pArr->pInt[_pArr->iCount++] = _iData;
}

// 배열 메모리 해제
void ReleaseArr(tArr* _pArr)
{
	delete[] _pArr->pInt;
	_pArr->iCount = 0;
	_pArr->iMaxCount = 0;
}

 

CArr.h

#pragma once

#include <assert.h>

// 클래스 템플릿
template<typename T>
// T는 가변 배열에 저장될 데이터 타입이다.
class CArr
{
private:
	T* m_pData;  // 일단 템플릿 부분을 지우거나 주석 처리 후 <Ctrl + R + R>로 전체적으로 해당 이름을 바꾸자.
	int m_iCount;  // 현재 들어와있는 데이터 개수
	int m_iMaxCount;  // 최대 개수
	// ※ 무지성으로 다 T로 바꾸지 않게 주의하기

public:
	void push_back(const T& _Data);  // 데이터 추가
	// 데이터 단위가 크면 지역 변수에 복사하는 것 자체가 비효율적이다.
	// 그래서 T타입이 커질 것을 대비하여 주소를 주거나 참조를 하는 것이 좋다.
	// 원본은 수정하지 않고 참조만 받을 것이기 때문에 const T& 타입으로 했다.

	void resize(int _iResizeCount);  // 재할당 => 몇 칸으로 늘릴 것인지를 _iResizeCount로 받는다.
	// 데이터 타입과 무관한 가변 배열의 개수이기 때문에 int가 맞다.

	T& operator[] (int idx);

public:
	CArr();
	~CArr();
};

//----------CArr.cpp에 있던 내용은 CArr.h에 있어야 한다.----------//
// 생성자
template<typename T>
CArr<T>::CArr()  // CArr 클래스의 T 버전 안에 구현되어있는 생성자를 지칭한다.
	: m_pData(nullptr)
	, m_iCount(0)
	, m_iMaxCount(2)
{
	m_pData = new T[2];
}

// 소멸자
template<typename T>
CArr<T>::~CArr()
{
	delete[] m_pData;
}

template<typename T>
void CArr<T>::push_back(const T& _Data)
{
	int i = 0;

	if (m_iMaxCount <= m_iCount)
	{
		// 재할당
		resize(m_iMaxCount * 2);
	}

	// 데이터 추가
	m_pData[m_iCount++] = _Data;
}

template<typename T>
void CArr<T>::resize(int _iResizeCount)
{
	if (m_iMaxCount >= _iResizeCount)
	{
		assert(nullptr);
	}

	// 1. resize시킬 개수만큼 동적 할당한다.
	T* pNew = new T[_iResizeCount];

	// 2. 기존 공간에 있던 데이터들을 새로 할당한 공간으로 복사시킨다.
	for (int i = 0; i < m_iCount; ++i)
	{
		pNew[i] = m_pData[i];
	}

	// 3. 기존 공간은 메모리 해제
	delete[] m_pData;

	// 4. 배열이 새로 할당된 공간을 가리키게 한다.
	m_pData = pNew;

	// 5. iMaxCount 변경점 적용
	m_iMaxCount = _iResizeCount;
}

template<typename T>
T& CArr<T>::operator[](int idx)
{
	// m_pInt가 가리키고 있는 인덱스 첫 번째 데이터를 반환한다.
	return m_pData[idx];
}

 

main.cpp

#include <iostream>
#include "Arr.h"
#include "CArr.h"

// 함수 템플릿
template<typename T>
T Add(T a, T b)
{
	return a + b;
}

int main()
{
	// 템플릿
	// 1. 함수 템플릿 (예시 - Add())
	// Add() 함수 호출
	int i = Add<int>(10, 20);  // T 부분에 int가 들어간 함수를 요청한다.
	Add<int>(100, 200);

	// 2. 클래스 템플릿 (예시 - CArr.h)
	//CArr carr;  // 오류 => 더이상 클래스가 아닌 클래스 템플릿이기 때문이다.
	
	// int 버전
	CArr<int> icarr;
	icarr.push_back(10);
	icarr.push_back(20);
	icarr.push_back(30);

	int iData = icarr[1];
	icarr[1] = 100;

	// float 버전
	CArr<float> fcarr;
	fcarr.push_back(2.64f);
	fcarr.push_back(12.2f);
	fcarr.push_back(8.29f);

	float fData = fcarr[1];

	return 0;
}