언어 자료구조 알고리즘/Escort C++

[C++] 함수 개체

언제나휴일 2016. 4. 15. 15:21
반응형

7.4 함수 개체

 

 함수 개체란 함수 호출 연산자가 중복 정의되어 해당 개체를 함수처럼 사용할 수 있는 개체를 말합니다. 이는 직접 연관 관계에 있을 때에 명령을 내릴 수 있는 개체는 명령을 받아 수행하는 개체의 위치를 알고 있지만, 역으로 명령을 받아 수행하는 개체가 명령을 내리는 개체를 알게 구현하는 것은 전체 프로그램 구조를 취약하게 만듭니다. 하지만 특정한 경우에 피 명령 개체가 특정 사실을 명령 개체에게 알려줄 필요가 생기는데 이 같은 경우에 콜백(호출하는 방향이 제공자에서 사용자를 호출하는 것)을 구현하게 됩니다. 이와 같은 콜백을 구현함에 있어 명령 개체에서 정의한 함수를 피 명령 개체에게 명령을 지시할 때 입력 인자로 전달하여 해당 함수가 정의된 코드를 수행하게 할 수 있습니다. 또 다른 방법으로 함수 개체를 입력 인자로 전달하여 특정 사건이 발생할 때 전달받은 함수 혹은 함수 개체를 호출함으로써 명령 개체에게 이를 통보할 수 있게 됩니다. 여기에서는 이와 같은 함수 개체를 만드는 방법과 어떠한 곳에서 사용되는지 살펴봅시다.

 

 먼저, 함수 호출 연산자를 중복 정의하는 방법을 살펴볼게요. 함수 호출 연산자는 () 연산자를 중복 정의를 하면 되고 입력 매개 변수 리스트는 함수 개체를 정의하는 개발자가 정하게 됩니다.

 

 간략한 예로 사칙연산을 하는 Calculator 클래스에 함수 호출 연산자 중복 정의를 해 보도록 할게요.

 

Calculator.h

#pragma once

 

class Calculator

{

public:

    enum CmdType{ADD,SUB,MUL,DIV};

    int operator()(int a,int b,CmdType ctype);

private:

    int Add(int a,int b);

    int Sub(int a,int b);

    int Mul(int a,int b);

    int Div(int a,int b);

};

 

 

  Calculator.h를 보시면 () 호출 연산자의 입력 인자로는 계산할 두 수와 사칙 연산 종류를 받고 연산 결과로 정수를 반환할 수 있게 약속하였습니다.

  

Calculator.cpp

#include "Calculator.h"

int Calculator::operator()(int a,int b,CmdType ctype)

{

    switch(ctype)

    {

    case ADD: return Add(a,b);

    case SUB: return Sub(a,b);

    case MUL: return Mul(a,b);

    case DIV: return Div(a,b);

    }

    return 0;

}

int Calculator::Add(int a,int b)

{

    return a+b;

}

int Calculator::Sub(int a,int b)

{

    return a-b;

}

int Calculator::Mul(int a,int b)

{

    return a*b;

}

int Calculator::Div(int a,int b)

{

    if(b)

    {

        return a/b;

    }

    throw "젯수가 0 일 수 없습니다.";

}

 

 Calculator.cpp 파일을 보시면 ()호출 연산자 중복 정의한 함수에서는 입력 인자로 받은 사칙 연산 종류에 따라 각 연산을 수행하는 함수를 호출하여 결과를 반환하도록 구현하였습니다.


함수 개체

[그림 7.12]

 

 [그림 7.12]에서 보시는 것처럼 Calculator 형식의 개체 ca를 마치 함수처럼 호출하는 것이 가능하다는 것을 알 수 있습니다. 이와 같이 함수 호출 연산자가 중복 정의되어 있어 개체를 함수처럼 호출할 수 있는 개체를 함수 개체라 부릅니다.

 

 이제 함수 개체를 사용했을 때 효과적으로 프로그래밍 할 수 있는 한 가지 예를 살펴보기로 하겠습니다.

 

 회원 관리 프로그램을 만들려고 하는데 아직 회원에 대한 구체적인 부분은 결정되지 않았습니다. 다만, 해당 프로그램에서는 회원을 순차적으로 보관하고 원하는 방식으로 회원들을 정렬할 수 있고 특정 조건에 맞는 회원을 찾거나 보관된 전체 회원에 대해 공통된 작업을 수행하는 등의 작업이 가능하게 구현하려고 합니다. 현재까지 약속된 것을 기반으로 회원을 보관하는 컬렉션을 정의할 수는 없을까요? 이와 경우에 수행할 행위의 추상적인 약속을 하고 구체적인 행위에 대해서는 컬렉션을 사용하는 곳에서 정의하게 할 수 있습니다. 그리고 사용하는 곳에서는 구체적인 명령 개체를 생성하여 컬렉션의 메서드 호출 시에 입력 인자로 전달하게 하면 컬렉션에서 이를 호출하여 원하는 목적을 달성할 수 있습니다. 이와 비슷한 경우에 대한 설명을 GoF의 디자인 패턴에서는 [그림 7.13]과 같이 Command 패턴으로 설명하고 있습니다.



Command 패턴

[그림 7.13]

 

 

 

 [그림 7.14]는 예제로 보여 줄 데모의 클래스 다이어그램 일부분입니다.


함수 개체 사용 예제의 클래스 다이어그램

[그림 7.14]

 

 회원 관리 프로그램에 모든 개체를 관리하는 개체는 Zone클래스로 정의를 해 봅시다. Zone 클래스에서는 회원들을 Collection 개체를 통해 보관하고 특정 조건에 맞게 검색하고 정렬하는 등의 작업을 수행할 것입니다. Collection은 검색에 필요한 추상화 된 기반 클래스 형식인 Search를 사용하여 Zone이 요구하는 회원을 검색해 줍니다. 실제 회원을 비교하는 클래스는 Search 클래스에서 파생된 SearchByNum 클래스입니다. 이는 Zone에서 개체를 생성하여 Collection 개체에 검색을 질의할 때 해당 개체를 입력 인자로 전달할 것입니다. 이 외에도 특정 조건으로 비교하여 정렬을 하는 부분과 Collection에 보관된 모든 회원들에 대해 특정 작업을 수행하게 하는 부분도 비슷한 매커니즘으로 수행할 수 있게 구현해 봅시다.

 

 먼저, Collection 클래스와 Command들에 대한 추상화 된 클래스들을 정의 및 구현해 봅시다.



함수 개체 실습에 정의할 형식들

[그림 7.15]

 

 [그림 7.15]는 여기서 구현할 Collection 클래스의 멤버 필드와 public으로 노출할 메서드들과 Command들에 대한 추상화 된 클래스들입니다.

 

 먼저, 여기서 구현할 Collection 클래스의 각 멤버들에 대한 역할에 대해 살펴봅시다.

 

멤버 명

설명

base

Member 개체들을 보관할 버퍼의 위치 정보

max_capacity

버퍼의 크기

nsize

현재 보관된 개체 수

~Collection

소멸자 메서드

Collection

생성자 메서드

FindMember

index 위치부터 특정 조건에 맞는 Member 개체 찾아준다.

GetCount

현재 보관된 개체 수를 반환한다.

GetMember

index 위치부터 특정 조건에 맞는 Mebmer 개체를 찾아 반납한다.

ListAll

특정 논리를 보관된 모든 Member 개체에 적용한다.

Push

Member 개체를 순차적으로 보관한다.

Sort

특정 비교 논리를 이용하여 정렬한다.

 

 












 그리고, Command들에 대한 추상 클래스로는 보관된 Member 를 입력 인자로 받아 특정 조건이 참인지를 확인할 수 있는 Search 클래스, 보관된 전체 Member 개체들에게 특정 논리를 적용하는 DoSomething 클래스, 정렬에 필요한 특정 비교 논리를 위한 Compare 클래스를 추상 클래스로 약속을 할 것입니다. 이처럼 구현하면 Collection 개체를 사용하는 Zone 개체에서는 검색하기 위한 특정 논리를 Search 클래스를 파생받아 순수 가상 함수인 operator()를 재 정의한 클래스 개체를 FindMember GetMember 메서드를 호출할 때 입력 인자로 전달하면 이를 이용하여 원하는 개체를 찾아줄 수 있습니다. 이 외에 필요한 메서드가 있다고 생각하시면 각자가 추가해 보시기 바랍니다.

 

 다음은 이들 클래스를 정의한 헤더 파일입니다. Compare, DoSomething, Search 클래스를 보시면 함수 연산자가 중복 정의된 순수 가상 함수가 있는 추상 클래스 형태로 되어 있습니다.

 

Collection.h

#pragma once

#include "Member.h"

class Compare

{

public:    virtual int operator()(Member *mem1,Member *mem2)=0;

};

class DoSomething

{

public:    virtual void operator()(Member *mem1)=0;

};

class Search

{

public:    virtual bool operator()(Member *mem1)=0;

};

class Collection

{

    Member **base;

    const int max_capacity;

    int nsize;

public:

    Collection(int _max_capacity);

    ~Collection(void);

    Member *FindMember(Search &search,int index=0);

    Member *GetMember(Search &search,int index=0);

    int GetCount()const;

    int GetCapacity()const;

    void ListAll(DoSomething &doit);

    bool Push(Member *member);

    void Sort(Compare &compare);

    bool IsFull()const;

private:

    void Erase(int index);

};

 

 다음은 Collecion의 각 멤버 메서드를 구현한 소스 코드입니다.

 

Collection.cpp

#include "Collection.h"

Collection::Collection(int _max_capacity):max_capacity(_max_capacity),nsize(0)

{

    base = new Member*[max_capacity];

}

Collection::~Collection(void)

{

    delete[] base;

}

Member *Collection::FindMember(Search &search,int index)

{

    for(int i = 0; i < nsize ; i++)

    {

        if(search(base[i]))

        {

            return base[i];

        }

    }

    return 0;

}

Member *Collection::GetMember(Search &search,int index)

{

    Member *re=0;

    for(int i = 0; i < nsize ; i++)

    {

        if(search(base[i]))

        {

             re = base[i];

            Erase(i);

            break;

        }

    }

    return re;

}

 

bool Collection::Push(Member *member)

{

    if(IsFull())

    {

        return false;

    }

    base[nsize] = member;

    nsize++;

    return true;

}

 

void Collection::ListAll(DoSomething &doit)

{

    for(int i = 0; i< nsize ; i++)

    {

        doit(base[i]);

    }

}

 

void Collection::Sort(Compare &compare)

{

    Member *temp=0;        

    for(int i = 0; i < nsize ; i++)

    {

        for(int j=i+1; j < nsize; j++)

        {

            if(compare(base[i],base[j])>0)

            {

                temp = base[i];

                base[i]=base[j];

                base[j]=temp;

            }

        }

    }

}

 

 

void Collection::Erase(int index)

{

    int nsize_minus_one = nsize-1;

    for(int i = index; i<nsize_minus_one;i++)

    {

        base[i] = base[i+1];

    }

    nsize_minus_one;

}

int Collection::GetCount()const

{

    return nsize;

}

int Collection::GetCapacity()const

{

    return max_capacity;

}

bool Collection::IsFull()const

{

    return max_capacity == nsize;

}

 

 FindMember 메서드나 GetMember 메서드 등을 보면 추상 클래스 형식의 참조 형태의 변수로 인자로 전달받은 구체화한 함수 개체를 이용하여 비교나 원하는 논리의 개체를 검색 등에 사용됩니다.

 

Member *Collection::FindMember(Search &search,int index)

{    

      for(int i = 0; i < nsize ; i++)

             {

                           if(search(base[i]))

                           {                         

                                        return base[i];

                           }

             }

             return 0;

}

 

 

 이들을 사용하는 코드들을 살펴보기로 합시다. 먼저, Member 클래스는 다음과 같이 간단하게 회원 번호와 이름을 멤버 필드로 갖고 개체 출력자를 구현을 하였습니다.

 

Member.h

#pragma once

#include "MyGlobal.h"

class Member

{

    const int num;

    string name;

public:

    Member(int _num,string _name);

    int GetNum()const;

    string GetName()const;

};

extern ostream &operator<<(ostream &os,const Member &member);

extern ostream &operator<<(ostream &os,const Member *member);

 

Member.cpp

#include "Member.h"

Member::Member(int _num,string _name):num(_num),name(_name){}

int Member::GetNum()const{ return num;}

string Member::GetName()const

{

    return name;

}

ostream &operator<<(ostream &os,const Member &member)

{

    os<<"번호:"<<member.GetNum()<<endl;

    os<<"이름:"<<member.GetName()<<endl;

    return os;

}

ostream &operator<<(ostream &os,const Member *member)

{

    return os<<*member;

}

  

 그리고 Zone에서 수를 입력받거나 문자열을 입력받거나 메뉴를 선택하기 위해 기능 키를 입력받는 정적 메서드들로 구성된 MyGlobal 클래스를 정의하겠습니다.

 

 MyGlobal 클래스는 여기서 얘기하고자 하는 함수 개체와는 아무런 관련성이 없는 부분입니다. MyGlobal 클래스는 개체 인스턴스를 만들 수 없게 하고 전체 프로그램에서 사용하게 될 함수들을 정적 멤버 메서드로 구현을 하는 방법을 보여주는 예입니다. Java C#과 같은 언어에서는 전역 스코프를 지원하지 않지만 MyGlobal 클래스와 같이 노출 수위가 public인 정적 멤버들로 구성된 클래스를 정의하여 모든 스코프에서 이들을 전역 스코프에 있는 자원처럼 사용할 수 있게 해 줍니다. 물론, C++에서는 전역 스코프를 지원하기 때문에 이처럼 제공하지 않고 전역에 함수를 정의하여 사용할 수도 있습니다.

 

 MyGlobal에서는 기능 키를 입력받는 메서드 GetKey를 제공하고 있습니다. 그리고 GetKey에서 반환하는 형식을 MyGlobal 클래스 내부에 열거형 형식으로 KeyData를 정의하였습니다. 이처럼 형식 내부에 형식을 정의하는 경우에도 내부 형식의 접근 수준이 public인 경우에만 외부 스코프에서 사용할 수 있습니다.

 

MyGlobal.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

#pragma warning(disable:4996)

class MyGlobal

{

public:

    enum KeyData

    {

        NO_DEFINED,F1,F2,F3,F4,F5,F6,F7,ESC

    };

    static int GetNum();

    static string GetStr();

    static KeyData GetKey();

private:

    MyGlobal(void){}

    ~MyGlobal(void){}

};

 

  

 먼저, 정수를 입력받는 메서드에 대해서 살펴보기로 합시다. cin >> 연산자를 통해 정수를 입력을 받으면 최종 사용자가 정수가 아닌 문자를 입력하면 이에 대해 처리하지 않고 cin의 내부 버퍼에 처리하지 않은 문자들이 존재하게 됩니다. 이 떄문에 cin >> 연산을 통해 다시 입력을 받으려 할 때 최종 사용자로부터 스트림을 입력받지 않고 내부 버퍼에 있는 스트림을 사용하게 됩니다. 이러한 특징이 C로 콘솔 프로그래밍을 할 때 처리가 쉽지 않을 수 있습니다.  


[그림 7.16]

 

 [그림 7.16]을 보면 정수를 입력해야 하는 곳에서 최종 사용자가 잘못 입력했을 때 다음 입력에도 영향을 준다는 것을 알 수 있습니다. 여기에서는 최종 사용자로부터 하나의 스트림을 지역 변수에 입력 받고 cin의 버퍼를 비워준 후에 지역 변수에 있는 것을 정수로 변환하여 반환하도록 함으로써 이러한 문제를 해결하였습니다.

 

int MyGlobal::GetNum()

{

      int num;

      char buf[256+1];

      cin.getline(buf,256);

      cin.clear();

      sscanf(buf,"%d",&num);

      return num;

}

 

 

 

 또한, cin >>를 통해 문자열을 입력받을 때 공백이나 탭이 중간에 입력되면 이들 이전까지만 하나의 문자열로 얻어오게 되고 마찬가지로 나머지는 cin의 내부 버퍼에 존재하게 되어 다음 입력 시에 최종 사용자로부터 스트림을 입력받지 않고 내부 버퍼에 있는 것을 사용하게 됩니다. 여기에서는 하나의 스트림을 지역 변수에 받아온 후에 cin 내부 버퍼는 비워주고 받아온 것을 반환하는 형태로 구현하였습니다.

 

string MyGlobal::GetStr()

{    

      char buf[256+1];

      cin.getline(buf,256);                         

      cin.clear();

      return buf;

}

 

 마지막으로 기능 키를 입력받는 GetKey 메서드는 getch를 이용하여 구현하였습니다. getch 함수는 F1이나 F2 등의 기능 키를 입력한 것을 판단하기 위해서는 getch함수를 두 번 호출해야 합니다. getch 함수를 호출하였을 때 F1키를 누르면 0을 반환하고 다시 getch 함수를 호출하면 최종 사용자의 입력을 대기하지 않고 59를 반환합니다. 그리고, ESC키를 눌렀을 경우에는 한 번만 호출해도 되는데 반환되는 값은 27입니다. 이에 대한 부분은 약속에 의거한 것이라 특별히 설명할 부분은 없습니다. 다만, GetKey에서는 MyGlobal 클래스 내부에 정의한 KeyData 형식을 반환하도록 하였는데 이에 대한 구현부에서 반환 형식을 MyGlobal::KeyData라고 명시해야 합니다. KeyData라는 형식은 MyGlobal 내부에 있는 형식이기 때문입니다.

 

 다음은 MyGlobal.cpp 소스의 구현 내용입니다.

 

MyGlobal.cpp

#include "MyGlobal.h"

#include <stdio.h>

#include <conio.h>

int MyGlobal::GetNum()

{

    int num;

    char buf[256+1];

    cin.getline(buf,256);

    cin.clear();

    sscanf(buf,"%d",&num);

    return num;

}

string MyGlobal::GetStr()

{

    char buf[256+1];

    cin.getline(buf,256);

    cin.clear();

    return buf;

}

 

MyGlobal::KeyData MyGlobal::GetKey()

{

    int key = getch();

    if(key == 27)

    {

        return ESC;

    }

    if(key==0)

    {

        key = getch();

        switch(key)

        {

        case 59: return F1;

        case 60: return F2;

        case 61: return F3;

        case 62: return F4;

        case 63: return F5;

        case 64: return F6;

        case 65: return F7;

        }

    }

    return NO_DEFINED;

}

  

 이제 회원들을 관리하는 Zone 클래스를 정의 및 구현해 보기로 합시다.


[그림 7.17]

 

 [그림 7.17]은 여기서 구현할 Zone 클래스 다이어그램입니다. 멤버 필드로 회원들을 보관하는 Collection 형식 포인터 collection이 있습니다. 그리고 회원 추가, 삭제, 번호로 검색, 이름으로 검색, 번호 순으로 정렬하기, 이름순으로 정렬하기 등이 있습니다. Zone 클래스의 초기화 부분에서 최대 관리할 회원 수를 입력받고 회원들을 보관할 수 있는 Collection 개체를 생성합니다. Zone 클래스 Run 부분에서는 최종 사용자로부터 메뉴를 입력받고 입력한 메뉴에 따라 회원 추가, 삭제, 번호로 검색, 이름으로 검색, 번호순으로 정렬, 이름순으로 정렬, 전체 보기를 수행합니다. 그리고, Zone의 소멸자에서는 종료화를 수행하고 종료화에서는 보관된 모든 회원 개체를 소멸하고 회원을 보관하는 Collection 개체를 소멸하는 작업을 수행합니다.

 

 여기에서는 함수 개체를 사용하는 부분에 초점을 맞추어 설명하도록 하겠습니다. 먼저, 회원 추가 기능에서는 추가하려는 번호의 회원이 있는지를 검색을 하여 존재하지 않을 때만 추가하도록 해 봅시다. 회원 추가하는 과정에서 특정 번호의 회원이 있는지를 검색하기 위해서는 최종 사용자가 입력한 번호에 해당하는 회원 개체가 이미 있는지를 확인하여야 할 것입니다. Collection에서는 검색을 위한 논리에 대한 부분은 Search클래스에서 추상화된 형태로 약속하였습니다. 이를 파생받아 구체화 된 검색 논리에 사용할 SearchByNumFun 클래스를 정의하고 해당 형식의 개체를 Collection FindMember 메서드의 입력 인자로 넘겨서 Collection에서는 전달받은 함수 개체를 호출하여 원하는 회원을 검색하여 반환하게 됩니다.

 

class SearchByNumFun:public Search

{

      int num;

public:

      SearchByNumFun(int _num){num = _num;}

      virtual bool operator()(Member *mem)

      {

                    return mem->GetNum() == num;

      }

};

 

 SearchByNumFun 개체는 생성 시에 찾고자 하는 번호를 입력 인자를 전달하여 생성합니다. 그리고 특정 회원의 번호와 생성 시에 입력 인자로 전달받은 멤버 필드 num과 비교한 값을 전달하게 구현하였습니다. 이처럼 구체화 된 검색 논리에 해당하는 개체를 통해 원하는 회원을 검색할 수 있습니다.

 

void Zone::AddMember()

{

      ... 중략...

      cout<<"추가할 회원 번호를 입력하세요"<<endl;

      int num = MyGlobal::GetNum();

      SearchByNumFun sbn(num);

      if(collection->FindMember(sbn))

     ... 중략...

}

 

 다음은 이미 앞에서 언급된 바가 있는 Collecion 클래스의 FindMember 메서드입니다.

 

Member *Collection::FindMember(Search &search,int index)

{    

      for(int i = 0; i < nsize ; i++)

      {

                    if(search(base[i]))

                    {                         

                                 return base[i];

                    }

      }

      return 0;

}

 이와 같은 논리로 회원 삭제, 번호로 검색, 이름으로 검색, 번호순으로 정렬, 이름순으로 정렬, 전체 보기 및 해제화 부분을 구현해 보시기 바랍니다. Zone 클래스를 정의하는 곳에서는 회원 번호로 검색하기 위한 함수 개체 클래스, 이름으로 검색하기 위한 클래스, 번호순으로 정렬에 사용할 클래스, 이름으로 정렬에 사용할 클래스, 회원 정보를 보여주기 위한 클래스, 회원을 소멸하기 위한 클래스에 대한 구체적 정의 및 구현이 있어야 할 것입니다.


7장 산자 의 Part1

7장 산자 의 Part2

(모든 동영상 강의는 무료입니다.)

반응형