[C언어] 포인터의 이해

김민정·2024년 8월 12일
0
post-thumbnail

Chapter 12. 포인터의 이해

12-1 "포인터란 무엇인가?"

포인터 변수란? 정수 형태의 주소 값을 저장하는 목적으로 선언되는 변수다.
변수는 메모리 상에서 아래 그림과 같이 저장된다. (나란히 할당되지 않을 수도 있다.)

👉

1바이트의 메모리 공간을 단위로 하나의 주소 값이 할당되며 주소 값도 1씩 증가한다.
참고로, 포인터는 변수 형태의 포인터와 상수 형태의 포인터 모두를 어우르는 표현이다.
포인터와 관련된 이야기의 대부분이 포인터 변수와 관련이 있으므로 포인터라 하면 우선 포인터 변수를 연상하면 된다.

포인터 변수와 & 연산자

포인터 변수의 선언방법과 변수의 주소 값을 얻을 때 사용하는 & 연산자는 이따가 더 자세하게 배우겠지만 간단한 설명을 통해 1차적 이해를 해보자.

"정수 7이 저장된 int형 변수 num을 선언하고 이 변수의 주소 값 저장을 위한 포인터 변수 pnum을 선언하자, pnum에는 num의 주소 값을 저장하자."
이 문장을 코드로 작성하면 아래와 같다.

int main()
{
	int num = 7;
    int *pnum;		// 포인터 변수 pnum의 선언
    pnum = #	// num의 주소 값을 포인터 변수 pnum에 저장
	printf("%d %d"\n, num, pnum);
    ...
}

> 출력
10 -133171196

코드를 보면 알 수 있듯이 포인터 변수 선언과 & 연산자의 사용은 아래와 같다.

  • pnum : 포인터 변수의 이름
  • int * : int형 변수의 주소 값을 저장하는 포인터 변수의 선언
  • &연산자 : 오른쪽에 등장하는 피연산자의 주소 값을 반환하는 연산자

이 상황을 그림으로 표현하면 아래와 같다.

포인터 변수 pnum에는 변수 num의 시작번지 주소 값이 저장된다. 따라서 포인터 변수 pnum이 int형 변수 num을 가리킨다 라고 표현할 수 있다.

포인터 변수의 크기는 시스템에 따라 다르다.
32비트(4바이트) 시스템에서는 주소 값을 32비트로 표현하기 때문에 포인터 변수의 크기가 4바이트이고,
64비트(8바이트) 시스템에서는 주소 값을 64비트로 표현하기 때문에 포인터 변수의 크기가 8바이트다.
따라서 주소 값의 크기와 포인터 변수의 크기가 동일하다.

포인터 변수 선언하기

포인터 변수는 가리키고자 하는 변수의 자료형에 따라서 선언하는 방법이 달라진다.
사실 주소 값은 동일한 시스템에서 그 크기가 동일하면 모두 정수의 형태를 띈다.

  • 포인터 변수 선언의 기본 공식
    type * ptr;
    : type형 변수의 주소 값을 저장하는 포인터 변수 ptr의 선언

포인터 변수의 선언형태만 보고도 이 포인터가 현재 가리키는 변수의 자료형을 짐작할 수 있다.

포인터의 형(type)

int *, char *, double * 등을 가리켜 포인터 형(type)이라 한다.

// 포인터의 형(type)
type *		// type형 포인터
type * ptr;	// type형 포인터 변수 ptr

사실 *연산자의 위치는 상관이 없다.
type * ptr, type* ptr, type *ptr 모두 가능하다.


12-2 "포인터와 관련 있는 연산자: & 연산자와 * 연산자"

일반적으로 &연산자*연산자포인터 연산자라 한다.

&연산자

&연산자는 피연산자의 주소 값을 반환하는 연산자이다.

int main (void)
{
	int num = 5;
    int * pnum = #	// num의 주소 값을 반환해서 포인터 변수 pnum을 초기화
}

&연산자는 피연산자의 변수여야하며, 상수는 피연산자가 될 수 없다.
그리고 변수의 자료형에 맞지 않는 포인터 변수의 선언은 문제가 될 수 있다.
int형 변수 대상의 &연산의 반환 값은 int형 포인터 변수에,
double형 변수 대상의 &연산의 반환 값은 double형 포인터 변수에 저장한다.

*연산자

*연산자는 포인터가 가리키는 메모리 공간에 접근할 때 사용하는 연산자다.

int main (void)
{
	int num = 10;
    int * pnum = #
    *pnum = 20;		// pnum이 가리키는 변수에 20을 저장.
    printf("%d", *pnum);
}

위에서 *pnum에 새로운 값을 저장하는데 이는, 포인터 변수 pnum이 가리키는 메모리 공간인 변수 num에 정수 20을 저장하는 것이다.

예제를 통해 프로그램의 흐름에 대해 이해해보자.

#include <stdio.h>
int main()
{
	int num1 = 100, num2 = 100;
	int* ptr;

	ptr = &num1;
	(*ptr) += 30;

	ptr = &num2;
	(*ptr) -= 30;

	printf("num1: %d,\nnum2: %d \n\n", num1, num2);
	return 0;
}

> 출력
num1: 130,
num2: 70

여기서 중요한 것은 pnum이 가리키는 대상이 num1에서 num2로 변경되었다는 것이다.

그렇다면 아래와 같은 상황은 어떨까?

int main(void)
{
	int num=10;
    int * ptr1 = &num;
    int * ptr2 = ptr1;
    
    (*ptr1)++;
    (*ptr2)++;
    printf("%d \n", num);
    return 0;
}

> 출력
12

위 같은 상황은 ptr1과 ptr2가 동시에 num이라는 변수를 가리키게 된다~
따라서 12가 출력이 되는 것!

그리고 일반 변수와 포인터 변수를 하나의 문장 안에서 동시에 선언하는 것도 가능하다.
ex) int num, * pnum;
그러나 변수 형이 다르면 당연히 문장을 달리해 선언해주는 것이 좋다.

다양한 포인터 형이 존재하는 이유

포인터에 다양한 형이 존재하는 이유는 뭘까?
포인터 변수를 return문을 통해 반환하려고 해보자.
return *pnum;
pnum에 저장된 주소를 시작으로 몇 바이트를 읽어 들여야하는지, 읽어 들인 데이터는 정수로 해석해야하는지, 실수로 해석해야하는지 포인터 형이 메모리 공간을 참조(접근)하는 기준이 되기 때문이다.
포인터에 형이 존재하지 않다면 *연산을 통한 메모리의 접근이 불가능하다.

잘못된 포인터의 사용과 널 포인터

포인터 변수에는 메모리의 주소 값이 저장되고, 이를 이용해서 해당 메모리 공간에 접근도 가능하기 때문에 포인터와 관련해서는 상당히 주의해야한다.

  • 포인터 잘못 사용 예시 1) 포인터 변수를 선언만 하고 초기화 하지 않은 경우
    : 포인터 변수는 쓰레기 값으로 초기화 된다.
    int main (void)
    {
        int * ptr = 125;	// 125번지가 어딘지 알고,,,?
        *ptr = 10;
        ...
    }
    따라서 포인터 변수를 선언만 해놓고 이후에 유효한 주소 값을 채워 넣고 싶다면 아래와 같이 초기화 해주는 것이 좋다.
    int main (void)
    {
    	int * ptr1 = 0;
      int * ptr2 = NULL;
      ...
    }
    초기화 하는 값 0을 가리켜 널 포인터라고 한다. 0번지가 아닌 '아무데도 가리키지 않는다.'라는 의미이다!

<Review>

드디어 c언어를 많이 포기하게 만든다는 포인터에 대해서 배웠다.
사실 처음 배우면 포인터가 굳이 왜 필요하지? 란 생각을 하게 된다.
하지만 이 후에 포인터와 관련된 더 많은 것들을 배우게 되면 생각이 바뀔 것이다 ㅎㅎㅎ
앞으로도 화이팅~

(요즘 꽂힌 귀여운 웜뱃 사진으로 마무리🥰)

profile
백엔드 코린이😁

0개의 댓글

관련 채용 정보