6.6 포인터 사용시 유의점     [gdoc]     [smts]

 C 프로그래밍에서 포인터를 사용할 때 주의하지 않으면 치명적인 오류를 발생하게 된다. C프로그래밍이 어렵다는 인식이 있는 것은 바로 포인터를 적절하게 사용하기가 상당히 어렵다는 점에서 기인한다. 포인터는 곧 ‘주소’이므로 어느 주소값을 가르키고 있는가가 가장 중요하다. 먼저 확인할 사항은 포인터가 초기화가 되었는가이다.


 초기화되지 않은 포인터가 있을 경우 컴파일러에 따라 경고 메시지를 내기도 하고 경고 없이 실행하다가 실행 중 오류(runtime error)를 발생시켜서 프로그램이 다운될 수도 있다. 포인터를 사용할 때는 반드시 초기화를 시켜야 한다는 것을 알아두자.


다음 예를 보자


0604-01.c
#include <stdio.h>
int main()
{
double *dpa;
*dpa = 1.0;  //(1)
}

이 예는 언듯 문제가 없어보일 지도 모르지만 포인터 dpa가 초기화되지 않고 (1)에서 사용되었다. 이 프로그램은 컴파일러에 따라 컴파일시 오류를 발생하거나 실행이 되더라도 정상적인 동작을 하지 않고 프로그램이 죽어버릴 것이다. 그 이유는 포인터가 초기화되지 않은 상태이므로 정상적인 주소값을 가지고 있기 때문이다. 비정상적인 주소에 실수값 1.0을 대입하였으니 프로그램이 죽어버리는 것이다.


 포인터를 초기화시키려면 다음과 같이 기존 변수의 주소값을 사용하는 방법이 있다.


0604-02.c
#include <stdio.h>
int main()
{
  double da = 0.0;
  double *dpa = &da; // 변수 da의 주소로 포인터 dpa를 초기화
  *dpa = 1.0;
}

아니면 malloc()함수를 사용하여 정상적인 메모리 공간에  새로운 저장 공간(메모리)과 주소를 할당받을 수 있다.


#include <stdio.h>
int main()
{
  double *dpa = malloc(sizeof(double)); //새로운 저장공간과 주소값 할당
  *dpa = 10;
  …
  free(dpa); // 다 사용한 후 반드시 저장공간을 반환해야 한다.
}

malloc()함수와 free()함수는 stdio.h 헤더파일에 정의된 표준 함수이며 각각 메모리를 할당하고 반환하는 역할을 한다. sizeof()함수는 입력 인자의 바이트수를 반환하는데 sizeof(double) 은 double형의 바이트 수를 반환한다. (이 경우는 8) 따라서 8바이트 저장 공간을 확보한 후 그 주소를 반환한다. 그 주소는 포인터 dpa에 저장된다. 단, malloc()함수로 할당받은 저장 공간은 반드시 free()함수로 반환시켜야 한다.


다음 예는 소위 dangling 포인터의 예이다.


0604-04.c
#include <stdio.h>

int main()
{
int ia=10, *ipa = &ia;
printf("*ipa = %d\n", *ipa);
if (ia==10) {
int ib = 20;
ipa = &ib;
}
printf("*ipa = %d\n", *ipa);
}

if 블록 안에서 ipa가 &ib로 초기화가 되었지만 이 블록에서 벗어나는 순간 내부 변수 ib는 소멸되므로 ipa포인터도 그 소재가 불분명하게 된다. 이러한 경우를 dangling 포인터라고 하는데 프로그램이 길어질 경우 프로그래머가 의식하지 못하는 부분에서 이러한 실수가 일어날 수도 있게 된다. 당연히 이러한 경우도 방지해야 한다.


 다음 예를 보자. 다음 예는 ①에서 func(ipa)라고 호출을 하고 있다. 이 함수 내에서 포인터는 ②에서 내부 지역변수 ia의 주소값으로 초기화 된다. 그런데 func()함수 내부의 ipb포인터는 main()함수의 ipa와는 별개의 포인터이다. 따라서 main()함수 안의 ipa는 여전히 초기화가 되지 않은 포인터이다. 따라서 엉뚱한 값이 나오거나 프로그램이 실행 중지되기도 한다.


0604-03.c
#include <stdio.h>
void func(int*);

int main()
{
int *ipa;
func(ipa); //①
printf("*ipa = %d\n in main()", *ipa);
}

void func(int *ipb)
{
int ia = 20;
ipb = &ia; //②
printf("*ipb = %d in func().\n", *ipb);
}

 이와 같이 포인터를 사용할 경우에는 메모리의 할당과 해제에 각별한 주의를 기울여야 하는데 메모리 누수(leakage)와 같은 예기치 않은 동작을 발생시키기 쉽기 때문이다.



Posted by 살레시오
,

 C/C++에서 함수의 반환값이 포인터가 될 수 도 있다. 이 경우 함수의 선언은 다음과 같다.

반환자료형 *함수명(입력데이터형1, 입력데이터형2, …);


함수 내부에서 return 명령어에 의해 반환되는 변수도 당연히 반환자료형의 포인터가 되어야 한다. 이것은 문자열을 반환하거나 배열을 반환해야 하는 함수를 작성할 때 사용된다.


 예를 들어서 getMsg()라는 함수의 반환형이 문자열일 경우 다음과 같이 선언한다.

char *getMsg();


자료형 (char*)은 문자배열 즉 문자열을 표현하는 것이다. 이것을 이용하여 예제를 들어보면 다음과 같다.


#include <stdio.h>

char *getMsg(); // 함수 선언

int main()
{
printf("%s", getMsg());
}

char *getMsg() // 함수 정의
{
char *str = "getMsg() called.\n";
return str;
}


getMsg() called.


이 예제에서 getMsg() 함수는 내부에서 문자열 포인터 str을 생성한 후 그것을 반환한다. main()함수에서는 그것을 받아서 그대로 printf()함수의 입력 인자로 주는 간단한 예제이다.


C++ 강좌 전체 목록 >>>


c{c++},n{c0010}

Posted by 살레시오
,

6.3 포인터 연산     [doc]     [smts]

 포인터 변수에 대해서 더하기와 빼기 연산이 가능하다. 먼저 다음 예제를 살펴보자.


0602-01.c
#include <stdio.h>
int main()
{
  short sa=10, sb=11, *spa=&sa, *spb=&sb;
  long la=20, lb=21, *lpa=&la, *lpb=&lb;
  printf("%p, %p\n", spa, spb);
  printf("%p, %p\n", lpa, lpb);
}

printf()함수에서 %p 형식지정자는 포인터를 출력할 때 사용되며 주소를 16진수로 표시해준다. 실행 결과는 다음과 같다. (주소는 PC마다 다를 수 있다.)


ffffff12, ffffff10
0028ff0c, 0028ff08

변수 sa와 sb는 인접한 메모리에 저장되는데 주소값의 차이가 2가 난다. short형이 2바이트 자료형이기 때문이다. 마찬가지로 long형은 4바이트 자료형이므로 인접한  la와 lb는 주소가 4가 차이가 난다.


 이와 같이 포인터에 정수를 더하거나 빼기도 하고 포인터끼리 뺄셈을 하는 등 연산 기능을 제공한다. 이러한 연산을 통해 포인터가 가리키는 주소를 변화시키거나 포인터들이 가리키는 주소 간 거리를 계산할 수 있다. 이러한 기능은 특히 배열을 다룰 때 유용하게 사용된다.


포인터에 정수를 더하거나 뺄 수 있는데 이 경우 (주소의) 변량은  다음과 같다.


  • 더하거나 빼는 정수×자료형의 크기


다음 예를 보자.


0602-02.c
#include <stdio.h>
int main()
{
  short sa = 10;
  short *spa = &sa;
  printf("%p\n", spa++);
  printf("%p\n", spa);
  printf("%p\n", spa+2);
}

실행결과는 다음과 같다.(주소값은 PC마다 다를 수 있다.)


0028ff1a
0028ff1c
0028ff20

포인터 spa를 1 증가시켰는데 주소값은 2가 증가되었다. 이는 spa가 short형 포인터이고 short는 2바이트를 차지하는 자료형이기 때문이다. 그리고 spa+2는 주소값이 4가 증가되었는데 (정수)*(바이트수) 만큼 증가되기 때문이다.


증감연산자와 포인터 연산자를 조합한 몇 가지 혼동할 여지가 있는 예를 들어보자.


*(++ipA)  // 포인터값을 먼저 증가시킨 해당 변수의 값을 참조
*(ipA++)  // 해당 변수값을 참조한 다음 포인터값을 증가
++(*ipA)  // *ipA 변수값 1 증가후 그 값 참조
(*ipA)++  // *ipA 값 참조 후 변수값 1 증가

이 예제들의 경우 괄호를 생략하여 가독성을 떨어뜨리는 것은 바람직하지 않다.


Posted by 살레시오
,

6.2 포인터 선언과 초기화     [doc]     [smts]

 포인터의 선언은 다음과 같이 한다.


  데이터형 *포인터변수명;

기본 데이터형의 선언 방식에서 데이터 형과 변수명 사이에 별표(*)가 추가되었음을 알 수 있다. 이 문자가 추가됨으로서 뒤의 변수는 ‘포인터(주소)’가 된다. 예를 들어서 int형 포인터는 다음과 같이 선언한다.


  int *ip;

여기서 ip는 int형 포인터(주소)로 선언된 것이다. 이 포인터변수는 선언만 되어 있고 아직 초기화되지 않았다. int형 포인터의 초기화는  int형 변수의 주소를 대입한다.


  int ia=1, *ip;
  ip = &ia;

ia는 일반변수이고 ip는 포인터 변수이다. 두 번째 줄에선 ip포인터가 변수 ia의 주소값으로 초기화 되었다. &ia 는 ‘변수 ia의 주소값’이다. &는 변수의 주소를 구해주는 연산자이다.


 한 줄에 여러 개의 포인터 변수를 정의하려면 다음과 같이 각각의 변수에 반드시 *를 붙여야 한다.


float *pa, *pb, *pc;


여기서 pa, pb, pc 는 모두 float형 포인터이다.


 포인터도 선언과 동시에 초기화를 할 수 있다.


  int ia=1;
  int *ip = &ia;

 계속 언급하는 바와 같이 포인터가 가리키는 것은 변수의 주소이다. 만일 가리키는 변수의 값을 읽어오거나 수정할 경우에는 실행문에서 포인터형 변수 앞에 ‘*’를 붙이면 된다.


0601-01.c
#include <stdio.h>

int main() {
   int ia = 1;
   int *ip = &ia; // (1)
   *ip = 2; // (2)
   printf("ia=%d, *ip=%d", ia, *ip); //(3)
}
실행 결과
ia=2, *ip=2

이 예에서 (1)에서는 int형 포인터 ip를 선언하고 동시에 ia의 주소로 초기화하였다. 그리고 (2)에서 ip의 주소에 저장된 (int형) 데이터를 2로 수정하였다. 그렇다면 변수 ia도 같이 변하는 효과가 있는 것이다. 이와 같이 포인터에 저장된 데이터를 수정하려면 포인터 앞에 별표(*)를 같이 써주면 된다. 즉 *ip 는 변수 a와 완전히 동일하게 사용할 수 있다.


  • ip 는 &ia 와 같다.

  • *ip는 ia와 같다.


 한 가지 혼동하기 쉬운 것은 포인터 선언문에서 쓰이는 별표(*)와 나중에 실행문에서  포인터에 붙여서 쓰는 별표(*)의 의미가 다르다는 것이다.


  int ia = 1, ib = 2;
  int *ipa = &ia, *ipb = ipa;
  *ipb = ib;

이 예제와 같이 선언/초기화하였다면 ia와 *ipa, *ipb 는 동일한 변수와 같이 사용된다. 즉 *ipa 가 변하면 ia도 변경되고 *ipb가 변경되도 마찬가지로 ia도 수정된다.


 다음 예제를 보자.


0601-02.c
#include <stdio.h>

int main() {
   float fa = 1.1, fb = 2.2;
   float *pf;
   
   pf = &fa;
   *pf = 11.1; // (2)
   
   pf = &fb;
   *pf = 22.2;
   
   printf("fa=%.2f, fb=%.2f", fa, fb); //(3)
}

실행 결과

fa=11.10, fb=22.20

이 예는 float형 포인터 변수 fp를 이용하여 fa와 fb의 값을 각각 변경시키는 예제이다. 이와 같이 포인터는 한 번 초기화 된 이후에도 얼마든지 그 값을 바꿀 수 있다.


 포인터는 ‘메모리 주소’이고 주소도 어떤 숫자값이지만 포인터 변수에 직접 숫자 상수를 대입하여 초기화 할 수는 없다. 포인터 변수에 대입할 수 있는 값은 다음의 세 종류이다.


  • NULL

  • 다른 변수의 주소나 배열명, 함수명

  • malloc()이나 calloc() 함수에 의해 반환되는 주소값


 첫 번째로 NULL 상수는 보통 ‘비정상적인 포인터’ 혹은 ‘아직 초기화되지 않은 포인터’임을 나타내는 상수로 내부적으로는 0으로 정의되어 있다.


long *pl = NULL;

포인터를 먼저 초기화 시켜놓고 추후에 정상적인 값을 대입하고자 할 때 NULL을 대입하면 된다.


 두 번째로 다른 변수의 주소를 대입시키는 경우이다. 앞에서 설명한 대로 기존 변수명에 주소연산자 ‘&’를 붙여서 구한 주소값으로 초기화 시키는 경우이다.


 마지막으로 malloc() 함수나 calloc()함수는 지정된 바이트 수의 메모리를 할당한 후 그것의 포인터(주소)를 반환하는 함수이다. 다음과 같이 포인터를 선언했다고 가정하자.


double *pd;

지금 이 상태로는 포인터 변수 pd에는 유효하지 않은 주소값(쓰레기 값)이 저장되어 있을 뿐 실제 double형 값을 저장할 메모리조차 할당되지 않은 상태이다.  8 byte 메모리를 할당한 후 그 메모리의 주소를 반환받기 위해서는 다음과 같이 malloc 함수를 사용하면 된다.


#include <stdlib.h>
...
double *pd = malloc(8); //8 바이트를 할당한다.
...

malloc() 함수는 stdlib.h 헤더 파일에 저장되어 있으므로 반드시 include 시켜야 한다. 하지만 보통은 sizeof 연산자와 조합해서 사용하는 것이 일반적이다.


#include <stdlib.h>
...
double *pd = malloc(sizeof(double));

...

*pd = 12.345; // 이제 값을 대입할 수 있다.
free(pd);

메모리가 할당된 이후에는 실제로 값을 저장할 수 있다. 한 가지 유의할 점은 malloc()함수를 이용하여 할당된 메모리는 사용이 끝나면 (보통 함수가 종료되기 전에)  반드시 free() 함수를 이용하여 수동으로 반환하여야 한다는 점이다.


Posted by 살레시오
,