레이블이 포인터인 게시물을 표시합니다. 모든 게시물 표시
레이블이 포인터인 게시물을 표시합니다. 모든 게시물 표시

2015년 5월 9일 토요일

C/C++에서 포인터 사용시 주의할 점

 C/C++ 프로그래밍에서 포인터를 사용할 때 주의하지 않으면 치명적인 오류를 발생하게 된다. 포인터는 곧 ‘주소’이므로 어느 주소값을 가르키고 있는가가 가장 중요하다. 먼저 확인할 사항은 포인터가 초기화가 되었는가이다.
 초기화되지 않은 포인터가 있을 경우 컴파일러에 따라 경고 메시지를 내기도 하고 경고 없이 실행하다가 실행 중 오류(runtime error)를 발생시켜서 프로그램이 다운될 수도 있다. 포인터를 사용할 때는 반드시 초기화를 시켜야 한다는 것을 알아두자.

 다음 예를 보자

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

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

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

#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()함수로 반환시켜야 한다.

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

#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);
}

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

#include <stdio.h>

void func(int*);

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 포인터라고 하는데 프로그램이 길어질 경우 프로그래머가 의식하지 못하는 부분에서 이러한 실수가 일어날 수도 있게 된다. 당연히 이러한 경우도 방지해야 한다.



2015년 5월 7일 목요일

C/C++의 함수가 포인터를 반환하는 경우

 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++의 배열과 포인터

 포인터와 배열은 깊은 관계가 있는데 사실 배열의 이름이 바로 포인터이다. 다음 예를 보자.

#include <stdio.h>
int main()
{
   int iaa[] = {11,22,33,44,55};
   int ib = 4;
   printf("iaa : %p\n", iaa); //(1)
   printf("*iaa : %d\n", *iaa); //(2)
   printf("*(iaa+ib) : %d\n", *(iaa+ib)); //(3)
   printf("iaa[ib] : %d\n", iaa[ib]); //(4)
}

iaa : 0028ff08
*iaa : 11
*(iaa+ib) : 55
iaa[ib] : 55

이 예에서 (1)의 결과 를 보면 포인터(주소)값이 찍히는 것을 알 수 있으며 (2), (3), (4)의 결과를 보면 배열명은 내부적으로 배열의 첫 번째 요소의 포인터(주소)임을 알 수 있다. 그래서 배열의 첫 번째 요소를 참조하는 다음 표현식은 모두 같은 것들이다.

  • *iaa
  • *(iaa + 0)
  • iaaa[0]

마찬가지로 배열의 5번째 요소는

  • iaa[5]
  • *(iaa+5)

로 표현할 수 있다.
 또 다른 예를 보자.

#include <stdio.h>
int main()
{
   int iaa[] = {11,22,33,44,55};
int *ip = iaa;
for(int k=0; k<5; k++){
printf("iaa[%d]:%d, ", k, ip[k]);  //(1)
}
printf("\n");
for(int k=0; k<5; k++){
printf("iaa[%d]:%d, ", k, *(ip+k)); //(2)
}
printf("\n");
for(int k=0; k<5; k++){
printf("iaa[%d]:%d, ", k, *(ip++)); //(3)
}
}

iaa[0]:11, iaa[1]:22, iaa[2]:33, iaa[3]:44, iaa[4]:55,
iaa[0]:11, iaa[1]:22, iaa[2]:33, iaa[3]:44, iaa[4]:55,
iaa[0]:11, iaa[1]:22, iaa[2]:33, iaa[3]:44, iaa[4]:55,

여기서 (1)과 (2)는 완전히 같은 코드이다. 즉 ip[k] 와 *(ip+k)는 완전히 동일한 표현식이다. 하지만 (3)에서는 앞의 경우들과 달리 ip값이 직접 증가하여 변한다는 점이 다르다. 하지만 배열명에 정수를 더하거나 뺄 수 없다. 즉 iaa++, --iaa 과 같은 표현식은 에러를 발생시킨다. 배열 포인터를 직접 증감시킬 수 없으므로 다른 포인터를 하나 선언해서 거기에 배열 포인터를대입한 다음 그 포인터를 조작해야 한다.
 배열과 포인터의 차이점을 요약하면 다음과 같다.
  • 배열은 메모리가 자동으로 생성되지만 포인터는 메모리를 할당해야 한다.
  • 배열은 주소 이동이 불가능하지만 포인터는 가능하다.
  • 배열은 크기가 ‘데이터형×배열크기’ 이지만 포인터는 2 또는 4바이트이다.


C/C++ 문자열과 포인터

 문자열의 이름은 사실은 포인터로서 문자열의 첫 문자가 저장되는 주소값(이것을 base address라고 한다.)을 가진다. 문자열은 다음과 같이 정의할 수 있다.

char str[6] = {‘h’,’e’,’l’,’l’,’o’,’\0’}; // 문자 배열
char str[] = “hello”; //(1) 초기화할 경우 일차 배열의 크기를 생략할 수 있다.
char *str = “hello”; //(2) 윗 줄과 완전히 동일하다.

위의 예에서 (1)과 (2)는 동일하며 str 이라는 문자열 이름은 “hello” 문자열의 첫 문자 ‘h’가 저장된 메모리의 주소이다. 따라서 다음과 같은 것들이 가능하다.

str[0] 은 *(str+0) 와 같고(혹은 *str) 문자 ‘h’이다.
str[1] 은 *(str+1) 와 같고 문자 ‘h’이다.
str[2] 은 *(str+2) 와 같고 문자 ‘e’이다.
….
str[4] 은 *(str+4) 와 같고 문자 ‘o’이다.
str[5] 은 *(str+5) 와 같고 널문자(‘\0’)’이다.

따라서 다음과 같은 연산도 가능하다.

char ca = *str; // char ca = str[0] 과 같다.
str[0] = ‘H’; // 첫 문자를 대문자 ‘H’로 바꾼다.
printf(“%s”, str); // “hello” 가 출력된다.
printf(“%s”, str+1); // “ello” 가 출력된다.

문자열변수에 다른 변수를 대입하는 것도 물론 가능하다.

char *s1=”Hello world.”, *s2;
s2 = s1+6;
printf(“%s”, s2);

이 프로그램을 실행시키면 “world”라고 출력될 것이다. 포인터 s2 는 ‘w’를 가리키고 있기 때문이다.


H
e
l
l
o

w
o
r
l
d
\0
offset
0
1
2
3
4
5
6
7
8
9
10
11

 이와 같이 문자열 변수는 사실  char형 포인터이고 문자열의 첫 문자가 저장된 곳의 주소라는 사실을 잘 이해해야 한다.


2015년 5월 6일 수요일

C/C++ 언어에서 포인터를 함수의 인자로 넘기기

 어떤 함수의 내부에서 호출하는 쪽의 변수를 직접 조작하려면 그 변수의 포인터를 넘겨받으면 된다. 이렇게 변수의 포인터를 넘겨 받아서 호출하는 함수의 변수를 직접 접근하는 방식을 참조에 의한 호출(call-by-reference) 라고 한다. 프로그래밍에서  참조(reference)라는 용어는 주소(address)와 거의 같은 의미로 사용된다.

 다음 예제를 살펴보자.

#include <stdio.h>
void addOne(float *); // 함수 선언

int main()
{
   float fa = 10.0f;
   addOne(&fa);
   printf("fa=%f\n",fa);
}

void addOne(float *fpa){
   *fpa += 1.0f;
}

 이 예제에서 addOne()함수는 float형 포인터를 받아서 fpa에 저장한다. addOne()함수에 &fa 를 넘긴 것을 눈여겨 보아야 한다. 그리고 내부에서 *fpa 값을 1,0만큼 증가시킨다. 하지만 addOne() 함수의 *fpa 는 main()함수의 fa 변수와 같다. 따라서 fa변수를 직접 조작하는 효과가 나는 것이다. 실행 결과를 보면 다음과 같다.

fa=11.000000

함수를 선언할 경우에는 굳이 인자의 이름을 써 줄 필요가 없고 인자의 자료형만 밝혀주면 된다. 위의 예제에서

void addOne(float *fpa);
void addOne(float *);//이렇게 인자의 이름 fpa를 생략해도 됨

라고 addOne()함수를 선언했는데 이 함수의 첫 번째 인자가 float형 포인터(float *)라고 명시한 것이다.

 다른 예로 두 변수의 값을 바꾸는 swap()함수를 작성해 보자.

#include <stdio.h>
void swap(int*, int*); // 함수 선언

int main()
{
   int ia = 11, ib = 22;
   swap(&ia, &ib);
   printf("ia=%d, ib=%d\n", ia, ib);
}

void swap(int *ipa, int *ipb){
   int itmp = *ipa;
   *ipa = *ipb;
   *ipb = itmp;
}

ia = 22, ib = 11

이 예에서 swap()함수는 두 개의 int형 포인터를 받는다. 그리고 그 내부에서 *ipa와 *ipb 값을 교환했는데 이는 main()함수에서 ia, ib를 바꾼 것과 같은 효과를 가진다.