본문 바로가기

무언가 만들기 위한 지식/C,C++,Embedded C

[ALL] 2차원 배열의 동적할당 및 함수의 인자전달

동적할당은 영어로 Dynamic Allocation 이다.
이는 메모리영역 및 크기를 잡는데 연관이 있다.
조금 풀어서 말하자면 컴파일 시간에 메모리 영역 및 크기가 결정되는 것이 아니라, 실행시간에 크기가 결정되는 것이다.
반대의 개념은 정적할당이라고 한다.

정적할당을 하게되면 메모리 영역중 Stack 영역에 크기가 할당되고, 동적할당을 하게되면 메모리 영역중 Heap영역에 할당된다.

메모리 영역 지정자에 대한설명 : http://shinluckyarchive.tistory.com/286
(스택과 힙이 뭔지 모르거나 C언어와 매치하여 이해가 안된다면 위 링크를 타고 그 내부 링크를 타고 공부해보자.)

동적할당을 하는 이유는 메모리를 실행시간에 조금 더 효율적으로 활용하기 위해서 사용한다.
실행시간 도중에 사용할 메모리를 할당할 수 있기 때문에 메모리를 절약할 수도 있고, 필요에 따라 할당 또는 해제가 가능하다.

단 동적할당의 경우 포인터를 많이 활용하기 때문에 사전에 그에 대한 이해는 거의 필수적이라고 볼 수 있다.

1차원 배열 및 연관 포인터에 관한 포스팅 : http://shinluckyarchive.tistory.com/199
(배열과 포인터가 어떻게 연관되어 있는지 한번 생각해볼 수 있다.)

일단 기본적인 할당 코드를 보자.

[CPP의 New를 이용한 동적할당]
[작성환경
: Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

[C의 malloc를 이용한 동적할당]
[작성환경
: Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : C]

2가지의 코드 모두 동적으로 Heap영역에 사용할 메모리를 할당하는 코드이다.
단 CPP의 경우 New 명령어를 사용했고, C의 경우 malloc을 사용했다.
기억해야 할 것은 Heap영역에 메모리를 할당하게 되면 사용후 반드시 해제를 해 주어야 한다는 것이다.
Stack의 경우 해당 함수(범위)가 끝나고 리턴(Branch)이되면, 자동으로 그 Stack상에서 할당된 메모리는 해제가 되지만, Heap의 경우 완전히 다른 영역에 존재하기 때문에 수동으로 메모리를 해제하여야 한다.
그렇지 않으면 메모리 누수(Memory Leakage)가 발생하여 프로그램 종료시까지 해당 메모리가 낭비되게 된다.
그래서 항상 해당 메모리의 사용이 끝나면 반드시 할당과 마찬가지로 해제를 해주어야 한다.
해제의 경우 C에서는 free 명령어를 사용하고, CPP의 경우 delete 명령어를 사용하여 해제를 하게 된다.

위 예문에서는 2차원 배열을 동적할당한 것이다. 2차원 배열의 경우 1차원 배열보다 조금 복잡하다.
할당시에는 일단 행(row)을 할당한후에 크기에 맞는 열(column)을 할당한다.
그렇기 때문에 보통 행을 할당후 for문으로 각 행에서 열을 할당하게 된다. (한번에 동적할당은 안된다는 것이다.)
위에서는 10 x 100 행렬이 할당된 모습이다.
( char pAry[10][100] 라고 그냥 선언해버리는 것과 같지만 메모리 영역이 다르다.)

위의 두가지 코드의 결과는 아래와 똑같이 나타난다.
(코드를 요약하자면 문자열 배열을 생성한 것이다. = 2차원 배열)

※ 2차원 배열의 각 원소마다 값을 집어넣는 노가다는 귀찮아서 문자열로 두고 sprintf로 채워버렸다. 
    strcpy로 하여도 무방.



2차원 배열에 대해 알아볼때, 고려해야할 것은
첫째로는 바로 위와 같이 배열의 동적생성 (이 포스팅의 위 내용들)
두번째는 포인터와의 관계 (http://shinluckyarchive.tistory.com/199)
세번째는 함수로의 인자전달
이다.

여기서 함수로의 인자전달 부분이 생각보다 까다로운 면이 많다.
다음에서 알아보자.

인자전달을 할때 배열은 기본적으로 Call by Value 방식으로 처리되지 않는다.
배열명자체가 주소이기 때문에 인자로는 주소를 전달하는 Call by Reference 방식으로 인자가 전달되게 된다.
즉 Callee에서 값을 변경하면 Caller에서 인자로 선택되었던 값이 변하게 된다.

함수에서 Return을 할때 Return 값을 배열로 하게되면 컴파일 에러가 발생한다. 한마디로 배열은 리턴될 수가 없다.
이는 포인터를 이용하여 리턴한다면 가능해지긴 한다. 2차원 배열로 ** Type으로 Return Type을 잡아주면 컴파일에러는 발생하지 않는다.

함수에서 인자전달부분을 조심해야하는 이유는 그 형식에 있다.
일단 정답부터 말하자면,

인자가 동적으로 생성된 2차원 배열이면 이중포인터(ex: char **p)를 사용이 가능하다.
그러나 정적으로 생성된 2차원 배열의 경우 이중포인터는 불가능하고 열의 크기를 선언한 상태에서만 가능하다.
(ex: int (*p)[10])

위의 두가지 예제 내의 printAry 함수를 보자. 이는 모두 더블포인터를 인자로 받아 사용한 예제이다. 이것은 동적할당받은 배열이기 때문에 가능하다. 엄밀히 말하자면 인자로서 char **pAry와 인자선언시 char **p가 같기 때문에 컴파일이 가능하다.
이때 인자를 char p[10][100] 처럼 선언하게되면 컴파일 에러가 발생한다.

그럼 정적인 선언일 경우 인자 전달은 어떻게 될 것인가.
이 경우 char **p와 같은 식으로 선언할 경우 컴파일 에러가 발생하게 된다.
아래 예제를 보자.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]
 
이 코드는 위의 2가지 예제와 같은 내용이며 같은 OutPut을 출력한다.
다른 점은 동적할당이 아닌 단순 2차원 배열로 선언한 경우이다.
이 경우 인자로 전달할 때는 가장 기본적으로 char (*p)[100]을 사용하게 되고, char p[10][100]도 가능하다.
그러나 char **p 처럼 2중 포인터를 사용하게되면 Compile Error를 발생하게 된다.
[cannot convert parameter 1 from 'char [10][100]' to 'char ** ']

그 이유를 한번 생각해 볼만 하다.
첫째로,
char [10][100]char **는 그 값이 똑같을 수는 있어도 의미하는 것은 전혀 다르기 때문에 컴파일 에러가 발생한다.
필자는 생각해본 것이, "배열 첫번째 주소만 받아온 후 인덱스이용하여 자유롭게 사용할 수 없을까" 이었다. 
하지만 그런 행동은 컴파일 에러를 발생한다. Compiler가 컴파일시 최대한의 에러를 방지하기 위해서 프로그래머가 하지 말아야 할 행동을 미리 제한하여 주고 알려주는 것이다. 

두번째로,
인덱스를 이용하여 배열의 주소를 계산하기 위해서이다.
배열을 자료형으로 저장할때는 두가지 방법이 있다.
하나는 행 우선(Row Major) 방법과 열 우선(Column Major) 방법이다.
이는 C보다 아래층의 어셈블리영역에서 배열을 일련의 공간인 메모리에서 저장할때의 방식을 말한다.
C Language에서는 Row Major 방식을 사용한다.
이는 하나의 행을 메모리에 올린후, 다음 행을 메모리에 올리는 방식이다. 즉 행을 중심으로 메모리 공간을 확보하는 방식이다.  
(배열을 어셈영역에서 메모리에 할당할때, 낮은 주소에 첫번째 원소의 주소를 할당하고, 다음 원소로 갈수록 높은 주소에 할당하게 된다.)

어셈영역에서의 배열할당 : http://shinluckyarchive.tistory.com/141
(SPARC기반이지만 기본적인거라 이해가 갈 것이다.)

주소를 계산하는 방식을 보면, 다음과 같다.
배열의 시작주소(즉 [0][0]의 주소) + [Row INDEX * 행의크기(열의 수)] + Column INDEX 
위 링크를 따라가면 이해에 도움이 될 것이다. 
(더하기를 하는 이유는 메모리 영역을 잡을때 한번에 전체 배열의 크기를 잡은 후에 더하기를 하여 낮은 주소 쪽에서 높은 주소쪽으로 올라가게 된다.)

이때 계산을 하게될 때 시작주소와 Row INDEX, Column INDEX는 인자를 통해 알게되지만,   
(시작주소=배열명, Row INDEX, Column INDEX= pAry[3][44]라고 할때 3과 44가 각각의 값)
행의 크기는 알 수 없다.

즉, 행(row)의 크기(=열의 갯수)를 알아야 배열의 원소값에 접근할 수 있기때문에 인자에 그 정보를 전해주어야 하는 것이다.

추가로 여담이지만, 필자가 그 원인에 대해 생각해봤을때 이런생각도 해봤다.
그렇다면 왜 동적할당일 경우에는 이중포인터로 인자를 받아들일 수 있는 것일까?
동적할당은 영역이 Heap이라서 시작점만 알고 있으면 자유롭게 접근이 가능할 것이고, 분명 추가로 메모리의 크기가 변할 수 있다. (realloc 계열을 사용한다면..) 그렇기 때문에 컴파일러에서 이중포인터로 주소를 받아들여 자유롭게 제어할 수 있도록 허용해 준 것이다. (물론 필자의 생각.)
하지만 실행시간에 어떻게 접근이 가능해 질까?
동적으로 생성될때, 위치정보가 표시되는 것일까? 이 점은 아직 알 수 없다 ㅠ.ㅜ;
추가적인 정보없이 접근가능한 것은 확실한데, 그 점이 Heap영역이라 Stack 영역과는 다르게 작동하는 듯하다.

Stack영역은 sp와 fp가 번갈아가며 일정 크기의 주소가 할당되고 / 해제되기 때문에 그 정보가 중요한데,
Heap영역은 한번 할당되면 계속남아 접근이 가능해지기 때문에 인듯싶다. (물론 필자의 생각.)
(Stack영역의 작동은 확실히 알겠는데, Heap은 필자가 잘 모른다. ㅠ.ㅜ)

※ 결국 힙과 스택의 차이점이라고 추측해본다.