정신과 시간의 방
카테고리
작성일
2023. 1. 31. 12:32
작성자
risehyun

우선 지난 시간까지 배웠던 개념들을 되짚어 보면,

우리가 데이터를 저장할 때 프로그램 실행 도중 계속 데이터가 확장될 수 있도록 하기 위해서는 주소가 필요했다.

자료형을 처음부터 아무리 크게 선언해도 결국 한계가 있기 때문이다.

 

그래서 힙 메모리 영역에 동적 할당을 해서 들어오는 새로운 데이터들을 넣어 주기로 했었다.

int 데이터를 저장하는 가변 배열의 경우 int 포인터를 사용했다.

왜냐하면 새롭게 생성한 메모리를 할당한 곳을 int 데이터를 저장하는 목적으로 볼 것이기 때문이다.

거길 int 단위로 접근하고 수정하기 위해서는 int 포인터를 준비해서 그쪽 주소를 받아놓아야 한다.

 

지금부터는 클래스 필드를 선언해 볼 것이다.

 

<CArr.h>

class CArr
{
    private:
        int*    m_pInt;
        int     m_iCount;
        int     m_iMaxCount;
        
public:
     CArr();
     ~CArr();
};

 

이전에 배웠던 구조체는 무조건 공개 상태에 있었다.

하지만 클래스에서는 접근 제한자를 사용해서 외부에 대한 멤버 공개 여부를 직접 설정해줄 수 있다. 

여기서는 접근 제한자를 private 로 설정해 외부로부터 클래스 멤버를 보호할 것이다.

 

이때 만약 클래스에 아무것도 적지 않는다면 해당 멤버는 기본적으로 디폴트 접근 제한 지정자인 private를 따르게 된다.

하지만 위의 헤더 파일에서는 직관적으로 코드를 이해하기 위해서 명시적으로 private 표시를 해주었다.

 

이어서 아래에는 public 상태로 생성자와 소멸자를 만들어주었다.

특별히 public 상태로 선언한 이유는 private로 생성자와 소멸자를 선언하면

접근이 제한되어 main.cpp의 메인 함수에서 호출할 수 없기 때문이다.

 

생성자와 소멸자는 따로 만들지 않아도 기본적으로 컴파일러가 생성을 해주지만,

현재 코드는 추후 생성자와 소멸자에서 수행해야 할 일이 있기 때문에 명시해주었다.

 

이제 헤더에 기본적인 틀을 만들었으니 아래의 cpp 파일에서 구체적인 구현을 해보자.

클래스에서의 동적 할당은 new 키워드로, 메모리 해제는 delete 키워드로 할 수 있다.

활용 예시 코드는 다음과 같다.

 

<CArr.cpp>

#include "CArr.h"

CArr::CArr()           // 초기화시 헤더 파일에서 작성한 순서대로 초기화해야 성능상 더 빠르다.
    : m_pInt(nullptr)
    , m_iCount(0)
    , m_iMaxCount(2)
{
    m_pInt = new int[2]; // malloc 함수의 C++ 버전. int크기 2개만큼을 할당하므로 8바이트 할당과 같다
}

CArr::~CArr()
{
    delete[] m_pInt;      // free 함수의 C++ 버전. 
                          // m_pInt를 지울 건데 그곳에 가면 인트가 연속적으로 있다는 의미

//    int* p = new int[10]; // 배열로 변수 여러 개를 한번에 동적할당했을 때 이걸 한번에 지우려면
//    delete[] p;           // [] 을 사용해야 한다.

}

구현 파트에서는 생성자와 소멸자에 접근하기 위해, 클래스 내부에 선언한 기능에 접근하기 위한 범위 지정 연산자(::)를 사용했다.

 

<main.cpp>

// C++ 동적할당 new, delete
CTest* pTest = new CTest;
delete pTest;

 

이어서 우리가 가변 배열을 구현할 때 지원 되는 서포팅 함수들이 몇가지 있었다.

현재 클래스에서는 생성자와 소멸자에서 할당과 해제 처리를 해주기 때문에 우리가 구현 해야 할 것은 PushBack 함수와 저장 데이터 공간이 부족할 때 이를 확장시켜줄 resize 함수다.

이때 멤버 함수라는 것은 객체를 통해 호출 되기 때문에 아래와 같이 들어올 데이터만 적어주면 된다.

 

<CArr.h>

#pragma once
class CArr
{
private:
	int* m_pInt;
	int  m_iCount;
	int  m_iMaxCount;


public:
	void push_back(int _Data);
	void resize(int _iResizeCount);


public:
	CArr();
	~CArr();

};

 

<CArr.cpp>

#include "CArr.h"
#include <cassert>

CArr::CArr()
    : m_pInt(nullptr)
    , m_iCount(0)
    , m_iMaxCount(2)
{
    m_pInt = new int[2];
}

CArr::~CArr()
{
    delete[] m_pInt;

    int* p = new int[10];
    delete[] p;

}


void CArr::push_back(int _Data)
{
	if (this->m_iMaxCount <= this->m_iCount) // 힙 영역에 할당한 공간이 다 찬 경우
	{
		// 재할당
        resize(m_iMaxCount * 2);
	}

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

}

void CArr::resize(int _iResizeCount)
{
    // 현재 최대 수용량 보다 더 적은 수치로 확장하려는 경우 예외처리함
    if (m_iMaxCount >= _iResizeCount)
    {
        assert(nullptr);
    }

    // 1. 리사이즈 시킬 개수만큼 동적할당 한다
    int* pNew = new int[_iResizeCount];

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

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

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

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


}

이제 구현한 클래스를 활용해보자.

 

<main.cpp>

#include <iostream>

#include "Arr.h"
#include "CArr.h"

class CTest
{
private:
	int a;

public:
	CTest()
		: a(10)
	{
	
	}
};

int main()
{
	// 기존의 가변 배열 사용시 방법

	tArr arr = {};
	InitArr(&arr);

	PushBack(&arr, 10);
	PushBack(&arr, 20);
	PushBack(&arr, 30);

	ReleaseArr(&arr);


	// 클래스를 이용한 방법
	// 초기화와 메모리 해제를 알아서 해주기 때문에 코드가 훨씬 간결해졌다.

	CArr carr;
	carr.push_back(10);
	carr.push_back(20);
	carr.push_back(30);

	return 0;

}

 

이번에는 클래스에서도 배열처럼 인덱스를 사용해 특정 데이터에 도달할 수 있도록 하는 기능을 직접 만들어보자.

구현하고자 하는 연산은 다음과 같다.

carr[1];

 

이것을 직접 구현해보자.

 

<CArr.h>

#pragma once
class CArr
{
private:
	int* m_pInt;
	int  m_iCount;
	int  m_iMaxCount;


public:
	void push_back(int _Data);
	void resize(int _iResizeCount);
    
    int operator[] (int idx);


public:
	CArr();
	~CArr();

};

 

<CArr.cpp>

int CArr::operator[](int idx)
{
    return m_pInt[idx];
}

 

이제 구현한 내용을 적용해보자.

주의할 점은 인덱스 [] 연산은 가능해졌지만 이를 이용해서 특정 인덱스 주소에 값을 집어넣는 것은 불가능하다.

 

<main.cpp>

int iData = carr[1];

// carr[1] = 200;  // 함수를 반환할 때는 함수가 종료될 때 
                   // 반환값은 임시적으로 저장하고 있던 곳에서 꺼내 온다.
                   // 따라서 이 임시 공간에 넣어 두고 복사본을 받아온 상태에서는
                   // 임시 공간이므로 이를 수정할 수도 없을 뿐더러
                   // 현재 반환 타입이 그냥 int이기 때문에 진짜 내가 원하는 곳에 수정할 수 없다.

 

이를 가능하게 하기 위해서는 다음과 같이 코드를 수정해야 한다.

레퍼런스를 활용해 반환 시키고자 하는 변수의 참조를 전달시킴으로서 바닥 안쪽에서는 반환 타입이지만, 그것이 똑같이 반환 할때 값과 동일시 되기 때문에 바로 직접적인 수정이 가능해지는 것이다.

 

<CArr.cpp>

int& CArr::operator[](int idx)
{
    return m_pInt[idx];
}

 

<main.cpp>

carr[1] = 100;

 

이제 클래스가 좀더 배열과 비슷한 형태가 되었다.

데이터를 마음대로 집어넣을 수 있고, 기본 문법에서 제공하는 배열처럼 실제 주소 연산은 아니지만 오퍼레이터 함수 클래스에서 구현해 놓은 것을 마치 기본 연산자처럼 주소 연산을 통해 넣을 수 있으며, 인덱스를 이용해 데이터에 접근 하는 모양과 유사하게 작동하도록 구현하였다.

'C,C++' 카테고리의 다른 글

namespace, 입출력 구현 (cin, cout)  (0) 2023.02.01
함수 템플릿, 클래스 템플릿, 클래스 템플릿 리스트  (0) 2023.01.31
클래스  (0) 2023.01.28
함수 포인터  (0) 2023.01.27
리스트  (0) 2023.01.26