[C++] 구조체 · 클래스 템플릿으로 리스트 구현

2024. 3. 18. 20:50Programming Language/C++

코드를 작성할 때 노드는 구조체로, 리스트는 클래스로 만들었지만, C++에서 클래스와 구조체는 같아졌기 때문에 어떤 것을 사용하든 크게 상관은 없다.

구조체도 생성자, 소멸자, 상속이 가능해졌다.

단, 클래스와 구조체의 차이점은 범위 지정자의 기본값이 다르다는 것이다.

구조체는 public이고 클래스는 private이다.

 

구조체와 클래스 중 어떤 것을 사용할지 모르겠을 때, 나름의 규칙을 만들면 좋다.

ex) 기능이 많지 않을 때(단순히 데이터를 묶어놓고 데이터를 저장하는 등)는 구조체 키워드를, 리스트처럼 컨테이너로써의 복잡한 기능이면 클래스로 선언한다.

 

※ 구조체 템플릿은 구조체가 아니다.

프로그래머는 넣은 데이터가 노드 단위인 것을 알 필요가 없어야 된다.

다시 말해서, 리스트에 데이터를 넣을 뿐, 데이터를 전달받은 대상이 노드라는 것을 만들어서 데이터를 관리하는지 사용자는 알 필요가 없어야 한다.

리스트에 int 데이터가 들어가는 버전을 요청했다면, 자연스럽게 리스트 클래스는 Tint를 받아와서 int를 저장하는 노드를 가리키는 멤버를 가진다.

따라서 리스트의 타입을 정해주면 노드의 타입도 정해진다.


생성자 오버로딩

생성자도 오버로딩이 가능하다.

// 기본 생성자
tListNode()
    : data()
    , pPrev(nullptr)
    , pNext(nullptr)
{
}
// 생성자 오버로딩
	// 인자를 받아오는 버전의 생성자
	// 받아온 인자로 초기화한다.
tListNode(const T& _data, tListNode<T>* _pPrev, tListNode<T>* _pNext)
    : data(_data)
    , pPrev(_pPrev)
    , pNext(_pNext)
{

}

CList.h

#pragma once

// 양방향으로 관리하기 위해 노드 부분에는 pPrev를, 리스트 부분에는 m_pTail을 추가했다.
template<typename T>
struct tListNode
{
	T data;
	// 선언한 구조체 템플릿의 내부에 있기 때문에 tListNode에 <T>를 적지 않아도 되지만, 명확성을 위해 적어주는 것이 좋다.
	tListNode<T>* pPrev;  // 이전 노드 주소
	tListNode<T>* pNext;  // 다음 노드 주소

	// 기본 생성자
	tListNode()
		: data()
		, pPrev(nullptr)
		, pNext(nullptr)
	{
	}
	// 생성자 오버로딩
	tListNode(const T& _data, tListNode<T>* _pPrev, tListNode<T>* _pNext)
		: data(_data)
		, pPrev(_pPrev)
		, pNext(_pNext)
	{
		
	}

	// 소멸자는 딱히 할 일이 없어서 만들지 않았다.
};

// 구조체가 아니라 구조체 템플릿이라서 아직 정해지지 않았기 때문에 리스트도 템플릿으로 만들어야 한다.
template<typename T>
class CList
{
private:
	// 리스트는 실제로 사용하기 때문에 tListNode에 <T>를 붙여줘야 한다.
	tListNode<T>* m_pHead; // 시작 노드 주소
	tListNode<T>* m_pTail;  // 마지막 노드 주소
	int m_iCount;  // 리스트 안에 있는 데이터 개수

public:
	void push_back(const T& _data);
	void push_front(const T& _data);

public:
	CList();
	~CList();
};

template<typename T>
void CList<T>::push_back(const T& _data)
{
	// 노드 생성
		// 입력된 데이터를 지정할 노드를 동적할당했다.
	//tListNode<T>* pNewNode = new tListNode<T>;  // new하고 옆에 아무것도 적지 않으면 기본 생성자를 호출한다.
	// 받아온 데이터는 tListNode의 T data; 부분에 data에 들어간다.
	// 만약 기본 생성자로 호출했다면 pNewNode를 만들고 나서 일일이 구조체로 접근해서 각각의 파트에 맞는 것을 넣어줘야 한다.
	// 때문에 초기값을 생성자로 한 번에 호출하는데, 전달해준 인자로 초기화를 하기 위해 인자를 전달받아서 초기화되는 버전의 생성자를 오버로딩했다.
	tListNode<T>* pNewNode = new tListNode<T>(_data, nullptr, nullptr);

	// 처음 입력된 데이터라면
	if (nullptr == m_pHead)
    // 헤드 포인터가 null이면 데이터가 처음 입력된 데이터이다.
	{
		m_pHead = pNewNode;
		m_pTail = pNewNode;
	}
	else
	// (강의 영상의 설명)
	// 처음 입력된 데이터가 아니라면
		// 데이터가 1개 이상 입력된 경우
		// 현재 가장 마지막 데이터(tail)를 지정하고 있는 노드와 새로 생성된 노드가 서로 가리키게 한다.
	{
	// (내가 강의 듣고 작성한 내용)
		// 현재 가장 마지막 노드를 알아야 생성시킨 노드를 연결해줄 수 있다.
			// 그 마지막 노드에서 다음 주소를 가르키는 부분이 null이면 현재 가장 마지막 노드이다.
			// 다음 주소를 가리키는 부분에 생성시킨 노드의 주소를 넣어준다.
		// 양방향이기 때문에 새로운 노드도 생성시킨 노드를 가리켜야 된다.
		m_pTail->pNext = pNewNode;
		pNewNode->pPrev = m_pTail;

		// 리스트가 마지막 노드의 주소값을 새로 입력된 노드로 갱신한다.
		m_pTail = pNewNode;  // 현재 가장 마지막 노드를 생성시킨 노드로 갱신시킨다.
	}

	// 데이터 개수 증가
	++m_iCount;
}

template<typename T>
void CList<T>::push_front(const T& _data)
// 노드를 하나 생성시켜서 리스트가 생성시킨 노드를 헤드로 가리키고, 생성시킨 노드는 원래 헤드였던 노드를 가리킨다.
{
	// 생성시킨 노드는 이전이 존재하면 안 된다. => _pPrev는 null
	// 생성시킨 노드는 원래 헤드의 주소값을 가르켜야 한다. => _pNext는 m_pHead
		// 새로 생성된 노드의 다음을 현재 헤드 노드의 주소값으로 채운다.
	tListNode<T>* pNewNode = new tListNode<T>(_data, nullptr, m_pHead);

	// 현재 헤드 노드의 이전 노드 주소값을 새로 생성된 노드의 주소로 채운다.
	m_pHead->pPrev = pNewNode;

	// 리스트가 새로 생성된 노드를 새로운 헤드 주소로 갱신한다.
	m_pHead = pNewNode;

	// 데이터 개수 증가
	++miCount;
}

// 생성자
template<typename T>
CList<T>::CList()
	: m_pHead(nullptr)
	, m_pTail(nullptr)
	, m_iCount(0)
{

}

// 소멸자
template<typename T>
CList<T>::~CList()
{
	tListNode<T>* pDeleteNode = m_pHead;  // 첫 번째 노드를 받아온다.

	while (pDeleteNode)
	{
		// 다음 노드의 다음 꺼를 기억해두고 지우는 것을 반복한다.
		tListNode<T>* pNext = pDeleteNode->pNext;  // 삭제할 노드(pDeleteNode)의 다음 노드(pNextNode)를 pNext에 넣는다.
		delete(pDeleteNode);  // 해당 메모리 해제
		pDeleteNode = pNext;  // 삭제할 노드를 다음 노드로 갱신시킨다.
	}
}

 

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;
}

 

linkedList.h

#pragma once

// 노드 => 저장하고자 하는 데이터 + 다음 노드의 주소
typedef struct _tagNode
{
	int iData;
	struct _tagNode* pNextNode;
}tNode;

typedef struct _tagList
{
	tNode* pHeadNode; // 노드 주소
	int iCount;  // 데이터 개수
}tLinkedList;

 

linkedList.cpp

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

void InitList(tLinkedList* _pList)
{
	_pList->pHeadNode = nullptr;
	_pList->iCount = 0;
}

void PushBack(tLinkedList* _pList, int _iData)
{
	tNode* pNode = (tNode*)malloc(sizeof(tNode));  // 노드 사이즈만큼 만들고 주소는 노드 단위로 데이터를 사용할 것이기 때문에 tNode*로 받아온다.
	pNode->iData = _iData;
	pNode->pNextNode = nullptr;

	// 지금 들어온 데이터(추가한 데이터)가 처음인지 아닌지를 따져봐야 한다.
	// 만약 현재 들어온 데이터의 개수가 0(또는 pHeadNode(첫 번째 노드)가 nullptr)이라면
	if (0 == _pList->iCount)
	{
		_pList->pHeadNode = pNode;  // 노드의 주소를 저장한다.
	}
	// 노드 안에 데이터가 1개 이상이면 else에 걸린다.
	else
	{
		// 현재 가장 마지막 노드 즉, 바로 앞전의 노드를 찾아서 해당 노드의 pNextNode를 생성시킨 노드의 주소로 채운다.
		// 단방향에서 양방향으로 하려면 Head뿐만 아니라 제일 마지막 노드인 Tail도 알아야 한다.
		tNode* pCurFinalNode = _pList->pHeadNode;
		// pNextNode가 nullptr이면 0이고, 이는 false를 뜻하므로 반복문이 끝나게 된다.
		// 반대로 0이 아닌 모든 수는 true이기 때문에 pNextNode가 존재한다면 반복문이 실행된다.
		while (pCurFinalNode->pNextNode)
		{
			// 헤드 노드의 다음이 있다면, 주소 변수를 본인이 가리키고 있는 pNextNode로 갱신시킨다.
			pCurFinalNode = pCurFinalNode->pNextNode;
		}

		pCurFinalNode->pNextNode = pNode;  // pNextNode를 지금 생성시킨 노드의 주소로 넣는다.
	}

	++_pList->iCount;  // 데이터 하나가 들어왔으니 카운트 하나 증가한다.
}

void PushFront(tLinkedList* _pList, int _iData)
{
	// 새로 생성시킨 노드의 다음을 기존의 헤드로 지정한다.
	tNode* pNewNode = (tNode*)malloc(sizeof(tNode));
	pNewNode->iData = _iData;
	pNewNode->pNextNode = _pList->pHeadNode; // 원래 리스트가 가리키고 있던 헤드가 다음 노드가 되어야 들어올 노드가 그 앞에 들어와서 리스트가 이를 헤드로 지정한다.

	// 리스트의 헤드 노드 포인터를 갱신한다.
	_pList->pHeadNode = pNewNode;

	// 데이터가 들어왔으니 카운트를 증가시킨다.
	++_pList->iCount;
}

// 연결형 리스트 메모리 해제 방법 2가지
// 1. 재귀함수(효율은 좋지 않다.)
// 함수가 데이터의 개수만큼 호출되기 때문에 데이터의 양이 많아지면 효율이 좋지 않기 때문에 구현 방법은 간단하지만 좋은 방법은 아니다.
void Release(tNode* _pNode)
{
	// 탈출 조건
	// 들어온 포인터가 null일 경우에 아무것도 하지 않고 리턴한다.
	if (nullptr == _pNode)
		return;

	//free(_pNode);  // 받자마자 해제하면 데이터가 다 날라가버려서 다음 노드로 갈 수가 없다.

	// Release로 먼저 호출하여 먼저 pNextNode의 주소를 받아서 그 다음 노드를 지운다.
	// 그럼 맨 마지막 노드부터 차례대로 지워진다.
	Release(_pNode->pNextNode);
	free(_pNode);  // 그렇게 메모리가 해제되다가 첫 번째 노드까지 돌아오고 나면 free() 함수로 인해 메모리가 해제된다.
}

// 2. 반복문
void ReleaseList(tLinkedList* _pList)
{
	tNode* pDeleteNode = _pList->pHeadNode;  // 첫 번째 노드를 받아온다.

	// pDeleteNode가 null이 될 때까지 반복문은 반복된다.
	while (pDeleteNode)
	{
		// 다음 노드의 다음 꺼를 기억해두고 지우는 것을 반복한다.
		tNode* pNext = pDeleteNode->pNextNode;  // 삭제할 노드(pDeleteNode)의 다음 노드(pNextNode)를 pNext에 넣는다.
		free(pDeleteNode);  // 해당 메모리 해제
		pDeleteNode = pNext;  // 삭제할 노드를 다음 노드로 갱신시킨다.
	}
}

 

main.cpp

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

int main()
{
    // int버전 리스트
    CList<int> list;

    // 0~3까지 넣기
    for (int i = 0; i < 4; ++i)
    {
        list.push_back(i);
    }

    // std:cout vs. printf => std:cout은 치환될 부분을 적지 않고 작성할 내용을 쭉 작성하면 된다.
    // << => 비트 연산자와 똑같이 생겼지만 출력에 사용하고 적어나가는 느낌이다.
    // 1. C++ printf
    std::cout << "안녕" << 10 << std::endl;

    // 2. C++ scanf
    int iInput = 0;
    std::cin >> iInput;

    return 0;
}