Dev/C

[C언어] 배열과 포인터의 관계 (1차원 배열 & 2차원 배열)

청월누리 2025. 4. 8. 01:44

C언어에서 포인터와 배열은 밀접한 관계를 가지고 있습니다. 이 글은 1차원 배열과 2차원 배열을 중심으로, 포인터와 배열의 관게에 대해 정리한 글입니다.


1차원 배열과 포인터

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])로 자동 변환이 이루어짐
    • "배열 이름 = 포인터"라고 해석하는 것보다는, "배열 이름이 포인터처럼 취급될 수 있다"라고 이해하는 것이 좋음

포인터 연산과 배열

  • 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차원 배열의 형태

  • 2차원 배열은 1차원 배열이 여러 개 모여 있는 형태 (흔히 격자 형태로 표현하는데, 실제로는 격자 형태가 아니라 1차원 배열이 여러 개 모여있는 형태이다.)
  • 내부적으로 보면 행(row)마다 원소가 연속적으로 저장된 1차원 배열들이 연속적으로 놓여 있는 구조

2차원 배열의 메모리 구조

2차원 배열의 메모리 구조

  • 2차원 배열의 메모리 구조는 1차원 배열이 연속적으로 나열된 형태
    • ex) int arr2D[3][4]가 존재한다면,
      • 총 3개의 1차원 배열(arr2D[0], arr2D[1], arr2D[2])이 존재
      • 각 1차원 배열은 원소가 4개인 1차원 배열
      • 따라서, 총 12(3 x 4)개의 int 원소가 메모리에 연속적으로 배치

코드로 확인하는 메모리 구조

#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차원 인덱스(ex i * 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차원 배열을 전달할 때는, 반드시 마지막 차원(열의 크기)을 명시해야 함