C,C++

[C,C++] 포인터 동적 메모리 할당

Acdong 2019. 2. 25. 17:09
728x90

void 형 포인터

선언 :

void *vp;

이렇게 선언하면 vp포인터 변수의 대상체는 void형이 되며 이는 곧 대상체가 정해져 있지 않다는 뜻이다.

void 형은 함수와 포인터 변수에게만 적용되는 타입으로 일반 변수에는 쓸 수 없다.

 


 

void 형 포인터의 특징

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

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

vp = pi;

vp = pd;

 

좌변이 void형 포인터일 때는 우변에 임의의 포인터형이 모두 올 수 있다.

반대로 임의의 포인터에 void형 포인터를 대입할 떄는 반드시 캐스팅을 해야한다.

캐스팅 ex)

pi = (int * ) vp;

pd = (double *)vp;

 

 

 


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

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

 

{

int i = 1234;

void *vp;

 

vp = &i;

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

}

 

 

result)

vp는 대상체가 정수형 변수라는 것을 모르기 때문에 *vp로 이 번지에 들어있는 값을 읽을 수는 없다.

description)

vp가 정수형이라는 것을 캐스팅을 통해 printf 문을 수정해야한다.

캐스트 연산자 : vp를 잠시 정수형 포인터로 바꾼다.

printf("%d\n",*(int *)vp);

 


 

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

+ 1 이 몇 바이트 뒤인지를 결정하지 못하기 떄문이다.

 


 

void 형 포인터의 활용

void 형은 함수를 정의할 때 유용하게 사용된다.

함수 예시

void *memset(void*s, int c, size_t n);

 

첫 번째 인수 s 가 void형 포인터로 되어있기 때문에 정수형 , 실수형 , 문자열을 구분하지 않고 모두 인수로 받아들일 수 있다.

void형이 없다면 memsetint , memsetchar , memsetdouble 같은 함수를 따로 만들어야 할 것이다.


NULL 포인터

 

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

0이라는 상수보다는 좀 더 쉽게 구분되고 의미를 명확히 표현할 수 있는 NULL 이라는 명칭의 매크로 상수를 쓰는 것이 좋다.

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

응용프로그램이 이 번지에 어떤 값을 저장하거나 읽을 수 없도록 보호되어있다.

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

NULL 의 활용 - 에러처리

함수의 리턴값이 NULL인지 아닌지 점검해 보고 NULL이 아닐 때만 원하는 작업을 하며 에러 발생시 적절하게 에러 처리해야 한다.

if ( func()==NULL) {

// 에러 처리

} else {

// 하고 싶은일

}

ex)

{

char str[] = " korea " ;

char *p;

p = strchr(str,'r');

if ( p != NULL ) {

*p = 's';

}

puts(str);

}

 

 

description)

"korea"라는 문자열에서 'r'을 찾아 's'로 변경하되 'r'이 발견되지 않으면 아무 것도 하지 않도록 했다.

만약 NULL 점검을 하지않고 'z'를 찾아 's' 로 바 꾸려고 한다면 0 번지를 잘못 건드리는 오작동을 할 가능성이있다.


동적 메모리 할당

 

 

할당의 필요성

프로그램이 실행되기 위해서는 메모리가 필요하다.

정적 할당 ?

int Score ; = 4byte 할당

double Rate; = 8byte 할당

이런 식으로 프로그램을 작성할 때 미리 메모리 필요량을 알려주는 할당을 정적 할당이라고 한다.

동적 할당 ?

프로그램을 작성할 때 메모리 필요량을 지정하는 정적 할당과는 달리 실행 중에(Run time) 필요한 만큼 메모리를 할당하는 기법이다.

어떤 경우에 동적할당이 필요할까?

메모리 필요량을 프로그램 작성시에 전혀 예측할 수 없는 경우가 있다.

임의의 학교에서 학생관리 프로그램일 경우 학생수가 학교마다 각각 다를 수 있다.

그냥 상수로 여유있게 배열크기를 지정할 수 있지만 이럴 경우 효율성이 떨어진다.( 메모리 낭비가 된다. )

int stNum;

printf("학생수를 입력해 주세요.");

scanf("%d",&stNum);

int arScore[stNum];


description)

프로그램 시작 직후에 학생수를 질문하여 stNum에 입력받았다.

그리고 입력된 stNum 크기만큼 arScore 배열을 선언했다.

하지만 !!

이 코드는 에러가 발생한다.

배열 선언문의 크기값은 변수로 지정할 수 없고 반드시 상수로만 지정해야 하기 떄문이다.

그래서 동적할당을 사용해야한다!!..

 


메모리 관리 원칙

1. 메모리 관리의 주체는 운영체제이다 .

반드시 운영체제를 통해서만 메모리를 할당받을 수 있다.

2. 운영체제는 메모리가 있는 한은 할당 요청을 거절하지 않는다.

메모리가 없을 경우에만 메모리가 부족하다는 에러를 리턴한다.

3. 한 번 할당된 메모리 공간은 절대로 다른 목적을 위해 재할당되지 않는다.

운영체제는 메모리를 반납하기 전에는 응용프로그램이 이 공간을 독점적으로 사용할 수 있도록 보장한다.

4. 응용 프로그램이 할당된 메모리를 해제하면 운영체제는 이 공간을 빈 영역으로 인식하고 다른 목적을 위 해 사용할 수 있도록한다.

메모리 관리 원칙은 한마디로 '중앙 집중적 신고제' 라고 할 수있다.

# 허가제가 아니라 신고제 이다.


할당 및 해제

메모리를 동적으로 할당 및 해제할 때는 다음 두 함수를 사용한다.

malloc 과 free 함수의 프로토타입

void *malloc(size_t , size ) ;

void free(void * memblock ) ;


먼저 malloc (엠얼룩) 함수는

인수로 필요한 메모리양을 바이트 단위로 전달하면 요청한만큼 할당한다.

size_t 는 메모리의 양을 나타내는 단위인데 _t로 끝나는 사용자 정의 타입은 표준에 의해 반드시 정의하도록 되어 있으므로 기본 타입과 거의 대등한 자격을 가진다.

10바이트가 필요하면 malloc(10)이라고 호출하고 1000바이트가 필요하면 malloc(1000)이라고 호출하면된다.

** 실행중에 할당하는 것이므로 malloc(Num)과 같이 변수도 사용할 수 있다.

할당한 메모리를 어떤 목적에 사용할지는 알 수 없으므로 malloc 은 void *형을 리턴하며 받는 쪽에서 원하는 타입으로 캐스팅 해야한다.

free 함수는 동적으로 할당한 메모리를 해제한다.

반드시 메모리를 다 사용한 후에 free 함수를 호출하여 메모리를 해제해야 한다.

그래야 이 영역이 다른 프로그램을 위해 재활용될 수 있다.

ex ) 정수형 변수 10개를 담을 수 있는 메모리를 할당하는 예이다.

int *ar;

ar = ( int *)malloc(10*sizeof(int));

// ar 사용

free(ar);

 

description)

malloc은 인수로 전달된 크기 만큼의 메모리를 할당하고 그 시작 번지를 리턴하되 리턴 타입이 void*형 이므로 이 포인터를 변수에 대입할 때는 반드시 원하는 타입으로 캐스팅할 필요가있다.

이렇게 할당하면 ar 번지 이후 40바이트를 응용프로그램이 배타적으로 소유하게 된다.

즉 ar[10]과 같이 정수형 배열과 같아지며 메모리 내에서 실제 모양과 용도 , 적용되는 문법도 배열과 동일하다. 마치 정적 할당된 배열처럼 이 메모리를 활용할 수 있다.

물론 사용한 후에 반드시 free 를 사용하여 해제 해야한다.

ex) 동적할당을 사용한 성적처리프로그램

{

int *arScore;

int i,stNum;

int sum;

printf("학생수를 입력하세요 : ");

scanf("%d",&stNum);

arScore=(int *)malloc(stNum*sizeof(int));

if (arScore == NULL ) {

printf("메모리가 부족합니다.\n");

exit(0);

}

for ( i =0 ; i < stNum; i ++ ) {

printf("%d번 학생의 성적을 입력하세요 : ", i + 1 );

scanf("%d",&arScore[i]);

}

sum = 0;

for ( i = 0 ; < stNum; i ++ ) {

sum+=arScore[i];

}

printf("\n총점은 %d점이고 평균은 %d 점입니다. \n", sum,sum/stNum);

free(arScore);

}


재할당 - realloc

malloc 함수와 마찬가지로 메모리를 할당하되 필요한 메모리양을 지정하는 방법만 다르다.

calloc 함수의 프로토타입 = void *calloc ( size_t num , size_t size _);

malloc은 필요한 메모리를 바이트 단위 하나로만 전달받지만 calloc은 두 개의 값으로 나누어 전달받는 점이 다르다.

malloc = " 몇 바이트 할당해주세요 "

calloc = " 몇 바이트짜리 몇 개 할당해 주세요 "

calloc(10*sizeof(int),2) = 정수형 (4바이트 ) 10 개 짜리 메모리를 2개 할당해 주세요.

malloc 은 메모리 할당만 함으로 메모리에는 초기에 쓰레기값이 들어있지만,

calloc으로 할당하면 할당 후 모든 메모리를 0으로 채운다.

realloc 함수의 프로토 타입 : void *realloc ( void *memblock, size_t size );

첫 번째 인수로 malloc 이나 calloc으로 할당한 메모리의 시작 번지를 주고 두 번쨰 인수로 재할당할 크기를 전달한다. 만약 첫 번째 인수가 NULL 일 경우 , 즉 할당되어 있지 않을 경우는 새로 메모리를 할당하므로 realloc의 동작은 malloc 과 같아진다.

두 번쨰 인수 size 가 0 일 경우는 할당을 취소하는 얘기이므로 free와 같아진다.

 

ex ) realloc

 

{

int *ar;

ar = ( int *)malloc(5*sizeof(int));

ar[4] = 1234;

ar=(int *)realloc(ar,10*sizeof(int));

ar[9]=5678;

printf("ar[4]=%d, ar[9]=%d\n",ar[4],ar[9]);

free(ar);

}

 

result)

ar[4]=1234. ar[9]=5678

동적 할당이 필요한 이유는 컴파일할 시점에 필요한 메모리양을 모를 때가 있기 때문이다.

실행중에라도 필요한 메모리양을 가늠할 수 없을 때가 빈번하게 존재한다.

예를 들어,

네트워크를 통해 파일을 전송하는 프로그램을 작성한다고 해보자. 네트워크의 반대편에서 보내는 파일을 받아야 하는데 이 파일의 크기는 다 받아 보기 전에는 알 수 없는 상황이다.

이럴 때는 최초 적당한 크기로 버퍼를 할당한다. 가령 1M 정도만 할당한 채로 네트워크로 들어오는 패킷을 이 버퍼에 누적시키고 총량이 1M가 넘을경우 메모리를 재할당하여 2M로 늘린다.

이런식으로 패킷을 다 받을 때 까지 재할당하면 된다.

이런 상황은 훨씬 더 자주 발생하는데 압축을 해제한다거나 DB 쿼리를 실행할 때도 재할당이 필요하다.

 

반응형