C언어 문법 - 포인터(Pointer)

dev-jjun·2021년 9월 10일
post-thumbnail

Python과 결이 매우 다른 C언어.. 그냥저냥 할 만했지만, 이제는 어렵다.
바로 '포인터'의 개념이 처음 등장했기 때문.

그래서 Pointer에 대해 공부한 내용을 정리하고자 글을 썼다.

포인터(Pointer)란?

프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간 주소를 가리키는 변수

포인터가 가리키는 값을 가져오는 것을 '역참조'라고 한다.

그렇다면 포인터는 언제 사용할까?

포인터 변수 선언

메모리 주소를 변수에 저장하고자 할 때 선언하는 변수가 포인터 변수이다.

자료형 *포인터이름;
포인터 = &변수
의 형태로 사용한다.
이때 *(Asterisk, 애스터리스크)의 위치에 따른 차이는 없다.

특정 변수의 메모리 공간을 가리키므로, 그 변수의 자료형을 따르고 포인터의 자료형과 변수의 자료형은 서로 다르다.
ex. int -> pointer to int(int*)

역참조(dereference)

포인터로 선언한 변수 앞에 *을 붙여서 사용하면 해당 메모리 주소에 접근하여 공간에 존재하는 값을 가져올 수 있다.

또한, 값을 가져오는 것 뿐만 아니라 다른 값으로 저장해 변경할 수도 있다.

'*(이하 ☑로 표시)'의 역할
-> 포인터 선언 시, 이 변수가 포인터임을 알려주는 신호
ex> int☑ numPtr, int ☑numPtr, int numPtr☑
-> 포인터에 사용할 때, 포인터의 메모리 주소를 역참조 하겠다는 신호
ex> ☑numPtr

(☑: Asterisk, 애스터리스크)

차이점: 변수 선언 시 필요한 자료형이 있는가
_
int형과 int 포인터형은 서로 다른 자료형이므로, 주소연산자 '&'를 붙여주거나 int ☑numPtr;에서 ☑numPtr처럼 역참조해야 한다.
(이유: num1과 numPtr의 자료형을 하나로 통일시키기 위해서)

  • pointer to int = address of int (자료형)

'&'은 메모리 주소를 가리키고, 포인터는 메모리 공간을 가리키므로 포인터 변수를 선언한 뒤 특정 변수에 대한 메모리 주소를 할당하면 그 변수의 값을 조작할 수 있게 된다.

즉, 포인터 변수에는 메모리 주소가 저장되어 있고, 메모리 주소가 있는 곳으로 이동하여 값을 가져오고 싶을 때 '역참조(dereference)'를 사용한다.

#include <stdio.h>

int main()
{
    int* numPtr;      //포인터 변수 선언
    int num1 = 10;

    numPtr = &num1;   //&로 변수의 주소를 구해서 포인터 변수에 저장

    *numPtr = 20;     //역참조 연산자로 메모리 주소에 접근하여 다른 값을 저장하면 num1의 값도 바뀜

    printf("%p\n", numPtr);
    printf("%p\n", &num1);    //공간에 저장된 값을 바꾸었으므로 numPtr, num1 모두 똑같이 바뀐 메모리 주소를 출력한다. 
    printf("%d\n", *numPtr);  //역참조 연산자로 메모리 주소에 접근하여 값을 가져옴

    return 0;
}

변수, 주소 연산자, 역참조 연산자, 포인터의 차이

포인터에 저장된 메모리 주소 확인 방법

메뉴의 디버그(D) > 창(W) > 메모리(M) > 메모리 1(1) 클릭

numPtr 위에 마우스 커서를 가져다 놓으면 포인터에 저장된 메모리 주소가 나오고, 메모리 주소를 선택하고 복사해서 메모리 창에 붙여넣기 하면 메모리 주소의 내용을 확인할 수 있다.

역참조 후에는?

메모리 주소는 변하지 않고, 메모리 공간에 있는 내용만 바뀌어서 바뀐 내용이 빨간색으로 처리된다.

다양한 자료형의 포인터

자료형마다 포인터를 다르게 선언해야 하는 이유는,

포인터에 저장되는 메모리 주솟값은 정수형이지만, 선언하는 자료형에 따라 크기가 달라져 메모리에 접근하는 방법도 달라지기 때문이다.

상수를 가리키는 키워드 'const'를 붙이면, 포인터 자체가 상수가 되므로 역참조로 값을 변경하는 것은 불가능하다

  • 자료형이 정해지지 않은 'void 포인터'
    포인터 자료형이 달라도 컴파일 경고가 발생하지 않는다. 어떤 자료형의 포인터든 모두 저장할 수 있고, 다양한 자료형의 포인터에 void 포인터를 저장할 수 있으므로 범용 포인터라고도 한다.

BUT, void 포인터는 역참조를 할 수 없다.
실제로 존재하는 메모리 주소라면 포인터에 직접 저장할 수 있지만, 아닐 시에는 실행 에러가 뜬다.

이중 포인터

int ** numPtr2; pointer to pointer to int

  • int *numPtr1; 선언 후 사용
  • numPtr2->numPtr1->num1

포인터를 선언할 때 *를 2번 사용한 것이 이중 포인터이다.
포인터의 포인터를 선언한 것이며, 역참조 할 때 역시 역참조 연산자를 2번 사용해야 한다.


포인터에 원하는 만큼 메모리 공간을 할당받아 사용하는 방법

malloc -> 사용 -> free 패턴

  • malloc(memory allocation)
    malloc 함수로 원하는 시점에 원하는 만큼 메모리를 할당할 수 있는 동적 메모리 할당을 하여 메모리 공간의 크기를 넣어준다. malloc 함수는 힙에서 메모리를 할당하므로 반드시 해제를 따로 해주어야 메모리 사용량이 급증하지 않는다. *일반 변수는 스택에서 할당하므로 해제를 하지 않아도 상관 X

  • free
    동적으로 할당한 메모리를 해제하는 함수

  • memset(memory set)
    memset(포인터, 설정할 값, 크기);
    메모리의 내용을 원하는 크기(byte 단위)만큼 특정값으로 설정할 수 있다. 주로 설정할 값을 0으로 지정하여 메모리의 내용을 모두 0으로 만들 때 사용한다. *크기 지정 시 sizeof 사용

#include <stdio.h>
#include <stdlib.h>   // malloc, free 함수가 선언된 헤더 파일
#include <string.h>   // memset 함수가 선언된 헤더 파일

int main()
{
    int num1 = 20;
    int* numPtr1;
    long long *numPtr3 = malloc(sizeof(long long));

    numPtr1 = &num1;
    
    memset(numPtr3, 0x27, 8);   // numPtr3가 가리키는 메모리를 8byte만큼 0x27로 설정

    int* numPtr2;

    numPtr2 = malloc(sizeof(int));   // int의 크기인 4바이트만큼 동적 메모리 할당

    printf("%p\n", numPtr1);
    printf("%p\n", numPtr2);
    printf("0x%llx\n", *numPtr3);

    free(numPtr2);   // 동적으로 할당한 메모리 해제(해제하지 않으면 메모리 사용량이 계속해서 증가하여 메모리 누수현상 발생)
    free(numPtr3);

    return 0;
}

실행할 때마다, 컴퓨터마다 메모리 주소는 달라진다.

profile
서버 개발자를 꿈꾸며 성장하는 쭌입니다 😽

0개의 댓글