본문 바로가기
언어/C언어

[C 언어] 10.포인터

by 천무지 2024. 4. 14.
반응형

(찾는 내용이 있으시다면 Ctrl + F를 눌러 원하는 내용을 찾아주세요.)

포인터

포인터는 주소값을 저장하기 위한 변수입니다.

포인터의 선언은 다음과 같이 합니다.

int *x;
int a;
x = &a

이렇게 선언한다면 데이터형이 int인 포인터형을 선언한 것입니다.

포인터는 주소를 저장하는 변수이기 때문에 변수의 주소를 반환하는 연산자 &를 사용하여 변수의 주소값을 할당합니다.

*은 참조 연산자로 포인터가 가리키는 주소에 저장 되어있는 값을 참조합니다.

이때의 *은 포인터를 선언했을 때의 *와는 다릅니다.

포인터 변수의 크기는 사용하는 컴파일러마다 다릅니다.

개발환경이 32비트면 포인터의 크기도 32비트, 64비트이면 64비트 입니다.

 

포인터 사용의 예시를 보겠습니다.

#include <stdio.h>

int main(void) {
   int a = 100;
   int *ptr = &a;
   printf("a: %d\n", a);
   printf("*ptr: %d\n", *ptr);
   printf("&a: %p\n", &a);
   printf("ptr: %p\n", ptr);
   (*ptr)++;
   printf("%d\n", a);
   printf("%d\n", *ptr);
}

100이라는 숫자를 출력하려면 a나 포인터 변수에 참조연산자가 붙은 *ptr을 사용합니다.

만약 a의 주소를 알고 싶다면 &a나 포인터 변수 ptr 자체를 사용합니다.

포인터의 주소를 출력할깨 형식지정자는 %p로 봅니다.

만약 32비트면 %u, 64비트면 %llu로 해도 좋습니다.

 

포인터의 사용예를 보았으므로 잘못 사용하는 상황도 보겠습니다.

#include <stdio.h>
void main(void) {
   int *ptr;	//points to a random address
   *ptr = 10;	//access to a random address
}

위와 같이 하면 안됩니다. 포인터변수 ptr이 어떤 변수의 주소를 가리키는 지도 모르는데 참조연산자를 이용해서 접근하면 안됩니다.

실제로 포인터 변수를 초기화 하지 않으면 임의의 메모리 공간을 가리킬 수도 있기 때문에 이런식의 접근은 메모리를 손상시킬 가능성이 있습니다.

 

#include <stdio.h>
void main(void) {
   int *ptr = 10;
   ptr = 20;
}

또한 주소값을 사용자가 임의로 할당하면 안됩니다.

사용자가 할당하는 값이 실제로 이미 사용하고 있는 메모리 공간일 수 있기 때문입니다.

따라서 메모리의 접근은 OS가 허용하는 주소에만 접근이 가능합니다.


배열과 포인터

배열과 포인터는 공통점이 있습니다.

주소값을 가진다는 것, 그리고 인덱싱과 포인터 연산이 가능하다는 점입니다.

차이점이 있다면 배열과 포인터는 크기가 다르고 배열의 이름은 상수라는 것입니다.

배열을 상수포인터라고도 합니다.

 

배열의 이름은 배열의 첫번째 원소의 주소입니다.

따라서 a는 a[0]의 주소를 가리키는 포인터입니다.

실제로 a와 a[0]의 주소를 출력하면 같은 값이 나옵니다.

 

배열과 포인터의 인덱싱예시를 보겠습니다.

#include <stdio.h>

void main(void) {
  int a[5] = { 0, 1, 2, 3, 4 };
  int *ptr;
  ptr = a;
  printf("%d, %d, %d\n", ptr[0], ptr[1], ptr[2]);
}

포인터 변수 ptr에 a를 할당하면 ptr은 a[0]의 주소를 가리키게 됩니다.

이후에 ptr은 배열처럼 인덱싱이 가능해집니다.

포인터도 배열과 같이 인덱싱을 사용할 수 있는 것입니다.

 

포인터는 연산도 가능합니다.

일반적으로 더하기 빼기만 가능합니다.

여기서 더하기 빼기는 단순한 +1과 -1같은 것을 의미하는 것은 아닙니다,

포인터 변수 p를 어떤 자료형에 대한 포인터라고 하면 p + 1은 해당 자료형의 다음 포인터를 의미합니다,

자료형에 따라 연산되는 값이 차이가 나는 것입니다.

 

예시를 들어보겠습니다.

#include <stdio.h>
void main(void) {
	int *p1 = 0;
	char *p2 = 1;
	double *p3 = 2;
	printf("%d, %d, %d\n", p1, p2, p3);
	printf("%d, %d, %d\n", ++p1, ++p2, ++p3);
}

<예시로 들기위해서 주소값을 직접 정했습니다. 실제로는 이렇게 하면 안됩니다.>

포인터가 가리키는 주소를 각각 int는 0, char는 1, double은 2로 했습니다.

이후에 증감연산자를 통해 포인터가 가리키는 주소값을 +1씩 했습니다.

여기서 단순이 1, 2, 3이 되는 것이 아니라 데이터 타입의 크기만큼 커지는 것입니다.

int 는 4바이트, char은 1바이트, double은 8바이트이기 때문에 결과적으로 4, 2, 10이 출력됩니다.

 

연산을 하고나서도 인덱싱을 할 수 있으며, 참조연산자를 통해서 값에 접근할 수 있습니다.

#include <stdio.h>
void main(void) {
  int a[5] = { 1, 2, 3, 4, 5 };
  int *p = a;
  printf("%d\n", *p);
  printf("%d\n", *(p + 1));
  printf("%d\n", *(p + 2));
  printf("%d\n", *(p + 4));
  printf("%d, %d\n", a[1], *(a + 1));
  printf("%d, %d\n", p[1], *(p + 1));
}

해당 예시를 통해서 알 수 있는 사실은 a[i] 는  *(a+i) 와 동일하다는 것입니다.

 

1차원 배열뿐만 아니라 2차원 배열도 포인터 연산이 가능합니다.

#include <stdio.h>
void main(void) {
   int table[3][4];
   int i,j;
   for (i = 0; i < 3; i++) {
      for (j = 0; j < 4; j++) {
         table[i][j] = i * j;
      }
   }
   printf("%d \n", *(*(table + 1) + 3));
   printf("%d \n", table[1][3]);
}

여기서 *(table + 1)은 table 주소에 16을 더하는 겁니다. 이유는 table 배열의 행이 4칸씩 가지고 있고 int 형이기 때문에 16바이트를 더합니다.

즉, 행을 1칸 움직인것입니다.

결과적으로 *(table + 1)은 table[1][0]의 주소를 가리키게 됩니다. [table + 1과 *(table + 1)모두 table[1][0]의 주소를 의미합니다.]

이제 *(table + 1)에서 +3을 할 차례 입니다. 

여기서 +3은 배열을 행렬로 봤을 때 열의 움직임을 나타냅니다 따라서 int 형이 3칸 움직였기때문에 12바이트를 이동합니다.

마지막으로 참조 연산자를 달아주면 *(*table + 1) + 3)은 table[1][3]을 의미합니다.

제가 방금 [table + 1과 *(table + 1)모두 table[1][0]의 주소를 의미합니다.] 라고 말씀을 드렸습니다.

그러면 *(*table + 1) + 3)도 *((table + 1) + 3)와 같냐고 물어보시면 다릅니다. 현재 후자의 제일 바깥의 괄호 안을 보면 결국 +1 + 3해서  + 4가 되는것을 볼 수 있습니다.

따라서 행과 열을 어떤 식으로 이동할 건지 결정하기 위해서는 구별이 필요하기때문에 전자를 써주어야 합니다.

 

배열을 포인터로 설정할 수도 있습니다.

포인터형 배열을 만드는 것입니다.

바로 예시로 살펴보겠습니다.

#include <stdio.h>
void main(void) {
   int a = 1, b = 2;
   int *a1[3] = { &a, &b};
   printf("%d\n", *a1[0]);
   printf("%d\n", *a1[1]);
   char* a2[2] = {"J", "love" };
   printf("%s\n", a2[0]);
   printf("%s\n", a2[1]);
}

a1과, a2를 포인터형 배열로 선언했고 배열은 주소값를 저장합니다.

여기서 눈여겨 볼 점은 char로 선언한 포인터입니다. 문자열은 가장 맨 앞의 문자의 주소를 주소값으로 가지고 맨 끝에는 널 문자를 가집니다.

printf("%s\n", a2[0]);

printf("%s\n", a2[1]);

를 통해 출력할 때 주소연산자를 안쓴 이유입니다.

 

이전에 Call by value를 살펴봤었습니다. 포인터를 이용하면 함수의 호출을 이용해 실제로 두 값을 바꿀 수 있습니다.

코드를 먼저 보겠습니다.

#include <stdio.h>
void swap(int *x, int *y);
void main(void) {
   int a = 3;
   int b = 4;
   int *p1 = &a;
   int *p2 = &b;
   printf("%d, %d \n", a, b);
   swap(p1, p2);
   printf("%d, %d \n", a, b);
}
void swap(int *x, int *y) {
   int temp;
   temp = *x;
   *x = *y;
   *y = temp;
}

매개변수로 값을 전달할때 주소값을 전달했습니다. 따라서 함수안에서는 복사된 주소값을 통해 값을 참조하여 두 값을 서로 바꿀 수 있었던 것입니다.

제가 예전에 올렸던 글을 참고하시면 도움이 될 것 같습니다.

https://shu07002.tistory.com/entry/C%EC%96%B8%EC%96%B4-call-by-value-call-by-address-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

 

[C언어] call by value, call by address 알아보기

안녕하세요. 복학하고 바빠서 오랜만에 글을 쓰네요. 오늘 알아볼것은 call by value, call by address 입니다. 이것들은 함수의 매개변수에 어떤값을 넣는가에 대한 개념입니다. 순서대로 대략적인 의

shu07002.tistory.com

 

함수의 인자가 배열일 때는 배열 원소의 개수 정보는 사용자가 직접 인자로 넘겨야합니다.

배열의 시작 주소는 알 수 있지만 배열이 어디에서 끝나는지 함수는 알 수 없기 때문입니다.

#include <stdio.h>
int sumArr(int *, int);
void main(void) {
   int a[] = { 1, 2, 3, 4, 5 };
   int sum;
   printf("%d \n", sumArr(a, 5));  // 5대신 sizeof(arr) / sizeof(int) 사용권장
}
int sumArr(int *ptr, int n) {       //인자로 사용시 int *ptr <==> int ptr[]
   int i = 0;
   int sum = 0;
   for (i = 0; i < n; i++) {
      sum += ptr[i];
   }
   return sum;
}

 

2차원 이상의 배열을 인자로 넘길때도 주의가 필요합니다.

#include <stdio.h>
void printArray(int(*ptr)[3], int a);
void main(void) {
   int a[2][3] = { 1,2,3,4,5,6 };
   printArray(a, 2);
   printf("%d\n", a);
   printf("%d\n", a+1); //12바이트가 증가 
   int(*p)[3] = a; 
   printf("%d\n", p);
   printf("%d\n", p+1);
   printf("%d\n", sizeof(a));
   printf("%d\n", sizeof(p));
}
void printArray(int(*ptr)[3], int a) { // int(*ptr)[3] <==> int ptr[][3]
   int i, j;
   for (i = 0; i<a; i++) {
      for (j = 0; j<3; j++) 
         printf("%d", ptr[i][j]);
      printf("\n");
   }
}

2차원 배열을 포인터로 할당할때는 int (*p)[i] = a; 처럼 써줍니다.

배열의 제일 상위 인덱스는 몇인지 함수의 인자로 전달해주어야 합니다. 나머지 하위 인덱스의 숫자는 직접 써줍니다.

다시 정리하면 다음과 같습니다.

그리고 매개변수에 쓸 때는 int (*p)[i] 로 써줍니다.

 

이중포인터는 포인터가 가리키는 값이 포인터인 상황을 말합니다.

#include <stdio.h>
void pswap(int **p1, int **p2);
void main(void) {
   int a = 10, b = 20;
   int *pa, *pb;
   int **ppa, **ppb;
   pa = &a, pb = &b;
   ppa = &pa, ppb = &pb;
   printf("pa가 가리키는 변수: %d \n", *pa);
   printf("pb가 가리키는 변수: %d \n", *pb);
   pswap(ppa, ppb);
   printf("pa가 가리키는 변수: %d \n", *pa);
   printf("pb가 가리키는 변수: %d \n", *pb);
}
void pswap(int **p1, int **p2) {
   int *temp;
   temp = *p1;
   *p1 = *p2;
   *p2 = temp;
}

이해를 돕기 위해 한가지 경로만 먼저 살펴보겠습니다. ppa는 pa의 주소를 저장하고 pa는 a의 주소를 저장합니다.

처음에 출력되는 부분은 우리가 생각한 것처럼 나올 것입니다. 문제는pswap 함수를 수행하고난 다음이 궁금하다는 것입니다.

pswap으로 ppa가 전달됩니다. ppa는 이중포인터이고 pa의 주소값을 저장하고 있습니다. 호출된 함수에서 ppa는 p1이라는 이름으로 사용됙고 있습니다. 이중포인터 p1이 가리키는 주소값을 참조하면  pa의 값이 됩니다.

pa의 값은 a의 주소입니다.

교환을 한다면 pa의 a의 주소와 pb의 b의 주소의 교환이 일어나게 됩니다.

결과적으로 pa 는 b의 주소를 가리키고, pb는 a의 주소를 가리킵니다.

참조를 하게되면 결국 두 값이 바뀐 상태로 출력합니다.

반응형

'언어 > C언어' 카테고리의 다른 글

[C 언어] 12. File  (0) 2024.04.14
[C 언어] 11. Input/Output Stream, String  (0) 2024.04.14
[C 언어] 9. 함수  (0) 2024.04.14
[C 언어] 8. 배열  (0) 2024.04.14
[C 언어] 7. 제어문  (0) 2024.04.14