정보공간_1

[2기 광주 조영진] C++ 배열의 크기 알아내기 본문

IT 놀이터/Elite Member Tech & Talk

[2기 광주 조영진] C++ 배열의 크기 알아내기

알 수 없는 사용자 2012. 9. 7. 06:01

안녕하세요.

광주멤버십 21-1기 조영진입니다.

저번에는 template에 관한 내용을 살펴보았는데요~

이번에는 template을 활용할 수 있는 하나의 경우에 대해서 살펴보려고 합니다!


1. 배열이란?

배열은 동일한 데이터 타입들을 저장하는 메모리상의  공간입니다.

일반적으로 선형적으로 연속된 메모리 공간에 저장되지만

특수한 경우에는 연속되지 않은 공간에 저장되는 경우도 있다고 하네요!

배열을 사용하기 위해 초기화하는 다양한 방법들을 알아봅시다!

위 코드의 실행 결과는 4가지 출력이 모두 동일합니다.

그리고 C / C++에서는 배열의 인덱스는 0번부터 시작하고 마지막 인덱스는

배열의 크기 - 1 까지 입니다.

따라서 위에서 선언한 배열들은 0 부터 4까지의 인덱스를 가지게 되고

5 이상의 인덱스에 대한 접근에 대해서는 C++표준에서 잘못된 접근이라고 정의하고 있습니다.

하지만 배열에 접근할 때 마다 인덱스의 유효성을 검사하게 되면 그에 따른 오버헤드가 심하기 때문에

프로그래머가 정상적으로 사용하리라고 믿고 언어 차원에서 자동적으로 검사를 해 주지는 않습니다.

따라서 배열을 사용할 때는 항상 허용된 인덱스 범위 내에서만 배열에 접근하도록 코딩하여야 합니다.

C / C++ 언어에서 배열에 관한 내용이 나올때 항상 언급되는 내용이 있습니다.

"배열의 이름은 배열의 시작 주소이다." 사실 이 말은 다음과 같습니다.

"배열의 이름은 두가지 경우를 제외하고 배열의 시작 주소처럼 사용된다."

여기서 언급한 두가지 경우는 sizeof 연산자와 &연산자가 적용되는 경우를 의미합니다.

이 내용은 다음에 설명할 내용들에 적용될 중요한 개념이니 꼭 기억해 두세요!!!!

배열에 대한 기본적인 지식은 이쯤에서 마무리 짓고 배열을 활용할 경우에 대해서 알아보도록 하겠습니다.


2. 배열의 크기

많은분들이 알고 계실 sizeof 연산자는 해당하는 변수 또는 타입에 대하여 차지하고있는 메모리 공간의

크기를 Byte 단위로 알려주는 역할을 하는 연산자입니다.

위의 코드를 잘 살펴보시면 sizeof 연산자를 ()와 함께 사용하는 경우와 ()가 없이 사용하는 경우

2가지 방법으로 사용하는 것을 보실 수 있죠?

하지만 저 코드에서 틀린 부분이 있습니다.

변수에 대하여 sizeof 연산자를 사용할 경우에는 ()의 유무가 상관이 없으나

바로 sizeof int와 sizeof double처럼 타입에 대하여 sizeof 연산자를 사용할 때는

반드시 ()와 함께 사용하여야 합니다.

물론 잘못된 부분을 제외하고 실행하면 일반적으로 

int와 x에 대해서는 4라는 결과가 출력될 것이고

double과 d에 대해서는 8이라는 결과가 출력될 것입니다.

간혹 시스템에 따라 int형이 8Byte인 경우가 있습니다.

이는 해당 시스템이 int를 8Byte로 설정한 경우입니다.

이제 32Bit 시스템에서 int와 주소가 4Byte인 환경을 기준으로 설명하도록 하겠습니다.

자, 이제 이러한 sizeof 연산자를 배열에 적용해 보도록 하겠습니다.

위 코드를 컴파일하고 실행하게 되면 다들 예상하신 대로 40이라는 결과가 출력됩니다.

그럼 다음 코드를 볼까요?

위와 같은 코드에서는 40이 아니라 4라는 값이 출력되게 됩니다.

한가지 경우를 더 볼까요?

바로 배열을 동적으로 할당한 경우입니다.

배열을 동적으로 할당하는 경우는 대구멤버십 이현복 회원이 작성해준 다음 포스팅을 참조하세요^^

http://blog.secmem.org/117

자 위 코드의 실행 결과는 어떻게 될까요?

p는 int * 형식의 변수입니다.

int *는 결국 32Bit 시스템에서는 32Bit만큼이 주소공간으로 사용되기 때문에

32Bit만큼을 저장할 수 있도록 정의되어 있습니다.

물론 64Bit 시스템에서는 64Bit만큼을 저장할 수 있도록 정의되어 있구요~!

따라서 현재 32Bit 시스템에서는 4라는 결과가 출력되게 됩니다.

두 번째 경우는 p의 타입이 int *이기 때문에 4라는 것을 쉽게 알 수 있지만

첫 번째 경우에는 코드만 보고는 이해가 되지 않는 부분이 있습니다.

이에 대하여 자세 알아보도록 하겠습니다!


3. 함수 인자로서의 배열

C++ 표준에서는 함수를 리턴하는 함수, 함수를 인자로 받는 함수, 

배열을 리턴하는 함수, 배열을 인자로 받는 함수는 정의할 수 없도록 되어 있습니다.

이번에는 배열에 대한 내용을 살펴보기로 했으니 함수는 잠시 접어두고

배열에 대한 내용만 집중적으로 파헤쳐 보겠습니다.

분명히 좀 전에 배열을 인자로 받는 함수는 정의할 수 없다고 했는데

위의 2가지 경우 모두 정상적으로 컴파일 되고 실행 결과 또한 오류가 없는 것을 확인할 수 있습니다.

그 이유는 다음과 같습니다.

함수의 진입 부분에 브레이크 포인트를 걸고 디버그 모드로 들어가서

함수의 이름에 마우스를 올려놓거나 함수의 이름에서 오른쪽 버튼을 누른 뒤

"조사식 추가" 라는 메뉴를 선택하시면 위 화면과 비슷한 화면을 보실 수 있습니다.

여기서 중요한 것은 우리가 코드상에 int arr[10], int arr[5]라고 적었지만 

실제 컴파일된 값과 형식에서는 int *로 변환이 되어 있다는 것입니다.

이것은 C언어를 만들 때 함수 인자에 배열을 사용할 수 없게 해 놓았으나 많은 프로그래머가

함수의 인자로 배열을 넘기기를 원하였기 때문에 이러한 암시적인 형변환을 통해서

위와 같은 코드가 지원되도록 컴파일러를 제작하였기 때문입니다.

C++에서는 C의 이러한 점을 그대로 이어받았기 때문에 동일한 현상이 나타나게 되는 것입니다.

따라서 우리가 배열을 인자로 넘기더라도 함수에서는 포인터형식으로 받기 때문에

인자로 넘어온 배열의 실제 크기가 얼마인지에 관계 없이 모두 주소공간의 크기로 인식하게 되는 것입니다.

따라서 함수 매개변수에 적힌 배열의 크기는 아무런 의미가 없으며

함수 내부에서는 우리가 넘겨준 배열의 크기를 알아낼 수 없습니다.

그래서 일반적으로 함수 호출에 배열을 넘기는 경우에는 배열의 크기를 별도의 int형 변수를 사용하여 

같이 넘겨주도록 함수를 정의하게 되는 것이구요.

하지만 이런식으로 라이브러리와 같은 코드를 작성하게 되면 사용하는 입장에서는 좀 귀찮을 수 있기 때문에

이러한 점을 개선해 보도록 하겠습니다^^


4. 동적 할당한 배열의 실제 크기 알아내기

먼저 동적할당을 통해 생성된 배열의 크기를 알아보도록 하겠습니다.

일반적으로 지역변수의 메모리는 Stack이라는 메모리 공간에 할당되고

동적할당을 통해 생성된 메모리는 Heap이라는 메모리 공간에 할당됩니다.

여담으로 정상적으로 초기화 된 전역변수는 Data 메모리 공간에 할당되고

초기화되지 않은 전역변수는 BSS 메모리 공간에 할당됩니다.

이러한 메모리 공간의 차이에 대해서는 차후에 자세히 알아보도록 하겠습니다^^

일단 Heap 영역에 생성되는 메모리는 사용하고 있는 운영체제가 관리해주고 있습니다.

따라서 동적 할당된 메모리 공간의 크기를 알아내기 위해서는 운영체제가 제공해주는 API를 사용해야 합니다.

저는 Windows 를 사용하고 있으므로 Windows를 기준으로 설명드리겠습니다.

malloc.h 헤더를 include 하게 되면 사용할 수 있는 함수인

_msize라는 함수를 통해서 동적할당된 메모리 공간의 크기를 알아낼 수 있는데요

사용법은 sizeof 연산자와 같지만 _msize는 연산자가 아니라 함수이므로 반드시 호출을 통해 사용해야 합니다.

위 코드와 같은 형식으로 사용하시면 되고 결과는 당연히 40이라는 결과가 나오게 됩니다.

이 _msize라는 함수는 내부적으로 HeapSize라는 Windows API를 호출하는데요

_msize 함수의 구현부를 보면 다음과 같이 되어 있습니다.

인자로 넘어온 메모리 공간이 유효한지 확인하고 기본 힙에서 메모리 공간이 차지하는 크기를

HeapSize API를 통해 계산한 뒤 반환하는 형식으로 구현되어 있습니다.

하지만 Windows API를 사용하면 기본 힙 뿐만 아니라 새로운 힙을 생성할 수 있는데

이러한 경우에는 _msize함수는 사용할 수 없습니다.

따라서 HeapSize 함수를 직접 사용해야 합니다.

위 코드와 같이 Windows API를 사용하기 위해 Windows.h 헤더를 추가해 주시고

_msize와 HeapSize를 사용해보시면 차이점에 대해서 확인하실 수 있습니다^^

3줄요약!

동적할당된 메모리 공간의 크기를 확인하기 위해서는 운영체제의 도움을 받아야 한다.

Windows에서 힙을 새로 생성하는 특수한 경우를 제외하고는 _msize 함수를 사용하면 된다.

힙을 새로 생성한 경우에는 HeapSize 함수를 사용하면 된다.

HeapSize 함수만으로 두가지 경우를 다 처리하고 싶다면 다음과 같이 하면 됩니다.

GetProcessHeap API를 사용해서 현재 프로세스의 기본 Heap을 가져온 다음

해당하는 메모리 공간의 크기를 조사하면 _msize를 사용한것과 동일한 결과를 얻을 수 있습니다.


5. 함수 인자로 사용된 배열의 실제 크기 알아내기

다음으로 함수의 인자로 사용된 배열의 실제 크기를 알아보겠습니다.

제가 모르는 것일 수도 있으나 현재 C언어에서는 이 상태의 배열의 크기를 알아낼 수 있는 방법은 없습니다.

동적할당한 배열의 경우에는 운영체제의 API를 사용하는 것이므로 C언어에서도 크기를 알아낼 수 있으나

함수의 인자로 넘어간 경우에는 C언어에서는 모두 포인터로 인식하기는 하지만 그 실제 주소가

Heap공간의 주소가 아닌 Stack공간의 주소이므로 HeapSize와 같은 

운영체제의 API를 통해서 접근하게 되면 RunTime Error가 발생하게 됩니다.

하지만 C++에서는 새롭게 도입된 참조형식을 사용하면

함수의 인자로 사용된 배열의 크기도 알아낼 수 있습니다^^

함수의 매개변수를 위 코드와 같은 형식으로 선언하게 되면

컴파일러는 해당 인자를 int 10개짜리 배열의 참조로 인식하게 되어 실행시키게 되면

40이라는 결과가 나오는 것을 확인할 수 있습니다.

C언어에서 2차원 배열에 대한 포인터를 자주 선언해 보신 분들은 익숙하시겠지만

2차원 배열에 대한 포인터를 자주 선언해보지 않으신 분들은 &arr에 ()를 씌워놓은 모습이

어색해 보일수도 있습니다.

그 이유는 C / C++의 연산자 우선순위 때문입니다.

연산자 우선순위는 일단 ()가 가장 높고 그 다음이 단항 연산자인데 단항 연산자 중에서도

변수명의 뒤에 오면서 가까운 것들이 우선순위가 높습니다.

따라서 &도 단항 연산자이고 [] 도 단항 연산자인데 [] 연산자가 arr의 뒤쪽에 위치하므로

컴파일러는 먼저 [] 연산자를 해석하게 되어서 ()가 빠지게 된다면

위 그림과 같이 int &arr[10] 은 arr은 10개짜리 배열인데 배열에는 참조가 들어있고 그 참조는 int를 가리킨다.

라고 해석이 되게 됩니다.

하지만 우리의 의도는 arr은 참조인데 그 참조는 10개짜리 배열을 가리키고 배열에는 int가 들어있다.

라고 해석되기를 의도했으므로 ()를 통해서 다음 그림과 같이 해석되도록 우선순위를 변경해 준 것입니다.

하지만 위와 같이 선언을 하게 되면 int 10개짜리 배열만 인자로 받을 수 있습니다.

이러한 경우를 해결하기 위한 가장 좋은 방법은 저번 포스팅에 사용한 template을 사용하는 것 입니다.

template을 사용하여 위 코드와 같이 함수를 선언하게 되면 함수의 인자로 사용되는 배열의

실제 크기를 알아낼 수 있는 함수를 작성할 수 있습니다.

여기서 저번 포스팅에서 언급한 tempalte specialization을 사용하게 되면

동일한 함수 이름으로 동적할당한 경우와 아닌 경우에 대해서 모두 실제 크기를 알아낼 수 있습니다.

위와 같이 하시면 p는 위의 ArraySize를 사용하여 크기를 구하게 되고

p2는 아래의 ArraySize를 사용하여 크기를 구하게 됩니다.

T*&라는 표현이 어색하게 느껴지시는 분들이 계실수도 있는데

써진 그대로 해당 타입의 포인터의 참조형식이라고 생각하시면 됩니다.

여기서 T*라고만 적게되면 p또한 포인터로 적절히 변환될 수 있기 때문에

아래의 ArraySize를 호출하게 되고 이는 Stack공간의 메모리를 통해서

Heap 공간의 메모리 크기를 알아내려고 하는 시도이기 때문에

RunTime Error가 발생하게 됩니다.

따라서 명확하게 포인터형인 경우에만 아래의 ArraySize를 사용하도록 명시하기 위하여

포인터의 참조형식을 사용한 것입니다^^

이번 포스팅은 여기서 마치도록 하고 다음 시간엔 더 유익한 정보를 알려드릴 수 있기를 기대하면서

이번 포스팅을 마치도록 하겠습니다.

감사합니다^^