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
:

int main(void)

{

Point pos1(5, 5);        //복사생성자의 대표적인 상황.

Point pos2 = pos1;

}


※ 중요한 사실은 새로 생성하는 객체 pos2의 초기회에 기존에 생성된 객체 pos1이 사용되었다는 점이다.


int main(void)

{

Point pos1(5, 5);        //대입 연산자의 대표적인 상황.

Point pos2(3, 3);


pos2 = pos1;

}


※ 중요한 사실은 pos2도, 그리고 pos1도 이미 생성 및 초기화가 진행된 객체라는 사실이다.





디폴트 대입 연산자의 문제..



called destructor! 이것이 딱 한번만 출력되었다는 것을 보아 소멸자의 호출과정에서 문제가 발생했음을 알 수 있다.


37행: man2가 가리키고 있던 주소 값을 읽게 되므로, 더 이상 문자열에 접근이 불가능하다.

 때문에 소멸도 불가능한 상태가 되어 메모리의 누수로 이어지게 된다.



# 정리하면, 생성자 내에서 동적 할당을 하는 경우, 디폴트 대입 연산자는 두 가지 문제를 일으키므로 다음의 형태로 직접 대입 연산자를 정의해야 한다.


1. 깊은 복사를 진행하도록 정의한다. 

2. 메모리 누수가 발생하지 않도록, 깊은 복사에 앞서 메모리 해제의 과정을 거친다.




Posted by scii
:

대입 연산자의 오버로딩은 클래스 정의에 있어서 생성자, 복사 생성자와 함께 빠질 수 없는 요소이다.


#include <iostream>

using namespace std;


class First

{

private:

int num1, num2;

public:

First(int n1=0, int n2=0)

:num1(n1), num2(n2)

{ }


void ShowData()

{

cout<<num1<<", "<<num2<<endl;

}

};


class Second

{

private:

int num3, num4;

public:

Second(int n3=0, int n4=0)

:num3(n3), num4(n4)

{ }


void ShowData()

{

cout<<num3<<", "<<num4<<endl;

}


Second& operator=(const Second& ref)        //반환형이 참조형임을 주목!!

{

cout<<"Second& operator=()"<<endl;

num3 = ref.num3;

num4 = ref.num4;

return *this;

}

};


int main(void)

{

First fsrc(111, 222);

First fcpy;


Second ssrc(333, 444);

Second scpy;


fcpy = fsrc; //fcpy.operator=(fsrc);

scpy = ssrc;


fcpy.ShowData();

scpy.ShowData();


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

First fob1, fob2;

Second sob1, sob2;


fob1 = fob2 = fsrc;

sob1 = sob2 = ssrc;        //반환형이 참조형이므로 이런식의 연산이 가능하다.


fob1.ShowData();

fob2.ShowData();

sob1.ShowData();

sob2.ShowData();


return 0;

}



First 클래스에 자동으로 삽입된 디폴트 대입 연산자는 다음과 같다.


First& operator=(const First& ref)

{

num1=ref.num1;

num2=ref.num2;

return *this;

}


※ 멤버 대 멤버의 복사가 이뤄지는 것을 보면서, C언어의 구조체 변수간 대입연산의 결과와 비슷하다고 생각하기 쉽다. 

그러나 앞서 보였듯이, 객체간의 대입연산은 C언어의 구조체 변수간의 대입연산과 본질적으로 다르다.

이는 단순한 대입연산이 아닌, 대입 연산자를 오버로딩 한 함수의 호출이기 때문이다.

Posted by scii
: