[CPP] 복사와 삭제.

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;
}

Leave a Reply

Your email address will not be published. Required fields are marked *