다차원 포인터란
포인터 변수를 선언할 때 * 키워드를 한 개만 사용한 포인터를 1차원 포인터라고 이야기하며 * 키워드를 하나씩 더 사용할 때마다 차원이 하나씩 증가한다 . 그리고 * 키워드를 두 개 이상 사용해서 선언한 포인터를 '다차원 포인터'라고 한다.
포인터 변수를 선언할 때 사용하는 * 키워드는 최대 7개까지 사용할 수 있다. 그리고 포인터 변수를 선언할 때 사용한 * 키워드의 개수만큼 포인터를 사용할 때 * 연산자를 사용할 수 있다. 예를 들어 char *p; 라고 선언하면 포인터 변수 자체를 의미하는 p 또는 포인터 변수가 가리키는 곳에 값을 저장하기 위한 *p, 이렇게 두 가지 표현을 사용 할 수 있다.
따라서 char ***p3; 라고 *키워드를 3개 사용해서 선언하면 p3, *p3, **p3 또는 ***p3라는 네가지 표현을 사용할 수 있다.
char *p1; // 1차원 포인터 : p1, *p1
char **p2; // 2차원 포인터 : p2, *p2, **p2
char ***p3; // 3차원 포인터 : p3, *p3, **p3, ***p3
특정한 대상을 간접적으로 여러번 가리키는 포인터를 다차원 포인터라고 부른다.
일반 변수의 한계와 다차원 포인터
주소 값을 저장할 수 있는 크기(4바이트)의 변수라면 포인터 변수가 아니더라도 주소를 저장할 수는 있다. 따라서 다음과 같이 4바이트 크기의 자료형인 int형으로 my_ptr 변수를 선언하고 & 연산자를 사용하여 data 변수의 주소 값을 my_ptr 변수에 저장할 수 있다.
short data = 0;
int my_ptr = (int)&data; /*&data는 short * 형식의 값을 가지기 때문에 int형 변수인 my_ptr에 저장하기 위해서(int)로 형 변환한다. mt_ptr은 4바이트의 크기라서 정상적으로 주소를 저장한다.*/
*my_ptr = 3;
*일반 변수에도 주소값을 저장할 수 있다. 하지만 해당 주소로 가서 값을 읽거나 쓸 수 없기 때문에 의미가 없다.
my_ptr 변수는 포인터 변수가 아니기 때문에 * 연산자를 사용할 수 없다. 이것은 my_ptr 변수에 저장된 주소로 이동하여 값을 대입하거나 읽을 수 없다는 뜻이다. 그래서 일반 변수에 주소 값을 저장하지 않고 * 연산자를 사용해서 포인터 변수에 주소 값을 저장하는 것이다. 포인터 변수는 자신이 저장하고 있는 주소에 가서 값을 읽거나 쓸 수 있는 기능을 가지고 있다.
2차원 포인터
2차원 포인터의 선언과 사용
다차원 포인터 중 가장 자주 사용하는 것은 2차원 포인터다. 2차원 포인터를 사용하면 2차원 배열을 다루는게 훨씬 쉬워진다. 3차원 포인터 부터는 2차원 포인터에서 단계가 하나씩 추가될 뿐 원리는 같다.
2차원 포인터는 아래와 같이 * 키워드를 두 개 사용해서 선언한다. 그래서 포인터를 사용할 때 pp, *pp또는 **pp처럼 * 연산자를 최대 2개까지 사용할 수 있다.
short **pp;
예를 들어 '**pp'라고 사용하면 두번째 주소를 타고 세번째 주소로 향한다 하지만 2번째 주소에 들어있는 정보가 주소가 아닌 다른 의미의 값(예를 들어 5)가 저장되어 있다면 프로그램이 엉뚱한 메모리를 사용해서 오류가 발생한다.
short **pp;
int data = 3; (&data로 얻는 주소의 자료형이 int *이기 때문에 포인터 변수 pp와 자료형을 맞추기 위해서 short **로 형 변환한다.)
pp = (short **)&data; //data의 주소가 2차원 포인터 변수 pp에 저장됨
이처럼 2차원 포인터는 세 번째 주소로 한 번 더 이동할 수 있기 때문에 두 번째 주소에 마지막 주소의 값이 저장되어야 한다. 따라서 두 번째 주소는 다음과 같이 1차원 포인터 변수가 되어야 안정적인 구조가 된다.
short **pp;, *p, data = 3;
p = &data; //data변수의 주소 값이 포인터 변수 p에 저장됨
pp = &p; // 1차원 포인터 변수 p의 주소 값이 2차원 포인터 변수 pp에 저장됨
**pp = 5; //data 변수의 값이 3에서 5로 변경됨
다차원 포인터 구조는 오른쪽으로 하나씩 이동할 때마다 차원을 하나씩 줄여주는 것이 좋은 구조다. 이와 같은 구조를 유지하면 강제적인 형 변환을 할 필요도 없고 자연스럽게 코드를 구성할 수 있다.
1차원 포인터 변수에 1차원 포인터 변수의 주소를 저장하면?
1차원 포인터 q변수를 사용하여 1차원 포인터 p변수의 주소 값을 저장할 수는 있다. 하지만 1차원 포인터 q변수는 *연산자를 한개만 사용할 수 있기 때문에 포인터 변수 p까지만 이동할 수 있고 data 변수까지는 이동할 수 없다. 따라서 2차원 포인터를 사용하는 것과 구조는 비슷하지만 실용성은 떨어진다.
int *q, *p, data = 3;
p = %data; //포인터 변수 p는 data 변수의 주소 값을 기억함
q = (int *)&p; //포인터 변수 q는 포인터 변수 p의 주소값을 기억함
또한 이렇게 사용하면 차원이 맞지 않는 문제가 발생한다. 일반 변수의 주소를 &연산자를 사용하여 계산하면 1차원 형식의 포인터 값으로 반환된다. 1차원 포인터의 주소 값을 & 연산자를 사용해서 계산하면 차원이 하나 증가하여 2차원 형식의 포인터 값으로 반환된다. 따라서 차원을 맞추기 위해서 (int*) 형식으로 형 변환을 해주어야 한다.
2차원 포인터가 가리키는 첫 대상이 일반 변수인 경우
2차원 포인터가 가리키는 첫 대상이 일반 변수이며 해당 변수이면 주소 값이 아닌 일반 숫자 값을 저장하고 있을 확률이 높다. 일반 숫자 값을 주소로 사용하면 ** 연산자를 사용할 때 문제가 발생할 수도 있다. 그러므로 2차원 포인터가 가리키는 첫 대상은 1차원 포인터가 되는게 일반적이다. 그러면 2차원 포인터의 첫 대상으로 일반변수를 사용하면서, 그 변수에 정상적인 주소 값을 저장해 사용하면 어떻게 될까?
2차원 포인터 변수 pp가 일반 변수의 주소값(106번)을 저장하더라도, 그 일반 변수가 다른 변수의 주소 값(110번)을 저장하고 있다면 구조적으로는 문제가 되지 않는다. 다만 차원이 하나씩 감소하는 일반적인 구조가 아니기 때문에 각 변수 간에 자료형이 맞지 않아서 형 변환을 많이 해주어야 한다.
2차원 포인터 변수 | 일반 변수 | 일반 변수 |
pp (2차원) | my_ptr(0차원) | data(0차원) |
일반 변수 my_ptr은 * 연산자를 사용할 수 없기 때문에 data 변수의 주소 값을 저장하고 있더라도 이 주소로 이동할 수 없다. 하지만 2차원 포인터 변수인 pp는 **pp를 사용하여 두 번째 대상인 data 변수를 가리킬 때 첫번째 대상에 저장된 주소 값을 읽어서 data 변수를 가리킬 수 있다. 즉 첫 번째 대상인 my_ptr이 어떤 형식의 변수이든 상관없이 4바이트 크기의 메모리이고 주소 값만 정상적으로 저장되어 있다면 그 주소 값을 사용하여 두 번째 대상을 가리킬 수 있다는 뜻이다.
따라서 2차원 포인터 변수인 pp는 **pp = 5; 라고 사용했을 때 my_ptr 변수가 1차원 포인터가 아니더라도 4바이트의 크기의 메모리이고 정상적인 주소 값을 저장하고 있기 때문에 110번지로 한번에 이동하여 값 5를 대입할 수 있다.
short**pp, data = 3;
int my_ptr = (int)&data;
//&data는 short * 형식의 값을 가지기 때문에 int형 변수인 int로 형변환한다. 4바이트 크기라서 정상적으로 주소를 저장한다.
pp = (short **)&my_ptr; // my_ptr의 주소 값이 2차원 포인터 변수 pp에 저장됨
**pp = 5; //data 변수의 값이 3에서 5로 변경됨
결론적으로 첫 번째 상자가 2차원 포인터 변수이고 두 번째 상자는 어떤 형식의 변수가 오든지 4바이트 크기이기만 하면 무조건 세번째 상자로 이동하여 값을 변경할 수 있다.
2차원 포인터가 가리키는 대상을 동적으로 할당하기
1차원 포인터는 동적으로 할당한 메모리의 주소 값을 받아 사용할 수 있다. 따라서 2차원 포인터도 다음과 같이 두 번째 상자에 해당하는 4 바이트 메모리를 malloc 함수로 동적으로 할당해서 사용할 수 있다. 2차원 포인터는 자신이 가리키는 첫 대상이 어떤 종류의 변수인지 상관없이 4바이트 크기의 주소 값만 저장되어 있으면 최종 대상을 사용할 수 있기 때문이다.
short **pp, data = 3;
pp = (short **)malloc(4); // 두 번째 상자로 사용할 4바이트 메모리를 할당하고 그 주소값을 2차원 포인터 pp에 저장
*pp = &data; //data 변수의 주소 값을 두 번째 상자 (동적으로 할당된 메모리)에 저장함
**pp = 5; // data 변수의 값이 3에서 5로 변경됨
102번지 | 300번지 | 106번지 |
pp (2차원) 300 |
malloc 함수로 4바이트 동적 할당 106 |
data (0차원) |
위 표에서 pp변수와 data 변수는 스택에 할당되지만 두 번째 상자에 해당하는 메모ㅠ리는 동적으로 할당되기 때문에 힙에 할당된다. 따라서 사용하는 메모리 영역이 다르므로 두 번째 상자의 주소 번지를 100번대가 아닌 300번지에 할당되었다고 가정한다.
*2차원 포인터가 가리키는 메모리는 간접적으로 1차원 포인터의 역할을 한다
이 예시는 동적으로 할당된 4 바이트 메모리는 포인터가 안니라서 *연산자를 사용할 수 없기 때문에 스스로는 아무것도 할 수 없다. 하지만 **pp라고 사용할 때 이 4바이트 메모리에 저장된 주소값을 사용하여 세 번째 상자로 이동하기 때문에 이 4 바이트 메모리는 간접적으로 1차원 포인터처럼 사용된다. 즉 2차원 포이넡 기준으로 봤을 때 이 4바이트 메모리는 short* 로 선언한 포인터처럼 사용되기 때문에 malloc(4)로 적는 것보다 다음과 같이 적는 것이 코드의 의미를 더 분명하게 전달할 수 있다.
pp = (short **)malloc(sizeof(short*)); //short *는 포인터이기 때문에 크기가 4바이트임
결국 이 형식은 short * 형식의 포인터를 동적으로 할당하여 그 주소 값을 2차원 포인터 pp에 저장하는 형태이다. 그래서 위와 같이 적으면 1차원 포인터 한 개를 동적 할당한다고 이야기 한다.
다음 예제는 두번째 상자와 세번쨰 상자까지 동적 메모리 할당을 사용하여 2차원 포인터 구조를 구성한 것이다. 이렇게 구성하면 스택공간에는 포인터 변수 pp만 할당되고 pp에 할당받은 동적 메모리는 힙에 할당된다.
이 예제에서 동적으로 할당한 메모리를 해제하는 순서가 정말 중요하다. 왜냐하면 두 번째 상자가 세 번째 상자의 주소값을 가지고 있기 때문에 두 번째 상자를 먼저 해제하면 세번째 상자의 주소값을 잃어버리기 때문이다. 그러면 세번째 상자에 해당하는 메모리를 해제하지 못하기 되므로, 반드시 세 번째 상자의 메모리를 먼저 해제하고 두 번째 상자의 메모리를 해제해야 한다.
2차원 포인터가 가리키는 대상을 동적으로 할당하면 좋은 점
위의 예제에서는 2차원 포인터 개념을 설명하기 위해서 2차원 포인터가 가리키는 첫 번째 대상을 4바이트로 동적 메모리 할당하고 두 번째 대상을 2바이트로 동적 메모리 할당했다. 즉 첫 번째 대상은 4바이트 1개이고 두 번째 대상은 2바이트 1개를 할당한 것이다.
하지만 꼭 이렇게 가리키는 대상을 1개로 구성해야 하는 것은 아니다.
short **pp = (short **)malloc(3 * sizeof(short*)); /* 12바이트 (3*4) 크기로 메모리가 할당됨*/
이 코드처럼 첫 번째 대상을 4바이트가 아닌 12바이트(4바이트 3개)로 할당해도 된다. 12바이트면 4바이트 크기로 메모리를 나눠서 사용할 수 있기 때문에 총 세 개의 주소를 저장할 수 있다. 그리고 첫 번째 대상의 첫 번째 주소에 접근하고 싶으면 포인터의 주소 연산을 사용해서 *(pp + 0)이라고 써 주면 된다. 그리고 첫 번째 대상의 두 번째 주소에 접근하고 싶으면 *(pp + 1) 그리고 세 번째 주소에 접근하려면 *(pp + 2)라고 쓰면 된다.
2차원 포인터 pp가 가리키는 두 번째 대상도 2바이트가 아닌 4바이트로 할당해서 short 형식의 정수 값을 한 개가 아닌 두 개 저장할 수도 있다.
*pp = (short *)malloc(2 * sizeof(short)); /*4바이트 (2 * 2) 크기로 메모리가 할당됨*/
위와 같이 4바이트가 할당되면 2바이트(short)씩 나눠서 2개의 정수를 저장할 수 있다. 첫 번째 정수 값을 저장하기 위해서는 *(*pp + 0)또는 **pp라고 사용하면 되고 두 번째 정수값을 저장하기 위해서는 **pp + 1 이라고 사용하면 된다.
2차원 포인터와 함수의 매개변수
다음과 같이 8바이트의 메모리를 동적으로 할당하는 GetMyData 함수를 만들고 이 함수를 호출하는 예제 코드를 구성했다. 이 예제는 컴파일은 성공하지만 프로그램이 실행될 때 12행에서 오류가 발생한다.
위의 예제는 컴파일이 잘 되어서 별 문제가 없는 것처럼 보이지만 프로그램이 실행되면 12행을 수행하다가 오류가 발생하여 프로그램이 멈춰버린다. 이 문제가 발생한 이유는 main함수의 포인터 변수 p에서 GetMyData 함수의 포인터 변수 q로 원하지 않는 주소 값이 전달되었기 때문이다. 이 상황을 명령이 실행되는 순서대로 적어보면 다음과 같다.
main() | GetMyData(int *q) |
1. int *p; 2. GetMyData(p); 5. |
4. q = (int *)malloc(8) |
3. int *q = p; |
1. p는 초기화 되지 않아서 쓰레기 값을 저장함
2. GetMyData 함수의 매개변수로 p 전달함
3. p에 저장된 주소 값을 q에 대입함. p와 q는 같은 주소 값을 저장하고 있음.
4. 8바이트 동적 메모리 할당. 할당된 메모리의 주소 값을 q에 저장함. 결국 q에는 새로 할당된 메모리의 주소가 저장되었을 뿐 p 포인터 변수에는 별다른 영향을 미치지 않음
5. p는 처음 가지고 있던 쓰레기 값을 그대로 가지고 있는 상황인데, 그 주소에 5를 저장하려고 하면 원하지 않는 주소에 값을 대입하는 것이기 때문에 오류가 생김
앞의 코드를 보면 포인터 변수 p에는 실제 메모리 주소 값이 대입된 적이 없다. 초기화 되지않은 쓰레기 값만 들어갔을 뿐이다. 그런데 이 포인터 변수를 *p=5;라고 사용했으니 유효하지 않은 주소로 이동해서 값 5를 대입하기 때문에 문제가 발생하는 것이다. 이 문제를 해결하려면 어떻게 해야 할까?
동적으로 할당된 주소값을 포인터 변수에 대입하면 문제를 해결할 수 있을까?
먼저 malloc 함수를 사용하여 8바이트 메모리를 할당한다. 그리고 할당된 메모리의 주소 값은 포인터 변수q에 저장하고, q가 저장하고 있는 주소 값을 포인터 변수 p에 대입해 보자.
main() | GetMyData(int *q) |
1. int *p; 2. GetMyData(p); 5. *p=5 free(p); |
4. q = (int*)malloc(8); |
3. int **q = &p; |
1. p는 초기화 되지 않아서 끄레기 값을 저장함
2. GetMyData 함수의 매개변수로 &p 전달함.
3. 2차원 포인터 q에 1차원 포인터 p의 주소값을 저장함
4. 8바이트 동적 메모리 할당. 할당된 메모리의 조수 값은 q가 가리키는 대상인 포인터 변수 p에 저장함.
5. p에 저장된 주소로 가서 값 5를 대입한다.
위와 같이 2차원 포인터와 함수의 매개변수 개념을 사용하면 변수p와 q가 서로 다른 함수의 지역 변수라도 main함수에 선언된 포인터 변수 p의 주소 값을 변경할 수 있다. 이제 이 개념을 GetMyData 함수에 적용해 보자.
동적 메모리 할당을 설명할 때 malloc 함수와 free 함수를 한 쌍으로 사용하는 것이 좋다고 했는데 위의 예제에서는 분리해서 사용했다. 그 이유는 GetMyData 함수에서 동적으로 할당한 메모리의 주소 값을 main 함수의 포인터 변수 p에 저장하여 사용하기 때문이다. 따라서 GetMyData 함수에서 free 함수를 사용해서 메모리를 해제하면 6행에서 할당한 메모리가 해제되어 버린다. 즉 main 함수의 포인터 변수 p에 전달된 주소 값은 이미 해제된 메모리의 주소값인 것이다. 따라서 GetMyData 함수에서 할당된 메모리를 해제하면, main 함수에서 포인터 변수 p를 사용하는 13, 14행에 문제가 발생하므로 주의해야 한다.
'C언어' 카테고리의 다른 글
14) 구조체와 연결 리스트 (0) | 2021.07.12 |
---|---|
13-2) 2차원 포인터와 2차원 배열 (1) | 2021.06.29 |
12-1) 동적 메모리 할당 및 해제 (0) | 2021.05.01 |
12) 프로세스와 메모리 할당 (0) | 2021.04.10 |
11-2) 배열 시작 주소 (0) | 2021.03.14 |