본문 바로가기
C

포인터

by stdlib.h 2016. 3. 31.

포인터란?


메모리의 한 지점, 간단히 말해 번지값을 가지는 변수이다.

어떠한 형태의 변수던지(register형만 제외하고) 반드시 메모리에 보관되며 모든 메모리는 번지를 가지고 있다.

따라서 이 변수의 번지를 가르키는 포인터 변수를 항상 선언할 수 있다.


int, char , double 등 기본적인 데이터 타입에 대하여 int *, char *, double* 형의 변수를 선언할 수 있음은 물론이고, 공용체, 구조체, 배열에 대해서 포인터형을 만들 수도 있다.

사용자가 직접 만든 타입에 대해서도 포인터형 변수를 선언할 수 있으며, 심지어는

포인터 타입에 대해서도 포인터를 선언할 수 있다.



포인터를 선언할때는

가르킬타입 *(에스터리크스) 포인터변수의 이름;

이렇게 선언한다.

int* ptr; 도 가능하고 int *ptr; 도 가능하다.

또한 int * ptr; 식도 가능하다.

C언어는 프리포맷을 지원하기 때문이다.


하지만 주로 int* ptr; 이나 int *ptr; 이 사용된다.


포인터가 가르키는 번지에 들어있는값, 즉 포인터가 가리키는 실체를 대상체 라고 한다.

예를들어 int 형 포인터가 가르키는 대상체는 int형 변수이며, double형 포인터가 가르키는 대상체는 double 형 변수이다.

*연산자로 포인터가 가르키는 곳을 읽으면 포인터의 대상체에 저장된 값이 읽어질것이다.



포인터가 저장하는 번지값은 모두 4바이트 크기로 고정되어있고, 이변수에 저장될 값은 항상 부호없는 정수형이다.

또한, 포인터변수는 4바이트이며, 번지는 0 을 포함한 양수값이다.

포인터 변수가 정수를 가르키든, 실수를 가르키든, 배열을 가르키든, 포인터변수는 항상 4바이트의 부호없는 정수값이다.


포인터변수의 크기가 정해져있는데 왜, char* 이나 dobule* 처럼, 대상체의 데이터형을 밝혀야 할까?


그 첫 번째 이유는 , *연산자로 포인터의 대상체를 읽거나 쓸때 대상체의 바이트 수와 비트 해석방법을 알아야 하기 때문이다.


ex>

#include <Stdio.h>


int main(void)

{

int i=1234;

int *ptr;

double d=3.14;

double *pd;

ptr = &i;

pd = &d;

printf("정수 = %d", *ptr);

printf("실수 = %d", *pd);

}

위와같은 코드가 있을때, int*인 ptr 이 int형 변수 i 를 읽었기때문에 제대로 읽혀지고,


double*인 pd 가 double 형 변수 d 를 읽었기 때문에 제대로 읽혀진것이다.



*연산자에 대하여


*연산자는 포인터가 가리키는 곳의 대상체를 읽는 연산자이다.

이 연산자가 제대로 값을 읽기 위해서는, 대상체의 타입을 정확히 알고 있어야 한다.


위의 예시에서, ptr 은 int 형 포인터이므로, 4바이트를 읽어올것이다.

하지만 pd 는 double형 포인터이므로, 8바이트를 읽어올것이다. 이후

이값을 부호, 가수, 지수로 분리한후 그값을 얻게된다.


pd 나 ptr 이나 같은 포인터지만, 선언했을때 대상체의 타입을 명시했기 때문에

*연산자가 이 포인터들로 부터 읽는값이 달라질 수 있다.


앞서, 타입을 명시해야하는 첫번째이유를 말했는데, 두번째 이유는 다음과 같다.


인접한 다른 대상체로 이동할 때 이동 거리를 알기 위해서이다.

번지를 가르키는 포인터도 일종의 변수이므로, 다른 번지를 가르키도록 변경할 수 있다.


이때보통 증감연산자로 앞뒤를 가르키도록 할수 있다.


ex>

int main(void)

{

int arr[]={1,2,3,4,5};

int *ptr;

ptr = arr;

printf("%d", *ptr);

ptr++;

printf("%d", *ptr);

}


위 코드와 같은 예시가 있다면, 출력값은 1, 2, 가 될것이다.

왜냐하면 배열의 이름은 첫번째 주소값이기때문에,

포인터에 배열의 첫번째 주소가 저장될것이고 *연산자로 인해 그 값을 불러올것이다.

이후 ptr++ 로, 가르키는 주소를 4바이트 뒤로 밀어서 두번째 인자를 가르킨다.

또 다시, *연산자로 그값을 불러올것이다.


이처럼 증감연산자나 다른방법을 이용할 수도 있다.

이전에 배열포스팅을 했을때,

배열에 접근할때 포인터처럼 사용할 수 있었던것도,

배열자체가 상수포인터이기 때문이다.


즉 위내용을 정리하면,

T형 포인터변수 ptr 에 정수 i 를 더하면,

ptr = ptr + (sizeof(T)*i) 가 되는것이다.




다음은 포인터 연산에 관해서이다.


포인터 연산이란 피연산자중의 하나가 포인터인 연산이다.


pi++; 과 같이 포인터형 변수에 대한 연산,

pi1-pi2 처럼 포인터끼리의 연산이나,

arr - pi, pi+3과 같이 포인터 변수나

포인터 상수가 피연산자중에 하나라도 있으면 포인터 연산이라고 한다.


포인터연산에는 여러 규칙이 있는데 다음과 같다.



1. 포인터끼리 더할 수 없다.


덧셈은 기본적인 연산이지만, 포인터끼리의 덧셈은 안된다.


왜냐면 번지값끼리 더한다 해도 의미가 없기 때문이다.

ex> pi1 가 1500이라는 주소를, pi2 가 2000이라는 주소를 가르킬때 pi+pi2 는 분명 3500이겠지만, 이는 관련이 없는 주소이기 때문이다.


덧셈자체는 가능하다. 하지만, 그 결과값이 아무런 의미를 가지지 못한다.

그래서 포인터끼리 덧셈은 컴파일러가 에러를 출려갛고 거부한다.

대개 오작동할 위험성이 있다.

정 더하고싶다면, 포인터를 unsigned 로 캐스팅해서 더한후, 다시 포인터타입으로 캐스팅하면 가능하다.



2.포인터끼리 뺄 수는 있다.


포인터끼리 더한 값은 아무런 의미가 없지만, 뺀값은 두 요소간의 상대적인 거리라는

의미가 있다.

그래서 포인터끼리의 뺄셈은 원칙적으로 혀용되며 많이 사용한다. 카더라.

타입이 같은 임의의 두 포인터에 대해 뺄셈이 가능하다.

하지만 일반적으로 두포인터 가 같은 배열내의 다른 요소에 가리키고 있을때만 실질적인 의미가 있다.



뺀 값은 그냥 상대적인 거리이므로 다음과 같은건 안된다.


ptr = ptr1 - ptr2;



3. 포인터에 정수를 더하거나 뺄 수 있다.


아까 서술한것 처럼,

int* 인 ptr ++ 을 하면, 지금 가르키는 번지에서 4바이트 뒤의 주소를 가르키겠다는 것이다.

그렇다면, ptr -- 는 4바이트 앞의 주소를 가르킬것이다.


이를 배열에서 활용한게 *(배열이름 + i) 이다




4. 포인터끼리 대입할 수 있다.


int* ptr과 ptr2 가 있을때, ptr = ptr2 처럼,

ptr2가 기억하고 있는 번지를 ptr 에 대입할 수도 있다.


다만, 좌변과 우변의 포인터 타입이 일치해야한다.

아니라면 캐스트 연산자로 맞춰줘야한다.



5. 포인터에 실수와의 연산은 허용되지 않는다.


왜냐하면, 번지라는값은 정수의 범위에서만 의미가 있는것이기 때문이다.



6. 포인터에 곱셈이나 나눗셈을 할수는 없다.


앞서 포인터끼리 더할 수 없는 이유와 같다.



7.포인터끼리 비교는 가능하다.


두 포인터가 같은번지를 가르키고 있는지를 조사하기 위해,

!= 나 ==등의 상등비교 연산자를 사용할 수 있다.

물론 이때도 데이터타입은 일치해야 한다.

주로 유효성을 검사하기위해 NULL값과 비교할 때 쓴다.





Void 형 포인터



포인터형 변수는 선언할 때 반드시 대상체의 타입을 밝혀야 한다.

가리키는 대상체의 타입을 알아야 *연산자로 대상체를 읽을 수 있고, 증감 연산자로 전후이동이 가능하다.

이런 일반적인 포인터에 비해 선언할 때 대상체의 타입을 명시하지 않는 특별한 포인터형이 있는데, 이것이 바로 void 형 포인터이다.


선언은 다음과 같다.


void *ptr;

이렇게 선언하면 vp 포인터 변수의 대상체는 void 형이 되며, 이는 곧 대상체가 정해져 있지 않다는 뜻이다. void 형은 함수와 포인터 변수에게만 적용되는 타입이므로 일반 변수에는 쓸 수 없다.


void 형의 특징은 다음과 깥다.


1.임의의 대상체를 가리킬 수 있다.


대상체가 정해져 있지 않다는건, 임의의 대상체를 가리킬 수 있다는것과 같다.


void 형 포인터는 다른포인터와 달리, 어떠한 대상체라도 가르킬 수 있다.


void 형 포인터에 대입할때는 우변에 아무 포인터나 와도 상관없다.


하지만 역으로 아무 포인터에 void 형 포인터를 대입할때는 대입받는 포인터의

타입에 맞게 void 형 포인터를 형변환 해주어야 한다.



2.*연산자는 쓸 수 없다.


void 형 포인터는 임의의 대상체에 대해 번지값만을 저장하며 이 위치에는 어떤 값이 들어 있는지는 알지 못한다.


따라서 *연산자로 이 포인터가 가리키는 메모리의 값을 읽어올수 없다.

이 이유는 위에 기재한것처럼, 얼마나 읽어야할지와 해석방법등을 모르기 때문이다.


만약 사용하고싶다면, *(출력할 타입에 맞는 포인터형)보이드형 포인터;



3.증감 연산자를 쓸 수 없다.


대상체의 타입이 정해져 있지 않으므로, 증감 연산자도 곧바로 사용할 수 없다.


정수값과 바로 가감연산을 하는것도 허용되지 않는다.

이또한 앞서 기재한 것처럼, 얼마만큼 이동할지 모르기 때문이다.








NULL 포인터


NULL 포인터는 0으로 정의되어 있는 포인터 상수값이다.

아주 특별한 시스템에서는 0이 아닐수도 있지만, 일반적으로는 0이라고 생각하면 된다.


stdio.h 헤더파일에는 NULL 이 0으로 define 되어있다.

0보다 좀더 의미를 표현하기위해 NULL을 쓴다.



어떤 포인터가 NULL 값을 가지고 있다면, 이포인터는 0번지를 가리키는것이다.

0번지라면 메모리 공간의 제일 처음에 해당하는 첫 번째 바이트인데, 이 위치도 분명 실존하는 메모리이므로, 포인터가 가리킬 수도 있다.

그러나 대부분의 플랫폼에서 0번지는 ROM 이거나 시스템 예약 영역에 해당하므로

응용 프로그램이 고유의 데이터를 저장하거나 읽을 수 없도록 보호되어 있다.

그렇기 때문에, 이런 상황은 일종의 에러로 간주되며 그렇게 하기로 약속되어 있다.

포인터를 리턴하는 거의 대부분의 함수는 에러가 발생했을때  NULL값을 리턴한다.


이때 NULL을 반환하는건 없다, 불가능하다 라는 뜻이지,

0을 가리켜라 라는 뜻이 아니다.

그래서 포인처를 리턴하는 함수를 호출하는 구문은 일반적으로 다음과 같다.


if (func() == NULL)

{

에러처리

}

else

{

할일

}

함수의 리턴값이 NULL 인지 아닌지 점검해 보고 NULL 이아닐때만 원하는 작업을 하며 에러

발생시 적절하게 에러 처리해야 한다.

에러처리가 필요없다면 if(func()){ 할일 } 의 형태로 간단히 쓸수도 있다.

만약 리턴값을 점검하지 않고, 리턴된 NULL 을 0번지로 해석하여 이영역을 읽거나

쓰게되면, 프로그램은 다운되어 버린다.



ptr = 1234 처럼 포인터에 상수 번지를 대입한다거나, 포인터를 정수 상수와 비교하는 것은 허락되지 않는다.

왜냐하면, 응용프로그램 수준에서 절대 번지를 프로그래밍 해야 할 경우가 없으며, 반드시 운영체제가 제공한 위치의 메모리만을 사용할 수 있기 때문이다.

만약 꼭 그렇게하려면, 캐스트연산자로 강제로 대입할 수 있지만, 일반적이지는 않다.

하드웨어를 직접 다루는 디바이스 드라이버 저옫 되어야 절대번지를 사용할 일이 있다.


포인터와 상수를 직접 연산할 수 없다는 것은 쉽게 이해가 되는데, 이 규칙의 예외가 존재한다.


바로 NULL 이다. NULL 은 실제로 0으로 정의된 상수이지만, 이 상수는 아주 특별하게도 포인터 변수와 직접적인 연산이 가능하다.

ptr = NULL; 이라는 대입문은 ptr 을 무효화 시키며 if(ptr == NULL) 이라는 비교 연산문은

ptr 이 유효한지를 검사할 수 있다.



'C' 카테고리의 다른 글

배열과 포인터  (0) 2016.04.03
동적할당  (0) 2016.04.03
C언어]재귀 - 피보나치 수열  (0) 2016.03.29
C언어] 1부터 N까지의 합  (0) 2016.03.29
C언어] 재귀 - A 부터 B 사이의 홀수 출력.  (0) 2016.03.29