C++에서 새로운 클래스를 정의한다는 것은 새로운 타입을 하나 정의하는 것과 같다.
하기 항목들은 클래스 설계 시 고려해야 되는 항목들이다.

 

 - 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?

 

 - 객체 초기화는 객체 대입과 어떻게 달라야 하는가?

   생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정짓는 요소이다.
   초기화와 대입을 헷갈리지 않는 것이 가장 중요하다.

 

 - 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?

 

   어떤 타입에 대해 '값에 의한 전달'을 구현하는 쪽은 복사 생성자이다.

 

 - 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?

 

 - 기존의 클래스 상속계통망에 맞출 것인가?
 

   이미 가지고 있는 클래스로부터 상속을 시킨다면, 이들 클래스에 의해 제약을 받는다. 특히 멤버 함수가 가상인지 비가상인가의

   여부가 가장 큰 요인이다.

   내가 만든 클래스를 다른 클래스들이 상속할 수 있게 만들자고 결정했다면, 멤버 함수의 가상 함수 여부가 결정된다.

   특히 소멸자가 이런 경우에 해당한다.

 

 - 어떤 종류의 타입 변환을 허용할 것인가?

 

   타입 변환이 되도록 하고 싶다면 암시적/명시적 타입 변환을 선택해서 만들어야 한다.

 

 - 어떤 연산자와 함수를 두어야 의미가 있을까?


   어떤 것들이 멤버 함수로 적당할 것이고, 또 몇몇은 그렇지 않을 것이다. (항목 23,24,46참조)

 

 - 표준 함수들 중 어떤 것을 허용하지 말 것인가?


   어떤 함수를 private으로 선언해야 하는지 생각할 것.

 

- 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?

 

   어떤 클래스 멤버를 public, private, protected 영여겡 둘 것인가를 결정하는데 도움을 주게 될 질문이다.

   또한 프렌드로 만들어야 할 클래스 및 함수를 정하는 것은 물론이고 한 클래스를 다른 클래스에 중첩시켜도 되가는가에 대한

   결정을 내리는 데도 도움을 줄 질문이다.

 

 - 선언되지 않은 인터페이스로 무엇을 둘 것인가?

 

 - 새로 만드는 타입이 얼마나 일반적인가?

 

   타입 하나를 정의하는 것이 아닐지도 모른다.
   설계된 클래스는 동일 계열의 타입군 전체일지도 모르기 때문에 클래스 템플릿을 정의해야할지도 모른다.

 

 - 정말로 꼭 필요한 타입인가?

 

   기존의 클래스에 대해 기능 몇 개가 아쉬워서 파생 클래스를 새로 뽑고 있다면, 차라리 간단하게 비멤버 함수라든지 템플릿을

   몇 개 더 정의하는 편이 더 좋다.

 

위에 질문들은 만만하게 볼 수 없는 질문들이다. 그래서 효과적인 클래스를 정의하는 일이 무척이나 어렵다는 말이 나오는 것이다.

 

1줄 요약

 

 - 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보자.

제대로 쓰기엔 쉽고 엉터리로 쓰기에 어려운 인터페이스를 개발하려면 우선 사용자가 저지를 만한 실수의

종류를 머리에 넣어두고 있어야 한다.

 

예시 1

 

아래 예는 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하는 과정이다.

 

1
2
3
4
5
6
class Date
{
public:
    Date(int month, int day, int year);
    ...
};
cs

 

문제점

 

 - 매개 변수의 전달 순서가 잘못될 여지가 있다.

1
Date d(303 , 2017); //월에 30이라는 값이 들어가짐
cs

 

 - 월과 일에 해당하는 숫자가 잘못된 숫자일 수도 있다.

1
Date d(340 , 2017); //3월 40일이라는 값이 들어가짐.
cs

 

 

개선 1-1

 

새로운 타입을 들여와 인터페이스를 강화하면 사용자의 실수를 막을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Day
{
    explicit Day(int d) : val(d) {    }
 
    int val;
};
 
struct Month
{
    explicit Month(int dm) : val(m) {    }
 
    int val;
};
 
struct Year
{
    explicit Year(int y) : val(y) {    }
 
    int val;
};
 
class Date
{
public:
    Date(const Month& m, const Day& d, const Year& y);
    ...
};
 
Date d(3302017);                    //타입이 틀려서 에러 발생.
 
Date d(Day(30), Month(3), Year(2017));  //타입이 틀려서 에러 발생.
 
Date d(Month(3), Day(30), Year(2017));  //정상 컴파일.
cs

 

 

개선 1-2

 

'월'이 가질 수 있는 유효한 값을 검사해야 하는데 enum을 사용해도 되지만 타입 안정성은 그리 믿음직하지 못하다.

타입 안정성이 신경 쓰인다면 유효한 Month의 집합을 미리 정의해 두어도 좋다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Month
{
public:
    static Month Jan() { return Month(1);  }
    static Month Feb() { return Month(2);  }
    static Month Mar() { return Month(2);  }
    ...
    static Month Dec() { return Month(12); }
 
private:
    explicit Month(int m);    //Month 값이 외부에서 생성되지 못하도록
                              //생성자를 private으로 선언.
};
 
Date d(Month::Mar(), Day(30), Year(2017));
cs

 

여기서 귀찮게 하나 하나 함수로 return하는 식으로 프로그램이 되어 있다.

아래와 같이 편하게 정적 객체로 사용할 수도 있는데.. 그러지 않은 이유는....

 

1
2
3
4
5
6
7
8
9
class Month
{
public:
    static int Jan = 1;
    static int Feb = 2;
    static int Mar = 3;
    ...
    static int Dec = 12;
};
cs

 

항목4에서 배운 '비지역 정적 객체들의 초기화 순서는 정해지지 않는다'는 특성 때문이다.

 

 

예시 2

 

예상되는 사용자 실수를 막는 다른 방법으로는 어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일들을

묶어 버리는 방법이 있다.

제약 부여 방법으로 아주 흔히 쓰이는 예가 'const 붙이기'이다. 항목3에서 설명했듯이 operator*의 반환 타입을

const로 한정함으로서 사용자가 사용자 정의 타입에 대해 다음과 같은 실수를 저지르지 않도록 할 수 있었다.

 

1
if ( (a * b) = c ) // 비교하려고 했으나 a*b 에 c를 대입해 버린 코드.
cs

 

여기서 하고자 하는 얘기는 '별다른 이유가 없다면 사용자 정의 타입은 기본 제공 타입처럼 동작하게 만들어라'

라는 것이다. int 등의 타입 정도는 사용자들이 그 성질을 이미 다 알고 있기 때문에, 사용자를 위해 만드는 타입도

웬만하면 이들과 똑같이 동작하게 만들라는 것이다.

 

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위해서이다.

제대로 쓰기에 괜찮은 인터페이스를 만들어주는 요인 중에 중요한 것은 일관성이다.

또한 불편한 인터페이스를 만들어주는 요인 중에 비일관성을 따라오는 것도 거의 없다.

 

사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘 못 쓰기 쉽고 언제라도 잊어버릴 수 있다.

 

 

예시 3

 

항목 13에서 썼던 팩토리 함수를 한 번 보자.

 

1
Investment* createInvestment();
cs

 

이렇게 사용하면 자원 누출을 피하기 위해 createInvestment에서 얻어낸 포인터를 나중에라도 삭제해야 한다.

 

문제점 

 

 - 사용자가 포인터 삭제를 잊을 수 있다.

 - 똑같은 포인터에 대해 delete가 두 번 이상 적용될 수 있다.

 - 스마트 포인터에 createInvestment에서 얻은 포인터를 저장시키는 것을 잊을 수 있다.

 

개선

 

1
std::tr1::shared_ptr<Investment> createInvestment();
cs

 

애초에 팩토리 함수가 스마트 포인터를 반환하게 만들자.

이렇게 해 두면, 함수의 반환 값은 tr1::shared_ptr에 넣어둘 수 밖에 없을 뿐더러, 나중에 Investment 객체가

필요 없어졌을 때 객체를 삭제하는 것에 사용자가 신경쓸 필요가 없어진다.

 

tr1::shared_ptr을 반환하는 구조는 자원 해제에 관련된 상당수의 사용자 실수를 사전 봉쇄할 수 있고 생성 시점에

자원 해제 함수(삭제자)를 직접 엮을 수 있는 기능을 갖고 있기 때문에 설계자에게 좋다.

 

 

예시 4

 

createInvestment를 통해 얻은 Investment* 포인터를 직접 삭제하지 않게 하고 getRidOfInvestment라는 함수에

포인터를 넘겨 삭제하도록 하는 기능을 준비했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Invest
{
public:
    ...
};
 
Invest* createInvest())
{
    return new Invest;
}
 
void getRidOfInvestment(Invest* obj)    //Invest 객체 삭제 전용 함수.
{
    delete obj;
}
 
void Fucn()
{
    Invest* pInvest = createInvest();
 
    ...        //pInvest 사용.
 
    getRidOfInvestment(pInvest);    //pInvest 객체 삭제.
}
cs

 

문제점

 

 - 사용자가 getRidOfInvestment를 사용하지 않고 직접 delete를 할 수 있다.

 - 실수로 delete도 하고 getRidOfInvestment도 호출하는 일이 발생할 수 있다.

 

개선

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Invest
{
public:
    ...
};
 
void getRidOfInvestment(Invest* obj) //삭제자.
{
    delete obj;
}
 
std::tr1::shared_ptr<Invest> createInvest()
{
    std::tr1::shared_ptr<Invest> ptr(new Invest, getRidOfInvestment); //초기화 포인터, 삭제자.
 
    return ptr;
}
 
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::tr1::shared_ptr<Invest> pInvest = createInvest();
 
    return 0;
}
 
cs

 

createInvesetment 함수를 삭제자가 묶인 tr1::shared_ptr을 반환하도록 구현하자.

그러면 알아서 pInvest가 가리키는 객체가 0이 되면 삭제자를 호출하여 객체를 삭제할 것이다.

그런데 그냥 위에처럼 쓸거면 삭제자 자체를 만들 필요는 없다. 삭제자가 없어도 저 기능을 알아서 하기 때문이다.

 

삭제자를 별도로 사용하는 경우는 만약 일반 지역 변수의 주소를 스마트 포인터로 가리킬 때, 일반 지역 변수는 new로 생성된

것이 아니기 때문에 소멸 시 delete를 하면 안되므로 delete를 하지 않는 삭제자를 별도로 만들어주기 위해서 사용한다.

또는 배열을 스마트 포인터로 가리킬 경우에 delete[]를 사용해주기 위한 삭제자를 별도로 만들어줄 때 사용한다.

 

즉, 삭제자가 명시된 스마트 포인터는 스마트 포인터의 소멸자를 호출하지 않고 명시된 삭제자를 호출한다.

삭제자가 없는 일반 스마트 포인터는 소멸자가 호출되어 자신이 가리키던 객체의 Heap memory를 delete 시켜준다.

 

그리고 삭제자 함수를 사용할 때, 삭제자 함수는 스마트 포인터가 가리키는 포인터형의 파라미터를 반드시 가지고 있어야한다.

 

 

예시 5

 

tr1::shared_ptr는 '교차 DLL 문제'가 생기는 경우에도 이런 문제를 미연에 방지해주는 역할을 해준다.

'교차 DLL 문제'가 생기는 경우는 어떤 DLL에서 객체를 생성하고 이 객체를 다른 DLL에서 소멸시킬 때 이다.

즉, 객체가 생성(new)되고 삭제(delete)되는 것이 서로 다른 DLL에서 이뤄지면 안된다는 것이다.

 

그런데 tr1::shared_ptr을 사용하면 이 문제를 피할 수 있다. tr1::shared_ptr은 생성된 DLL과 동일한 DLL에서 delete를

사용하도록 삭제자가 만들어져 있기 때문이다.

 

1
2
3
4
5
6
7
8
9
10
class stock : Invest
{
    ...
};
 
std::tr1::shared_ptr<Invest> createInvest()
{
    return std::tr1::shared_ptr<Invest>(new stock);
}
 
cs

 

createInvest가 반환하는 shared_ptr은 다른 DLL들 사이에 이리저리 넘겨지더라도 교차 DLL 문제를 걱정하지 않아도 된다는

뜻이다.

 

4줄 요약

 

 - 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터로리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록

   고민해야 한다.

 

 - 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본 제공 타입과의 동작

   호환성 유지하기이다.

 

 - 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기,

   자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.

 

 - tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아주며,

   뮤텍스 등을 자동으로 잠금 해제하는데 사용할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget
{
   ...
};
 
int Calculate();
 
void processWidget(std::tr1::shared_ptr<Widget> pw, int Value);
 
void fucn()
{
    processWidget(new Widget, Calculate());
}
cs

 

위와 같이 스마트 포인터와 int 타입의 매개 변수를 가지는 processWidget 함수를 호출하려고 하면,

컴파일 에러가 발생한다.

 

포인터를 받는 tr1::shared_ptr의 생성자는 explicit으로 선언되어 있으므로, new Widget으로 생성된 포인터가

tr1::shared_ptr 타입의 객체로 바뀌는 암시적 변환이 이루어질 수 없기 때문이다.

 

그래서 하기와 같이 명시적 변환으로 함수를 호출해야 한다.

 

1
2
3
4
void fucn()
{
    processWidget(std::tr1::shared_ptr<Widget>(new Widget), Calculate());
}
cs

 

그런데 이 함수 호출 방식은 자원 관리 객체를 쓰고 있는데도 자원 누출이 될 가능성이 있다.

 

컴파일러는 processWidget 호출 코드를 만들기 전에 우선 이 함수의 매개변수로 넘겨지는 인자를 평가한다.

여기서 첫 번째 인자는 두 부분으로 나누어져 있는데 이 것이 문제의 원인이 될 수 있다.

 

첫 번째 인자가 나뉘는 두 부분.

 

 1. "new Widget" 표현식을 실행하는 부분.

 2. tr1::shared_ptr 생성자를 호출하는 부분.

 

그러므로 processWidget 함수 호출이 이루어지기 전에 컴파일러는 세 가지 연산 코드를 만들어야 한다.

 

 1. Calculate 함수 호출.

 2. "new Widget"을 실행.

 3. tr1::shared_ptr 생성자 호출.

 

그런데 문제는 각 각의 연산이 실행되는 순서가 컴파일러 제작사마다 다르다는 점이다.

자바 및 C#은 매개변수의 평가 순서가 특정하게 고정되어 있는 반면에 C++의 평가 순서는 높은 자유도를 가진다.

그래서 어떤 컴파일러의 경우에는 아래와 같은 순서로 진행될 수 있다.

 

 1. "new Widget"을 실행.

 2. Calculate 함수 호출.

 3. tr1::shared_ptr 생성자 호출.

 

만약 이러한 호출 과정을 진행하는 도중에, Calculate 함수에서 예외가 발생한다면 "new Widget'으로 만들어졌던 포인터가

스마트 포인터에 저장이 되지 않는 문제가 발생해 버린다.

즉, processWidget 호출 중에 자원이 누출될 가능성이 있는 이유는, 자원이 생성되는 시점("new Widget")과 새성한 자원이

자원 관리 객체로 넘어가는 시점 사이에 예외가 끼어들 수 있기 때문이다.

 

해결 방법

 

 - Widget을 생성해서 스마트 포인터에 저장하는 코드를 별도의 문장 하나로 만들고, 그 스마트 포인터를 ProcesWidget에

   넘기는 방법이다.

 

1
2
3
4
5
6
7
8
void fucn()
{
    std::tr1::shared_ptr<Widget> pw(new Widget); //new Widget으로 생성된 포인터를
                                                 //스마트 포인터에 저장하는
                                                 //별도의 문장.
 
    processWidget(pw, Calculate());
}
cs

 

1줄 요약

 

 - new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만들자.
   이것이 안되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있다.

1
2
3
4
5
std::string* stringArray = new std::string[100];
 
...
 
delete stringArray;
cs

 

위 코드를 보면 stringArray가 가리키는 100개의 string 객체들 가운데 99개는 정상적인 소멸 과정을 거치지 못할 것이다.

 

객체 배열 해제 시에는 []delete를 해야만 하는 이유를 알아보자.

 

new 연사자를 사용하면 다음과 같은 2가지 내부 동작을 진행한다.


 1. 메모리 할당. (operator new라는 이름의 함수가 쓰임.)
 2. 할당된 메모리에 대해 한 개 이상의 생성자가 호출.

 

이와 마찬가지로 delete 연산자를 사용하면 2가지 내부 동작을 진행한다.

 

 1. 할당된 메모리에 대해 한 개 이상의 소멸자가 호출.
 2. 메모리 해제. (operator delete라는 이름의 함수가 쓰임.)

 

여기서 단일 객체를 new / delete 하는 경우와 객체 배열을 new/delete 하는 경우의 메모리 배치구조는 서로 다르다.

 

단일 객체의 힙 메모리 배치 구조.

 

Object

           

 

객체 배열의 힙 메모리 배치 구조.

 

6

Object

Object 

Object 

Object 

Object 

Object 

 

상기와 같이 대다수의 컴파일러의 경우에는 객체 배열이 힙 메모리에 할당되면 맨 앞에 배열의 크기 정보가 함께 배치된다.

이 때문에, delete 연산자는 소멸자가 몇 번 호출될지를 쉽게 알 수 있게된다.

즉, []delete는 앞쪽의 배열 크기를 읽고, 배열 크기에 해당하는 횟수만큼 소멸자를 호출하게 된다.


만약 delete를 사용하게되면 그냥 단일 객체라 간주하고 소멸자 호출 횟수가 부족해져 메모리 누수가 발생할 것이다.

 

 

1
2
3
std::string stringObjec = new std::string//단일 객체.
 
[]delete stringObjec; //단일 객체를 []delete로 해제.
cs

 

반대로 위와 같이 단일 객체를 해제하려고 할 때, []delete 연산자를 사용한다면... []delete 연산자는 stringObject 객체 메모리 앞쪽의

몇 바이트를 읽어 배열 크기라고 해석할 것이다. 그리고 배열 크기만큼 소멸자를 호출할텐데 결국 엉뚱한 메모리를 해제하려 할테고

알 수 없는 동작이 진행될 것이다.

 

그러므로 new 연산자에 []를 썼으면, delete 연산자에도 []를 써야 한다는 간단한 규칙만 생각해두면 된다.

 

 

1
2
3
4
5
6
7
typedef std::string stringArray[4];
 
std::string *pa1 = new stringArray; //new stringArray이 new string[4]라는 점을 잊으면 안된다.
 
...
 
delete[] pa1;
cs

 

또한 typedef로 정의된 어떤 타입의 배열을 생성하려고 new를 썼다면 delete 또한 []를 잊지 말아야 한다.

위의 예제와 같이 typedef를 사용한 경우도 결국 배열이다.
그러나 저렇게 배열 타입이 눈에 명확하게 보이지 않기 때문에, 배열 타입을 typedef 타입으로 만들지 않는 것이 좋다.

 

1줄 요약


 - new 표현식에 []를 썼으면, 대응되는 delete 표현식에도 []를 쓰자.
   마찬가지로 new 표현식에 []를 안 썼으면, 대응되는 delete 표현식에도 []를 쓰지 말자.

자원 관리 클래스는 실수로 터질 수 있는 자원 누출을 튼튼히 막아 주는 보호벽 역할을 해준다.
그러나 이미 많이 사용하고 있는 API들이 직접 참조하도록 만들어져 있어서, 자원 관리 객체의 보호벽을 슬그머니 넘어가

실제 자원을 직접 조작해야 할 일이 있다.

 

13장에서 작성했던 Investment 예제를 사용해서 shared_ptr을 통해 Investment 객체를 가리킨다고 하자.

 

1
2
3
4
5
6
7
8
9
10
11
int dayHeld(const Investment *pi)
{
    ...
}
 
void func()
{
    std::tr1::shared_ptr<Investment> pInv(createInvesment());
 
    int days = dayHeld(pInv);    //컴파일 에러 발생.
}
cs

 

위와 같이 dayHeld라는 Investment* 타입의 매개변수를 가진 함수에 스마트 포인터 객체를 전달한다면 컴파일 에러가 발생한다.

이유는 dayHeld 함수는 Investment* 타입의 실제 포인터를 원하는데, tr1::shared_ptr<Investment> 타입의 객체를 넘겨주었기

때문이다. 

사정이 이렇다 보니, RAII 클래스(shared_ptr)의 객체를 그 객체가 감싸고 있는 실제 자원(Investment*)으로 변환해주어야 한다.

 

변환할 방법으로는 명시적 변환(explicit conversion)과 암시적 변환(implicit convension)이 있다.

tr1::shared_ptr과 auto_ptr은 명시적 변환을 수행하는데 get이라는 멤버 함수를 통해 실제 자원을 얻어낼 수 있다.

 

1
    int days = dayHeld(pInv.get());
cs

 

잘 설계된 스마트 포인터 클래스라면 거의 모두가 그렇듯, 포인터 역참조 연산자(operator-> 및 operator*)를 오버로딩

하고 있다. 따라서 스마트 포인터 클래스가 자신이 관리하는 실제 포인터에 대한 암시적 변환도 쉽게 가능하게 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CTest
{
public:
    void PRINT(std::string strText)
    {
        printf("%s\n",strText.c_str());
    }
};
 
CTest* CreateTest()
{
    return new CTest();
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::tr1::shared_ptr<CTest> spTest(CreateTest());
    std::auto_ptr<CTest> apTest(CreateTest());
 
    //shared_ptr
    spTest->PRINT("AAAA");
    (*spTest).PRINT("BBBB");
 
    //auto_ptr
    apTest->PRINT("CCCC");
    (*apTest).PRINT("DDDD");
 
    return 0;
}
 
cs

 

RAII 객체 안에 들어 있는 실제 자원에 대한 접근을 매끄럽게 할 수 있도록 암시적 변환을 제공하는 설계자들도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FontHandle getFont();            //C API에서 가져온 함수, 매개변수는 생략.
 
void releaseFont(FontHandle fh); //C API에서 가져온 함수.
 
class Font
{
public:
    explicit Font(FontHandle fh) //자원 획득. 값에 의한 전달이 수행되는 것은
    : f(fh)                      //자원 해제를 C API로 하기 때문.
    {
 
    }
    ~Font()
    {
        releaseFont(f);
    }
 
private:
    FontHandle f; //실제 Font 자원

 };

cs

 

FontHand의 규모가 무척 크다고 가정하면, Font 객체를 FontHandle로 변환해야 할 경우도 무척 많을 것이다.

그래서 Font 클래스에서는 이를 위한 명시적 변환 함수로 get을 제공하도록 만들 수 있다.

 

<명시적 형변환 추가.>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FontHandle getFont();        //C API에서 가져온 함수, 매개변수는 생략.
 
void releaseFont(FontHandle fh); //C API에서 가져온 함수.
 
class Font
{
    ...
    FontHandle get() const //명시적 변환.
    {
        return f;
    }
private:
    FontHandle f;
};
 
void changeFontSize(FontHandle f, int newsize); //폰트 API의 일부

 

 

//Form Class
int func()
{
    Font font(getFont());
    int newFontSize;
 
    ...
 
    changeFontSize(f.get(), newFontSize);    //Font에서 FontHandle로 명시적 변환.
}
cs

 

그런데 저렇게 자주 변형을 해줘야하는 경우에 사용자는 변환할 때마다 get을 호출해줘야 하기 때문에 짜증이나 Font 클래스를

사용하지 않게 될 수 있다. 그래서 FontHandle로의 암시적 변환 함수를 Font에서 제공하도록 한다.

 

<암시적 형변환 추가>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FontHandle getFont();        //C API에서 가져온 함수, 매개변수는 생략.
 
void releaseFont(FontHandle fh); //C API에서 가져온 함수.
 
class Font
{
    ...
    
    operator FontHandle() const //암시적 변환 함수.
    {
        return f;
    }
private:
    FontHandle f;
};
 
void changeFontSize(FontHandle f, int newsize); //폰트 API의 일부
 
//Form Class
 
int func()
{
    Font font(getFont());
    int newFontSize;
 
    ...
 
    changeFontSize(f, newFontSize);    //Font에서 FontHandle로 암시적 변환.
}
cs

 

암시적으로 변환이 되기 때문에 매끄럽게 사용하기는 좋지만 진짜 Font를 쓰려고 한 부분에서 원하지 않게 FontHandle로 바뀔 수 있다.

 

1
2
3
4
5
6
7
8
9
10
int func1()
{
    Font f1(getFont());
 
    ...
 
    FontHandle f2 = f1; //여기서 문제 발생.
                        //원래 의도는 Font 객체를 복사하는 것이었는데
                        //엉뚱하게도 f1이 FontHandle로 바뀌고 나서 복사됨.
}
cs

 

이렇게 되면 Font 객체인 f1이 관리하고 있는 Fonthandle이 f2를 통해서도 직접 사용할 수 있는 상태가 된다.

하나의 자원이 양다리를 걸치고 있는 상황은 좋지 않다. 특히 f1이 소멸되버리면 f2는 해제된 폰트를 가리키고 있는 꼴이 된다.

 

RAII 클래스를 실제 자원으로 바꾸는 방법은 특정한 용도와 사용 환경에 따라 달라지는데, 어쨌든 가장 잘 설계된 클래스라면 뒤에서 배울

맞게 쓰기에는 쉽고, 틀리게 쓰기에는 어려워야 한다.

항상 그런것은 아니지만 명시적 변환을 제공하는 쪽이 나을 때가 많다. 그러나 암시적 변환에서 생기는 사용 시의 자연스러움이 빛을 발하는

경우도 있다.

 

2줄 요약

 

 - 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을

   열어주어야 한다.

 

 - 자원 접근은 명시적 변환과 암시적 변환이 있는데, 안정성만 따진다면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면

   암시적 변환이 괜찮다. (즉, 스스로 잘 판단해서 만들어라.)

힙 기반 자원에 대해서 auto_ptr과 tr1::shared_ptr 클래스로 관리하는 내용을 알아봤었다.
그러나 모든 자원은 힙에서 생기지 않고 힙에 생기지 않는 자원을 스마트 포인터로 처리해주는 것 또한 말이 되지 않는다.
항상 그런 것은 아니지만 자원 관리 클래스를 스스로 만들어야 할 필요를 느끼는 경우들이 있다.

 

예를 들어 Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용하는 중이라고 가정해보자.
C API에서 제공하는 함수 중엔 lock 및 unlock이 있다.

 

1
2
3
void lock(Mutex* pm);  //pm이 가리키는 뮤텍스에 잠금을 건다.
 
void unlock(Mutex* pm); //pm이 가리키는 뮤테스의 잠금을 푼다.
cs

 

그런데 뮤텍스 잠금을 관리하는 클래스를 하나 만들고 싶은데 이는 뮤텍스 잠금을 잊지 않고 풀어 주고싶기 때문이다.
이런 용도의 클래스는 기본적으로 RAII 버칙을 따라 구성한다. 즉, 생성 시에 자원을 획득하고 소멸 시에 자원을 해제하는 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Lock
{
public:
    explicit Lock(Mutex* pm) : m_pMutex(pm)
    {
        lock(m_pMutex); //자원 획득
    }
 
    ~Lock()
    {
        unlock(m_pMutex); //자원 해제
    }
 
private:
    Mutex* m_pMutex;
};
 
cs

 

사용자는 이제 Lock을 사용할 때 RAII 방식에 맞춰 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void Func()
{
    Mutex m;        //뮤텍스 정의
 
    ...
 
    {                //임계 영역을 정하기 위해 블록 생성
        Lock(&m);    //뮤텍스 잠금
 
        ...
    }                //Block이 끝나면서 뮤텍스에 걸렸던 잠금이 자동으로 풀림.
                    
}
cs

 

여기까지 보면 문제가 없을 것 같지만 만약 Lock 객체가 복사가 된다면 어떤 동작이 이루어져야 할지 생각해봐야 한다.

 

1
2
Lock m1(&m); //m에 잠금을 건다.
Lock m2(m1); //m1을 m2로 복사한다.
cs

 

선택 1 : 복사를 금지한다.

 

 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안되는 경우가 꽤 많다.
 앞에 항목6에서 했던 것처럼 복사 연산을 private 멤버로 만들어준다.

 

선택 2 : 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.

 

 자원을 사용하고 있는 마지막 객체가 소멸될 때까지 자원을 해제시키지 않아야하는 경우도 종종 있다.
 그렇다면 자신의 자원 관리 클래스에 참조 카운팅 방식의 복사 동작을 넣고 싶을 때, tr1::shared_ptr을 데이터 멤버로 넣으면

 간단히 해결될 것이다. 즉, Lock이 참조 카운팅 방식으로 돌아가면 좋을 것 같다고 생각하여 m_pMutex의 타입을 Mutex*에서

 tr1::shared_ptr<Mutex>로 바꾸라는 것이다. 그러나 아쉽게도 tr1::shared_ptr은 참조 카운트가 0이 될 때 자신이 가리키는 객체를

 삭제해 버리도록 되어있다. 내가 원한 것은 Mutex를 다 썼을 때 잠금만 해제하는 것이지 삭제까지는 하고 싶은 것은 아니였다.

 

 다행히도 tr1::shared_ptr이 '삭제자' 지정을 허용한다는 점이다. 삭제자란, tr1::shared_ptr이 유지하는 참조 카운트가 0이 되었을 때

 호출되는 함수 혹은 함수 객체를 말한다. (auto_ptr은 삭제자 기능 없음.)

 삭제자는 tr1::shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣어 줄 수 있다.

 그래서 하기와 같은 클래스 형태로 변경될 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void lock(std::_Mutex* pm)
{
    pm->_Lock();
}
 
void unlock(std::_Mutex* pm)
{
    pm->_Unlock();
}
 
class Lock
{
public:
    explicit Lock(std::_Mutex* pm) : m_pMutex(pm, unlock) //삭제자로 unlock 함수 설정.
    {
        lock(m_pMutex.get()); //get에 대한 것은 다음 항목에서 설명.
    }
 
private:
    std::tr1::shared_ptr<std::_Mutex> m_pMutex; //shared_ptr 사용.
};
 
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::_Mutex mutex;
 
    {
        Lock oLock(&mutex);
 
        //현재 Block 동안에는 Lock이 걸려져 있다.
    }
 
    return 0;
}
cs

 

 여기서 특이한 점은 Lock 클래스의 소멸자가 없어졌다는 점이다. 이유는 필요 없기 때문이다.

 항목5에서 얘기했듯이 클래스의 소멸자는 비정적 멤버 변수들의 소멸자를 자동으로 호출해준다.

 m_pMutex 스마트 포인터도 비정적 멤버 변수이기 때문에 Lock 객체가 소멸될 때 자동으로 소멸이 된다.

 

 즉, m_pMutex는 참조 카운트가 0이 되면 소멸되는 것이 아니라 삭제자로 지정된 unlock 함수를 호출하여

 Mutex 잠금을 해제하고 나서 해당 Lock 객체가 소멸될 때에 비로서 Mutex 멤버 변수는 해제되는 것이다.

 

 여기서 주의할 점

 만약 외부에서 다른 shared_ptr 포인터가 mutex를 참조하고 있다면 어떻게 되는 것일까??

 하기 주석 참고...

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int _tmain(int argc, _TCHAR* argv[])
{
    std::_Mutex mutex;
    std::tr1::shared_ptr<std::_Mutex> pMutex;
 
    {
        Lock oLock(&mutex); //Lock 동작.
 
        pMutex = oLock.GetPtr(); //oLock 객체의 스마트 포인터 반환. (mutex를 가리키고 있는 스마트 포인터)
                                 //이 구간에서 pMutex와 oLock의 m_pMutex 2개의 스마트 포인터가 mutex를 가리키고 있다.
    
    }                            //Block이 종료되면서 oLock 객체는 소멸되기 때문에 자동적으로 m_pMutex 또한 소멸된다.
                                 //그런데... 아직 pMutex는 mutex를 가리키고 있는 중이다. (즉, mutex를 pMutex만 가리키게 됨) 
                                 //Lock 클래스에서 m_pMutex의 삭제자로 지정한 unlock은 mutex를 가리키는 참조자가 0개여야지만
                                 //삭제자를 호출한다. 그러므로 이 구간이 종료되어 oLock 객체가 소멸되었지만 unlock 함수는 호출되지
                                 //않는다.
 
    ...                          //여러가지 처리 중....
 
    return 0;                     
 
}                                //이 부분에 도달해야 pMutex 또한 소멸되어진다. 그러므로 mutex를 가르키고 있는 객체는 0개가 되고
                                 //unlock 함수가 호출된다.
                                 //이렇게 되면 결국 lock 범위는 내가 생각한 것처럼 되지 않는다.
cs

 

선택 3 : 관리하고 있는 자원을 진짜로 복사한다.

 

 때에 따라서는 자원을 원하는 대로 복사할 수도 있다. 이때는 자원을 다 썼을 때 각각의 사본을 확실히 해제해줘야 한다.
 자원 관리 객체를 복사하면 이 객체가 관리하는 자원까지 모두 복사가 되어야 한다.
 즉, 깊은 복사를 수행해야 한다는 이야기이다.

 

선택 4 : 관리하고 있는 자원의 소유권을 옮긴다.

 

 흔한 경우는 아니지만, 특정원 자원에 대해 실제로 참조하는 RAII 객체가 딱 하나만 존재하도록 만들고 싶다면,

 객체가 복사될 때 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 한다. auto_ptr이 바로 이런 방식이다.

 

 

2줄 요약

 

 - RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의
   복사 동작이 결정된다.

 

 - RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것이다.

   하지만 이 외의 방법들도 가능하니 참고하자.

투자를 모델링해주는 클래스 라이브러리를 가지고 어떤 작업을 한다고 가정하자.

 

1
2
3
4
5
6
class Investment
{
    ...
};
 
Investment* createInvestment(); //팩토리 함수.
cs

 

createInvestment 함수를 통해 객체를 얻기 때문에 사용자가 직접 객체를 해제시켜줘야 한다.

 

1
2
3
4
5
6
7
8
9
void func()
{
    Investment *pInv = createInvestment(); //팩토리 함수를 통해 객체 생성.
 
    ...          //pInv를 가지고 이것 저것 처리.
 
    delete pInv; //반드시 사용자가 객체 해제.
}
 
cs

 

그러나 pInv를 가지고 처리를 하는 도중 문제가 발생하여 delete 부분에 도달하지 않고 함수를 빠져나가거나

사용자가 실수로 delete 구문을 빼먹을 수도 있다.
createInvestment 함수로 얻어낸 자원이 항상 해제되도록 만들 방법은 객체가 소멸될 때 소멸자에서 메모리를

해제시키는 방법이다.

 

표준 라이브러리를 보면 auto_ptr이라는 것이 있는데 auto_ptr은 포인터와 비슷하게 동작하는 객체(스마트 포인터)로서,

가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어 있다.

 

1
2
3
4
5
6
7
void func()
{
    std::auto_ptr<Investment> pInv(createInvestment()); //팩토리 함수를 통해 객체 생성.
 
    ...
}        //auto_ptr의 소멸자를 통해 pInv를 삭제한다.
        //그러므로 사용자가 직접 delete를 호출할 필요가 없다.
cs

 

자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징이 있다.


1. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.

 상기 예제를 보면, createInvestment 함수가 만들어준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화하는 데 쓰이고 있다.
 실제로, 이렇게 자원 관리에 객체를 사용하는 아이디어에 대한 용어도 자주 통용되는데 '자원 획득 즉 초기화' (RAII)라 부른다.

 

2. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.

 

 소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가

 제대로 이루어지게 된다.

 

auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 하기 때문에, auto_ptr은 오직 1개의 객체만

가리키는 것이 가능하다. 그래서 auto_ptr을 이용해서 객체를 복사하면 원본 객체는 null이 되버린다.

 

1
2
3
4
5
6
7
    std::auto_ptr<Investment> pInv1(createInvestment()); //팩토리 함수를 통해 객체 생성.
 
    std::auto_ptr<Investment> pInv2(pInv1); //pInv2는 pInv1이 가리키던 객체를 가리키고
                                            //pInv1은 null이 된다.
 
    pInv1 = pInv2;    //pInv1은 pInv2가 가리키던 객체를 가리키고
                    //pInv2는 null이 된다.
cs

 

이러한 특성때문에, STL 컨테이너의 경우에는 원소들이 '정상적인' 복사 동작을 가져야 하기 때문에, auto_ptr은 STL 컨테이너들의

원소로 허용되지 않는다.

 

auto_ptr을 쓸 수 없는 상황이라면, 그 대안으로 참조 카운팅 방식 스마트 포인터(RCSP)를 사용하면 좋다.

RCSP는 특정 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 해제시킨다.
마치 가비지 컬렉션과 비슷해보이지만, 참조 상태가 고리를 이루는 경우(예로 서로 다른 두 객체가 서로를 가리키는 경우)에는

자원을 해제시킬수 없다는 점은 가비지 컬렉션과 다르다.

 

1
2
3
4
5
6
7
8
void Func()
{
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());
 
    std::tr1::shared_ptr<Investment> pInv2(pInv1);
 
    pInv2 = pInv1;
}
cs

 

tr1::shared_ptr을 사용하기 위해서는 VS2008 sp1 이상이여야 하며, #include <Memory>를 선언해줘야 한다.

 

auto_ptr, tr1::shared_ptr은 자원 관리를 하는 몇 가지 방법들 중 하나일 뿐 중요한 점은 자원 관리 객체를 사용해서 자원을

관리하는 것이 중요하다는 것이다.

 

알아둬야 할 점이 한 가지 더 있다. auto_ptr 및 tr1:shared_ptr은 소멸자 내부에서 delete 연산자를 사용한다.

delete[] 연산자가 아니다!! 즉, 동적으로 할당된 배열에 대해 auto_ptr이나 tr1::shared_ptr을 사용하면 절대 안된다는 것이다.

문제는 컴파일 에러도 발생하지 않는다는 점이다.

C++ 표준 라이브러리에서는 동적 할당된 배열을 위한 스마트 포인터가 제공되지 않는다.

이유는 동적으로 할당된 배열은 이제 vertor 및 string으로 거의 대체할 수 있기 때문이다.

 

2줄 요약

 

 - 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.

 

 - 일반적으로 RAIi 클래스는 tr1::shared_ptr과 auto_ptr이 있다. tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에

   사용하기 더 좋다. auto_ptr은 복사되는 객체를 null로 만들어 버린다.

 

 

 

 

캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있다.

복사 생성자와 복사 대입 연산자인데 이 둘을 통틀어 복사 함수라고 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void logCall(const std::string& funcName);
 
class Customer
{
public:
    ...
    Customer(const Customer& rhs);              //복사 생성자.
    Customer& operator=(const Customer& rhs); //복사 대입 연산자.
    ...
 
private:
    std::string name;
};
 
Customer::Customer(const Customer& rhs) : name(rhs.name)
{
    logCall("Customer copy construction");
}
 
Customer& Customer::operator=(const Customer& rhs)
{
    logCall("Customer copy assignment operator");
 
    name = rhs.name;
 
    return *this;
}
cs

 

문제될 것이 없는 코드이지만 멤버 변수 하나를 추가하면서 문제가 발생한다.
기존에 작성된 코드에서 멤버 변수가 추가됨에 따라 생성자, 복사생성자, 복사 대입 연산자 모두를 수정해야한다.
혹여나 여기서 하나라도 빼먹게되면 부분 복사가 되버리게 된다.
특히, 상속 관계에서는 더욱 더 신경써줘야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class priorityCustomer : public Customer
{
public:
    ...
    priorityCustomer(const priorityCustomer& rhs);
    priorityCustomer& operator=(const priorityCustomer& rhs);
    ...
private:
    int priority;
};
 
priorityCustomer::priorityCustomer(const priorityCustomer& rhs) : priority(rhs.priority)
{
    logCall("priorityCustomer copy constructor");
}
 
priorityCustomer& priorityCustomer::operator =(const priorityCustomer& rhs)
{
    logCall("priorityCustomer copy assignment operator");
 
    priority = rhs.priority;
 
    return *this;
}
cs

 

겉으로 보기에는 모든 정보를 복사하고 있는 것처럼 보이지만, 그렇지 않다.
PriorityCustomer에 선언된 데이터는 모두 복사하고 있는 것은 맞지만, 부모 class인 Customer로부터 상속한 데이터 멤버들은

복사가 되지 않고 있다.
Customer의 데이터들도 엄연히 파생클래스인 PriorityCustomer에 들어있기 때문에 Customer의 데이터도 반드시 모두 복사해줘야 한다.
PriorityCustomer의 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시되어 있지 않아서 PriorityCustomer형 객체의 Customer 부분은

인자없이 실행되는 Customer 기본 생성자에 의해 기본적인 초기화만 된다.


또한 PriorityCustomer의 복사 대입 연산자의 경우에는 기본 클래스의 데이터 멤버를 건드릴 시도도 하지 않기 때문에,

기본 클래스의 데이터 멤버는 변경되지 않고 그래로 있게 된다.


즉, 파생 클래스에 대한 복사 함수를 직접 만든다면 기본 클래스 부분을 복사에서 빠뜨리지 않도록 주의해야 한다.
기본 클래스의 멤버 변수들은 private일 가능성이 높으므로 직접 접근하기 어렵다.


그러므로 파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출하도록 만들어서 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class priorityCustomer : public Customer
{
public:
    ...
    priorityCustomer(const priorityCustomer& rhs);
    priorityCustomer& operator=(const priorityCustomer& rhs);
    ...
private:
    int priority;
};
 
priorityCustomer::priorityCustomer(const priorityCustomer& rhs)
Customer(rhs),    //기본 클래스의 복사 생성자를 호출한다.
  priority(rhs.priority)
{
    logCall("priorityCustomer copy constructor");
}
 
priorityCustomer& priorityCustomer::operator =(const priorityCustomer& rhs)
{
    Customer::operator=(rhs);  //기본 클래스 부분을 대입한다.
 
    logCall("priorityCustomer copy assignment operator");
 
    priority = rhs.priority;
 
    return *this;
}
cs

 

또한 복사 생성자와 복사 대입 연산자의 동작은 비슷하므로 함수 본문 또한 비슷하다.
그래서 아마도 코드 중복을 피하려고 한쪽에서 다른 한쪽을 호출하는 것을 생각할 수 있는데…. 절대 그래선 안된다.
(대입 연산자에서 복사생성자를 호출한다던지 복사생성자에서 대입 연사자를 호출한다던지)

복사 대입 연산자는 이미 생성된 객체에서만 사용이 가능한데 복사 대입 연산자에서 복사 생성자를 호출하면 이미 존재하는

객체를 또 만들려는 동작이 되버린다. (이미 태어난 아이에게 엄마 뱃속으로 들어가라는 꼴임)

헷갈릴까봐 적는거지만 생성과 동시에 = 를 이용해서 대입하면 복사 대입 연산자가 아닌 복사 생성자가 호출된다.

즉, 복사 대입 연산자는 이미 생성된 객체에서만 사용 가능하다는 의미이다.

 

거꾸로 복사 생성자에서 복사 대입 연산자를 호출한다는 것 또한 말이 되지 않는다.

대입 연산자의 의미는 이미 초기화가 끝난 객체에게 값을 주는 것인데 아직 초기화가 완료되지 않은 객체에 대입을 하게되므로

프로그램이 죽을 수도 있다.

 

하고 싶다면 별도의 private 함수를 만들어서 각 함수에서 호출하도록 하자.

 

2줄 요약

 

 - 객체 복사 함수는 주어진 객체의 모든 데이터 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해라.

 

 - 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대하지 마라.

 

+ Recent posts