앞서 설명했듯이, 배열의 이름은 1차원, 2차원, 3차원 배열에 상관없이 배열의 시작 주소를 나타낸다.
여기에서 추가로 알아야 할 점은 다음과 같다.
- 1차원 배열에서는 배열 이름 뒤에 첨자를 사용하면, 해당 첨자의 배열 요소를 가리킨다.
- 2차원 배열에서는 배열 이름 뒤에 행 첨자를 사용하면, 해당 행의 시작 주소를 가리킨다.
예시를 위해, 2차원 배열 b가 있다고 가정해보자.
int b[4][3];
b[0] 은 0행의 시작 주소,
b[1] 은 1행의 시작 주소,
b[2] 는 2행의 시작주소를 가리킨다.
그리고 2차원 배열의 각 원소에 접근하기 위해서는 인덱스가 2개씩 붙어야 한다.( b[0][0], b[0][1], b[1][0], b[1][1]...).
이제 배열 이름의 증감에 따른 변화를 살펴보도록 하자.
배열 이름이 포인터이기 때문에, 2차원 배열에서는 포인터 연산이 행 단위로 이동한다.
b는 0행의 시작 주소
b + 1은 1행의 시작 주소,
b + 2는 2행의 시작 주소를 가리킨다.
따라서 2차원 배열에서 포인터 연산을 하게 되면, 열의 개수에 따라 포인터의 메모리 공간이 증가하게 된다.
예를 들어, 열이 2개인 배열이라면 8바이트씩 증가하고, 열이 3개인 배열이라면 12바이트씩 증가하게 된다.
그러나 배열 이름 뒤에 행 첨자를 붙이게 되면 연산 방식이 달라진다.
b0] 배열에서,
b[0] + 1은 0행의 두 번째 원소를 가리킨다. == b[0][1]
b[0] + 2는 0행의 세 번째 원소를 가리킨다. == b[0][2]
정리하자면,
- 포인터인 배열의 이름(b)은 행 단위로 이동한다.
- 배열의 이름에 행 첨자(b[0])를 붙이면 열 단위로 이동한다.
이는 배열의 첫 번째 요소가 저장된 메모리 주소 바로 옆에 두 번째 요소가, 그 다음에 세 번째 요소가 순차적으로 저장되는 메모리 주소의 연속성 때문이다.
배열 이름 b는 배열 전체의 시작 주소를 가리키는 동시에 첫 번째 행을 가리키는 포인터로 동작한다.
즉, b는 첫 번째 행(b[0])의 시작 주소를 가리키지만, 그 타입은 int[3]이다. 따라서 b+1을 하면 첫 번째 행 전체(12바이트)를 건너뛰고 두 번째 행(b[1])의 시작 주소로 이동한다.
반면, b[0]은 첫 번째 행의 시작 주소를 가리키는 포인터의 역할을 한다. 즉, b[0] == &b[0][0] 과 같다.
b[0][0]의 주소에는 4바이트 메모리가 할당되어 있기 떄문에, b[0] + 1은 첫 번째 행에서 한 칸(4바이트)만큼 이동한 b[0][1]의 주소값을 가리키게 된다.
또한, 배열 이름은 포인터이기 때문에, 인덱스가 사실상 포인터 연산과 동일하다는 점도 기억해야 한다.
참고로, 인덱스를 사용하는 방법과 포인터 연산은 동일한 결과를 내지만, 인덱스(b[0][1])는 포인터 연산(*(b[0] + 1))으로 컴파일러에 의해 변환되는 단계를 거친다. ( b[0]은 첫 번째 행의 시작 주소를 가리키는 int* 타입의 포인터처럼 동작하기 때문에, b[0][1] 은 (*b[0]+1)과 같다. )
따라서 인덱스를 사용하여 배열 원소를 접근하는 것과 포인터 연산을 통해 배열 원소를 접근하는 것은 동일한 결과를 얻게 된다.
결국, 2차원 배열은 본질적으로 여러 개의 1차원 배열이 모여 있는 것이다.
1차원 배열은 단순히 포인터 하나로 시작 주소를 알면, 나머지 요소들은 메모리 상에서 연속적으로 배치되어 있으므로 쉽게 접근할 수 있는 반면, 2차원 배열에서는 배열이 고정 크기로 할당되는지, 동적으로 할당되는지에 따라 메모리 관리 방식이 달라진다.
- 고정 크기의 2차원 배열
고정 크기의 2차원 배열은 메모리 상에서 연속적으로 붙어서 배치된다. 따라서, 첫 번째 행의 주소를 알면, 마지막 행의 주소까지도 알 수 있으니까 단 하나의 포인터로도 메모리 관리가 가능한다.
#include <stdio.h>
int main() {
int a[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// 배열 a의 시작 주소를 포인터로 가리킴
int (*p)[3] = a; // p는 int[3]를 가리키는 포인터
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", p[i][j]); // p[i]는 a[i]와 동일하게 동작
}
printf("\n");
}
return 0;
}
- 동적 크기의 2차원 배열
동적 할당을 통해 2차원 배열을 만들 때는 각 행이 할당될 때 메모리 배치가 여기 저기 될 수 있다. 그래서 첫 행의 주소를 안다고 해서 두 번째 행의 주소를 찾을 수 있다는 보장을 할 수 없다. 그렇기 떄문에 각 행의 시작 주소를 따로 관리해야 하고, 이를 위해 포인터 배열을 사용해준다.
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3;
int cols = 3;
// 각 행의 시작 주소를 가리킬 포인터 배열
int** a = (int**)malloc(rows * sizeof(int*));
// 각 행을 위한 메모리 동적 할당
for (int i = 0; i < rows; i++) {
a[i] = (int*)malloc(cols * sizeof(int));
}
// 값 초기화
int value = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
a[i][j] = value++;
}
}
// 배열 출력
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
// 할당된 메모리 해제
for (int i = 0; i < rows; i++) {
free(a[i]);
}
free(a);
return 0;
}
'C 언어' 카테고리의 다른 글
C 언어: scanf 사용 시 입력 버퍼 비우는 방법 (0) | 2024.09.15 |
---|---|
반복문 종료 조건 (논리 연산자 &&와 ||의 차이) (0) | 2024.09.14 |
c언어_2차원 배열과 포인터의 관계(1) (0) | 2024.09.06 |
c언어 포인터와 1차원 배열의 관계 (0) | 2024.08.25 |
C언어 포인터 배열과 이중 포인터 (0) | 2024.08.25 |