'Programming~*/C++'에 해당되는 글 13건

Programming~*/C++
신고
0 0
Programming~*/C++
신고
0 0
Programming~*/C++

1번째 방법
먼저 오픈되어 있는 프로젝트를 종료하시구요.
해당 프로젝트 폴더에서 확장자 clw 파일을 삭제하세요.
그리고 다시 프로젝트를 오픈하시고, Ctrl+W로 클래스 위자드를 실행하시면
메세지 창이 뜨실거에요.
거기서 확인을 누르시면 Select Source Files 창이 보이실거에요.
해당 창에서 왼쪽 리스트에 현재 프로젝트에 사용중인 cpp와 h 파일이 정상적으로
등록되어 있는지 확인하시고 OK 버튼을 누르시면 정상적으로 보이실거에요..^^

2번째방법
비주얼 C++에서 클래스 뷰가 제대로 안보이면 내부 파일 분석하고 기록중 오류가 발생했기 때문이다. 이때는 프로젝트 폴더로 가서 프로젝트 이름.NBC 파일을 지우고 비주얼 C++을 재실행한다.

3번째방법
없어진 클래스의 멤버변수를 지웠다 CTRL+Z로 복구시킨다.

신고
0 0
Programming~*/C++

출처: http://www.winapi.co.kr

1. 함수 템플릿

타입만 다른 함수들

템플릿(Template)이란 무엇인가를 만들기 위한 형틀이라는 뜻이다. 플라스틱 모형을 만들기 위한 금형이라든가 주물을 만들기 위한 모래틀이 형틀의 예이며 좀 더 이해하기 쉬운 예를 들자면 붕어빵을 만드는 빵틀을 들 수 있다. 템플릿은 모양에 대한 본을 떠 놓은 것이며 한 번만 잘 만들어 놓으면 이후부터 재료만 집어 넣어서 똑같은 모양을 손쉽게 여러 번 찍어 낼 수 있다. 길거리의 붕어빵 장사들을 보면 빵틀에 밀가루와 팥만 집어 넣어서 똑같이 생긴 붕어빵을 얼마든지 찍어 내고 있지 않은가?

템플릿의 또 다른 특징은 집어 넣는 재료에 따라 결과물들이 조금씩 달라진다는 것이다. 금형에 플라스틱을 집어 넣으면 플라스틱 제품이 나오고 고무를 집어 넣으면 고무로 된 제품을 만들 수 있다. 방틀에도 밀가루를 넣으면 붕어빵이 나오지만 찹쌀 가루를 넣으면 잉어빵이라는 좀 더 부가가치가 높은 상품이 만들어진다. 제품의 모양만 같을 뿐이지 내용물은 조금씩 달라지는 것이다.

함수 템플릿은 함수를 만들기 위한 형틀이라고 생각하면 된다. 비슷한 모양의 함수들을 여러 개 만들어야 한다면 각 함수들을 매번 직접 정의할 필요없이 함수 템플릿을 한 번만 만들어 놓고 이 템플릿으로부터 함수들을 찍어낼 수 있다. 다음 예제는 일정한 타입의 변수 두 개의 값을 교환하는 Swap 함수를 만든다.

 

: SwapFunc

#include <Turboc.h>

 

void Swap(int &a, int &b)

{

     int t;

     t=a;a=b;b=t;

}

 

void Swap(double &a, double &b)

{

     double t;

     t=a;a=b;b=t;

}

 

void main()

{

     int a=3,b=4;

     double c=1.2,d=3.4;

     Swap(a,b);

     Swap(c,d);

     printf("a=%d,b=%d\n",a,b);

     printf("c=%f,d=%f\n",c,d);

}

 

main에서 변수 여러 개를 선언한 후 Swap 함수로 값을 교환한 후 확인을 위해 출력했다. 정수형, 실수형 변수들이 애초에 선언된 값과 반대로 바뀌어 있음을 확인할 수 있다.

 

a=4,b=3

c=3.400000,d=1.200000

 

두 값을 교환하는 알고리즘은 무척 간단해서 두 변수의 값을 서로 대입하기만 하면 된다. 단, 먼저 대입받는 변수의 값을 잠시 저장해 놓기 위한 임시 변수 하나가 필요하며 실인수의 값을 바꿔야 하므로 참조 호출을 해야 한다. 예제에는 정수에 대한 Swap, 실수에 대한 Swap 함수가 작성되어 있는데 교환 대상의 타입이 달라지더라도 알고리즘은 동일하며 본체 내용 중 달라지는 부분은 인수와 임수 변수의 타입 뿐이다.

int와 double외에 char, long, 사용자 정의 구조체 등의 변수들도 교환해야 한다면 각 타입에 대해서도 Swap 함수를 일일이 만들어야 할 것이다. 알고리즘은 같지만 인수와 임시 변수의 타입이 다르므로 한 함수로 임의 타입의 변수를 교환할 수는 없다. 그나마 C++은 오버로딩을 지원하므로 함수의 이름이라도 똑같이 작성할 수 있지만 C에서는 함수의 이름마저도 달라야 한다. 이런 반복되는 함수를 일일이 만들어야 한다는 것은 무척 짜증나는 일이며 수정하기도 번거롭다. 그래서 이 함수들을 통합할 수 있는 여러 가지 방법들이 존재한다.

 

1) 우선 인수의 타입을 #define이나 typedef로 정의한 후 본체에서는 이 매크로를 참조하는 방법을 생각할 수 있다. 교환 대상에 대한 중간 타입을 정의하고 함수에서는 중간 타입을 사용하는 것이다. 필요할 때마다 매크로의 정의를 바꾸면 임의의 타입에 대해 교환하는 함수를 만들 수 있다. 다음이 그 예이다.

 

#define SWAPTYPE int

void Swap(SWAPTYPE &a, SWAPTYPE &b)

{

     SWAPTYPE t;

     t=a;a=b;b=t;

}

 

SWAPTYPE이 int로 정의되어 있으므로 현재 Swap 함수는 int형 변수값을 교환하지만 SWAPTYPE을 double로 바꾸면 실수를 교환하는 함수로 탈바꿈할 것이다. 그러나 이 방법은 컴파일할 때마다 필요한 타입으로 바꿔야 한다는 점이 불편하다. 쉽게 말해서 자동이 아니라 수동이다. 또한 이 방법은 하나의 매크로가 두 개의 값을 가질 수는 없으므로 두 개의 타입을 교환하는 함수가 동시에 존재할 수 없다는 점이 문제다.

2) 두 번째로 다음과 같은 매크로 함수를 쓰는 방법도 가능하다. 중간 타입을 쓰는 것이 아니라 아예 함수 자체를 매크로로 만들어서 필요할 때마다 전개하는 방식이다.

 

#define SWAP(T,a,b) { T t;t=a;a=b;b=t; }

 

이 매크로 함수는 잘 동작하기는 하지만 매크로내에서 임시 블록 변수 t를 선언해서 사용하므로 교환 대상의 타입을 일일이 가르쳐 줘야 한다. 그래야 임시 변수 t의 타입을 결정할 수 있다. 정수값 a와 b를 바꾸려면 SWAP(int, a, b)로 호출해야 하는데 첫 번째 인수로 전달되는 int라는 타입이 왠지 불편해 보이고 최소한의 의사 표시 원칙에도 맞지 않다.

또한 매크로 함수는 치환될 때마다 코드가 반복되므로 프로그램이 커지는 고질적인 문제가 있다. 그래서 복잡한 동작을 하는 함수에는 부적합하며 값을 교환하는 SWAP 정도의 초간단 함수에만 적용할 수 있다. 게다가 매크로 함수는 여러 가지 부작용도 많아 일반적인 용도로 쓰기에는 한계가 있다.

3) 이외에 void *라는 일반적인 포인터 타입을 쓰는 방법도 있다. void *는 임의의 타입을 가리킬 수 있으므로 교환 대상 변수의 번지를 전달하여 메모리 복사하는 방식으로 두 값을 교환할 수 있다. 실제로 이런 방식이 가능한지 예제를 만들어 보자.

 

: SwapVoid

#include <Turboc.h>

 

void Swap(void *a,void *b,size_t len)

{

     void *t;

     t=malloc(len);

     memcpy(t,a,len);

     memcpy(a,b,len);

     memcpy(b,t,len);

     free(t);

}

 

void main()

{

     int a=3,b=4;

     double c=1.2,d=3.4;

     Swap(&a,&b,sizeof(int));

     Swap(&c,&d,sizeof(double));

     printf("a=%d,b=%d\n",a,b);

     printf("c=%f,d=%f\n",c,d);

}

 

실행 결과는 앞의 예제와 완전히 동일한데 두 개의 함수를 만들지 않아도 한 함수로 정수형과 실수형을 모두 교환할 수 있다. 함수가 포인터를 요구하므로 호출측에서는 교환대상에 일일이 &를 붙여 번지를 넘기고 또한 길이도 같이 전달해야 한다. void *는 임의의 변수가 있는 번지를 가리킬 수는 있지만 길이에 대한 정보가 없으므로 길이도 같이 전달하는 수밖에 없다.

Swap 함수 내부도 다소 복잡한데 임의의 타입을 교환해야 하므로 단순한 대입으로는 값을 교환할 수 없으며 변수가 차지하고 있는 영역끼리 메모리 복사를 통해 교환해야 한다. 이때 교환을 위한 임시 변수도 반드시 동적으로 할당해야 하는 부담이 있는데 교환대상의 길이를 전혀 예측할 수 없으므로 충분한 길이의 임시 버퍼로는 안전하지 않다. 16바이트 정도면 왠만한 기본 타입은 다 교환할 수 있겠지만 크기 10K의 구조체가 전달될지도 모르기 때문에 동적으로 할당해야 한다. 이 방법대로라면 아주 큰 배열까지도 교환할 수 있다.

void *를 이용한 교환 함수는 나름대로 실용성도 있고 그야말로 임의의 타입을 다룰 수 있다는 점에서 훌륭하다. 하지만 일일이 &를 붙여 번지를 전달해야 하고 길이까지 가르쳐 주어야 한다는 점에서 불편하기는 마찬가지이다.

 

여러 가지 대안들이 있지만 신통하게 마음에 드는 방법은 딱히 없다. 지금까지 이 문제에 대한 전통적인 해결방법은 복사한 후 원하는 부분을 수정하는 이른바 몸으로 떼우기 작전밖에 없었다. 약간의 수고만 감수하면 Swap(int, int)를 복사한 후 Swap(double, double)이나 Swap(unsigned, unsigned)를 얼마든지 만들 수 있다. 복사된 수만큼 함수가 늘어나기는 하지만 적어도 호출할 때마다 함수의 본체가 반복되지는 않으며 완전한 함수이므로 지역변수를 자유롭게 쓸 수 있고 복잡한 동작도 얼마든지 가능하다.

그러나 이런 전통적인 방법은 필요한 타입이 늘어날 때마다 사람의 작업을 필요로 하므로 생산성이 떨어지며 또한 일부를 수정하지 않는 실수의 가능성이 있어 위험하기도 하다. 이런 복사 후 수정 작업을 컴파일러가 대신 하는 문법적 장치가 바로 함수 템플릿이다. 원하는 함수의 모양을 템플릿으로 등록해 두면 함수를 만드는 나머지 작업은 컴파일러가 알아서 한다. 다음 예제는 Swap 함수를 템플릿으로 정의한 것이다.

 

: SwapTemp

#include <Turboc.h>

 

template <typename T>

void Swap(T &a, T &b)

{

     T t;

     t=a;a=b;b=t;

}

 

struct tag_st {int i; double d; };

void main()

{

     int a=3,b=4;

     double c=1.2,d=3.4;

     char e='e',f='f';

     tag_st g={1,2.3},h={4,5.6};

 

     printf("before a=%d, b=%d\n",a,b);

     Swap(a,b);

     printf("after a=%d, b=%d\n",a,b);

     Swap(c,d);

     Swap(e,f);

     Swap(g,h);

}

 

Swap 함수 템플릿을 정의한 후 정수, 실수, 문자열, 구조체 등에 대해 Swap 함수를 호출해 보았다. 임의의 타입에 대해 Swap 함수를 사용할 수 있되 단 함수 내에서 지역적으로 선언된 타입은 사용할 수 없다. 지역 타입은 함수 내부에서만 쓰는 것이므로 함수간의 통신에는 사용할 수 없기 때문이다. 그래서 tag_st 구조체를 전역으로 선언했는데 이 구조체 선언문이 main 함수 안에 포함되면 에러로 처리된다. 모든 타입에 대해서 제대로 동작하는데 정수형의 a, b에 대해서만 결과를 확인해 보았다. 실행 결과는 다음과 같다.

 

before a=3, b=4

after a=4, b=3

 

함수 템플릿을 정의할 때는 키워드 template 다음에 <> 괄호를 쓰고 괄호안에 템플릿으로 전달될 인수 목록을 나열한다. 템플릿 인수 목록에는 키워드 typename 다음에 함수의 본체에서 사용할 타입의 이름이 오는데 함수의 형식 인수와 비슷한 기능을 한다고 생각하면 된다. 이 이름은 명칭 규칙에 맞게 마음대로 작성할 수 있으나 일반적으로 T나 Type이라는 짧은 이름을 많이 사용한다. 이어지는 함수의 본체에서 템플릿 인수를 참조하여 본체 코드를 작성한다.


이미지를 클릭하시면 창이 닫힙니다













함수 호출부에서 int 타입을 사용했으면 T는 int가 되며 함수 본체에서 참조하는 T는 모두 int가 될 것이다. 마찬가지로 double이 전달되면 T는 double이 되고 char가 전달되면 T는 char가 된다. 호출부에서 전달되는 실제 타입을 템플릿 정의에서 표기하기 위한 임시적인 이름이 바로 typename T인 것이다. 템플릿이 빵틀이라면 T는 빵틀에 집어넣는 재료에 비유될 수 있다.

템플릿 인수 목록에는 키워드 typename 대신 class를 쓸 수도 있으며 구형 컴파일러들은 이 자리에 class를 사용했었다. template <typename T>와 template <class T>는 같은 표현이다. 어차피 클래스도 타입이고 int, double 등도 일종의 클래스이므로 의미상 틀리지는 않지만 이렇게 되면 반드시 클래스 타입만 가능한 것처럼 보여 오해의 소지가 있다. 그래서 새로 개정된 표준에는 좀 더 일반적인 의미를 가지는 typename이라는 키워드가 새로 도입되었으며 가급적이면 class 대신 typename을 사용하는 것이 좋다. 현재 class라는 키워드는 클래스를 정의할 때만 쓰도록 권장된다.

템플릿이란 컴파일러가 미리 등록된 함수의 형틀을 기억해 두었다가 함수가 호출될 때 실제 함수를 만드는 장치이다. 그렇다면 다음과 같이 함수를 만드는 매크로 함수를 정의하는 것과는 어떤 점이 다를까?

 

#define MakeSwap(T) \

void Swap(T &a, T &b)\

{\

     T t;\

     t=a;a=b;b=t;\

}

 

struct tag_st {int i; double d; };

MakeSwap(int)

MakeSwap(double)

MakeSwap(char)

MakeSwap(tag_st)

 

MakeSwap 매크로 함수로 타입 T를 전달하면 T 값 두 개를 교환하는 함수 Swap이 만들어진다. 문법적으로는 분명히 가능한 방법이며 템플릿과 개념상 비슷하지만 두 방법은 지원 주체의 레벨이 다르다. 매크로 함수는 전처리기가 처리하지만 템플릿은 컴파일러가 직접 처리한다. 전처리기는 지시대로 소스를 재구성할 뿐이므로 개발자가 필요한 타입에 대해 일일이 매크로를 전개해야 하므로  수동이지만 템플릿은 호출만 하면 컴파일러가 알아서 함수를 만드는 자동식이므로 매크로 함수보다는 역시 한수 위이다.

그래도 MakeSwap 매크로 함수는 그럴 듯해 보이기는 하는데 함수는 궁한대로 이런 방법을 쓸 수도 있다. 그러나 클래스는 이런 방법으로 클래스를 정의하는 매크로 함수를 만들 수 없다. 왜냐하면 함수는 이름이 같아도 타입이 다르면 오버로딩할 수 있지만 클래스는 오버로딩이 안되기 때문이다. ## 연산자를 쓰면 가능은 하겠지만 타입에 따라 클래스의 이름이 매번 달라지므로 불편하다.

흔하지는 않지만 템플릿 인수 목록에서 두 개 이상의 타입을 전달받을 수도 있다. 함수의 형식 인수 개수에 제한이 없듯이 함수 본체에서 변화가 생길만한 타입이 둘 이상이라면 함수 템플릿도 여러 개의 인수를 가질 수 있다. 이때는 원하는만큼 typename을 반복하되 각 타입의 이름은 구분할 수 있도록 다르게 작성해야 한다.

 

template <typename T1, typename T2>

 

당연한 얘기가 되겠지만 함수 템플릿 정의는 함수 호출부보다 먼저 와야 한다. 함수 템플릿 정의문에 의해 컴파일러는 임의의 타입 T의 값을 교환하는 함수의 모양을 Swap이라는 이름으로 기억할 것이다. 만약 main을 더 앞쪽에 두고 싶다면 순서를 바꿀 수 있되 템플릿에 대한 원형을 호출부의 앞쪽에 미리 선언해야 한다. 템플릿 함수의 원형은 template 키워드부터 시작해서 템플릿 함수의 선두를 그대로 가져간 후 세미콜론만 붙이면 만들 수 있다.

 

template <typename T>

void Swap(T &a, T &b);

 

일반 함수와 원형을 만드는 방법은 동일한데 원형 선언이 두 줄에 걸친다는 점에서 다소 어색해 보이기는 한다. 물론 한 줄에 붙여써도 별 이상은 없다. 이 함수 템플릿으로부터 실제 함수가 어떻게 만들어지는지는 다음 항에서 연구해 보자.


구체화

함수 템플릿은 어디까지나 함수를 만들기 위한 형틀에 지나지 않으며 그 자체가 함수인 것은 아니다. 컴파일러는 함수 템플릿 정의문으로부터 앞으로 만들어질 함수의 모양만 기억하며 실제 함수가 호출될 때 타입에 맞는 함수를 작성한다. 함수 템플릿으로부터 함수를 만드는 과정을 구체화 또는 인스턴스화(Instantiation)라고 하는데 호출에 의해 구체화되어야만 실제 함수가 만들어진다. 존재하는 모든 타입에 대해 함수를 미리 만들어 놓는 것이 아니다.

이때 함수 템플릿으로부터 만들어지는 함수를 템플릿 함수라고 한다. 용어가 비슷해서 다소 헷갈리는데 둘 다 뒤쪽에 강세를 두고 읽으면 실체 파악이 쉽다. 함수 템플릿은 함수를 만들기 위한 템플릿이고 템플릿 함수는 템플릿으로부터 만들어지는 함수이다. 배열 포인터, 포인터 배열 등의 용어도 마찬가지인데 한국말은 대체로 뒤쪽 단어에 진짜 뜻이 있으며 끝까지 들어 봐야 무슨 말인지 알 수 있다.

만약 템플릿만 정의하고 함수를 호출하지 않으면 아무런 일도 일어나지 않으며 템플릿 자체는 메모리를 소모하지 않는다. 마치 붕어빵틀이 붕어빵이 아니어서 먹을 수도 없고 재료를 소모하지 않는 것과 마찬가지이다. 호출에 의해 템플릿이 구체화되어 실제 함수가 될 때만 프로그램의 크기가 늘어난다. 호출되지도 않는 함수를 만들 필요는 전혀 없는 것이다. 템플릿만 선언해 놓고 비주얼 C++로 맵 파일을 만들어서 확인해 보면 과연 그렇다는 것을 확인할 수 있다.

SwapTemp 예제를 통해 템플릿 함수가 구체화되는 것을 확인해 보자. main에서 정수, 실수, 문자, 구조체 등 각각의 타입으로 Swap 함수를 호출하는데 이때마다 컴파일러는 Swap 함수 템플릿을 참조하여 실인수의 타입에 맞는 실제 Swap 함수를 구체화한다. 이 예제의 경우 네가지 버전의 함수가 구체화될 것이다.


이미지를 클릭하시면 창이 닫힙니다
















맵 파일을 만들어 확인해 보면 과연 4개의 Swap 함수가 작성되어 있음을 눈으로 직접 확인할 수 있다. 맵 파일(Map File)은 함수나 변수가 어느 주소에 배치되었는지에 대한 일종의 컴파일 결과 보고서인데 프로젝트 설정 페이지의 링크 탭에서 옵션을 선택하면 Debug 디렉토리에 *.map 파일로 생성된다. 물론 long, short, char *, float 등등 다양한 타입에 대해 Swap 함수를 호출하면 더 많은 Swap 함수들이 생성될 것이다.

 

0001:000001b0       ?Swap@@YAXAAH0@Z           004011b0 f i SwapTemp.obj

0001:00000200       ?Swap@@YAXAAN0@Z           00401200 f i SwapTemp.obj

0001:00000260       ?Swap@@YAXAAD0@Z           00401260 f i SwapTemp.obj

0001:000002b0       ?Swap@@YAXAAUtag_st@@0@Z   004012b0 f i SwapTemp.obj

 

타입만 다른 함수들을 직접 복사해서 정의하는 방법과 함수 템플릿을 정의한 후 컴파일러가 구체화하도록 하는 것과는 어떤 차이점이 있을까? 일단은 반복되는 부분이 통합되므로 소스 길이가 짧아지고 수정할 필요가 있을 때 템플릿만 수정하면 된다. 따라서 관리하기 편리해진다는 이점이 있으며 이후 임의의 타입에 대한 함수를 새로 구체화하는 것도 컴파일러가 알아서 하므로 확장성도 훨씬 더 좋다.

더 이상 사용하지 않는 함수를 삭제하는 것도 컴파일러의 몫이다. Swap(c, d) 호출문을 삭제하고 재컴파일해 보면 Swap(double, double) 함수는 다시 생성되지 않음을 확인할 수 있다. 그러나 함수 템플릿을 쓴다고 해서 실행 파일의 크기가 작아지는 것은 아님을 유의하자. 구체화되는 함수들은 각자가 메모리를 따로 차지하므로 실행 파일의 크기면에서는 별다른 이점이 없다. 복사해서 수정하는 방법과 똑같이 메모리를 차지한다. 이런 면에서 볼 때 템플릿보다는 오히려 SwapVoid 예제의 방식이 메모리 절약면에서는 유리하다.

명시적 인수 지정

컴파일러는 호출부의 실인수 타입을 판별하여 필요한 함수를 구체화하는데 예를 들어 Swap(a, b)는 a, b가 정수이므로 Swap(int, int) 함수를 구체화할 것이고 Swap(c, d)는 c, d가 실수형이므로 Swap(double, double) 함수를 구체화할 것이다. 템플릿 타입 정의에 의해 두 인수의 타입은 같아야 하므로 SwapTemp 예제에서 Swap(a, c)는 두 인수의 타입이 달라 에러로 처리된다. Swap(a, c) 호출에 대해 Swap(double, double) 함수를 구체화하고 a를 double로 암시적 변환해서 호출할 수도 있을 것 같지만 템플릿은 타입이 정확해야 하므로 암시적 변환까지는 고려하지 않는다.

상수는 변수와 달리 그 형태만으로 타입을 정확하게 판단하기 힘든 경우가 있다. 그래서 템플릿 함수를 호출할 때 실인수와는 다른 타입을 강제로 지정할 수 있는데 이때는 함수명 다음의 < > 괄호안에 원하는 타입을 밝힌다. Max<double>(3, 4)로 호출하면 실인수 3, 4가 정수형 상수지만 산술 변환되어 Max(double, double) 함수가 호출된다. 물론 Max(3.0, 4.0)이라고 호출해도 마찬가지이다. 이 기법은 비슷 비슷한 함수를 여러 벌 만들지 말고 한 타입에 대해서만 함수를 구체화하고 싶을 때 아주 실용적이다. 다음 예제 코드를 보자.

 

template <typename T>

void LongFunc(T a)

{

     // 아주 긴 함수의 본체

}

 

int i=1;

unsigned u=2;

long l=3;

 

LongFunc(i);

LongFunc(u);

LongFunc(l);

 

LongFunc은 본체가 굉장히 큰 함수이고 길이가 길다고 할 때 int, unsigned, long 각각에 대해 이 함수를 일일이 구체화하면 실행 파일의 용량이 굉장히 커질 것이다. 이 외에도 int와 호환되는 타입은 char, short, 열거형 등 아주 많은 타입이 있는데 사실 이 타입들은 int와 거의 똑같은 방법으로 처리할 수 있으므로 굳이 본체를 따로 만들 필요까지는 없을 것이다. 이럴 때는 호출문을 다음과 같이 작성하여 구체화되는 함수의 수를 줄일 수 있다.

 

LongFunc<int>(i);

LongFunc<int>(u);

LongFunc<int>(l);

 

인수의 타입을 강제로 바꿀 때 외에도 리턴 타입이나 인수로 사용되지 않는 타입을 지정하기 위해서 명시적으로 타입을 지정할 수 있다. 리턴 타입은 호출할 함수를 결정할 때는 사용되지 않으며 또한 인수로 전달되지 않고 함수 내부에서만 사용하는 타입도 함수 호출문에는 나타나지 않는다. 이럴 때는 컴파일러가 함수 호출문만으로 구체화할 함수를 결정할 수 없으므로 어떤 타입의 템플릿 함수를 원하는지를 분명히 지정해야 한다.

 

: TempReturn

#include <Turboc.h>

#include <iostream>

using namespace std;

 

template <typename T>

T cast(short s)

{

     return (T)s;

}

 

template <typename T>

void func(void)

{

     T v;

 

     cin >> v;

     cout << v;

}

 

 

void main()

{

     int i=cast<int>(1234);

     double d=cast<double>(5678);

 

     printf("i=%d, d=%f\n",i,d);

     func<int>();

}

 

cast는 인수로 전달된 s를 템플릿 인수가 지정하는 타입으로 캐스팅하는 함수이다. cast(1234) 호출문만으로는 어떤 버전의 함수를 만들지 결정할 수 없으므로 명시적으로 인수를 밝혀서 호출해야 한다. 이 예제의 경우 int cast(short), double cast(short) 두 버전의 함수가 구체화되는데 이 두 함수는 이름이 동일하고 인수 목록까지 같으므로 오버로딩 조건을 만족하지 못한다. 이처럼 리턴 타입만 다른 경우라도 템플릿에 의해 각각 따로 구체화될 수는 있지만 호출할 때 어떤 함수를 호출하는지를 반드시 밝혀야 한다. 그냥 cast(1) 이라고 호출해 버리면 어떤 함수를 원하는지 결정할 수 없어 모호하므로 에러로 처리된다.

func 함수는 내부적인 처리를 위해 T형의 지역변수 v를 선언하여 사용한다. 물론 T가 가변적인 타입이므로 본체는 전달된 모든 타입에 대해 가능한 코드만 사용해야 한다. func는 인수로 리턴값도 없으므로 호출부만 봐서는 도대체 어떤 함수를 구체화해야 할 지 전혀 결정할 수 없다. 따라서 func()라고 호출하면 컴파일러는 뭘 원하는지 어리둥절해 할 것이다. 이때도 func<int>() 처럼 v의 타입을 명시적으로 전달해야 한다.

리턴 타입만 다른 템플릿이나 알지도 못하는 타입의 지역변수를 선언하는 함수는 그다지 실용성이 없어 보이고 저런 걸 어디다 쓸까 싶지만 호환되는 여러 가지 타입의 객체 중 원하는 것을 선택해서 대신 생성해 주는 래퍼 함수를 만들고 싶을 때 이런 기법이 가끔 사용되기도 한다. 그때를 위해 이런 문법도 있다는 것은 기억해 두도록 하자.

명시적 구체화

함수의 호출부를 보고 컴파일러가 템플릿 함수를 알아서 만드는 것을 암시적 구체화라고 한다. 개발자가 원하는 타입으로 함수를 호출하기만 하면 나머지는 컴파일러가 다 알아서 하며 호출하지 않는 타입에 대해서는 구체화하지 않는다. 만약 특정 타입에 대한 템플릿 함수를 강제로 만들고 싶다면 이때는 명시적 구체화(Explicit Instantiation)를 하는데 이는 지정한 타입에 대해 함수를 생성하도록 컴파일러에게 지시하는 것이다. 예를 들어 float 타입을 교환하는 함수를 생성하고 싶다면 다음 명령을 사용한다.

 

template void Swap<float>(float, float);

 

명시적 구체화 명령의 표기는 일단 키워드 template가 앞에 오고 함수 이름 다음에 생성하고 싶은 타입을 < > 괄호안에 적는다. 이 선언에 의해 float형을 인수로 취하는 Swap(float, float) 함수가 만들어진다. 당연한 얘기겠지만 템플릿이 어떤 모양인지를 알아야 컴파일러가 이런 함수를 만들 수 있으므로 명시적 구체화 명령은 템플릿 선언보다 뒤에 와야 한다.

이 함수가 당장 필요치 않더라도 일단 만들어 놓고 싶다면 명시적 구체화로 강제 생성을 지시할 수 있다. 예를 들어 지금 작성하는 소스에서는 이 함수가 필요치 않지만 컴파일된 라이브러리로 배포하고 싶다면 명시적 구체화를 할 필요가 있다. 그러나 실제 상황에서 이런 경우는 거의 발생하지 않는데 왜냐하면 함수 템플릿 정의문은 보통 헤더 파일에 작성하며 헤더 파일을 배포하기 때문이다. 라이브러리를 사용하는 측에서 헤더 파일을 인클루드하고 Swap(float, float)를 호출하면 그때 컴파일러가 이 함수를 구체화할 것이므로 문제가 되지 않을 것이다.

다만 함수의 내용을 숨기고 싶을 때는 함수 템플릿을 공개할 수 없으므로 이럴 때는 명시적 구체화로 자주 사용할만한 타입에 대해 일련의 함수 집합을 미리 생성해 놓는다. 이 라이브러리의 사용자는 개발자가 명시적으로 구체화해 놓은 함수만 사용할 수 있을 것이다. 명시적 구체화는 컴파일 속도에도 긍정적인 효과가 있는데 미리 필요한 함수를 생성해 놓으면 컴파일러가 어떤 함수를 생성할 것인지를 판단하는 시간을 조금 절약할 수 있다.


동일한 알고리즘 조건

함수 템플릿은 코드는 동일하고 타입만 다른 함수의 집합을 정의한다. 즉, 템플릿으로 정의할 수 있는 함수들은 문제를 푸는 알고리즘이 동일해야 하며 알고리즘이 다른 함수는 템플릿의 일원이 될 수 없다. 이런 함수들의 집합을 몇 개 더 구경해 보도록 하자.

 

: TemplateFunc

#include <Turboc.h>

 

template <typename T>

T Max(T a, T b)

{

     return (a > b) ? a:b;

}

 

template <typename T>

T Add(T a, T b)

{

     return a+b;

}

 

template <typename T>

T Abs(T a, T b)

{

     return (a > 0) ? a:-a;

}

 

void main()

{

     int a=1,b=2;

     double c=3.4,d=5.6;

     printf("더 큰 정수 = %d\n",Max(a,b));

     printf("더 큰 실수 = %f\n",Max(c,d));

}

 

두 값 중 큰 값을 찾는 Max, 두 값의 합을 구하는 Add, 절대값을 찾는 Abs 함수들이 템플릿으로 정의되어 있다. 이 함수들은 인수로 전달된 임의의 타입에 대해 동작할 수 있으며 호출부에서는 실인수의 타입을 보고 적절한 함수를 구체화하여 호출한다.

예제의 세 함수 템플릿들을 보면 값을 비교, 연산하고 선택하는 알고리즘이 타입과 상관없이 항상 동일하다는 것을 알 수 있다. 달라질 수 있는 것은 오로지 타입뿐이므로 이런 함수들이 템플릿으로 통합될 수 있는 것이다. 만약 알고리즘이 동일하지 않다면, 즉 함수의 본체가 완전히 달라야 한다면 이 함수들은 같은 템플릿으로 통합될 수 없다. 예를 들어 두 값을 교환하는 Swap 함수 템플릿의 경우 임의의 타입에 대해 잘 동작하지만 배열에 대해서는 동작하지 않는다. 만약 다음과 같이 배열을 가리키는 포인터를 두 개 선언하고 이 포인터를 Swap 함수로 전달했다고 해 보자.

 

int a[]={1,2,3},b[]={4,5,6};

int *pa=a,*pb=b;

Swap(pa, pb);

// Swap(a, b);

 

Swap(pa, pb)는 일단 정상적으로 동작하는 것처럼 보인다. 그러나 이는 배열을 가리키는 포인터만 교환한 것이지 배열 자체가 교환된 것은 아니다. Swap(a, b) 호출로 배열 자체를 교환하려고 시도하면 포인터 상수인 배열명을 변경할 수 없다는 에러로 처리된다. 두 배열의 타입과 크기가 일치하더라도 배열을 교환하는 알고리즘은 단순 타입이나 구조체를 교환하는 것과는 다르다. 배열끼리는 대입되지 않으므로 배열의 요소들을 일대일로 교환해야 하며 배열의 크기가 가변적이므로 길이에 대한 별도의 정보를 더 전달해야 한다.

두 배열의 내용을 통채로 교환하려면 별도의 함수를 만들어야 하는데 다양한 배열 요소에 대해 동작하려면 요소의 타입별로 일련의 함수를 만들어야 할 것이다. 이때도 요소의 타입만 달라지므로 템플릿을 사용할 수 있다. 다음 예제는 배열을 교환하는 함수 템플릿을 정의하는데 임의의 T형을 요소로 가지는 num길이의 두 배열을 메모리 복사를 통해 교환한다.

 

: SwapArray

#include <Turboc.h>

 

template <class T>

void SwapArray(T *a, T *b,int num)

{

     void *t;

 

     t=malloc(num*sizeof(T));

     memcpy(t,a,num*sizeof(T));

     memcpy(a,b,num*sizeof(T));

     memcpy(b,t,num*sizeof(T));

     free(t);

}

 

void main()

{

     int a[]={1,2,3},b[]={4,5,6};

     char c[]="문자열",d[]="string";

     SwapArray(a,b,sizeof(a)/sizeof(a[0]));

     printf("before c=%s,d=%s\n",c,d);

     SwapArray(c,d,sizeof(c)/sizeof(c[0]));

     printf("after c=%s,d=%s\n",c,d);

}

 

앞에서 만들었던 SwapVoid와 상당히 유사한데 메모리의 길이를 인수로 전달받는 것이 아니라 요소의 개수를 전달받는다는 점이 다르다. main에서 크기 3의 정수형 배열과 크기 7의 문자형 배열에 대해 교환을 했으므로 SwapArray는 두 가지 버전으로 구체화될 것이다. 실행 결과는 다음과 같다.

 

before c=문자열,d=string

after c=string,d=문자열

 

보다시피 배열을 교환하는 알고리즘은 단순 타입을 교환하는 알고리즘과 완전히 틀리고 필요한 인수 목록도 다르기 때문에 하나의 함수 템플릿으로 통합될 수 없으며 따로 템플릿을 구성해야 한다. 이 예제에서는 배열을 교환하는 함수 템플릿에 SwapArray라는 이름을 사용했는데 인수 목록이 달라 오버로딩이 가능하므로 Swap이라는 이름을 같이 써도 상관없다. 즉 템플릿끼리도 오버로딩은 가능하다.


임의 타입 지원 조건

함수 템플릿의 본체 코드는 임의의 타입에 대해서도 동일하게 동작해야 하므로 타입에 종속적인 코드는 사용할 수 없다. 기본 타입에 대해 이미 오버로딩되어 있는 +, - 등의 연산자를 사용하거나 cout과 같이 피연산자의 타입을 스스로 판별할 수 있는 코드만 사용해야 한다. printf 함수처럼 타입에 따라 서식을 미리 결정해야 하는 함수는 함수 템플릿에서 쓰지 않는 것이 바람직하다.

앞 예제의 Add 함수 템플릿은 + 연산자로 피연산자를 더하는데 + 는 대부분의 기본 타입에 대해 오버로딩되어 있으므로 아무 타입이나 잘 더할 수 있을 것 같다. 그러나 이 연산자를 쓸 수 있는 정수, 실수, 문자형 등의 수치형에 대해서만 사용할 수 있을 뿐이다. 문자열(char *)끼리 더할 때는 포인터끼리 더할 수 없으므로 strcat 함수를 사용해야 한다. 더하는 알고리즘이 완전히 다르므로 이 함수를 사용할 수 없다. 물론 + 연산자를 오버로딩하고 있는 문자열 객체라면 이 함수로 연결할 수 있을 것이다.

템플릿 본체에 사용된 모든 코드와 호환되는 타입에 대해서만 구체화할 수 있다. 두 값 중 큰 값을 찾는 Max 템플릿은 단순히 > 연산자와 삼항 조건 연산자만으로 값의 대소를 판단할 뿐이다. 지극히 간단해서 별다른 제약이 없을 것 같지만 이 간단한 템플릿도 안 통하는 경우가 있다. 다음 예제를 보자.

 

: MaxObject

#include <Turboc.h>

 

template <typename T>

T Max(T a, T b)

{

     return (a > b) ? a:b;

}

 

struct S {

     S(int ai) : i(ai) { }

     int i;

     //operator >(S &Other) { return i > Other.i; }

};

 

void main()

{

     int i1=3,i2=4;

     double d1=1.2,d2=3.4;

     S s1(1),s2(2);

 

     Max(i1,i2);

     Max(d1,d2);

     Max(s1,s2);

}

 

Max는 두 값 중 큰 값을 리턴하는데 정수나 실수에 대해서는 잘 동작한다. 그러나 구조체 S에 대해서는 동작하지 않는데 구조체끼리는 > 연산자로 비교할 수 없기 때문이다. 구조체에 > 연산자를 오버로딩해 놓으면 이때는 S객체끼리 대소 비교가 가능해 지므로 Max(s1, s2) 호출도 잘 컴파일된다. S가 > 연산자를 오버로딩하지 않더라도 이 호출을 주석으로 처리하면 아무 일 없다는 듯 잘 컴파일된다.

컴파일러는 구체화된 템플릿 함수에 대해서만 에러 체크를 할 뿐이지 템플릿 자체에 대해서는 상세한 점검을 할 수 없다. 템플릿을 정의할 때는 어떤 타입이 전달될지 모르므로 컴파일러는 템플릿의 모양만 기억해 둘 뿐 구문상의 에러 체크를 하지 않는다. 심지어 다음과 같은 템플릿도 아무 문제없이 컴파일된다.

 

template <typename T>

void Some(T arg)

{

     *arg.ar[34]=arg.next;

     arg.next->value=1234;

}

 

이 템플릿은 인수로 전달받은 arg가 구조체이고 이 구조체안에 크기가 최소한 35이상인 ar 배열이 멤버로 포함되어 있으며 next는 다른 구조체를 가리키는 포인터이고 next가 가리키는 구조체의 value라는 멤버는 정수형 변수라는 것을 가정하고 있다. 이 가정이 맞는지 아닌지는 실제로 전달되는 T가 어떤 타입인가에 따라 달라지는 것이므로 컴파일러는 구체화될 때까지는 에러 체크를 보류할 수밖에 없는 것이다. 오타가 있거나 if = for + while; 같은 말도 안되는 구문까지도 일단은 컴파일된다.

템플릿은 인수로 전달된 임의의 타입에 대해 동작할 수 있는 함수의 형틀이지만 그 본체에서는 전달될만한 타입을 모두 지원하는 범용적인 코드만 작성해야 한다. 또는 템플릿으로 전달된 타입이 해당 템플릿의 본체 코드의 요구 조건을 모두 만족해야 한다. 그렇지 않을 경우 잘 사용하던 템플릿도 특정 타입에 대해 구체화했을 때 갑자기 에러가 발생할 수도 있다. 다음 예제로 테스트해 보자.

 

: SwapPerson

#include <Turboc.h>

 

template <typename T>

void Swap(T &a, T &b)

{

     T t;

     t=a;a=b;b=t;

}

 

class Person

{

private:

     char *Name;

     int Age;

 

public:

     Person() { Name=NULL; Age=0; }

     Person(char *aName, int aAge) {

          Name=new char[strlen(aName)+1];

          strcpy(Name,aName);

          Age=aAge;

     }

     Person(const Person &Other) {

          Name=new char[strlen(Other.Name)+1];

          strcpy(Name,Other.Name);

          Age=Other.Age;

     }

/*

     Person &operator =(const Person &Other)   {

          if (this != &Other) {

              delete [] Name;

              Name=new char[strlen(Other.Name)+1];

              strcpy(Name,Other.Name);

              Age=Other.Age;

          }

          return *this;

     }

//*/

     virtual ~Person() {

          delete [] Name;

     }

     virtual void OutPerson() {

          printf("이름 : %s 나이 : %d\n",Name,Age);

     }

};

 

void main()

{

     Person A("이승만",10);

     Person B("박정희",20);

     A.OutPerson();B.OutPerson();

     Swap(A,B);

     A.OutPerson();B.OutPerson();

}

 

예제의 선두에는 앞에서 만들어서 이미 테스트가 완료된 Swap 템플릿이 정의되어 있다. 변수 교환 알고리즘이 워낙 간단해서 별 문제가 없을 것 같지만 Person 객체에 대해서는 제대로 동작하지 않으며 끝낼 때 다운된다. Swap 템플릿은 변수값 교환을 위해 세 번 대입을 하므로 이 코드가 이상없이 동작하려면 대입이 가능해야 하고 대입에 의해 별다른 문제가 없어야 한다. 그러나 예제의 Person 클래스는 대입 연산자를 제대로 정의하고 있지 않고 있어 템플릿의 코드와는 맞지 않다.

Person 객체 A와 B에 대해 Swap(A,B)를 호출했을 때 어떤 일들이 벌어지는지 상상해 보자. 컴파일러는 함수 호출부의 타입을 보고 Swap(Person, Person) 함수를 구체화하여 형식 인수 a, b로 실인수 A, B를 전달한다. Person에는 복사 생성자가 정의되어 있으므로 여기까지는 아주 정상적이다. 그러나  교환을 위해 a를 t에 대입하는 순간 t는 얕은 복사에 의해 a와 버퍼를 공유하게 되며 이 상태에서 t의 값을 b가 대입받았다. Swap 함수가 종료될 때 a는 b의 값을 무사히 대입받았지만 b와 t가 버퍼를 공유하며 지역 객체 t가 파괴되면서 b의 버퍼를 정리해 버린다. main으로 돌아 왔을 때 실인수 B의 버퍼가 이중 해제되므로 다운되는 것이다.


이미지를 클릭하시면 창이 닫힙니다









틀린 코드임에도 불구하고 컴파일 에러가 발생하지 않는 이유는 대입 연산자는 디폴트가 있기 때문에 일단 대입이 가능하기 때문이다. 디폴트 대입 연산자에 의한 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 대입 연산자를 정의하면 문제가 해결된다. 예제의 주석으로 묶여있는 대입 연산자를 풀어 보면 아무 이상없이 잘 동작할 것이다.

템플릿은 지금 당장 잘 컴파일되고 이상없이 동작하는 것처럼 보이더라도 타입이 바뀌면 어떻게 될지 장담할 수 없다. 완벽할 수는 없겠지만 템플릿은 가급적이면 많은 타입을 지원할 수 있도록 범용적인 코드를 작성해야 하며 템플릿의 인수로 사용될 클래스는 템플릿 본체가 요구하는 모든 기능을 지원해야 한다. 가장 이상적인 타입은 기본 타입인 int이므로 int와 똑같은 방식으로 동작하는 클래스를 만든다면 거의 안전하다. 왜 클래스가 완전한 타입 흉내를 내려고 그토록 몸부림을 치는지 이해할 수 있겠는가?


특수화

같은 템플릿으로부터 만들어진 함수는 타입만 제외하고 동일한 본체를 가지므로 동작도 동일하다. 만약 특정 타입에 대해서만 다르게 동작하도록 하고 싶다면 이때는 특수화(Specialization)라는 기법을 사용한다. 예를 들어 Swap 함수를 실수에 대해 적용할 때는 값을 전부 교환하지 말고 정수부만 교환하고 싶다고 하자. 이럴 때는 double 형에 대해서 특수한 함수를 하나 만들면 된다.

 

: Specialization

#include <Turboc.h>

 

template <class T>

void Swap(T &a, T &b)

{

     T t;

     t=a;a=b;b=t;

}

 

template <> void Swap<double &>(double &a, double &b)

{

     int i,j;

 

     i=(int)a;

     j=(int)b;

     a=a-i+j;

     b=b-j+i;

}

 

void main()

{

     double a=1.2,b=3.4;

     printf("before a=%g, b=%g\n",a,b);

     Swap(a,b);

     printf("after a=%g, b=%g\n",a,b);

}

 

Swap 함수 템플릿을 정의해 두고 double형에 대해서 특별한 Swap 함수를 따로 정의했다. double에 대해 특수화된 Swap 함수의 본체는 정수부만 교환하는 고유한 코드를 가진다. main 에서는 두 개의 실수를 Swap 함수로 교환했는데 실행 결과는 다음과 같다.

 

before a=1.2, b=3.4

after a=3.2, b=1.4

 

만약 double에 대한 특수화를 하지 않으면 일반적인 Swap 함수가 호출되어 소수부, 실수부가 같이 바뀔 것이다. 컴파일러는 템플릿 함수 호출 구문이 있을 때 항상 템플릿의 정의보다 특수화된 정의에 우선권을 주므로 동일한 이름의 템플릿과 특수화 함수가 존재하면 특수화된 함수가 호출된다. 특수화 함수를 표기하는 방법은 여러 가지가 있다.

 

template <> void Swap<double &>(double &a, double &b)

template <> void Swap<>(double &a, double &b)

template <> void Swap(double &a, double &b)

void Swap<double &>(double &a, double &b)

void Swap<>(double &a, double &b)

void Swap(double &a, double &b)

 

특수화된 함수라는 것을 표시하기 위해 template <> 로 시작하는데 <>가 없으면 명시적 구체화 구문이 되므로 잘 구분해야 한다. 함수 이름 뒤에는 어떤 타입에 대한 특수화 함수인지 <> 괄호와 특수화된 타입 이름을 밝힌다. ①번 표기법이 가장 완전한 표기법이되 좀 더 간략한 표기법도 쓸 수 있다. 어떤 타입에 대해 특수화되었는지는 어차피 인수의 타입으로도 알 수 있으므로 함수명 다음의 <> 괄호는 생략 가능하며 <>만 남겨 두고 타입만 생략하는 것도 가능하다. 단, 템플릿 인수가 리턴 타입이나 내부 지역변수로 사용될 때는 반드시 ①번 타입만 가능하다.

또한 ④번처럼 함수명 다음에 <> 괄호가 있다면 이 표기로부터 함수 템플릿에 대한 특수화 함수라는 것을 알 수 있으므로 앞쪽의 template <> 도 생략 가능하다. 이 표기법은 구형 컴파일러들이 주로 사용하던 방법이며 여기서 <> 괄호안에 타입명을 생략해도 상관없다. 대부분의 컴파일러가 아직까지도 이 표기법을 지원하고는 있지만 최신 표준에는 이 표기법이 인정되지 않으므로 가급적 사용을 자재해야 한다.

 특수화 함수를 표기하는 방법이 왜 이렇게 많은가 하면 템플릿이라는 기능이 처음부터 표준에 의해 정립된 것이 아니라 각 컴파일러 제작사들에 의해 비공식적으로 발전해 오다가 비교적 최근에 표준으로 채택되었기 때문이다. 표준이 제정되었다고 해서 이전에 사용하던 형식을 무시할 수는 없기 때문에 이런 많은 표기법들이 난무하는 상황이 되었는데 이런 면을 보면 표준이 얼마나 중요한가를 알 수 있다. 표준 제정이 늦어지면 변종들이 생겨 여러 사람들이 피곤해진다.

마지막 ⑥번 형식은 특수화 함수가 아니라 그냥 일반 함수 Swap을 정의하는 것이다. 이렇게 일반 함수를 정의해도 일단은 목적을 이룰 수 있지만 우선 순위의 문제가 있어 바람직하지 않다. 함수 템플릿과 특수화된 함수, 그리고 일반 함수가 동시에 존재할 경우 어떤 함수를 우선적으로 선택할 것인가는 컴파일러마다 다르다. 만약 일반 함수가 템플릿 함수보다 우선 순위가 늦다면 지정한 타입에 대해 특수한 처리를 할 수 없을 것이다.


2. 클래스 탬플릿

타입만 다른 클래스들

클래스 템플릿은 함수 템플릿과 비슷하되 찍어내는 대상이 클래스라는 것만 다르다. 구조나 구현 알고리즘은 동일하되 멤버들의 타입만 다를 경우 클래스를 일일이 따로 만드는 대신 템플릿을 정의한 후 템플릿으로부터 클래스를 만들 수 있다. 실용적 가치는 별로 없지만 화면상의 특정 좌표에 출력될 값에 대한 정보를 표현하는 클래스를 만들어 보자. 정보의 타입에 따라 값을 표현하는 멤버의 타입이 달라지므로 타입에 따라 클래스를 일일이 만들어야 한다.

 

class PosValueInt

{

private:

     int x,y;

     int value;

public:

     PosValue(int ax, int ay, int av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

class PosValueChar

{

private:

     int x,y;

     char value;

public:

     PosValue(int ax, int ay, char av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

class PosValueDouble

{

private:

     int x,y;

     double value;

public:

     PosValue(int ax, int ay, double av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

좌표값 x, y는 모든 클래스에서 int형이며 값을 표현하는 value 멤버의 타입만 달라진다. 클래스는 함수에서와 같은 오버로딩이 지원되지 않으므로 이름을 모두 다르게 작성해야 하며 value의 타입에 따라 생성자의 원형도 각기 다르다. 결국 실제로 다른 부분은 value의 타입뿐이며 나머지는 모두 동일하므로 이 클래스들을 하나의 템플릿으로 통합할 수 있다.

 

: PosValueTemp

#include <Turboc.h>

#include <iostream>

using namespace std;

 

template <typename T>

class PosValue

{

private:

     int x,y;

     T value;

public:

     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

template <typename T>

void PosValue<T>::OutValue()

{

     gotoxy(x,y);

     cout << value << endl;

}

 

void main()

{

     PosValue<int> iv(1,1,2);

     PosValue<char> cv(5,1,'C');

     PosValue<double> dv(30,2,3.14);

     iv.OutValue();

     cv.OutValue();

     dv.OutValue();

}

 

클래스 선언문앞에 template <typename T>를 붙이고 타입에 종속적인 부분에만 T를 사용하면 된다. 예제의 PosValue 클래스는 템플릿 인수로 전달받은 타입 T를 value의 타입으로 선언하였고 생성자의 세 번째 인수도 T형이 된다. 이렇게 정의된 클래스 타입의 객체를 생성할 때 클래스 이름 다음의 < > 괄호안에 원하는 타입을 밝혀야 한다. 클래스 템플릿으로부터 만들어지는 클래스를 템플릿 클래스라고 하는데 템플릿 클래스의 타입명에는 < > 괄호가 항상 따라 다닌다. value가 int형인 클래스의 이름은 PosValue<int>이고 value가 char형인 클래스의 이름은 PosValue<char>이다. 단 예외적으로 생성자의 이름은 원래 클래스의 이름을 따라가지만 클래스 템플릿의 경우 템플릿 이름을 그대로 쓰며 < > 괄호를 붙이지 않는다.


이미지를 클릭하시면 창이 닫힙니다













클래스 템플릿의 멤버 함수를 선언문 외부에서 작성할 때는 템플릿에 속한 멤버 함수임을 밝히기 위해 소속 클래스의 이름에도 <T>를 붙여야 하며 T가 템플릿 인수임을 명시하기 위해 template <typename T>가 먼저 와야 한다. OutValue 멤버 함수는 PosValue<T> 클래스 소속이며 이때 T는 템플릿 인수 목록으로 전달된 타입의 이름이다. 함수 본체 내에서는 T를 언제든지 참조할 수 있다. 클래스 선언문 내부에서 인라인으로 함수를 선언할 때는 클래스 선언문앞에 T에 대한 설명이 있으므로 이렇게 하지 않아도 상관없다.

 

template <typename T>

class PosValue

{

     ....

     void OutValue() {

          gotoxy(x,y);

          cout << value << endl;

     }

};

 

클래스 템플릿으로부터 객체를 선언할 때는 템플릿 이름 다음에 < >괄호를 쓰고 괄호안에 T로 전달될 타입의 이름을 명시해야 한다. PosValue<int>는 int타입의 value를 멤버로 가지는 PosValue 템플릿 클래스를 의미하며 PosValue<double> 클래스의 value는 double 타입이 된다. 템플릿 클래스의 이름에는 타입이 분명히 명시되어야 한다. PosValue라는 명칭은 어디까지나 템플릿의 이름일 뿐이므로 이 이름으로부터 객체를 생성할 수는 없다.

컴파일러는 객체 선언문에 있는 초기값의 타입으로부터 어떤 타입에 대한 클래스를 원하는지 알 수 있을 것도 같다. 예를 들어 PosValue iv(1,1,2)라고 쓰면 제일 마지막 인수가 int형 상수이므로 PosValue<int> 타입이라고 유추 가능할 것이다. 그러나 생성자가 오버로딩되어 있을 경우 이 정보만으로는 원하는 타입을 정확하게 판단하기 어렵다. 또한 생성자를 호출하기 전에 객체를 위한 메모리를 할당해야 하는데 이 시점에서 생성할 객체의 크기를 먼저 계산할 수 있어야 하므로 클래스 이름에 타입이 명시되어야 한다.

함수에서와 마찬가지로 클래스 템플릿도 단순한 선언에 불과하며 컴파일러는 이 템플릿의 모양을 기억해 두었다가 객체가 생성될 때 전달된 타입에 맞는 클래스 정의를 구체화한다. 만약 클래스 템플릿 선언만 있고 객체를 생성하지 않는다면 템플릿은 무시된다. main에서 int, char, double 타입의 PosValue 객체를 각각 선언했는데 이 선언문에 의해 세 개의 클래스가 구체화될 것이다. 확인을 위해 세 개의 객체를 만든 후 OutValue 함수를 호출하여 각 좌표에 값을 출력해 보았다.

템플릿으로부터 만들어지는 클래스도 분명히 클래스이며 일반적인 클래스와 전혀 다를 바가 없다. 템플릿 클래스로부터 상속하는 것도 가능하며 문법도 동일하되 기반 클래스의 이름에 < > 괄호가 사용되는 차이밖에 없다. 다음 클래스는 PosValue<int>로부터 새로운 클래스를 파생한다.

 

class PosValue2 : public PosValue<int> { ... }

 

템플릿 클래스가 다른 클래스의 기반 클래스로 사용되면 컴파일러는 클래스를 즉시 구체화된다. 설사 이 클래스의 인스턴스 선언문이 없더라도 말이다.

템플릿으로부터 만들어지지 않은 일반 클래스의 멤버 함수만 템플릿으로 선언하는 것도 가능하다. 멤버 함수도 분명히 함수이므로 타입에 따라 여러 벌이 필요하다면 원하는 함수 하나만 함수 템플릿으로 만들면 된다. 다음 예제는 그 예를 보여 준다.

 

: TempMember

#include <Turboc.h>

#include <iostream>

using namespace std;

 

class Some

{

private:

     int mem;

 

public:

     Some(int m) : mem(m) { }

     template <typename T>

     void memfunc(T a) {

          cout << "템플릿 인수 = " << a << ", mem = " << mem << endl;

     }

};

 

void main()

{

     Some s(9999);

 

     s.memfunc(1234);

     s.memfunc(1.2345);

     s.memfunc("string");

}

 

Some 클래스에는 함수 템플릿이 하나 포함되어 있으며 이 함수는 임의 타입 T형의 변수 a를 인수로 전달받아 그 값을 화면으로 출력한다. 실제 어떤 멤버 함수가 호출되는가에 따라 클래스 Some의 멤버 함수 개수가 결정될 것이다.


템플릿의 위치

클래스 템플릿 선언문은 반드시 사용하기 전에 와야 한다. PosValueTemp 예제에서 보다시피 main 함수보다 템플릿 선언이 더 위에 있는데 이 순서가 바뀌게 되면 main에서 참조하는 PosValue<int>, PosValue<char>가 무엇을 의미하는지 모르므로 에러로 처리될 것이다. 단, 템플릿 클래스의 멤버 함수 본체 정의문은 앞쪽에 이미 소속과 원형이 선언되어 있으므로 main 함수보다 뒤에 있어도 상관없다.

예제 수준에서는 한 파일안에 클래스 선언과 멤버 함수의 정의, 그리고 이 클래스를 사용하는 테스트 코드까지 모두 같이 작성하는 것이 편리하지만 실제 프로젝트에서는 클래스별로 모듈을 구성하는 것이 일반적이다. 클래스 템플릿의 경우도 마찬가지로 별도의 모듈을 작성할 수 있는데 이때 템플릿 선언문과 멤버 함수의 정의까지 모두 헤더 파일에 작성되어야 한다.

멤버 함수를 정의하는 함수 템플릿은 실제로 함수의 본체를 만드는 것이 아니므로 구현 파일에 작성해서는 안된다. 만약 PosValue 클래스 템플릿은 PosValue.h에서 선언하고 이 클래스에 속한 멤버 함수에 대한 정의는 PosValue.cpp에 다음과 같이 따로 작성한다고 해 보자.

 

#include "PosValue.h"

 

template <typename T>

void PosValue<T>::OutValue()

{

     gotoxy(x,y);

     cout << value << endl;

}

 

이렇게 되면 OutValue 함수는 PosValue.cpp 안에서만 알려지므로 다른 모듈에 있는 main 함수에서는 OutValue가 정의되지 않은 것으로 인식되어 에러로 처리된다. 일반 함수는 컴파일시에 원형만 선언하면 컴파일 가능하고 링크할 때 바인딩되는데 비해 템플릿은 컴파일할 때 구체화되어야 하므로 같은 번역 단위안에 선언이 있어야 한다.

템플릿은 만들고자 하는 클래스와 멤버 함수의 모양을 컴파일러에게 알리기만 할 뿐이지 그 자체가 코드를 생성하는 것은 아니며 따라서 외부로 알려지지 않는다. 클래스 템플릿은 헤더 파일에 작성하는 것이 원칙적이며 실제 코드를 생성하는 것이 아니므로 설사 이 헤더 파일을 여러 모듈에서 인클루드하더라도 중복 정의되었다는 에러가 발생하지는 않는다. 한 모듈에서 #define을 두 번 하면 안되지만 #define 문이 있는 헤더를 각 모듈에서 인클루드해도 문제가 안되는 것과 같다.

그러나 헤더 파일에 클래스 템플릿을 두게 되면 최종 사용자에게 이 클래스의 코드를 숨길 수 없다는 단점이 있다. 기술적으로 중요한 사항을 담고 있는 클래스의 소스가 누출될 수 있는 보안상의 문제가 있는 것이다. 그래서 최신 C++ 표준은 cpp 파일에 클래스 템플릿의 멤버 함수를 정의할 수 있는 export 키워드를 도입하고 이 키워드를 사용하면 구현 파일에 정의된 멤버 함수가 외부로도 알려지도록 한다. 템플릿 선언앞에 export를 붙이면 된다.

 

export template <typename T>

void PosValue<T>::OutValue() { ... }

 

그러나 이 키워드는 몇몇 시험적인 컴파일러들만 지원하고 있을 뿐이며 비주얼 C++, gcc를 포함한 대부분의 컴파일러에서 아직 지원하지 않는다. 표준으로 채택되었음에도 불구하고 지원하지 못하는 컴파일러가 많은 이유는 이 키워드가 전통적인 모듈 분할 방식의 컴파일러와는 잘 맞지 않기 때문이다. C/C++ 컴파일러는 번역 단위별로 컴파일하여 링크할 때 합치는 방식인데 export로 지정된 함수에 대해서는 모든 번역 단위에 대해서도 그 정의를 알려야 하므로 근본적인 컴파일 방식을 바꾸기 전에는 지원하기가 대단히 어렵다. 안타깝게도 이 키워드는 당분간은 쓸 수 없으며 그래서 템플릿 라이브러리들은 거의 대부분 소스가 공개되어 있다.


비타입 인수

템플릿의 인수 목록에 전달되는 것은 통상 타입이다. 알고리즘은 같되 타입만 다른 함수나 클래스를 작성하고 싶을 때 템플릿을 사용한다. 그러나 클래스 템플릿의 경우는 타입이 아닌 상수를 템플릿 인수로 전달할 수 있는데 이를 비타입 인수(Nontype Argument)라고 한다.

다음 예제의 Array 클래스는 임의의 타입에 대한 배열을 정의하고 배열 요소의 값을 변경하거나 읽는 기능을 제공한다. 임의의 타입에 대한 배열을 만들기 위해 타입 이름을 템플릿 인수로 전달받으며 배열의 크기 지정을 위해 정수 상수를 전달받는다.

 

: NonTypeArgument

#include <Turboc.h>

 

template <typename T, int N>

class Array

{

private:

     T ar[N];

public:

     void SetAt(int n,T v) { if (n < N && n >=0) ar[n]=v; }

     T GetAt(int n) { return (n < N && n >=0 ? ar[n]:0); }

};

 

void main()

{

     Array<int,5> ari;

     ari.SetAt(1,1234);

     ari.SetAt(1000,5678);

     printf("%d\n",ari.GetAt(1));

     printf("%d\n",ari.GetAt(5));

}

 

기능상 단순 배열과 유사하지만 좀 더 안전한 액세스를 지원하는데 요소값을 읽거나 쓰는 Get(Set)At 함수가 전달된 첨자의 범위를 점검하므로 실수로 범위 바깥을 액세스해도 치명적인 에러를 발생시키지 않는다. 이외에 확장하기에 따라서는 얼마든지 다양한 기능을 더 넣을 수 있을 것이다. main에서는 크기 5의 정수형 배열 ari를 선언하고 배열에 값이 잘 기억되는지 확인해 보았으며 엉뚱한 첨자에 대해 방어를 제대로 하는지도 테스트해 보았다. 1234와 0이라는 값이 출력되면 정상적으로 동작하는 것이다.

Array 클래스의 T형 배열 멤버 ar은 크기 N을 가지는데 이 크기는 객체를 선언할 때 인수로 주어지는 정수 상수이다. 템플릿으로 전달되는 인수가 타입이 아니므로 비타입 인수라고 한다. 컴파일러는 ari 선언문에 명시되어 있는 타입 Array<int,5>로부터 다음과 같은 클래스를 구체화할 것이다.

 

class Array

{

private:

     int ar[5];

public:

     void SetAt(int n,int v) { if (n < 5 && n >=0) ar[n]=v; }

     int GetAt(int n) { return (n < 5 && n >=0 ? ar[n]:0); }

};

 

임의 타입에 대해 임의 크기까지를 지원하는 안전 배열 클래스를 만들고 싶다면 이런 비타입 인수를 사용할 수 있다. 물론 이 경우 임의 크기를 지원하는 더 좋은 방법은 생성자의 인수로 전달되는 값으로 동적할당을 하는 것이다. 포인터형 멤버 변수를 선언하면 필요한만큼 할당할 수 있으며 원할 경우 실행중에라도 크기를 마음대로 바꿀 수도 있어 굉장히 신축적이다.

그러나 알다시피 동적할당은 생성자와 파괴자를 요구하고 정확하게 동작하기 위해서는 복사 생성자, 대입 연산자가 반드시 적절하게 정의되어야 하며 상속 관계를 고려하면 대부분의 멤버 함수들은 가상 함수가 되어야 한다. 좋기는 하지만 코드가 져야 할 부담이 너무 많은 것이다. 이럴 때 동적할당 대신 필요한 크기만큼의 요소를 정적으로 가지는 클래스를 만들어 쓰면 속도도 빠르고 위험하지도 않으며 무엇보다 단순해서 좋다.

실행중에 크기를 결정하기 힘든 중요한 상수에 대해서는 이런 식으로 템플릿과 비타입 인수를 사용할 수 있다. 크기가 다른 객체를 선언할 때마다 클래스가 구체화된다는 점에서 낭비가 조금 있기는 하지만 말이다. 클래스 선언문의 템플릿 인수가 다르면 객체의 타입도 달라진다. 컴파일러는 완전히 같지 않은 템플릿 인수에 대해서는 개별적으로 구체화를 하기 때문이다. 심지어 멤버 함수들도 전부 따로 만들어진다. 다음 코드를 보자.

 

Array<int,5> ari;

Array<int,5> ari2;

Array<int,6> ari3;

 

ari=ari2;

ari=ari3;        // 에러

 

ari와 ari2는 같은 타입이므로 서로 대입 가능하지만 ari3를 ari에 대입하는 것은 에러이다. 왜냐하면 ari는 Array<int,5> 타입이고 ari3는 Array<int,6> 타입이기 때문이다. 물론 Array<double,5> 타입도 Array<int, 5>와 호환되지 않는 다른 타입이다. 클래스 선언문의 비타입 인수는 반드시 상수여야 하며 실행중에 값이 결정되는 변수는 인수로 사용할 수 없다. 다음 선언문은 에러로 처리된다.

 

int size=5;

Array<int,size> ari;

 

size는 변수이며 이 값은 실행중에 수시로 변할 수 있으므로 템플릿의 인수로 사용할 수 없다. 템플릿이란 컴파일러가 인수를 적용하여 컴파일 중에 클래스를 만들어 내는 형틀이므로 모든 정보를 컴파일중에 알 수 있어야 한다. 실행중에 없던 클래스를 만들어내는 기능이 아니라 컴파일 중에 구체화해야 하므로 변수는 쓸 수 없다. 물론 const int size=5; 로 상수 선언했다면 가능하다.

함수로도 비타입 인수를 전달할 수 있다. 단, 함수의 형식 인수 목록에 어떤 상수가 올 수는 없으므로 비타입 인수는 함수의 본체에서만 사용해야 하며 함수 호출문에 템플릿 인수를 명시적으로 지정해야 한다. 다음 예제는 비타입 인수 N이 지정하는 크기만큼의 지역 배열을 선언하는 함수 템플릿이다.

 

: NonTypeArgFunc

#include <Turboc.h>

 

template <int N>

void func(void)

{

     int ar[N];

 

     printf("배열 크기=%d\n",N);

}

 

void main()

{

     func<5>();

     func<8>();

}

 

main에서 func 함수를 두 번 호출했는데 비타입 인수 N으로 5와 8을 전달했다. 이 두 함수는 지역변수의 선언문이 다르므로 각각 따로 구체화되어야 한다. 비타입 인수는 함수의 인수와는 용도가 다른데 함수의 형식 인수는 실행 시간에 전달되는 변수이므로 배열 선언문 등 상수가 필요한 곳에 사용할 수 없지만 비타입 인수는 구체화될 때 함수 본체에 직접 기입되므로 상수일 수 있다.

func 템플릿은 비타입 인수 N을 요구하므로 func()라고만 호출해서는 지역 배열의 크기를 결정할 수 없으므로 함수를 구체화할 수 없다. 반드시 명시적인 템플릿 인수를 전달해야 한다. 이 예제는 gcc, 비주얼 C++ 7.0이상의 최신 컴파일러에서는 잘 실행되지만 비주얼 C++ 6.0에서는 컴파일되지 않는다. 비주얼 C++ 6.0은 클래스의 비타입 인수는 지원하지만 함수의 비타입 인수는 아직 지원하지 못한다.


디폴트 템플릿 인수

함수의 디폴트 인수는 함수를 호출할 때 생략된 인수에 대해 기본적으로 적용되는 값이다. 클래스 템플릿에도 이와 비슷한 개념인 디폴트 템플릿 인수가 있는데 객체 선언문에서 인수를 생략할 경우 템플릿 선언문에서 지정한 디폴트가 적용된다. 사용하는 표기법이나 주의 사항도 대체로 함수의 경우와 동일하다. 예를 들어 PosValue 클래스 템플릿의 T에 int라는 디폴트 타입을 지정하고 싶다면 다음과 같이 템플릿을 작성한다.

 

template <typename T=int>

class PosValue

{

     ....

 

< > 괄호안의 타입 이름 다음에 = 구분자를 쓰고 디폴트로 적용될 타입을 지정한다. 이제 별다른 지정이 없으면 T는 디폴트 타입인 int가 된다. 정수형의 PosValue 객체를 선언할 때 다음과 같이 간단하게 클래스 타입을 지정할 수 있다. 타입 지정없이 빈 < >괄호만 쓰면 된다.

 

PosValue<> iv(1,1,2);

 

물론 PosValue<double>과 같이 타입을 분명히 밝히면 디폴트는 무시될 것이다. 디폴트를 그대로 받아들일 경우는 타입 지정을 하지 않으면 되는데 그렇더라도 빈 괄호 < >는 꼭 있어야 한다. 타입을 여러 개 가지는 클래스의 경우 오른쪽 인수부터 차례대로 디폴트를 지정할 수 있으며 객체를 선언할 때는 오른쪽부터 순서대로 생략 가능하다. 이 점도 함수의 디폴트 인수와 같다.

클래스 템플릿에는 디폴트 인수를 줄 수 있지만 함수 템플릿에는 디폴트를 정의할 수 없다. 클래스는 객체를 선언할 때 클래스 타입을 지정하므로 생략 가능하지만 함수는 호출할 때 실인수의 타입을 보고 구체화할 함수를 결정한다. 실인수가 생략되어 버리면 도대체 어떤 타입의 함수를 원하는지 컴파일러가 알 방법이 없기 때문이다.


특수화

클래스 템플릿도 함수 템플릿과 마찬가지로 실제 클래스 타입이 사용될 때만 구체화된다. 만약 특정 타입에 대해 미리 클래스 선언을 만들어 놓을 필요가 있다면 명시적 구체화를 할 수 있다. 예를 들어 float 타입의 PosValue 클래스를 미리 정의해 두고 싶다면 다음과 같이 한다.

 

template class PosValue<float>;

 

이 선언에 의해 컴파일러는 PosValue<float> 클래스를 미리 생성한다. 설사 이런 타입의 객체를 당장 선언하지 않는다 하더라도 컴파일러는 클래스 선언과 클래스 소속의 멤버 함수들을 모두 구체화해 둘 것이다. 특정 타입에 대한 클래스를 따로 생성하는 특수화도 물론 지원된다. 다음 예제는 tag_Friend 타입에 대해 PosValue 클래스를 특수화한다.

 

: SpecializationClass

#include <Turboc.h>

#include <iostream>

using namespace std;

 

template <typename T>

class PosValue

{

private:

     int x,y;

     T value;

public:

     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

template <typename T>

void PosValue<T>::OutValue()

{

     gotoxy(x,y);

     cout << value << endl;

}

 

struct tag_Friend {

     char Name[10];

     int Age;

     double Height;

};

 

template <> class PosValue<tag_Friend>

{

private:

     int x,y;

     tag_Friend value;

public:

     PosValue(int ax, int ay, tag_Friend av) : x(ax),y(ay),value(av) { }

     void OutValue();

};

 

void PosValue<tag_Friend>::OutValue()

{

     gotoxy(x,y);

     cout << "이름:" << value.Name << ", 나이:" << value.Age

          << ", 키:" << value.Height << endl;

}

 

void main()

{

     PosValue<int> iv(1,1,2);

     tag_Friend F={"아무개",25,177.7};

     PosValue<tag_Friend> fv(2,2,F);

     iv.OutValue();

     fv.OutValue();

}

 

PosValue 클래스는 위치를 가지는 임의 타입의 값을 표현하는데 임의 타입이라고 했으므로 int, char, double 등의 표준 타입은 물론이고 구조체나 클래스 타입에 대해서도 동작해야 한다. 그러나 OutValue 멤버 함수가 값 출력을 위해 cout 표준 출력 객체를 사용하기 때문에 사실상 cout이 인식하는 타입에 대해서만 지원하는 셈이다. tag_Friend 구조체 타입에 대한 PosValue 클래스를 작성하려면 이 타입에 대한 특수화된 버전을 만들고 OutValue 함수의 코드를 조금 다르게 작성할 필요가 있다. 특수화를 할 때는 다음 형식으로 클래스를 정의한다.

 

template<> class 클래스명<특수타입>

 

이렇게 정의하면 지정한 타입에 대해 특수화된 클래스를 생성한다. 인수의 타입이 이미 결정되어 있으므로 특수화된 클래스의 멤버 함수를 외부에서 정의할 때는 template < >를 붙이지 않아도 상관없다. OutValue 함수는 tag_Friend 구조체의 각 멤버를 순서대로 출력하도록 수정했는데 원래의 PosValue 템플릿에 있는 OutValue와는 코드가 다르다. 실행해 보면 (2,2) 위치에 구조체 F의 내용이 출력될 것이다.

특수화를 하면 특수화된 클래스는 객체를 선언하지 않더라도 자동으로 구체화된다. 즉, 클래스 정의가 만들어지고 멤버 함수들은 컴파일되어 실행 파일에 포함된다. 따라서 특수화된 클래스에 대한 정의는 일반적인 템플릿 클래스와는 달리 헤더 파일에 작성해서는 안되며 구현 파일에 작성해야 한다. 예제에서는 구조체에 대해서도 PosValue 템플릿을 쓰기 위해 특수화를 사용했는데 사실 이보다 더 간단한 방법은 tag_Friend 구조체가 << 연산자를 오버로딩해서 기존 템플릿의 본체 코드를 지원하는 것이다.

부분 특수화(Partial Specialization)란 템플릿 인수가 여러 개 있을 때 그 중 하나에 대해서만 특수화를 하는 기법이다. 다음 템플릿을 보자.

 

template <typename T1, typename T2> class SomeClass { ... }

 

SomeClass 클래스 템플릿은 두 개의 인수를 가지므로 <int, int>, <int, double>, <short, unsigned> 등 두 타입의 조합을 마음대로 선택할 수 있다. 부분 특수화는 이 중 하나의 타입은 마음대로 선택하도록 그대로 두고 나머지 하나에 대해서만 타입을 강제로 지정하는 것이다. T2가 double인 경우에 대해서만 특수화를 하고 싶다면 다음과 같이 한다.

 

template <typename T1> class SomeClass<T1, double> { ... }

 

이 상태에서 SomeClass<int, unsigned>나 SomeClass<float, short>는 특수화되지 않은 버전의 템플릿으로부터 생성되지만 SomeClass<int, double>이나 SomeClass<char, double>은 부분 특수화된 템플릿으로부터 생성될 것이다. 두 번째 인수가 double인 클래스에 대해서만 부분적으로 특수화를 했기 때문이다. gcc는 부분 특수화를 지원하지만 비주얼 C++에서는 지원되지 않는다.


3. 컨테이녀

TDArray

C++이 지원하는 템플릿 개념은 사실 그다지 어렵지 않다. 하지만 여기에 구체화, 특수화, 템플릿 중첩, 프렌드와의 관계, 정적 멤버 등이 개입되면 상당히 복잡한 문법이 만들어지며 표기법도 생소해서 쉽게 익숙해지기 어렵다. 또한 잘 만들어 놓았더라도 템플릿 클래스의 타입이 길고 복잡해서 코드의 의미를 얼른 파악하기도 무척 어렵다. 이런 복잡성에도 불구하고 C++이 템플릿을 지원하는 이유는 컨테이너를 만들기 위해서라고 해도 과언이 아니다.

컨테이너(Container)란 객체의 집합을 다룰 수 있는 객체이다. 쉽게 말해서 배열이나 연결 리스트같은 것들을 컨테이너라고 하는데 동일한 타입(또는 호환되는 타입)의 객체들을 저장하며 이런 객체들을 관리할 수 있는 기능을 가지는 또 다른 객체이다. 2부에서 우리는 동적으로 크기를 변경할 수 있는 동적 배열을 만들어 본 바 있으며 또한 앞 장에서 동적 배열의 기능을 캡슐화한 DArray라는 클래스도 작성해 보았다.

이 클래스가 무척 실용적이라는 것은 경험해 보아서 알 것이고 클래스로 캡슐화하면 사용하기도 무척 편리하다. 이렇게 만들어진 DArray가 바로 컨테이너이다. 그러나 아직 아쉬움이 있는데 바로 ELETYPE이라는 매크로로 배열 요소의 타입을 결정해야 한다는 점이다. DArray 클래스를 사용하기 전에 ELETYPE을 원하는 타입으로 바꿔야 하고 int 배열과 double 배열을 동시에 사용할 수도 없어서 활용성이 크게 떨어진다.

이런 문제를 해결하기 위해 만들어진 문법이 바로 템플릿이다. 요소의 타입은 객체를 선언하는 시점으로 연기해 두고 일단 필요한 알고리즘만 템플릿에 작성한다. 이후 타입만 바꾸면 이 타입을 요소로 가지는 동적 배열 클래스를 만드는 작업은 컴파일러가 알아서 할 것이다. 다음 소스는 DArray 클래스의 템플릿 버전인 TDArray이며 원칙에 따라 TDArray.h라는 헤더 파일에 작성했다.

 

: TDArray.h

template <typename T>

class TDArray

{

protected:

     T *ar;

     unsigned size;

     unsigned num;

     unsigned growby;

 

public:

     TDArray(unsigned asize=100, unsigned agrowby=10);

     virtual ~TDArray();

     virtual void Insert(int idx, T value);

     virtual void Delete(int idx);

     virtual void Append(T value);

 

     T GetAt(int idx) { return ar[idx]; }

     unsigned GetSize() { return size; }

     unsigned GetNum() { return num; }

     void SetAt(int idx, T value) { ar[idx]=value; }

     void Dump(char *sMark);

};

 

template <typename T>

TDArray<T>::TDArray(unsigned asize, unsigned agrowby)

{

     size=asize;

     growby=agrowby;

     num=0;

     ar=(T *)malloc(size*sizeof(T));

}

 

template <typename T>

TDArray<T>::~TDArray()

{

     free(ar);

}

 

template <typename T>

void TDArray<T>::Insert(int idx, T value)

{

     unsigned need;

 

     need=num+1;

     if (need > size) {

          size=need+growby;

          ar=(T *)realloc(ar,size*sizeof(T));

     }

     memmove(ar+idx+1,ar+idx,(num-idx)*sizeof(T));

     ar[idx]=value;

     num++;

}

 

template <typename T>

void TDArray<T>::Delete(int idx)

{

     memmove(ar+idx,ar+idx+1,(num-idx-1)*sizeof(T));

     num--;

}

 

template <typename T>

void TDArray<T>::Append(T value)

{

     Insert(num,value);

}

 

template <typename T>

void TDArray<T>::Dump(char *sMark)

{

     unsigned i;

     cout << sMark << " => 크기=" << size << ",개수=" << num << " : ";

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

          cout << GetAt(i) << ' ';

     }

     cout << endl;

}

 

DArray에 비해 어떤 점이 바뀌었는지 보자. 일단 이름이 바뀌었는데 굳이 이름을 바꿀 필요는 없지만 템플릿 버전이라는 것을 분명히 표시하기 위해 앞에 T자를 하나 더 붙였다. ELETYPE 매크로는 사라졌으며 클래스 정의문앞에 template <typename T>가 추가되었고 소스내의 모든 ELETYPE은 T로 대체했다. 이제 배열 요소의 타입은 매크로가 아닌 템플릿 인수에 의해 결정되며 객체를 선언할 때마다 원하는 타입을 지정할 수 있다.

멤버 함수의 소속은 모두 TDArray<T> 클래스가 되며 함수 본체의 ELETYPE은 T로 바뀐다. 멤버 함수의 코드는 원칙적으로 변경할 필요가 없다. 기존 클래스가 템플릿화되면서 꼭 바뀌어야 하는 부분은 사실상 없는 셈이며 만약 있다면 이는 그 클래스가 템플릿화를 할만큼 충분히 일반화되지 못한 것이다.

이제 TDArray는 임의의 타입에 대한 동적 배열을 만들 수 있는 클래스 템플릿이 되었으며 이 안에는 동적 배열을 관리하는 모든 알고리즘이 포함되어 있다. 어디까지나 템플릿일 뿐이므로 아직 클래스는 아니지만 원하는 타입과 함께 객체를 선언하면 컴파일러에 의해 구체화된다. 제대로 동작하는지 예제를 작성해 보자.

 

: TDArrayTest

#include <Turboc.h>

#include <iostream>

using namespace std;

#include "TDArray.h"

 

void main()

{

     TDArray<int> ari;

     TDArray<double> ard;

     int i;

 

     for (i=1;i<=5;i++) ari.Append(i);

     ari.Dump("5개 추가");

     for (i=1;i<=3;i++) ard.Append((double)i*1.23);

     ard.Dump("3개 추가");

}

 

TDArray.h 헤더 파일만 포함하면 템플릿이 정의된다. main에서는 정수형 동적 배열 TDArray<int> 타입의 객체 ari와 실수형 동적 배열 TDArray<double> 타입의 ard 객체를 선언했으며 잘 동작하는지 확인해 보기 위해 값을 추가한 후 Dump만 해 보았다.

 

5개 추가 => 크기:100,개수:5 값 : 1 2 3 4 5

3개 추가 => 크기:100,개수:3 값 : 1.23 2.46 3.69

 

정수에 대해서나 실수에 대해서나 TDArray는 잘 작동함을 확인할 수 있다. TDArray.h 헤더 파일만 포함시키고 객체를 선언할 때 원하는 타입만 밝히면 임의 타입에 대해 동작하는 동적 배열을 쉽게 사용할 수 있다. TDArray는 임의 타입에 대해서 잘 동작하는 배열이기는 하지만 모든 경우에 두루 쓸 수 있는 일반성을 갖추지는 못했다. 내부에서 동적 할당을 하므로 복사 생성자와 대입 연산자를 원칙대로 적절히 정의해야 한다.

또한 동적 할당되는 포인터에 대한 배열이나 클래스에 대한 배열로 쓰기에는 조금 불편한 점이 있다. 포인터의 경우 삭제할 때 포인터가 가리키는 곳도 해제하는 것이 좋을 것이고 객체의 경우 생성자와 파괴자도 호출해 주면 편리하다. 물론 그렇다고 해서 TDArray를 포인터나 객체의 배열로 쓸 수 없다는 얘기는 아니며 외부에서 관리해야 한다는 점이 불편할 뿐이다. TDArray는 어디까지나 예제일 뿐이고 훨씬 더 잘 만들어진 동적 배열 템플릿(예:vector, CTypePtrArray) 들이 많이 공개되어 있으므로 실무를 할 때는 이런 것들을 쓰기 바란다.


TStack

다음 예제는 19장에서 작성했던 정수형 스택을 클래스로 만든 것이다. 스택을 표현하는데 필요한 정수형 배열과 배열 크기, 스택 포인터 등을 멤버 변수로 포함시키고 Push, Pop 등의 기본 동작은 멤버 함수로 구현했다. 스택을 초기화하는 기능은 생성자에 작성하고 해제하는 코드는 파괴자에 두면 된다.

 

: iStack

#include <Turboc.h>

 

class iStack

{

private:

     int *Stack;

     int Size;

     int Top;

 

public:

     iStack(int aSize) {

          Size=aSize;

          Stack=(int *)malloc(Size*sizeof(int));

          Top=-1;

     }

     virtual ~iStack() {

          free(Stack);

     }

     virtual BOOL Push(int data) {

          if (Top < Size-1) {

              Top++;

              Stack[Top]=data;

              return TRUE;

          } else {

              return FALSE;

          }

     }

     virtual int Pop() {

          if (Top >= 0) {

              return Stack[Top--];

          } else {

              return -1;

          }

     }

};

 

void main()

{

     iStack iS(256);

     iS.Push(7);

     iS.Push(0);

     iS.Push(6);

     iS.Push(2);

     iS.Push(9);

     printf("%d\n",iS.Pop());

     printf("%d\n",iS.Pop());

     printf("%d\n",iS.Pop());

     printf("%d\n",iS.Pop());

     printf("%d\n",iS.Pop());

}

 

스택이 필요할 때는 iStack 클래스의 인스턴스를 생성하는데 인수로 스택의 크기만 밝히면 나머지 필요한 초기화는 생성자에서 한다. 그리고 Push, Pop 등의 멤버 함수를 호출하여 스택을 편리하게 사용할 수 있다. 다 사용한 스택은 파괴자에서 정리하므로 별도의 해제를 할 필요가 없다.

동작에 필요한 필수 멤버들이 한 클래스에 캡슐화되어 있으므로 확실히 사용하기에는 편리하다. 그러나 아직 타입에 대한 종속성을 해결하지는 못했는데 iStack은 정수형 값만 저장할 수 있을 뿐이며 double이나 char 형을 저장할 수는 없다. 다른 타입에 대한 스택을 만들려면 iStack의 일부를 수정해야 하는데 Stack 멤버의 타입, Push의 인수, Pop의 리턴 타입 정도만 바꾸면 된다. 타입만 변경될 뿐 알고리즘은 동일하므로 템플릿을 사용하면 임의 타입을 지원할 수 있다.

19장에서 실수형 스택과 문자형 스택을 사용하여 수식을 계산하는 TextCalc 예제를 만들어 본 적이 있는데 잘 동작하기는 하지만 똑같은 논리를 사용하는 스택이 두 카피 존재한다는 점이 무척 불합리해 보인다. 단지 타입만 다를 뿐인데 이것들을 하나로 합칠 수가 없는 것이다. 이렇게 되면 코드의 양이 많아지는 것은 물론이고 기능을 확장할 때도 일일이 두 군데를 고쳐야 하므로 유지하기가 아주 어려워진다. 템플릿을 사용하여 스택 하나로 두 가지 타입을 지원하도록 수정해 보자. 먼저 스택 클래스 템플릿을 헤더 파일에 작성한다.

 

: TStack.h

template<typename T>

class TStack

{

protected:

     T *Stack;

     int Size;

     int Top;

 

public:

     TStack(int aSize) {

          Size=aSize;

          Stack=(T *)malloc(Size*sizeof(T));

          Top=-1;

     }

     virtual ~TStack() {

          free(Stack);

     }

     virtual BOOL Push(T data) {

          if (Top < Size-1) {

              Top++;

              Stack[Top]=data;

              return TRUE;

          } else {

              return FALSE;

          }

     }

     virtual T Pop() {

          return Stack[Top--];

     }

     virtual int GetTop() { return Top; }

     virtual T GetValue(int n) { return Stack[n]; }

};

 

템플릿 기반으로 수정했으므로 이름을 TStack이라고 지었으며 스택에 저장할 타입을 인수열로 전달받는다. 멤버 함수의 논리는 앞의 iStack 예제와 동일하며 int가 들어가야 할 위치에 T가 대신 들어갔을 뿐이다. 몇 가지 차이점도 있는데 우선 에러 처리를 위해 Top 위치를 조사하는 GetTop 멤버 함수와 우선 순위 조사를 위해 지정한 위치의 값을 삭제하지는 않고 읽기만 하는 GetValue 함수가 추가되었다.

그리고 Pop 함수의 에러 처리 코드가 삭제되었는데 임의의 타입에 대해 동작하기 위해서는 -1을 리턴하는 단순한 방법을 쓸 수 없기 때문이다. -1이라는 특이값은 수치형에만 존재하므로 스택에 객체를 저장할 때는 에러를 처리하는 다른 방법이 필요하다. 스택에 아무 것도 없는 상태에서 pop을 호출하는 것은 분명한 논리적 에러이므로 assert를 쓰는 것이 가장 합리적이다. 이 템플릿이 제대로 동작하는지 계산기 예제를 만들어 보자.

 

: TextCalcTemplate

#include <Turboc.h>

#include <math.h>

#include "TStack.h"

 

int GetPriority(int op)

{

     switch (op) {

     case '(':

          return 0;

     case '+':

     case '-':

          return 1;

     case '*':

     case '/':

          return 2;

     case '^':

          return 3;

     }

     return 100;

}

 

void MakePostfix(char *Post, const char *Mid)

{

     const char *m=Mid;

     char *p=Post,c;

     TStack<char> cS(256);

 

     while (*m) {

          // 숫자 - 그대로 출력하고 뒤에 공백 하나를 출력한다.

          if (isdigit(*m)) {

              while (isdigit(*m) || *m=='.') *p++=*m++;

              *p++=' ';

          } else

          // 연산자 - 스택에 있는 자기보다 높은 연산자를 모두 꺼내 출력하고 자신은 푸시한다.

          if (strchr("^*/+-",*m)) {

              while (cS.GetTop()!=-1 && GetPriority(cS.GetValue(cS.GetTop())) >=

                   GetPriority(*m)) {

                   *p++=cS.Pop();

              }

              cS.Push(*m++);

          } else

          // 여는 괄호 - 푸시한다.

          if (*m=='(') {

              cS.Push(*m++);

          } else

          // 닫는 괄호 - 여는 괄호가 나올 때까지 팝해서 출력하고 여는 괄호는 버린다.

          if (*m==')') {

              for (;;) {

                   c=cS.Pop();

                   if (c=='(') break;

                   *p++=c;

              }

              m++;

          } else {

              m++;

          }

     }

     // 스택에 남은 연산자들 모두 꺼낸다.

     while (cS.GetTop() != -1) {

          *p++=cS.Pop();

     }

     *p=0;

}

 

double CalcPostfix(const char *Post)

{

     const char *p=Post;

     double num;

     double left,right;

     TStack<double> dS(256);

 

     while (*p) {

          // 숫자는 스택에 넣는다.

          if (isdigit(*p)) {

              num=atof(p);

              dS.Push(num);

              for(;isdigit(*p) || *p=='.';p++) {;}

          } else {

              // 연산자는 스택에서 두 수를 꺼내 연산하고 다시 푸시한다.

              if (strchr("^*/+-",*p)) {

                   right=dS.Pop();

                   left=dS.Pop();

                   switch (*p) {

                   case '+':

                        dS.Push(left+right);

                        break;

                   case '-':

                        dS.Push(left-right);

                        break;

                   case '*':

                        dS.Push(left*right);

                        break;

                   case '/':

                        if (right == 0.0) {

                             dS.Push(0.0);

                        } else {

                             dS.Push(left/right);

                        }

                        break;

                   case '^':

                        dS.Push(pow(left,right));

                        break;

                   }

              }

              // 연산 후 또는 연산자가 아닌 경우 다음 문자로

              p++;

          }

     }

     if (dS.GetTop() != -1) {

          num=dS.Pop();

     } else {

          num=0.0;

     }

     return num;

}

 

double CalcExp(const char *exp,BOOL *bError=NULL)

{

     char Post[256];

     const char *p;

     int count;

    

     if (bError!=NULL) {

          for (p=exp,count=0;*p;p++) {

              if (*p=='(') count++;

              if (*p==')') count--;

          }

          *bError=(count != 0);

     }

 

     MakePostfix(Post,exp);

     return CalcPostfix(Post);

}

 

void main()

{

     char exp[256];

     BOOL bError;

     double result;

 

     char *p=strchr("^*/+-",NULL);

     strcpy(exp,"2.2+3.5*4.1");printf("%s = %.2f\n",exp,CalcExp(exp));

     strcpy(exp,"(34+93)*2-(43/2)");printf("%s = %.2f\n",exp,CalcExp(exp));

     strcpy(exp,"1+(2+3)/4*5+2^10+(6/7)*8");printf("%s = %.2f\n",exp,CalcExp(exp));

 

     for (;;) {

          printf("수식을 입력하세요(끝낼 때 0) : ");

          gets(exp);

          if (strcmp(exp,"0")==0) break;

          result=CalcExp(exp,&bError);

          if (bError) {

              puts("수식의 괄호짝이 틀립니다.");

          } else {

              printf("%s = %.2f\n",exp,result);

          }

     }

}

 

실행해 보면 잘 계산된다.

 

2.2+3.5*4.1 = 16.55

(34+93)*2-(43/2) = 232.50

1+(2+3)/4*5+2^10+(6/7)*8 = 1038.11

수식을 입력하세요(끝낼 때 0) : (1+2+3)*4

(1+2+3)*4 = 24.00

 

중위식을 후위식으로 변환하는 MakePostfix 함수는 문자형의 스택이 필요하므로 TStack<char> 타입의 cS를 256크기로 선언해서 사용한다. 이 선언문에 의해 컴파일러는 char 타입에 대한 스택 클래스를 구체화할 것이다. cS가 지역변수이므로 함수가 종료될 때 자동으로 파괴되며 따라서 별도의 정리 코드를 작성할 필요가 없다.

후위식을 연산하는 CalcPostfix 함수는 연산 과정의 중간값 저장을 위해 실수형 스택이 필요하다. 그래서 TStack<double> 타입의 dS를 선언해서 사용하고 있다. 두 함수가 각각 타입이 다른 스택을 만들어 사용하지만 클래스가 템플릿으로 선언되어 있으므로 아무 문제가 없다. 필요하다면 얼마든지 많은 타입에 대해 스택을 만들어 쓸 수 있을 것이다.


템플릿 중첩

템플릿의 인수열에 들어갈 수 있는 타입에는 특별한 제한이 없다. 기본 타입은 물론이고 클래스 타입도 템플릿의 인수열에 넣을 수 있다. 그렇다면 템플릿으로 만든 클래스도 분명히 타입의 일종이므로 다른 템플릿의 인수가 될 수 있다는 얘기인데 즉, 템플릿끼리 중첩될 수 있다. 다음 예제는 PosValue 템플릿 클래스를 요소로 가지는 스택을 정의한다.

 

: NestTemplate

#include <Turboc.h>

#include <iostream>

using namespace std;

#include "TStack.h"

 

template <typename T>

class PosValue

{

private:

     int x,y;

     T value;

public:

     PosValue() : x(0),y(0),value(0) { }

     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }

     void OutValue() {

          gotoxy(x,y);

          cout << value << endl;

     }

};

 

void main()

{

     TStack<PosValue<int> > sPos(10);

 

     PosValue<int> p1(5,5,123);

     PosValue<int> p2;

     sPos.Push(p1);

     p2=sPos.Pop();

     p2.OutValue();

}

 

선두에는 TStack 클래스 템플릿과 PosValue 클래스 템플릿이 선언되어 있으며 이 두 템플릿으로 임의의 타입에 대한 스택과 PosValue 객체를 만들 수 있다. 초기화되지 않은 객체를 만들 수 있도록 하기 위해 PosValue에 디폴트 생성자를 추가로 정의했다. main에서는 다소 복잡한 형식을 가지는 sPos라는 객체를 선언하고 있는데 이 객체는 일단은 TStack으로부터 만들어졌으므로 스택이다. 스택에 들어가는 요소는 인수열에 있는 PosValue<int> 타입이므로 이런 객체들의 임시 저장소가 된다.

main의 나머지 코드는 PosValue<int>형의 객체 p1, p2 둘을 선언하고 p1을 스택에 푸시한 후 p2로 팝해 보았다. 푸시한 값을 그대로 빼내 대입했으므로 p2가 p1과 같아질 것이다. p2의 값을 출력해 보면 p1의 생성자에서 초기화한 위치에 123이라는 값이 출력될 것이다. 컴파일러는 중첩된 선언문에서 안쪽 클래스부터 차례대로 구체화한다. 템플릿끼리 중첩되어 있을 뿐이지 별다른 사항은 없다. 단, 이런 중첩 선언문을 작성할 때 다음과 같이 작성해서는 안된다.

 

TStack<PosValue<int>> sPos(10);

 

템플릿 인수열안에 인수열이 있으므로 닫는 괄호 >가 두 번 연거푸 나오는데 이렇게 되면 컴파일러는 >>를 오른쪽 쉬프트 연산자로 해석하게 된다. 선언문에 연산자가 올 수 없으므로 이 문장은 에러로 처리될 것이다. 그래서 템플릿끼리 중첩될 때 인수열의 닫는 괄호 사이에는 반드시 공백을 하나 넣어 쉬프트 연산자와 구분되도록 해야 한다.

C++은 템플릿의 중첩을 문법적으로 허가하므로 이중 삼중으로 템플릿을 중첩할 수도 있다. 그러나 문법과는 별개로 템플릿끼리 중첩되려면 두 클래스가 임의의 타입에 대해서도 잘 동작할 수 있도록 충분히 일반화되어 있어야 한다. 대상 타입을 수치형으로 가정하여 -1같은 특이값을 사용해서는 안되며 대입 연산을 하는 요소는 대입 연산자를 적절하게 오버로딩해야 한다. 출력문으로 cout을 사용한다면 대상 타입은 << 연산자도 정의해야 한다.

템플릿끼리 중첩 가능하므로 자신이 자신을 포함하는 템플릿을 만들 수 있다. TDArray 클래스 템플릿은 임의의 타입을 배열 요소로 가질 수 있는데 그 타입을 TDArray로 준다면 동적 배열의 동적 배열을 만드는 것도 가능하다는 얘기이다. TDArray<TDArray<int> > ara;는 정수형을 요소로 가지는 동적 배열을 요소로 가지는 동적 배열 ara를 선언한 것이다.


템플릿 클래스 인수

템플릿 클래스의 타입명에는 항상 인수열이 같이 따라 다녀야 한다. 템플릿은 클래스를 만드는 선언문일 뿐이므로 그 자체가 타입이 될 수는 없으며 인수를 밝혀야만 타입이 될 수 있다. 다음 코드를 보자.

 

TStack<int> iS(10);

TStack<int> *piS;

TStack<char> *pcS;

piS=&iS;

pcS=&iS;                              // 에러

 

iS는 크기 10의 정수형 스택이다. 이런 정수형 스택을 가리키는 포인터 타입을 만들 때는 공식에 따라 타입명 다음에 *기호만 붙이면 된다. iS의 타입이 TStack<int>이므로 이런 스택을 가리킬 수 있는 포인터 타입은 TStack<int> *가 된다. piS가 같은 타입의 &iS를 대입받을 수 있는 것은 당연하다. pcS는 문자형 스택을 가리키는 포인터 변수인데 이 변수로는 정수형 스택 iS의 번지를 대입받을 수 없다. 타입이 다르기 때문인데 만약 억지로라도 대입하려면 캐스트 연산자를 사용한다.

 

pcS=(TStack<char> *)&iS;

 

이 연산문에서 보다시피 TStack<char>라는 표현식이 하나의 타입으로 인정되며 따라서 그 유도형도 타입으로 인정되어 캐스트 연산자가 될 수 있다. 템플릿 클래스를 함수로 넘길 때도 인수열과 함께 형식 인수의 타입을 밝히면 된다. 다음 예제의 DumpStack 함수는 정수형 스택을 인수로 전달받아 그 내용을 덤프한다.

 

: TemplateTypePara

#include <Turboc.h>

#include <iostream>

using namespace std;

#include "TStack.h"

 

void DumpStack(TStack<int> &S)

{

     int i;

 

     for (i=S.GetTop();i>=0;i--) {

          cout << i << "번째 = " << S.GetValue(i) << endl;

     }

}

 

void main()

{

     TStack<int> iS(10);

 

     iS.Push(1);

     iS.Push(2);

     iS.Push(3);

     iS.Push(4);

     DumpStack(iS);

}

 

형식 인수열의 S에 대한 타입을 TStack<int> &로 밝히기만 하면 된다. 템플릿 클래스 타입을 인수로 받아들이는 함수는 사실 별 실용성이 없으며 이런 함수는 클래스의 멤버 함수가 되는 편이 훨씬 더 깔끔하다.

 

템플릿은 하나의 알고리즘을 여러 타입에 두루 사용할 수 있는 문법적인 장치이며 템플릿을 사용하면 한 번 만들어 놓은 코드를 타입에 상관없이 사용할 수 있는 이점이 있다. 그래서 템플릿은 코드의 재사용성을 극대화하는 도구로 사용되며 표준 템플릿 라이브러리의 문법적 기반이 된다. 템플릿을 사용하는 이런 프로그래밍 방법을 일반화(Generic) 프로그래밍이라고 하며 객체 지향의 다음 세대로 지칭되기도 한다.

신고
0 0
Programming~*/C++
신고
0 1
Programming~*/C++
Docbook 원문
  1. 솔라리스에서의 malloc()관련문제 추가 - 정리해서 원문에 넣을 수 있음 yundream

    Contents

    1 솔라리스에서의 메모리 할당


동적 메모리할당

윤 상배

dreamyun@yahoo.co.kr



1절. 소개

어떤 언어를 이용해서 프로그래밍을 하든지 프로그램이 하는 주요한 임무는 결국 데이타를 주고/받고 이를 가공하는 작업이다. 이는 사람이 사회에서 살아가기 위한 가장 주요한 일이 서로간의 대화인것과 마찬가지이다.

사람은 서로 대화를 하기 위해서 대화할 내용을 미리 어딘가에 저장하고 있어야 한다. 사교를 위한 대화를 위해서라면 머리에 저장되어 있는 (저장이라고 말하니 좀 이상하긴하다 --;) 데이타(경험)를 활용해서 대화를 하면 될것이고, 업무상 중요한 대화라면, 미리 노트를 하든지 해서 실제 대화때 중요한 내용을 빠트리지 않도록 준비를 해야 할것이다.

프로그램도 마찬가지로 어떤 수행을 위해서는 데이타를 어딘가에 저장해 두고 있어야 할것이다. 이러한 데이타 의 저장은 메모리공간 혹은 디스크 공간을 이용한다.

이문서는 데이타저장을 위해서 어떻게 메모리 공간을 이용해야 하는지에 대한 내용을 다루고 있다.


2절. 메모리 할당

메모리 할당을 위한 방법은 크게 2가지가 있다. 정적메모리 할당과 동적 메모리 할당이 그것인데, 동적 메모리 할당을 설명하기 전에 정적 메모리 할당에 대해 간단히 알아보도록 하겠다.


2.1절. 정적 메모리 할당에 대해서

Static Memory Allocation 이라고 불리우며, 메모리의 크기가 미리 고정시켜서 할당하는 것을 말한다. 일반적으로 메모리크기를 할당하는 쉬운 방법으로, 할당시켜줘야할 메모리의 한계 크기를 명확히 알고 있을경우 사용한다.

예를 들어 주소를 저장하기 위한 메모리 공간이 필요하다고 할때, 우리는 주소를 저장하기 위해서 어느정도의 메모리 공간이 필요한지를 대충 계산할수 있다. 주소길이가 아무리 길어봐야 256 자를 넘지 않을것이기 때문이다.

이러한 정적 메모리 할당은 프로그램 시작시에 미리 고정시켜서 할당시켜 버린다.(그런 이유로 Static 이란 단어가 붙는다.)

...
int main()
{
    char address[256];
    char zipcode[10];

    ... 
    ...
}				
			
정적으로 메모리를 할당할경우 약간의 메모리 낭비가 있을수 있다. 보통 할당할 메모리를 결정할때, 최대 사용가능하다고 생각되는 메모리량보다 약간더 크게 잡는게 보통이기 때문이다.

그러나 크기의 한계를 명확히 알수 있을경우, 동적 메모리 할당보다 사용하기 쉽고, 버그가 발생할 확률도 적다는 장점을 가지고 있다. 보통 프로젝트를 진행하게 될경우 약간의 메모리 낭비보다는 버그의 발생을 더큰 프로그램 위험요소로 생각하기 때문에, 가능한한 정적 메모리 할당을 사용한다. 동적 메모리 할당을 사용할경우 메모리 누수, 혼동되는 포인터의 사용에 의한 잘못된 메모리 참조등 여러가지 문제를 발생시킬수 있기 때문이다. 포인터 잘못사용해서 발생하는 문제가 얼마나 프로그래머를 괴롭히는지는 말하지 않아도 잘 알고 있으리라 생각된다.


2.2절. 동적 메모리 할당에 대해서

Dynamic Memory Allocation 이라고 불리우며, 말그대로 프로그램 실행중에 동적으로 메모리의 크기를 할당시켜줘야 할 필요가 있을경우 사용한다.

예를 들어서 간단한 에디터 프로그램을 만든다고 했을때, 보통 파일의 내용을 메모리 상에 읽어 들이게 될것이다. 그런데 파일의 크기가 얼마가 될지는 아무도 알수 없다. 파일이 작을경우 그 크기가 0이 될수도 있겠지만 파일이 클경우 수십 메가 바이트 혹은 그 이상이 될수도 있을것이기 때문이다. 이경우에는 정적 메모리 할당을 사용할수 없으며, 어쩔수 없이 동적 메모리 할당을 사용해야 할것이다.

"어쩔수 없이" 란 말을 붙인 이유는 되도록이면 동적 메모리 할당을 사용하는것 보다는 정적메모리 할당을 사용하는게 여러모로 이익이 많기 때문이다.


2.2.1절. malloc, realloc, sizeof, memset, free

2.2.1.1절. malloc

동적 메모리 할당을 위해서 C 는 주요한 몇가지 시스템 함수와 키워드를 제공한다. 이중 malloc 는 메모리 상에서 연속된 일정 크기의 공간을 할당받기 위해서 사용하는 가장 일반적인 함수이다.

void *malloc(size_t number_of bytes);
					
malloc() 함수는 인자로 할당받고자 하는 메모리의 크기를 byte 단위로 명시한다. 만약 메모리 할당이 성공했다면 malloc 함수는 할당된 메모리의 시작 위치를 가리키는 포인터를 반환한다. (포인터에 대한 내용은 데이타와 포인터) void * 를 받는 이유는 void * 를 이용할경우 어떤 타입으로라도 형변환이 가능하기 때문이다.

만약 100 byte 의 문자를 저장하기 위한 공간을 할당받기 원한다면 다음과 같이 malloc() 함수를 호출하면 될것이다.

char *cp;

cp = (char *)malloc(100);
					
위의 방법을 통해서 메모리 할당을 하게 된다면, 메모리는 다음과 같이 구성되게 될것이다.
  0 1 2 3          0  ....        100
 +-+-+-+-+        +-+-+-+-+-+-+-+-+-+
 | cp    |        |                 |
 +-+-+-+-+        +-+-+-+-+-+-+-+-+-+
  |                |
  +----------------+ 
					
malloc() 을 통해서 메모리 할당요청을 받은 운영체제(커널)은 100 byte 크기만큼의 연속된 메모리를 할당하고, 할당된 메모리의 첫번째를 가리키는 주소값을 넘겨준다. 우리가 실제 *cp 를가지고 하는 여러가지 데이타 관련된 조작은 할당된 메모리의 주소값을 이용해서 이루어지게 된다.

아래의 예제를 실행시켜보면 좀더 쉽게 이해가 가능할것이다.

예제 : malloc_1.c

#include <unistd.h>

int main()
{
    char buf[128] = "12345";
    char buf2[128] = "12345";
    char *ch;

    char *cp;
    char *ct;

    ch = buf;
    printf("address ch           : %x\n", &ch);
    printf("address ch -> buf    : %x\n", ch);
    printf("address buf          : %x\n", buf);
    printf("address buf -> first : %x\n", &(*buf));
    printf("\n");

    printf("address cp not      : %x\n", &cp);
    printf("address cp ->       : %x\n", cp);
    cp = (char *)malloc(100);
    cp = buf;
    printf("address cp -> buf   : %x\n", cp);
    cp = buf2;
    printf("address cp -> buf2  : %x\n", cp);
}
					
다음은 위의 프로그램을 실행시킨 결과이다. 결과 값은 때와 장소와 시스템에 따라 달라질수 있다. (1, 2, 3, 4, ... 는 필자가 설명을 위해 붙인 숫자이다)
   
[root@localhost test]# ./malloc2
address ch           : bffff76c --- 1
address ch -> buf    : bffff7f0 --- 2
address buf          : bffff7f0 --- 3
address buf -> first : bffff7f0 --- 4

address cp not      : bffff768 --- 5
address cp ->       : 0        --- 6
address cp ->       : 8049860  --- 7
address cp -> buf   : bffff7f0 --- 8
address cp -> buf2  : bffff770 --- 9
					
1 번 결과는 포인터 *ch 가 할당된 곳의 주소값이다. 2 번 결과는 *ch 가 포인터(가리키는)하는 곳의 첫번째 메모리 의 주소값이다. *ch 가 가리키는 곳은 buf 가 저장된 메모리의 첫번째 주소 값이므로 bffff7f0 이 세팅되었다. 3번, 4번 결과 는 *ch 가 실제 buf의 주소를 가리키고 있음을 확인하기 위해 출력한 값이다. 이들 결과를 보면 실제로 포인터가 어떻게 값을 가리키고 있는지 확인할수 있을것이다.
  0 1 2 3       0 ....        100
 +-+-+-+-+     +-+-+-+-+-+-+-+-+-+
 | ch    |     | buf             |
 +-+-+-+-+     +-+-+-+-+-+-+-+-+-+
 bffff76c      bffff7f0
  |             |
  +-------------+
					

5 번부터 8 번까지는 malloc()을 호출함으로써 메모리 구성이 실제로 어떻게 되는지를 보여준다. 5 번은 cp 가 위치한 곳의 주소이니까 별로 신경쓸필요는 없다. 6 번은 malloc() 하기전에 *cp 가 가리키고 있는 곳의 주소 값을 보여주는데 메모리 할당되어 있지않은경우이다. 위의 경우 NULL 을 가리키고 있는데, 이것은 상황에 따라 변한다. 다시 말해서 메모리 할당하지 않았을경우 임의의 영역을 가리킨다고 보면 무난하다. 7 번이 malloc()을 이용해서 메모리 할당한후 가리키는 곳의 주소인데, malloc() 하기전과 비교해 보면 가리키고 있는 곳의 주소가 명확하게 정해져 있음을 확인할수 있다. 8,9 번은 각각 buf 와 buf2 를 대입했다.

 malloc 하기전
  0 1 2 3 
 +-+-+-+-+
 | cp    |
 +-+-+-+-+     ?
  |            |
  +------------+
 
 malloc 한후
  0 1 2 3           0 ...       100       0 ....        100
 +-+-+-+-+         +-+-+-+-+-+-+-+-+     +-+-+-+-+-+-+-+-+-+
 | cp    |         |               |     | buf             |
 +-+-+-+-+         +-+-+-+-+-+-+-+-+     +-+-+-+-+-+-+-+-+-+
                    8049860              bffff7f0
  |                 |
  +-----------------+

 buf를 대입한후 

  0 1 2 3           0 ...       100       0 ....        100         
 +-+-+-+-+         +-+-+-+-+-+-+-+-+     +-+-+-+-+-+-+-+-+-+        
 | ch    |         |               |     | buf             |        
 +-+-+-+-+         +-+-+-+-+-+-+-+-+     +-+-+-+-+-+-+-+-+-+        
 bffff7ec           8049860              bffff7f0                   
  |                                       |
  +---------------------------------------+
					


2.2.1.1.1절. 메모리를 사용하기 위해서는 반드시 할당해야 한다.

당연한 얘기이지만 메모리를 사용하기 위해서는 적당한 공간을 할당해주어야 한다. 바로 위의 그림에서 malloc 하기전에 *cp 가 가리키는 주소를 보면 상황에 따라 달라진다고 했다. 그럼으로 할당되지 않은 포인터에 데이타를 입력하면 임의의 주소에 어떤 값을 입력하는 꼴이 된다. 운이 좋으면 임의의 주소 영역에 데이타를 충분히 저장할 공간이 확보되어 있어서 에러없이 프로그램이 실행될수도 있지만, 다른 프로세스가 차지하고 있는 메모리 영역을 침범할수도 있다. 이럴경우 메모리 영역에 대한 우선권은 먼저 획득한 프로세스에게 있음으로, 커널은 이 메모리영역에 데이타를 쓰려고 하는 프로세스를 강제 종료시킬것이다(세그먼트 폴트 에러)

위에서 메모리 할당을 하지 않았음에도 불구하고 프로그램이 제대로 실행되면 운이 좋은경우라고 했는데, 사실 이경우는 운이 좋은경우가 아니고 운이 나쁜경우가 된다. 언뜻 보기에 정상적으로 실행되는것 처럼 보일수 있기 때문에 디버깅 작업을 어렵게 만들수 있기 때문이다(다른 프로세스의 메모리 영역을 침범할 가능성을 가지고 있는 불완전한 코드이다). 잘 돌다가 어느날 아침에 확인해보니까 프로그램이 죽게 될 확률이 높다. 사용하는 메모리의 공간이 작을수록(크면은 다른 영역을 침범할 가능성이 크다) 운 좋게(나쁘게) 제대로 작동될 확률이 크다.

그러므로 아래와 같은 코드는 심각하게 잘못된 코드이다.(아마 어떤경우에는 제대로 실행되고, 어떤 경우에는 세그먼트 폴트가 떨어질것이다)

char *cp;

memcpy(cp, "1234", 100);
						
위의 코드는 아래와 같이 미리 공간을 할당한후 사용하도록 제작성 해야 한다.
char *cp;

cp = (char *)malloc(100);
memcpy(cp, "1234", 100);
						


2.2.1.2절. realloc

메모리의 크기를 조정하고자 할때 사용한다. 에디터 프로그램을 예로 들어보자면, 2가지 메모리 할당을 할수 있을건데, 파일의 크기를 읽은다음 파일의 크기만큼 한번에 메모리 할당을 해버리는 방법과 1024 바이트 정도로 할당하고, 파일을 읽어들이다가 1024 를 초과하게 되면, realloc 를 이용해서 1024를 더 할당해주는 방법이 있다.

어느걸 사용하든지 관계는 없지만, realloc 은 기본행동이 만약 연속된 메모리 공간이 충분하지 않을경우 연속된 메모리 공간을 할당할수 있도록 새로 공간을 잡게 되며, 이와중에 기존의 데이타가 복사되므로 상당히 많은 비용이 소모될수 있다.(실제 테스트 해보면 알겠지만 malloc 에 비해서 눈에 띄게 많은 시간이 소모된다)

예제 : realloc.c

#include <unistd.h>

int main()
{
    char *cp;
    int i;

    cp = (char *)malloc(100);
    memcpy(cp, "111", 100);
    printf("100     : %x\n", cp);
    cp = (char *)realloc(cp, 10000);
    printf("10000   : %x\n", cp);
    cp = (char *)realloc(cp, 1000000);
    printf("1000000 : %x\n", cp);
    printf("Value is (%s)\n", cp);
}
					
다음은 위의 예제를 실행시킨 결과이다.
[root@localhost doc]# ./realloc
100     : 8049770
10000   : 8049770
1000000 : 4015e008
Value is (111)
					
위의 값은 상황에 따라 변할수 있다. 100, 10000 은 같은 메모리 주소를 사용하고 있지만, 1000000 이 되면서 메모리의 위치가 변경되었음을 알수 있다. 아무래도 100 과 10000 의 경우 그리 큰차이가 나지 않기 때문에 현재 위치에서 연속된 메모리 공간을 확보하기가 수월하지만 값이 커질수록 연속된 메모리 공간을 확보가 어려워지기 때문에, 어쩔수 없이 메모리 이동이 일어나게 된다. 메모리 위치 이동이 일어난다 하더라도 값은 그대로 복사되고 있음을 알수 있다.


2.2.1.3절. sizeof

sizeof 는 C 에서 제공하는 키워드로 해당 자료형의 크기를 돌려준다. sizeof 는 메모리 할당에 있어서 꽤 중요한 역할을 가진다. 이유는 각 자료형마다 차지하는 byte 크기가 틀리고, 같은 자료형이라 할지라도 운영체제에 따라 그 크기가 달라질수 있기 때문이다. 예를들어 int 형 자료 4개를 저장하기 위해서 다음과 같이 메모리 크기를 할당했다고 하자.

int *ip;

ip = (int *)malloc(16);
					
int 형은 보통의 경우 4byte 이니까 4개의 자료를 저장하기 위해서 는 16 만큼의 크기가 필요한건 확실하다. 그러나 int 형의 크기가 4byte 인것은 보통의 경우이고 2byte 혹은 8byte 인 경우가 있을수도 있다. 그럴경우 위의 프로그램은 현재 운영체제에서는 문제 없겠지만, 다른 운영체제로 포팅하고자 할때 문제가 될수도 있다.

이런 문제를 없애기 위해서 sizeof 키워드를 제공받아서 사용한다. 이 키워드를 사용하면 운영체제에서 사용하는 자료형의 크기를 돌려주게 됨으로 위에서와 같은 문제점이 발생하지 않는다.

int *ip;

ip = (int *)malloc(sizeof(int)*4);

printf("int size is %d\n", sizeof(int));
					
int 자료형을 위해서 4byte 공간을 필요로 하는 운영체제라면 16, 8byte 만큼을 필요로 하는 운영체제라면 32byte 만큼을 할당받을수 있게 될것이다.


2.2.1.4절. free

malloc() 은 메모리 할당을 커널에 요청하는 시스템 함수이다. 그러므로 일단 malloc()에 의해서 할당받은 메모리는 프로세스가 종료될때까지 커널에 의해서 보호받게 된다.

이말은 malloc() 를 잘못 사용할경우 쓸데없는 메모리 공간의 낭비 를 가져 올수 있으며, 심각할경우 메모리 누수를 가져올수 있다는 뜻이된다. 다음의 예를 보자

warn_mem.c

#include <unistd.h>

int main()
{

    char *cp;

    while(1)
    {
        cp = (char *)malloc(10000);
        printf("%x\n", cp);
        sleep(1);
    }
}
					
위의 프로그램을 실행시켜보면, 운영체제에서 계속적으로 10000byte 크기의 새로운 메모리 공간을 프로세스에게 할당해 주는것을 볼수 있다.
[root@localhost doc]# ./warn_mem
8049690
8061d38
807a3e0
8092a88
80ab130
80c37d8
					
실제 warn_mem 의 실행결과 메모리 누수가 생기는지 확인을 위해서 간단한 스크립트를 만들어서 테스트를 해보도록 하자.
[root@coco /root]# while [ 1 ]
> do 
> ps -aux | grep warn_mem | grep -v vi | grep -v grep
> sleep 1
> done
USER       PID %CPU %MEM   VSZ  RSS TTY      STAT START   TIME COMMAND
root     11719  0.0  0.3  1436  400 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  1636  408 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  1736  412 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  1836  416 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  1936  420 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  2036  424 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  2236  432 ttypc    S    11:24   0:00 ./warn_mem
root     11719  0.0  0.3  2336  436 ttypc    S    11:24   0:00 ./warn_mem
					
ps 의 헤더 부분은 원래 스크립트의 실행결과에는 표시되지 않지만 ps 결과의 필드 구분을 쉽게 하기 위해서 추가 시켰다. 위의 결과를 보면 warn_mem 프로세스에서 점유하는 메모리의 크기가 지속적으로 증가되고 있음을 볼수 있다. 이러한 메모리 누수는 프로그램과 시스템에 매우 치명적일수 있다. 특히 이러한 종류의 문제는 컴파일러에서 처리를 해주지 않기 때문에 나중에 문제점을 찾기가 매우 곤란해진다. 흔히 말하는 몇일 잘돌다가 죽는 프로그램이 될 가능성이 크다.

그러므로 사용하지 않는 메모리공간은 반드시 운영체제에게 되돌려 주어야 한다. C 는 동적으로 할당된 메모리의 해제를 위해서 free() 를 제공한다. 다음은 warn_mem.c 의 메모리 누수 문제를 free() 를 통해서 해결한 코드이다.

free_mem.c

#include <unistd.h>

int main()
{

    char *cp;

    while(1)
    {
        cp = (char *)malloc(100000);
        printf("%x\n", cp);
        sleep(1);
        free(cp);
    }
}
					
위의 프로그램을 실행시키고, 테스트를 해보면 기존에 있던 메모리 누수 현상이 사라졌음을 확인할수 있을것이다.


3절. 동적 메모리 할당의 응용

이번장에서는 동적메모리 할당의 응용 예제를 만들어 볼것이다. 만들 응용 예제는 linked list 이다.


3.1절. 링크드 리스트 에 대해서

링크드 리스트는 그리 간단히 다룰수 있는 주제가 아니다. 링크드 리스트에 대한 자세한 설명은 자료구조를 다룰 기회가 있으면 그때 하기로 하고, 여기에서는 개념정도만 설명하도록 하겠다.

링크드 리스트는 말그대로 리스트를 만들기 위해서 사용되는 자료구조이다. 리스트를 만들기 위해서는 자료의 연속된 순서를 만들어 줘야 한다. 그럴려면 다음자료의 정보가 무엇인지를 알아야 하는데, 링크드 리스트란 다음자료의 정보가 무엇인지를 알고 있는 구조를 말한다. 여기서 다음자료의 정보란 다음 자료가 위치하고 있는 주소의 값이 될것이다.

 P: Pointer
 +--------++-+   +--------++-+   +--------++-+
 | Data 1 ||P|   | Data 2 ||P|   | Data 3 ||P|
 +--------++-+   +--------++-+   +--------++-+
            |     |         |     |         |
            +-----+         +-----+         +--- NULL 
			
대충 위와 같은 방식으로 연결된다. 리스트를 이루고 있는 리스트 멤버들은 실제 Data 와 더불어 다음 데이타의 주소정보를 가지고 있는 Pointer 을 가지고 있으며, 프로그래머는 이 Pointer 정보를 이용해서, 데이타에 접근할수 있게 된다. 위의 그림을 보면 알겠지만 Data 3 에 접근하기 위해서는 Data 1 번부터 순차적으로 접근해야 한다는 것을 알수 있다.


3.2절. 동적 메모리 할당을 통한 링크드 리스트의 구현

위에서 말했지만 자료구조 자체의 설명을 목적이 아닌 관계로 가장 단순한 형태의 링크드 리스트를 구현하도록 하겠다. 이 링크드 리스트는 다음과 같은 기능을 가진다.

데이타 삽입

멤버 데이타를 삽입한다. 최초 데이타가 삽입될때는 다음 데이타가 없음으로 다음데이타를 가리키는 Pointer 은 NULL 이 될것이다. 만약 두번째 데이타가 들어온다면 첫번째 데이타의 Pointer 은 두번째 데이타의 위치를 가리키게 될것이다. 두번째 데이타는 다시 NULL 을 가리키게 될것이다.

데이타 삭제

데이타 삭제를 제대로 구현하고자 한다면, 찾기후 삭제를 구현해야 하겠으나 여기에서는 POP 스타일의 삭제를 구현하도록 한다.(가장 먼저 들어온 데이타가 가장 먼저 삭제되는 방식)

데이타 출력

역시 간단하게 처음의 리스트 멤버부터 순차적으로 검색해가면서 데이타를 출력하는 방식으로 구현할 것이다.

위의 3가지 구현은 링크드 리스트 뿐만 아니라 다른 자료구조에서도 가장 기본이 되는것들이며 위의 구현방식의 약간 다른 응용으로 만들어진다. 좀더 난이도 있는 구현은 각자 공부삼아서 해보기 바란다.


3.3절. 예제

linked_list.c

#include <unistd.h>
#include <string.h>

typedef struct
{
    char name[12];
    struct list_item *next_link;
} list_item;

list_item * add_item(list_item *, char *);
list_item * remobe_item(list_item *);
void print_list(list_item *item);

int main()
{
    list_item *list;

    list = NULL;

    list = add_item(list, "yundream");
    list = add_item(list, "kknd2");
    list = add_item(list, "hohoho");
    list = add_item(list, "loveisall");
    print_list(list);

    printf("\n");
    list = remove_item(list);
    print_list(list);
}

list_item * add_item(list_item *item, char *name)
{
    list_item *lp = item;

    // 기존에 Item 이 있을경우 
    // 가장 최근의 Item의 next_link 가 추가 되는 Item 의 
    // 주소를 가리키도록 포인터를 조정한다.  
    if (item != NULL)
    {
        while(item->next_link != NULL)
            item = item->next_link;

        item->next_link = (struct list_item *)malloc(sizeof(list_item));
        item = item->next_link;
        strcpy(item->name, name);
        return lp;
    }
    // 처음 Item 추가시에는 가리킬 데이타가 없음으로 
    // next_link 는 NULL 이 된다.  
    else
    {
        item = (struct list_item *)malloc(sizeof(list_item));
        item->next_link = NULL;
        strcpy(item->name, name);
        return item;
    }
}

list_item * remove_item(list_item *item)
{
    list_item *tmp;
    printf("Element remove is %s\n", item->name);
    // 첫번째 링크가 가리키는 다음 데이타  
    // 즉 두번째 데이타의 정보를 tmp 에 대입하고 
    // 첫번째 데이타를 free 시켜줌으로 
    // 링크드 리스트에서 제거시킨다. 
    tmp = item->next_link;
    free(item);
    return tmp;
}

void print_list(list_item *item)
{
    if (item == NULL)
        printf("NONE LIST\n");

    // Item 의 처음부터 끝까지 순차적으로 
    // 검색하면서 데이타를 출력시킨다. 
    else
        while(item != NULL)
        {
            printf("%10s : %x %x\n",
                            item->name,
                            item,
                            item->next_link);
            item = item->next_link;
        }
}
			

다음은 필자의 컴퓨터에서 실행시킨 결과다. 번호는 설명을 위해서 붙인것이다.

[root@localhost test]# ./linked_list 
  yundream : 80498a0 80498b8 --- 1
     kknd2 : 80498b8 80498d0 --- 2
    hohoho : 80498d0 80498e8 --- 3
 loveisall : 80498e8 0       --- 4

Element remove is yundream
     kknd2 : 80498b8 80498d0
    hohoho : 80498d0 80498e8
 loveisall : 80498e8 0
			
2개의 주소값이 출력되는데, 첫번째 주소값은 자신의 주소값이고 2번째 주소값은 다음 가리키는 데이타의 주소값이다. 보면 1 -> 2 -> 3 -> 4 의 식으로 데이타를 가리키고 있음을 알수 있다.


1 솔라리스에서의 메모리 할당

  • indra(1ndr4@hanmail.net)

#include <stdio.h> 
#include <stdlib.h> 
 
#define MAX     (1024*1024)*1024 
#define ALERT(funct, funct2, line) { \ 
        printf("Executed %s function. (in %s function. %d line)\n", \ 
        funct, funct2, line); \ 
} 
 
int main() 
{ 
        char *buf; 
 
        sleep(5); 
        ALERT("malloc", __FUNCTION__, __LINE__); 
        buf = (char*)malloc(MAX); 
        sleep(5); 
        ALERT("free", __FUNCTION__, __LINE__); 
        free(buf); 
        sleep(5); 
        ALERT("exit", __FUNCTION__, __LINE__); 
        exit(0); 
} 
 

OS 환경은 SunOS 5.8 i86 이며, 사양은 320 RAM, intel Pentium 3 800, LG IBM 노트북, 컴파일러는 GNU gcc 버전 2.95(20010315 release) 버전이다.

bash-2.03# uname -a; gcc -v 
SunOS indra 5.8 Generic_108529-16 i86pc i386 i86pc 
Reading specs from /usr/local/lib/gcc-lib/i386-pc-solaris2.8/2.95.3/specs 
gcc version 2.95.3 20010315 (release) 
bash-2.03# 
 

위의 코드를 컴파일 하여 백그라운드로 실행하면서 vmstat 명령을 이용,
메모리 할당 부분에 대해서 알아보았다.

bash-2.03# cc -o free free.c 
bash-2.03# ./free  
[1] 18777 
bash-2.03# vmstat 1 
 procs     memory            page            disk          faults      cpu 
 r b w   swap  free  re  mf pi po fr de sr cd -- -- --   in   sy   cs us sy id 
 0 0 0 1270732 247888 29 247 4  0  0  0  0  1  0  0  0  131 1368  306  5  3 92 
 0 0 0 1269968 239636 59 515 0  0  0  0  0  0  0  0  0  121 2540  553  8  7 85 
 0 0 0 1269968 239616 59 509 0  0  0  0  0  0  0  0  0  124 2485  543  8  6 86 
Executed malloc function. (in main function. 15 line) 
 0 0 0 1269968 239600 59 516 0  0  0  0  0  0  0  0  0  119 2487  539  9  5 86 
 0 0 0 221380 239580 59 508  0  0  0  0  0  0  0  0  0  123 2480  543  8  6 86 
 0 0 0 221380 239564 59 508  0  0  0  0  0  0  0  0  0  122 2476  540 10  4 86 
 0 0 0 221380 239552 59 508  0  0  0  0  0  0  0  0  0  120 2488  546  9  5 86 
 0 0 0 221380 239540 59 508  0  0  0  0  0  0  0  0  0  118 2472  534 10  4 86 
Executed free function. (in main function. 18 line) 
 0 0 0 221380 239528 59 508  0  0  0  0  0  0  0  0  0  123 2487  542  9  6 85 
 0 0 0 221380 239608 59 508  0  0  0  0  0  0  0  0  0  120 2484  540  8  6 86 
 0 0 0 221380 239596 59 508  0  0  0  0  0  0  0  0  0  118 2474  538  9  5 86 
 0 0 0 221380 239584 59 508  0  0  0  0  0  0  0  0  0  124 2495  548  8  6 86 
 0 0 0 221380 239568 59 508  0  0  0  0  0  0  0  0  0  121 2483  543  9  5 86 
Executed exit function. (in main function. 21 line) 
 0 0 0 221380 239556 59 508  0  0  0  0  0  0  0  0  0  122 2474  534  8  6 86 
 0 0 0 1270024 239624 59 508 0  0  0  0  0  0  0  0  0  122 2479  542 11  3 86 
 0 0 0 1270024 239612 59 508 0  0  0  0  0  0  0  0  0  118 2469  534  6  8 86 
 0 0 0 1270024 239600 59 508 0  0  0  0  0  0  0  0  0  118 2513  543  7  7 86 
^C 
[1]+  Done                    ./free 
bash-2.03# 
 

실제적으로 free() 함수로 동적메모리 할당을 해제했다고 하더라도 어플리케이션 단위에서는 해당 메모리를 그대로 가지고 있는것으로 나타났다.

같은 코드를 가지고 RedHat linux 배포판에서 test 를 해보았다. OS 환경은 RH 8.0, gcc 버전은 3.2(20020903 release) 버전이다.

indra@ ~test> ./free  
[1] 25255 
indra@ ~test> while :; do ps -aux | grep "./free"; sleep 1 ; done 
indra    25255  0.0  0.0  1308  216 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.0  1308  216 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.0  1308  216 pts/5    S    13:07   0:00 ./free 
Executed malloc function. (in main function. 15 line) 
indra    25255  0.0  0.1 1049892 312 pts/5   S    13:07   0:00 ./free 
indra    25255  0.0  0.1 1049892 312 pts/5   S    13:07   0:00 ./free 
indra    25255  0.0  0.1 1049892 312 pts/5   S    13:07   0:00 ./free 
indra    25255  0.0  0.1 1049892 312 pts/5   S    13:07   0:00 ./free 
Executed free function. (in main function. 18 line) 
indra    25255  0.0  0.1  1312  308 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.1  1312  308 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.1  1312  308 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.1  1312  308 pts/5    S    13:07   0:00 ./free 
indra    25255  0.0  0.1  1312  308 pts/5    S    13:07   0:00 ./free 
Executed exit function. (in main function. 21 line) 
[1]+  Done                    ./free 
^C 
 
indra@ ~test> 
 

SunOS 에서의 메모리 관리가 user 의 눈으로 보이는것만 다른것인지 아니면 실제 free() 후 메모리 관리가 효율성을 위한 측면으로 다른 OS 와 관리체계가 다른지는 아직까지 불 분명 하다.

이에 대한 yundream 님의 답변:


http://www.joinc.co.kr/modules.php?op=modload&name=Forum&file=viewtopic&topic=28365&forum=1&2

솔라리스 운영체제에서 테스트 한거 아닙니까 ?
리눅스상에서는 free 했을경우 제대로 해제가 되는데,
솔라리스에서는 해제가 안되더군요.
정확히 말하면 안되는것처럼 보인다고 해야 정확한 표현일듯 싶은데,

저도 예전에 솔라에서 작업하다가,
malloc 후 free 가 안되는 문제로 꽤 고민을 한적이 있었습니다.
몇몇 문서를 찾아봤더니, 메모리관련작업 안정성을 확보하기 위해서라고 되어 있는것 같기는 하던데
확실히는 잘 모르겠습니다.

어쨋든 저것때문에 메모리 누수와 같은 문제가
발생하지 않습니다.
솔라에서 메모리관리를 어떻게 하는지좀 알아봐야 겠네요.

저문제로 가끔 짜증날때가 있는데,
어떤 이유로 일시적으로 한 100메가 이상 메모리를 잡아서 쓰게되었다면 free를 해도 ps 상에서는
100메가를 그대로 잡고 있는걸로 보이기 때문에,
프로그래머 입장에서는 문제가 되지 않겠지만,
가끔 고객에게 프로그램을 제공할때 문제가 될수도 있죠.
"왜 이렇게 메모리를 많이 잡아먹어요? 문제 있는거 아닌가요?"
이런 문제가 발생할수 있습니다.
설명을 해도 쉽게 납득을 하지 못하죠.


마지막으로 realloc() 을 통한 메모리 재 할당을 test 하여 보았다.
linux 에서는 realloc() 을 사용시, 메모리 사이즈가 재 설정되는 양상을
보였지만 SunOS 에서는 역시 처음 malloc() 으로 할당된 메모리 사이즈를
유지하고 있었다.

#include <stdio.h> 
#include <stdlib.h> 
 
#define MAX     (1024*1024)*1024 
#define ALERT(funct, funct2, line) { \ 
        printf("Executed %s function. (in %s function. %d line)\n", \ 
        funct, funct2, line); \ 
} 
 
int main() 
{ 
        char *buf; 
 
        sleep(5); 
        ALERT("malloc", __FUNCTION__, __LINE__); 
        buf = (char*)malloc(MAX); 
        sleep(5); 
        ALERT("realloc", __FUNCTION__, __LINE__); 
        buf = (char*)realloc(buf, 1024*1024); 
        sleep(5); 
        ALERT("free", __FUNCTION__, __LINE__); 
        free(buf); 
        sleep(5); 
        ALERT("exit", __FUNCTION__, __LINE__); 
        exit(0); 
} 
 

indra@ ~test> ./free  
[1] 25389 
indra@ ~test> while :; do ps -aux | grep "./free"; sleep 1 ; done 
indra    25389  0.0  0.0  1308  216 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.0  1308  216 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.0  1308  216 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.0  1308  216 pts/5    S    13:29   0:00 ./free 
Executed malloc function. (in main function. 15 line) 
indra    25389  0.0  0.1 1049892 312 pts/5   S    13:29   0:00 ./free 
indra    25389  0.0  0.1 1049892 312 pts/5   S    13:29   0:00 ./free 
indra    25389  0.0  0.1 1049892 312 pts/5   S    13:29   0:00 ./free 
indra    25389  0.0  0.1 1049892 312 pts/5   S    13:29   0:00 ./free 
Executed realloc function. (in main function. 18 line) 
indra    25389  0.0  0.1  2340  316 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  2340  316 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  2340  316 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  2340  316 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  2340  316 pts/5    S    13:29   0:00 ./free 
Executed free function. (in main function. 21 line) 
indra    25389  0.0  0.1  1312  312 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  1312  312 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  1312  312 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  1312  312 pts/5    S    13:29   0:00 ./free 
indra    25389  0.0  0.1  1312  312 pts/5    S    13:29   0:00 ./free 
Executed exit function. (in main function. 24 line) 
[1]+  Done                    ./free 
^C 
indra@ ~test> 
 



=linked list 예제소스에 warning 이 있어 수정하여 보았습니당=

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

typedef struct _list_item
{
char name12;
struct _list_item *next_link;
} list_item;

list_item * add_item(list_item *, char *);
list_item * remove_item(list_item *);
void print_list(list_item *item);

int main()
{
list_item *list;

list = NULL;

list = add_item(list, "yundream");
list = add_item(list, "kknd2");
list = add_item(list, "hohoho");
list = add_item(list, "loveisall");
print_list(list);

printf("\n");
list = remove_item(list);
print_list(list);

return 0;
}

list_item * add_item(list_item *item, char *name)
{
list_item *lp = item;

if (item != NULL)
{
while(item->next_link != NULL)
item = item->next_link;

item->next_link = (list_item *)malloc(sizeof(list_item));
item = item->next_link;
strcpy(item->name, name);
return lp;
}
else
{
item = (list_item *)malloc(sizeof(list_item));
item->next_link = NULL;
strcpy(item->name, name);
return item;
}
}

list_item * remove_item(list_item *item)
{
list_item *tmp;
printf("Element remove is %s\n", item->name);
tmp = item->next_link;
free(item);
return tmp;
}

void print_list(list_item *item)
{
if (item == NULL)
printf("NONE LIST\n");
else
while(item != NULL)
{
printf("%10s : %p %p\n",
item->name,
item,
item->next_link);
item = item->next_link;
}
}

출처 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/malloc
신고
0 0
Programming~*/C++

[Open developerWorks]

애플리케이션 개발시의 메모리 디버깅 : 메모리 누수 발견 기법



김현우
김현우 whytok@hanmail.net

알고리즘과 바이오인포메틱스 분야에 SCI 논문을 비롯한 다수의 논문을 게재하였고, 한국생명정보학회에서 IBM Basic Research Award를, 정보과학회에서 우수 논문 발표상을 수상한 바 있다. 현재는 LG전자에 근무하고 있으며 디지털 방송 관련 애플리케이션을 개발하고 있다.


난이도 : 초급
2006년 12월 19일


[오픈 디벨로퍼웍스]는 여러분이 직접 필자로 참가하는 코너입니다. 이번에는 김현우 님이 작성한 애플리케이션 개발시의 메모리 부족 에러에 대비하는 메모리 누수 발견 프로그램 개발 방법을 함께 살펴봅니다.

필자는 DVD 레코더와 셋톱박스의 복합 모델을 개발하는 팀에 소속되어 있다. 현재 유럽에서는 아날로그 방송을 디지털로 서서히 대체하고 있기 때문에, 관련 제품의 개발 요청이 쇄도하고 있다.
얼마 전 유럽을 타깃으로 3개의 유사 모델(D197, D198, D199)을 개발하고 있을 때의 일이다. 우여곡절 끝에 기본 모델인 D197 개발을 마치고 양산 시켰으며, D198도 완료하여 QA 그룹에 테스트를 의뢰한 후 결과를 기다리고 있었다. 팀원들 모두, D197 모델이 별 이상 없었으니 부가기능을 조금 추가한 D198 역시 무난히 양상 단계로 넘어갈 것이라고 판단, 모처럼의 한가한 시간을 보내고 있었다.
그런데 그 순간 옆자리에서 통화하는 소리가 들렸다.
"네, 세트가 멎었다고요?"
테스트 그룹으로부터 D198 세트가 오동작 한다는 연락을 받은 것이다. 우리는 즉시 테스트 그룹으로 달려갔다. 노트북을 D198에 연결하고 에러 메시지를 확인했다. 디버그 화면에는 메모리 부족을 나타내는 경고 메시지가 반복해서 출력되고 있었다. 사용할 수 있는 메모리가 바닥나서 더 이상 동작하지 못하고 멈춰버린 것이었다.
D198의 DRAM은 시스템을 동작시키고 남을 넉넉한 크기여서, 정상적인 경우라면 메모리 부족 문제가 발생할 이유가 없었다. D197 모델에서는 없었던 메모리 문제가 왜 발생했을까? 팀원들은 오늘도 일찍 퇴근하기는 틀렸다는 생각에 투덜거리며 D198에서 새로 추가된 코드를 중심으로 살펴보기 시작했다.

메모리 누수

자바나 C# 같은 언어에는 가비지 컬렉터(Garbage Collector)가 있어서, 알아서 메모리 관리를 해주지만 프로그래밍 언어의 대표주자 C/C++ 에서의 메모리 할당과 해제는 프로그래머의 몫이다. 따라서 애플리케이션이 메모리를 할당 받아서 사용했다면, 사용이 끝난 뒤에는 반드시 반납해서 해당 메모리를 재사용할 수 있도록 해야 한다. 그러나 프로그래머도 사람인지라 할당 받은 메모리를 실수로 해제하지 않는 경우가 발생할 수 있다. 이러한 경우 해제되지 않은 메모리는 사용이 끝난 뒤에도 남아있어 공간만 차지하는 상태로 있게 된다. 이것을 메모리 누수라고 한다.
언뜻 생각해보면 메모리를 할당 받고 해제하는 것이 무슨 어렵고 복잡한 일인가? 의문을 가질 수도 있겠지만, 실제로 코딩을 하다 보면 그렇게 간단한 문제가 아니다.
상용코드는 여러 프로그래머들이 개발하고 유지보수하기 때문에 시간이 지나면 일관성을 잃고 난해해지기 쉬울 뿐 아니라 그 양도 방대해진다. 때문에 복잡해진 애플리케이션의 상태나 조건을 후임 개발자가 꿰뚫고 있기는 쉽지 않다.
여기에 새로운 기능을 추가하다 보면 예상치 못한 상태에 빠지기도 하고 복잡해진 조건에 누락되는 부분이 생겨 메모리 누수가 발생하기 쉽다. 특히 문제의 루틴이 단발적으로 사용되는 것이 아니라면 루틴이 실행될 때마다 메모리 누수가 누적되어 언젠가는 메모리 부족 문제가 터질 수밖에 없게 된다.

메모리 감시자

짧은 분량의 간단한 프로그램이라면 차분히 코드를 살펴보면서 메모리 누수를 찾아 나설 수도 있다. 실제로 느긋하게 코드를 읽어 나가는 것은 메모리 누수뿐 아니라 야근하며 작성했던 코드의 결함을 찾아내는 가장 좋은 방법일 수도 있다.
그러나 방대한 분량의 상용 코드에서 생긴 문제라면 어떻겠는가? 거기에 상용 프로그램 개발에 느긋한 일정을 주는 회사를 본 적이 있는가? 긴박한 일정에 메모리 누수까지 생겼다면 과중한 스트레스로 이력서를 고쳐 쓰고 취업 사이트를 뒤지게 될지도 모르겠다.
방대한 프로그램에 숨겨진 감쪽같은 메모리 누수는 사막에서 잃어 버린 바늘과도 같다. 맨 손으로 사막에서 바늘을 찾으려는 것은 너무도 무모한 짓이다. 사막에서 바늘을 찾으려면 도구가 필요하다. 이를테면 금속탐지기 같은 것이 있으면 큰 도움이 될 것이다. 우리도 메모리 누수를 찾기 위한 금속탐지기부터 구해 보도록 하자.
필자는 도입부의 문제 상황에서 다른 팀원들과 함께 코드를 살펴보는 대신에 메모리 감시 모듈을 작성하여 코드에 추가하는 작업을 했다. 물론 다른 팀원들이 열심히 코드를 분석하는 동안에 다른 코드를 작성하고 있었기 때문에, 자칫 딴짓을 하는 것처럼 보였을 수도 있었겠지만 결국은 필자의 방법으로 문제를 해결할 수 있었다. 두 세시간 정도의 시간이 지나자 코드 분석을 하던 팀원들은 하나 둘씩 지쳐서 포기하기에 이르렀는데, 그 즈음에 필자의 프로그램이 완성되었다.
필자가 개발한 메모리 감시자는 메모리의 할당과 해제 상태를 저장하고 리포트 하는 기능을 하는데, 감시 모듈을 포함시킨 D198을 부팅시켜 기본동작을 수행시켜 본 후 메모리 상태를 확인해 보았다. 예상대로 D198에서 새롭게 추가된 유료방송 처리 부분이 문제였다.
우리는 리포트 받은 부분의 코드를 살펴보고 곧 문제를 찾아내어 메모리 누수를 해결할 수 있었다. 그리고 덤으로 세트에 주는 영향은 미미하지만 감추어져 있던 또 다른 메모리 누수(몇 년 정도 동작 시키면 세트를 멈추게 할 수도 있는 문제)도 찾아낼 수 있었다.

메모리 감시자의 구현

메모리 감시자가 하는 일은 거창해 보이지만 구현은 생각보다 간단하다. C언어에서는 malloc이나 calloc을 이용하여 메모리를 할당하고, free를 이용하여 메모리를 해제한다. 마찬가지로 C++에서는 new와 delete가 같은 동작을 한다. 메모리 감시자는 링크드 리스트로 구현되며, malloc이 호출 되면 할당한 메모리의 주소를 리스트에 저장해 두었다가 free로 해당주소를 해제하면 저장된 리스트에서 해제된 주소 값을 찾아 삭제한다.
이러한 동작으로 메모리 감시자는 현재 할당되어 있는 메모리의 주소만을 보관하고 있게 된다. 따라서 메모리 감시자를 포함한 어플리케이션을 실행시킨 후 리포트를 확인하면, 비정상적으로 여러 번의 메모리 할당을 받은 부분을 어렵지 않게 발견할 수 있다. 메모리 감시자는 헤더 파일 하나(TraceMem.h)와 소스 파일 하나(TraceMem.c)로 구성된다. 각각의 구현 방법에 대해 설명하도록 하겠다.




위로


TraceMem.h

메모리 감시자가 애플리케이션이 할당 받은 동적 메모리의 주소 값을 저장하는 역할을 하지만, 메모리 감시자 자체도 링크드 리스트로 구현되기 때문에 스스로가 동작하는 동안에도 동적 메모리의 할당을 하게 된다. 즉, 애플리케이션에서 할당한 메모리 주소를 메모리 감시자의 링크드 리스트에 저장하기 위해 다른 메모리를 할당해야 하는 것이다.
그러면 메모리 감시자는 주소 값을 저장하기 위해 할당 받은 메모리의 주소를 또 다시 저장하려 들것이다. 이 때문에 순환참조가 발생되며, 메모리 누수 잡기를 시작도 하기 전에 시스템이 다운되는 상황이 벌어지게 된다. 이러한 문제를 피하기 위해 메모리 감시자가 스스로의 목적에 의해 사용하는 동적 메모리의 할당과 해제는 메모리 감시자의 감시를 받지 않도록 할 필요가 있다.
아래 코드를 살펴보면 매크로를 이용해서 _MALLOC와 _FREE를 정의하였다. _MALLOC와 _FREE는 realloc을 이용하였는데, realloc은 malloc과 free의 기능도 모두 할 수 있는 범용적인 메모리 함수다. 본래 realloc은 malloc이나 calloc으로 할당한 메모리의 크기를 변경시킬 때 사용하는 함수이다. 기존에 할당 받은 주소를 NULL로 주면 메모리를 새로 할당하는 malloc의 역할을 하고, 새로 할당할 메모리의 크기를 0으로 하면 기존의 메모리만 해제시켜 버리기 때문에 free와 같은 역할을 한다.
메모리 감시자는 malloc과 free가 호출될 때 할당되거나 해제된 주소를 관리하므로, 메모리 감시자 코드를 구현하는 데는 malloc이나 calloc을 사용하지 않고 realloc을 사용한 _MALLOC과 _FREE를 사용할 것이다.
TraceMem은 메모리 감시자의 몸체가 되고 MemItem은 메모리 주소 값을 저장하는 리스트의 노드이다. STL을 사용할 경우는 더욱 편리하게 리스트를 구현할 수 있지만, C에서도 사용할 수 있도록 링크드 리스트를 직접 구현해 보겠다.

#define _MALLOC(p)	realloc(NULL, p)
#define _FREE(p)	realloc(p, 0)

void* dbgMalloc(int size, char* file, int line);
#define malloc(n) dbgMalloc(n, __FILE__, __LINE__)

void dbgFree(void* ptr);
#define free(p)	dbgFree(p)

typedef struct _MemItem
{
	void* ptr;
	char* file;
	int line;
	unsigned long size;
	int num;
	struct _MemItem *next;
} MemItem;

typedef struct _TraceMem
{
	MemItem* head;
	MemItem* tail;
	int num;
} TraceMem;

TraceMem* TraceMemCreate();
void TraceMemDelete(TraceMem*);
void TraceMemPrint(TraceMem* self);
TraceMem* TraceMemGetSummary(TraceMem* self);

extern TraceMem* traceMem;

TraceMem.c

dbgMalloc과 dbgFree는 malloc과 free를 대신하여 수행되며 메모리 감시자에 메모리 주소를 추가하고 삭제하는 역할을 하며 아래 코드는 메모리 감시자의 몸체 부분으로 임베디드 환경에서도 사용할 수 있도록 C를 객체지향 스타일로 작성한 것이다.

void* dbgMalloc(int size, char* file, int line)
{
	void* ptr = _MALLOC(size);

	TraceMemAdd(traceMem, ptr,  file, line, size);

	return ptr;
}

void dbgFree(void* ptr)
{
	TraceMemRemove(traceMem, ptr);

	_FREE(ptr);
}

TraceMem* traceMem = NULL;

/* 링크드 리스트 노드 생성자 */
MemItem* MemItemCreate(void* ptr, char* file, int line, unsigned long size)
{
	MemItem* self = (MemItem*) _MALLOC(sizeof(MemItem));

	self->file = (char*)_MALLOC(strlen(file) + 1);
	strcpy(self->file, file);
	self->line = line;
	self->size = size;
	self->num = 1;
	self->ptr = ptr;

	return self;	
}

/* 소멸자 */
void MemItemDelete(MemItem* self)
{	
	if (self == NULL)
	{
		return;
	}
		
	_FREE(self->file);
	_FREE(self);	
}

void MemItemPrint(MemItem* self)
{	
	printf(" ++ [%s:%d:%p] : %d/%d\n", self->file, self->line, self->ptr, self->num, self->size );	
}

/* 메모리 감시자 생성자 */
TraceMem* TraceMemCreate()
{
	TraceMem* self = (TraceMem*) _MALLOC(sizeof(TraceMem));
	
	self->head = MemItemCreate(0, "Head", 0, 0);
	self->tail = MemItemCreate(0, "Tail", 0, 0);
	
	self->head->next = self->tail;
	self->tail->next = NULL;
	
	self->num = 0;

	return self;
}

/* 소멸자 */
void TraceMemDelete(TraceMem* self)
{
	while ( self->head->next != self->tail )
	{
		MemItemDelete(	TraceMemPop(self, self->head->next) );		
	}

	if (self->num != 0)
	{
		printf(" ++++ ERROR : TraceMem has Items %d", self->num);
	}

	_FREE(self->head);
	_FREE(self->tail);
	_FREE(self);
}

/* 주소값을 이용한 노드 검색 */
MemItem* TraceMemFindPtr(TraceMem* self, void* ptr)
{
	MemItem* iter;

	for ( iter = self->head->next; iter != self->tail; iter = iter->next )
	{
		if (ptr == iter->ptr)	
			return iter;
	}

	return NULL;	
}

int TraceMemPush(TraceMem* self, MemItem* item)
{
	MemItem* next = self->head->next;

	self->head->next = item;
	item->next = next;

	return (self->num++);
}

MemItem* TraceMemPop(TraceMem* self, MemItem* item)
{
	MemItem *iter;

	for ( iter = self->head; iter != self->tail; iter = iter->next )
	{
		if (iter->next == item)
		{
			iter->next = iter->next->next;			
			item->next = NULL;
			self->num--;
			
			return item;
		}
	}

	return NULL;
}

/* 메모리 정보를 리스트에 추가 */
int TraceMemAdd(TraceMem* self, void* ptr, char* file, int line, unsigned long size)
{
	MemItem *tar;
	
	if ( (tar = TraceMemFindPtr(self, ptr)) == NULL)
	{
		MemItem* item = MemItemCreate(ptr, file, line, size);		
		TraceMemPush(self, item);
	}
	else
	{
		TraceMemPrint(self);
	}

	return 0;
}

/* 메모리 정보를 리스트에서 제거 */
int TraceMemRemove(TraceMem* self, void* ptr)
{
	MemItem *tar;
	
	if ( (tar = TraceMemFindPtr(self, ptr)) != NULL)
	{
		MemItemDelete( TraceMemPop(self, tar) );
	}
	else
	{
		TraceMemPrint(self);
	}

	return 0;
}

/* 메모리 감시자 내용 출력 */
void TraceMemPrint(TraceMem* self)
{
	MemItem *iter;

	printf("\n ++ TraceMemPrint\n");

	for(iter = self->head->next; iter != self->tail; iter = iter->next)
	{
		MemItemPrint(iter);
	}
}

/* 파일명과 라인수로 노드를 찾는 함수 */
MemItem* TraceMemFindFileLine(TraceMem* self, char* file, int line)
{
	MemItem* iter;

	for ( iter = self->head->next; iter != self->tail; iter = iter->next )
	{
		if (line == iter->line && strcmp(file, iter->file) == 0)
			return iter;
	}

	return NULL;	
}

/* 정리된 메모리 상태를 주는 함수 */
TraceMem* TraceMemGetSummary(TraceMem* self)
{
	MemItem *iter, *tar;
	TraceMem* sum = TraceMemCreate();

	for (iter = self->head->next; iter != self->tail; iter = iter->next)
	{		
		if ( (tar = TraceMemFindFileLine(sum, iter->file, iter->line)) == NULL)
		{
			MemItem *item = MemItemCreate(0, iter->file, iter->line, iter->size);
			TraceMemPush(sum, item);
		}
		else
		{
			tar->num ++;
			tar->size += iter->size;
		}
	}

	return sum;	




위로


Test 수행하기

  
#include <iostream>
#include "TraceMem.h"

using namespace std;

int main()
{
	traceMem = TraceMemCreate();

	int *ptr = (int*) malloc(100);  // Line 11
	int *ptr2 = (int*) malloc(200);  // Line 12

	for (int i=1; i<=100; i++)
	{
		malloc(i); // Line 16
		if (i%4 == 0)
		{
			malloc(i);  // Line 19
		}
	}

	free(ptr);
	free(ptr2);

	TraceMem* summary = TraceMemGetSummary(traceMem);
	TraceMemPrint(summary);
	
	TraceMemDelete(summary);
	TraceMemDelete(traceMem);

	return 0;
}

 > Report
 ++ TraceMemPrint
 ++ [D:\Works\VCxx\MemTest\Main.cpp:16:00000000] : 100/ 5050
 ++ [D:\Works\VCxx\MemTest\Main.cpp:19:00000000] : 25/ 1300


위의 예는 Main.cpp의 11, 12 번째 줄에서 할당한 메모리는 정상적으로 해제한 반면 16, 19번째 줄에서 할당한 메모리는 해제하지 않았다. Report는 해제되지 않은 메모리를 보여주는데, Main.cpp의 16번째 줄의 두 숫자 100은 해제되지 않은 메모리의 개수이고 5050은 메모리 사이즈의 합이다. 테스트 코드에서는 1부터 100까지의 크기로 메모리를 할당했었기 때문에, 할당된 횟수는 100회이고 사이즈는 1부터 100의 합인 5050이 된다.

결론

이상으로 메모리 감시자를 이용하여 메모리 누수를 손쉽게 찾을 수 있는 방법에 대해 알아봤다. 소개한 코드에서는 malloc과 free를 사용한 경우를 예로 들었지만 calloc에 대해서도 적용할 수 있으며, 약간의 매크로 트릭과 연산자 오버로딩을 이용하면 C++의 new와 delete에도 적용할 수 있다. 물론 소개한 알고리즘을 응용하여 가비지 컬렉터가 없는 여타의 언어에 대해서도 모두 적용할 수 있다.
메모리 감시자는 간단하지만 부담스러운 가격의 상용 디버깅 툴을 사용하지 않더라도 메모리 누수를 찾아내는 강력한 기능을 제공한다. 더구나 대부분 C를 이용하여 작성되는 임베디드 환경에서라면, 그나마도 변변한 디버깅 툴도 없는 실정이어서 특히 유용한 툴이 될 수 있을 것이다.

신고
1 0
Programming~*/C++

void ex(int** parr,int* psiz)// 전달인자, (배열,배열크기)
{
 int exsize=*psiz+3;
 int* temp=(int*)malloc(sizeof(int)*exsize);
 int i;

 for(i=0; i<exsize; i++)
  temp[i]=(*parr)[i];

 free(*parr);
 *parr=temp;
 *psiz=3;

}

 

위의 함수는 새로운 배열 생성해서 복사 하는 겁니다. 그리고 기존에 있는 배열은 지우고, 새로운 배열로 만드는겁니다. 다만 기존 배열의 값을 새로운 배열에 복사하고, 크기만 3만큼 늘이는것입니다.

 

여기서 헷갈리는건 포인트 자체적으로 헷갈립니다.

물론 공부 안한건 아닙니다.. 해도 헷갈립니다.

 

처음에 배열을 더블포인트로 전달받고, 사이즈크기도 포인트로 전달받습니다.

처음에 exsize에 기존 포인터 크기에 +3을 더한것을 대입합니다.

여기서 *psiz는 아마도 psiz의 주소값의 값에다가 3을 더하는걸로 알고 있습니다.

그리고 맬로케이션 함수로 동적메모리 활당.

temp에 인트형 크기에 exsize크김만큼 받아서, 활당하고, 활당받은 메모리 첫번째 주소의 반환값을

temp에 저장합니다. 대충 이것도 아는건데..

문제는 더블 포인트입니다.. 이게 스왑일때도 헷갈렸는데 여기서도 헷갈려요

 

for(i=0; i<exsize; i++)
  temp[i]=(*parr)[i];

 

이건 i에서 확장된 크기만큼 증가합니다.

temp는 포인트입니다. 하지만 parr은 더블포인트입니다.

 

이걸 대입연사자로 할때 저는 무지무지 x 10 정도로 헷갈립니다..

한마디로 전 포인트와  더블포인트 비교 혹은 대입연사자일 경우 너무나 헷갈립니다..

 

한마디로 개념이 제대로 안잡혀있는거 같은데 책 봐도 잘 모르겠습니다.

 물론 그 뒤에

 

free(*parr);
 *parr=temp;
 *psiz=3;

 

이건 비워주는건데 더블포인트에서 왜 !! **parr 이 아닌 *parr일까요?

더블이 포인트 주소를 저장하는 변수인데.. 즉 *parr은 그 저장한 포인터의 주소? 인가요??

더블포인트// 포인트 // 변수

a - > b -> c

즉 a가 parr 이면 즉 **parr 은 c를 나타내는것이고, *parr 은 c의 주소를 가지고 있는 b를 나타내는것인가요??

 

아 헷갈려..ㅠㅠ..

 

그리고 *parr=temp를 다시 복사해주는데.. 여기서도 더블포인트 형과 포인트형과의 연산도

무지무지 헷갈립니다.. 부디 프로그램 고수님들 저 같은 무지한 자를 구원해주소서~~ ㅜㅜ

 

한마디로 전 더블포인터가 나옴 무지 헷갈립니다.

걍 포인트는 어느정도 이해되는거 같은데..ㅠㅠ

신고

의견 쓰기

질문자 채택된 경우, 추가 답변 등록이 불가합니다.

질문자 선택

re: 포인터에 대해서 질문합니다. 고수님들 부탁드려요 ㅠㅠ

xenoslave

답변채택률 95.4%

2009.03.04 16:30

질문자 인사 감사합니다

 

 

ex 함수는 위에 파란 선과 같이 arr을 새로 할당한 메모리를 가리키게 하는 게 목적입니다.

 

arr 값 자체를 바꾸려면 arr 변수의 주소를 알아야 그 주소에 새 값을 쓸텐데,

 

arr이 포인터이므로 이를 받는 매개변수는 이중 포인터가 되어야 맞습니다.

 

포인터 변수의 주소를 받아와야 그 포인터에 적힌 기존 메모리 주소를 새 메모리 주소로 적을 테니까요.

 

만약 그냥 포인터 값을 넘긴다면 아래 그림과 같이 기존 메모리 주솟 값이 넘어가는데,

 

이렇게 되면 지역변수인 parr에만 새 메모리가 할당되고 정작 변경해야 할 arr은 건드릴 방법이 없습니다.

 

게다가 parr은 함수가 끝나면 사라지는데 그렇다면 새로 할당한 메모리는 해제할 방법조차 없어집니다.



-출처 : 지식인

신고
1 0
Programming~*/C++
신고
0 0
Programming~*/C++

rand()함수로 난수를 10개씩 생성해 출력 하는 간단한 프로그램을 만들고 나서 이를 계속 실행 해보면

실행 될 때 출력되는 숫자들이 동일한 숫자임을 알 수 있다.

  1.     
  2. #include <stdio.h>   
  3. #include <stdlib.h>   
  4. #include <time.h>   
  5. int main(void)   
  6. {   
  7.   
  8.      int i;   
  9.      printf("난수들의 집합:{");   
  10.     for(i=0; i<10; i++)   
  11.     {   
  12.         printf("%d", rand()%10);   
  13.             if(i!=9)   
  14.             printf(",");       
  15.     }   
  16.      printf("}\n");    
  17. }  

srand() 는 한번만 호출합니다.

따라서 이 문제를 해결 하기 위해서는 srand()를 사용 해야 한다.

단 srand()는 main()함수의 앞부분에 한번만 호출 한다. (for문 에 들어 가면 안됨)

  1. srand((unsigned)time(NULL));  

여기에 time() 함수를 사용하여 난수 생성의 초기 값으로 현재 시간을 사용 함으로써  난수를 좀 더

세밀하게 표현 가능 하다.

인자로 NULL을 넘기는건 그냥 time()함수가 리턴 하는 값만 사용한다는 것입니다.


#MSDN  : ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.ko/dv_vccrt/html/75d9df25-7aaf-4a88-b940-2775559634e8.htm


Generates a pseudorandom number

Remarks

The rand function returns a pseudorandom integer in the range 0 to RAND_MAX (32767). Use the srand function to seed the pseudorandom-number generator before calling rand.

신고
0 0
Programming~*/C++

본문스크랩 C언어 문제 풀이 좀 해주세요~~ 문제 / 소스/ C / C++

2009/05/22 20:37

복사 http://blog.naver.com/intel258/67439786

출처 지식iN >C, C++
질문: C언어 문제 풀이 좀 해주세요~~ konan415 / 2009-05-22 18:25

학교 레포트인데..ㅡㅡ;

풀시간두 없구...잘 모르겠네요..

 

아시는 분 있으시면 도와주세요~~

 

 

 

다음과 같이 학생들의 과목별 성적이 있습니다. 학생들의 이름과 과목별
점수를 평균점수의 내림차순으로 출력하는 프로그램을 작성하세요.

- 학생수는 매크로로 정의하여 데이터수가 바뀌더라도 매크로 정의부분과
  데이터만 수정하여 프로그램을 사용할 수 있도록 작성하세요.

 

학생이름   국어    영어   수학   과학
Britney     90     75      70     75
Mariah      80     80      90     90
Jessica     60     70      85     75
Yuhki       80     80      100     90
Westlife    65     60       80     70


=-=-=-=-=-=-=-=-=-= 예 시 =-=-=-=-=-=-=-=-=-=
<출력>
======================================
학생이름  국어  영어  수학    과학
======================================
Yuhki       80     80     100    90
Mariah      80     80      90    90
Britney     90     75      70    75
Jessica     60     70      85    75
Westlife    65     60      80    70

답변: re: C언어 문제 풀이 좀 해주세요~~ intel258 / 2009-05-22 20:34

#include
#include

struct sc{
 char name[10]; // 이름
 int kor; // 국어
 int eng; // 영어
 int math; // 수학
 int sien; // 과학
 int avr; // 평균
};

using namespace std;

void main(){
 int i=0;
 sc abc[5] = {{ "Britney", 90, 75, 70, 75, 0}, { "Mariah", 80, 80, 90, 90, 0},
 { "Jessica", 60, 70, 85, 75, 0}, { "Yuhki", 80, 80, 100, 90, 0}, { "Westlife", 65, 60, 80, 70, 0}};

 for(i=0; i<5; i++){
  abc[i].avr = (abc[i].kor + abc[i].eng + abc[i].math + abc[i].sien) / 4;
 }
 for(int i=0; i<5; i++){
  for(int j=i; j<5; j++){
   if(abc[i].avr <= abc[j].avr){
    swap(abc[i], abc[j]);
   }
  }
 }
 for(i=0; i<5; i++){
  cout << abc[i].name << ' ' << abc[i].kor << ' ' << abc[i].eng << ' ' <<
   abc[i].math << ' ' << abc[i].sien << endl;
 }
}

 

열공하세요^^*




내가 풀어준 그임.... ㅋ 

신고
0 0
Programming~*/C++

본문스크랩 링크드 리스트에 정확한 개념좀... 무엇 하는데 사용? 문제 / 소스/ C / C++

2009/05/27 22:16

복사 http://blog.naver.com/intel258/67725645

출처 지식iN >C, C++
질문: 링크드 리스트에 정확한 개념좀... 무엇 하는데 사용? jck1 / 2005-07-25 23:45

링크드 리스트를 많이 들어봤는데 정확한 개념을 모르겠습니다.

 

삼국지 게임에서요

 

도시들이 각각 분포 되 있잖아여...

 

지도가 대략

 

 

영창  천수 

 

북평

 

진양

 

이렇게 있으면요...

 

진양에서 천수로 가는길은 진양 => 북평 => 영창 => 천수인데요..

여기서 진양에서 바로 천수로 못가게 만드는것이 링크드 리스트인가요?

그러니깐 인접 리스트말이죠?  인접한곳만 거쳐서 간다.. 이 개념이 링크드 리스트인가요?

 

답변: re: 링크드 리스트에 정확한 개념좀... 무엇 하는데 사용? motor0070 / 2005-07-26 00:44

링크드 리스트와 유사하면서 비교되는 개념이 배열이랍니다.

일딴 배열이 무엇 인지는 아시겠죠.

배열은 같은 자료형을 여러개 사용할때 유용하게 쓸수 있겠죠.

그러나 그 같은 자료형들이 몇개가 생길지 예상을 못하거나 너무 가변적일때가 있잖아요.

 

예를 들어 학생 데이터를 배열로 잡았을 경우,

학생이 30명 정도인데 적어도 100명은 안 넘을 것이다고 배열을 100개를 할당한다면

70명 분은 '낭비' 가 생기는 거죠.

 

링크드 리스트라는 것은 주로 일자로 연결하여 순차적으로 접근해서 모든 자료를

접근 할 수 있게 하면서 크기를 동적으로 늘였다 줄였다 할 수 있는 것입니다.

 

따라서 링크드 리스트와 배열과의 차이점은

링크드 리스트는 크기를 동적으로 할당하여 메모리 낭비를 막는 개념이고

배열은 크기를 정적으로 할당하므로 주로 크기가 한정되거나 거의 변화가 없는 부분에서 많이 사용합니다.

(여기서 배열도 동적으로 생성 가능하다고 태클 하실 분이 많으실텐데.. 중간에 삽입될때를 생각하셔서 태클을 자제 좀.. ^^)

대신 링크드 리스트는 임의접근이 거의 불가능하고 배열은 임의접근이 가능하죠.

이 말은 100개의 데이터 중에서 50번째 데이터를 접근하려고 할때

링크드 리스트는 첫번째부터 링크를 따라 50번을 가야 원하는 데이터에 접근을 할수 있는데 배열은 array[49] 이렇게 인덱스를 이용하여 단 한번에 접근이 가능하지요.

그러므로 그 성격에 맞게 링크드 리스트와 배열을 선택해서 사용하는게 중요합니다.

 

 

님께서 질문하신 부분은 그래프 개념에서 나온것으로 추측되는데

그래프를 데이터로 나타낼때 배열이나 링크드 리스트를 이용한 개념을

자료구조나 알고리즘 책에서 주로 다룹니다.

 

예를 들어서

│ 

①─②──③─④

│   └⑤   │

⑥      │   ⑦

         ⑧─⑨

이런 길이 있다고 합니다.

줄로 연결 되어 있는 부분이 님께서 말씀하신 삼국지의 길이라고 한다면

배열로 나타낼 경우에는

 

   0 1 2 3 4 5 6 7 8 9

0 - 0 - - - - - - - -

1 0 - 0 - - - 0 - - -

2 - 0 - 0 - 0 - - - -

3 - - 0 - 0 - - 0 - -

4 - - - 0 - - - - - -

5 - - 0 - - - - - 0 -

6 - 0 - - - - - - - -

7 - - - 0 - - - - - -

8 - - - - - 0 - - - 0

9 - - - - - - - - 0 -

 

첫칸과 첫줄은 이중배열 인덱스를 나타냅니다.

이런식으로 0번과 1번이 연결 되어 있으므로 a[0][1], a[1][0] 에 연결 되어 있다는 체크를 합니다.

보통 1과 0으로 표시하지만 보기 쉽게 "0"이 연결 되어 있는 것이고 "-"가 연결이 안된 부분 입니다.

저렇게 하면 100개의 저장 공간이 필요합니다.

즉 도시개수의 제곱만큼의 공간이 필요하다는 것이죠.

 

 

링크드 리스트의 경우에 표현 하는 방식은

│ 

①─②──③─④

│   └⑤   │

⑥      │   ⑦

         ⑧─⑨

0->1

1->0->2->6

2->1->3->5

3->2->4->7

4->3

5->2->8

6->1

7->3

8->5->9

9->8

이렇게 표현합니다. 맨 앞에 있는 숫자는 보통 배열로 잡습니다.

그러면 a[10]크기의 배열과 노드가 18개이므로 28개의 저장공간이 필요하니

같은 내용을 표시하는데 72개의 공간이 절약이 됩니다.

(물론 포인터공간은 셈하지 않았습니다. 그러나 데이터의 크기가 크다면

포인터의 크기는 무시할 수 있습니다.)

 

 

따라서

진양에서 천수로 가는길은 진양 => 북평 => 영창 => 천수인데요..

이건 그래프를 링크드 리스트로 표현한것으로 보기 힘들고

그래프를 링크드 리스트로 표현한다면

진양에 인접한 도시들이 옆에 붙게 됩니다.

만약 이 그래프에서

│ 

①─②──③─④

│   └⑤   │

⑥      │   ⑦

         ⑧─⑨

0번이 천수쯤 될테고 3번을 진양으로 본다면

링크드 리스트 구조에서 찾아 볼때 3번에서 0번으로 가는 길을 찾기 위해서

0->1

1->0->2->6

2->1->3->5

3->2->4->7

4->3

5->2->8

6->1

7->3

8->5->9

9->8

3번에 연결되어 있는 2, 4, 7번으로 갑니다. 원래는 다 방문해서 가봐야 하는데

우리는 눈에 길이 보이니까 ^^; 2번으로 갑시다.

그럼 2번에 연결 되어 있는 1, 3, 5번이 있는데 역시 1번으로 가죠.

그럼 1번에 연결된 도시는 0, 2, 6번으로 0번으로 갈 수 있는거죠.

따라서 3번과 0번은 같은 그래프 안에 있다고 할 수 있습니다.

(가는 길이 있다는 말이죠.)

이런 식으로 표현을 합니다.

 

이렇게 보면 배열로 나타낸 그래프가 너무 초라해 지는것 같아서 좀 덧 붙이자면

다루기 쉬운걸로 보면 링크드 리스트는 배열을 따라갈 수가 없겠죠.




신고
0 2
블로그 이미지

Software Architect

DaddyGom