함수 호출에 사용되는, 인자의 전달에 사용되는 () 도 연산자이다. 따라서 이 연산자 역시 오버로딩이 가능한 연산자이다.


ex) adder(4, 5); 가 있다면 adder.operator()(4, 5); 이렇게 해석이 된다.




위 예제에서 정의한 Adder 클래스와 같이 함수처럼 동작하는 클래스를 가리켜 '펑터(Functor)' 라 한다.

그리고 '함수 오브젝트(Function Object)' 라고도 불린다.





펑터의 위력 & 활용


- 펑터는 함수 또는 객체의 동작방식에 유연함을 제공할 때 주로 사용된다.



매개변수 형이 SortRule 의 참조형이므로, SortRule 클래스를 상속하는 AscendingSort 클래스와 DescendingSort 클래스의 객체는 인자로 전달 가능하다. 그리고 SortRule 의 operator() 함수는 virtual 로 선언되었으니, 유도 클래스의 operator() 함수가 대신 호출된다. 

때문에 펑터로 무엇이 전달되느냐에 따라서 정렬의 기준이 바뀌게 된다. 그리고 이것이 펑터를 정의하는 이유이다.

Posted by scii
:

스마트 포인터

Programming/C++ 2012. 10. 4. 13:16 |

스마트 포인터란 말 그대로 똑똑한 포인터이다. 스마트 포인터는 자기 스스로 하는 일이 존재하는 포인터이다.

스마트 포인터는 객체이다 포인터의 역할을 하는 객체를 뜻하는 것이다.


※ 프로그램의 개발을 목적으로 라이브러리에서 제공하는 스마트 포인터를 사용할 때 큰 도움이 될 것이다.


#include <iostream>

using namespace std;


class Point 

{

    private:

        int xpos, ypos;


    public:

        Point(int x=0, int y=0)

            :xpos(x), ypos(y)

        {

            cout<<"Point object created!!"<<endl;

        }


        ~Point()

        {

            cout<<"Point object cease to exist!!"<<endl;

        }

        

        void SetPos(int x, int y)

        {

            xpos = x;

            ypos = y;

        }


        friend ostream& operator<<(ostream& os, const Point& pos);

};


ostream& operator<<(ostream& os, const Point& pos)

{

    os<<pos.xpos<<", "<<pos.ypos<<endl;

    return os;

}


class SmartPtr        //기본적으로 여기 정의된 형태의 일을 포함해서 정의하게 되어있다.

{

    private:

        Point * posPtr;


    public:

        SmartPtr(Point * ptr)

            :posPtr(ptr)

        {   }


        Point& operator*() const

        {

            return *posPtr;

        }


        Point* operator->() const

        {

            return posPtr;

        }


        ~SmartPtr()

        {

            delete posPtr;

        }

};


int main(void)

{

    SmartPtr sptr1(new Point(1, 2));

    SmartPtr sptr2(new Point(2, 3));

    SmartPtr sptr3(new Point(3, 4));

    

    cout<<*sptr1;

    cout<<*sptr2;

    cout<<*sptr3;


    sptr1->SetPos(10, 20);

    sptr2->SetPos(20, 30);

    sptr3->SetPos(30, 40);


    cout<<*sptr1;

    cout<<*sptr2;

    cout<<*sptr3;


    return 0;

}


위의 예제에서 가장 중요한 사실은, Point 객체의 소멸을 위한 delete 연산이 자동으로 이뤄졌다는 사실이다.

그리고 바로 이것이 스마트 포인터의 똑똑함이다.

Posted by scii
:

포인터를 기반으로 하는 연산자 모두를 포인터 연산자라 한다. 그런데 그 중에서도 대표적인 포인터 연산자 둘은 다음과 같다.


->  포인터가 가리키는 객체의 멤버에 접근.

*  포인터가 가리키는 객체에 접근.


※ 둘 다 피 연산자가 하나인 단항 연산자의 형태로 오버로딩 된다는 특징만 기억하면 된다.




19행: 이 함수는 객체자신의 주소 값을 반환하도록 -> 연산자를 오버로딩 하고 있다. 

-> 연산자를 다른 형태로 오버로딩 하는 것도 가능하지만, 이 연산자의 오버로딩을 허용하는 이유는,

주소 값의 반환이 목적이기 때문에 다른 형태로는 오버로딩 하지 않는 것이 좋다.


38행: 19행의 멤버함수가 반환하는 것은 주소 값이니, ShowData 함수의 호출은 문법적으로 성립하지 않는다.  

ex) num.operator->() ShowData();

때문에 반환되는 주소 값을 대상으로 적절한 연산이 가능하도록 -> 연산자를 하나 더 추가하여 진행해야 한다.


operator-> 함수가 반환하는 것이 주소 값이니, 이를 통한 멤버의 접근을 위해서 -> 연산자를 하나 더 추가시켜서 해석한 것이다.

Posted by scii
:

new 연산자 오버로딩


new, delete 연산자의 오버로딩은 다른 연산자 오버로딩과 많이 다르다!!

이 둘은 분명 연산자이다. 따라서 연산자 오버로딩이 가능하며, 이 둘을 이용해서 클래스 별로 독특한 객체의 생성과정을 정의할 수도 있다. 그런데 이 둘에 대한 연산자 오버로딩은 이전에 보았던 연산자 오버로딩과는 다소 차이가 있다.


기본적으로 제공되는 new 연산자가 하는 일


1. 메모리 공간의 할당

2. 생성자의 호출

3. 할당하고자 하는 자료형에 맞게 반환된 주소 값의 형 변환


new 연산자가 진행하는 세 가지 작업 중에서 1번에 해당하는 메모리 공간의 할당만 오버로딩 할 수 있다,


다음과 같이 오버로딩 하도록 이미 약속이 되어있다

void* operator new(size_t size) {    }

반환형은 반드시 void 포인터 형이어야 하고, 매개변수는형은 size_t이어야 한다.


※ 일반적으로 size_t 는 다음과 같이 정의되어 있다.

typedef unsigned int size_t;

그래서 0 이상의 값을 표현할 목적으로 정의된 자료형이다.


※ 크기정보는 바이트 단위로 계산되어 전달된다.




delete 연산자 오버로


delete ptr; 이것의 문장으로 객체의 소멸을 명령하면,

컴파일러는 먼저 ptr이 가리키는 객체의 소멸자를 호출한다. 그리고는 다음의 형태로 정의된 함수에 ptr에 저장된 주소 값을 전달한다.

void operator delete(void* adr){    }


따라서 delete 함수는 다음의 형태로 정의해야 한다. 즉, 소멸자는 오버로딩 된 함수가 호출되기 전에 호출이 되니 오버로딩 된 함수에서는 메모리 공간의 소멸을 책임져야 한다. 


void operator delete(void* ptr)

{

delete []ptr;

}


참고로 사용하는 컴파일러에서 void 포인터 형 대상의 delete 연산을 허용하지 않는다면, 위의 delete문을 다음과 같이 작성하면 된다.

delete []((char*)adr);



19행; 컴파일러에 의해, 필요한 메모리 공간의 크기가 바이트 단위로 계산되어서 인자로 전달되니, 크기가 1바이트인 char 단위로 메모리 공간을 할당해서 반환한다. 


Point* ptr = new Point(3, 4);


아직 객체생성이 완성된 상태가 아닌데, 어떻게 멤버함수의 호출이 가능했을까?

그것은 operator new 함수와 operator delete 함수가 static 으로 선언된 함수이기 때문이다.

비록 멤버함수의 형태로 선언을 해도 이 둘은 static 함수로 간주가 된다. 그래서 객체생성의 과정에서 호출이 가능했던 것이다.






operator new & operator new[]


new 연산자는 다음 두 가지의 형태로 오버로딩이 가능하다.


void* operator new(size_t size){    }

void* operator new[](size_t  size){    }


두번째 함수는 new 연산자를 이용한 배열할당 시 호출되는 함수이다.

Point* arr  = new Point[3]; 이런 문장을 만나면,

컴파일러는 세 개의 Point객체에 필요한 메모리 공간을 바이트 단위로 계산해서, 이를 인자로 전달하면서 new[] 함수를 호출한다.

즉, 열할당 시 호출되는 함수라는 사실을 제외하고는 operator new 함수와 차이가 없다.


마찬가지로 delete 연산자도 다음 두 가지의 형태로 오버로딩이 가능하다.


void operator delete(void* adr){    }

void operator delete[](void* adr){    }


두 번째 함수는 delete 연산자를 이용한 배열소멸 시 호출되는 함수이다.

delete []arr;


58행:  객체로 구성된 배열이기 때문에 모든 객체의 소멸자가 호출된 다음에 36행에 정의된 멤버함수(static 함수)가 호출된다.



Posted by scii
:



Posted by scii
:

객체를 저장할 수 있는 배열 클래스를 정의할 때, 두 가지 형태로 각각 정의할 수 있다.


1. 객체를 저장하는 배열 기반의 클래스.


2. 객체의 주소 값을 저장하는 배열 기반의 클래스.


즉, 저장의 대상이 객체이냐, 아니면 객체의 주소 값이냐에 차이가 있는 것이다.



# Point 객체를 저장하는 배열 기반의 클래스



36행: Point 객체로 이뤄진 배열을 생성하고 있다. 인자를 받지 않는 void 생성자의 호출을 통해서 배열요소를 이루는 객체가 생성되므로, 위에 정의된 생성자에 설정된 디폴트 값에 의해 객체의 모든 멤버가 0으로 초기화된다.


67~70행: 임시객체를 생성해서 배열요소를 초기화하고 있다. 초기화의 과정에서 디폴트 대입 연산자가 호출되어 멤버 대 멤버의 복사가 진행된다. 저장의 대상이 객체라면, 여기서 보이는 것과 같이 대입 연산자를 통해서 객체에 저장된 값을 복사해야 한다.






# Point 객체의 주소 값을 저장하는 배열 기반의 클래스

(많이 사용되는 방법)




24행: Point 포인터 형을 의미하는 POINT_PTR을 정의하였다. 

저장의 대상, 또는 연산의 주 대상이 포인터인 경우, 이렇듯 별도의 자료형을 정의하는 것이 좋다.


39행: 저장의 대상이 point 객체의 주소 값이기 때문에 POINT_PTR 배열을 생성하였다.


77행: Point 객체의 주소 값을 저장하고 있다. 이렇듯 객체의 주소 값을 저장할 경우, 깊은 복사냐 얕은 복사냐 하는 문제를 신경 쓰지 않아도 된다.


※ 위의 예제와 같이 주소 값을 저장하는 경우, 

객체의 생성과 소멸을 위한 new, delete 연산 때문에 더 신경 쓸 것이 많아 보이지만, 

깊은 복사냐 얕은 복사냐 하는 문제를 신경 쓰지 않아도 되기 때문에 이 방법이 더 많이 사용된다.

Posted by scii
:




45행: 함수 내에서 배열에 저장된 데이터를 변경하지 못하도록 매개변수 형이 const로 선언되었다. 그리고 이는 매우 좋은 선언이라 할 수 있다. 그런데 문자는 이 선언으로 인해 49행이 원인이 되어 컴파일 에러가 발생한다는 것이다.

왜냐면, 인덱스 연산은 다음과 같이 해석이 되며, 

ref.operator[](idx); 이 때 호출되는 operator[]함수는 const 함수가 아니기 때문이다.


=> 문제 해결: operator[]함수에 const를 선언하면 문제를 해결할 수 있지만, 이렇게 되면 저장 자체가 불가능해진다. 또한 데이터를 저장하는 배열의 특성상 적절한 해결책이 아님을 알 수 있다.

따라서 const함수의 오버로딩을 통해 해결을 해야 한다. 


const 의 선언유무도 함수 오버로딩의 조건에 해당합니다.


위의 예제를 하나의 모델로 해서 const 기반의 함수 오버로딩이 유용하게 사용될 수 있음을 기억하자!

Posted by scii
:

※ C, C++의 기본 배열은 다음의 단점을 지니고 있다.


- 경계검사를 하지 않는다.


물론 이러한 특성이 유용하게 활용될 수도 있지만, 지금은 이러한 특성의 부정적 측면만을 고려해서 이야기를 전개한다.


이러한 담점의 해결을 위해서 "배열 클래스"라는 것을 디자인해야 한다. (배열의 역할을 하는 클래스)



17행: 반환형이 참조형이다. 때문에 배열요소의 참조값이 반환되고, 이 값을 이용해서 배열요소에 저장 된 값의 참조뿐만 아니라 변경도 가능하다.


위의 실행결과를 통해서 잘못된 배열접근이 있었음이 확인되었다. 때문에 위 유형의 클래스 정의를 통해서 배열접근의 안전성을 보장받을 수 있다.


※ []연산자는 연산의 기본 특성상 멤버함수 기반으로만 오버로딩 하도록 제한되어 있다.






다음과 같이 복사 생성자와 대입 연산자를 private으로 선언해서, 복사 또는 대입을 원천적으로 막을 수 있다.


이같이 하는 이유는

배열은 저장소의 일종이고, 저장소에 저장된 데이터는 유일성이 보장되어야 하기 때문에, 대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주된다. 따라서 깊은 복사가 진행되도록 클래스를 정의할 것이 아니라, 위의 코드에서 보이듯 빈 상태로 정의된 복사 생성자와 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 원천적으로 막는 것이 좋은 선택이 되기도 한다.



### 나의 모든 C++ 복습은 "열혈강의 C++" 에서 발췌하여 옮김.

Posted by scii
: