복습을 마쳤고, 이제 가변배열을 구조체가 아니라
클래스로 구현해보고자 한다.
//iarr = (int*)malloc(sizeof(int) * 2);
iarr = new int[2];
위처럼 new를 하면, int형 2개짜리를 iarr이라는 포인터 변수에 넣어라 라는 말이다.
malloc이라는 void포인터를 사용하여 따로 명시해서 찝찝하게 사용하는 방법 대신에, new라는 예약어로 자료형을 명시해줄 수 있다.
해당 자료형의 크기만큼 공간이 만들어지고,
해당 자료형으로 해당 메모리를 인식하게 된다.
int main()
{
CArr c;
c.addArr(10);
c.addArr(20);
c.addArr(30);
return 0;
}
위처럼 매우 간단하게 구현이 가능하다.
int main()
{
SArr s;
addArr(&s, 100);
addArr(&s, 200);
addArr(&s, 300);
ReleaseArr(&s);
return 0;
}
원래라면 위처럼 구조체 객체의 주소를 넣어야 했다.
하지만 클래스의 멤버변수를 사용하여 접근하면, 알아서 c.으로
해당 객체가 전달되기 때문에 간단해진다.
그리고 Free로 힙메모리 해제를 굳이 해줄 필요가 없는것이,
main이라는 스택이 자동으로 해제되면서 클래스 객체도 해제되고,
자동으로 소멸자가 호출이 될건데, 해당 소멸자에 delete[]를 구현해 놓았기 때문이다.
#include "CArr.h"
#include <iostream>
#include <assert.h>
//클래스 선언부 밖에서 멤버함수를 구현하면, 명시해야한다.
CArr::CArr()
:iarr(nullptr), iCount(0), maxCount(2)
{
//iarr = (int*)malloc(sizeof(int) * 2);
iarr = new int[2];
}
CArr::~CArr()
{
//free(iarr);
delete[] iarr; //단 하나의 값이라면, delete arr
}
void CArr::addArr(int _iData)
{
if (iCount >= maxCount) {
//재할당
resizeArr(maxCount * 2);
}
iarr[iCount] = _iData;
iCount++;
}
//리사이즈하고, 이사시킨다.
void CArr::resizeArr(int size)
{
if (size <= maxCount) {
//오류, 더 큰 수로 할당해야함
assert(nullptr);
}
int* tmp = new int[size];
for (int i = 0; i < iCount; i++) {
tmp[i] = iarr[i];
}
delete[] iarr;
iarr = tmp;
maxCount = size;
}
위처럼 구현을 해보았다.
각각 생성자소멸자, 데이터추가, 배열이므로 크기의 증가
#pragma once
class CArr
{
private:
int* iarr;
int iCount;
int maxCount;
public:
CArr();
~CArr();
public:
void addArr(int _iData);
void resizeArr(int size);
//void resizeArr(CArr* arr); << 객체를 가져오지 않아도 된다.
//위에 addArr도, addArr(CArr* arr, int _iData);인 것이다.
그리고 헤더는 위와 같이 접근을 제한하였다.
직접 데이터에 접근을 할 수 없다.
가변배열이므로 배열답게,
특정 인덱스의 값을 가져오는 기능을 추가하려고 한다.
int getData = c[1];
이런걸 어떻게 구현할까
int* CArr::operator[](int index) {
return iarr + index;
}
위의 연산자함수를 재정의하여 사용한다.
현재 할당된 메모리 공간에 index만큼을 추가하면,
메모리 공간이 new int[]로 선언이 되었기 떄문에 int형만큼 배가 되어 추가되고,
해당 주솟값을 return 한다.
int* getData = c[1];
printf("%d", *getData); //20
*c[2] = 30;
getData = c[2];
printf("%d", *getData); //30
그러면
c[]이 오른쪽에 있을때는 해당 주솟값을 리턴하여 포인터에 저장하고,
c[]이 왼쪽에 있을때는 해당 주솟값을 리턴한것의 참조를 하여 값을 저장한다.
근데 기존에 쓰던 배열과는 참조기호도 붙어있고 뭔가 이상하다.
int CArr::operator[](int index) {
return iarr[index];
}
이렇게 포인터를 떼고 쓴다고 친다면,
값을 가져오는건 가능하겠지만,
대입하는건 할 수가 없을 것이다. 왜냐면 지역변수처럼 특정 공간에
iarr[2]이런 값을 넣어두고 그 tmp메모리의 값을 바꾸기 때문이다.
레퍼런스가 여기서 쓰인다.
int& iRef = a;
iRef = 100;
레퍼런스는 a의 주솟값을 가져와서 iRef에 대입을 하는데,
iRef에는 주솟값이 들어있음에도 불구하고,
iRef를 다시 사용하면, 참조를 붙이지 않고도 a값을 수정할 수 있다.
즉, 지금같이 참조를 굳이 쓰지 않고 싶은 상황에서 유용한 것이다.
int& CArr::operator[](int index) {
return iarr[index];
-----main-----
int getData = c[1];
printf("%d", getData);
c[2] = 30;
getData = c[2];
printf("%d", getData);
}
위처럼 사용이 가능해진다는 것이다.
int&를 return 한다는 것의 의미는,
반환되는 것을 참조하는 것이, 반환되는 것과 동일시된다는 의미이다.
(참조하는 메모리의 위치, 그리고 값 모든 것이)
즉, c객체의 iarr의 index위치의 주솟값을 getData에 넣고,
getData를 c객체의 iarr[index]와 동일하게 보겠다는 것이다.
배열이 주소의 의미도 있지만, 해당 값의 의미도 있어서 살짝 헷갈림
주소를 담을 수 있지만, 참조할때는 참조기호를 굳이 붙이지 않아도 되는
레퍼런스라는 특이한 기능 떄문에 가능한 일이다.
template<typename TT> TT Add(TT a, TT b)
{
return a + b;
}
위처럼 template<typename ~> 으로 선언하는데,
해당 typename이 자료형으로 선언한다면,
어떤 함수를 만들 때, 같은 기능이라면, 자료형만 다르게 구현을 하지 않아도 된다.
int a = Add<int>(2, 3);
위처럼 TT부분에 int가 들어가도록 대체되어 실행이 된다.
float로도 설정이 가능한 것이다.
함수템플릿이라는 것은,
템플릿의 typename자리에 어떤게 들어갈지 요청을 해야, 해당 자료형의 함수가 자동으로 만들어지는데,(float형은 또 따로 만들어지고)
그렇기 떄문에 함수와는 다르고,
template라는 주형틀의 의미처럼, 함수를 찍어내는 어떤 공장같은거지,
함수 그 자체는 아니다.
클래스에서도 템플릿을 사용할 수 있다.
class CArr
{
private:
int* iarr;
int iCount;
int maxCount;
이전에 만든 클래스에서는 int*로 int형의 자료만 담을 수 있는
가변배열을 구현해보았다.
하지만 해당 클래스는 평생 int만 담아야 한다.
저장하고 싶은 타입마다 클래스를 만든다면
모든 멤버함수들을 그때마다 다시 만들어야될 것이다.
template <typename T>
class CArr
{
private:
T* iarr;
int iCount;
int maxCount;
사용방법은 같다.
함수처럼 그냥 주형틀처럼 만들고 싶은 것 위에다 써주면 된다.
#pragma once
template <typename T>
class CArr
{
private:
T* iarr;
int iCount;
int maxCount;
public:
CArr();
~CArr();
public:
void addArr(T _iData);
void resizeArr(int size);
//void resizeArr(CArr* arr); << 객체를 가져오지 않아도 된다.
//위에 addArr도, addArr(CArr* arr, int _iData);인 것이다.
public:
T& operator[] (int index);
};
그렇게 위처럼 T에 대해서 바꾸어 주었는데,
public:
void addArr(T _iData);
이 부분이 나중에 문제가 될 수 있다고 한다.
왜냐하면, T는 어떤자료형이든지 가능해서,
엄청나게 큰 구조체까지도 가능하기 때문에,
지역변수로 임시로 해당 값을 받아와서, 해당값을 추가하는 이 과정이 너무 비용이 커질 수 있기 때문에, 직접 해당 값을 참조하여 가져오는
레퍼런스 방식으로 사용할 수도 있다고 한다.
public:
void addArr(const T& _iData);
const와 레퍼런스를 붙였다.
const를 붙인 이유는, const-(포인터-const)이런 느낌인데,
해당 값을 임의로 변경하지 않겠다는 의미의 방어적 코드이다.
추가로
클래스 템플릿을 만들때 가장 중요한 것중 하나가,
템플릿의 함수들의 구현은 무조건 헤더에 있어야 한다고 한다.
그 이유는,
typename당 주형틀로 찍어서 만들게 되는데,
cpp로 만들었다면, 해당 파일어서 선언한 주형틀만 사용이 가능하고,
다른 cpp에서 같은 자료형을 썼어도, 다시 또 찍어야 하기 때문에,
헤더에서 모든걸 다 선언한다.
템플릿클래스를 헤더와 cpp로 나눠서 구현한다면,
헤더는 #include로 인해서 컴파일타임에 존재를 알리게되고,
cpp파일은 링크타임에 존재를 알리게되는데,
헤더라는게 있고, 기능이 존재하는데 구현이 안되어있다는 것을
컴파일러가 인식을 하고 오류를 발생시키기 때문에, 헤더에 구현되어야 한다.
굳이 분할 구현을 한다면 #include를 활용하여 템플릿을 사용하는 cpp에,
템플릿클래스가 구현된 cpp파일을 #include하여 컴파일타임에 인식시키면 될 것이다.
template<typename T>
CArr<T>::CArr()
:iarr(nullptr), iCount(0), maxCount(2)
{
//iarr = (int*)malloc(sizeof(int) * 2);
iarr = new int[2];
}
생성자도 위처럼 template를 붙여줘야한다.
그리고, CArr<T>::CArr()처럼
CArr의 T 타입의 CArr을 따로 선언하라는 것이다.
CArr은 템플릿의 이름인 것이고, CArr<T>까지가 클래스 이름이 된다.
#pragma once
#include <assert.h>
template <typename T>
class CArr
{
private:
T* iarr;
int iCount;
int maxCount;
public:
CArr();
~CArr();
public:
void addArr(const T& _iData);
void resizeArr(int size);
//void resizeArr(CArr* arr); << 객체를 가져오지 않아도 된다.
//위에 addArr도, addArr(CArr* arr, int _iData);인 것이다.
public:
T& operator[] (int index);
};
//클래스 선언부 밖에서 멤버함수를 구현하면, 명시해야한다.
template<typename T>
CArr<T>::CArr()
:iarr(nullptr), iCount(0), maxCount(2)
{
//iarr = (int*)malloc(sizeof(int) * 2);
iarr = new T[2];
}
template<typename T>
CArr<T>::~CArr()
{
//free(iarr);
delete[] iarr; //단 하나의 값이라면, delete arr
}
template<typename T>
void CArr<T>::addArr(const T& _iData)
{
if (iCount >= maxCount) {
//재할당
resizeArr(maxCount * 2);
}
iarr[iCount] = _iData;
iCount++;
}
//리사이즈하고, 이사시킨다.
template<typename T>
void CArr<T>::resizeArr(int size)
{
if (size <= maxCount) {
//오류, 더 큰 수로 할당해야함
assert(nullptr);
}
T* tmp = new T[size];
for (int i = 0; i < iCount; i++) {
tmp[i] = iarr[i];
}
delete[] iarr;
iarr = tmp;
maxCount = size;
}
template<typename T>
T& CArr<T>::operator[](int index) {
return iarr[index];
}
최종 코드
하지만 template을 굳이 모든 함수에 다 붙여야 하는지는 모르겠다.
클래스도 배웠고,
탬플릿도 배웠고,
리스트도 배웠기에, 이를 융합하여 만들어본다.
추가로 단방향이 아닌, 양방향으로.
struct sNode {
};
class CList
{
};
노드는 구조체, 클래스는 리스트로 생성을 해본다.
C에서는 struct만 존재했고, typedef를 해야지만 사용할 수 있었다.
C++에는 class가 생겼고, struct도 typedef를 하지 않아도 사용이 가능하다.
C++의 구조체는 생성자,소멸자,상속도 가능해서 class와 거의 같다.
하나 다른 점은, struct는 필드를 설정하지 않으면 public으로,
class는 private로 설정이 되는 것이다.
굳이 노드를 구조체로 두는 이유는, 개인적인 사용방식인데,
struct는 간단한 기능이나 데이터를 묶어놓을 경우 사용하고,
class는 여러 기능이 많이 존재할 때 사용한다고 한다.
template<typename T>
struct sNode {
sNode<T>* nextNode; //본인 내부라서 <T>는 생략해도 됨
sNode<T>* prevNode;
T sData;
};
template<typename T>
class CList{
private:
sNode<T>* headNode;
int nodeCount;
sNode<T>* tailNode;
};
//리스트의 T에 자료형을 넣어 생성한다면,
//자동으로 sNode의 T가 붙어서 구조체객체가 생성이 되고,
//노드는 해당 템플릿에 맞게 코드가 생성이 된다. (연쇄적)
그래서 위처럼 생성을 해보았다.
cList부터 template의 연쇄적인 동작을 한다.
//맨뒤에 더하기
template<typename T>
void CList<T>::addBack(const T& _data)
{
//우선 더해야하기 때문에 공간부터 생성
sNode<T>* tmpNode = new SNode<T>;
tmpNode->nextNode = nullptr;
tmpNode->prevNode = nullptr;
tmpNode->sData = _data;
if (nullptr == headNode) {
}
}
우선 공간을 생성할 때, 노드 구조체만큼의 크기를 생성해야하기에
위처럼 new를 사용하여 공간의 주소를 받아왔는데,
받아와서, 구조체 멤버변수를 설정해야만 한다.
하지만 구조체도 클래스와 다를바가 없으므로,
생성자를 사용할 수 있고,
이를 통한 초기화가 가능하다.
template<typename T>
struct sNode {
sNode<T>* nextNode;
sNode<T>* prevNode;
T sData;
//생성자
sNode() : nextNode(nullptr), prevNode(nullptr), data() {
}
//생성자오버로딩
sNode(T sData) : nextNode(nullptr), prevNode(nullptr), data(sData) {
}
};
위처럼 인자를 받아서 생성자로 호출할 수 있다.
//맨뒤에 더하기
template<typename T>
void CList<T>::addBack(const T& _data)
{
//우선 더해야하기 때문에 공간부터 생성
sNode<T>* tmpNode = new SNode<T>(nullptr, nullptr, _data);
if (nullptr == headNode) {
headNode = tmpNode;
tailNode = tmpNode;
}
else {
//서로 맞물리게 연결함
tailNode->nextNode = tmpNode;
tmpNode->prevNode = tailNode;
//tail을 교체
tailNode = tmpNode;
}
nodeCount++;
}
//맨앞에 더하기
template<typename T>
void CList<T>::addFront(const T& _data)
{
//우선 더해야하기 때문에 공간부터 생성
sNode<T>* tmpNode = new SNode<T>(nullptr, nullptr, _data);
if (nullptr == headNode) {
headNode = tmpNode;
tailNode = tmpNode;
}
else {
tmpNode->nextNode = headNode;
headNode->prevNode = tmpNode;
headNode = tmpNode;
}
nodeCount++;
}
이번 리스트는 head와 tail노드 2개를 기억하고 있을 것이라고 했다.
그래서 각각의 더하는 방식을 작성하였다.
논리적으로 고려해야할 부분이 가장 많은 것 같다.
C++에서는 c-in/out이란걸 제공한다고 한다.
std::cout<<"안녕"<<10<<std::endl; //printf
int input = 0;
std::cin>>input; //scanf
보면 CList::addFront와 비슷해보인다.
CList클래스 안에 존재하는 addFront를 호출한다는 의미이고,
std::cout도 같은 맥락이다.
std는 클래스가 아니고, namespace로, 관련 기능들을 모아놓고 그냥 이름지은 것이다.
namespace YU {
void cout() {
return;
};
}
namespace std {
void cout() {
return;
};
}
위처럼 namespace를 지정해두면, 같은 이름의 함수도 선언이 가능하다.
접근은 :: 를 붙여서 하게 된다.
using namespace std;
cout();
하지만, 위처럼 using 키워드를 붙인다면,
std::cout을 하지 않고
그냥 암묵적으로 std의 cout을 사용할 수 있게 된다.
아래의 namespace내에서 자주쓰는 특정 기능만
using std::cout
cout();
위처럼 선언하는 것이 문제의 소지가 덜하다.
함수명이나 변수명의 중복을 막을 수 있는 방어적인 요소이다.