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


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

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


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




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
:



※ 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성된다.


즉, 34~35행은 AAA mem = ref; 로 해석이 된다.

그러므로 복사 생성자만 호출이 된다.


하지만 45~48행은 생성자와 대입연산자 이 두개가 호출이 된다. 

즉, AAA mem;

     mem = ref; 이렇게 해석이되므로 인해.. 생성자 호출이 되어지고 그다음 대입연산자가 호출이 된다. 


※ 생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도의 문장에서 진행하는 형태로 바이너리 코드가 생성된다.



위의 예제에서 보이듯이 이니셜라이저를 이용해서 초기화를 진행하면, 함수의 호출횟수를 줄일 수 있어서, 그리고 초기화의 과정을 단순화시킬 수 있어서 약간의 성능향상을 기대할 수 있다.


Posted by scii
:

※ 유도 클래스의 생성자에는 아무런 명시를 하지 않아도 기초 클래스의 생성자가 호출되지만, 

유도 클래스의 대입 연산자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다.



# 36행 ~ 43행: 이 대입 연산자를 정의한 것을 지워버리면 디폴트 대입 연산자가 자동으로 생성된다. 그리고는 기초클래스의 대입연산자가를 자동적으로 호출한다.

하지만, 대입 연산자를 정의할 생각이 있다면 이렇듯 기초클래스의 대입연산자를 명시적으로 호출해야 한다.


# 13행: C++ 에서는 A형 참조자는 A 객체 또는 A를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있기 때문에 이렇게 가능하다.


#include <iostream>

#include <cstring>

using namespace std;


class Book

{

private:

char* title;

char* isbn;

int price;


public:

Book(char* title, char* isbn, int value)

:price(value)

{

this->title = new char[strlen(title)+1];

this->isbn = new char[strlen(isbn)+1];

strcpy(this->title, title);

strcpy(this->isbn, isbn);

}


explicit Book(const Book& ref) //복사 생성자

:price(ref.price)

{

title = new char[strlen(ref.title)+1];

isbn = new char[strlen(ref.isbn)+1];

strcpy(title, ref.title);

strcpy(isbn, ref.isbn);

}


Book& operator=(const Book& ref) //대입 연산자

{

delete []title;

delete []isbn;


price = ref.price;

title = new char[strlen(ref.title)+1];

isbn = new char[strlen(ref.isbn)+1];

strcpy(title, ref.title);

strcpy(isbn, ref.isbn);

return *this;

}


void ShowBookInfo() const

{

cout<<"제목: "<<title<<endl;

cout<<"ISBN: "<<isbn<<endl;

cout<<"가격: "<<price<<endl;

}


~Book()

{

delete []title;

delete []isbn;

}

};


class EBook :public Book

{

private:

char* DRMKey;


public:

EBook(char* title, char* isbn, int value, char* key)

:Book(title, isbn, value)

{

DRMKey = new char[strlen(key)+1];

strcpy(DRMKey, key);

}


EBook(const EBook& ref) //복사 생성자

:Book(ref) //기초클래스의 복사 생성자 호출

{

DRMKey = new char[strlen(ref.DRMKey)+1];

strcpy(DRMKey, ref.DRMKey);

}


EBook& operator=(const EBook& ref) //대입 연산자

{

delete []DRMKey;


Book::operator=(ref); //기초클래스의 대입 연산자 호출.

DRMKey = new char[strlen(ref.DRMKey)+1];

strcpy(DRMKey, ref.DRMKey);

return *this;

}


void ShowEBookInfo()

{

ShowBookInfo();

cout<<"인증키: "<<DRMKey<<endl;

}


~EBook()

{

delete []DRMKey;

}

};


int main(void)

{

Book book("zzzz", "adf2342343", 23434);

book.ShowBookInfo();

cout<<endl;


EBook ebook("asdf", "s234234", 23423, "1234ee");

ebook.ShowEBookInfo();

cout<<endl<<endl;


///////////////////////////////////////////////////////////////////


Book book1(book); //복사생성자 호출

book1.ShowBookInfo();

cout<<endl;


Book book2("thg", "234", 234221);

book1 = book2; //대입 연산자 호출

book1.ShowBookInfo();

cout<<endl;

///////////////////////////////////////////////////////////////////


EBook ebook1("asdf", "wer", 123, "qwe");

ebook1 = ebook; //대입연산자 호출 ebook1.operator=(ebook);

ebook1.ShowEBookInfo();

cout<<endl;


EBook ebook2(ebook1); //복사생성자호출 ebook2 = ebook1;

ebook2.ShowEBookInfo();


return 0;

}


Posted by scii
: