[문과 코린이의 IT 기록장] - C,C++ 2차원 배열(2차원 배열, 2차원 배열에서의 포인터)
1. 2차원 배열의 [ ]연산자
int A[2][3];이 표현되는 방식
ex )
이를 보면 arr[0] = &arr[0][0]이고, arr[1] = &arr[1][0]라는 것을 알 수 있다.
이를 통해, 1차원배열과 마찬가지로 sizeof나 주소값 연산자와 사용되지 않을 경우, arr[0]은 arr[0][0]을 가리키는 포인터로 암묵적으로 타입 변환되고, arr[1]은 arr[1][0]을 가리키는 포인터로 타입 변환된다.
2. 2차원 배열 sizeof
ex )
- 총 열의 개수
: sizeof(arr[0]) / sizeof(arr[0][0]);
// 총 열의 개수는 한 행의 크기에, 한 배열의 크기를 나누면 알 수 있다. sizeof(arr[0])은 0번째 행의 크기가 나온다. 그리고 sizeof(arr[0][0])은 int형 크기인 4를 반환하게 된다. 따라서 값은 3이 나온다.
- 총 행의 개수
: sizeof(arr) / sizeof(arr[0]);
// 총 행의 개수는 전체 크기를 열의 크기로 나누면 알 수 있다. sizeof(arr)은 전체 사이즈의 크기를 나타내고, sizeof(arr[0])은 하나의 열의 크기를 나타내므로, 값은 2가 나온다.
3. 2차원 배열에서, 포인터의 type
1) 포인터의 형(type)을 결정짓는 두 가지 요소
ex )
// **parr에 arr의 주소값을 저장함.
컴파일 시 이상한 오류가 발견되게 된다.
이는, parr[1][1]에서 이상한 메모리 공간에 접근했기 때문에 발생한 일이다.
그렇다면, 왜 이상한 공간에 접근하게 되었을까?
이해를 도모하기 위해서, 일차원 배열과 이차원 배열에 대한 주소 접근 방식을 설명하도록 하겠다.
a. 일차원 배열에서의 경우
- 배열의 자료형과, 시작 주소값을 안다고 가정한다면?
- 시작주소 : 0x K
- 자료형 : int 형
- 원소 : N개
만약 N번째 원소에 접근한다면, (K + 4(N-1))의 주소로 접근해야 한다.
b. 이차원 배열의 경우
int arr[A][B]의 경우 , 2차원 평면 그림을 그린다면?
이렇게 이루어진다.
우리가 보기 쉽게 그러놓은 2차원 배열 평면도를, 실제 메모리상에 올리게 되면 1차원으로 올라가게 된다.
그렇다면 메모리에 올라간, 이차원 배열을 그려보면?
이와 같이 표현된다.
문제 )
그렇다면 위의 일차원 배열과 같이, 이차원 배열의 시작주소와 자료형을 알고 있는 arr[C][D]라는 원소에 접근한다고 가정해본다면?
- 시작주소 : 0x K
- 자료형 : int 형
- 원소 : N개
풀이 )
arr[C][D]라는 원소는, (C+1)번째 행의 (D+1)열에 위치해 있다.
1. (C+1)번째 행의 시작 주소를 계산해 본다면?
: (K + 4(C*B)) // K는 시작주소, 4는 int형 반영, C는 이전까지의 행의 개수, B는 행들의 길이
2. (D+1)번째에 있다는 것까지 반영해서 계산해본다면?
: (C+1행 시작주소) + 4*D = (K+4(C*B)) + 4*D
* 이 식에 B가 들어간다는 것을 확인해볼 수 있다. *
즉, 다시말하자면 arr[A][B]를 정의했을 때의 B가, 원소의 주소값을 계산하기 위해 필요하다는 것이다.
따라서, 앞의 예제로 돌아가보자면, parr[1][1]은 parr으로 컴퓨터가 원소를 참조하려고 하지만, B의 값을 알 수 없기 때문에 제대로 된 연산을 수행할 수 없다는 것이다.
결론적으로 포인터 형을 결정하는 2가지를 주의할 필요가 있다.
1. 가리키는 것에 대한 정보 // int*이면 int를 가리키고, char **이면 char*을 가리킨다는 것
2. 1 증가시 커지는 크기 // 2차원 배열에서는 B*(형의 크기 ex. int형이면 4byte)
2) 2차원 배열을 가르키는 포인터
ex 1) 2차원 배열을 가르키는 포인터 예제
- 위의 내용들에 기반하면, 2차원 배열을 가르키는 포인터는 배열의 크기에 관한 정보가 있어야 한다.
[ 2차원 배열을 가르키는 포인터 정의 방법 ]
배열의 자료형 (* 포인터 이름)[ 2차원 배열의 열 개수 ]
ex) int (*parr)[3] // 포인터 변수 parr은, int형 3개의 열의 개수를 가진 2차원 배열을 가르킨다.
ex 2 ) 복습 예제
배열의 크기 (열의 개수)가 다르기 때문에, crr은 실행이 불가능함.
4. '&배열 이름' 이해하기 (2차원 배열 개념을 포함한, 1차원 배열의 심화과정)
배열 이름에 주소값 연산자 사용
- 배열 이름에 sizeof연산자, 혹은 주소값 연산자를 사용할 때 빼고는, 전부 포인터로 변환이 이루어진다.
- 그렇다면, 주소값 연산자(&)를 사용하게 된다면?
ex )
int (*parr)[3] = &arr;
// arr은 크기가 3인 배열이기 때문에, &arr을 보관할 포인터는 크기가 3인 배열을 가리키는 포인터가 되어야 한다.
&arr은 포인터로 암묵적 변환이 이루어지지 않는다.
그렇다면, &arr은 어떤 역할을 하는가?
arr과 parr, &arr이 모두 배열의 첫 번째 원소의 주소값을 출력한다.
이렇게 동작하는 이유는, B언어로부터 알 수 있다.
C언어는 B언어에서 파생된 언어인데, B언어에는 실제 배열이 존재하고, 그 배열을 가리키는 포인터가 따로 있었다. 즉, B언어에서 arr과 arr[0], arr[1]은 서로 다른 메모리를 차지하며, arr은 실제로 arr[0]을 가리키는 포인터였다.
따라서 arr의 값을 출력할 때, 실제로 arr[0]의 주소값이 나왔고, &arr은 arr의 주소값이 나왔다. 따라서 B언어에서는 arr과 &arr이 다른 값을 출력했다.
그러나 C언어에서는, B언어의 문법을 계승하지만, 이와 같은 비효율적으로 배열을 정의할 때 배열의 시작점을 가리키는 포인터로 공간을 낭비하고 싶지 않아서, 조금 이상하지만 메모리 공간을 효율적으로 쓸 수 있도록, 위와 같은 배열-포인터의 관계를 만들게 되었다.
5. 2차원 배열의 연산
2차원 배열을 가르키는 포인터의, 배열의 크기(열의 개수)에 대한 내용을 한번 더 생각해보자.
(arr + 1) - arr = 0x C = 12 (16진수를 10진수로 변환)
12 = 3 * 4(int형의 크기)
배열의 크기(열의 개수)는 3이 된다는 것을 알 수 있다.
이를 통해 arr + 1을 하면, 하나의 열을 뛰어넘게 된다는 것을 알 수 있다.
즉, arr[0][0]에서 arr[1][0]으로 뛰어넘게 된다는 것이다.
다시 말하자면, arr + 1이, 'a[1][0]의, 주소값을 가리키는 포인터(a[1])'를 가리키게 된다는 것이다.
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |