2024. 4. 2. 10:15ㆍProgramming Language/C++
아래 링크 클릭 시 해당 본문으로 이동
템플릿 (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;
}
'Programming Language > C++' 카테고리의 다른 글
[C++] 접근 지정자 (0) | 2024.04.01 |
---|---|
[C++] 래퍼런스 (vs. 포인터) (0) | 2024.04.01 |
[C/C++] 가변 배열 - 구조체/분할 구현 (0) | 2024.03.28 |
[C++] wchar_t 배열 함수 - wcslen(), wcscat_s(), wcscmp(), wcscpy_s() (0) | 2024.03.27 |
[C++] 문자열 함수 - strlen(), strcat_s(), strcmp(), strcpy_s() (0) | 2024.03.27 |