본문 바로가기

3.구현/C or C++

C로 객체지향 흉내내기1

아휴~ 일을 또 하나 만들어버렸다. 아직도 작성중인 자료가 많은데, 또 다른 작성중 문서를 만들어 버렸다. 언제 끝날지 모르겠다. 내용도 쉽지 않고, 이해하기도 어려워서 틀리거나, 이건 아니다 싶은 것도 많을 것 같다.

"Object Oriented Programming in Ansi C"(이하 OOPC)라는 글을 참조 했다. 내가 찾아본 내용 중에 내가 의도했던 것과 가장 비슷한 내용이었다. 다음 내용은 상당 부분 앞의 글을 많이 번역했다.

사실 C로 완벽한 C++과 같은 구현한 한 코드들도 있다. 해당 글을 읽다 보면, 사용하기 복잡하며, 실행속도도 C++에 비해 그다지 큰 향상이 없다고 한다. 오히려 떨어진다고 한다. 이부분은 내가 검증해보지 못해서 장담하지 못하지만..

이 내용은 하나의 컨샙으로 아이디어를 얻거나 일부 참고할 정도로 봐주면 될 듯하다.

작성일: 2009.07.16 (http://ospace.tistory.com/), ospace114@empal.com

추상 데이터형

추상형

데이터형은 다양하다. 이는 언어마다 표현 방법이 틀리며, 다루는 방법도 다양하다. cpp에서 가장 매력적인 개념이 추상화 개념이다. cpp에서 추상화란 객체의 자세한 부분은 생략하고 중요하거나 공통적인 부분을 추출하는 과정이라 볼 수 있다. 가급적인 객체가 갖고 있는 값을 숨기고, 처리하는 것을 원한다. 그러나 C에서 그렇지 못한 경우가 많다.

C에서는 구조체를 이용해서 데이터를 묶는데, 프로그래머는 내부에 모든 데이터 형과 이름을 자세히 알고 있어야한다. 그리고 숨길수가 없다.

이런 추상 테이터형은 정보 은닉, 분할 정복의 원리를 적용할 수 있게 한다.

Set 예제

여러 구성요소의 집합을 add, find, drop을 제공하는 예이다. 각 기능은 직관적이기에 설명이 필요 없을 것이다. 각 기능은 추가되거나 찾거나 제거된 요소를 반한한다.

위의 기능은 Set.h에 함수로 선언하였다.

#ifndef SET_H
#define SET_H

extern const void * Set;

void * add (void * set, const void * element);
void * find (const void * set, const void * element);
void * drop (void * set, const void * element);
int contains (const void * set, const void * element);

#endif

위 코드의 세부적인 부분은 건너뛰고, set은 void 포인터인 추상 데이터형이다. C에서는 추상 데이터 형을 void 포인터 형태로 다뤄지게 된다. 즉 set은 어떤 데이터이든 받아들일 수 있다.

그리고 항상 모든 함수는 첫번째 인자로 set을 넘겨준다. 그러나 모든 데이터가 하나의 구조체일 수는 없다. 그럼 어떻게 해야될까? 이부분은 앞으로 차근차근 해결하도록 하자.

그럼 위의 Set.h 파일은 완벽한 형태일까? 사용할 수 있을까? 아직은 아니다. 해결해야될 부분이 너무 많다.

우리는 set이라는 객체(실제 set인지도 모르는)에 대한 메소드를 선언해둔 것이다.

메모리 관리

다음으로 넘어가기 전에 메모리 관리를 다뤄야겠다. 모든 함수에서 받는 인자가 viod 포인터 형태이다. 즉, 대부분 메모리를 할당해서 사용한다고 보면 된다. cpp에서 본다면 new와 delete 같은 기능이 필요하다.

사실 c에서 malloc()나 free()를 사용하면 되지 않으냐할 수 있지만, 이는 단순 하나의 구조체에만 사용할 수 있고, 여러 구조체가 중첩되거나 연결되어 있다면, 하나로 관리하기 힘들다. 그러나 cpp에서는 하나로 다 관리된다.

해당 기능을 new.h에 선언해보자.

void * new (const void * type, ...);
void delete (void * item);

앞의 헤더 파일에 비해서 단순하다. 그리고 함수 선언에서 "..."라고 된 것은 오타가 아니다.

new()은 할당될 타입이 오면, 해당 타입에서 할당에 필요한 정보를 갖고와서 메모리를 할당하고 할당된 메모리의 포인터를 반환한다. 위에 "..."은 생성자에 넘겨주는 기본값이라고 보면된다. 지금은 필요하지 않다.

delete()은 단순히 할당된 객체을 받아서 할당해주면 된다. 물론 내부에 중첩된 구조체들도 모두 할당을 해제시켜준다. 물론 실제 동작은 프로그래머가 일일히 해줘야하지만...

할당과 해재는 ANSI C의 malloc()와 free()를 사용할 것이다.

좀더 추가해보기

기본적인 뼈대는 만들어진 것 같다. 문서에서 추가적인 함수와 외부 변수를 Object.h에 선언했다.

extern const void * Object; /* new(Object); */
int differ (const void * a, const void * b);

differ()은 앞의 함수와는 틀리게 리턴 값이 다르다. 이 함수는 두개의 객체를 받아서 같으면 양이 정수, 틀리면 0을 반환한다. 혹은 특정 순서에 따라서 음수, 양수를 반환한다. 대충 이정도이다. 이부분은 나도 정확히 이해하지 못하는 부분이다.

응용프로그램

어떻게 사용해야될지, 어떤 식으로 작동되는지 잘 모르면, 실제 사용하는 예를 작성해보자. 파일 명은 main.c이다. 물론 앞의 모든 예제 코드는 참조[1]에 내용이다.

#include <stdio.h>
#include "new.h"
#include "Object.h"
#include "Set.h"

int main()
{
    void * s = new(Set);
    void * a = add(s, new(Object));
    void * b = add(s, new(Object));
    void * c = new(Object);

    if (contains(s, a) && contains(s, b))
        puts("ok");

    if (contains(s, c))
        puts("contains?");

    if (differ(a, add(s, a)))
        puts("differ?");

    if (contains(s, drop(s, a)))
        puts("drop?");

    delete(drop(s, b));
    delete(drop(s, c));

    return 0;
}

대충 감이 가는가? 여기서는 Set과 Object에 대한 정확한 데이터형은 없다. 단지tSet는 데이터를 보관하는 컨테이너 역활 정도이며, 그 곳에 저장되는 데이터는 Object라는 정도이다.

새로운 Set객체를 생성하고 새로운 Object를 Set객체에 추가한다. 그리고, 객체 내에 값을 비교한다. 모든 작업이 끝나면 할당된 객체를 해제시켜준다.

Set 몸체 만들기

앞의 main.c는 컴파일은 되지만 링크는 되지 않는다. 앞에서 선언했던 추상 데이터형과 이를 관리할 메모리 관리자도 구현해보자. 실제 Set이라는 컨테이너 구현방법은 여러가지 일 수 있다. 여기서는 고정된 배열로서 벡터 형태의 컨테이너라고 보면다. 이렇게 이야기 해도 C개발자보다는 Java 개발자가 이해하기 쉽다. ㅡ.ㅡ;

자세한 것은 건너뛰고, 다시 본편으로 가서 실제 구현을 보자.

#if ! defined MANY || MANY < 1
#define MANY 10
#endif

static int heap [MANY];

void * new (const void * type, ...)
{
    int * p; /* & heap[1..] */
    for (p = heap + 1; p < heap + MANY; ++ p)
        if (! * p)
            break;
    assert(p < heap + MANY);
    * p = MANY;
    return p;
}

heap이라는 정수형 전역 변수를 선언한다. 할당될 장소는 고정되었으니 new()에서 heap 배열에서 하나씩 때어내어서 넘겨주면 된다.

배열이 비어 있다면 heap[]에 0으로 표시하고, 사용 중에 있다면 MANY로 표시한다. heap[0]은 사용하지 않는다. 객체가 set이라면 heap[0]이 될 것이다. 그리고, 더 이상 분배해줄 heap이 없다면 out of memory이고 이런 일은 발생되서는 안되므로 assert에 의해서 에러 처리한다. 이런 에러처리는 개인 취향이기 때문에 알아서 잘 하면 된다.

new()의 동작은 간단하다. heap에서 비어있는 배열(0인 값이 있는 배열)을 찾아서 MANY 값을 저장하고 해당 포인터를 반환한다.

아직 new는 type을 사용하지 않는다. 여기서는 단지 heap만 사용하여 할당한다.

그럼, delete()도 구현해보자.

void delete (void * _item)
{
    int * item = _item;
    if (item)
    {
     assert(item > heap && item < heap + MANY);
        * item = 0;
    }
}

해제는 간단하다. 앞에서 heap에 할당된 배열(MANY값이 저장)을 할당 안된 비어 있는 배열로 표시하면 된다. 즉 해당 배열에 "0"값을 넣어주면 된다. 물론 해제할 객체는 heap 배열 범위 안에 있어야겠지.

이젠 Set의 메소드인 add, find, contain, drop 그리고 differ을 보자.

기능은 대충 알고 있으므로 실제 코드를 보면서 이해해보자. 해당 코드는 Set.c에 정의된다.

void * add (void * _set, const void * _element)
{
    int * set = _set;
    const int * element = _element;

    assert(set > heap && set < heap + MANY); // set이 heap범위 안에 있는지 확인
    assert( *set == MANY); // 해당 set이 할당되었다고 확인.
    assert(element > heap && element < heap + MANY); // element도 heap 범위 안에 있음.
    if( *element == MANY) // element에 값이 있는지 여부
        * (int *) element = set - heap; // element에 set이 할당된 heap의 index를 저장
    else
        assert(*element == set - heap); // element에 이미 MANY가 아닌 다른 값이 저장.

    return (void *) element;
}

결국 add된 element 값에는 element가 속한 set의 배열 index값이 저장된다. 다시 말하면 heap 배열에 값이 MANY가 아닌 다른 값이 있다면 해당 배열은 해당 index에 해당하는 배열에 속해 있다.

void * find (const void * _set, const void * _element)
{
    int * set = _set;
    const int * element = _element;

    assert(set > heap && set < heap + MANY);// set이 heap범위 안에 있는지 확인
    assert( *set == MANY);// 해당 set이 할당되었다고 확인.
    assert(element > heap && element < heap + MANY);// element도 heap 범위 안에 있음.
    assert(*element); // elmenet에 0이 아닌 값이 있음. 이미 다른 set에 포함되어 있음.

// 해당 element가 set에 인덱스 값이라면 element의 주소가 반환되고 아니면 0이 반환
    return * element == set - heap ? (void *) element : 0; 
}

// 이는 find에 의한 결과가 0이 아니면 포함되어있다는 의미
int contains (const void * _set, const void * _element)
{
    return find(_set, _element) != 0;
}

// drop는 해당 element가 더이상 set에 속해 있지 않으므로 해당 값이 MANY설정하면 된다.
void * drop (const void * _set, const void * _element)
{
    int * element = find(_set, _element); // element가 당연히 set에 포함되어 있어야함.

    if(element)
        *element = MANY;
    return element;
}

아직 가야할 길이 멀다. 여기서 선언되지 않은 몇가지 추상 데이터형이 있다. 바로 Set과 Object이다. 아무런 의미도 없이 main()에서 사용되었고, new(), delete()뿐만 아니라 add(), find(), contains(), drop()에도 사용되지 않았다. 사실 필요는 없지만, 나중에 이런 추상 데이터형을 통해서 실제 객체를 할당하고 해제하는 등의 작업을 하려는 것이다. 일단은 사용하고 있으니 정의는 해두어야 링크는 될 것이다. Set.h에 정의해둔다.

const void * Set;
const void * Object;

다른 구현 예제 - Bag

이제 앞에서 건너뛰 Set과 Object를 사용하는 예제를 보도록 하자. 앞의 Set.h에 별다른 변경은 없고 Set과 Object만 재정의 한다.

struct Set { unsigned count; };
struct Object { unsigned count; struct Set * int; };

count는 Set에 포함된 element의 개수이다. Object가 element로서 count는 몇 개의 set에 추가되었는지 표시한다. drop()될때 마다 감소하고 0이 될때까지 삭제된다. 가방처럼 Set에는 참조계수가 있는 element가 들어있다.

Set과 Object가 새로 선언되었으니 이를 할당할 new()와 delete() 정의가 필요하다. 이제는 동적메모리 할당을 사용한다.

static const size_t _Set = sizeof(struct Set);
static const size_t _Object = sizeof(struct Object);

const void * Set = &_Set;
const void * Object  = & _Object;

new() 정의는

void * new (const void * type, ...)
{
    const size_t size = * (const size_t *) type;
    void * p = calloc(1, size);
    assert(p);
    return p;
}

_Set과 _Object는 각각 구조체의 크기이며, 동적 할당할때에 할당할 크기로 사용된다. type으로 해당 값이 들어오면 이를 size_t로 캐스팅해서 동적할당해서 사용한다.

void * add (void * _set, const void * _element)
{
    struct Set * set = _set;
    struct Object * element = (void *) _element;

    assert(set);
    assert(element);

    if (! element->in)
        element->in = set;
    else
        assert(element->in == set);
    ++element->count, ++set->count;
    return element;
}

void * find (const void * _set, const void * _element)
{
    const struct Object * element = _element;

    assert(_set);
    assert(element);

    return element->in == _set ? (void *) element : 0;
}

void * drop (void * _set, const void * _element)
{
    struct Set * set = _set;
    struct Object * element = _element;

    if (element)
    {
        if(--element->count == 0)
            element->in = 0;
        --set->element;
    }
    return element;
}

unsigned count (const void * _set)
{
    const struct Set * set = _set;
    assert(set);
    return set->count;
}

사실 count는 간단하게 직접 Set에서 액세스할 수 있다. 이는 Set 외부로 돌출되지 않고 Set 안으로 숨기려고 한다. 이 함수 호출의 오버헤드는 중요한 값이 변경같은 위험에 비해 아무것도 아니라고 한다. 어느 정도 맞는 말이다. 그러나 사실 이런 부분에 대해서 대부분의 C개발자는 꺼려한다. 직관적이지 않고, 번거로우며, 더 많은 오류가 발생할 것이라고 생각한다.

이번 예제를 Bag라고 한 것은 element가 여러번 추가될 수 있다. 추가된 만큼 삭제가능하며, 결국 set에서 없어질뿐이다.

앞의 예제에서 a가 set에 두번 추가된다. set에서 한번 삭제되면 그래도 contains()가 bag에서 값을 찾을 수 있다.

요약

추상 데이터형은 응용프로그램 코드에서 모든 구현을 숨긴다. 데이터 아이템의 표현방식같은 것.

애플리케이션은 헤더 파일만 참고하고 이곳에 데이터 형과 해당 테이터 형에 맞는 연산자를 선언한다. 인자와 리턴은 모두 제러릭 포인터를 사용한다.

해당 descriptor pointer를 new()로 넘기면 해당 데이터 아이템에 대한 포인터를 얻는다. 이 포인터는 다시 delete()로 넘겨져서 연관된 자원은 재활용하게 된다.

보통 추상 데이터 형은 한 개의 파일로 구현된다. 이상적으로, 다른 데이터 형의 형태를 다룰 수 없다. descriptor pointer는 데이터 아이텀이 필요로하는 공간의 크기를 나타내는 size_t 상수 값을 가리킨다.

연습

Object가 여러 set에 동시에 속해있다면 이러기 위해서 set을 다르게 표현해야한다. 작은 유일한 정수 값으로 object를 연속적으로 표현하고 유요한 object 개수를 제한하고 싶다면 set는 bitmap으러 긴 문자열 처럼 표현할 수 있다. 여기에 object 값에 의해 선택된 bit는 set에 object의 존재 유무에 따라 설정되거나 클리어된다.

좀더 일반적이로 쉬운 해결안은 set을 선형 리스트로 set에 있는 object의 주소를 저장하는 것이다. 이는 object에 대한 제한이 없고, object가 어떻게 생겼는지 알필요 없이 set을 구현할 수 있다.

디버기을 위해서 각각의 object를 살펴볼 수 도 있다. 이에 대한 간단한 해결책은 다음 두가지 함수이다.

int store (const void * object, FILE * fp);
int storev (const void * object, va_list  ap);

store()는 파일 포인터에 대한 object의 디스크립터를 기록한다. storev()는 va_arg()를 사용하여 ap에서 인자 목록에서 파일 포인터를 획득한다. 함수 둘다 기록된 문자 개수를 반환한다. stove()는 set을 위한 다음과 같이 구현할 수 있다.

int apply (const void * set, int (* action) (void * object, va_list ap), ...);

apply()가 가각의 element에 대해서 action()를 호출하고, 나머지 인자 목록을 넘겨준다. action()은 set는 변경하지 않지만 apply()가 종료되면 0을 반환한다. apply()는 모든 element가 처리되면 true를 반환한다.

이상은 참조[1]의 내용이다. 사실 이렇게 봐서는 나도 모르겠다. 대중은 알겠지만 도대체 어떻게 하는건지.. .ㅡㅡ; 혹시 알거나 이해하셨다면, 코멘트 부탁~~ ^^

이상으로 간단하게 추상 테이터형을 살펴봤습니다. 어떻게 응용할지는 각자가 알아서.. ㅡ.ㅡㅋ

본인도 이정도 까지 100% 이해가 힘들더군요.

va_list은 인자 목록을 사용하는 것도 컴파일러에 따라 달라질 수 있는 것이기에 실제 플로그래밍 할때는 꼭 필요할때가 아니면 피하는데.. 여기서는 기본적으로 사용하더군요.

개인마다 틀리기때문에 뭐라고 단언하기도 힘들죠. 아무튼 뭔가 얻어갔으면 하네요.

참조자료

[1] Object Oriented Programming in ANSI C, Axel-Tobias Schreiner, Rochester Institute of Technology

반응형