5장 함수와 참조, 복사 생성자
- 5.1 함수의 인자 전달 방식 리뷰
- 5.2 함수 호출 시 객체 전달
- 5.3 객체 치환 및 객체 리턴
- 5.4 참조와 함수
참조형 변수
(valueVsRef.ppt 참고하여) 아래 세 가지 질문에 대해 답할 수 있어야 함.
- - 포인터형 변수와 참조형 변수의 유사점?
- - 포인터형 변수와 참조형 변수의 차이점?
- - C++ 참조형과 Java 참조형의 차이점?
함수를 호출할 때 매개변수를 전달(하고 리턴 값을 반환)하는 방법
1) 값 방식 매개변수 전달 (call-by-value)
void f(int x) { ...} // 값 방식 매개변수 전달
void g(int* p) { ... } // 값(포인터 주소) 방식 매개변수 전달
cf. 포인터는 주소라는 특별한 형태의 값
Ex)
int add(int x, int y) { // 값 방식의 좋은 예
return x+y;
}
Ex)
void swap(int x, int y) { // 값 방식의 나쁜 예
int tmp = x;
x = y;
y = tmp;
}
Ex)
void swap(int* px, int* py) { // 값 방식의 나쁜 예를 포인터 형으로 보완
int tmp = *px;
*px = *py;
*py = tmp;
}
cf. 함수의 리턴 값을 돌려주기
- MyClass f() { ... }
- MyClass* f() { ... }
2) 참조 방식 매개변수 전달 (call-by-reference)
void h(int& r) { ... } // 참조 방식 매개변수 전달
Ex)
void swap(int& x, int& y) { // 값 방식의 나쁜 예를 참조형으로 보완
int tmp = x;
x = y;
y = tmp;
}
cf. 참조 방식 리턴
- MyClass& f() { ... }
- 참고) P. 235 : 그림 5-9 예제
P. 237 : 예제 5-8
참조형을 사용하는 전형적인 두 가지 예
- - 할당 연산자 정의
- - 입출력 연산자 정의
Ex)
class Money {
int dolar;
int cent;
public:
...
};
Money m1, m2(10,20);
m1 = m2; // <== 할당 연산자를 프로그래머가 직접 작성할 수 있다.
Money& operator=(Money& left, const Money& right) {
left.setDollar( right.getDollar() );
left.setCent( right.getCent() );
return left;
}
cin >> m1 >> m2; // <== 이렇게 코드를 작성할 수 있다.
// Money 클래스의 입력 연산자를 아래와 같이 정의하면
istream& operator>>(istream& is, Money& m) {
int d, c;
is >> d >> c;
m.setDollar(d);
m.setCent(c);
return is;
}
cout << m1 << m2; // <== 이렇게 코드를 작성할 수 있다.
// Money 클래스의 출력 연산자를 아래와 같이 정의하면
ostream& operator<<(ostream& os, const Money& m) {
os << m.getDolar() << m.getCent();
return os;
}
5.5 복사 생성자 (할당 연산자 + 소멸자)
- - 포인터형 멤버변수를 포함하는 클래스에서 복사 생성자, 할당 연산자, 소멸자를 정의하는 방법
- - 포인터 멤버 변수를 포함하는 클래스에서 깊은/얕은 복사/삭제를 구분하여 할당연산자, 복사생성자, 소멸자를 작성하는 방법
1) 형식
2) 언제 호출되는지
- - 소멸자 => 생성자가 호출되는 3가지 경우와 연관지어 설명 (객체 선언, 명시적 생성자 호출, new 연산자 사용)
- - 할당연산자 => 호출객체와 인자가 각각 할당 연산자의 왼쪽 오른쪽
- - 복사생성자 => 다음 3가지 경우에 호출됨
- 1) 값 방식으로 매개변수를 복사 전달하는 경우 호출
- 2) 값 방식으로 리턴할 때 호출
- 3) 객체를 선언하면서 동시에 초기화할 때 (ex. Money m1 = m2; )
3) 구현
- - 소멸자 (깊은 방식으로 지우기 delete [] a)
- - 할당연산자와 복사생성자는 서로 코드 비교
- - 클래스에 할당 연산자, 복사 생성자, 소멸자를 따로 정의하지 않았다면 얕은 복사와 얕은 삭제 방식의 기본 메소드들을 컴파일러가 추가
Table 클래스
- - Name* p 멤버 변수를 포함
- - 할당 연산자, 복사 생성자, 소멸자를 어떻게 정의하는지
- - 1) 깊은 방식 구현 vs. 2) 얕은 방식 구현
=====
Table 클래스
1) 깊은 방식 구현
class Table {
public:
Table (const Table&); // copy constructor
Table& operator = (const Table&); // copy assignment
virtual ~Table(); // deconstructor
…
private:
string *p; // String 배열을 가리키는 포인터
int sz;
};
Table::Table(const Table& t) { // copy constructor
sz = t.sz;
p = new string[sz];
for (int i=0; i<sz; i++) p[i] = t.p[i];
}
Table& Table::operator= (const Table& t) { // copy assignment
if ( this != &t ) { // self-assignment protection
delete [] p;
sz = t.sz;
p = new string[sz];
for (int i=0; i<sz; i++) p[i] = t.p[i];
}
return *this;
}
Table::~Table() { // deconstructor
delete [] p;
}
2) 얕은 방식으로 구현
class Table {
public:
Table (const Table&); // copy constructor
Table& operator = (const Table&); // copy assignment
virtual ~Table(); // deconstructor
…
private:
string* p; // string형 객체를 원소로 하는 배열
int sz;
};
Table::Table(const Table& t) { // copy constructor
sz = t.sz;
p = t.p;
}
Table& Table::operator= (const Table& t) { // copy assignment
sz = t.sz;
p = t.p;
return *this;
}
Table::~Table() { // deconstructor
p = NULL;
}
실습 문제 6번: MyIntStack 클래스
[ main1.c ]
목표:
- 스택 자료구조의 특징 LIFO - Last-In First-Out
- pop함수에서 참조형 인자를 사용
int main() {
MyIntStack s(10);
s.push(1);
s.push(2);
s.push(3);
int v;
s.pop(v);
cout << v;
s.pop(v);
cout << v;
s.pop(v);
cout << v;
}
Q. 위 프로그램의 실행 결과는?
[ main2.c ]
목표:
- main 함수에서 만든 MyIntStack 객체를 함수 f에 값 방식으로 전달
void f(MyIntStack s1) { int v; s1.pop(v); cout << v; s1.pop(v); cout << v; s1.pop(v); cout << v; s1.push(4); s1.push(5); } int main() { MyIntStack s2(10); s2.push(1); s2.push(2); s1.push(3); f( s2 ); int w; s2.pop(w); cout << w; s2.pop(w); cout << w; }
Q. main 함수에서 s2.pop(w)를 실행해서 얻은 w의 값은?
w의 값으로 보아 s1과 s2가 어떤 형태로 관련있는지 유추하시오.
[ main3.c ]
목표:
- 함수 인자로 값 방식으로 전달할 때 복사 생성자가 호출되는 것을 확인.
- (프로그램에 복사 생성자가 없으면 컴파일러가 자동으로 아래와 같은 얕은 방식의 복사 생성자 코드를 만들어 준다.)
class MyIntStack { ... MyIntStack(const MyIntStack& myIntStack); ... } MyIntStack::MyIntStack(const MyIntStack& myIntStack) { cout << "MyInStack 복사 생성자" << endl; this->size = myIntStack.size; this->p = myIntStack.p; // 얕은 복사 this->tos = myIntStack.tos; } void f(MyIntStack s1) { cout << "f() begins." << endl; int v; s1.pop(v); cout << v; s1.pop(v); cout << v; s1.pop(v); cout << v; s1.push(4); s1.push(5); }
int main() { cout << "main()" << endl; MyIntStack s2(10); s2.push(1); s2.push(2); s1.push(3); cout << "Before f() is called." << endl; f( s2 ); cout << "After f() is called." << endl; int w; s2.pop(w); cout << w; s2.pop(w); cout << w; }
Q. "MyIntStack 복사생성자"가 출력되는 위치로 판단하건데 복사생성자가 언제 호출되었는지?
Q. 값 방식으로 인자를 받는
void f(MyIntStack s1) { ... }를
참조 방식으로 인자를 받도록 수정하고,
void f(MyIntStack& s1) { ... }를
복사 생성자가 호출되는지 확인하시오.
Q. 값 방식으로 인자를 받는
void f(MyIntStack s1) { ... }를
포인터 방식으로 인자를 받도록 수정하고,
void f(MyIntStack* s1) { ... }를
복사 생성자가 호출되는지 확인하시오. 함수 f를 호출할 때 주소를 넘겨주도록 추가 수정이 필요하다.
Q. 함수 인자를 전달하는 세 가지 방법, 값 방식, 참조 방식, 포인터 방식에서 복사 생성자가 호출되는 경우는?
[ main4.c]
목표:
- 깊은 방식 복사 생성자를 만들어 동일한 코드의 main 함수에서 w 값이 어떻게 달라지는지 확인.
class MyIntStack { ... MyIntStack(const MyIntStack& myIntStack); ... } MyIntStack::MyIntStack(const MyIntStack& myIntStack) { cout << "MyInStack 복사 생성자" << endl; this->size = myIntStack.size; // int형 값의 경우 // 깊은 복사와 얕은 복사의 // 차이가 없음 this->p = new int[this->size]; // 깊은 복사 for(int i=0; i<this->size; i++) // this->p[i] = myIntStack.p[i]; // this->tos = myIntStack.tos; }
[ main5.c ]
목표:
- 복사 생성자가 호출되는 3가지 경우를 확인
void f(MyIntStack s3) { ... } MyIntStack g() { // MyIntStack& 참조형이나 // MyIntStack* 포인터형이 아닌 // MyIntStack 값형으로 리턴 return MyIntStack(10); } int main() { MyIntStack s1(10); MyIntStack s2 = s1; // (1) 객체 선언하면서 초기화하는 경우 f( s1 ); // (2) 함수에 값 형 인자를 전달하는 경우 g(); // (3) 값 형으로 리턴 받는 경우 }
Q. (1), (2), (3)의 경우에 MyIntStack 복사 생성자가 호출되는 것을 확인하시오.
[ main6.c ]
목표:
- 얕은 방식으로 복사하면 얕은 방식으로 삭제하고 깊은 방식으로 복사하면 깊은 방식으로 삭제하는 것이 일반적으로 바람직하다.
Q. MyIntStack 클래스의 얕은 방식 소멸자와 깊은 방식 소멸자를 각각 작성하시오.
(힌트: Table 클래스의 소멸자 구현을 참고)
Q. main2.c에서 깊은 방식의 소멸자와 얕은 방식의 복사 생성자를 가정할 때, 프로그램 실행에 문제가 발생한다. 이 문제를 설명하시오.
[ main7.c ]
목표:
- 복사생성자, 소멸자와 함께 할당 연산자도 일관된 방식으로 구현하는 것이 바람직하다.
int main() { MyIntStack s1(10); MyIntStack s2; s2 = s1; }
Q. MyIntStack 클래스의 얕은 방식 할당연산자와 깊은 방식 할당연산자를 각각 작성하시오.
(힌트: Table 클래스의 할당연산자 구현을 참고)
Q. 복사생성자와 할당연산자의 차이를 아래의 코드로 비교하시오.
MyIntStack s1(10);
MyIntStack s2;
s2 = s1; // 할당연산자 호출
MyIntStack s1(10);
MyIntStack s2 = s1; // 복사생성자 호출
// 할당연산자를 호출하지 않음
=====
아래 예제에서 MyIntStack 클래스를 디폴트 방식(얕은 방식)과 깊은 복사 방식의 복사 생성자를 사용할 때 프로그램 실행 결과가 어떻게 차이를 내는지 보여준다.
[ 얕은 방식의 복사생성자 (디폴트 버전) ]
void f(MyIntStack s1) { { s1.tos == 2, s2.tos == 2, s1.p == s2.p, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } int v; s1.pop(v); cout << v; // 3 2 1을 출력 s1.pop(v); cout << v; s1.pop(v); cout << v; { s1.tos == -1, s2.tos == 2, s1.p == s2.p, // s1.tos와 s2.tos가 달라짐 s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } s1.push(4); s1.push(5); { s1.tos ==1, s2.tos == 2, s1.p == s2.p, // s1.tos와 s2.tos가 다르고, s2.p[0]==4, s2.p[1]==5, s2.p[2]==3 } // s2.p[0], s2.p[1]을 덮어씀 } int main() { MyIntStack s2(10); { s2.tos == -1 } s2.push(1); s2.push(2); s2.push(3); { s2.tos==2, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } f( s2 ); { s2.tos == 2, // f에서 리턴되기 전 상태와 동일 s2.p[0]==4, s2.p[1]==5, s2.p[2]==3 } // 하지만 s1은 사라짐 int w; s2.pop(w); cout << w; // 3 5를 출력 s2.pop(w); cout << w; { s2.tos == 0, s2.p[0]==4, s2.p[1]==5, s2.p[2]==3 } return 0; }
[ 깊은 방식의 복사생성자 ]
void f(MyIntStack s1) { { s1.tos == 2, s2.tos==2, s1.p[0]==1, s1.p[1]==2, s1.p[2]==3, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } int v; s1.pop(v); cout << v; // 3 2 1을 출력 s1.pop(v); cout << v; s1.pop(v); cout << v; { s1.tos == -1, s2.tos==2, s1.p[0]==1, s1.p[1]==2, s1.p[2]==3, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } s1.push(4); s1.push(5); { s1.tos == 1, s2.tos==2, s1.p[0]==4, s1.p[1]==5, s1.p[2]==3, // s1.p[0]과 s1.p[1]을 변경 s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } // s2.p[0]과 s2.p[1]은 그대로 } int main() { MyIntStack s2(10); { s2.tos == -1 } s2.push(1); s2.push(2); s2.push(3); { s2.tos==2, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } f( s2 ); { s2.tos==2, // f가 리턴하기 전 상태와 동일 s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } // 하지만 s1은 사라짐 // f를 호출하기 전후의 s2.p[i]값은 변하지 않음 // (i=0,1,2) int w; s2.pop(w); cout << w; // 3 2를 출력 s2.pop(w); cout << w; { s2.tos == -1, s2.p[0]==1, s2.p[1]==2, s2.p[2]==3 } return 0; }