본문 바로가기
C++/기초

C++ 기초 : 클래스를 이용한 배열

글: 시플마 2024. 4. 14.

구조체로 만들었던 가변 배열을

이번에는 클래스로 만들어 보겠습니다.

 

 

우선 단축키 Ctrl + Shift + X 를 입력하여 

클래스 마법사를 열어 주겠습니다.

 

이후 '클래스 추가' 버튼을 눌러 클래스 이름을 입력해 주면

해당 이름을 가진 헤더 파일과 소스 파일이 만들어집니다.

 

 

 

 


 

 

 

 

그리고 cArr.h 파일에 

 

cArr의 멤버 변수와 생성자와 소멸자를 선언하겠습니다.

 

 

전역 함수를 구현할 때, 헤더 파일에 정의까지 해 놓으면

문제가 발생했었죠? 다른 소스 파일에서 해당 헤더 파일을

참조하게 되면, 참조한 모든 소스 파일에 같은 함수가 생겨

함수가 여러 개로 인식되는 문제가 발생했었습니다.

 

그래서 선언과 정의는 따로 분리하는 게 좋습니다.

 

물론 클래스에서 public으로 선언된 함수는 

인라인이라는 개념으로 인해

위와 같은 문제가 발생하지는 않습니다. 

 

 

생성자와 소멸자를 public으로 선언하여 

공개하는 이유는 객체를 만들면 컴파일러가 알아서 생성자를 호출,

main 함수가 끝나면서 자동으로 소멸자를 호출하게 됩니다.

 

근데 생성자와 소멸자를 private로 선언하면

 

객체를 만드는 순간 생성자를 호출해야 하는데

컴파일러가 접근할 수 없게 되어 오류가 발생하죠.

 

이제 cArr.cpp 파일에 

 

생성자와 소멸자를 정의하도록 하겠습니다.

 

함수명 앞에 cArr::를 붙여서 해당 함수가

cArr이라는 클래스의 범위에 속한다는 것을

명시해줘야 합니다.

 

 

클래스에서는 이니셜라이저를 이용해 

멤버 변수를 초기화할 수 있습니다.

(멤버가 선언된 순서대로

초기화해 주는 것이 좋습니다.)

 

Heap 영역에 동적 할당된 공간을 가르키는

포인터 변수 m_pInt는 nullptr로

해당 공간에 존재하는 데이터의 수를 나타내는

m_iCount는 0으로 초기화했습니다.

 

그리고 현재 공간의 최대 개수를 나타내는

m_iMaxCount는 2로 초기화하겠습니다.

해당 클래스를 통해 객체를 만들면 일단

2개의 공간을 만들어 둘 것이기 때문이죠.

 

생성자가 호출되는 순간 

8번째 줄에 있는 코드로 인해 int형으로 사용할 공간

2개를 동적 할당하게 됩니다.

 

기존 C에서 사용하던 malloc 함수 대신

C++에서는 new라는 키워드를 제공하므로

이를 사용하여 동적 할당하였습니다.

 

malloc 함수로 동적 할당하면, 동적 할당한 공간을

어떻게 사용할지는 어떤 자료형으로 해당 공간을

읽을 것느냐에 따라 달랐습니다.

 

그러나 new로 동적 할당하면 키워드 옆에

해당 공간을 어떤 자료형으로 사용할지 명시해 줍니다.

(대괄호 안에는 몇 칸 할당할지 적습니다.)

 

이러한 점에서 오는 차이점은 

 

위와 같은 코드를 통해 알 수 있습니다.

 

 

아래 코드를 보시면

 

malloc을 통해 동적 할당한 cArr형 포인터 객체 carr은

초기화가 진행되지 않았고

 

 

new를 통해 동적 할당한  cArr형 포인터 객체 carr1은

 

초기화가 진행되었습니다.

 

둘 다 동적 할당을 하는 키워드인데

왜 이런 차이가 발생하는 것일까요?

 

6번째 줄에서 carr은 100Byte의 공간을 가리키고 있습니다.

단순히 carr에는 100Byte 공간의 시작 주솟값만 저장되었겠죠.

근데 carr은 클래스로 만든 자료형을 통해 생성된 객체이죠.

즉 생성자가 호출됩니다. 생성자가 호출되면서 초기화가 진행되죠.

m_iCount를 0으로 초기화해 줘야 하는데 저 100Byte 공간 중

어디를 m_iCount로 보고 초기화를 해줘야 할까요?

또 어디서부터 어디까지를 m_iMaxCount로 봐야 할까요?

알 수 없기 때문에 값을 넣어주지 못합니다.

 

반면 8번째 줄에서 carr1은 동적 할당된 공간을

cArr 형태로 인식하고 있습니다. 마찬가지로 carr1도

생성자가 호출되면서 초기화가 진행되겠죠.

동적 할당된 공간을 cArr형으로 인식하기 때문에

m_pInt와 m_iCount, m_iMaxCount의 자리를

찾아가서 값을 넣어 줄 수 있죠.

 

 

main 함수가 끝나면서 cArr로 생성된

객체가 사라질 겁니다. 그 순간 소멸자가 호출되죠.

이때 동적 할당된 메모리를 해제해 주려면

delete 키워드를 사용합니다. 해당 클래스의 멤버

m_pInt는 배열 형태로 사용할 공간을 가리키는 

포인터 변수이므로 이를 해제할 때는

delete 옆에 [ ]를 붙입니다.

 

cArr형 포인터 carr이 있다고 가정해 봅니다.

 

해당 포인터가 여러 개의 cArr형 객체가 있는

동적 할당된 공간을 가리키고 있는 상황에서

main 함수가 종료되어 포인터가 사라지면서 소멸자가

호출되면 delete[ ]가 실행될 것이고, 객체 하나씩 

소멸자가 호출되면서 delete될 것입니다.

 

delete[ ]를 통해 메모리 해제를 여러 개 진행하는데 

어떻게 끝을 인식하고 메모리 해제를 멈출까요?

 

동적 할당한 끝부분에 끝을 알리는 비트값이 있기 

때문에 멈출 수 있는 것이죠. 문자열의 마지막에는 

널문자(\0)를 통해 끝부분임을 표시하는 것처럼요.

 

 

만약 포인터 변수 m_pInt가 배열이 아닌

 

m_pInt = new int;

 

위와 같이 단일 공간을 가리킨다면

delete 키워드만 사용하면 됩니다.

 

 

 


 

 

 

 

이제 가변 배열을 위한 함수를 작성해 보죠.

 

cArr.h에 선언만 해 주고

cArr.cpp에는 

 

동적 할당된 공간에 데이터를 넣기 위한 함수, 

push_back을 정의합니다.

 

구조체로 구현했을 때와 다르게 

객체의 주솟값을 인자로 받지 않습니다.

 

클래스로 구현할 때는 어차피 해당 함수가

객체를 통해 호출될 것이기 때문에 

해당 객체의 주솟값이 this에 들어가 있습니다.

 

그래서 this를 통해 멤버에 접근하면

해당 객체의 멤버라는 표현이므로 

객체의 주솟값을 받을 필요가 없죠.

 

심지어 아래 코드처럼

 

push_back 함수에 적혀 있는 

변수는 해당 객체의 멤버라는 것을 

컴파일러는 알고 있기에 this를 지워도

문제가 없습니다.

 

 


 

 

 

데이터를 넣으려고 하는데 공간이 부족하면

공간을 늘려줘야 할 겁니다. 그런 기능을 하는

함수도 구현해 봅시다.

 

 

cArr.h에 

공간 개수를 재조정하는 함수,

resize를 public으로 선언하였습니다.

 

public으로 선언함으로써

'데이터를 넣어야 하는데 공간이 부족할 때' 뿐만 아니라,

공간이 더 필요하다고 판단되면 resize 함수를 통해

미리 공간을 확보할 수 있게 하였죠.

 

데이터를 넣어야 하는데 공간이 부족할 때에만

작동하게 되면 데이터의 개수만큼 성능이 떨어집니다.

 

예를 들어 공간이 두 개인데 100개의 데이터를 넣으면

값을 2개 넣은 후 3번째 값을 넣으려고 하면 공간이 부족하기

때문에 resize의 함수가 호출될 겁니다.

그리고 10개의 새로운 공간을 할당하여 그곳에 값을 복사하고 

그곳을 포인터로 가리킨 후 기존 공간을 해제합니다.

 

이후 11번째 값을 넣을 때 또 위의 과정을 반복하게 될 겁니다. 

 

그것을 방지하기 위해 애초에 데이터가 많이 들어갈 것으로

예상이 되면 resize 함수를 호출하여 공간을 넉넉하게 

잡을 수 있게 한 것이죠.

 

 

cArr.cpp에 

 

resize 함수를 정의하였습니다.

 

해당 함수는 직접 개수를 정하여 공간의 개수를 조정할 수 있기

때문에 기존 공간의 개수보다 더 작거나 같은 값이 들어올 수 있습니다.

그래서 예외 처리를 먼저 해 주겠습니다.

 

만약 들어온 값이 기존 공간의

최대 개수보다 작거나 같다면

assert 함수를 통해 오류를 발생시킵니다.

(해당 함수를 사용하려면 assert.h 파일을 포함시켜야 합니다.)

 

그리고 new를 통해 인자로 받은 값만큼 

동적 할당을 진행합니다.

 

또한 메모리를 해제할 때,

delete를 통해 해제합니다. 

m_pInt가 가리키는 공간은 배열의 

형태이기 때문에 [ ]를 붙여 줍니다.

 

공간의 최대 개수를 나타내는

변수 m_iMaxCount에 인자로 받은 값을 

대입합니다.

 

 

resize 함수를 만들었으니

 

데이터 추가 과정에서 공간이 부족하면

자동으로 resize 함수가 호출되도록 하죠.

 

기존 공간의 최대 개수보다 2배만큼

공간을 확보할 수 있도록 하였습니다.

 

 


 

 

 

 

클래스로 구성하면 

객체가 만들어질 때 알아서 생성자를 호출하여

초기화가 진행되고 객체가 사라질 때 알아서

소멸자를 호출하기 때문에 메모리 해제가 진행됩니다.

 

그 덕분에 초기화와 메모리 해제 함수를 따로 구현하지 않아도 되죠.

 

 

 


 

 

 

근데 객체 carr을 통해서 동적 할당을 하였고

그 공간을 배열처럼 사용하였습니다.

 

그래서 아래 코드처럼

 

마치 일반 자료형으로 만든 배열처럼

인덱스에 접근하고 값을 넣으려고 하면 오류가 발생합니다.

 

객체 carr은 작성자가 직접 정의한 자료형으로 

만들어졌기 때문에 기본적으로 제공되는 자료형처럼

사용할 수는 없습니다. 만약 위와 같은 동작을 원한다면

해당 클래스에 연산자 오버로딩을 진행해야 하죠.

 

이를 위해 cArr.h에 

 

연산자 오버로딩을 하였습니다. 

 

객체 carr에 [ ] 연산을 하기 위해

operator[ ]를 선언하였습니다. 

 

 

cArr.cpp에는

 

operator[ ]를 정의하였습니다.

 

m_pInt가 가리키는 동적 할당 공간에

인자로 받은 값을 인덱스로 하여 접근하면 됩니다.

 

실행 결과를 보니

 

객체 carr에 [ ] 연산을 하였더니 대괄호 안에 있는

1을 인덱스로 인식하여 1번 인덱스의 값인 200을 

iData에 대입하였습니다.

 

 

하지만 여전히

 

1번 인덱스에 있는 값인 200을

1로 바꾸는 것은 불가능합니다. 

 

 

이걸 가능하게 하려면

 

m_pInt에다가 인자로 전달받은 값을 더하여

주소 연산을 하고 반환해야 합니다.

 

'm_pInt'는 배열의 시작 주솟값을 저장하고 있습니다.

여기에 idx의 값을 더하면 시작 주솟값에서

idx의 값만큼 건너 그곳의 주솟값을 얻을 수 있죠.

 

 

대신 아래 코드처럼

 

주솟값을 반환받은 것이기 때문에 앞에 *을 붙여

해당 주솟값으로 이동한 뒤 값을 대입시켜야 합니다.

 

아래 그림처럼

 

operator [ ]을 통해 배열의 시작 주솟값에서

1(int형 배열이므로 주솟값은 4(Byte)를 더합니다.)을

더한 주솟값을 반환하면 반환한 곳으로 

가서 주솟값을 얻은 후 *을 통해 접근하여 값을 

수정해야 하는 것이죠.

 

 

하지만 이렇게 사용하는 것은 일반 자료형으로

배열을 구성하여 값을 대입할 때와는 다른 모습입니다. 

 

그리고 이제는

 

해당 공간에 있는 값이 아닌

해당 공간의 주솟값을 반환하기 때문에

인덱스로 접근하여 값을 얻는 것도 안되죠. 

 

 

결국 일반 자료형으로 선언된 배열과 같은 형태로

 

[ ] 연산을 하고 싶다면 위 코드처럼

레퍼런스를 사용해야 합니다.

 

 

레퍼런스를 반환 타입으로 하니

 

carr을 배열처럼 사용할 수 있게 됩니다.

 

 

아래 그림을 보시면

 

operator[ ]가 200이 들어있는 공간을 

레퍼런스로 반환하고 난 후, 여기에 1을 대입하면

200이 있는 공간에 실제로 1이 대입됩니다.

 

결국 레퍼런스도 포인터와 마찬가지로 주솟값을 통해

접근하여 값을 수정하겠지만 그건 컴파일러가 할 일이죠.

 

레퍼런스를 사용하는 사람 입장에서는

operator[ ]가 반환한 것을 수정하면 마치 1004번지를

직접 수정하는 것과 같은 효과를  볼 수 있는 것입니다.

 

 

 

 

강의 출처 : https://www.youtube.com/watch?v=PFc4g8mxOiI&list=PL4SIC1d_ab-aOxWPucn31NHkQvNPHK1D1&pp=iAQB

 


 

'C++ > 기초' 카테고리의 다른 글

C++ 기초 : 클래스 템플릿  (0) 2024.04.17
C++ 기초 : 함수 템플릿  (0) 2024.04.17
C++ 기초 : 클래스 (3)  (0) 2024.04.13
C++ 기초 : 클래스 (2)  (0) 2024.04.13
C++ 기초 : 클래스 (1)  (0) 2024.04.13