이중 포인터로 2차원 배열 다루기 – 개념부터 실전 사용까지
프로그래밍/C언어C로 코딩하다 보면 2차원 배열을 함수로 넘기거나 동적으로 만들고 싶은데,
int arr[3][4]; 만으로는 뭔가 막히는 느낌이 올 때가 있다.
이때 자주 등장하는 게 바로 이중 포인터(int **) 다.
이 글에서는
- 정적 2차원 배열과 이중 포인터의 차이
- 왜 int arr[][4]는 되는데 int **는 안 되는지
- int **로 2차원 배열처럼 동적 할당하는 방법
- free 할 때 주의점, 자주 하는 실수
까지 한 번에 정리해본다.

1. 2차원 배열과 포인터의 기본 관계
먼저 익숙한 2차원 배열부터 보자.
int arr[3][4];
- 타입: “4개의 int로 이루어진 배열이 3개”
- arr 의 타입은: int [3][4]
- 포인터로 decay(함수 인자로 전달 등)될 때 타입: int (*)[4]
→ “int 4개짜리 배열을 가리키는 포인터”
즉, arr는 **“배열의 배열”**이고,
arr가 포인터처럼 쓰일 때는 **“한 행을 가리키는 포인터”**가 된다.
int (*p)[4] = arr; // OK
p[0][1] = 10; // arr[0][1]과 동일
여기서 중요한 포인트:
정적 2차원 배열 int arr[3][4] 는 int **가 아니라
int (*)[4] 타입으로 취급된다.
그래서 함수 인자로 받을 때도 이렇게 해야 한다.
void print_matrix(int arr[][4], int rows);
// 또는
void print_matrix(int (*arr)[4], int rows);
void print_matrix(int **arr, int rows, int cols); 로 받으면
정적 2차원 배열을 그대로 넘겨서는 안 맞는다.
2. 이중 포인터 int ** 는 뭔가?
int ** 는 말 그대로
“int를 가리키는 포인터를 가리키는 포인터”
구조를 그림으로 그리면 이런 느낌이다.
- int **pp : “행 포인터들을 모아둔 배열(또는 영역)을 가리킨다”
- pp[i] : i번째 행을 가리키는 int *
- pp[i][j] : i번째 행, j번째 열의 int
즉, **“포인터들의 배열 + 각 행을 별도로 할당한 구조”**라고 보면 된다.
그래서 int ** 를 제대로 쓰려면
할당도 2단계로 해줘야 한다.
3. int ** 로 2차원 배열처럼 동적 할당하기
예를 들어 rows x cols 크기의 int “2차원 배열처럼” 쓰고 싶다고 하자.
3-1. 할당
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3;
int cols = 4;
// 1단계: 행 포인터 배열 할당
int **arr = malloc(rows * sizeof(int *));
if (arr == NULL) {
perror("malloc");
return 1;
}
// 2단계: 각 행마다 int 배열 할당
for (int i = 0; i < rows; i++) {
arr[i] = malloc(cols * sizeof(int));
if (arr[i] == NULL) {
perror("malloc");
// 이미 할당한 부분 정리 후 종료
for (int k = 0; k < i; k++) {
free(arr[k]);
}
free(arr);
return 1;
}
}
// 이제 arr[i][j] 로 접근 가능
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
arr[i][j] = i * cols + j;
}
}
// 출력 테스트
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%2d ", arr[i][j]);
}
printf("\n");
}
// 3단계: 해제
for (int i = 0; i < rows; i++) {
free(arr[i]); // 각 행 해제
}
free(arr); // 행 포인터 배열 해제
return 0;
}
이 구조의 특징:
- 메모리가 연속적이지 않을 수 있다.
각 행을 따로 malloc 했기 때문. - 대신 행 크기를 서로 다르게 할당하는 것도 가능하다. (jagged array)
예:
arr[0] = malloc(3 * sizeof(int)); // 3열
arr[1] = malloc(10 * sizeof(int)); // 10열
// 이런 식으로 ‘삐뚤빼뚤한’ 2차원 구조도 가능
4. 정적 2차원 배열을 함수에 넘길 때 vs int **
정적 2차원 배열을 함수에 넘기고 싶을 땐:
void print_matrix(int rows, int cols, int arr[][4]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main(void) {
int arr[3][4] = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
print_matrix(3, 4, arr);
}
또는 C99 이상이라면 가변 길이 배열(VLA) 를 써서
void print_matrix(int rows, int cols, int arr[rows][cols]);
이렇게도 가능하다.
반면, int ** 로 받은 함수는 int ** 로 동적 할당한 데이터만 안전하게 처리해야 한다.
void print_matrix_dyn(int rows, int cols, int **arr);
여기에 int arr[3][4]; 를 그대로 넘기면
타입이 맞지 않고, 심지어 우연히 돌아가도 UB(정의되지 않은 동작) 이 될 수 있다.
5. 이중 포인터로 2차원 배열을 다룰 때 자주 하는 실수
❌ 1) 한 번만 malloc 하고 int ** 로 캐스팅
int **arr = malloc(rows * cols * sizeof(int)); // 잘못된 패턴
이렇게 한 덩어리로 할당해 놓고 arr[i][j] 로 쓰는 건
정상적인 int ** 구조가 아니다.
이 경우 arr 는 사실상 int *로 취급되어야 한다.
int *arr = malloc(rows * cols * sizeof(int));
arr[i * cols + j] = 10;
처럼 1차원 배열 + 인덱스 계산으로 써야 한다.
❌ 2) free 순서를 잘못 처리
int ** 구조에서는
- 먼저 각 행(arr[i])을 free
- 마지막에 arr 자체를 free
순서가 중요하다.
for (int i = 0; i < rows; i++) {
free(arr[i]); // 행 해제
}
free(arr); // 행 포인터 배열 해제
반대로 먼저 free(arr); 를 해버리면
그 뒤의 arr[i] 접근은 이미 해제된 메모리에 접근하는 셈이라 위험하다.
6. 언제 int ** 를 쓰는 게 좋을까?
- 행마다 크기가 다른 2차원 구조가 필요할 때
- 런타임에 행 수, 열 수가 결정되는 2차원-like 데이터를 다룰 때
- 재사용 가능한 라이브러리 코드에서
“2차원처럼 보이는 포인터 배열”을 받도록 설계할 때
반면,
- 크기가 고정이고
- 메모리가 한 덩어리로 연속되어 있는 게 좋다면
int arr[ROWS][COLS]; 또는
int *arr = malloc(rows * cols * sizeof(int));
처럼 1차원 + 수동 인덱싱 방식이 더 단순할 수 있다.
마무리 정리
- 정적 2차원 배열 int arr[3][4] 는
int (*)[4] 로 decay되지, int ** 가 아니다. - int ** 는 “포인터들의 배열” 구조이므로
행 포인터 배열 + 각 행 메모리를 따로 할당해야 한다. - int **로 할당한 건 int ** 로만 다루고,
정적 배열은 int (*)[COL] 또는 int arr[][COL] 로 받자. - free 할 때는 행 먼저, 마지막에 상위 포인터를 해제하는 순서를 지키기.
'프로그래밍 > C언어' 카테고리의 다른 글
| 📝 C 언어 함수 포인터와 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴 (0) | 2025.11.21 |
|---|---|
| C 언어 포인터 완벽 정리 – 이 글 하나로 끝내기 (0) | 2025.11.19 |
| C언어 포인터, 동적할당, 메모리 관리 (0) | 2024.10.15 |
| C언어 구조체란? (3) | 2024.10.14 |
| 자료구조 연결리스트 개념, 장점, 단점, 배열과의 차이 (1) | 2023.06.04 |