result에 객체 nb1과 nb2를 더한 값을 result로 대입하고 있는데, 에러를 보시면 "이러한 피연산자와 일치하는 "+" 연산자가 없습니다. 피연산자 형식이 NUMBOX + NUMBOX입니다."라는 에러가 뜨시는것을 보실 수 있습니다.
#include <iostream>
using namespace std;
class NUMBOX
{
private:
int num1, num2;
public:
NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
void ShowNumber(){
cout << "num1: " << num1 << ", num2: " << num2 << endl;
}
};
int main()
{
NUMBOX nb1(10, 20);
NUMBOX nb2(5, 2);
NUMBOX result = nb1 + nb2; // 에러 발생
nb1.ShowNumber();
nb2.ShowNumber();
}
#include <iostream>
using namespace std;
class NUMBOX
{
private:
int num1, num2;
public:
NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
void ShowNumber()
{
cout << "num1: " << num1 << ", num2: " << num2 << endl;
}
NUMBOX operator+(NUMBOX &ref)
{
return NUMBOX(num1+ref.num1, num2+ref.num2);
}
};
int main()
{
NUMBOX nb1(10, 20);
NUMBOX nb2(5, 2);
NUMBOX result = nb1 + nb2;
// NUMBOX result = nb1.operator+(nb2);
nb1.ShowNumber();
nb2.ShowNumber();
result.ShowNumber();
}
num1: 10, num2: 20
num1: 5, num2: 2
num1: 15, num2: 22
계속하려면 아무 키나 누르십시오 . . .
nb1 + nb2은 nb1.operator+(nb2)로 해석되어 컴파일 됩니다.
물론, operator+가 아니여도 operator<연산자>(operator+, operator-, operator*..)와 같은 형식으로 사용이 가능합니다.
#include <iostream>
using namespace std;
class NUMBOX
{
private:
int num1, num2;
public:
NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
void ShowNumber()
{
cout << "num1: " << num1 << ", num2: " << num2 << endl;
}
NUMBOX operator+(int num)
{
return NUMBOX(num1+num, num2+num);
}
};
int main()
{
NUMBOX nb1(10, 20);
NUMBOX result = nb1 + 10;
nb1.ShowNumber();
result.ShowNumber();
}
num1: 10, num2: 20
num1: 20, num2: 30
계속하려면 아무 키나 누르십시오 . . .
객체.operator+(피연산자), 객체 + 피연산자식으로 이루어져, 자료형이 다른 두 피연산자를 대상으로 하는 연산시, 반드시 객체가 왼쪽에 위치해야 연산이 가능operator+(피연산자, 피연산자), 피연산자 + 피연산자 의 식으로 객체가 뒤에 위치해도 정상적인 결과를 출력합니다.
#include <iostream>
using namespace std;
class NUMBOX
{
private:
int num1, num2;
public:
NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
void ShowNumber()
{
cout << "num1: " << num1 << ", num2: " << num2 << endl;
}
NUMBOX operator+(int num)
{
return NUMBOX(num1+num, num2+num);
}
friend NUMBOX operator+(int num, NUMBOX ref);
};
NUMBOX operator+(int num, NUMBOX ref)
{
ref.num1 += num;
ref.num2 += num;
return ref;
}
int main()
{
NUMBOX nb1(10, 20);
NUMBOX result = 10 + nb1 + 40;
nb1.ShowNumber();
result.ShowNumber();
}
friend 키워드가 붙은 이유는, 이 함수가 클래스의 멤버 함수가 아니기 때문에 멤버 변수에 접근할 수 없으므로 붙여준 것입니다.
operator+ 함수를 통해 operator+(10, nb1)으로 인식됩니다.
이어서 main 함수를 다시 한번 보시면 10과 nb1을 더하고 그 결과에서 40을 더해 result에 대입하고 있습니다.
#include <iostream>
using namespace std;
class NUMBOX
{
private:
int num1, num2;
public:
NUMBOX() { }
NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
void ShowNumber() {
cout << "num1: " << num1 << ", num2: " << num2 << endl;
}
NUMBOX operator++(){
num1+=1;
num2+=1;
return *this;
}
NUMBOX operator++(int)
{
NUMBOX temp(*this);
num1+=1;
num2+=1;
return temp;
}
};
int main()
{
NUMBOX nb1(10, 20);
NUMBOX nb2;
nb2 = nb1++;
nb2.ShowNumber();
nb1.ShowNumber();
nb2 = ++nb1;
nb2.ShowNumber();
nb1.ShowNumber();
}
num1: 10, num2: 20
num1: 11, num2: 21
num1: 12, num2: 22
num1: 12, num2: 22
계속하려면 아무 키나 누르십시오 . . .
6~21행에서 ++ 연산자가 멤버 함수의 형태로 오버로딩 되었음을 보실 수 있습니다.
안을 살펴보면, num1에 1을 더하고, num2에 1을 더하고, this가 아닌 *this를 반환합니다.
this는 객체의 주소를, *this는 this 포인터가 가리키는 객체, 실질적인 데이터를 의미합니다. 이런 형식은 전위 증가 연산입니다.
22~28행을 살펴보면 ++ 연산자가 멤버 함수의 형태로 오버로딩 되었으나, 위와는 달리 인수 목록에 int 타입이 등장합니다.
++nb = nb.operator++(); // 전위 증가 연산
nb++ = nb.operator++(int); // 후위 증가 연산
🛑int는 그저 전위 증가 연산과 후위 증가 연산을 구분하는 기준일 뿐, int 타입의 데이터를 인자로 전달한다는 뜻으로 오해하지 마시기 바랍니다.
24행을 보시면 NUMBOX 객체를 만들어두고 인자로 *this를 넘깁니다. 이 문장은 NUMBOX temp(num1, num2)와 같습니다.
25~26행에선 num1, num2의 값을 1씩 증가시키고 27행에선 기존의 값을 담은 temp를 반환합니다.
#include <iostream>
using namespace std;
class A
{
private:
int num1, num2;
public:
A() { } // 디폴트 생성자
A(int num1, int num2) : num1(num1), num2(num2) { }
void ShowData() { cout << num1 << ", " << num2 << endl; }
};
class B
{
private:
int num1, num2;
public:
B() { }
B(int num1, int num2) : num1(num1), num2(num2) { }
void ShowData() { cout << num1 << ", " << num2 << endl; }
};
int main()
{
A a1(10, 50);
A a2;
B b1(10, 20);
B b2;
a2 = a1;
b2 = b1;
a2.ShowData();
b2.ShowData();
return 0;
}
10, 50
10, 20
계속하려면 아무 키나 누르십시오 . . .
27행에서 a1 객체가 만들어짐과 동시에 생성자에게 10과 50을 각각 전달하고, 멤버 변수 num1, num2를 초기화 합니다.
28행에서는 a2 객체가 만들어지고 디폴트 생성자가 호출됩니다. (초기화되지 않음)
29~30행도 마찬가지로 생성과 동시에 멤버 변수가 초기화된 b1 객체와, 그렇지 않은 b2 객체로 나뉩니다.
32~33행을 보시면 대입 연산자가 쓰였는데, 이상하게도, 우리가 대입 연산자를 정의하지 않았음에도 이런 문장은 정상적으로 멤버 대 멤버 복사가 이루어집니다.
사실은, 디폴트 복사 생성자와 같이, 대입 연산자가 정의되지 않으면 디폴트 대입 연산자가 삽입이 되는 것입니다.
멤버 대 멤버 복사를 수행할때 깊은 복사가 아닌 얕은 복사를 진행합니다.
#include <iostream>
using namespace std;
class Student
{
private:
char * name;
int age;
public:
Student(char * name, int age) : age(age)
{
this->name = new char[10];
strcpy(this->name, name);
}
void ShowInfo() {
cout << "이름: " << name << endl;
cout << "나이: " << age << endl;
}
~Student()
{
delete []name;
cout << "~Student 소멸자 호출!" << endl;
}
};
int main()
{
Student st1("김철수", 14);
Student st2("홍길동", 15);
st2 = st1;
st1.ShowInfo();
st2.ShowInfo();
return 0;
}
이름: 김철수
나이: 14
이름: 김철수
나이: 14
~Student 소멸자 호출!
계속하려면 아무 키나 누르십시오 . . .
5~25행에 Student 클래스가 정의되었습니다.
이름을 나타내는 name, 나이를 나타내는 age 멤버 변수가 존재합니다.
생성자를 보시면, 멤버 이니셜라이저를 통해 age를 초기화 하고, this->name에 길이가 10인 char형 공간을 할당해주고, 인자로 받은 name을 this->name로 복사합니다.
20~24행은 소멸자가 정의되었는데, 소멸자 안을 살펴보시면 따로 할당한 name을 메모리 공간에서 해제하고, 소멸자가 호출되었음을 알리기 위해 "~Student 소멸자 호출!"을 화면에 출력하게 했습니다.
29~30행에서 st1, st2 객체가 생성됨과 동시에 멤버 변수 초기화를 했습니다.
32행에서 디폴트 대입 연산자에 의해 멤버 대 멤버 복사가 이루어지는데, 여기서 문제가 발생합니다.
복사가 이루어지면서 st2는 "홍길동"이 아닌 "김철수"란 문자열이 담긴 주소를 가리키고, "홍길동"이란 문자열은 접근도, 소멸도 불가능 해지는 상황이 벌어집니다.
또한, 두 객체의 소멸자가 호출될 때 st1, st2 객체 모두 "김철수"란 문자열이 담긴 주소를 가리키고 delete를 통해 소멸할 때 중복 소멸하는 문제가 일어납니다.
✔ 이것을 해결하기 위해선 어떻게 해야할까요? 얕은 복사가 아닌 깊은 복사를 정의하면 됩니다. 아래와 같이 말이죠.
#include <iostream>
using namespace std;
class Student
{
private:
char * name;
int age;
public:
Student(char * name, int age) : age(age)
{
this->name = new char[10];
strcpy(this->name, name);
}
void ShowInfo() {
cout << "이름: " << name << endl;
cout << "나이: " << age << endl;
}
Student& operator=(Student& ref)
{
delete []name;
name = new char[10];
strcpy(name, ref.name);
age = ref.age;
return *this;
}
~Student()
{
delete []name;
cout << "~Student 소멸자 호출!" << endl;
}
};
int main()
{
Student st1("김철수", 14);
Student st2("홍길동", 15);
st2 = st1;
st1.ShowInfo();
st2.ShowInfo();
return 0;
}
이름: 김철수
나이: 14
이름: 김철수
나이: 14
~Student 소멸자 호출!
계속하려면 아무 키나 누르십시오 . . .
20~27행을 보시면 대입 연산자를 정의(연산자 오버로딩)하고 있습니다.
name을 메모리 공간에서 해제시키고, 23행에서 새로 공간을 할당합니다. 24행에서 strcpy 함수를 통해 ref.name을 name에 복사시킵니다. 25행에서는, age에 ref.age를 대입하고, 객체가 담고있는 값을 반환합니다.이러게 되면, "홍길동"란 문자열이 해제되지 않고 메모리 공간에 남아있는 문제를 해결할 수 있고(22행의 delete 연산), 정의된 대입 연산자에 의해 복사가 이루어지고 st1의 "김철수"와 st2의 "김철수"는 서로 다른곳을 가리키게 됩니다.