[문과 코린이의 IT 기록장] C,C++ - 연산자 오버로딩 3 : 대입연산자 (대입연산자, 디폴트 대입 연산자의 문제점, 상속 구조에서의 대입 연산자 호출, 이니셜라이저)
[문과 코린이의 IT 기록장] C,C++ - 연산자 오버로딩 3 : 대입연산자
(대입연산자, 디폴트 대입 연산자의 문제점, 상속 구조에서의 대입 연산자 호출, 이니셜라이저)
1. 대입연산자
1) 대입 연산자 오버로딩은, 복사 생성자와 매우 유사하다.
[ 디폴트 대입 연산자 ] |
2) 복사생성자 vs 대입연산자
: 호출되는 시점에서 차이가 존재한다.
a. 복사생성자 호출 상황
int main(){
Point pos1(5,7);
Point pos2 = pos1; // 새로 생성하는 객체 pos2의 초기화에, 기존에 생성된 객체 pos1이 사용되었다.
....
}
b. 대입연산자 호출 상황
int main(){
Point pos1(5,7);
Point pos2(2,3);
pos2 = pos1;
// pos1과 pos2 둘 다, 이미 생성 및 초기화가 진행된 객체이다.
// 따라서 pos2.operator=(pos1); 으로 해석된다.
....
}
즉, 기존 생성된 두 객체 간의 대입연산시에는, 대입연산자가 호출된다.
ex )
#include<iostream>
using namespace std;
class First { // 대입연산자 정의 X 클래스 (디폴트 대입 연산자가 삽입된다.)
private:
int num1, num2;
public:
First(int n1 = 0, int n2 = 0) :num1(n1), num2(n2) { } // 생성자
void ShowData() { cout << num1 << ", " << num2 << endl; } // num1, num2 출력 함수
};
class Second { // 대입연산자 정의 O 클래스
private:
int num3, num4;
public:
Second(int n3 = 0, int n4 = 0) :num3(n3), num4(n4) { } // 생성자
void ShowData() { cout << num3 << ", " << num4 << endl; } // num3, num4 출력 함수
Second& operator=(const Second& ref) { // 대입연산자 (맴버 대 맴버의 복사 진행)
cout << "Second & operator=()" << endl;
num3 = ref.num3;
num4 = ref.num4;
return *this; // 대입연산자의 반환형은 참조형이다. (객체를 반환)
}
};
int main() {
First fsrc(111, 222);
First fcpy;
Second ssrc(333, 444);
Second scpy;
fcpy = fsrc; // 이미 생성된 객체에 대입하는 것이므로, 대입연산자 오버로딩. fcpy.operator=(fsrc)로 해석.
scpy = ssrc; // scpy.operator=(ssrc)로 해석
fcpy.ShowData();
scpy.ShowData();
First fob1, fob2;
Second sob1, sob2;
fob1 = fob2 = fsrc; // 대입 연산자는 오른쪽에서 왼쪽으로 진행됨. (디폴트)대입 연산자의 반환형이 참조형이기 때문에, 이 문장은 실행 가능. 즉 fob1.operator=(fob2.operator=(fsrc))가 실행된다는 것.
[ 자동으로 삽입된, 디폴트 대입 연산자 ] First & operator=(const First &ref){ num1=ref.num1; num2=ref.num2; return *this; } |
sob1 = sob2 = ssrc;
fob1.ShowData();
fob2.ShowData();
sob1.ShowData();
sob2.ShowData();
return 0;
}
2. 디폴트 대입 연산자의 문제점
: 복사생성자에서 보였던 문제와 유사하고, 해결책 또한 유사하다.
ex 1 )
#include <iostream>
#include <cstring>
using namespace std;
class Person {
private:
char* name;
int age;
public:
Person(const char* myname, int myage) { // Person 생성자
int len = strlen(myname) + 1;
name = new char[len];
strcpy(name, myname);
age = myage;
}
void ShowPersonInfo() const { // 이름, 나이 출력 함수
cout << "이름 : " << name << endl;
cout << "나이 : " << age << endl;
}
~Person() { // Person소멸자
delete[]name;
cout << "called destructor!" << endl;
}
};
int main() {
Person man1("Lee dong woo", 29);
Person man2("Yoon ji yul", 22);
man2 = man1; // Person 클래스에서는 대입 연산자가 정의되어 있지 않으므로, man2.operator=(man1) 디폴트 대입 연산자가 호출된다.
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
실행 결과를 보면, 문자열 "called destructor" 즉, 소멸자가 한번 출력한 것을 볼 수 있다.
디폴트 대입 연산자의 문제점은, 디폴트 복사 생성자의 문제점과 많이 유사하다.
즉, 디폴트 대입 연산자가 호출되면, 다음 문장은 단순히 맴버 대 맴버를 복사만 하므로, 다음의 구조를 띠게 된다.
man2 = man1;
이와 같이 하나의 문자열을 두 개의 객체가 동시에 참조하게 되는 상황이 벌어지며, 다음 두 가지 문제가 발생한다.
a. 문자열 "Yoon ji yul"을 가리키던 문자열의 주소값을 잃게 된다. // 메모리 누수 발생
b. 얕은 복사로 인해, 객체 소멸과정에서 지워진 문자열을 중복 소멸하는 문제가 발생한다. // 즉 남아있는 man1 객체의, delete[]name 소멸자가 실행되지 못한다.
이러한 문제를 해결하기 위해, 생성자 내에서 동적할당을 하는 경우, 다음의 형태로 대입연산자를 지정해야 한다.
- 깊은 복사를 진행하도록 정의한다.
- 메모리 누수가 발생하지 않도록, 깊은 복사에 앞서 메모리 해제의 과정을 거친다.
ex 2 )
3. 상속 구조에서의 대입 연산자 호출
- 생성자는 아무런 명시를 하지 않아도, 유도 클래스에서 기초 클래스의 생성자가 호출된다.
- 그러나, 유도 클래스의 대입 연산자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다.
ex 1 )
#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; } // num1, num2 출력 함수
First& operator=(const First& ref) { // 대입연산자
cout << "First &operator=()" << endl;
num1 = ref.num1;
num2 = ref.num2;
return *this;
}
};
class Second : public First { // 유도 클래스
private:
int num3, num4;
public:
Second(int n1, int n2, int n3, int n4) :First(n1, n2), num3(n3), num4(n4) { } // 생성자
void ShowData() { // 데이터 출력 함수
First::ShowData();
cout << num3 << ", " << num4 << endl;
}
/*
Second& operator=(const Second& ref) {
cout << "Second &oeprator=()" << endl;
num3 = ref.num3;
num4 = ref.num4;
return *this;
}
*/
};
int main() {
Second ssrc(111, 222, 333, 444);
Second scpy(0, 0, 0, 0);
scpy = ssrc; // scpy.operator=(ssrc);
scpy.ShowData();
return 0;
}
- 유도클래스에 삽입된, 디폴트 대입 연산자가, 기초 클래스의 대입 연산자까지 호출한다.
ex 2 ) 주석 해제
- 유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연산자 호출문을 삽입하지 않으면, 기초 클래스의 대입 연산자는 호출되지 않아서, 기초 클래스의 맴버변수는 맴버 대 맴버의 복사 대상에서 제외된다.
- 따라서, 유도 클래스의 대입 연산자를 정의해야 하는 상황이 생기면, 기초 클래스의 대입 연산자를 명시적으로 호출해야 한다.
Second & operator=(const Second & ref){ // ref는 Second 참조형이지만, Second객체 혹은 Second를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.
cout<<"Second& operator=()"<<endl;
First::operator=(ref); // 기초 클래스의 대입 연산자 호출을 명령
num3 = ref.num3;
num4 = ref.num4;
return *this;
}
4. 이니셜라이저
- 이니셜라이저를 사용하면, 성능이 향상된다. 그 이유에 대해 확인해보자.
ex )
#include <iostream>
using namespace std;
class AAA {
private:
int num;
public:
AAA(int n = 0) :num(n) { // 생성자
cout << "AAA(int n=0)" << endl;
}
AAA(const AAA& ref) :num(ref.num) { // 복사생성자
cout << "AAA(const AAA & ref)" << endl;
}
AAA& operator=(const AAA& ref) { // 대입연산자
num = ref.num;
cout << "operator=(const AAA &ref)" << endl;
return *this;
}
};
class BBB {
private:
AAA mem;
public:
BBB(const AAA& ref) : mem(ref) { } // 복사생성자
// BBB 클래스는, 이니셜라이저를 이용해서 맴버를 초기화하고 있다.
[ 이니셜라이저를 사용하면, 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 형태가 생성된다. ] |
};
class CCC {
private:
AAA mem;
public:
CCC(const AAA& ref) { mem = ref; } // 복사생성자
// CCC 클래스는, 대입연산을 이용해서 맴버를 초기화하고 있다. (BBB클래스와 CCC클래스의 초기화 방식 차이)
[ 생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도의 문장에서 진행하는 형태로 바이너리 코드가 생성된다. ] 따라서, 생성자와, 대입연산자가 각각 한번씩 호출된다. AAA(int n=0) operator=(const AAA &ref) |
};
int main() {
AAA obj1(12); // AAA클래스의, obj1객체 생성
cout << "*****************************" << endl;
BBB obj2(obj1); // BBB 클래스의 obj2 객체 생성 (obj1의 복사생성자)
cout << "*****************************" << endl;
CCC obj3(obj1); // CCC클래스의 obj3 객체 생성 (obj1의 복사생성자)
return 0;
}
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |