Skip to Main Content

C언어 기초 가이드 STEP 2: 포인터&배열

코딩 학습 가이드는 소프트웨어학과 송오영교수님이 검수하였습니다.

포인터와 배열의 관계

배열은 자료형이 같은 변수를 메모리에 연속으로 할당한 것입니다. 따라서 각 배열 요소는 일정한 간격으로 주소를 갖습니다.
예를 들어 int arr[4];의 배열이 메모리 100번지부터 할당되어 있다면 메모리의 구조는 이렇게 구성될 것입니다.
int형 변수는 4바이트이기 때문에 각 배열 요소의 주소는 100, 104, 108, 112번지가 됩니다.
결국 첫 번째 요소의 주소를 알면 나머지 요소의 주소도 쉽게 알 수 있습니다. 따라서 컴파일러는 첫 번째 요소의 주소를 쉽게 사용하도록 배열명첫 번째 배열 요소의 주소로 변경합니다.
즉, 배열의 이름은 배열의 시작 주소 값입니다.


 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main(void)
{
    int arr[3= {123};
 
    printf("배열의 이름 : %p\n", arr);
 
    printf("첫 번째 요소 : %p\n"&arr[0]);
    printf("두 번째 요소 : %p\n"&arr[1]);
    printf("세 번째 요소 : %p\n"&arr[2]);
 
    return 0;
}
 
 

실행결과

배열의 이름 : 005AFA14
첫 번째 요소 : 005AFA14
두 번째 요소 : 005AFA18
세 번째 요소 : 005AFA1C
 

배열의 첫 번째 주소값이 005AFA14인데, 배열의 이름을 출력한 결과도 005AFA14로 결과가 같습니다.
주소값을 저장할 수 있는 변수는 포인터 변수뿐입니다. 여기서 일단 배열의 이름은 포인터 변수임을 알 수 있습니다.
하지만 배열의 이름은 값의 저장이 불가능하고(대입연산자의 피연산자가 될 수 없습니다.), 주소 값의 변경이 불가능합니다.
그래서 배열의 이름을 '상수 형태의 포인터', '포인터 상수'라고 부르기도 합니다.

포인터 연산

포인터 연산은 +, -, ++, -- 연산자를 사용하여 포인터 값을 증가/감소시키는 연산입니다. (포인터로 곱셈이나 나눗셈은 안됩니다. 주소값을 곱하거나 나누면 안되겠죠~)
예를 들어 크기가 4바이트인 int형 변수의 주소 100번지에 1을 더한 결과는 101이 아닌 104가 됩니다. 그리고 연산 결과 또한 주소입니다.

주소 ± 정수


주소 ± (정수 * 주소를 구한 변수의 크기) 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
int main(void)
{
    char * ptr1 = 0;    // 0 대신 NULL을 사용해도 됩니다.
    int * ptr2 = 0;    // 0 대신 NULL을 사용해도 됩니다.
    double * ptr3 = 0;    // 0 대신 NULL을 사용해도 됩니다.
 
    printf("%d번지, %d번지, %d번지\n", ptr1, ptr2, ptr3);
 
    ptr1++;
    ptr2++;
    ptr3++;
 
    printf("%d번지, %d번지, %d번지\n", ptr1, ptr2, ptr3);
 
    return 0;
}
 
 

실행결과

0번지, 0번지, 0번지
1번지, 4번지, 8번지

각 포인터들은 타입에 맞게 1, 4, 8바이트씩 증가했습니다.


이러한 포인터의 연산 특성으로 인해서 배열 접근이 가능합니다.
7행에 선언된 포인터 변수 ptr은 int형 포인터이므로 값을 1 증가시킬 때마다 실제로는 4가 증가합니다.

아래 예제에서 *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4)의 출력결과는 arr[0], arr[1], arr[2], arr[3], arr[4]의 출력결과와 모두 동일한 출력결과를 보입니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main(void)
{
    int arr[5= { 12345 };
 
    int * ptr = arr;
 
    printf("%d %d %d %d %d\n"*ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4));
    printf("%d %d %d %d %d\n", arr[0], arr[1], arr[2], arr[3], arr[4]);
 
    return 0;
}
 

실행결과

1 2 3 4 5
1 2 3 4 5

배열의 이름과 포인터 변수는 상수, 변수의 차이만 있을 뿐, 사실은 동일합니다.
따라서 ptr에 저장된 값이 arr의 주소값이기 때문에 결과를 확인하면 4문장 모두 같은 값을 나타내는 것을 확인할 수 있습니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
int main(void)
{
    int arr[3= {123};
 
    int * ptr = arr;
 
    printf("%d %d %d\n"*ptr, *(ptr+1), *(ptr+2));
    printf("%d %d %d\n", ptr[0], ptr[1], ptr[2]);
    printf("%d %d %d\n"*arr, *(arr + 1), *(arr + 2));
    printf("%d %d %d\n", arr[0], arr[1], arr[2]);
 
    return 0;
}
 

 

 

실행결과

1 2 3
1 2 3
1 2 3
1 2 3
 
*(ptr+0)
ptr[0]
*(arr+0)
arr[0]
*(ptr+1)
ptr[1]
*(arr+1)
arr[1]
*(ptr+2)
ptr[2]
*(arr+2)
arr[2]




★ 이를 통해 arr[i] == *(arr+i) 라는 결론을 내릴 수 있습니다. 

포인터 배열

포인터 배열이란 배열 요소로 포인터 변수를 가지는 배열을 의미합니다. 즉, 포인터 변수로 이루어진 '배열'을 의미합니다.
포인터 배열은 기본 자료형 배열과 크게 다르지 않습니다. 다만 주소값을 저장할 수 있는 포인터 변수를 대상으로 선언된 배열일 뿐입니다.
포인터 배열을 선언할 때는 배열 요소의 자료형이 포인터형이므로 배열이름 앞에 *을 붙이면 됩니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main(void)
{
    int num1 = 1, num2 = 2, num3 = 3;
    int * arr[3= { &num1, &num2, &num3 };    // 포인터 배열 선언 및 초기화
 
    printf("주소값 : %p %p %p\n", arr[0], arr[1], arr[2]);
    printf("값 : %d %d %d\n"*arr[0], *arr[1], *arr[2]);
 
    return 0;
}

 

실행결과

주소값 : 00EFFB24 00EFFB18 00EFFB0C
값 : 1 2 3

 포인터 배열은 메모리에 연속해서 위치하지만
      포인터 배열에 저장된 주소값은 연속성을 지니지 않습니다.

포인터 배열과 변수의 관계를 그림으로 표현하면 다음과 같습니다.

 


포인터 배열은 다양한 응용이 가능합니다. 포인터 배열은 문자열을 저장할 수 있습니다.
문자열 배열은 문자열의 주소값을 저장하는 배열로서 사실 char형 포인터 배열입니다.
" " (큰 따옴표)로 묶인 문자열은 메모리 공간에 저장된 후 그 주소값이 반환됩니다. 그리고 저장된 메모리 공간의 시작 주소를 알려줍니다.
반환된 문자열의 주소값은 문자열 첫번째 문자의 주소입니다. 이렇게 char형 포인터 배열에 저장이 가능합니다.

 
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main(void)
{
    char * strarr[3= {"sejong""university""computer"};    // 문자열 배열 선언 및 초기화
 
    printf("%s %s %s\n", strarr[0], strarr[1], strarr[2]);
 
    return 0;
}


 

실행결과

sejong university computer

포인터 배열의 각 요소는 문자열 상수가 저장된 메모리에서 첫번째 문자열의 주소값을 저장하고 그 문자를 가리킵니다.
따라서 이 값을 변환문자 %s로 출력하면 null 문자가 나올 때까지 모든 문자를 차례로 출력하므로 하나의 문자열이 출력되는 것입니다.

배열 포인터

배열 포인터배열을 가리킬 수 있는 '포인터'를 의미합니다.
1차원 배열은 이름 자체가 포인터이기 때문에 배열 포인터가 필요없습니다. 하지만 2차원 배열은 이름 자체로 포인터의 역할을 할 수 없습니다.
배열 포인터를 사용하는 이유는 2차원 이상의 배열을 가리킬 때 포인터를 배열처럼 사용하기 위함입니다.
따라서 배열 포인터는 2차원 이상의 배열에서만 의미를 가집니다.


arr2d[0]과 arr2d[1]은 각 부분 배열의 시작 주소를 가리킵니다. arr2d[0]은 2차원 배열의 첫 번째 행을 의미하고, arr2d[1]은 두 번째 행을 의미합니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main(void)
{
    int arr2d[2][3= {
    {102030},
    {405060},
    };
 
    printf("%d %d\n"*arr2d[0], *arr2d[1]);
 
    return 0;
}

실행결과

10 40

배열 포인터는 다음과 같이 정의합니다.

자료형 (*포인터이름)[가로(행)길이]


 * 포인터가 자료형쪽에 붙는게 아니라
      포인터변수이름 쪽에 소괄호 안에 붙게 됩니다.

- int : 가리킬 수 있는 대상에 대한 정보(즉, int형 변수를 가리키는 포인터)를 의미합니다.

- (*ptr) : ptr은 포인터임을 의미합니다.

- [3] : 포인터 연산에 따른 증가/감소의 폭입니다.
         (값을 1 증가시 int형이므로 4바이트*3 = 12바이트 이동)


다음 예제는 배열 포인터를 사용하여 배열과 같은 인덱싱 방법으로 배열 요소를 참조하는 예제입니다.
포인터에 배열명을 저장하면 배열 포인터를 2차원 배열처럼 쓸 수 있습니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
 
int main(void)
{
    int arr2d[2][3] = {
    {102030},
    {405060},
    };
 
    int i, j;
 
    int (*ptr)[3] = arr2d;    // 배열 포인터 선언
 
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 3; j++)
            printf("%d ", ptr[i][j]);    // 배열 포인터로 참조
        printf("\n");
    }
 
    return 0;
}

실행결과

10 20 30
40 50 60

앞서 1차원 배열에서 arr[i] == *(arr+i) 라는 문장이 성립한다고 했습니다. 마찬가지로 2차원배열에서도 arr[i] == *(arr+i) 입니다.
아래 수식들은 모두 같은 표현입니다.

arr[i][j] *(arr+i)[j] *(arr[i]+j) *(*(arr+i)+j)

결과를 확인하면 4문장 모두 같은 값을 나타내는 것을 확인할 수 있습니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
int main(void)
{
    int arr2d[2][3= {
        {102030},
        {405060},
    };
    int i, j;
 
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("%d ", arr2d[i][j]);
            printf("%d ", (*(arr2d + i))[j]);
            printf("%d "*(arr2d[i] + j));
            printf("%d\n"*(*(arr2d + i) + j));
        }
    }
 
    return 0;
}

실행결과

10 10 10 10
20 20 20 20
30 30 30 30
40 40 40 40
50 50 50 50
60 60 60 60

포인터 배열 / 배열 포인터 차이

포인터 배열(Array of Pointer)과 배열 포인터(Pointer to Array)는 비슷해 보입니다.
- 포인터 '배열'은 주소값들을 저장하는 '배열'입니다.
- 배열 '포인터'는 배열의 시작주소값을 저장할 수 있는 '포인터'입니다.

 

int *ptr[3]; int (*ptr)[3];

첫 번째는 int형 변수로 이루어진 int형 포인터 배열입니다.
두 번째는 가로길이가 3인 int형 2차원 배열을 가리키는 포인터 변수입니다.
( ) (괄호)의 유무가 중요하며, 포인터 배열과 배열 포인터 사이의 차이점을 정확히 이해하고 넘어가시기 바랍니다.