C언어 포인터

프로그래밍/C 2019.04.17 댓글 Plorence

C언어의 꽃, 포인터(Pointer)

포인터는 C언어의 꽃이라고 할 수 있으며 정말 많이 쓰입니다.
변수는 메모리 영역에 저장돼있고 메모리 주소는 이 변수가 메모리 영역 중 어디에 위치해있느냐를 나타냅니다.
이 메모리 주소를 가지고 포인터를 사용하여 읽기/쓰기가 가능합니다.
포인터 변수란 메모리의 주소 값을 저장하기 위한 변수입니다.

 

포인터의 개념

"포인터는 어렵다"는 써먹기 어려운 거지, 개념 자체는 매우 쉽습니다. 이점은 C언어의 다른 문법에도 어느 정도 해당하는 말입니다.
변수가 메모리에 할당될 때 어디에 위치해있냐를 알기 위해 특별한 연산자가 제공됩니다.
포인터는 이 위치 값을 참조하여 접근해서 읽기/쓰기를 하는 겁니다.
예로 하나 들어보자면 청와대라고 하면 정확히 위치가 어디인지 모르시는 분이 많을 겁니다.
그래서 특별한 연산자를 사용하여 청와대의 위치를 알아낸 후 접근한다고 보시면 됩니다. (주소를 모르면 지도 검색하시는 분이 대부분이잖습니까.)
이렇게 접근하게 도와주는 놈이 포인터입니다.

다른 예시를 들어볼까요?
포인터는 하나의 포탈(다리)라고 생각하면 됩니다.
이 포탈을 이용하려면 도착하려고 하는 위치가 반드시 필요합니다. (통행료는 공짜)
이 위치가 변수의 메모리 주소입니다.

포인터 변수의 크기는 4바이트도 될 수 있고, 8바이트도 될 수 있습니다.
32비트 시스템은 포인터 변수의 크기가 4바이트
64비트 시스템에서 포인터 변수의 크기가 8바이트입니다.

 

포인터 변수의 선언

type * ptr;

포인터 변수는 위와 같이 선언합니다.
자료형처럼 포인터형(type)이라는 게 있습니다.
만약 접근하려는 변수와 포인터형이 다르다면 에러가 발생합니다.

int * (int형 포인터)
double * (double형 포인터)
int *pnum; (int형 포인터 변수 pnum)
double* pnum; (double형 포인터 변수 pnum)

부를 땐 "'타입명' 포인터 변수 '변수명'"라고 부릅니다.
선언할 때 *위치는 상관없습니다.

 

포인터와 관련 있는 연산자

포인터와 항상 같이 따라다니는 연산자는 *연산자와 &연산자가 있습니다.
&연산자는 많이 보셨을 겁니다.

&연산자

&연산자의 피연산자는 반드시 변수여야 하며 변수의 주소 값을 반환하는 연산자입니다.
청와대의 정확한 위치를 몰라서 지도를 보듯이,&연산자를 사용하여 해당 변수의 주소를 가져옵니다.

#include <stdio.h>
int main (void){
    int num = 5;
    int * ptr = &num;
    
    return 0;
}

&연산자를 사용하여 변수 num의 주소값을 반환하여 int형 포인터 변수 ptr에 대입하는 코드입니다.
이때 int형 말고 다른 타입을 대상으로 할 수는 없습니다.
이미 가리키는 대상의 타입이 int형이라고 선언하였기 때문입니다.
그래서 &연산자의 피연산자 타입과 포인터형이 일치하지 않으면 컴파일 에러가 발생하게 됩니다.

 

*연산자

*연산자는 해당 포인터 변수가 가지고 있는 주소 값(메모리 공간)에 접근할 때 사용하는 연산자입니다.
읽기 쓰기 모두 가능하며 ++,--,+=,-=,*,/ 같은 연산자는 모두 사용이 가능합니다.
(아까도 말했지만 포탈 역할을 하기 때문에 접근해서 읽기/쓰기만 하는 거지 나머지 처리는 개발자 스스로 몫입니다.)

#include <stdio.h>
int main (void){
    int num = 5;
    int * pnum = &num;
    *pnum = 100;
    return 0;
}

*연산자를 통해 int형 변수 num에 접근하고 100을 대입했습니다.
즉 포인터 변수가 가지고 있는 주소 값에 해당하는 변수에 접근한다고 보시면 됩니다.
만약 해당 주소 값이 읽기/쓰기가 불가능한 영역이나 할당이 안된 영역이라면 런타임 에러가 발생합니다.
컴파일 타임에서 검사를 안 하거든요.

 

포인터형이 존재하는 이유

*연산자를 통한 메모리 공간의 접근 기준을 마련하기 위함입니다.
포인터형은 메모리 공간을 참조하는 기준이 됩니다.
pnum는 int형 포인터 변수이니 int형 변수의 주소 값을 저장할 수 있습니다.
(pnum이 int형 포인터 변수이므로 pnum에 저장된 주소를 시작으로 4바이트로 읽어 들여서 이를 정수로 해석해야 합니다.)

포인터 변수는 일반 변수와 마찬가지로 선언과 동시에 초기화하지 않으면 쓰레기 값을 가집니다.
포인터 변수를 우선 선언만 해놓고 이후에 유효한 주소 값을 채워 넣을 생각이면 
int * pnum1 = 0; 또는
int * pnum2 = null; (null은 사실상 0을 의미)

위에서 pnum1을 초기화하는 값 0을 가리켜 널 포인터(Null Pointer)라고 합니다. 0번지를 의미하는 게 아니라 아무 데도 가리키지 않는다 라는 의미입니다.

 

포인터 변수의 장점(쓰임새)

포인터 변수는 다른 함수에 있는 지역변수에 접근이 가능합니다.
일반적으로 지역변수는 선언된 함수 내에서만 접근이 가능했는데
포인터 변수로 접근하면 어디서든지 접근이 가능하다는 것입니다.

왜냐하면 변수의 주소 값을 알고 있으니까요.

#include <stdio.h>
int Function2(int A) {
       A = 200;
}
int Function1(int * ptr_A) {
       *ptr_A = 100;
}
int main(void) {
       int A = 0;
       Function1(&A);
       Function2(A);
       return 0;
}

포인터는 C언어에서 대부분 이 목적으로 사용합니다.
A가 마지막에 200이 될지 안 될지 생각해보세요. 또한 A는 어떤 값을 가지게 될지 생각해보세요.

풀이

아까도 말했듯이 포인터는 가지고 있는 주소에 해당하는 변수의 자료형과 일치하고 그 주소를 알고 있으면 접근해서 읽기/쓰기가 가능합니다.
Function1 함수를 호출할 때 &연산자를 사용하여 변수 A의 주소 값을 첫 번째 인자로 보냅니다.
함수 내부에서 *연산자를 사용하여 해당 주소에 접근해서 100을 대입합니다.

반면에 Function2 함수는 인자로 변수 A의 값인 0을 전달합니다.
그렇게 Function2 함수의 매개변수 A는 0으로 초기화됐습니다. 이때 매개변수 A와 main함수에 있는 A와 다릅니다.
그래서 쓰기를 한다고 해서 main함수에 있는 변수 A에는 영향이 없습니다.
인자 전달에 대한 건 call by [reference, address, value]을 찾아보시면 됩니다.

댓글