본문 바로가기

언어/C

포인터의 이해(3) - 포인터와 1차원, 2차원 배열 / 더블포인터 / 배열의 포인터 / 포인터 배열

1차원 배열과 포인터

 

배열 arr를 출력하면 arr[0]의 주소값이 출력된다고 했다.

또한, [] 연산자는 * (arr + i) 로 바뀌어서 []안에 있는 숫자에 해당하는 위치의 값을 출력한다고 배웠다.

 

다른 int* 포인터가 이 배열을 가리킬 수 있는지 확인해보자.

 

#include <stdio.h>

int main() {
    int arr[] = { 1,2,3,4,5 };
    int* p ;

    p = &arr[2];

    printf("arr : %d \n", arr[1]);
    printf("arr2 : %d  ", p[1]);
    return 0;
}

 

 

int 형을 가리키는 포인트 변수 p를 정의하고, p = &arr[2]; 를 통하여 p에 arr[2]의 주소값을 대입하였다.

그리고 p[1]를 실행하면 *(p + 1) 로 바뀌어서 arr[3]의 값인 4가 출력되는 것이다.

 

다른 예를 들어보자.

 

#include <stdio.h>

int main() {
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p;
    int sum = 0;

    p = arr;

    while (p - arr <= 9) {
        sum += *p;
        p++;
    }

    printf("sum : %d", sum);
    return 0;
}

 

 

먼저, arr를 정의하고 int형 배열을 가리키는 포인터 함수 p를 정의한다. 그리고 이 p 함수는 arr의 주소값을 갖도록 정의한다.

다음으로 while의 조건인 p - arr <= 9 로 설정하고 sum 변수에 p가 가리키는 주소의 값을 가져와 하나씩 더해준다. 

마지막으로 p++를 통하여 p의 주소가 4 바이트씩 증가하고 arr[0]을 가리켰던 p 함수는 arr[1]을 가리키게 된다. 이렇게 반복문을 진행하고 1부터 10까지 더해진 결과가 도출되는 것이다.

 

그렇다면 왜 굳이 p를 따로 선언하여 p++를 해주었을까? arr ++와 *arr를 사용하여 반복문을 돌릴 수도 있지 않을까?

 

arr의 값을 변경하려 하면 컴파일 오류가 발생한다. 배열을 정의하고 그 배열의 이름을 출력하면 첫 번째 원소를 가리키는 포인터로 타입이 변경되는데 이는 단순히 배열의 첫 번째 원소를 가리키는 주소값 자체일 뿐입니다. 그래서 arr++의 문장은 컴파일러 입장에서 (0x7fff1234) ++ ; 를 수행하는 것과 같다. 이것은 애초에 말이 안되는 문장이다.

 

 

 

포인터의 포인터

 

int **p; 를 통하여 포인터의 포인터를 정의할 수 있는데 이것은 int를 가리키는 포인터를 가리키는 포인터이다. 먼저 예를 통하여 이해해보자.

 

#include <stdio.h>

int main() {
    int a;
    int *p;
    int **pp;

    p = &a;
    pp = &p;

    a = 123;

    printf("a : %d // *p : %d // **pp : %d \n", a, *p, **pp); //1
    printf("&a : %p // p : %p // *pp : %p \n", &a, p, *pp); //2
    printf("&p : %p // pp : %p \n", &p, pp); //3

    return 0;
}

 

 

int a 를 정의하고 int 형을 가리키는 포인터 p를 정의한다. 그리고 int 형을 가리키는 포인터를 가리키는 포인터 pp를 정의한다. 그러고 a에 123을 대입을 해주었다.

 

1번의 출력 문장을 보면 a와 *p가 123이 되는 것은 이해될 것이다. **pp는 *(*pp)가 되는데 *pp는 a의 주소값이 들어있으므로 *(*pp)는 123이 출력되는 것이다.

 

 

그림을 통해서 다시 이해해보자. pp에는 p의 주소값이 p는 a의 주소값이 a는 123을 가지고 있다.

2번의 출력 문장을 이해해보면 &a는 a의 주소값이 출력되고, p도 a의 주소값이 출력되고, *pp 또한 a의 주소값이 출력되는 것이다.

 

마지막으로 3번의 출력 문장을 이해해보면 &p는 p의 주소값, pp도 p의 주소값이 출력된다.

 

 

 

배열 이름의 주소값

배열을 사용할 때 포인터로 암묵적 변환이 이루어진다는 것을 알았다. 주소값 연산자를 사용하면 어떻게 되는지 예를 통하여 알아보자.

 

#include <stdio.h>

int main() {
    int arr[3] = { 1, 2, 3 };
    int(*pa)[3] = &arr;

    printf("arr[1] : %d \n", arr[1]);
    printf("pa[1] : %d \n", (*pa)[1]);

    return 0;
}

 

 

[]를 사용하지 않고 int 형 배열의 변수인 arr만 출력했을 경우 int * 로 암묵적 변환이 된다는 것을 알았는데, &arr를 사용하여 명시적으로 주소값 연산자를 사용하는 경우 암묵적 변환은 이루어지지 않는다.

 

arr의 크기가 3인 배열이기 때문에, arr의 주소값인 &arr를 보관한 포인터는 크기가 3인 배열을 가리키는 포인터이다. 그래서 int (*pa)[3] 을 사용하여 정의한 것이다. ( )를 사용하지 않고 int *pa[3]으로 정의하면 int * 원소 3개를 가지는 배열을 정의한 것으로 생각한다.

pa는 크기가 3인 배열을 가리키는 포인터로 정의되어서 직접적으로 배열을 나타내기 위해서는 (*pa)[1] 처럼 * 연산자를 사용해서 나타내야 한다.

 

 

 

2차원 배열의 [] 연산자

 

2차원 배열은 1차원 배열이 여러 개 있는 것과 같다. 2차원 배열의 메모리 구조를 보았을 때 선형으로 퍼져서 있음을 확인할 수 있다.

 

출처 : 씹어먹는 c언어

 

2차원 배열에서 arr[0]은 어떤 것을 의미하는지 예를 통하여 확인해보자.

 

#include <stdio.h>

int main() {
    int arr[2][2];

    printf("arr[0] : %p \n", arr[0]);
    printf("&arr[0][0] : %p \n", &arr[0][0]);

    printf("arr[1] : %p \n", arr[1]);
    printf("&arr[1][0] : %p \n", &arr[1][0]);

    return 0;
}

 

 

arr[0]과 &arr[0][0]을 비교 했을 때 같은 값이 나온 것을 확인할 수 있다. 즉, arr[0]은 arr[0][0]를 가리키는 포인터로 암묵적 변환이 이루어진 것이고, arr[1]의 경우 arr[1][0]를 가리키는 포인터로 타입 변환이 된 것이다.

 

그렇다면 arr[0]의 주소값은 int * 가 보관할 수 있으니 arr의 주소값은 int ** 가 보관할 수 있을까? 라는 생각이 들 수 있는데 정답은 아니오 이다. 

 

 

 

포인터의 형(type)을 결정짓는 두 가지 요소

먼저, 위에서 말했던 int ** 형이 될 수 없는 이유에 대해서 예를 들어서 확인해보자.

 

#include <stdio.h>

int main() {
    
    int arr[2][2] = { {1,2}, {3,4} };
    int** pa;

    pa = arr;

    printf("pa : &p \n", pa);
    printf("arr[0][0] : %d \n", arr[0][0]);
    printf("pa[0][0] : %d \n", pa[0][0]);

    return 0;
}

 

 

컴파일 시에 오류가 발생한다. pa[2][2]에서 이상한 메모리 공간의 값에 접근했기 때문이다. 

먼저, int arr[5] 라는 배열에서 n번째 원소의 주소값을 알아내는 방법을 생각해보자.

배열의 시작 주소를 addr라고 하면 arr[n]의 주소값은 addr + 4n 일 것이다.

 

그렇다면, int arr[a][b]인 2차원 배열이 존재하면 arr[b] 짜리의 배열이 a개 존재한다고 생각하면 된다. 

배열의 시작 주소를 addr라고 하면 arr[n][0]의 주소값은 addr + 4nb 일 것이다.

즉, arr[n][m]의 주소값은 addr + 4nb + 4m이 되는 것이다.

 

여기서 포인트는 int arr[n][m]의 주소값을 정확하게 계산하기 위해서는 n, m 값 뿐만 아니라, b의 값도 알아야 한다는 것이다.

 

따라서 2차원의 배열을 가리키는 포인터를 통해서 원소들을 정확하게 접근하기 위해서는

가리키는 원소 형태의 크기와 b의 값에 대한 정보가 있어야 한다.

 

그렇다면 예를 통하여 2차원 배열이 가리키는 포인터는 어떻게 생겼는지 알아보자

 

 

#include <stdio.h>

int main() {
    
    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int(*pa)[3];

    pa = arr;

    printf("pa[1][2] : %d, arr[1][2] : %d", pa[1][2], arr[1][2]);

    return 0;
}

 

 

위에서 말했듯이, 2차원 배열을 가리키는 포인터는 배열의 크기에 관한 정보가 있어야 주소값을 구할 수 있다. 그래서 int (*pa)[3]; 처럼 2차원 배열을 가리키는 포인터를 정의할 때 2차원 배열의 열 개수도 함께 적어줘야 한다.

int (*pa)[3]; 은 int 형인 2차원 배열을 가리키는데, 그 배열 한 행의 길이가 3이라는 의미이다.

 

위와 같은 형태의 정의가 익숙할 수 있다. 사실 pa는 크기가 3인 배열을 가리키는 포인터를 의미한다. 1차원 배열에서 배열의 이름 자체는 첫 번째 원소를 기리키는 포인터로 타입이 변환된 것 처럼, 2차원 배열에서 배열의 이름이 첫 번째 행을 가리키는 포인터로 타입이 변환이 된다. 그리고 그 첫 번째 행은 사실 크키가 3인 1차원 배열인 것이다.

 

 

#include <stdio.h>

int main() {
	int arr1[2][3];
	int arr2[10][3];
	int arr3[2][5];

	int(*pa)[3];

	pa = arr1;  
	pa = arr2;  
	pa = arr3;  // 오류

	return 0;
}

 

지금까지 이해를 했다면 위의 코드에서 pa = arr3에서 왜 에러가 나는지 알 수 있을 것이다.

 

 

 

포인터 배열

위에서는 배열을 가리키는 포인터에 대해서 설명했고, 이번에는 포인터들을 모아놓은 배열에 대해서 공부해보자. 

 

 

#include <stdio.h>

int main() {
	int* arr[3];
	int a = 1, b = 2, c = 3;

	arr[0] = &a;
	arr[1] = &b;
	arr[2] = &c;

	printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
	printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
	printf("b : %d, *arr[2] : %d \n", c, *arr[2]);

	printf("&a : %p, arr[0] : %p \n", &a, arr[0]);

	return 0;
}

 

 

int* arr[3]; 을 통하여 배열의 각각 원소는 int를 가리키는 포인터 형으로 선언된 것이다. 따라서 int* 배열에서 각각의 원소를 포인터로 취급한 것이다.

 

출처 : 씹어먹는 c언어

다음의 그림과 같이 arr[0] = &a; / arr[1] = &b; / arr[2] = &c 로 이해하면 되는 것이다.

즉, arr[0] 에는 변수 a의 주소값이 들어가고 arr[1] 에는 변수 b의 주소값이 들어가고, arr[2]에는 변수 c의 주소값이 들어있는 것이다.