깊은 복사/ 얕은 복사를 다시 한번 정리하자면.
-> 일단 얕은 복사는 말그대로 멤버대 멤버를 고대로 복사한다. 이때, 복사한다는 건 말 그대로 같은 메모리의 주소를 가리키도록 복사한다는 것이다. 따라서 복사한 本데이터를 수정하면 복사받은 데이터도 변경되는 기상천외한 일이 생긴다. 각종 메모리 누수나 런타임에러를 일으키는 주범이 된다.
-> 깊은 복사는 복사 시 해당 객체와 인스턴스 변수까지 전부 복사해서 새로운 주소에 담는 방식이다.
새 주소에 담기 때문에 本데이터가 바뀌어도 변경되지 않는다.
class SimpleClass {
private:
int num1;
char * name;
public:
SimpleClass(const char *myname, int n2) {
int len = strlen(myname) + 1;
name = new char(len);
strcpy(name, myname);
num1 = n2;
}
void SimpleFunc() {
cout << "simple func" << endl;
}
void SimpleFunc() const {
cout << "const simple func" << endl;
}
int GetNum1() const {
return num1;
}
void showData() const {
cout << name << ' ' << num1 << endl;
}
SimpleClass& adder(int n) {
num1++;
return *this;
}
~SimpleClass() {
cout << "delete simpleclass" << endl;
delete []name;
}
};
위와 같은 클래스가 있을때
int main(void){
SimpleClass man1("myname", 29);
SimpleClass man2("asd", 10);
man2 = man1;
}
man2 =man1
으로 대입해버리면 man2
와 man1
에 디폴트 대입연산자가 작동하여 얕은복사가 되어 완전히 같은 메모리를 가리키고 공유하게 된다. 이렇게 되면 현재 생성되어있는 "asd"
라는 문자열은 낙동강 오리알이 되어 메모리 누수가 되고, man2
, man1
중 하나만 사라져도 둘 다 못쓰게 되버린다.Person& operator=(const Person& ref){
delete []name; //메모리 누수를 막기 위한 메모리 해제
int len = strlen(ref.name)+1;
name = new char[len];
strcpy(name, ref.name);
age=ref.age;
return *this;
}
유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연산자 호출문을 삽입하지 않으면, 기초 클래스의 대입 연산자는 호출되지 않아서, 기초 클래스의 멤버변수는 멤버 대 멤버의 복사 대상에서 제외된다!
Second& operator=(const Second& ref){
First::operator=(ref);
num3=ref.num3;
num4=ref.num4;
return this*;
}
BBB(const AAA& ref) : mem(ref); // 이건 AAA mem = ref로 번역되어 선언과 동시에 초기화가 이루어짐
CCC(const AAA& ref) : {mem=ref;); // AAA& ref 라는 선언과 mem = ref라는 선언이 각각 한번씩 진행됨
class BoundCheckIntArray {
private:
int * arr;
int arrlen;
public:
BoundCheckIntArray(int len) : arrlen(len) {
arr = new int[len];
}
int& operator[] (int idx) {
if (idx < 0 || idx >= arrlen) {
cout << "Arrau index out of bound exception" >> endl;
exit(1);
}
return arr[idx];
}
~BoundCheckIntArray() {
delete[] arr;
}
};
int main(void)
{
BoundCheckIntArray arr(5);
for (int i = 0; i < 5; i++) {
arr[i] = (i + 1) * 11;
}
for (int i = 0; i < 6; i++) {
cout << arr[i] << endl;
}
return 0;
}
위에서 보는 바와 같이 arr[5]에 접근하게 되면 에러 메시지를 출력하고 exit(1)
으로 에러를 뱉어낸다.
배열은 저장소의 일종이고, 저장소에 저장된 데이터는 '유일성'이 보장되어야 하기 때문에, 대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주된다. 따라서 깊은 복사가 진행되도록 클래스를 정의할 것이 아니라, 위의 코드에서 보이듯이 빈 상태로 정의된 복사 생성자와 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 원천적으로 막는 것이 좋은 선택이 되기도 한다.
BoundCheckIntArray
을 외부 함수가 const 참조형태의 매개변수로 부르는 것을 생각해보자void ShowAllData(const BoundCheckIntArray& ref)
{
int len = ref.GetArrLen();
for(int idx=0; i<5; i++{
cout << ref[idx] << endl;//컴파일 에러 발생!!
}
}
왜 컴파일 에러가 발생할까?
일단 매개변수 형이 const
인것은 매우 합당하다. ShowAllData()
함수는 어디까지나 데이터를 보여주는 함수이므로 내부에서 데이터의 값이 변경되면 안되고 변경되지 않도록 const
를 선언하는것은 당연하다. 하지만, 이때 const
때문에 ref[idx]
가 컴파일 에러가 난다.
왜냐하면 우리가 오버로딩한 operator[]
함수는 const
함수가 아니기 때문이다...
따라서, 이를 해결하기 위해선
const
의 선언유무도 함수 오버로딩의 조건에 해당된다.
이 조건을 이용하면 된다!
int& operator[] (int idx) {//원래 것
if (idx < 0 || idx >= arrlen) {
cout << "Arrau index out of bound exception" >> endl;
exit(1);
}
return arr[idx];
}
int operator[] const (int idx) {// 반환형이 int다! 새로운 객체를 생성해서 넘긴다. 어처피 이걸 쓸 함수는 값만 필요하지 참조값은 필요없으니 참조를 통해
//괜히 문제생길일을 원천차단한다.
if (idx < 0 || idx >= arrlen) {
cout << "Arrau index out of bound exception" >> endl;
exit(1);
}
return arr[idx];
}
위와 같이 const
함수 오버로딩을 사용하여 문제를 해결할수있다! 대박!!
new 연산자가 하는 일
이 중 new 연산자 오버로딩을 통해 할 수 있는건 첫번째에 해당하는 메모리 공간 할당만 오버로딩 할 수 있다.
new 오버로딩은 아래와 같이 하도록 이미 약속 되어있다.
void * operator new (size_t size) { ... }
(이때 사이즈는 바이트 단위이다, 즉 char
와 같은 크기다.
반환형은 반드시 void
포인터 형이고 매개변수형은 size_t
이어야 한다.
이때, Point * ptr = new Point(3,4)
를 작성하면 먼저 필요한 메모리 공간을 계산하고 그 후 operator new
를 호출하여 계산된 크기의 값을 인자로 전달한다!
new Point(3,4)
에서 보이듯 아직 객체 생성이 다 되지 않았는데 new를 쓸 수 있다.
그 이유는 **기본적으로 operator new
와 operator delete
는 둘다 static
으로 선언된 함수이기 때문이다!!
new 연산자 오버로딩과 큰 맥락은 같다.
void operator delete (void * adr) { ... }
new 연산자는 2가지 아래의 방식으로 오버로딩 가능하다
void * operator new (size_t size) { ... }
void * operator new[] (size_t size) { ... }
delete 연산자는 2가지 아래의 방식으로 오버로딩 가능하다
void operator delete (void * adr) { ... }
void operator delete[] (void * adr) { ... }
->
: 포인터가 가리키는 객체의 멤버에 접근
*
: 포인터가 가리키는 객체에 접근
class Number{
private:
int num;
public:
...
Number * operator->(){
return this;
}
Number& operator*(){
return &this;
}
}
int main(void){
...
Number num(20);
num.ShowData();
(*num)=30;
num->ShowData();
(*num).ShowData();
return 0;
}
나머지는 잘 이해가 가지만 num->ShowData()
는 무언가 문제가 있어보인다. 저 부분을 일반적인 해석을 하면
num.operator() ShowData();
인데 여기서 operator->()
가 반환하는 것은 주소값이니 (주소값) ShowData()
이라는 호출은 논리적으로 말이 되지않는다. 따라서 이때 반환되는 주소 값을 대상으로 적절한 연산이 가능하도록 -?
연산자를
하나 더 추가하며, num.operator->() -> ShowData()
형태를 만들고 해석을 진행한다.
스마트 포인터는 자기 스스로 하는 일이 존재하는 포인터로, 사실 포인터의 역할을 하는 객체이다. 따라서 구해야 할 대상이 아닌, 구현해야 할 대상임을 잊지 말자!.(여기선 라이브러리에서 제동하는 스마트 포인터의 사용방법이 아닌 직접 구현해보는 스마트 포인터를 다룬다)
class SmartPtr{
private:
Point * posptr;
public:
smartPtr(Point * ptr) : posptr(ptr){}
Point& operator*() const
{
retirm *posptr;
}
Point* operator->() const
{
retirm posptr;
}
~SmartPtr()
{
delete posptr;
}
}
위에서 스마트 포인터의 핵심은 *, ->
의 오버로딩에 있다.
Point 형을 받고 반환하며 자연스럽게 Point
클래스를 다룰 수 있다. 여기서 스마트포인터는 소멸자로 Point
객체의 소멸까지 담당해서 편한 Point
클래스의 사용이 가능하다
펑터(Functor)
라고 한다.class SortRule{
public:
virtual bool operator()(int num1, int num2) const = 0; // 순수 가상함수,즉 유도클래스에서 행동을 정함
}
class AscendingSort : public SortRule
{
public:
bool operator()(int num1, int num2) const{
if(num1> num2)
return true;
else
return false;
}
}
class descendingSort : public SortRule
{
public:
bool operator()(int num1, int num2) const{
if(num1 <num2)
return true;
else
return false;
}
}
class DataStorage
{
private:
int * arr;
int idx;
const int MAX_LEN;
public
...//생성자랑 각종 함수들
void SortData(const SortRule& functor)//버블 소트
{
for(int i =0; i < (idx-1); i++){
for(int j =0; j < (idx-1) - i; j++)
{
if(functor(arr[j], arr[j+1])
{
int temp=arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
int main(void){
DataStorage storage(5);
storage.AddData(40);
...
storage.SortData(AscendingSort());//AscendingSort는 객체인데 흡사 함수처럼 쓰인다 -> 펑터
storage.SortData(DescendingSort());//DescendingSort는 객체인데 흡사 함수처럼 쓰인다 -> 펑터
}
여기서 보면
void SortData(const SortRule& functor)//버블 소트
{
for(int i =0; i < (idx-1); i++){
for(int j =0; j < (idx-1) - i; j++)
{
if(functor(arr[j], arr[j+1])
{
int temp=arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
매개변수 형이 SortRule의 참조형이므로, SortRule 클래스를 상속하는 AscendingSort
클래스와 DescendingSort
클래스의 객체는 인자로 전달 가능하고, 기초 클래스인 SortRule
의 operator()
는 순수 가상함수로 선언되었으니, 유도 클래스의 operator()
가 대신 호출된다. 따라서, 펑터로 무엇이 전달되느냐에 따라 정렬의 기준이 바뀌게 된다.
그리고 이것이 펑터를 정의하는 이유다
A형 객체가 와야 할 위치에 B형 데이터(또는 객체)가 왔을 경우, B형 데이터를 인자로 전달받는 A형 클래스의 생성자 호출을 통해서 A형 임시객체를 생성한다.
이렇듯 기본 자료형 데이터를 객체로 형 변환하는 것은 적절한 생성자의 정의를 통해서 얼마든지 가능하다. 또한 반대로 객체를 기본 자료형 데이터로 형변환 하는 것도 가능하다.
실제 형변환을 위한 operator
형태는 다음과 같다.
class Number{
private:
int num;
public:
...
Number& operator=(const Number& ref)
{
cout << "operator=()" << endl;
num = ref.num;
return *this;
}
operator int () // 형 변환 연산자의 오버로딩
{
return num;
}
}
여기서 형 변환 연산자는
operator int () // 형 변환 연산자의 오버로딩
{
return num;
}
}
이것으로 이 함수를 통해 형 변환 연산자는 반환형을 명시하지 않는다는 것을 알 수 있다. 하지만 return 문에 의한 값의 반환은 얼마든지 가능하다. 여기서 int
의 뜻은
int형으로 형 변환해야 하는 상황에서 호출되는 함수이다.
이를 통해, Number num2 = num1 + 20
과 같은 계산이 가능해지며, 이때, num1
객체의 operator int
함수과 호출되어, 이때 반환하는 값 30과 20의 덧셈연산이 진행되고 연산의 결과로 num2
객체가 생성 된 것이다.