포인터한테 맨날 지면서 살 수는 없다.
오늘은 정복해보자.
Before Starting
지금까지는 포인터가 가리키는 데이터를 사용하기 위해 포인터를 사용했으나,
오늘은 주소
값 자체를 처리할 데이터로 생각해보려 한다.
즉, 주소를 저장한 포인터도 하나의 변수이고
그 주소를 다른 포인터에 저장하고 가리키는 것도 가능하다는 것이다.
예를 들어 어떤 변수를 가리키는 포인터 pi가 있고,
할당된 메모리의 시작 위치가 200번지라고 해보자.
이 주소를 저장하는 포인터가 바로 이중 포인터
이다.
즉, 포인터의 주소는 이중 포인터에 저장하며 포인터를 가리킨다.
아찬가지로, 간접 참조 연산을 수행하면 가리키는 대상인 포인터를 쓸 수 있다.
예제로 이해해보자.
#include <stdio.h> int main(void) { int a = 10; int *pi; int **ppi; pi = &a; ppi = π printf("변수 변수값 &연산 *연산 **연산\n"); printf(" a %10d %10u\n", a, &a); printf(" pi %10u %10u %10d\n", pi, &pi, *pi); printf("ppi %10u %10u %10u %10u\n", ppi, &ppi, *ppi, **ppi); return 0; }
이중 포인터는 7행과 같이 별(*)을 2개 붙여 선언한다.
이때 첫 번째 별은 ppi가 가리키는 자료형(int *)이 포인터 임을 뜻하고,
두 번째 별은 ppi 자신이 포인터임을 뜻한다.
메모리에 저장 공간이 할당되면 그 이후에 이중 포인터를 사용할 때는 변수명을 쓴다.
1. 포인터를 변수명(r-value)
으로 쓰면 그 안의 값이 된다.
pi와 ppi가 변수명으로 사용되어 그 안의 값이 된다.
2. 포인터에 & 연산
을 하면 포인터 변수의 주소가 된다.
pi와 ppi에 &연산을 한 결과는 자신의 주소 값을 의미한다.
3. 포인터의 * 연산
은 화살표를 따라간다.
ppi에 * 연산을 하면 ppi가 가리키는 대상 pi를 뜻한다.
ppi에 ** 연산을 하면 ppi가 가리키는 pi가 가리키는 대상이므로 변수 a가 된다.
포인터에서는 포인터가 가리키는 것
과 포인터 자신
의 형태를 구분해야 한다.
예를 들어 int 형 변수의 주소를 저장하는 포인터는
가리키는 자료형이 int형이고, 자신의 형태는 (int *)형이다.
단일 포인터도 가리키는 자료형에 따라 다양하게 선언하듯이
이중 포인터도 가리키는 포인터의 형태에 맞춰 선언해야 한다.
다음과 같이 변수와 포인터가 선언된 경우를 생각해보자.
double a = 3.5; double *pi = &a;
pi가 (double *)형 변수이므로 &pi는 (double *)형의 주소가 된다.
따라서 (double *)형을 가리키는 이중 포인터를 선언한다.
double **ppi;
pi나 ppi는 메모리에서 4바이트만 차지한다.
포인터 앞에 적어주는 자료형은 가리키는 자료형에 대한 정보일 뿐이지,
포인터 자체를 의미하진 않는다.
즉, 포인터는 주소 값만을 저장하는 변수이므로
주소 값 자체의 크기
에 따라 모든 포인터의 크기가 결정된다.
포인터는 변수이므로 주소 연산자를 사용하여 그 주소를 구할 수 있지만
상수인 주소에는 주소 연산자를 쓸 수 없다.
int a; int *pi = &a; // 주소를 포인터에 저장 π // 포인터에 주소 연산자 사용 가능 (O) &(&a); // a의 주소를 다시 주소 연산자 사용 불가능 (X)
단일 포인터와 마찬가지로 이중 포인터도 변수이므로
주소 연산자를 사용하면 그 주소를 구할 수 있다.
double ***ppp;
짠.
같은 방식으로 4중 이상의 포인터도 사용할 수 있으나 프로그램의 가독성을 떨어트리므로
가능하면 사용하지 말자.
이중 포인터는 포인터의 값을 바꾸는 함수의 매개변수에 사용한다.
예를 들어보자.
#include <stdio.h> void swap_ptr(char **ppa, char **ppb); int main(void) { char *pa = "success"; char *pb = "failure"; printf("pa -> %s, pb -> %s\n", pa, pb); swap_ptr(&pa, &pb); printf("pa -> %s, pb -> %s\n", pa, pb); return 0; } void swap_ptr(char **ppa, char **ppb) { char *pt; pt = *ppa; *ppa = *ppb; *ppb = pt; }
문자열 자체를 바꾸지 않고, 문자열을 연결하는 포인터의 값을 바꿨다.
두 변수의 값을 바꾸는 함수는
변수의 주소를 인수로 주고 함수가 그 주소를 간접 참조하여 변수의 값을 바꿔야 한다.
그런데 11행에서 바꾸고자 하는 변수 pa, pb는 포인터이므로
함수의 인수로 포인터 주소를 줘야 하고,
그 값을 받는 매개변수로 이중 포인터가 필요하다.
매개변수 ppa와 ppb를 사용하여 main 함수에 있는 포인터 pa, pb 값을 바꿔준다.
값을 바꾸는 데 사용할 임시 포인터를 pt로 선언하고 다음 3단계를 거치면 값이 바뀐다.
pt = *ppa; // ppa가 가리키는 pa값을 pt에 저장 *ppa = *ppb; // ppb가 가리키는 pb의 값을 ppa가 가리키는 pa에 저장 *ppb = pt; // pt의 값을 ppb가 가리키는 pb에 저장
이중 포인터는 포인터 배열을 매개변수로 받는 함수에도 사용한다.
배열명이 첫 번째 배열 요소의 주소이므로
int형 배열의 이름은 int형 변수의 주소인 것처럼
int형 포인터 배열의 이름은 int형 포인터의 주소가 된다.
#include <stdio.h> void print_str(char **pps, int cnt); int main(void) { char *ptr_ary[] = {"eagle", "tiger", "lion", "squirrel"}; int count; count = sizeof(ptr_ary) / sizeof(ptr_ary[0]); print_str(ptr_ary, count); return 0; } void print_str(char **pps, int cnt) { int i; for (i = 0; i < cnt; i++) { printf("%s\n", pps[i]); } }
ptr_ary는 포인터 배열의 이름이므로 포인터의 주소이다.
따라서 배열명을 인수로 받는 함수의 매개변수는 이중 포인터!
포인터가 배열명을 저장하면 배열명처럼 사용할 수 있으므로
함수 안에서는 매개변수를 배열명처럼 사용하여 문자열을 출력한다.
배열의 주소 &ary가 주소로 쓰이는 ary와 어떤 차이가 있을까?
#include <stdio.h> int main(void) { int ary[5]; printf(" ary의 값 : %u\t", ary); printf("ary의 주소 : %u\n", &ary); printf(" ary + 1 : %u\t", ary + 1); printf(" &ary + 1 : %u\n", &ary + 1); return 0; }
ary가 주소로 쓰일 때와 &ary의 값은 모두 배열의 시작 위치
이다.
그러나 ary 자체가 주소로 쓰일 때는 첫 번째 요소
를 가리키므로
주소에 1을 더한 결과는 4만큼 차이가 난다.
반면 &ary는 배열 전체
를 가리키므로 주소에 1을 더한 결과는 20만큼 차이가 난다.
이 둘의 차이를 명확히 구분하려면 다음 규칙을 이해해야 한다.
예를 들어 다음과 같은 배열이 있다고 하면,
int ary[5];
배열 ary는 크기가 20바이트(= 메모리에 할당된 크기) 이며
int형 변수 5개(= 배열 요소의 수)의 배열이란 자료형의 정보를 가진다.
배열의 정수 연산
ary + 1 → 0000100 + (1 * sizeof(ary[0])) → 0000100 + (1 * 4) → 0000104
배열의 주소에 정수 연산
&ary + 1 → 0000100 + (1 * sizeof(ary)) → 0000100 + (1 * 20) → 0000120
이제야 이해할 것 같다
배열 포인터는 배열을 가리키는 포인터로 2차원 배열의 이름을 저장할 수 있다.
#include <stdio.h> int main(void) { int ary[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; int (*pa)[4]; int i, j; pa = ary; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf("%5d", pa[i][j]); } printf("\n"); } return 0; }
2차원 배열의 이름을 저장할 배열 포인터의 선언할 때,
변수명 앞에 별(*)을 붙여 포인터임을 표시하고 괄호로 묶어야 한다.
이 예제는 사실 굳이 배열 포인터를 사용하지 않아도 되지만,
2차원 배열을 출력하는 함수에는 배열 포인터가 필요하다.
#include <stdio.h> void print_ary(int(*pa)[4]); int main(void) { int ary[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; print_ary(ary); return 0; } void print_ary(int(*pa)[4]) { int i, j; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf("%5d", pa[i][j]); } printf("\n"); } }
9행에서 print_ary 함수를 호출할 때 2차원 배열명을 인수로 주면
함수에는 첫 번째 부분배열의 주소가 전달된다.
따라서 이 값을 저장하기 위한 매개변수로 배열 포인터를 선언해야 한다.
printf("%5d", pa[i][j]);
22행처럼 함수 안에서 매개변수 pa를 배열처럼 사용하여 2차원 배열의 값을 출력한다.
2차원 배열에서 배열 요소
는
논리적으로는 1차원의 부분배열(행)을 뜻하고
물리적으로는 실제 데이터를 저장하는 부분배열의 요소를 뜻한다.
귀엽누