포인터 정리

강한친구·2022년 2월 10일

C / CPP

목록 보기
1/19

포인터란

우리가 컴퓨터 구조시간에 배웠던것처럼, 코딩 과정에서 선언하는 모든 변수는 메모리에 저장이 되고 그 메모리에는 주소형태로 올라가게 된다. 주소는 운영체제의 32비트 혹은 64비트에 맞춰서 작동하며 현재 대부분의 컴퓨터를 차지하는 64비트의 경우 64비트 8바이트 형태로 나타나게 된다.

포인터 역시 자료형을 가지며 연산자를 가지고 주소값을 표현한다

포인터 : 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수
포인터의 구조 : (포인터에 주소값이 저장되는 데이터의 형) (포인터의 이름);
(포인터에 주소값이 저장되는 데이터의 형)
(포인터의 이름); <-- 이와 같은 형태도 가능하지만 위에것으로 통일해서 쓰도록 하겠다.

예시 ) int *pa;

포인터의 연산자

*

  • 은 곱셈연산자이지만, 포인터를 의미하기도 한다.
    이를 사용하면 변수의 값을 이 주소에 들어있는 데이터값으로 고려하라는 뜻이다.
#include <stdio.h>
int main() {
	int a;
    int *pa;
    a = 2;
    
    pa = &a;
    
    printf("%d, \n", a);
    printf("%d \n", *pa);
    return 0;
}

다음과 같은 코드를 실행하면 일단
pa가 a의 주소를 지칭하게 되고, 이 a의 주소에는 2가 들어있다. 따라서 2가 출력되게 된다.

#include <stdio.h>
int main() {
	int a;
    int *pa;
    
    pa = &a;
    *pa = 3;
    
    printf("%d, \n", a);
    printf("%d \n", *pa);
    return 0;
}

이런식으로 작성하는것도 가능하다. 이는 pa포인터에 저장된 주소의 값을 3으로 해달라는 요청이다.

&

and 연산자로 쓰이기도 하는 이 연산자는 피연산자의 주소값을 불러오는 역할을 한다.
예를 들어 변수 a의 주소값을 알고싶다면 &a; 처럼 사용하면 되는것이다.

#include <stdio.h>
int main() {
	int a;
    a = 2;
    
    printf("%p, \n", &a);
    return 0;
}

다음과 같은 코드를 수행한다면, a의 주소값이 16진수 형태로 출력되게 될 것이다.

이제 포인터에 값을 넣어서 포인터의 동작을 확인하겠다.

#include <stdio.h>
int main() {
	int a;
    int *pa;
    a = 2;
    
    pa = &a;
    
    printf("%p, \n", &a);
    printf("%p \n", pa);
    return 0;
}

포인터 pa에 a의 주소(&a)를 넣어주고 실행하면 당연히 둘이 같은 주소값을 가지게 된다.

포인터에 타입이 있는 이유

각 자료형마다 크기가 다르다는 사실은 흔히들 알고있다. 따라서 자료형을 포인터에 표시해서 몇바이트부터 찾아갈지 표시해주기 위함이다.

상수 포인터

C에서는 const라는 상수 데이터를 만들수 있다.

const int a = 3;

이렇게 만들어진 값은 절대! 무슨일이 있어도! 변경할수 없다는것도 알고있을것이다.

하지만 포인터에도 const가 붙는다은것을 알고있는가?

#include <stdio.h>
int main() {
	int a;
    int b;
    const int *pa = &a; // a의 주소를 상수 포인터 pa에 저장
    
    *pa = 3; // 상수포인터이기에 값을 바꿀수 없다.
    pa = &b; // 포인터 자체의 주소를 변경하는것은 가능하다. 
    
    printf("%d, \n", a);
    printf("%d \n", *pa);
    return 0;
}

보다싶이 const int *pa는 int형의 자료형을 받고, 그 값을 절대로 바꾸지 않는다는 의미ㅡㄹ 가진다.
하지만 const의 위치가 변하게 되면 조건이 또 달라진다.

#include <stdio.h>
int main() {
	int a;
    int b;
    int const *pa = &a; // a의 주소를 상수 포인터 pa에 저장
    
    *pa = 3; // 주소값만 변하지 않는다면 그 안에 들어있는 데이터는 변해도 된다. 
    pa = &b; // pa가 const가 되었기때문에 주소값을 변경하는것은 안된다. 
    
    printf("%d, \n", a);
    printf("%d \n", *pa);
    return 0;
}

만약 이 두개를 합쳐서

const int const pa = &a;

같은 문장을 쓴다면 이는 주소도 내부 데이터도 바꾸지 말라는 뜻이다.

포인터의 덧셈

포인터의 덧셈 개념을 이해하는데 가장 중요한것은 자료형의 크기이다.

#include <stdio.h>
int main() {
	int a;
    int *pa = &a; // a의 주소를 상수 포인터 pa에 저장
    
    printf("%p, \n", pa);
    printf("%p \n", pa + 1);
    return 0;
}

다음과 같은 코드를 실행하면 주소값이 두개가 나오게 되는데, 이 주소값을든 정확히 4차이나게 된다.

이와 같은 결과가 발생하는 과정은 다음과 같다.

pa에는 자기자신을 가르키는 주소값이 들어있다. 그리고 pa의 자료형은 int형 4바이트이다. 따라서 이 주소값이 1 늘어나면, 다음 위치인 4바이트 후라는것을 알수 있다. 이에 4가 증가한 주소값이 나오게 되는 것이다.

만약 double (8)이나 char(1)에 적용하면 다른 결과가 나오게 된다.

하지만 그렇다고 포인터끼리 더하는것은 가능하지 않다.
두 주소의 값을 더하면 물론 수학적인 계산은 가능하니 어떠한 주소값이 나오겠지만, 실제로 그 주소에는 뭐가 있을지 모르는 주소이다.
따라서 이 계산은 의미가 없는 계산이고 오류발생가능성이 높아서 포인터끼리의 합은 불가능한것이다.

하지만 뺄셈은 가능하다.

배열과 포인터

컴퓨터 구조시간에 배운것처럼 배열(int형 기준) 한 칸이 4바이트를 가진 공간이다. 따라서 배열 역시 포인터로 쓸 수 있는것이다.

#include <stdio.h>
int main() {
	int arr[5] = {1,2,3,4,5};
    int *parr = arr[0]; // arr; 도 가능 
    
    for (int i = 0; i < 5; i++) {
    	printf("%p ", &arr[i]); // arr[i]의 주소값 출력
        printf("%p ", parr + i); // parr의 주소값 출력 
    }
}

이 코드를 보면 알겠지만, arr[0] 혹은 arr은 해당 array의 시작 주소를 가진다는것을 알 수 있다. 따라서 이를 포인터 parr에 넣어주면, array 포인터의 완성이다.

더블포인터

&a를 가르키는 포인터를 가르키는 포인터이다. 이게 무슨 말인지 좀 의아할수도 있지만, 자료구조에서 자주 사용했던 포인터를 포인팅하는 개념을 이해하면 된다

#include <stdio.h>
int main() {
	int a = 2;
    int *pa = &a;
    int **ppa = &pa;
    
    printf("%d \n", a);
    printf("%d \n", *pa);
    printf("%d \n", **ppa);
    
	return 0;
}

이를 실행해보면 a, pa, ppa 전부 2라는 값을 가지고 있는것을 알 수 있다.
즉 a값이 있고 pa포인터는 a를 포인팅하면서 a의 값을 가지고 ppa는 a의 값을 포인팅하는 pa를 포인팅하는것이다.

이차원배열 포인터

이차원 배열은
arr = [n][m]형태를 가지고 있다.
주로 직사각형 형태로 표현하곤 하는데 이는 우리가 쓸때나 그렇게 쓰는것이고 실제로는 기존 배열처럼 쭉 나열되어 있는것이다.
a[0][0], a[0][1], a[1][0], a[1][1] 같은 방식이다.

#include <stdio.h>
int main() {
	int arr[3][3];
    
    printf("%p \n", arr[0]);
    printf("%p \n", arr[0][0]); 
    
    printf("%p \n", arr[1]);
    printf("%p \n", arr[1][0]); 
    
    return 0;
}

이를 실행해보면, arr[0]은 arr[0][0]과 같은 값이고, arr[1]은 arr[1][0]과 같다는것을 알 수 있다.

포인터를 이용하는 함수

#include <stdio.h>

int change_val(int *pi) {
    printf("addr of pi : %p \n", pi);
    printf("value of pi : %p \n", *pi);

    *pi = 3;
    return 0;
}

int swap(int *pa, int *pb) {
    int temp = *pa;
    *pa = *pb;
    *pb = temp;

    return 0;
}

int add_number(int *parr) {
    for (int i = 0; i < 3; i++) {
        parr[i]++;
    }
    return 0;
}

int main() {
    int i = 0;
    printf("addr of pi : %p \n", &i);
    printf("value of pi : %d \n", *i);
    change_val(&i);
    printf("value of pi : %d \n", i);

    int i, j;
    scanf("%d %d", &i, &j);
    swap(&i, &j);
    printf("%d, %d \n", i, j);

    int arr[3];
    int i;

    for (i = 0; i < 3; i++) {
        scanf("%d", &arr[i]);
    }

    add_number(arr);

    printf("배열의 각 원소 : %d, %d, %d", arr[0], arr[1], arr[2]);

    return 0;
}

두 값을 바꾸는 함수

만약 a, b의 값을 바꾸고자 한다면

#include <stdio.h>

int a = 1;
int b = 2;
int temp;
temp = b;
b = a;
a = temp;

이처럼 해결하려 할지도 모른다. 물론 이렇게 main함수 안에서 실행하면 값을 바꿀수는 있지만, 값을 바꿀때마다 이걸 쓸수는 없는 노릇이라 대부분 함수로 해결하게 된다. Python에서는 swap이라는 내장함수로 구현되어 있지만, c에서는 직접 구현해야한다. 다만 이를 함수로 구현하게되면, 이는 함수 안에 들어온 인자 a, b 에만 해당되는것이지, 실제 a, b 값에는 아무런 영향이 없다.

따라서 포인터를 이용해 직접 a, b값의 주소에 접근해서 변경해야한다.

#include <stdio.h>

int swap(int *pa, int *pb) {
	int temp = *pb;
    *pb = *pa;
    *pa = temp;
    return 0;
}

int main() {
	int i, j;
    scanf("%d %d", &i, &j);
    swap(&i, &j);
    printf("%d, %d \n", i, j);
}

위와 같이 i, j의 주소값을 pa pb포인터에 연결해주고 이 값들을 서로 바꿔주면 된다.

값을 바꾸는 함수

#include <stdio.h>

int change_val(int *pi) {
    printf("addr of pi : %p \n", pi);
    printf("value of pi : %p \n", *pi);

    *pi = 3;
    return 0;
}

int main() {
	int a = 3;
    change_val(&a);
}

위와 비슷한 원리로 변수 a의 주소를 넣어서 그 값을 직접 3으로 변경하면 된다.

array의 값 일괄 변경

int add_num(int *parr) {
  for (int i = 0; i < 3; i++) {
  	parr[i] += 3
  }
  return 0;
}
int main() {
	int arr[3];
    int i;

    for (i = 0; i < 3; i++) {
        scanf("%d", &arr[i]);
    }

    add_number(arr);

    printf("배열의 각 원소 : %d, %d, %d", arr[0], arr[1], arr[2]);

    return 0;
}

배열은 앞서 설명한것처럼 parr이 1씩 늘어날떄마다 다음 배열로 이동하는 개념이기에 parr 포인터에 arr의 시작점의 주소 &arr[0] 혹은 arr을 넣어주고 계산하면 된다.

마치며

포인터를 처음 배울 때는 도대체 이걸 왜 배우는거지? 이걸 어디에 쓰지 싶지만, 컴퓨터 구조를 알고, C언어의 특성을 알고나면 왜그런지 알수있게 된다. 열심히 해보자

0개의 댓글