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
의 "김철수"는 서로 다른곳을 가리키게 됩니다.