[자료구조 C 언어] C 프로그래밍 기초 - 6 : 2차원 배열, 포인터 배열, 배열 포인터

Programming/C · 2020. 2. 24. 16:17

저번 게시물을 천천히 읽어가면서 이해하신 분들이면 포인터랑 배열이 무슨 공통점과 차이점이 있는지 아셨을 겁니다.

 

그 개념을 잘 기억하면서 조금 더 응용을 해봅시다.

 

포인터 배열과 배열 포인터가 무엇인지 간단히 확인하고 바로 사용하고 싶으신 분들은 제일 하단을 봐주시면 되겠습니다!

 

제 게시물의 목표는 코딩을 배우며 드는 의문점들을 의식의 흐름대로 확인하고 같이 알아가는 게 목적이니까 시간이 괜찮으신 분들이나 기초 지식이 없는 분들은 천천히 잘 읽어보시면 많은 도움이 되실 겁니다. 

 

1. 2차원 배열

 

1)

 

일단 2차원 배열은 쉽게 행렬을 생각하면 됩니다.

 

    int M1[3][4] = {{11, 12, 13, 14},{21, 22, 23, 24},{31, 32, 33, 34}};

    int M2[3][4] = {11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34};

 

이런 식으로 선언을 해주면 3X4 행렬이 생성됩니다.

 

M1과 M2의 차이점은 { }를 써주고 안 써주고 차이지만 저장되는 방식은 동일합니다.

 

제일 앞에 있는 값부터 4칸이 채워지면 다음 행으로 다시 4칸 또 다음 행으로 다시 4칸이 채워지는 형식입니다.

 

11 12 13 14

21 22 23 24

31 32 33 34

 

이렇게 저장이 됩니다.

 

그럼 굳이 { } 이걸 왜 쓰냐!! 

 

    int M1[3][4] = {{11, 12, 14},{21, 22, 23, 24},{31, 32, 33, 34}};

    int M2[3][4] = {11, 12, 14, 21, 22, 23, 24, 31, 32, 33, 34};

 

이렇게 13이란 숫자를 빼고 다시 선언을 해보겠습니다.

 

이때는 먼저 M1은 다음과 같습니다.

 

11 12 14

21 22 23 24

31 32 33 34

 

M2는 다음과 같습니다.

 

11 12 14 21

22 23 24 31

32 33 34

 

차이점이 느껴지시죠?

 

3개의 행과 4개의 열은 동일하지만 괄호로 구분하지 않은 2차원 배열은 순서대로 빈칸 없이 채워집니다.

 

2)

 

이번엔 2차원 문자 배열입니다.

 

    char C[5][20] = {"1234512345123451234", "horse", "dog", "tiger", "elephant"};

 

이런 식으로 선언이 되는데 5줄이고 한 줄 당 20칸의 문자를 저장할 수 있습니다.

 

하지만!! 20칸이 아니었습니다.

 

마지막에는 \n이 저장되기 때문에 19개의 문자를 저장할 수 있습니다. (위에 12345는 그걸 확인하려고 넣어봤습니다.)

 

    for(int i = 0; i < 5; i++){



        printf("%s\n", C[i]);



    }

 

이렇게 해주면 다음과 같은 출력물이 나옵니다.

 

    char C[5][20] = {"12345123451234512345", "horse", "dog", "tiger", "elephant"};

 

이렇게 20칸을 다 채우는 문자를 넣으면 다음과 같은 출력물이 나옵니다.

\n 자리를 차지해서 줄 바꿈이 안 됐습니다...

 

 

 

 

2. 포인터 배열

 

결론부터 말하자면 포인터를 배열의 요소로 가지는 배열입니다.

 

일반적인 배열이 다음과 같이 선언되어 있다고 합시다.

 

int arr[3];

 

이때는 arr[0], arr[1], arr[2]에 각각 배열의 자료형에 맞는 데이터를 저장할 수 있습니다.

 

int *parr[3];

 

이렇게 배열의 이름 앞에 포인터를 선언할 때 붙였던 *을 써주면 포인터 배열을 선언할 수 있게 됩니다.

 

이때는 parr[0], parr[1], parr[2]에 각각 주소값을 저장할 수 있습니다.

 

이전에 포인터를 선언할 때 보았던

 

int *p에서 p 자리에 &a와 같은 주소값을 저장하는 것과 같습니다.

 

p에 저장되어 있던 변수 a의 주소값을 p 앞에 * 붙여줌으로써 주소값 내부에 있는 a의 값을 참조했던 것과 동일하게

 

parr[n]에 저장되어 있는 변수의 주소값을 parr[n] 앞에 *을 붙여줌으로써 주소값 내부에 있는 데이터를 참조할 수 있습니다.

 

예시를 보시죠!

 

    int *p;

    int *parr[3];



    int a = 10;

    int b = 20;

    int c = 30;

    

    p = &a;

    

    parr[0] = &a;

    

    printf("*p = %d, *parr[0] = %d\n", *p, *parr[0]);

 

이때 출력 결과는 다음과 같습니다.

 

당연히 같은 값이 나오겠죠.

 

이렇듯 포인터 배열은 여러 개의 포인터를 동시에 선언할 때 사용하기 좋은 것 같습니다.

 

아래는 포인터 배열 내에 바로 변수의 주소값을 저장한 것입니다.

 

sizeof 함수를 통해 배열의 크기도 구해줬네요.

 

sizeof(arr)은 배열 내부에 int 형 포인터가 3개 들어있으니까 4*3인 12가 반환되고,

 

sizeof(arr[0])은 int 형 포인터 1개와 동일하니까 4*1인 4가 반환될 것입니다.

 

    int i, arr_len;

    

    int num1 = 10, num2 = 20, num3 = 30;

    

    int *arr[3] = {&num1, &num2, &num3};

    

    arr_len = sizeof(arr)/sizeof(arr[0]);

    

    

    printf("arr[0]      = %d\n", arr[0]);  // 여기에 &num1 주소값과 동일한 값이 저장됩니다.

    printf("&num1       = %d\n", &num1);

    

    for(i = 0; i < arr_len; i++){

        printf("*(*(arr+%d)) = %d\n", i, *(*(arr+i)));

        printf("*arr[%d]     = %d\n", i, *arr[i]);

    }

 

 

여기서 중요한 점은 *(*(arr+i))과 *arr[i] 가 동일한 결과를 출력한다는 것입니다.

 

이전에 내용을 충분히 이해하셨다면 어떤 원리인지 알 수 있습니다.

 

일단! 포인터 배열도 배열이라는 것을 명심해 주세요.

 

*(arr+n)이 arr[n]과 동일한 결과를 출력한다는 것은 저번 게시물에서 확실히 알려드렸습니다.

 

포인터 배열에서 배열의 원소는 주소값을 저장하고 있죠?

 

*은 주소값을 저장하고 있는 변수 앞에 붙였을 때 주소값 내부에 있는 메모리에 저장된 데이터를 불러올 수 있습니다.

 

따라서 *(arr+n) (= arr[n])에 저장되어 있는 주소값을 다시 바깥에 *( )를 씌워줌으로써 참조할 수 있는 것입니다.

 

여기까지 잘 이해하셔야 다음에 같이 확인할 배열 포인터를 이해하실 수 있습니다.

 

다시 한번!!

 

배열과 포인터로 선언된 변수 (a)에 대하여 다음은 동일한 결과를 보입니다.

 

*(a+i) = a[i]

 

3. 배열 포인터

 

배열 포인터는 배열을 가리키는 포인터입니다.

 

그냥 1차원 배열에는 잘 사용하지 않고 2차원 배열을 참조할 때 많이 사용합니다.

 

1번에서 봤듯이 2차원 배열은 다음과 같이 선언할 수 있습니다.

 

    int arr[3][4] = {

        {11, 12, 13, 14},

        {21, 22, 23, 24},

        {31, 32, 33, 34 }

    };

 

배열 포인터는 다음과 같이 선언합니다.

 

    int *parr[4];

 

그리고 앞선 배열을 다음과 같이 두 가지 방법으로 참조 가능합니다.

 

    int (*parr)[4];



    parr = arr;

OR    

 

    int (*parr)[4] = arr;

 

여기서 주의할 점은 *parr에 괄호를 꼭 쳐줘야 합니다.

 

그렇지 않으면 위에서 알아본 포인터 배열이 돼버립니다.

 

우선순위가 *보다 괄호가 앞서기 때문에 그렇습니다.

 

그리고 또 하나 주의할 점은 2차원 배열이 다음과 같을 때

 

int arr[행][열];



int *parr[열];

 

열 부분의 숫자가 동일한 배열만 가리킬 수 있습니다.

 

다음과 같이 배열 포인터가 가리키는 배열의 값을 출력할 수 있습니다.

 

 

  int row, col;

    

    int (*parr)[4] = arr;

    

    row = sizeof(arr) / sizeof(arr[0]);         // (3*4*4) / (4*4) = 3

    col = sizeof(arr[0]) / sizeof(arr[0][0]);   // (4*4) / (1*4) = 4

    

    for(int i = 0; i < row; i++){

        for(int j = 0; j < col; j++){

            printf("parr[%d][%d] = %d\n", i, j, parr[i][j]);

        }

    }

행의 크기와 열의 크기를 구하는 부분은 팁으로 봐주세요!!

 

곱하기 연산의 마지막 4는 모두 int형의 크기를 나타냅니다.

 

이제 위에서 봤던 parr[][]의 주소값을 확인해 봅시다.

 

뭔가 느껴지시나요??

 

전부 int 자료형의 크기인 4byte 씩 차이가 납니다.

 

사실 따지고 보면 4칸씩 일렬로 쭉 연결되어 있는 것과 동일하죠.

 

저희가 느끼기엔 2차원 배열을 가리키고 있는 배열 포인터 parr이 행렬처럼 행이 나눠져 있고 행마다 4개의 열이 연결되어 있지만

 

사실 내부에는 모든 원소가 쭉 연결되어 있고 편의상 2차 배열로 표기하고 사용하는 것을 확인할 수 있습니다.

(2차원 배열인 arr도 확인해 보면 동일하게 4씩 주소값이 차이가 납니다. 행이 나뉜다고 숫자가 더 크게 바뀌거나 하지 않아요.)

 

 

 

그렇다고 이렇게 parr[n] (n = 0, 1, 2 , ..., 11)로 사용할 수는 없습니다.

 

위에서 보시는 것과 같이 parr[n]의 주소는 행의 시작 주소와 같습니다.

 

그래서 4개의 열이 있으니까 4 (열의 개수) * 4 (int 자료형의 크기) = 16만큼 주소의 값이 변하게 됩니다.

 

당연히 parr[n]과 parr[n][0]의 주소는 같은 값을 가지고 있습니다.

 

이렇게까지 봐야 하나 싶으신 분들은 마지막 정리만 보셔도 사용하실 때는 불편한 게 없으실 겁니다.

 

저는 이렇게 확인을 안 하면 계속 찝찝해서 하나씩 뜯어봐야 합니다...ㅜㅜ

 

그래서 오래 걸려요... 눈으로 다시 확인하기 전까진 개념을 알아도 불안해서...

 

이전에 포인터나 배열은 parr[n]와 *(parr+n)가 동일한 역할을 할 수 있다고 했습니다.

 

그렇다면 parr+i는 어떤 값을 가지고 있는지 확인해 봅시다. 

 

위에서 parr[n]의 주소값을 확인했을 때랑 동일한 결과를 보이는 것을 알 수 있습니다.

 

여기까지 저희가 공부했던 내용과 동일한 결과를 보여줍니다.

 

이럴 때 안심이 되죠.. 제가 이해한 개념이 틀리지 않구나 하고 ㅎㅎ

 

 

총정리!

마지막으로 정리 한번 하겠습니다.

 

포인터 배열과 배열 포인터는 뒷 단어에 집중하면 됩니다.

 

포인터 '배열'은 다음과 같이 선언합니다.

 

int *parr[3]; // 원소로 parr[0], parr[1], parr[2]에 각각 주소값을 저장할 수 있습니다.

                    // *parr[n]로 저장된 주소값의 메모리에 있는 데이터를 가져올 수 있습니다.

 

배열 '포인터'는 다음과 같이 선언합니다.

 

int (*parr)[3]; // 열의 갯수가 동일한 배열을 가리킬 수 있습니다.

                      // parr[row][col]로 가리킨 배열의 값을 가져올 수 있습니다.

 

다음 게시물에서 더 자세히 확인해 보겠지만 배열 포인터는 함수를 선언할 때 배열을 참조하기 위해 사용됩니다.

 

void ArrayReference(int (*parr)[3]){

    for(int i = 0; i < 2; i++){

        printf("arr[%d][0] = %d\n", i, parr[i][0]);

    }

}

 

이렇게 선언된 함수에 아래와 같이 열의 개수가 같은 배열을 참조할 수 있습니다.

 

    int arr[2][3] = {

        {11, 12, 13},

        {21, 22, 23}

    };



    ArrayReference(arr);

 

출력 결과는 다음과 같습니다.

 

다음 게시물에서는 이중 포인터와 이차원 배열의 관계가 무엇인지

 

혹시 분량이 된다면 함수 포인터는 무엇인지 확인해 보겠습니다.

반응형