포인터는 C언어 전반에 많은 영향을 미치며 언어 자체에 많은 유연함을 제공한다. 포인터는 동적 메모리 할당의 매우 중요한 부부이기도 하며, 배열의 표기법과 밀접한 관련이 있다. 


오랫동안 포인터는 C언어 학습에 걸림돌이 되었는데 포인터의 기본적이 개념은 매우 간단하다. 포인터는 메모리 위치의 주소를 저장하는 변수이다. 하지만 포인터 연산자를 사용하고, 암호와도 같은 난해한 표기법을 식별하려고 할 때부터 이 개념이 급격하게 복잡해진다. 포인터를 이해하는 열쇠는 C프로그램에서 메모리가 어떻게 관리되는지 이해하는데 있다. 포인터는 메모리의 주소를 담고 있기 때문에 메모리가 구성되고 관리되는 방법을 이해하지 못한다면 포인터의 동작 방식을 이해하기 쉽지 않다. 그렇기에 메모리의 구성 방법을 확실하게 이해하고 나면 포인터를 이해가기가 쉽다.


1. 포인터와 메모리

컴파일된 C 프로그램은 다음의 세 종류의 메모리를 사용한다.



1) 정적/전역

 - 정적(STATIC)으로 선언된 변수들은 정적/전역 메모리에 할당된다. 전역(GLOBAL)변수들 또한 같은 미모레이 할당된다. 정적/전역 변수들은 프로그램이 시작될 때 할당되며 프로그램이 종료될 때까지 메모리 공간에 남아 있다. 모든 함수에서 접그할 수 있는 전역 변수와는 달리 정적 변수의 접근 범위는 해당 변수를 선언한 함수로 제한된다.


2) 자동/로컬

 - 자동 변수는 함수 안에서 선언되고 함수가 호출될 때 생성된다. 자동 변수의 접근 범위는 선언된 함수로 제한되며, 함수가 호출되는 동안에만 존재한다. 일반적으로 블록문 안ㅅ에서 선언된 변수의 범위는 해당 블록을 ㅗ제한된다.


3) 동적

 - 동적메모리는 힙메모리 영역에 할당되고 필요한 경우 해제된다. 포인터를 사용하여 할당된 메모리 영역을 참조하며, 포인터의 의해 접근이 제한된다. 메모리를 해제하지 않는 한 메모리에 존재한다.


 

접근 범위 

수명 

전역(GLOBAL) 

전체파일 

어플리케이션 실행 동안 유지 

정적(STATIC)

선언된 함수 

어플리케이션 실행 동안 유지 

자동(LOCAL) 

선언된 함수 

함수 실행 동안 유지 

동적(DYNAMIC) 

참조하는 포인터 

메모리 해제 전까지 유지 



포인터 변수는 다른 병수 및 객체 또는 함수의 메모리상 주소를 포함하고 있다. 여기서 객채란, malloc 함수 같은 메모리 할당 함삼수를 사용하여 할당된 메모리를 말한다. 포인터는 일반적으로 문자 타입 포인터와 같이 포인터가 가리킬 대상에서 따라 특정 타입을 가지도록 선언되며 정수, 문자, 문자열 또는 구조체와 같은 어떠한 c 데이터 타입도 될 수 있다. 그러나 포인터 자체는 포인터가 참조하는 데이터 타입의 속성을 가지지 않는다. 포인터는 단지 주소만을 가지고 있다.




2. 포인터를 잘 알아야 하는 이유

  • 빠르고 효율적인 코드 작성
  • 다양한 문제에 대한 효과적인 해결 방법 제공
  • 동적 메모리 할당 지원
  • 작고 간결한 표현의 사용
  • 큰 오버헤드 없이 데이터 구조를 포인터로 전달
  • 함수의 매게변수로 전달된 데이터 보호



3. 포인터를 사용할때 발생할 수 있는 문제점

  • 배열이나 데이터 구조의 경계를 넘는 접근
  • 소멸한 자동/로컬 변수의 참조
  • 할당 해제된 힙 메모리의 참조
  • 아직 할당되지 않은 포인터에 대한 역참조


4. 포인터 선언하기
- 포인터 변수는 데이터 타입과 별표 그리고 변수 이름을 순서대로 나열하여 선언한다. 

 int num;
 int *pi;


별표는 변수를 포인터로 선언하는 데도 사용되지만 두 수를 곱하거나 포인터를 역참조하는데 사용 된다. 포인터를 선언하는데 있어 다음과 같은 사항을 기억 해둬야한다. 초기화 하지 않은 메모리에 대한 포인터는 문제가 될 수 있다. 그런 포인터를 역참조하면 포인터가 유효한 주소를 가리지 않을 것이고, 설령 유효한 주소를 가리킨다 하더라도 그 주오세는 유효한 데이터가 없을 가능성이 높다. 유효하지 않은 주소라는 것은 프로그램의 접근이 허용되지 않는 주소를 의미한다. 이러한 유효하지 않은 주소에 대한 접근은 프로그램의 비정상적 종료의 원인이 되는데 수많은 심각한 문제를 일으킬수 있다.
  • pi에는 정수 변수의 주소가 할당되어야 한다.
  • 이 변수들은 초기화되지 않았으므로 쓰레기값을 가진다.
  • 포인터 구현 자체에는 포인터가 어떤 종류의 데이터를 참조하는지 포인터의 내용이 유효한지를 추측할 만한 내용이 포함되어 있지 않다.
  • 그러나 포인터의 타입은 지정되어 있고 포인터가 올바르게 사용되지 않으면 컴파일러는 오류를 표시한다.



5. 포인터 선언을 읽는 방법

 - 포인터 선언을 이해하기 쉽게 읽는 방법의 비결은 뒤에서부터 읽는 것이다. 


 const int *pci;


1) const int *pci;    -> 포인터 변수 pci

2) const int *pci;    -> 정수를 가리키는 포인터 변수 pci

3) const int *pci;    -> 상수 정수를 가리키는 포인터 변수 pci



6. 주소 연산자

- 주소 연산자(&)는 변수의 주소를 반환한다. 아래와 같이 주소연산자를 사용하여 pi 포인터를 num 변수의 주소로 초기화할 수 있다.

 

 int num = 0;

 int *pi = #


num변수가 100번지에 위치하고 있다면 포인터 변수 pi가 가리키는 주소는 100번지가 된다.


 num = 0

 pi = num


위와 같은 코드를 컴파일 할경우 컴파일러는 'error: invalid conversion from 'int' to 'int*'와 같은 에러 메세지를 반환할것이다.

변수 pi는 정수형 포인터 변수이고 num은 정수형 변수이다. 결과적으로 num 이갖고 있는 0이라는 값을 정수형 포인터 변수에 초기화 하려고 하는 과정에서 에러가 난다. 두 변수는 같은 크기의 바이트를 사용하지만(4byte) 같은 데이터 타입이 아니다. 그러나 포인터로 캐스팅하는 것은 가능하다.


 pi = (int *)num;


컴파일 하면 오류는 생성되지 않는다. 다만 포인터 변수가 가리키는 0번지는 대부분 운영체제에서 프로그램이 사용할 수 있는 유요한 주소가 아니다. 그러기에 비정상 종료가 될것이다.



7. 포인터값 출력하기

- 위 예제에서 사용한 변수들은 쉬운 설명을 위해 100과 104 같은 주소를 사용하고 있다고 쳤지만 아래의 코드를 실행해보면 다음 과 같은 결과를 볼 수 있다.


int main()

{

int num = 0;

int *pi = #


printf("address of num: %d value: %d\n", &num, num);

printf("address of pi: %d value: %d\n", &pi, pi);


    return 0;

}


결과 : 

address of num: 7339564 value: 0

address of pi: 7339552 value: 7339564



8. 간접지정 연산자로 포인터 역참조하기

- 간접지정 연산자(*)는 포인터 변수가 가리키는 위치의 값을 반환하며 '참조 연산자로 포인터 값의 참조'는 종종 포인터 역참조라 불린다. 다음 예제는 변수 num과 pi가 선언되고 초기화 되었다.


 int num = 5;

 int *pi =#


그리고 다음 코드에서 변수 num의 값 5를 출력하기 위해 간접지정 연산자를 사용한다.


 printf("%d\n", *pi);


그리고 또한 좌변값에(lvalue)에 참조 연산자의 결과를 사용할 수 있다. 좌변값은 할당 연사자의 좌측에 위치한 피연산자를 말한다. 

 

 *pi = 200;

 printf("%d", num) // 200 출력


포인터 변수 pi가 가리키는 정수(num)에 200을 할당한다. pi는 변수 num을 가리키고 있으며, 200이 변수 num에 할당된다.



9. 함수 포인터

- 포인터는 함수를 가리키도록 선언될 수 있다. 함수포인터 선언은 일반적인 포인터 표기법에 비해 다소 복잡하다. 


 void (*foo)();


이름이 foo이며 매개변수와 반환값이 없는 함수포인터 선언방법을 보여준다.



10. void 포인터

- void 포인터는 어떤 타입의 데이터도 참조할 수 있는 범용 포인터다. 아래의 void 포인터 선언의 예제가 있다.

 

 void *pv;


void 포인터 선언에 두가지 흥미로운 점

  • void 포인터는 char 포인터와 같은 표현과 메모리 정렬 방법을 사용한다.
  • void 포인터는 다른 포인터와 절대 같지 않다. 하지만 NULL 값이 할당된 두 개의 void 포인터는 서로 같다. void 포인터의 실제 동작은 시스템에 따라 다른다.

모든 포인터는 void 포인터에 할당될 수 있으며, 할당된 포인터는 다시 원래의 타입으로 캐스팅하여 사용할 수있다. 원래의 타입으로 캐스팅이일어나면 포인터의 값은 기존 값과 같게된다. 


int main()

{

int num;

int *pi = #

printf("value of pi:pp", pi);

void* pv = pi;

pi = (int *)pv;

printf("value of pi:pp", pi);


return 0;

}


결과: 

value of pi:006FF874
value of pi:006FF874

void 포인터는 데이터 타입의 포인터에 사용되며, 함수 포인터에는 사용되지 않는다. void 포인터에 sizeof 연산자가 사용될 수있다. 그러나 void 자체에는 sizeof 연산자를 사용할 수 없다.

size_t size = sizeof(void*);  // 유효함
size_t size = sizeof(void)    // 유효하지 않음



'C' 카테고리의 다른 글

[C언어] 열거형 enum  (0) 2018.08.24
[C언어] 구조체와 공용체 (struct, union)  (0) 2018.08.24
c언어 배열  (0) 2018.07.22
C언어 산술, 관계, 논리연산자  (0) 2018.07.22
C언어 포인터의 크기와 데이터 타입  (1) 2018.07.14

+ Recent posts