Dev/C
[C언어] 배열과 포인터의 관계 (1차원 배열 & 2차원 배열)
청월누리
2025. 4. 8. 01:44
C언어에서 포인터와 배열은 밀접한 관계를 가지고 있습니다. 이 글은 1차원 배열과 2차원 배열을 중심으로, 포인터와 배열의 관게에 대해 정리한 글입니다.
1차원 배열과 포인터
1차원 배열의 메모리 구조
- C 언어에서 1차원 배열을 선언하면, 해당 배열을 구성하는 모든 원소가 메모리에 연속적으로 배치
- 각 원소마다 정해진 크기(데이터 타입의 크기)만큼 연속해서 배치
#include <stdio.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
printf("배열 arr의 시작 주소 : %p\n", (void*)arr);
for (int i = 0; i < 5; i++) {
printf("arr[%d]의 주소: %p, 값: %d\n", i, (void*)&arr[i], arr[i]);
}
return 0;
}
- C 언어 표준에 따르면, 배열의 원소들은 연속된 메모리 공간에 차례로 배치
- 단일 타입으로 선언된 1차원 배열의 원소들 사이에는 추가적인 패딩이 들어가지 않음
- 예를 들어
int arr[5]
인 경우, 5개의int
가 실제로5 * sizeof(int)
바이트 연속으로 메모리 공간에 저장
- 예를 들어
스택/데이터 영역에서의 1차원 배열
배열이 어디에 선언되느냐(전역 or 지역)에 따라 실제로 어느 메모리 영역(Stack, Data, 등)에 배치될지 달라지게 됨
- 전역 배열 (ex.
int arr[5]
가 함수 밖에서 선언되는 경우)- 프로그램 실행 시점에서 데이터 영역 혹은 BSS 영역의 메모리에 할당
- 지역 배열 (ex. 함수 내부에서
int arr[5]
선언)- 함수가 실행될 때 스택 프레임에서 메모리가 할당되고, 함수가 종료되면 해제됨
➡️ 어떤 영역에서 배열이 존재하든, 배열의 원소가 메모리 공간에 연속적으로 배치된다는 성질은 동일함
배열 이름과 포인터
int arr[5];
/*
- arr == &arr[0]
- arr+1 == &arr[1]
- ...
- arr[i] == * (arr + i) // i번째 원소에 접근
*/
- C 언어 내부에서 1차원 배열은 포인터처럼 처리 ➡️ 포인터를 배열과 같이 사용이 가능
- 1차원 배열
arr
을 선언(int arr[4]
)하면,arr
은 배열 전체를 대표하는 이름이지만, 표현식에서arr
은 배열의 첫 번째 원소의 주소(&arr[0]
)로 자동 변환이 이루어짐 - "배열 이름 = 포인터"라고 해석하는 것보다는, "배열 이름이 포인터처럼 취급될 수 있다"라고 이해하는 것이 좋음
- 1차원 배열
포인터 연산과 배열
arr[i]
는*(arr + i)
와 동일arr + i
는 배열의 시작 주소에서i * sizeof(type)
만큼 떨어진 메모리 주소를 의미
➡️ 배열을 순회할 때 인덱스 연산(arr[i]
)을 사용하든, 포인터 연산(*(arr + i)
)을 사용하든 결과는 동일
배열을 함수 인자로 전달
배열을 함수에 전달 ➡️ 포인터가 전달
#include <stdio.h>
void printArray(int *p, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", p[i]); // 또는 *(p + i)
}
printf("\n");
}
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 배열이 '포인터'로 변환되어 전달
return 0;
}
- 함수
printArrary()
의 매개변수int* p
는 호출 시arr
을 전달 받음 ➡️ 배열의 첫번째 원소의 주소(&arr[0]
)가 전달되기 때문에 결과적으로 배열의 데이터를 순회할 수 있음- (참고) 함수의 매개변수로
int arr[]
이라고 사용하더라도, 사실상int* arr
과 동일한 의미
- (참고) 함수의 매개변수로
2차원 배열과 포인터
2차원 배열의 형태
- 2차원 배열은 1차원 배열이 여러 개 모여 있는 형태 (흔히 격자 형태로 표현하는데, 실제로는 격자 형태가 아니라 1차원 배열이 여러 개 모여있는 형태이다.)
- 내부적으로 보면 행(row)마다 원소가 연속적으로 저장된 1차원 배열들이 연속적으로 놓여 있는 구조
2차원 배열의 메모리 구조
- 2차원 배열의 메모리 구조는 1차원 배열이 연속적으로 나열된 형태
- ex)
int arr2D[3][4]
가 존재한다면,- 총 3개의 1차원 배열(
arr2D[0]
,arr2D[1]
,arr2D[2]
)이 존재 - 각 1차원 배열은 원소가 4개인 1차원 배열
- 따라서, 총 12(3 x 4)개의
int
원소가 메모리에 연속적으로 배치
- 총 3개의 1차원 배열(
- ex)
코드로 확인하는 메모리 구조
#include <stdio.h>
int main(void) {
int arr2D[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int* p = (int*)arr2D;
printf("arr2D[0]의 메모리 주소 : 0x%X\n", &arr2D[0]);
printf("arr2D[1]의 메모리 주소 : 0x%X\n", &arr2D[1]);
printf("arr2D[2]의 메모리 주소 : 0x%X\n", &arr2D[2]);
printf("arr2D[1][1]의 값 : %d / 메모리주소 : 0x%X\n", arr2D[1][1], &arr2D[1][1]);
printf("arr2D[1][1]의 값 : %d / 메모리주소 : 0x%X\n", *(p + 4), p + 4);
return 0;
}
- 메모리 주소를 확인하보면, 2차원 배열의 각 행은 12 byte (int형 타입 (4 byte) 3개만큼의 크기) 만큼 주소가 차이나는 것을 확인할 수 있음
- 2차원 배열의 값은 메모리 공간 상에 연속된 형태로 배치되기 때문에, 포인터를 이용한다면 1차원 배열과 같이 사용할 수 있음
배열 이름과 포인터 (2차원)
- 2차원 배열의 이름은 "연속으로 있는 1차원 배열들의 집합 (= 2차원 배열)"을 대표
- 2차원 배열의 주소 값 :
arr
=&arr[0]
=&arr[0][0]
- 각 형태는 모두 가리키는 주소 값은 동일하지만, 타입은 모두 다름
- ex)
int arr[3][3]
이 존재할 때,arr
의 타입 =int[3][3]
/arr[0]
의 타입 =int[3]
/arr[0][0]
의 타입 =int
#include <stdio.h>
int main() {
int arr[3][3];
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 전체 배열
printf("sizeof(arr[0]) = %zu\n", sizeof(arr[0])); // 첫 번째 1차원 배열
printf("sizeof(arr[0][0]) = %zu\n", sizeof(arr[0][0])); // 배열의 한 원소
return 0;
}
- 각 형태에 따른 데이터 사이즈를 통해 모두 다른 타입이라는 것을 유추 가능
2차원 배열에서의 포인터 연산
2차원 배열에서 arr[i][j]
는 내부적으로 아래와 같이 해석 가능
*(*(arr + i) + j)
arr + i
:i
번째 행(배열)의 시작 주소로 이동*(arr + i)
:i
번째 행(1차원 배열)을 가리키는 포인터 (=arr[i]
)*(*(arr + i) + j)
=*(arr[i] + j)
: 행 내에서 (1차원 배열 내에서)j
번째 원소로 이동- 결과적으로,
*(*(arr + i) + j)
=arr[i][j]
#include <stdio.h>
int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printf("index로 2차원 배열 출력 : ");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
}
printf("\n");
printf("포인터로 2차원 배열 출력 : ");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", *(*(arr + i) + j));
}
}
return 0;
}
2차원 배열을 함수 인자로 전달
고정 크기를 가진 2차원 배열 매개변수
#include <stdio.h>
// 열의 크기를 [4]로 고정
void print2DArray_fixed(int arr[3][4]) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%3d ", arr[i][j]);
}
printf("\n");
}
}
int main(void) {
int arr2D[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print2DArray_fixed(arr2D);
return 0;
}
- 배열의 행(row)과 열(col)의 크기가 고정되어 있는 경우 사용 가능
- 함수의 배개변수로 배열을 직접 명시 (ex.
int arr[3][4]
) - 내부적으로는 2차원 배열의 전체 크기(ex. 3 x 4 = 12)가 고정되어 있고, 마지막 차원(ex.
[4]
)은 컴파일러가 반드시 알아야 함
void print2DArray_fixed(int arr[][4]) {
// ...
}
- 행(row) 부분은 생략하고 열(col) 부분만 명시하는 형태로도 사용 가능
- 함수 내부에서는 각 행이 3개의
int
원소를 가진다는 것을 전제로 작업 수행 (실제로는 명시하는 것과 동일하게 작동)
1차원 포인터 형태로 전달 (포인터 연산 기반)
#include <stdio.h>
// 2차원 배열을 'int *'로 받아서, 인덱스로 접근
void print2DArray_ptr(int *arr, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
/*
(i * cols + j)는 "i번째 행, j번째 열"의 1차원 인덱스
*/
printf("%3d ", *(arr + i * cols + j));
}
printf("\n");
}
}
int main(void) {
int arr2D[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// arr2D는 "int [4]"가 3개 연속된 구조이므로, 실제론 12개 int가 연속 메모리에 존재
// => 'int *'로 형 변환하여 전달 가능
print2DArray_ptr((int*)arr2D, 3, 4);
return 0;
}
- 2차원 배열을 1차원 포인터로 받는 기법 (내부적으로 2차원 배열이 연속된 메모리에 배치된다는 점을 활용)
- 함수 매개변수를 1차원 포인터(ex.
int* arr
)로 받고, 행(rows
)과 열(cols
)을 따로 받아서 1차원 인덱스(exi * cols + j
)로 계산하여 각 원소에 접근 - 이 방식인 실제로 메모리에 2차원 배열이 연속 배치되어 있다는 전제하에 동작하기 때문에 정적으로 선언된 2차원 배열(또는
malloc
으로 한 번에 할당된 메모리)이 아니라면 사용에 주의가 필요
요약 및 정리
1차원 배열과 포인터
- 1차원 배열의 이름 = "배열의 첫 번째 원소 주소"로 해석
arr[i]
는*(arr + i)
와 같은 의미- 함수로 배열을 전달할 때는 사실상 포인터가 전달됨
2차원 배열과 포인터
- 2차원 배열 = "1차원 배열이 연속으로 놓은 구조"
- 표현식으로
arr
=&arr[0]
=&arr[0][0]
(실제 타입은 다름!) arr[i][j]
=*(*(arr + i) + j)
- 함수 매개변수에 2차원 배열을 전달할 때는, 반드시 마지막 차원(열의 크기)을 명시해야 함