[C++] 템플릿(Template)(2)

C++ 2017. 12. 25. 22:27

1] 템플릿 개념의 확장
1. 클래스 템플릿의 확장

template <class T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x=0, T y=0);
    void ShowPosition() const;
};

template <class T>
class BoundCheckArray
{
private:
    T *arr;
public:
    BoundCheckArray(T); 
};

위 클래스 기반으로 Point<int> 템플릿 클래스의 객체를 저장할 수 있는 객체는 어떻게 생성해야할까? 딱히 어렵지 않아 보인다.

BoundCheckArray<Point<int>> pointArr(50);

Point<Int>형 포인터라면, 다음과 같이 객체를 생성하면 된다.

BoundCheckArray<Point<int>*> pointArrPtr(50);

타입이 너무 길다면, typedef 선언으로 간략화 할 수 있다.

typedef Point<int>* POINT_PTR;

* 특정 템플릿 클래스의 객체를 인자로 받는 일반함수의 정의와 friend 선언
: Point<int>와 같은 템플릿 클래스의 자료형을 대상으로도 일반함수의 정의가 가능하고, 클래스 템플릿 내에서 이러한 함수를 대상으로 friend 선언도 가능하다. 


2] 클래스 템플릿의 특수화
: 클래스 템플릿의 특수화도 함수 템플릿의 특수화랑 매우 비슷하다.

1. 클래스 템플릿 특수화
: 클래스 템플릿을 특수화하는 이유는, 특정 자료형을 기반으로 생성된 객체에 대해, 구분이 되는 다른 행동양식을 적용하기 위해서이다.

template <class T>
class SoSimple
{
public:
    T SimpleFunc(T num)
    {
        ...
    }
};

위 클래스의 int형 특수화는 다음과 같다.

template<>
class SoSimple<int>
{
public:
    int SimpleFunc(int num)
    {
        ...
    }
};

2. 클래스 템플릿의 부분 특수화

template <class T1, class T2>
class MySimple { ... };

다음은 위 클래스 템플릿을 부분 특수화한 코드이다.

template <class T1>
class MySimple<T1, int> { ... }

그렇다. 두개의 템플릿 타입에 대하여 하나의 타입만 특수화를 한것이다. 

template <>
class MySimple<double, int> { ... }

위는 전체 타입에 대해 특수화를 시킨것이다. 만약 두개의 정의가 겹치는 즉 MySimple<double, int> 타입의 객체를 생성하려고 하면 전체 특수화에 대한 클래스 템플릿이 우선시 된다. 


3] 템플릿 인자
: 위에서 사용된 T 또는 T1, T2 같은 문자를 가리켜 "템플릿 매개변수" 라고 한다. 그리고 템플릿 매개변수에 전달되는 자료형 정보를 가리켜 "템플릿 인자" 라고 한다. 

1. 템플릿 매개변수에는 변수의 선언이 올 수 있다. 
: 다음의 클래스 템플릿 정의를 보자. 이 정의에서 독특한 사실은 템플릿 매개변수의 선언에 마치 함수처럼 변수의 선언이 등장했다는 점이다.

template <typename T, int len> 
class SimpleArray
{
private:
    T arr[len];
public:
    T& operator[] (int idx)
    {
        return arr[idx];
    }
};

이렇듯 템플릿 매개변수에도 변수가 올 수 있다. 그리고 이를 기반으로 다음의 형태로 객체생성이 가능하다.

SimpleArray<int, 5> i5arr;
SimpleArray<double, 7> d7arr;

위의 두 문장에서 템플릿 매개변수 len에 전달된 인자 5와 7은 해당 템플릿 클래스에서 상수처럼 사용된다. 즉, len은 각각 5와 7로 치환되어 템플릿 클래스가 각각 생성된다.

class SimpleArray<int, 5>
{
private:
    int arr[5];
public:
    int& operator[] (int idx)
    {
        return arr[idx];
    }
};

class SimpleArray<double, 7>
{
private:
    int arr[7];
public:
    double& operator[] (int idx)
    {
        return arr[idx];
    }
};

물론, 위의 두 템플릿 클래스 SimpleArray<int, 5>와 SimpleArray<double, 7>은 완전히 다른 타입이다. 

위에서 보인 것처럼 배열의 길이를 결정하기 위해서 굳이 위에처럼 하지말고 생성자를 이용해서 하면 더욱 편리할 것이다. 
그런데 굳이 이러한 템플릿 클래스를 만드는 이유는, 길이가 다른 두 배열 객체간의 대입 및 복사에 대한 부분을 신경 쓰지 않아도 된다는 것이다. 서로 다른 타입이기 때문에 길이가 다른 배열에 객체에 대해 대입 및 복사 연산을 할 경우 컴파일 에러가 발생할 것이기 때문이다. 
만약 생성자를 이용해서 배열의 길이를 결정하게 했다면, 길이가 같은 배열에 대해서만 대입을 허용하기 위해서 추가적인 코드의 삽입이 불가피하며, 이러한 추가적인 코드는 대입 및 복사의 과정에서 CPU가 수행해야 할 일을 늘리는 결과로 이어진다. 

2. 템플릿 매개변수는 디폴트 값 지정도 가능하다.
: 템플릿 매개변수에도 디폴트 값의 지정이 가능하다. 

template <class T=int, int len=7> 
class SimpleArray
{
    ....
}

SimpleArray <> arr;


4] 템플릿과 static
1. 함수 템플릿과 static 지역변수

template <class T>
void ShowStaticVlaue(void)
{
    static T num = 0;
    num += 1;
    cout << num << endl;
}

위의 함수 템플릿은 다음과 같은 템플릿 함수를 만들어 내며 각각의 함수가 static 변수를 따로 가지고 있다. 

void ShowStaticVlaue<int>(void)
{
    static int num = 0;
    num += 1;
    cout << num << endl;
}

void ShowStaticVlaue<long>(void)
{
    static long num = 0;
    num += 1;
    cout << num << endl;
}

2. 클래스 템플릿과 static 멤버변수

template <class T>
class SimpleStaticMem
{
private:
    static T mem;
public:
    void  AddMem(int num)
    {
        mem += num;
    }

    void ShowMem()
    {
        cout<<mem<<endl;
    }
};

template <class T>
T SimpleStaticMem<T>::mem = 0; // 템플릿 기반의 static 멤버 초기화 문장이다.

위 클래스 템플릿도 타입 별 static 멤버 변수가 각각 유지된다.

3. 템플릿 static 멤버변수 초기화의 특수화
위에서 다음과 같이 static 멤버변수를 초기화 하였다.

template <class T>
T SimpleStaticMem<T>::mem = 0;

그러면 long 타입만 5로 초기화하고 싶을때는 어떻게 할까? 간단하다.

template <>
long SimpleStaticMem<long>::mem = 5;


'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(1)  (0) 2017.12.25
[C++] 상속과 다형성  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,

[C++] 템플릿(Template)(1)

C++ 2017. 12. 25. 21:31

1] 템플릿에 대한 이해와 함수 템플릿
1. 함수를 대상으로 템플릿 이해하기 

template <typename T>
T Add(T num1, T num2)
{
    return num1 + num2;
}

위 템플릿 함수의 정의는 다음과 같다.
- 기능 : 덧셈
- 대상 자료형 : 결정되어 있지 않음

template <typename T>은 T라는 이름을 이용해서 아래이 함수를 템플릿으로 정의한다는 의미이다.(template <class T> 도 똑같은 의미이다.)

다음은 위 템플릿 함수를 이용해서 여러가지 함수를 만들어 보겠다.

int main(void)
{
    cout<< Add<int>(15, 20) << endl;
    cout<< Add<double>(2.9, 3.7) << endl;
    cout<< Add<int>(3.2, 3.2) << endl;  // 값 손실이 발생되어 6이 출력 됨.
    cout<< Add<double>(3.15, 2.75) << endl;

    return 0;
}

Add<int>(15, 20) 의 의미는 "T를 int로 해서 만들어진 Add 함수를 호출한다."
그래서 위 문장을 실행하면 컴파일러는 함수 하나를 만든다. 그러나 두번째의 Add<int>(3.2, 3.2) 문장은 다시 함수를 만들지 않는다. 기존에 만들어져있던 함수를 사용한다. 
그러나, 컴파일 타임에 함수가 만들어지기 때문에 런타임상 속도 저하를 가져오지 않는다. 

위 main 함수를 다음과 같이 변경해도 상관없다. 

int main(void)
{
    cout<< Add(15, 20) << endl;

    cout<< Add(2.9, 3.7) << endl;
    cout<< Add(3.2, 3.2) << endl; 
    cout<< Add(3.15, 2.75) << endl; 

    return 0;
}

위 코드를 컴파일하면 컴파일러가 자동으로 자료형을 결정해서 함수를 만들어 준다. 

2. 함수 템플릿과 템플릿 함수 
- 함수 템플릿
template <typename T>
T Add(T num1, T num2)
{
    return num1 + num2;
}

- 템플릿 함수
: 함수 템플릿 기반으로 컴파일러가 만들어내는 함수다.
: 컴파일러가 생성해 내는 함수라는 뜻으로 Generated Function으로도 불린다.

int Add<int>(int num1, int num2)
{
    return num1 + num2;
}

템플릿 함수와 일반 함수와 구분이 된다.

위 예제에 다음과 같은 일반함수가 추가되었다고 생각해보자.

int Add(int num1, int num2)
{
    return num1 + num2
}

int main(void)
{
    cout<< Add(5, 7) << endl;  // 일반함수를 호출한다.
    cout<< Add<int>(5, 7) << endl; // 템플릿 함수를 호출한다.

    return 0;
}

3. 둘 이상의 Type에 대해 템플릿 선언하기 

template <class T1, class T2>
void ShowData(double num)
{
    cout << (T1)num << " , " << (T2)num << endl; // 형 변환을 "T1(num)" 이런 형태로도 가능.
}

int main(void)
{
    ShowData<char, int>(65); // A, 65 출력
    ShowDate<short, double>(69.2) // 69, 69.2 출력

    return 0;
}

4. 함수 템플릿의 특수화(Specialization)

template <class T>
T max(T a, T b)
{
    return a > b ? a : b;
}

int main(void)
{
    cout<< Max(11, 15) << endl;  // 15 출력
    cout << Max('T', 'Q') << endl; // T 출력
    cout << Max("Simple", "Best") << endl; // 뭐가 출력될지 모름, 단순 주소값을 비교하기 때문
    
    return 0;
}

이런 경우 문자열의 경우 의도하지 않은 결과가 나타난다. 문자열 길이 비교가 목적이라면 다음과 같이 정의 해야 한다. 

const char* Max(const char* a, const char* b)
{
    return strlen(a) > strlen(b) ? a : b;
}

이렇듯 상황에 따라서 템플릿 함수의 구성방법에 예외를 둘 필요가 있는데, 이 때 사용되는 것이 함수 템플릿의 특수화 이다.

char* 타입과 const char* 타입에 대해서 특수화 해보겠다.

template <class T>
T max(T a, T b)
{
    return a > b ? a : b;
}

template <>
char* Max<char *>(char* a, char* b) // <char *> 생략 가능
{
    return strlen(a) > strlen(b) ? a : b;
}

template <>
const char* Max<const char*>(const char* a, const char* b) // <const char*> 생략 가능
{
    return strcmp(a, b) > 0 ? a : b;
}

"char*, const char*형 함수는 내가 이렇게 제시를 하니 필요한 경우에는 별도로 만들지 말고 이것을 써라" 라는 의미다. 


2] 클래스 템플릿
1. 클래스 템플릿 정의
template <class T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x =0, T y=0) : xpos(x), ypos(y) {  }
    
    void ShowPosition() const
    {
        cout<<xpos<<", ">>ypos<<endl;
    }
};

int main(void)
{
    Point<int> pos1(3, 4);
    Point<double> pos2(2.4, 3.6);
    
    return 0;
}

위 예제를 보면 템플릿 함수와 매우 비슷해서 쉽게 이해가 갈것이다. 
다만, 템플릿 함수와 달리 템플릿 클래스의 객체를 생성할 때 <int>, <double>과 같은 자료형 정보를 생략하는것은 불가능하다. 

2. 클래스 템플릿을 파일 분할할 때의 주의점
: 컴파일은 파일단위로 이뤄진다는 사실을 이미 알고 있을 것이다. 그렇다면 밑에 main 함수를 보자.

<main.cpp 파일>
int main(void)

{
    Point<int> pos1<3, 4>;

    return 0;
}

위 파일을 컴파일 하기 위해서는 Point 클래스의 정의 및 구현부분까지 알아야 한다. int 자료형에 맞게 템플릿 클래스를 생성해 내야되기 때문이다. 
만약 Point 클래스의 선언과 구현이 PointTemplate.h PointTemplate.cpp에 나눠져 있다면 두 파일 모두 include 해야 컴파일이 성공한다. 
만약 PointTemplate.cpp 파일을 include하기 싫으면 PointTemplate.h에 Point의 생성자와 멤버함수의 정의를 모두 넣어야 한다. 



'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(2)  (0) 2017.12.25
[C++] 상속과 다형성  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,

[C++] 상속과 다형성

C++ 2017. 12. 25. 20:10

1] 객체 포인터의 참조관계
1. 객체 포인터 변수 
: 객체의 주소 값을 저장하는 포인터 변수다.

Person *ptr; 
ptr = new Person();

위의 두 문장이 실행되면, 포인터 ptr은 Person 객체를 가리키게 된다. 
그러나 Person형 포인터는 Person 객체 뿐만 아니라 Person을 상속받은 Person의 자식클래스의 객체도 가리킬 수 있다. 
 
Base 클래스가 부모 클래스이고, Derived 클래스가 자식 클래스이다. 그리고 두 클래스 모두 aaa() 라는 메서드가 정의되어 있다면, 다음과 같은 행동도 가능하다.

Derived derived;

derived.Base::aaa(); 

즉, 자신의 aaa() 메서드를 호출하는 것이 아닌, 부모 클래스의 aaa() 메서드를 호출하는 것이다. 
단, 저렇게 사용할 일이 거의 없으니 문법적인 요소로만 알아두길 바란다.

2. 상속 클래스들의 메서드 호출 범위

class First
{
public:
    void FirstFunc()
    {
        cout<<"First Func"<<endl;
    }
};

class Second : public First
{
public:
    void SecondFunc()
    {
        cout<<"Second Func"<<endl;
    }
};

class Third : public Second
{
public:
    void ThirdFunc()
    {
        cout<<"Third Func"<<endl;
    }
};

int main(void)
{
    Third* third = new Third();
    Second* second = third;
    First* first = second;
    
    third->FirstFunc();     // 0
    third->SecondFunc(); // 0
    third->ThirdFunc();    // 0
    
    second->FirstFunc();  // 0
    second-> SecondFunc(); // 0
    second-> ThirdFunc() // X

    first->FristFunc(); //0
    first-> SecondFunc(); // X
    first->ThirdFunc(); // X

    return 0;
}

위의 코드에서 보는것과 같이, 실제 instance의 자료형이 무엇이던지 상관 없이 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다. 


2] 가상 함수(Virtual Function)
1. 가상 함수의 필요성
: 만약 위에 예제에서 First 클래스의 멤버 함수를 Second, Third 클래스에서 오버라이딩 한다면 당연히 포인터 형에 해당하는 메서드가 호출될 것이다. 
하지만, 함수를 오버라이딩 했다는 것은 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미인데, 포인터 변수의 자료형에 따라서 호출되는 함수의 종류가 달라지는 것은 문제가 있어 보일수도 있다.

그래서 가상함수가 필요한 것이다. 

class First
{
public:
    virtual void MyFunc()
    {
        cout<<"First Func"<<endl;
    }
};

class Second : public First
{
public:
    virtual void MyFunc()
    {
        cout<<"Second Func"<<endl;
    }
};

class Third : public Second
{
public:
    virtual void MyFunc()
    {
        cout<<"Third Func"<<endl;
    }
};

위와 같이 선언해 놓으면 포인터 변수의 자료형에 상관없이, 실제 instance의 자료형에 따라 메서드가 호출된다. 

*그리고 virtual 함수를 오버라이딩 하면 그 함수도 virtual 함수가 되어야만 한다. 

int main(void)
{
    First* first = new Third();
   
    first->MyFunc();
}

-> 결과값
: "Third Func"

2. 순수 가상함수(Pure Virtual Function)와 추상 클래스(Abstract Class)
: 추상 클래스는 다들 알다시피, 객체 생성을 목적으로 하는 클래스가 아니다. 순수 가상함수를 구현함으로써, 자식 클래스 들에게 구현의 강요성을 부여하는 것이다. 

class Employee 
{
private:
    char name[100];
public:
    Employee(char* name)
    {
        .....
    }

    void ShowYourName() const
    {
        ....
    }

    virtual int GetPay() const = 0;
    virtual void ShowSalaryInfo() const = 0;
};

위의 코드에서 가상 함수에 0을 대입한 것은 순수 가상 함수라는것을 표현하기 위함이다. 즉, 순수 가상함수는 함수의 몸체가 정의되지 않은 함수인 것이다.


3] 가상 소멸자와 참조자의 참조 가능성
: 가상함수 말고도 virtual 키워드를 붙여줘야 할 대상이 하나 더 있다. 그것은 바로 소멸자이다. 

1. 가상 소멸자(Virtual Destructor)

class First
{
private:
    char* strOne;
public:
    First(char* str)
    {
        strOne = new char[strlen(str) + 1];
    }
   
    ~First()
    {
        delete []strOne;
    }
};

class Second : public First
{
private:
    char* strTwo;
public:
    Second(char* str1, char* str2) : First(str1)
    {
        strTwo = new char[strlen(str2)+1];
    }

    ~Second()
    {
        delete []strTwo;
    }
};

int main(void)
{
    First* ptr = new Second("simple", "complex");

    delete ptr;

    return 0;
}

위 코드에서 ptr 포인터 변수는 First 포인터 형이기 때문에 Second 형 인스턴스를 참조하고 있더라도 delete ptr; 이 호출되면 First 객체의 소멸자만 호출된다. 따라서 메모리 leak이 발생된다. 

이러한 상황을 방지해주기 위해서 virtual 키워드를 소멸자에도 선언해줘야 한다. 

2. 참조자의 참조 가능성
: 참조자(reference)도 포인터 변수의 특성을 그대로 가져간다.
First형 참조자는 Second형, Third형 타입을 참조할 수 있고, 함수 호출 및 가상 함수에 적용되는 규칙도 모두 그대로 적용된다. 


'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(2)  (0) 2017.12.25
[C++] 템플릿(Template)(1)  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,