본문 바로가기

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

[ALL] 2차원 배열에 대한 고찰 및 분석(2-Array)

2차원 배열의 경우 1차원 배열과는 포인터에 대한 개념이 살짝 다르다.
다르다기 보다는 좀더 알아야할 표현들이 많이 존재한다.

필자도 개념은 잡혀있다고 생각하는데 가끔 책을 다시보다보면 약간 헷갈리는 점이 있어서
테스트와 함께 포스팅을 작성해본다. (이것 자체가 확실히 개념이 잡히지 않은건가 -.,-;;)

2차원 배열에 대한 단순 접근은 대부분의 사람들이 공부해서 알듯이 단순 명료하다.
그럼 2차원 배열의 선언 예제를 잠깐 보도록 하자.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]
#include<iostream>
using namespace std;
int main(){
	int ary[3][3]={{1,2,3},{4,5,6},{7,8,9}};
	int ary2[][3]={{1,2,3},{4,5,6},{7,8,9}};		
	//int ary3[][]={{1,2,3},{4,5,6},{7,8,9}}; <--Compile Error
	int ary4[3][3]={1,2,3,4,5,6,7,8,9};
	int ary5[][3]={1,2,3,4,5,6,7,8,9};
	//int ary6[][]={1,2,3,4,5,6,7,8,9}; <--Compile Error
	return 0;
}

위의 간단한 예제는 선언 방법에 대해 테스트를 해보았다.
예제를 보면 6, 9 Line이 Compile Error 가 발생하는 것을 볼 수 있다.
그 이유에 대해서는 이전 포스팅에서의 인자전달에서 언급하였다.
(정적 2차원 배열의 인자전달시 int (*p)[3] 식으로 전달하는데 행의 크기==열의 개수를 알려주어야 하기 때문이다.)

2차원 배열의 동적할당 및 함수의 인자전달 : http://shinlucky.tistory.com/326

즉, 열의 개수==행의 크기를 알아야 2차원 배열로 할당된 메모리상의 크기에 맞추어 INDEX에 맞추어 접근하여 데이터 값을 할당할 수 있는 것이다.
우측 r-Value에는 "{}"표시는 자유롭게 해주어도 상관없다.
 
그럼 다음으로 3, 3의 2차원 배열의 메모리 할당 모습을 보자. 테스트를 통해 메모리 주소를 비교해가며 확인해 볼 것이다.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

출력하는 부분이 조금은 지저분해 보일지만, 그나마 조금더 가독성 있으라고 다 풀어서 써봤다.
내용은 일단 처음에는 2차원 배열의 값과 주소값(R-value와 L-value)를 출력하였고,
그 밑에는
배열명을 이용하여 접근할 수 있는 여러 경우와 각 경우에 +1을 한 값이 무엇을 의미하며
각 경우가 가리키고 있는 것이 무엇인가

에 대해 테스트 해보았다.
위 테스트의 결과 값은 다음과 같다.


<깜빡하고 16진수 표시를 하지 않았지만 위 OutPut의 주소는 모두 16진수이다. 0x표시를 깜빡했다. ㅠ.ㅜ;>

위의 결과 값으로 2차원 배열을 분석해보자.

1-Array and Pointer(일차원 배열과 포인터) : http://shinluckyarchive.tistory.com/199

OutPut의 최상위 부분을 보면 인덱스 별로 주소값을 볼 수 있다. 2차원 배열의 원소가 INT Type, 즉 4byte로 잡혀있기 때문에 4byte를 단위로 메모리 공간이 잡히게 된다. 0x0012FF5c를 시작으로 4씩 증가하게 된다.
위의 출력 방식은 눈에 구별이 쉽게 행단위로 출력했고, 추상적인 메모리 위치이다. 3 x 3 행렬이다.
INDEXValue값은 같게 설정해 놓았다.

[분석해보기전에 기본적인 전제]
1. 배열명은 배열첫번째 요소의 주소값이다, 단 Sizeof로 쓰일때는 배열 전체를 가리킨다.
2. int ary[3][3]이 선언되어 있을때 ary[3]에 들어가는 값은 주소값이다. 엄밀히 말하자면 int [3]의 주소값, 그 배열의 첫번째 주소값==배열명이 들어가 있다. 그 이유는 []와 (), *에는 항상 주소값만 들어갈 수 있다.
int ary[3][3]을 분석하면 먼저 ary[3]의 원소가 int [3]라고 볼 수 있다. 이 점을 이해해야 한다.
3. 2차원배열에서 (역시 int ary[3][3] 선언) ary[1]의 값과 &ary[1], &ary[1][0]의 값은 같다.
   
다만 가리키는 대상이 다르다. 다르다는 것은 각 값에 +1을 해보면 그 차이를 알 수 있다. 
4. 2차원 배열의 의미는 하나의 배열에 원소값에 배열이 들어간 개념이다.

※ 필자가 테스트해보면서 약간 헷갈렸던 부분이 있었다.
    int ary[3][3]가 선언된 상태에서 ary[1]의 값이 주소값이 라면 &ary[1]은 주소값의 주소값이라는 이야긴데, 이러한 접근이 가능한가 의문이 들었다. 예를들면 일반변수 int a; 에서 &&a와 같은 선언을 하는거랑 마찬가지라고 생각했다. 하지만 실제로 주소값의 주소값은 말도 안될 뿐더러 &&와 같은 연산은 변수앞에서 주소의 주소로 선언될 수 없다. (컴파일 에러발생)
    그 이유는 아래에서 언급될 것이다.
     (요약하자면 주소값과 실제 값이 같은 것. 즉 실제값에 주소값이 들어 간 것이다. r-value와 l-value가 같다는 뜻)

OutPut의 첫번째 5행을 보자.
ary는 배열명으로 항상 배열의 첫번째 주소값을 갖는다. 또한 ary[0]의 값(R-Value)에는 또하나의 배열명이 들어있다.
즉 ary=ary[0]=&ary[0]=&ary[0][0] 이 4가지는 모두 값은 값을 나타낸다.
하지만 ary에 +1 을 할경우에는
ary는 첫번째 요소값 ary[0]의 주소를 가지고 있기 때문에 ary[1]의 값을 가리키게 된다.
(ary[1]은 또한 &ary[1][0]값을 나타낸다.)
&ary의 값은 ary의 주소값으로 첫번째요소의 주소값의 주소값이다. 말로하면 이해하기 힘들지만, 결국에는 ary라는 배열의 첫번째 값으로 ary 배열이 할당된 전체의 주소 개념이다. 그렇기 때문에 +1 을 할 경우에는 전체 배열크기를 증가시킨 0x0012FF80을 가리킨다. 이는 전체 배열 3x3x4 byte를 증가시킨 메모리 상의 위치이다.
ary[0]에는 또 하나의 배열이 들어있다. 그 배열명이 들어있는데, 이는 주소값이 들어있는 것이다. (배열명==첫번째 요소값)
헷갈리겠지만, 이 의미는 곧 ary[0]의 값(Value)과 &ary[0](ary[0]의 주소)값이 같다는 것이다. 이것이 바로 2차원 배열의 특징이다. ary[0]과 &ary[0]은 같은 값을 나타내는 점을 보면 알 수 있다.
차이점은 +1을 해보면 알 수 있다.
ary[0]은 배열명이 들어 있기 때문에 첫번째 요소를 가리키게 된다. 그렇기 때문에 +1을 하면 한 원소(4 Byte)가 증가한 모습을 볼 수 있다.
하지만 &ary[0]에 +1 할 경우에는 다르다. 이 경우는 ary[0]의 주소값에 +1을 했기 때문에 3개 원소(12 Byte)가 증가한 모습을 볼 수 있다.
이해가 안간다면 위의 [분석하기 위한 전제]를 잘 살펴보자.  

위와 같은 방식으로 각 배열의 Row Index에 따른 결과는 마찬가지이다.

솔직히 위 설명을 한번에 이해하기는 힘들 것이다.
그래서 처음에 무식하게 외우려면,
2차원 배열에서 요소의 "&"를 붙인 후 +를 하면 전체크기 만큼 증가한다고 생각하자. (약간 위험한 생각일지도 모른다.)
그 요소를 파악함에 있어서 그 요소가 배열인지, 아니면 그냥 자료형인지 정확히 판단하자.

조금이라도 보기 쉽게 그림으로 나타내면 다음과 같다.


위 글로된 설명을 기본으로 만들어본 표 모양이다.

결국
배열의 원소 값이 무엇이고,
주소의 개념을 확실히 알고,
포인터가 가리키고 있는게 무엇인지 안다면 쉽게(?) 이해가 갈 것이다.

2차원 배열의 개념적인 면에서 헷갈리는 이유는,
배열이라는 것 자체가, 배열명이 두가지로 쓰이기 때문일 것이다. (알아보고 싶다면 위에서 링크된 1차원 배열 포인터를 보자.)
그리고 2차원 배열의 경우 첫번째 배열(Row를 말하는 것임) 요소의 R-Value와 L-Value가 일치하는 특이한 구조라서 그런 것이라고 생각된다.

위의 2차원 배열에 대한 기본적인 접근이 이해가 갔다면 다음 내용도 바로 알 수 있을 것이다.
다음 예제를 보자.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

위 코드는 단순히 2차원 배열 내용을 출력하는 부분이지만, 방식이 다르다.
결과는 3가지 모두 같은 값을 출력한다. 하지만 처음의 for 문에서는 ary[i][j]를 통해 접근하였고,
두번째는 *(*(ary+i)+j)를 통해 접근,
세번째는 *(ary[i]+j)를 통해 Value에 접근한 모습이다.
첫번째 방법은 가장 기초적인 배열접근 방법으로 배열연산자 []를 통해 접근한 모습이고,
두번째는 포인터 주소를 이용하여 접근한 가장 직관적이고 포인터 개념에 명확한 접근 방식이다.
첫번째와  두번째는 많이 사용해 보아서 알 것이다.
세번째의 경우는 이를 혼합하여 사용한 경우이다.
직관적으로 이해하기 위해서는 배열값에 접근하는 경우에 []가 *와 비슷한 역할을 한다고 생각하면 된다.
(이것도 약간 위험한 생각이긴 하다 -.,-)

이것에 대한 설명은 이 코드 전에 2차원 배열 분석에서 충분히 되었다고 생각한다.
(비록 이해하기 어렵게 필자 생각대로 난해하게 설명했더라도 ㅠ.ㅜ;)

바로 예제를 보자.

[작성환경 : Window XP   작성툴 : Visual Studio 6.0   컴파일러 : Visual Studio 6.0    사용언어 : CPP]

위 코드는 포인터 p를 이용하여 출력을 해본 모습이다.
무심코 5 Line처럼 이중포인터로 설정하면 Compile Error가 발생된다.
6 Line처럼 열(column)의 개수를 알려주어야 한다.

이중포인터는 11~12 Line에서처럼 포인터 배열에서 사용이 가능하다.

포인터 배열 : 요소값이 포인터인 배열 (ex : int *p[10])
배열 포인터 :
요소값이 배열인 포인터 (ex : int (*p)[10])  <--배열 포인터가 이차원 포인터이다.

이해를 위해 연산자 우선순위를 참고 : http://shinluckyarchive.tistory.com/212