본문으로 바로가기

파일의 IT 블로그

  1. Home
  2. 프로그래밍 강좌/C
  3. [C언어 강좌] #12-2 [Array] 다차원 배열

[C언어 강좌] #12-2 [Array] 다차원 배열

· 댓글개 · KRFile

안녕하세요. 오늘은 앞에서 배운 1차원 배열에 이어 다차원 배열을 보겠습니다.

 

저희는 3차원 세계에 살고 있습니다. 그렇기 때문에 4차원을 이해할 수 없죠.

1차원은 선분, 2차원은 면, 3차원은 입체라고 합니다.

 

배열을 만들땐 1차원을 넘어 2차원, 3차원, 4차원,..., n차원 배열까지 만들 수 있습니다.

사실 4차원부턴 사용할 일이 그렇게 많지 않습니다. (AI 라던가, 테트리스 라던가..)

 

그렇기에 다차원 배열은 대개 3차원 까지를 가리킵니다. 물론 다차원 배열은 2차원 배열 이상을 가리키므로

4차원, 5차원을 다차원 배열이라고 불러도 무리가 없습니다.

 

'다차원 배열'이라고 이름 붙은 이유는 인간의 논리인 1차원, 2차원, 3차원을 배열에 적용시켜서 이해하기 위함입니다.

 

다음 사진은 2차원 배열의 논리적 구조와 물리적 구조를 보여주는 사진입니다.

왼쪽은 논리적 구조로 사람이 이해할만한 2차원(평면)의 구조입니다.

 

오른쪽은 2차원 배열의 물리적 구조입니다.

 

중요한 점

컴퓨터의 물리적 메모리는 2차원이 아닌 1차원 배열의 형태입니다.

컴퓨터의 물리적 메모리 구조 자체가 2차원이 아니기 때문이죠.

실제로 컴퓨터에 배열이 쌓이는 형태가 저렇다는 겁니다. 차곡차곡 쌓이죠.

 

앞으로 2차원 배열을 행, 열 / 3차원 배열을 면 행 열 로 배우는 것은 인간이 이해하기 편하게 하기 위한 "논리적 구조" 를 배우는 것 이며 

실제로 메모리란 것은 애초에 평면구조기 때문에,

실제 컴퓨터에 쌓이고 있는 메모리의 형태는 2차원 배열이던, 3차원 배열이던, n차원 배열이던 간에 결국엔 모두 1차원 배열의 형태로 수직적으로 쌓이게 됩니다.

 

우선은 인간의 사고로 이해하기 위해 2차원, 3차원은 왼쪽의 논리적 구조로 설명하겠지만, 우리가 사는 3차원 세계에선 4차원 그림을 그릴 수 없기 때문에 4차원 배열부턴 물리적 구조로 이해하셔도 좋을 거 같습니다.

 

 

2차원 배열의 선언

 

2차원 배열은 앞에 1차원 배열과 거의 동일합니다. 대괄호가 [ ] 하나 추가됐습니다.

 

물리적 구조는 그렇게 어렵지 않으니 지금부터는 논리적 구조로 설명하겠습니다.

행과 열로써 2차원 배열을 이해하시면 됩니다.

 

이해가 잘 안 되시면 아래 내용을 계속 읽어보시면 이해가 됩니다.

참고로 가로가 행이고 세로가 열입니다. 알고 계시죠 ㅎㅎ?

(가로 , 세로열

2차원 배열에 저장하기

#include <stdio.h>

int main(){
	int array[2][3]; //총 2행 3열 
	
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; //0행에 저장 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	printf("%d %d %d \n", array[0][0],array[0][1],array[0][2]);
	printf("%d %d %d", array[1][0],array[1][1],array[1][2]);
	return 0;
}

가로의 길이 2, 세로의 길이가 3인 2차원 배열에 값을 넣는 예제입니다.

 

그림으로 본다면 이렇게 되겠네요. 크기는 총 2x3x4 = 24byte입니다. 물론 배열을 그림으로 표시하는 건

편의를 위함이므로 배열 1개당 int형 자료형의 기본 크기인 4바이트라고 이해하시면 됩니다.

 

그럼 저번에 배운 중괄호 { }에 의한 선언도 해봐야겠죠?

 

#include <stdio.h>

int main(){
	int array1[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
	int array2[3][4] = {0,1,2,3,4,5}; //선언되지 않은부분은 0으로 자동초기화 
	//int array3[][] = {0,1,2,3,4,5}; //오류발생 
	
	int array4[2][3] =  //요런식의 선언도 가능 
	{
		{0,1,2}, //0행
		{3,4,5} //1행
	};
	
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 4; j++){
			printf("%3d", array1[i][j]);
		}
		printf("\n"); //행넘어갈때마다 개행 
	}
	
	printf("\n");
		
	for (int i = 0; i < 3; i++){
		for (int j = 0; j < 4; j++){
			printf("%3d", array2[i][j]);
		}
		printf("\n"); //행넘어갈때마다 개행 
	}
	
	printf("\n");
	
	for (int i = 0; i < 2; i++){
		for (int j = 0; j < 3; j++){
			printf("%3d", array4[i][j]);
		}
		printf("\n"); //행넘어갈때마다 개행 
	}
	return 0;
}

  0  1  2  3
  4  5  6  7
  8  9 10 11

  0  1  2  3
  4  5  0  0
  0  0  0  0

  0  1  2
  3  4  5

--------------------------------
Process exited after 0.03337 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음은 중괄호를 통해 2차원 배열을 선언하는 다양한 방법입니다.

첫 번째는 1차원 배열과 같게 중괄호 내부에 주욱 나열하는 방법

두 번째는 배열 크기를 선언하고 중괄호 내부에서 값을 적어주다가 더 이상 적지 않았습니다.

앞에 1차원 배열과 동일하게 역시 0으로 자동 초기화됨이 확인됩니다.

세 번째는 중괄호를 여러 개 사용하는 방법인데 가독성이 좋아서 저는 이 방법을 추천합니다.

 

출력은 for문을 통해 진행하였습니다. %3d 기억나시죠? (필드 폭 3칸 확보 후 오른쪽 정렬 나머지는 공백으로)

배열은 0부터 시작한다고 하였는데요. for문을 쓸 때도 int i = 0; i < 3; i++처럼 반복 조건에

거추장스럽게 +1, -1을 해줄 필요가 없어서 깔끔하네요. zero-based index 대한 장점이라고도 할 수 있겠습니다.

 

참고로 주석처리돼있는 부분처럼 선언하면 오류가 발생하는데요.

1차원 배열은 크기를 정해주지 않아도 값을 넣으면 크기가 자동으로 정해졌습니다.

왜냐면 넣는 방법이 차곡차곡 하나밖에 없거든요.

 

대신에 2차원 배열 이상부턴 넣는 방법이 2x3이나 3x2 라던지 여러 가 지므로 크기를 제대로 명시해주셔야 합니다.

 

#include <stdio.h>

int main(){

	int array1[3][] = {0,1,2,3,4,5}; //선언불가 
	int array2[][3] = {0,1,2,3,4,5}; //선언가능 
	

	return 0;
}

 

 단 열만 써주는 건 가능합니다.

데이터를 넣어주고 3 열이라고 명시해주면 행은 데이터 수만큼 자동으로 정해지죠.

 

행만 써주는 건 안 되는 게, 그 행에 몇 개를 넣을지 모르니깐요.

어쨌든 크기를 명시해주는 게 제일 좋습니다 ^^

 

2차원 배열의 주소와 값 참조하기

저번 시간에 사용한 & 주소 연산자, * 연산자를 활용하여 2차원 배열의 주소와 값을 참조해보겠습니다.

&2차원 배열의 요소 하면 2차원 배열 요소의 메모리 주소 값을 가져올 수 있습니다.

 

 

#include <stdio.h>

int main(){
	int array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	printf("%x %x %x \n", &array[0][0], &array[0][1], &array[0][2]);
	printf("%x %x %x", &array[1][0], &array[1][1], &array[1][2]);
	return 0;
}

62fe00 62fe04 62fe08
62fe0c 62fe10 62fe14
--------------------------------
Process exited after 0.03465 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

이렇게 출력해보면 역시나 4바이트씩 차이를 가지고 메모리 공간이 할당되어 있습니다.

+4 씩 연속적으로. 앞에서도 얘기했지만

결국 컴퓨터에 실제로 메모리에 쌓이는 건 1차원 배열형태란 게 나왔네요. (위의 물리적 구조 사진 참고)

 

* 배열은 2차원이든 3차원이든 연속된 메모리 공간에 1차원으로써 할당이 됩니다.(일렬로 배치가 된다는 뜻.)

 

#include <stdio.h>

int main(){
	char array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	printf("%x %x %x \n", &array[0][0], &array[0][1], &array[0][2]);
	printf("%x %x %x", &array[1][0], &array[1][1], &array[1][2]);
	return 0;
}

62fe10 62fe11 62fe12
62fe13 62fe14 62fe15
--------------------------------
Process exited after 0.04405 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

char은 1바이트씩.. 이것 역시 자명하죠?

 

그리고 배열 #1편에서 배열의 이름은 배열의 시작 주소이다.라는 말을 했었습니다.

2차원 배열도 배열이므로 마찬가지입니다.

 

#include <stdio.h>

int main(){
	int array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	
	printf("%x %x\n", array, &array[0][0]);
	printf("%x %x\n", array+1, &array[1][0]);
	return 0;
}

62fe00 62fe00
62fe0c 62fe0c

--------------------------------
Process exited after 0.02852 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

2차원 배열의 시작은 [0][0]에서 시작되죠.

그렇기에 printf("%x %x\n", array, &array[0][0]); 요 부분 출력이 동일하고요.

 

+1 연산을 하면 한 칸 건너뛴다고 했죠.  2차원 배열에서 주의할 점은 맨 앞 행을 건너뜁니다.

array + 1 = &array[1][0] 동일한 표현입니다

처음 시작주소 array[0][0]에서 array[1][0] 까지의 간격은 12바이트입니다. 즉 12바이트 건너뛴 거죠

 

16진수 표현법에 의해 0c = 12로 맞네요.

 

2차원 배열의 행의 대표 주소

 array[0]은 1차원 배열에서 array배열의 첫 번째 요소를 가리켰습니다.

이것이 2차원 배열에선 조금 다른데 array[0]은 2차원 배열 0행의 대표 주소

즉 array[0][0]의 메모리 주소를 가리킵니다.

 

array[1]은 1행의 대표주소 array[1][0]의 메모리를 가리키고요.

각 행 맨 앞에 있는 놈이 대표가 됩니다.

 

#include <stdio.h>

int main(){
	int array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	
	printf("%x %x\n", array[0], &array[0][0]);
	printf("%x %x\n", array[1], &array[1][0]);
	return 0;
}

62fe00 62fe00
62fe0c 62fe0c

--------------------------------
Process exited after 0.03974 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

 

1차원 배열에서 *(array+0) = array[0] 이였습니다.

배열의 이름은 배열의 시작주소, 그곳에 다시 값을 참조하는 * 연산자를 붙이면

array[0]의 값이 딱 하고 나오는거죠.

 

그런데 2차원 배열에선 좀 달랐습니다.

위에 보시다싶이

array+1이 1행의 대표주소가 됩니다.

*(array+1) 을 하게되면   *(array+1)  = array[1]이

되게되고 이것은 array[1]행의 대표주소를 가져오는 &array[1][0]이 되게되죠!

 

그런데 조금 납득 안가는 부분은 아까 예제에서 실행시켜본 바론 array+1 = &array[1][0] 가 되게 되는데 그럼

*(array+1)을 하게되면 array[1][0]의 값이 딱 나와야 할건데 그렇지 않다는거죠.

 

솔직히 *(array+0) 가 array+0 이랑 같다는게 납득이 좀 안되긴하죠 ^^;;

 

저는 *(array+1)  = array[1] 이것을 시행할때만큼은 1차원 배열의 논리를 따르기로 했다.. 이렇게 이해했습니다

그 후 array[1] = &array[1][0] 이 되는거죠.

 

정리하자면 *(array+1) = array[1] = &array[1][0] 입니다.

이거만 받아들이시면 뒤내용을 진행할 수 있습니다.

 

 

#include <stdio.h>

int main(){
	int array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	
	printf("%x %x\n", *(array+0), &array[0][0]);
	printf("%x %x\n", *(array+1), &array[1][0]);
	printf("%x %x\n", array+0, &array[0][0]);
	printf("%x %x\n", array+1, &array[1][0]);
	return 0;
}

62fe00 62fe00
62fe0c 62fe0c
62fe00 62fe00
62fe0c 62fe0c

--------------------------------
Process exited after 0.0325 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

예제를 읽어보고 값을 출력해보세요.

보시면 *(array+0) 의 값, array + 0 의값, &array[0][0]의 값 모두 같은 결과입니다.

자세히 안읽어보면 뭔가 다 거기서 거기같을 수 있거든요.

 

#include <stdio.h>

int main(){
	int array[2][3];
	array[0][0] = 1;  array[0][1] = 2; array[0][2] = 3; 
	
	array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
	
	
	printf("%x %x\n", array[0] + 1, &array[0][1]);
	printf("%x %x\n", array[1] + 1, &array[1][1]);
	
	return 0;
}

요러한 연산도 가능합니다.

 

array 

array[0] = *(array+0) 이고

array[0] + 1  = *(array + 0) + 1 입니다. 

array[0] 번 주소에서 1칸 건너뛰므로 array[0][1]의 주소와 동일해집니다.

 

어렵죠? 저도 이부분은 항상 공부하면서 여러번 복습해봐야 겠더라구요 ^^;;

 

 

3차원 배열의 선언

3차원 배열은 앞에 2차원 배열에 면이라는게 추가된 형태입니다.

지금까지 평면도형이였다면 이젠 입체도형으로도 그려볼 수 있습니다.

 

면은 직육면체(정육면체)에서 깊이로 해석할 수 있습니다.

 

논리적 그림으로 본다면 대충 이런형태가 되는데요..

열심히 만들어 본다고 했는데 선으로 나누질 않으니 영 배열이 아닌거같네요 ^^;; 양해좀 부탁드립니다.

 

 

출처 - https://eskeptor.tistory.com/17

인터넷에 찾아보면 이렇게 3차원 배열을 면을 각각 따로 나눠서 2차원 배열이

여러개 있는 형태로 그려놓은 그림도 있는데

 

혹시 위 그림(입체도형)이 직관적으로 잘 안와닿으신다면 다음과 같이 이해하셔도 무방합니다. 

 

 

3차원 배열에 저장하기

#include <stdio.h>

int main(){


	int array2[3][3][3] = 
	{
		{ //0면 
		  {0,1,2}, //0행 
		  {3,4,5}, //1행 
		  {6,7,8} //2행 
		},
		{ //1면 
		  {9,10,11}, //0행 
		  {12,13,14}, //1행 
		  {15,16,17} //2행 
		},
		{ //2면 
		  {18,19,20}, //0행 
		  {21,22,23}, //1행 
		  {24,25,26} //2행 
		}
	};
    
    printf("%d", sizeof(array2));
	

	return 0;
}

108
--------------------------------
Process exited after 0.0368 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

 대충 이런식으로 하시면됩니다.

3행 3열의 2차원 배열이 3개(3면) 있다고 생각하시면 됩니다.

 

크기는 3x3x3x4 = 108byte 입니다.

3차원 배열은 크게 설명할게 없네요..

 


사실 C언어하면서 멘붕이 오는부분..

2의 보수는 이해하면 그렇다고 쳐도 배열 부분에서 한번, 그리고 포인터와 배열을 섞어쓰기 시작할때

또 발생하죠. 결국 복습만이 살길입니다. 

 

이해를 돕기 위해 이런 어려운 내용들은 계속해서 수정, 추가해 나가겠습니다. 

이제 다음강의에선 C언어의 백미, Low-level 언어임을 보여주는 

메모리 주소를 다루는 변수 포인터에 대해 알아보겠습니다. 감사합니다 ^^

SNS 공유하기
💬 댓글 개
이모티콘창 닫기
울음
안녕
감사해요
당황
피폐

이모티콘을 클릭하면 댓글창에 입력됩니다.