[C] 포인터 정복

장세민·2022년 10월 22일
1

📝 TIL

목록 보기
27/40
post-thumbnail

포인터한테 맨날 지면서 살 수는 없다.

오늘은 정복해보자.

Before Starting

지금까지는 포인터가 가리키는 데이터를 사용하기 위해 포인터를 사용했으나,
오늘은 주소 값 자체를 처리할 데이터로 생각해보려 한다.

즉, 주소를 저장한 포인터도 하나의 변수이고
그 주소를 다른 포인터에 저장하고 가리키는 것도 가능하다는 것이다.

주소 안에 주소 ?



📌 이중 포인터

📖 개념

예를 들어 어떤 변수를 가리키는 포인터 pi가 있고,
할당된 메모리의 시작 위치가 200번지라고 해보자.

그럼 &pi 값을 저장할 변수는?

이 주소를 저장하는 포인터가 바로 이중 포인터이다.
즉, 포인터의 주소는 이중 포인터에 저장하며 포인터를 가리킨다.

아찬가지로, 간접 참조 연산을 수행하면 가리키는 대상인 포인터를 쓸 수 있다.

예제로 이해해보자.

  1. #include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int a = 10;
  6. int *pi;
  7. int **ppi;
  8.  
  9. pi = &a;
  10. ppi = &pi;
  11.  
  12. printf("변수 변수값 &연산 *연산 **연산\n");
  13. printf(" a %10d %10u\n", a, &a);
  14. printf(" pi %10u %10u %10d\n", pi, &pi, *pi);
  15. printf("ppi %10u %10u %10u %10u\n", ppi, &ppi, *ppi, **ppi);
  16.  
  17. return 0;
  18. }

이중 포인터는 7행과 같이 별(*)을 2개 붙여 선언한다.

이때 첫 번째 별은 ppi가 가리키는 자료형(int *)이 포인터 임을 뜻하고,
두 번째 별은 ppi 자신이 포인터임을 뜻한다.

메모리에 저장 공간이 할당되면 그 이후에 이중 포인터를 사용할 때는 변수명을 쓴다.

🔔 변수 a, 포인터 pi, 이중 포인터 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;	// 주소를 포인터에 저장
&pi;		// 포인터에 주소 연산자 사용 가능 (O)
&(&a);		// a의 주소를 다시 주소 연산자 사용 불가능 (X)

다중 포인터

단일 포인터와 마찬가지로 이중 포인터도 변수이므로
주소 연산자를 사용하면 그 주소를 구할 수 있다.

double ***ppp;

짠.

같은 방식으로 4중 이상의 포인터도 사용할 수 있으나 프로그램의 가독성을 떨어트리므로
가능하면 사용하지 말자.



📖 이중 포인터 활용

포인터 값을 바꾸는 함수의 매개변수

이중 포인터는 포인터의 값을 바꾸는 함수의 매개변수에 사용한다.

예를 들어보자.

  1. #include <stdio.h>
  2.  
  3. void swap_ptr(char **ppa, char **ppb);
  4.  
  5. int main(void)
  6. {
  7. char *pa = "success";
  8. char *pb = "failure";
  9.  
  10. printf("pa -> %s, pb -> %s\n", pa, pb);
  11. swap_ptr(&pa, &pb);
  12. printf("pa -> %s, pb -> %s\n", pa, pb);
  13.  
  14. return 0;
  15. }
  16.  
  17. void swap_ptr(char **ppa, char **ppb)
  18. {
  19. char *pt;
  20.  
  21. pt = *ppa;
  22. *ppa = *ppb;
  23. *ppb = pt;
  24. }

문자열 자체를 바꾸지 않고, 문자열을 연결하는 포인터의 값을 바꿨다.

두 변수의 값을 바꾸는 함수는
변수의 주소를 인수로 주고 함수가 그 주소를 간접 참조하여 변수의 값을 바꿔야 한다.

그런데 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형 포인터의 주소가 된다.


여러 개의 문자열을 출력하는 함수를 작성해보자.
  1. #include <stdio.h>
  2.  
  3. void print_str(char **pps, int cnt);
  4.  
  5. int main(void)
  6. {
  7. char *ptr_ary[] = {"eagle", "tiger", "lion", "squirrel"};
  8. int count;
  9.  
  10. count = sizeof(ptr_ary) / sizeof(ptr_ary[0]);
  11. print_str(ptr_ary, count);
  12.  
  13. return 0;
  14. }
  15.  
  16. void print_str(char **pps, int cnt)
  17. {
  18. int i;
  19.  
  20. for (i = 0; i < cnt; i++)
  21. {
  22. printf("%s\n", pps[i]);
  23. }
  24. }

ptr_ary는 포인터 배열의 이름이므로 포인터의 주소이다.
따라서 배열명을 인수로 받는 함수의 매개변수는 이중 포인터!

포인터가 배열명을 저장하면 배열명처럼 사용할 수 있으므로
함수 안에서는 매개변수를 배열명처럼 사용하여 문자열을 출력한다.



배열 요소의 주소와 배열의 주소

배열의 주소 &ary가 주소로 쓰이는 ary와 어떤 차이가 있을까?

  1. #include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. int ary[5];
  6.  
  7. printf(" ary의 값 : %u\t", ary);
  8. printf("ary의 주소 : %u\n", &ary);
  9. printf(" ary + 1 : %u\t", ary + 1);
  10. printf(" &ary + 1 : %u\n", &ary + 1);
  11.  
  12. return 0;
  13. }
  14.  

ary가 주소로 쓰일 때와 &ary의 값은 모두 배열의 시작 위치 이다.

그러나 ary 자체가 주소로 쓰일 때는 첫 번째 요소를 가리키므로
주소에 1을 더한 결과는 4만큼 차이가 난다.

반면 &ary는 배열 전체를 가리키므로 주소에 1을 더한 결과는 20만큼 차이가 난다.

이 둘의 차이를 명확히 구분하려면 다음 규칙을 이해해야 한다.

규칙 1. 배열은 전체가 하나의 논리적인 변수이다.

예를 들어 다음과 같은 배열이 있다고 하면,

int ary[5];

배열 ary는 크기가 20바이트(= 메모리에 할당된 크기) 이며
int형 변수 5개(= 배열 요소의 수)의 배열이란 자료형의 정보를 가진다.


규칙 2. 배열의 주소에 정수를 더하면 배열 전체의 크기를 곱해서 더한다.

배열의 정수 연산

ary + 10000100 + (1 * sizeof(ary[0]))0000100 + (1 * 4)0000104

배열의 주소에 정수 연산

&ary + 10000100 + (1 * sizeof(ary))0000100 + (1 * 20)0000120

이제야 이해할 것 같다



📖 2차원 배열과 배열 포인터

배열 포인터는 배열을 가리키는 포인터로 2차원 배열의 이름을 저장할 수 있다.

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

2차원 배열의 이름을 저장할 배열 포인터의 선언할 때,
변수명 앞에 별(*)을 붙여 포인터임을 표시하고 괄호로 묶어야 한다.

괄호가 없으면 포인터 배열이 되므로 주의!

이 예제는 사실 굳이 배열 포인터를 사용하지 않아도 되지만,
2차원 배열을 출력하는 함수에는 배열 포인터가 필요하다.

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

9행에서 print_ary 함수를 호출할 때 2차원 배열명을 인수로 주면
함수에는 첫 번째 부분배열의 주소가 전달된다.
따라서 이 값을 저장하기 위한 매개변수로 배열 포인터를 선언해야 한다.

printf("%5d", pa[i][j]);

22행처럼 함수 안에서 매개변수 pa를 배열처럼 사용하여 2차원 배열의 값을 출력한다.

2차원 배열 요소의 두 가지 의미

2차원 배열에서 배열 요소

논리적으로는 1차원의 부분배열(행)을 뜻하고
물리적으로는 실제 데이터를 저장하는 부분배열의 요소를 뜻한다.

profile
분석하는 남자 💻

1개의 댓글

comment-user-thumbnail
2022년 10월 29일

귀엽누

답글 달기