전 책에서 이 부분을 좋아합니다. 왜냐하면 이 책을 읽지 않았더라면 평생 모르고 생각하지도 않고 살았을 테니까요.
같은 기능의 함수라도 매개변수와 반환형을 다르게 할 수 있고 그거에 따른 복사 생성자와 대입 연산자 사용 횟수를
저는 절대 생각하고 깨닫지 못했을 거니까요. φ(◎ロ◎;)φ
여기 Complex 클래스가 정의돼있습니다.
class Complex {
double real, imag; // 복소수의 실수부와 허수부
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) { }
//...
};
void main() {
Complex a(1,2), b(3,4), c;
}
복소수 객체 a와 b를 더해 c에 저장하려고 합니다. 물론 계산 후 a와 b는 값이 변경되면 안 되고 c만 변경되어야 합니다.
이를 위한 방법은 크게 2가지로 구현할 수 있습니다.
- 절 자치 향적인 방법(일반 함수)
- 결과를 반환하지 않는 방법 : 4가지
- 결과를 반환하는 방법 : 3가지
- 객체지향적인 방법(Complex의 멤버 함수)
- 결과를 반환하지 않는 방법 : 3가지
- 결과를 반환하는 방법 : 3가지
▶일반 함수로 구현
함수 원형 설계 | 사용 방법 | 복사 생성자 | 대입 연산자 |
void add(Complex, Complex, Complex*); |
add(a, b, &c); | 2 | 0 |
void add(Complex*, Complex*, Complex*); |
add(&a, &b, &c); | 0 | 0 |
void add(Complex, Complex, Complex&); |
add(a, b, c); | 2 | 0 |
void add(Complex&, Complex&, Complex&); |
add(a, b, c); | 0 | 0 |
일반 함수로 add() 구현하는 방법들(결과를 반환하지 않음)
값이 변경되지 않아야 하는 a와 b는 객체(Complex)나 포인터(Complex*) 또는 참조자(Complex&) 중 어느 형태로 전달해도 상관없습니다.
그러나 c는 반드시 Complex*나 Complex&로 전달해야 합니다. 값을 변경하려면 당연히 이렇게 해야 합니다.
그냥 객체로 보내면 객체가 매개변수로 복사가 된 거라 c가 변경되지는 않습니다.
그리고 대입 연산자는 사용되지 않습니다.
그리고 객체로 전달하면 복사 생성자가 호출되는 것에 유의해야 합니다.
두 번째 네 번째 방법으로 하면 전혀 호출되지 않습니다.
함수 원형 설계 | 사용 방법 | 복사 생성자 | 대입 연산자 |
Complex add(Complex, Complex); | c = add(a, b); | 3 | 1 |
Complex add(Complex*, Complex*); | c = add(&a, &b); | 1 | 1 |
Complex add(Complex&, Complex&); | c = add(a, b); | 1 | 1 |
일반 함수로 add() 구현하는 방법들(결과 반환)
먄약 add()가 결과를 반환하려면 반드시 반환 자료형은 객체(Complex)가 되어야 합니다.
객체의 주소나 참조자를 함수에서 반환하기 위해서는 그 함수가 끝나도 소멸되지 않는 객체여야 합니다.
이러한 함수를 사용하려면 반드시 대입 연산자가 사용되어야 합니다.
그리고 매개변수와 반환형에 있는 Complex의 수만큼의 복사 생성자가 호출됩니다.
일반 함수로 구현한 총 7가지의 방법 중 어떤 것이 가장 좋을까요?
저는 복사 생성자와 대입 연산자를 사용하지 않는 방법을 추천합니다.
특히 최근 코드의 복잡도를 줄이기 위해 포인터보다는 참조자를 추천합니다.
이것을 구현한 코드
void add(Complex& a, Complex& b, Complex& c){
c.real = a.real + b.real;
c.imag = a.imag + b.imag;
}
▶멤버 함수로 구현
함수 원형 설계 (Complex 클래스의 멤버 함수) |
사용 방법 | 복사 생성자 | 대입 연산자 |
void add(Complex, Complex,); | add(a, b); | 2 | 0 |
void add(Complex&, Complex&); | add(a, b); | 0 | 0 |
멤버 함수로 add() 구현하는 방법들(결과를 반환하지 않음)
일반 함수에서 멤버 함수로 구현하고 클래스 내에서 구현하니까 c의 값을 불러낼 필요가 없으니 매개변수가 하나 줄어든 거 빼곤 변한 게 없다.
표에 포인터는 특별한 장점이 없으니 생략했다. 쓸 수는 있습니다.
함수 원형 설계 (Complex 클래스의 멤버 함수) |
사용 방법 | 복사 생성자 | 대입 연산자 |
Complex add(Complex); | c = a.add(b); | 2 | 1 |
Complex add(Complex&); | c = a.add(b); | 1 | 1 |
Complex operator + (Complex&); | c = a + b; | 1 | 1 |
멤버 함수로 add() 구현하는 방법들(결과 반환)
모든 함수에서 복사 생성자가 호출되고, 대입 연산자도 사용됩니다.
특히 세 번째 방법은 연산자 중복 정의 기법을 사용하였습니다.
멤버 함수도 복사 생성자와 대입 연산자를 사용하지 않은 방법을 추천합니다.
이것을 구현한 방법은
void add(Complex& a, Complex& b){
real = a.real + b.real;
imag = a.imag + b.imag;
}
복소수 하나를 더하기 위해 이렇게 많은 방법이 있는 거에 처음엔 짜증이 확 났습니다.
내가 이걸 왜 하고 있냐고 생각도 했는데, 게임을 구현하다 보니 함수를 구현할 때 잘 구현하는 게 중요하다는 걸
깨닫고 이후로 받아들였습니다.
책은 함수의 설계에 대해 다음을 강조하고자 합니다.
- 가능한 한 복사 생성자의 호출을 피하는 방법으로 구현하는 것이 안전하다.
예를 들어, c.add(a, b)가 c = a.add(b) 보다 유리하고 안전합니다.
- 매개변수로 객체(Complex)를 보내는 것보다 참조자(Complex&)가 유리합니다.
그리고 전달된 객체를 수정하지 못하도록 상수형(const Complex&)으로 선언하는 것이 가장 좋습니다.
- 연산자 중복을 이용하면 두 객체를 더하기 위해 c = a + b;와 같은 문장을 사용할 수도 있습니다.
그러나 이를 위해서는 객체의 반환이 필요합니다.
이와 같은 방법은 제한적으로 사용하는 것이 바람직하며, 함수는 가능하면 단순하고 안전하게 구현하는 게 좋습니다.
- 복사 생성자가 필요한 경우에도 대부분은 디폴트 복사 생성자를 사용하면 됩니다.
그러나 "깊은 복사"가 필요한 경우는 디폴트 복사 생성자를 사용할 수 없으며 개발자가 구현해 주어야 합니다.
'C++ 프로그래밍 > 클래스' 카테고리의 다른 글
상속 (0) | 2022.08.08 |
---|---|
.과 ->의 쓰는 용도 (0) | 2022.08.04 |
얕은 복사와 깊은 복사 (0) | 2022.07.28 |
객체의 복사와 복사 생성자 (0) | 2022.07.28 |
객체 생성되는 것과 안 되는 것 (0) | 2022.07.25 |