E D R , A S I H C RSS

EffectiveC++ (rev. 1.1)

EffectiveC++

작성자: 김영현(erunc0)

Effective C++ 요약

Contents

1. Shifting from C to C++
1.1. Item1: Prefer const and inline to #define
1.2. Item 2: Prefer iostream to stdio.h
1.3. Item 3: Prefer new and delete to malloc and free
1.4. Item 4: Prefer C++-style comments
2. Memory management
2.1. Item 5: Use the same form in corresponding uses of new and delete
2.2. Item 6: Use delete on pointer members in destructors
2.3. Item 7: Be prepared for out-of-memory conditions.
2.4. Item 8: Adhere to convention when writing operator new and operator delete
2.5. Item 9: Avoid hiding the "normal" form of new
2.6. Item 10: Write operator delete if you write operator new
3. Constructors, Destructors, and Assignment Operators (클래스에 관한것들.)
3.1. Item 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory
3.2. Item 12: Prefer initialization to assignment in constructors
3.3. Item 13: List members in an initialization list in the order in which they are declared
3.4. Item 14: Make sure base classes have virtual destructors
3.5. Item 15: Have operator= return a reference to *this
3.6. Item 16: Assign to all data members in operator=
3.7. Item 17: Check for assignment to self in operator=
4. Classes and Functions: Design and Declaration
4.1. Item 18. 최소한의 완전한 클래스 인터페이스를 추구한다.
4.2. Item 19. 멤버 함수, 비멤버 함수 및 프렌드 함수를 구별한다.
4.3. Item 20. 데이터 멤버를 공용(public) 인터페이스에 포함시키지 않는다.
4.4. Item 21. 가능한 const를 이용한다.
4.5. 항목 22. 값에 의한 호출보다는 레퍼런스에 의한 호출을 선호한다.
4.6. 항목 23. 객체 반환시 레퍼런스를 반환하지 않는다.
4.7. 항목 24. 함수 오버로딩과 디폴트 인자값 중에서 주의깊게 선택한다.
4.8. 항목 25. 포인터나 수치형 타입상의 오버로딩을 피한다.
4.9. 항목 26. 잠재적 모호성을 경계한다.
4.10. 항목 27. 의도하지 않은 내부 생성 멤버 함수의 이용을 명시적으로 막는다.
4.11. 항목 28. 전역 네임스페이스를 분할한다.
5. 클래스와 함수 : 구현
5.1. 항목 29. 내부 데이터에 대한 "핸들"을 리턴하는 것을 피해라.
5.2. 항목 30. 접근하기 어려운 멤버에 대한 비상수 포인터나 레퍼런스를 리턴하는 멤버 함수 사용을 피하라.
5.3. 항목 31. 지역 객체에 대한 참조나 함수 내에서 new를 이용해 초기화된 포인터를 가리키는 참조를 리턴하지 말라.
5.4. 항목 32. 변수 정의는 가능한 뒤로 늦춰라.
5.5. 항목 33. 인라인을 선별적으로 사용하라.
5.6. 항목 34. 파일간의 컴파일 의존성(dependency)을 최소화하라.
6. 인스턴스와 객체지향 설계
6.1. 항목 35. public 계승이 "isa"를 모델링하도록 하라.
6.2. 항목 36. 인터페이스 계승과 구현 계승의 차이점을 이해하라.
6.3. 항목 37. 계승된 비가상 함수를 재정의하지 않도록 한다.
6.4. 항목 38. 계승된 부재 인자값을 재정의하지 않도록 한다.
6.5. 항목 39. 계층도의 아래쪽 클래스를 다운캐스트(downcast)하지 않도록 한다.
6.6. 항목 40. 레이어링(layering)을 통해 "가지고 있는" 것과 "사용하여 구현된" 것을 모델링하도록 하자.
6.7. 항목 41. 계승과 템플릿과의 차이점을 이해한다.
6.8. 항목 42. private 계승을 바르게 사용하라.
6.9. 항목 43. 다중 계승을 바르게 사용하도록 하라.
6.10. 항목 44. 의미하는 바를 표현하도록 하라. 자신이 표현한 것의 의미를 이해하도록 하라.
7. 미묘한 부분
7.1. 항목 45. C++가 은밀하게 어떤 함수를 만들어주고 호출하는지 이해하기
7.2. 항목 46. 실행 시간 에러보다는 컴파일 시간과 링크 시간 에러가 좋다.
7.3. 항목 47. 비지역 정적(Non-local static) 객체는 사용되기 전에 초기화되도록 해야 한다.
7.4. 항목 48. 컴파일러의 경고(Warning)에 주의를 기울여라.
7.5. 항목 49. 표준 라이브러리를 잘 알아두자.
7.6. 항목 50. C++에 대한 이해를 넓혀라.
8. Thread

1. Shifting from C to C++

1.1. Item1: Prefer const and inline to #define

preprocessor(전처리기)보다는 compiler를 선호한다는 뜻.

DeleteMe #define(preprocessor)문에 대해 const와 inline을(compile)의 이용을 추천한다. --상민

-> const

~cpp 
   #define ASPECT_RATIO 1.653
ASPECT_RATIO는 소스코드가 컴파일로 들어가기 전에 전처리기에 의해 제거된다.

instead of upper..


define 된 ASPECT_RATIO 란 상수는 1.653으로 변경되기때문에 컴파일러는 ASPECT_RATIO 란것이 있다는 것을 모르고 symbol table 에?들어가지 않는다. 이는 debugging을 할때 문제가 발생할 수 있다. -인택
~cpp 
   const double ASPECT_RATIO = 1.653
책에서 언급한 두가지.

~cpp 
1. 상수 포인터(constant pointer)를 정의하기가 다소 까다로워 진다는 것.
   - ex -   
   const char * const authorName = "Scott Meyers";

2. 상수의 영역을 클래스로 제한하기 위해선 상수를 멤버로 만들어야 하며
   그 상수에 대한 단 한개의 복사본이 있다는 것을 확신하기 위해서 static으로
   멤버 변수를 만들어야 한다.
   - ex -
   // header.
   class GamePlayer 
   {
   private:
         static const int NUM_TURNS = 5;   // 상수 선언! (선언만 한것임)
         int scores[NUM_TURNS];            // 상수의 사용.
   }

   // source file
   ...
   const int GamePlayer::NUM_TURNS;        // 정의를 꼭해주어야 한다.
   ...


#define -> inline (매크로 사용시)
  • inline: 함수 호출로 인한 오버헤드를 줄일수 있는.. 거시기. 궁금하면 책찾아보세요.

~cpp 
   - ex -
   #define max(a,b) ((a) > (b) ? (a) : (b))
       // 매크로 작성시에는 언제나 매크로 몸체의 모든 인자들을 괄호로 묶어 주어야 한다.
       // 왜인지는 다들 알것이다. 
   
   // #define 을 inline으로..
   inline int max(int a, int b) { return a > b ? a : b; } // int형으로만 제한 되어있네.. 

   // template으로
   template class<T>
   inline const T& max (const T& a, const T& b) { return a > b ? a : b; }
const와 inline을 쓰자는 얘기였습니다. --; 왜 그런지는 아시는 분께서 글좀 남기시구요. ^^

#define 문을 const와 inline으로 대체해서 써도, #ifdef/#ifndef - #endif 등.. 이와 유사한 것들은

아직까지 유용하게 사용되므로, 안쓸필요는 없겠죠?

매크로는 말 그대로 치환이기 때문에 버그 발생할 확률이 높음. 상수선언이나 함수선언같은 경우는 가급적 const 나 inline으로 대체하는게 좋겠지. (으.. 그래도 실제로 짤때는 상수 선언할때는 #define 남용 경향이..

그럼.. 항목1 end.

횡설수설.

1.2. Item 2: Prefer iostream to stdio.h

scaf/printf -> cin/cout

1.3. Item 3: Prefer new and delete to malloc and free

* malloc & free
  • 생성자(constructor)와 소멸자(destructor)의 존재를 모른다.
* new & delete
  • 생성자 및 소멸자와 적절히 상호동작하기 때문에. they are clearly the superior choice.

1.4. Item 4: Prefer C++-style comments

/* */: C style의 주석

// : C++ style의 주석

자기만의 주석을 쓰면 되는거 아니야? 난. 주석다는게 제일 싫은데. ^^;

몇달 지난 프로그램은 자기가 만든게 아닌거야!? , 예전에 상민이 형이 얘기해준.. --;; ㅎㅎㅎ 동감..

2. Memory management

메모리를 올바로 얻는 것과 그것을 효율적으로 수행하게 만드는것(?)에 관한 얘기들.

2.1. Item 5: Use the same form in corresponding uses of new and delete

~cpp 
string *stringArray = new string[100];
...
delete stringArray;   // delete를 잘못 써주었습니다.
// stringArray에 의해 가르켜진 100개의 string object들중에 99개는 제대로 제거가 안됨.
  • new를 호출할때 []를 이용했다면 delete의 호출시에도 []를이 용한다. 간단간단!
typedef를 사용했을때의 delete.. --? : 새로운 문제 제기
~cpp 
typedef string AddressLines[4];      // 개인 주소는 4개의 줄을 차지하고
                                     // 각각이 스트링이다.
...
string *pal = new AddressLines;      // "new AddressLines" returns a string *, just "new string[4]".. 
...
delete pal;     // 어떻게 될지 몰라~
delete [] pal;  // fine.
이런 혼란(?)을 피하기 위해선 배열 타입들에 대한 typedef를 피하면 되지뭐. ^^

2.2. Item 6: Use delete on pointer members in destructors

동적 메모리 할당을 행하는 클래스들은 메모리를 할당하기 위해서 생성자에 new를 쓴다. (CString class같은것들?)

나중에 메모리를 해제하기 위해서 소멸자에서 delete를 사용한다.

그러나, 나중에 이렇게 만든 클래스를 누군가가 개선될경우 그리고, 개선된 클래스에서 포인터 멤버를 추가하게

된다면 밑의 세가지를 숙지하길 바란다.
  • Initialization of the pointer in each of the constructors. If no memory is to be allocated to the pointer in a particular constructor, the pointer should be initialized to 0 (i.e., the null pointer). - 생성자 각각에서 포인터 초기화
  • Deletion of the existing memory and assignment of new memory in the assignment operator. - 포인터 멤버에 다시 메모리를 할당할 경우 기존의 메모리 해제와 새로운 메모리의 할당
  • Deletion of the pointer in the destructor. - 소멸자에서 포인터 삭제
위의 세가지중 처음의 2가지는 제대로 안해주면 바로바로 눈에 띄이기 때문에 괜찮지만,

세번째 소멸자에서 포인터 삭제에 관한 것을 제대로 안해주면 메모리 유출(memory leak)으로 그냥 처리되기 때문에 클래스에 포인터 멤버를 추가할 때마다 반드시 명심해야 한다.

2.3. Item 7: Be prepared for out-of-memory conditions.

메모리가 부족할경우 적절한 처리를 해준다는 얘기인것 같은데...

잘모르겠음. 아시는 분의 설명이 매우 필요함

Comment 부탁해요

메모리 부족시에 대한 예외처리를 대비해두어라 정도면 적당할 것 같은데.


set_new_handler를 이용한 memory 할당 실패처리.

~cpp 
typedef void (* new_handler) {}; // 함수 pointer
new_handler set_new_handler (new_handler p) throw ();
...
// 연산자 new가 충분한 메모리를 할당하지 못할 경우 호출될 함수
void noMoreMemory ()
{
    cerr << "Unable to satisfy request for memory\n";
    abort ();
}
...
void main ()
{
    set_new_handler (noMoreMemory);
    int *pVigdataArray = new int [100000000]; // 100000000개의 정수공간을 할당할 수 없다면 noMoreMemory가 호출.
    ...
}


그리고, class내 에서 operator new와 set_new_handler를 정해 줌으로써 해당 class만의 독특(?)한

동작을 구현할 수 있다.
~cpp 
class X {
public:
  static new_handler set_new_handler(new_handler p);
  static void * operator new(size_t size);
private:
  static new_handler currentHandler;
};
...
// source file (??.cpp)

new_handler X::currentHandler;      // sets currentHandler
                                    // to 0 (i.e., null) by
                                    // default
new_handler X::set_new_handler(new_handler p)
{
  new_handler oldHandler = currentHandler;
  currentHandler = p;
  return oldHandler;
}

void * X::operator new(size_t size)
{
  new_handler globalHandler =                // install X's
    std::set_new_handler(currentHandler);    // handler
  void *memory;
  try {                                      // attempt
    memory = ::operator new(size);           // allocation
  }
  catch (std::bad_alloc&) {                  // restore
    std::set_new_handler(globalHandler);     // handler;
    throw;                                   // propagate
  }                                          // exception

  std::set_new_handler(globalHandler);       // restore
                                             // handler
  return memory;
}
...
void noMoreMemory();                           // decl. of function to
                                               // call if memory allocation
                                               // for X objects fails
...
X::set_new_handler(noMoreMemory);
                                               // set noMoreMemory as X's
                                               // new-handling function

X *px1 = new X;                                // if memory allocation
                                               // fails, call noMoreMemory

string *ps = new string;                       // if memory allocation
                                               // fails, call the global
                                               // new-handling function
                                               // (if there is one)

X::set_new_handler(0);                         // set the X-specific
                                               // new-handling function
                                               // to nothing (i.e., null)

X *px2 = new X;                                // if memory allocation
                                               // fails, throw an exception
                                               // immediately. (There is
                                               // no new-handling function
                                               // for class X.)

내 생각에는 이런게 있다라고만 알아두면 좋을것 같다. --;

2.4. Item 8: Adhere to convention when writing operator new and operator delete

operator new 와 operator delete 의 작성시 따라야 할것들.

''- 할당 루틴들이 new 핸들러 함수(memory 할당시 예외 처리같은 거들) 를 지원하고

크기가 0인 요구들을 올바로 처리할 수 있다.

- 할당해제 루틴들이 널 포인터에 대처할 수 있다.
''



멤버가 아닌 operator new
~cpp 
// operator new
void * operator new (size_t size)
{
if (size == 0) {                      // handle 0-byte requests
    size = 1;                           // by treating them as
  }                                     // 1-byte requests

  while (1) {

    // size bytes를 할당..

    if (the allocation was successful)
      return (a pointer to the memory);
    
    new_handler globalHandler = set_new_handler(0);
    set_new_handler(globalHandler);

    if (globalHandler) (*globalHandler)();
    else throw std::bad_alloc();
  }

}
operator new 가 하부 클래스로 상속된다면 어떻게 될까?

위의 메모리 할당 부분을 보면 size bytes만큼 메모리를 할당 하게 된다.

그런데, 이 클래스를 위해 만들어진 operator new 연산자가 상속될 경우.

상속받은 클래스내에 operator new연산자를 다시 재정의 해줘야 한다.

그럼 밑의 source를...
~cpp 
// in class
class Base {
public:
  static void * operator new(size_t size);
  ...
};

class Derived: public Base       // Derived doesn't declare
{ ... };                         // operator new

...
Derived *p = new Derived;        // calls Base::operator new!

// 만일 Base의 operator new가 이에 대처하기 위해 설계되지 않았다면 그를 
// 위한 최선의 방법은 다음과 같이 "잘못된" 양의 메모리를 요청하고 있는
// 호출들을 표준 operator new로 전달하는 것이다
void *Base::operator new (size_t size)
{
  if (size != sizeof (Base))      // size가 잘못 되었으면
    return ::operator new (size); // 요구를 처리한다

  ... // 그렇지 않으면 여기서 요구를 처리함
}
멤버가 아닌 operator delete
~cpp 
// operator delete
void operator delete(void *rawMemory)
{
  if (rawMemory == 0) return;    // do nothing if the null
                                 // pointer is being deleted

  // deallocate the memory pointed to by rawMemory;

  return;
}
이 연산자도 역시 상속될 경우 약간 골치아픈가?

이것 역시 메모리를 해제할 것의 size를 넣어서 해제하기 때문에.

operator new연산자처럼 잘(?) 처리 해주어야 한다.
~cpp 
// in class
class Base {                       // same as before, but now
public:                            // op. delete is declared
  static void * operator new(size_t size);
  static void operator delete(void *rawMemory, size_t size);
  ...
};

void Base::operator delete(void *rawMemory, size_t size)
{
  if (rawMemory == 0) return;      // check for null pointer

  if (size != sizeof(Base)) {      // if size is "wrong,"
    ::operator delete(rawMemory);  // have standard operator
    return;                        // delete handle the request
  }

  // deallocate the memory pointed to by rawMemory;

  return;
}

2.5. Item 9: Avoid hiding the "normal" form of new

간단. class 내에 operator new를 만들어 줄때.

~cpp 
class X {
public:
  void f();

  // new 핸들링 함수의 사양을 만족하는 연산자 new
  static void * operator new(size_t size, new_handler p);
};

void specialErrorHandler();     // definition is elsewhere

X *px1 =  new (specialErrorHandler) X; // calls X::operator new

X *px2 = new X;   // error!, "정상 form에 대해 호환이 이루어 지지않는 문제점."

위의 문제를 해결하기 위해.
~cpp 
class X 
{
public:
  void f();
  static void * operator new(size_t size, new_handler p);
  static void * operator new(size_t size) // normal form형식의 연산자도 만들어준다.
  { return ::operator new(size); }
};
X *px1 =
  new (specialErrorHandler) X;      // calls X::operator
                                    // new(size_t, new_handler)
X* px2 = new X;                     // calls X::operator
                                    // new(size_t)
or
~cpp 
class X 
{
public:
  void f();
  static
    void * operator new(size_t size,                
                        new_handler p = 0);         // default 값을 주어서 처리해준다
};
X *px1 = new (specialErrorHandler) X;               // ok
X* px2 = new X;                                     // ok
어떤 방법이든 상관 없지만, code를 약간이라도 덜치는 defaut 인자를 주는것이.. ㅡㅡ;; 하하

2.6. Item 10: Write operator delete if you write operator new

operator new 와 operator delete는 왜 쓸까?

효율성 때문이랍니다. 새로 작성해주는게 얼마나 큰 효율을 보이기에 default로 제공해주는 것을

쓰지 않는 것일까? --a 사실 몰랐는데, 일반 적은 new (default new연산자)를 사용하게 되면 할당된 블록의

크기를 나타내 주는 추가적인 정보를 같이 붙여 memory를 할당해 준다고 합니다. 그런데, operator new연산자를

직접만들어 주게되면 이런 추가 정보를 않붙여줘도 된다는 군요. 그러니까 추가 정보 크기만큼의 손실을 줄일 수

있다는 말이지요~ 한다마디로 효율성이 좋아졌다.(반면 ::operator new는 유연성이 좋다)

...


DeleteMe 그런 의미보다 String 이나, linked list 혹은 기타 여러 기타 데이터 형으로 많은 수의 할당을 통해서 쓸수 있는 인자의 경우에는 사용자 정의 new를 이용하여 가능하면 공용 메모리 공간에서 활동시켜서, 메모리 할당 코드를 줄이고 (메모리 할당의 new와 alloc는 성능에 많은 영향을 미칩니다.) 메모리를 줄이고 효율적 관리를 할수 있다는 의미 같습니다. 그런 데이터 형으로 쓰이는 인자가 아닌 한 app안에서 단 한번만 사용되는 클래스라면 구지 new를 성의해서 memory leak의 위험성을 증가 시키는 것보다, 일반적인 new와 생성자 파괴자의 규칙을 쓰는것이 좋을겁니다. --상민

3. Constructors, Destructors, and Assignment Operators (클래스에 관한것들.)

간과 하기 쉬운 생성자, 소멸자, 치환 연산자에 대한 얘기들.

3.1. Item 11: Declare a copy constructor and an assignment operator for classes with dynamically allocated memory

~cpp 
// 완벽하지 않은 String class
class String {
public:
  String(const char *value);
  ~String();
private:
  char *data;
};

String::String(const char *value)
{
  if (value) {
    data = new char[strlen(value) + 1];
    strcpy(data, value);
  }
  else {
    data = new char[1];
    *data = '\0';
  }
}

inline String::~String() { delete [] data; }
이 class에는 치환 연산자나 복사 생성자가 없다. 이런 클래스는 좋지 못한 결과를 발생시킨다.



객체 a의 포인터는 문자열 "Hello"를, 객체 b내의 포인터는 "World"문자열을 담고 있는 메모리를 가리킨다.

다음과 같은 치환연산을 하면..
~cpp 
b = a;
클래스 내에 operator=가 정의 되어 있지 않기 때문에, C++에서 default 치환 연산자를 호출한다.

default 치환 연산자는 클래스의 멤버 변수들을 하나씩 치환하는 작업을 하기 때문에 a와 b의 멤버 변수 data를

직접 복사 한다.



이 상태는 적어도 두가지의 문제점을 가지고 있다.

  • b에서 가리키고 있던 메모리가 삭제 되지 않았지 때문에, 영원히 일어버리게 되는 문제점인 memory leak.
  • a와 b모두 같은 문자열을 가리키는 포인터를 갖게 되었으므로 둘중하나가 지워지게 되면 나머지 하나역시 데이터를 잃어 버리게 된다.
두번째 상황의 예.
~cpp 
String a("Hello");      // a를 생성
...
{                       // 새로운 영역
  String b("World");    // b를 생성
  ...

  b = a;          // default 치환 연산자 수행
                     // b의 메모리를 잃게 된다.

}                 // 영역이 닫힌후,
                  // b의 소멸자가 호출된다. 그러므로, a가 가리키던 data도 소멸되게 된다.

String c = a;     // c의 data는 정의 되지 않는다. 
                  // 복사 생성자가 정의 되지 않았기 때문에 C++에서 제공하는 default 치환 연산자 호출.
                  // a의 data는 이미 지워 졌기 때문에 memory leak의 문제는 없다.
                  // 그러나, c와 a는 같은 곳을 가리킨다. 그리고, c의 소멸자가 호출 되면 위에서 삭제된 곳을 다시한번 
                     // 삭제 하게 된다. 결과 적으로 a와 c가 가리키던 곳이 두번 삭제 되는 경우가 발생된다. (a가 소멸할때, c가 소멸할때)
이상 치환 연산자에 관한것.
...

이하 복사 생성자에 관한것.
~cpp 
void doNothing(String localString) {}
...
String s = "The Truth Is Out There";
doNothing(s);  // deault 복사 생성자 호출. call-by-value로 인해 
                 // localString은 s안에 있는 포인터에 대한 복사본을 가지게 된다.
// 그래서, doNothing이 수행을 마치면, localString은 여역을 벗어나고, 소멸자가 호출된다.
// 그 결과 s는 localString이 삭제한 메모리에 대한 포인터를 가지게 된다. (data 손실)
* 클래스 안에 포인터를 조물딱 거리는 멤버 변수가 있을 경우에는 그 클래스에 복사 생성자와, 치환 연산자를 꼭 정의해 주어야 한다...

3.2. Item 12: Prefer initialization to assignment in constructors

~cpp 
template<class T>
class NamedPtr {
public:
  NamedPtr(const string& initName, T *initPtr);
  ...

private:
  string name;
  T *ptr;
};
멤버 변수를 초기화 하는 방법.


1. 초기화 리스트를 사용한다.
~cpp 
template<class T>
NamedPtr<T>::NamedPtr(const string& initName, T *initPtr  )
: name(initName), ptr(initPtr) {}
2. 생성자의 코드 부분에서 치환을 한다.
~cpp 
template<class T>
NamedPtr<T>::NamedPtr(const string& initName, T *initPtr)
{
  name = initName;
  ptr = initPtr;
}
2가지 방법 정도로 멤버 변수를 초기화 할수 있는데. 책에서는 초기화 리스트를 선호한다.

첫째는 초기화만 가능한 const멤버 변수를 초기화 할수 있다는 의미에서이고,

두번째는 실용주의(효율성) 차원에서 초기화 리스트를 선호 한다는 것이다.

(왜.. 효율성이 좋아지는지는 각자 생각~ 책에도 나와있고.. 생각만 해보면 알수 있는 문제~)


''
  • 가능한 경우 항상 멤버 초기화 리스트를 사용하는 습관을 들이면, const와 레퍼런스 변수들에 대한
    요구 조건을 채울 수 있을 뿐만 아니라, 멤버 변수들에 대한 비효율적인 초기화도 줄일수 있다.''

3.3. Item 13: List members in an initialization list in the order in which they are declared

클래스 멤버들은 클래스에 선언된 순서에 따라 초기화된다.

멤버 초기화 리스트에 나열된 순서는 아무런 영향도 미치지 못한다.

만약, 초기화 리스트에 나열된 순서대로 멤버 변수가 초기화 된다고 가정 하고 이 예를 보자.

~cpp 
class Wacko {
public:
  Wacko(const char *s): s1(s), s2(0) {}
  Wacko(const Wacko& rhs): s2(rhs.s1), s1(0) {}

private:
  string s1, s2;
};

Wacko w1 = "Hello world!";
Wacko w2 = w1;
w1과 w2의 멤버들은 다른 순서에 따라 생성될 것이다. 그리고, 다시 그 객체들(string 객체)을 소멸하기 위해서

객체들(string 객체)이 생성된 순서를 기억한다음 소멸자를 차례대로 호출해야 할것이다. 이런 overhead를 없애기 위해,

모든 객체에 대해서 생성자와 소멸자의 호출 순서는 동일하게 되어 있고, 초기화 리스트에 나열된 순서는 무시된다.

단, 이런 규칙은 비정적 데이터 멤버들만 따른다.

정적 데이터 들이야 단지 한번만 초기화 되기 때문에 이런것을 따를 필요는 없다.

3.4. Item 14: Make sure base classes have virtual destructors

베이스 클래스의 소멸자를 가상함수로 둔다는 얘기는 베이스 클래스가 계승 될경우 계승된 클래스에내에서 소멸자의

작용을 올바로 하기 위함이다. 적당한 예를 보도록 하자.

~cpp 
// base class
class EnemyTarget {
public:
  EnemyTarget() { ++numTargets; }
  EnemyTarget(const EnemyTarget&) { ++numTargets; }
  ~EnemyTarget() { --numTargets; }
  static unsigned int numberOfTargets()
  { return numTargets; }
  virtual bool destroy();                 // EnemyTarget 객체 파괴에
                                          // 성공하면 참을 돌려 준다

private:
  static unsigned int numTargets;               // 객체 카운터
};
// class내의 정적 변수는 클래스의 바깥쪽에 정의되어야 한다.
// 기본적으로 0으로 초기화된다.
unsigned int EnemyTarget::numTargets;
...
..
// base class를 상속한 클래스
class EnemyTank: public EnemyTarget {
public:
  EnemyTank() { ++numTanks; }
  EnemyTank(const EnemyTank& rhs)
  : EnemyTarget(rhs)
  { ++numTanks; }
  ~EnemyTank() { --numTanks; }
  static unsined int numberOfTanks()
  { return numTanks; }
  virtual bool destroy();
private:
  static unsigned int numTanks;         // object counter for tanks
};
unsigned int EnenyTank::numTanks;
EnemyTarget의 객체를 카운트 하기 위해 정적 멤버 변수 numTargets를 두었으며 EnemyTarget을 상속한 EnemyTank에서도

객체의 카운트를 위해 정적 멤버 변수 numTanks를 두었다.

그리곤, 다음과 같은 code를 적용시켜보자.
~cpp 
EnemyTarget *targetPtr = new EnemyTank;
...
delete targetPtr;  // 아무 문제가 없어 보인다.
The C++ language standard is unusually clear on this topic. 베이스 클래스에 대한 포인터를 사용해서 계승된 클래스를

삭제하려고 하며, 베이스 클래스는 가상 소멸자를 가지고 있지 않은 경우. 그 결과는 정의되어 있지 않다. 이것은 컴파일러로

하여금, 원하는 대로 코드를 생상허고 실행하도록 하는 결과를 초래한다. (실행 시간에 자주 발생하는 것은 계승된 클래스의 소멸자가

호출되지 않는다는 것이다. 위의 예에서, targetPtr이 삭제 될때 EnemyTank의 수가 제대로 조정되지 않는다는 것을 의미 한다.)

그래서 이문제를 피하기 위해서, EnemyTarget의 소멸자를 virtual로 선언해야 한다. 소멸자를 가상함수로 선언하면, 여러분이 원하는

방식으로 소멸자가 불리도록 할수 있다.

3.5. Item 15: Have operator= return a reference to *this

우리는 우선 operator=를 정의 해줄때
~cpp 
w = x= y = z = "Hello"; 
이 런식의 연속적인 치환 연산을 할 수 있어야 한다. 그렇기 때문에 operator=연산자의 리턴형을 void로 정의 하면 안된다.

그리고, operator=연산자의 리턴형을 const로 정의 해 주었을때. 밑의 예제와 같은 멍청한(?) 연산을 해주었을때 적용 되지 않는다. 밑의 연산은 멍청한(?) 연산이지만 C++의 기본 타입에 대해 저런 연산이 가능하기 때문에 저런 연산도 지원하게 끔 만들어야 한다.
~cpp 
class Widget {
public:
  ...                                            // note
  const Widget& operator=(const Widget& rhs);    // const
  ...                                            // return
};                                               // type
...
Widget w1, w2, w3;
...
(w1 = w2) = w3;         // assign w2 to w1, then w3 to
                        // the result! (Giving Widget's
                        // operator= a const return value
                        // prevents this from compiling.)

그래서, operator=의 리턴형을 const로 작성하면 안된다. (....--;...)

...

기본 형식을 갖는 치환 연산자에서, 리턴값으로 사용할 수 있는 두 가지 경우가 있다. 치환의 왼쪽 부분 (this)과 치환의 오른쪽 부분(인자 리스트에 있는것)이다. 어떤것을 리턴해 줄것인가? operator=과 관련된 밑의 두가지 경우를 보자.
~cpp 
String& String::operator=(const String& rhs)
{
  ...
  return *this;            // return reference
                           // to left-hand object
}
String& String::operator=(const String& rhs)
{
  ...
  return rhs;              // return reference to
                           // right-hand object
}
위의 두가지 경우는 별다른 차이가 없어보인다. 그러나, 중요한 차이점이 있으니 말을 꺼내는 것이 겠지? --;

첫째, rhs를 리턴하는 경우는 compile 되지 않을 것이다. 왜냐하면, rhs는 const String에 대한 레퍼런스 이고, operator=는 String에 대한 레퍼런스를 리턴하기 때문이다. 뭐 이런 문제야 밑에서 처럼 고치면 문제 되지 않는다.
~cpp 
String& String::operator=(String& rhs)   { ... }
하지만, 이번에는 클래스의 operator=를 사용하는 코드에서 문제가 발생한다.
~cpp 
x = "Hello";
치환의 오른쪽 부분이 String형이 아니라 char *형이기 때문에 컴파일러는 String의 생성자를 통해 임시 String객체를 만들어서 호출을 한다. 즉, 아래와 같은 code를 생성한다.
~cpp 
const String temp("Hello"); // 임시 객체를 만든다.
...
x = temp; // 임시 객체를 operator=에 전달한다.
컴파일러는 위와 같은 임시 객체를 만들려고 하지만, 임시 객체가 const라는 것에 주의. 그리고, operator=의 리턴형을 보면 String에 대한 레퍼런스를 돌려주기 때문에 리턴형이 일치하지 않게 된다. 그래서, error를 발생시킨다. 만약 error를 발생 시키지 않는다면, operator=이 호출되는 측에서 제공된 인자가 아니라 컴파일러가 발생시킨 임시 변수만 수정된다는 것에 놀랄것이다. --;

..
결론을 얘기 하자면, 치환의 왼쪽 부분에 대한 레퍼런스 *this를 되돌려 주도록 치환 연산자를 선언해야 한다. 만일 이외의 일을 하면, 연속적인 치환을 할 수 없게 되고, 클래스를 사용하는 코드에서의 묵시적인 타입 변환도 할 수 없게 된다.

3.6. Item 16: Assign to all data members in operator=

operator= 연산자를 수행할때 개체 내 각각의 모든 데이터 멤버를 치환할 필요가 있다는 얘기.



상속의 경우 특히나 조심해서 operator= 연산자를 만들어 줘야 한다.
~cpp 
class Base {
public:
  Base(int initialValue = 0): x(initialValue) {}
private:
  int x;
};
class Derived: public Base {
public:
  Derived(int initialValue)
  : Base(initialValue), y(initialValue)   {}
  Derived& operator=(const Derived& rhs);
private:
  int y;
};

// The logical way to write Derived's assignment operator is like this 
// erroneous assignment operator
Derived& Derived::operator=(const Derived& rhs)
{
  if (this == &rhs) return *this;    
  
  y = rhs.y;                         // assign to Derived's
                                     // lone data member
  return *this;                      // see Item 15
}

// Unfortunately, this is incorrect, because the data member x in 
// the Base part of a Derived object is unaffected by this assignment operator. 
// For example, consider this code fragment 
void assignmentTester()
{
  Derived d1(0);                      // d1.x = 0, d1.y = 0
  Derived d2(1);                      // d2.x = 1, d2.y = 1
  d1 = d2;			 // d1.x = 0, d1.y = 1!
}
보기와 같이 제대로 작동하지 않는 operator= 연산자이다. 그럼, 이것을 어떻게 고치면 좋을까? 이 문제를 해결하기 위해서는, 다음과 같이 Base클래스의 operator=연산자를 호출해 주면 된다. ( Derived 클래스의 operator= 연산자에서 x를 치환해 준다는 것은 허용되지 않기 때문에.)
~cpp 
// correct assignment operator
Derived& Derived::operator=(const Derived& rhs)
{
  if (this == &rhs) return *this;
  Base::operator=(rhs);    // call this->Base::operator=
  y = rhs.y;
  return *this;
}


상속과 관련하여 유사한 문제가 복사 생성자에서도 생길 수 있다. 밑의 코드를 보자.
~cpp 
class Base {
public:
  Base(int initialValue = 0): x(initialValue) {}
  Base(const Base& rhs): x(rhs.x) {}
private:
  int x;
};
class Derived: public Base {
public:
  Derived(int initialValue)
  :  Base(initialValue), y(initialValue) {}
  Derived(const Derived& rhs)      // erroneous copy
  : y(rhs.y) {}                    // constructor
private:
  int y;
};
Derived 클래스의 복사 생성자를 보면 Base클래스의 멤버 변수는 초기화 시키지 않음을 알수 있다. 이런 문제를 피하기 위해서는 밑의 코드와 같이 Base클래스의 복사 생성자를 호출해 주면 된다.
~cpp 
class Derived: public Base {
public:
  Derived(const Derived& rhs): Base(rhs), y(rhs.y) {}
  ...
};
이젠 Derived클래스의 복사생성자를 호출해도 Base클래스의 멤버 변수역시 잘 복사 된다.

3.7. Item 17: Check for assignment to self in operator=

assignment to self in operator= (재귀치환) 는 다음과 같을때 발생한다.
~cpp 
class X { ... };
X a;
a = a;                     // a is assigned to itself
'왜 이런 대입을 하는거지. 프로그램 짜는 놈이 바보 인가?' 라는 생각을 할 수 도있지만, 밑의 코드가 있다고 하자.
~cpp 
a = b
그런데 b가 a로 초기화된 레퍼런스라면 보기에는 재귀치환에 해당한다. 이런 가능한 상황에 대처하기 위해 특별히 주의를 가지는 것에 는 두가지 좋은 점이 있다. 첫째는 효율성이다. 치환 연산자의 상위 부분에서 재귀치환을 검사할 수 있다면, 바로 리턴할 수 있기 때문이다. 두번째는, 정확함을 확인하는 것이다. 일반적으로 치환 연산자는 객체의 새로운 값에 해당하는 새로운 리소스들을 할당하기 전에 객체에 할당된 리소스들을 해제해야만 한다. 이전 값들을 제거해야 한다는 말이다. 재귀치환일 경우 이런식으로 이전 값들을 제거할경우 큰 hazard를 가져 온다. 왜냐하면, 기존 리소스들이 새로운 리소들을 치환하는 과정에서 필요하게 될 수 있기 때문이다.




String 객체들의 치환을 생각해 보자. 여기에선 재귀치환을 검사하지 않고 있다.
~cpp 
class String {
public:
  String(const char *value);    // see Item 11 for
                                // function definition
  ~String();                    // see Item 11 for
                                // function definition
  ...
  String& operator=(const String& rhs);
private:
  char *data;
};

// an assignment operator that omits a check
// for assignment to self
String& String::operator=(const String& rhs)
{
  delete [] data;    // delete old memory

  // allocate new memory and copy rhs's value into it
  data =   new char[strlen(rhs.data) + 1];
  strcpy(data, rhs.data);
  return *this;      // see Item 15
}
그리고, 이와 같은 경우를 보자.
~cpp 
String a = "Hello";

a = a;               // same as a.operator=(a)
String객체의 operator= 연산자를 볼때 *this와 rhs는 다른것 처럼 보이지만, 이 둘은 같은 데이터를 pointing하고 있다. (같은 객체이기 때문에..)



치환 연산자가 수행하는 첫 번째 작업이 data를 삭제하는 것이며, 결과는 다음과 같다.





이제 치환연산자에서 strcpy를 수행할때 발생하는 일은 예측할 수 없게 되었다. 이것은 data가 삭제되면서 rhs.data가 삭제되었기 때문이다. 이것은 data, this->data 그리고, rhs.data가 모두 같은 포인터기 때문에 그렇게 된것이다. 최악의 상황이다..


이제 왜? 재귀치환을 검사해야 하는지 알았을 것이다.




그럼, 어떤식으로 재귀치환인지 아닌지를 검사할까? 종류는 다양하다.
~cpp 
// data의 동일성을 검사
String& String::operator=(const String& rhs)
{
  if (strcmp(data, rhs.data) == 0) return *this;
  ...
}

// 객체의 멤버 변수들의 값이 동일한지 검사
// 이때 operator== 을 다시 재정의 해주어야 한다
C& C::operator=(const C& rhs)
{
  // check for assignment to self
  if (*this == rhs)              // assumes op== exists
    return *this;
  ...
}

// 주소값을 통한 검사 - 가장 좋은 효과를 기대할만 하다.
C& C::operator=(const C& rhs)
{
  // check for assignment to self
  if (this == &rhs) return *this;
  ...
}

// 클래스마다 식별자를 두어 검사하는 방법
// 이경우에도 operator == 을 정의하여 식별자가 같은지 검사해보아야 한다.
class C {
public:
  ObjectID identity() const;            // see also Item 36
  ...
};

4. Classes and Functions: Design and Declaration

효과적인 클래스 설계란 무엇인가? 를 다루는 부분

1. 객체들이 어떻게 생성되고 소멸될 것인가?

2. 객체 초기화와 객체 치환이 어느 정도 차이가 나는가?

3. 새로운 타입의 객체를 값에 의해 전달하는 것은 무엇을 의미 하는가?

4. 새로운 타입의 객체가 상속 그래프에 맞는가?

5. 어떤 종류의 타입 변환이 허용되는가?

6. 어떤 연산자와 함수가 새로운 타입을 위해 적당한가?

7. 접근 연산자(public, protected, private)를 어떻게 적용할 것인가?

8. etc..

4.1. Item 18. 최소한의 완전한 클래스 인터페이스를 추구한다.

interface? 클래스를 이용하는 프로그래머가 접근할수 있는 수단을 주는 것이다. 일반적으로 함수들만 이러한 인터페이스 내에 존재한다. 만약 클래스내의 데이타 변들에게 접근을 허용하게 되면 많은 단점들이 생기기 때문이다. (별로 느끼지는 못해 봤다.. ^^;)

최소한의 완전한 클래스 인터페이스를 추구한다(?) 이말은 클래스내에 군더더기 즉 비슷한 일을 하는 멤버 함수가 존재 하지 않는다는 의미도 된다. 그리고, 그 클래스는 그만큼 복잡하지않을 것이다. 그리고, 최소한의 인터페이스로 이 클래스를 사용하는 사용자가 모든 일(?)을 할수가 있어야한다.

그런데, 왜 최소한인가? 여러가지 일을 할수 있는 멤버 함수들을 계속 추가해 나가면 안되는 것인가? 대답은 안된다. 왜 안되는 것일까? 당신은 멤버 함수가 10개 있는 클래스와 100개가 있는 클래스중 어떤것이 이해하기 쉽다고 생각하는가? 나 만 쓰려는 클래스가 아닌이상 다른 사용자들이 쉽게 이해 할수 있도록 만들어야 하지 않겠는가? 그렇기 때문에 최소한의 인터페이스를 추구하는 것이다. 그리고, 관리적인 면에서 볼때 적은 함수들을 가진 클래스가 용이하다는 것이다. 중복된 코드라던지 아니면 개선할 것들을 향후에 하기 쉽다는 것이다. 또한, document를 작성한다 든지 할때 적은 멤버 함수들을 가진 클래스 쪽이 용이하다는 것이다. 마지막으로 아주 긴 클래스 정의는 긴 헤더 파일을 초래 한다. 일반적으로 헤더 파일들은 프로그램이 컴파일될 때마다 매 번 읽혀져야 하기 때문에 필요 이상 긴 클래스 정의는 프로젝트 주기 중의 총 컴파일 시간을 갉아 먹는다. 그런 이유들 때문에 최소한의 클래스 인터페이스를 추구하는 것이 좀더 나은 판단이라는 것이다.

4.2. Item 19. 멤버 함수, 비멤버 함수 및 프렌드 함수를 구별한다.

대게 class내에 operator 연산자를 사용함으로서 좀더 편한 code(?)라는 것을 하기 위해서 선언 하는 함수들이 멤버 함수여야 하는지 아니면 friend함수여야 하는지를
쑥덕 쑥덕 하는 것이다. 멤버 변수들의 연산을 다양하게 적용시키고 싶다면 friend 함수를 써서 일반 상수가 클래스 인스턴스의 왼편에 자리 잡고 있어도 연산이 되게 하고싶다라는 생각을 가지고 있으면 friend함수를 써서 비 멤버 함수를 만들어 그 연산을 지원하라는 얘기 이다.
~cpp 
Num a(10);
int temp;
temp = 2 * a; // 이런 연산. - friend함수를 사용하여 연산자를 정의 해줘야지만 작동한다.
그렇지만, 이런 연산자들을 거의 안쓰는 것같다.. ㅡㅡ; 나도 friend함수 써본 일이 없다.. ㅡㅡ; 학교 시험에서 나올법한 얘기들.


- tip -

여기서 f는 적절히 선언하고자 하는 함수를 나타내고 C는 개념적으로 관련있는 클래스를 의미한다.
  • operator>>operator<<는 결코 멤버가 될수 없다. 만일 f가 operator>>또는 operator<<이라면, f를 비멤버 함수로 만든다. 게다가 f가 C의 비공용 멤버로 접근이 요구된다면 f를 C의 프렌드로 만든다.
  • 비멤버 함수들만 그들의 가장 왼쪽편에 있는 인자에서 타입 변환이 일어난다. 만일 f가 가장 왼쪽편에 있는 인자에서 타입 변환을 필요로 한다면 f를 비멤버 함수로 만든다. 게다가 f가 C의 비공용 멤버에 접근이 필요하다면 f를 C의 프렌드로 만든다.
  • 그 밖에 모든 것은 멤버 함수이어야 한다. 이상의 어떤 것에도 해당되지 않으면 f를 C의 멤버 함수로 만든다.

4.3. Item 20. 데이터 멤버를 공용(public) 인터페이스에 포함시키지 않는다.

제목그대로.

4.4. Item 21. 가능한 const를 이용한다.

const를 쓰는 이유는 요놈의 값은 절대로 변하지 말아야 한다는것을 보여주기 위함이랄까? 아무튼 const로 지정을 해놓으면 어떻게 하든 그 값은 절대로 변하지 않는 다는것은 보장이 된다.
~cpp 
const char *p = "Hello";
char * const p = "Hello";
const char * const p = "Hello"; 
// 이 세가지의 차이점은 뭘까요?
// 잘 생각 하면 알수 있을 거에요.
// pointer란 가리키는 곳의 값과, 주소로 이루어 진 놈이니까.
// 어떤때는 주소가 변하지 말았으면 할때이고,
// 어떤놈은 가리키는 곳의 값이 변하지 말았으면 할때.. 이런 식으로 생각하면 쉬울듯 하네요.. ㅎㅎ
.. 추후에 정리..

4.5. 항목 22. 값에 의한 호출보다는 레퍼런스에 의한 호출을 선호한다.

이 항목은 참조를 유용하게 사용할 수 있는 경우이다.
첫번째는 함수의 리턴값으로 사용시 무리하게 임시객체가 생성이 될 수 있다.
{{|
class Person
{
...
private:
string name, address;
}
class Student : public Person
{
...
private:
string schoolName, schoolAddress;
}
|}}
이렇게 클래스가 존재하는 경우
{{|
Student returnStudent(Student s)
{
return s;
}

...

Student plato;

returnStudent(plato);
|}}
이렇게 호출이 된다면

plato는 returnStudent함수에서 인자로 넘어가면서 임시객체를 만들게 되고 함수 내에 쓰이면서 s로 또 한번 생성되고 함수가 호출이 되고 반환 될때 반환된 객체를 위해 또 한번 복사 생성자가 호출된다.
총 3번의 부가적인 호출이 일어나면서 멤버인 string들은 총 12번이나 생성이되고 파괴가 된다.

두번째는 잘라지는 문제(slicing problem)로 위의 예에서 returnStudent함수에 인자로 Person형 객체가 다운 캐스팅해서 들어가는 경우 내부적인 임시객체들의 생성으로 Student형 객체로 인식되 Student형 객체만의 멤버를 호출하게되면 정상작동을 보장할 수 없게 된다.

4.6. 항목 23. 객체 반환시 레퍼런스를 반환하지 않는다.

객체 반환은 값의 의한 호출이 참조보다 훨씬 간단하고 명확하다.
그렇지 않고 참조에 의한 호출을 할 경우에 책에서는 내부 임시객체를 통해 반환을 하려고 할땐 그 임시 객체의 메모리는 스택에 있기 때문에 문제가 되고, new를 사용해서 힙 기반으로 만들때는 연달에 세번의 호출이 있을 경우 필연적으로 메모리가 누출된다. 그렇다고 static의 정적 객체의 경우에도 비교문(operator =)에서 사용된다면 언제가 참으로 계산이 될것이다. 그렇다고 정적 객체 배열로 무리해서 구현을 하고자 한다면 그건 바로 삽질이다.

4.7. 항목 24. 함수 오버로딩과 디폴트 인자값 중에서 주의깊게 선택한다.

  • std::numeric_limits<TYPE>::min(); // 헤더 <limits> TYPE의 최소값을 구해준다. INT_MIN과 같은 값.

디폴트 인자값을 사용할 수 있는 경우도 있고 없는 경우도 있다.
예를 들면 5개의 값의 평균을 낸다던가 하는 경우는 디폴트 인자를 사용할 수 없다.

자신이 알아서 상황에 따라 처신하도록.

4.8. 항목 25. 포인터나 수치형 타입상의 오버로딩을 피한다.

만약 다음과 같은 경우
{{|
void f(string *);
f(int);
..
f(0); // 정수형 0이므로 f(int)가 호출
f(NULL); // ?-_-? 굉장히 복잡 미묘하다-_-; 선언에 따라서 0이 될수도 (void*)0이 될수도 0L이 될수도 등등의 경우가 있다.
|}}
NULL이라는 상수에 대한 타입의 정의가 모호하다.

이런 경우 다음과 같은 방법이 있다.
{{|
const class // 클래스의 이름이 필요하지 않다.
{
public:
template<class T> operator T*() const // 모든 NULL 포인터를 대체한다.
{
return 0;
}
template
{
return 0;
}
private:
void operator&() const; // NULL의 주소값이란 존재하지 않는다.
} NULL;
|}}
결론은 되도록이면 이렇게 복잡하게 들어가는 경우를 만들지 않으면 된다.

4.9. 항목 26. 잠재적 모호성을 경계한다.

  • 누구나 나름대로의 철학을 가져야 한다. 어떤 이는 불간섭주의 경제학을 믿고 어떤 이는 윤회설을 믿는다. 또 어떤 이는 COBOL이 진짜 프로그래밍 언어라고 믿는다. C++도 나름의 철학을 가지고 있다. C++는 잠재적인 애매모호성은 에러가 아니라고 생각한다. 나는 과학-기술 서적에서도 이러한 충분히 깊은 생각의 이야기도 필요하다고 생각한다.

C++언어는 잠재저인 애매모호성을 가진 언어이다.
책에서는 몇가지 예를 들고 있다.
{{|
void f(A);

...

B b;
f(b);
|}}
를 호출시 함수 f는 인자로 A를 요구한다. 우선 B가 A의 자식이나 부모인지를 확인 해볼테고 operator A()를 찾아 볼지도 모른다.
만약 이 두개가 만족하는 B라면 애매모호성의 경우이다. (이런 경우는 만들면 안될꺼라고 생각한다.)

{{|
void f(int);
void f(char);

...

double d=6.02;
f(d)
|}}
가장 쉽게 접할 수 있는 애매모호한 경우이다.
이 경우는 static_cast<TYPE>를 통해 쉽게 해결 가능하다.

{{|
class Base1
{
public:
int doIt();
}
class Base2
{
public:
int doIt();
}
class Derived : public Base1, public Base2
{
}

...

Derived d;
d.doIt();
|}}
대표적인 다중상속의 애매모호성이다.
이것 또한 접근범위를 설정해 줌으로 쉽게 해결 가능하다. (d.Base1::doIt()과 같은 경우)

이와 같은 예에서 알수 있듯이 C++은 애매모호성을 하나의 성질로 받아 들였다고 생각한다.
그래서 대부분의 이와 같은 경우에 해결할 수 있는 유연성이 있다.

4.10. 항목 27. 의도하지 않은 내부 생성 멤버 함수의 이용을 명시적으로 막는다.

위의 항목에서 보듯이 C++은 애매모호성을 지니고 있다.
따라서 코딩상의 에러 위험성을 낮추기 위해서는 최대한 명시적으로 해결할 수 있는 방안을 넣으면 좋다.

예로 치환연산자를 설명한다.
만약 C++의 클래스로 C 언어의 배열과 유사한 클래스를 만들고자 한다면
C 언어의 배열과 유사한 성질을 지녀야 한다.
C 언어에서 배열의 치환은 불법이다.
그러므로 C++의 배열클래스의 경우도 치환연산자를 코딩상의 불법으로 만들면 차후 애매모호한점이 없어진다.
C++언어 클래스 기본 치환 연산자를 멤버의 복사로 정의되어 있으므로
private으로 오버라이딩하면 적절하게 대처한 경우이다.

4.11. 항목 28. 전역 네임스페이스를 분할한다.

C++에 들어오면서 사용된 네임스페이스는 훌륭한 모듈관리방법이다.
약간의 추가코딩으로 매우 쉽게 애매모호성을 제거한다.

5. 클래스와 함수 : 구현

5.1. 항목 29. 내부 데이터에 대한 "핸들"을 리턴하는 것을 피해라.

내부 자원에 대한 수정/삭제가 가능하도록 반환하는 것은 좋지 못한 방법이다.

만약 이와 같은 경우
{{|
class String
{
...

public:
operator char *() const;
private:
char *data;
}

...

String B("Hello World");
char *str = B;
strcpy(str, "Hi Mom");
|}}
String B는 자원에 대한 핸들을 넘겨주므로 메모리의 유출이 불가피하다.

C++ 표준의 String은 반환값을 const형으로 반환하는 c_str멤버 함수를 통해 이를 피하고 있다.
{{|
class String
{
...

public:
operator const char*() const;
}
|}}
이와 같이 사용한다면 충분히 위험을 피해갈 수 있다.

{{|
String someFamousAuthor()
{
return "E.H.Gombrich"; // the story of art의 저자
}

...

const char *pc = someFamousAuthor();
cout << pc;
|}}
이와 같이 반환값으로 사용되는 경우 임시객체가 반환되면서 포인터를 넘겨주고 삭제된다.
그렇게 되므로 잃어버린 핸들(dangle handle)만 남게 된다.

컴파일이 된다는 점에서 애매모호성라고 할수 있으나 해결하는데 비용이 많이 든다.
(치환연산자에서 const char*형태의 반환값을 넘겨줄때 핸들에 대한 자원을 하나 더 만들어 반환하면 가능은 하다.)

사용될 성질에 따라서 타협;을 하자.

5.2. 항목 30. 접근하기 어려운 멤버에 대한 비상수 포인터나 레퍼런스를 리턴하는 멤버 함수 사용을 피하라.

근본적인 C++ 클래스의 존재의 이유이다.
클래스의 멤버는 이미 접근자의 뜻을 가지고 있다.
이 때, 내용이 private형과 같이 내부사용만 허락할 경우 이 내용을 반환하는 형식이 나오면 안된다.
(String형과 같이 C 언어의 특성과 반드시 호환되게 해야할 경우 조금의 예외라고 생각해도 좋을듯 하다;;)

5.3. 항목 31. 지역 객체에 대한 참조나 함수 내에서 new를 이용해 초기화된 포인터를 가리키는 참조를 리턴하지 말라.

이번 항목은 앞에서 몇번 언급 되었던듯 한데
{{|
class Rational
{
...

public:
friend const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n*rhs.n, lhs.d,lhs.d);
return result;
}
private:
int n,d; // 분모와 분자값
}
|}}
이와 같은 경우 지역 객체 result는 함수의 범위를 벗어나면서 삭제된다.

{{|
class Rational
{
...

public:
friend const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n*rhs.n, lhs.d,lhs.d);
return *result;
}
private:
int n,d; // 분모와 분자값
}
|}}
만약 이와 같을 경우 힙영역에 메모리를 생성하고 함수의 범위를 벗어나더라도 삭제가 되지 않지만
중복된 operator *의 사용시 메모리 누출이 불가피하다.

이 문제에서는 레퍼런스가 아닌 값으로 반환함으로 해결할 수 있으나 너무 큰 비용이 든다.

  • 더 나은 방법을 아시는 분은 추가해주세요;;

5.4. 항목 32. 변수 정의는 가능한 뒤로 늦춰라.

C 언어를 사용할 때는 함수의 첫머리에 변수를 정의하고 시작하였다.
아마 초기의 C 언어에서 변수의 정의가 첫머리에 있어야 함도 있을테고 변수를 쉽게 식별하기 위해서도 사용된 방법이다.
이번 내용은 제목대로 변수의 정의를 가능한 늦춰서 사용되지 않을 경우 불필요한 작업을 없애기 위함이다.

{{|
string encryptedPassword(const string &password)
{
string encrypted;
if(password.length() < MINIMUM_PASS_LENGTH)
throw logic_error("password is too short");
encrypted = password;
encrypt(encrypted);
return encrypted;
}
|}}
이와 같은 경우 예외로 넘어가게 될 경우 string encrypted; 코드는 쓸모가 없다.

{{|
string encryptedPassword(const string &password)
{
if(password.length() < MINIMUM_PASS_LENGTH)
throw logic_error("password is too short");

string encrypted(password);

encrypt(encrypted);
return encrypted;
}
|}}
이렇게 고침이 현명하다.

  • 요즘 컴파일러는 대충 이정도는 알아서 해주지 않나요?;;

5.5. 항목 33. 인라인을 선별적으로 사용하라.

인라인은 매크로보다 뛰어나고 실행속도를 증가 시킬수도 있으나 때에 따라 추가적인 용량을 생성 시킬수도 있다.
{{|
// "example.h" file
inline void f() { ... }

// "source1.cpp" file
#include "example.h"

// "source2.cpp" file
#include "example.h"
|}}
이와 같은 경우 인라인함수 f를 source1.cpp와 source2.cpp에서 각각 생성시킬수도 있다.
이것 말고도 인라인 함수의 함수포인터를 취할 때 컴파일러에 따라 파일마다 정적 복사본을 생성시킬수도 있다.
이렇게 추가적인 용량이 늘어나는 경우 외에도 캐시의 적중률을 감소시켜 오히려 성능이 감소할 수도 있다.

80-20 파레토의 분석에 따라 중요한 코드는 약 20%가 차지한다는 것을 생각하고 중요한 부분에만 인라인 처리를 해주자.

  • 생성자와 소멸자는 인라인으로 사용해서는 안된다. 이는 컴파일러구현자가 때에 따라 생성자와 소멸자에 보이지않는 코드부분을 포함할 수도 있기 때문이다.

5.6. 항목 34. 파일간의 컴파일 의존성(dependency)을 최소화하라.

파일 한부분을 수정하고 다시 컴파일을 시도 했을때 모든 파일을 다시 컴파일하고 링크하고 실행하는 경험을 한번쯤은 해보았으리라 생각한다.
이와 같이 파일간의 의존성으로 인해 너무 많은 컴파일 시간을 잡아먹는것은 시간낭비이고 사람에 따라 매우 짜증나는 일이다.
이번 항목은 몇가지 방법으로 파일간의 의존성을 줄이는 방법을 제시한다.

우선 클래스의 전방 참조를 설명한다.
전방 참조를 함으로 헤더부분에서는 외부 클래스의 내용을 사용하지 않아 더 이상의 무의미한 컴파일의 연결고리를 끊을수 있다.
하지만 상황에 따라서는 반드시 클래스의 내용을 사용해야하는 경우도 있고 그럴때는 전방 참조의 의미는 약해진다.

이럴땐 Handle클래스와 프로토콜 클래스를 사용한다.
Handle클래스는 외부에서 사용될 내용의 부분을 따로 떼어 Handle클래스로 만듬으로 최소한의 내용을 헤더에 포함시킨다.
프로토콜 클래스는 인터페이스를 사용한다. 추상화된 인터페이스만 만들어 헤더에 포함시키는 방법으로 파일간의 의존성을 약화시킨다.

6. 인스턴스와 객체지향 설계

6.1. 항목 35. public 계승이 "isa"를 모델링하도록 하라.

{{|
class Person
{
...
}
class Student : public Person
{
...
}
|}}
이 관계는 public 계승인 "isa"관계이다.
하지만 Person isa Student이지 Student isa Person이 아님을 주의해야한다.

컴퓨터는 0과 1로 이루어진 고철이다. 우리가 생각하는 것을 바로 표현하지 못한다.
우리가 수학을 포함한 다른 부분들에서 익힌 직관이 바로 적용되지 못함을 인식해야 한다.

6.2. 항목 36. 인터페이스 계승과 구현 계승의 차이점을 이해하라.

{{|
class Shape
{
...

public:
virtual void draw() const;
}
class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };

...

Shape *ps1 = new Rectangle;
ps1->draw();
Shape *ps2 = new Ellipse;
ps2->draw();
|}}
클래스 Shape에서는 구현할 내용의 설계만 있다.
그리고 상속받은 클래스들에서 내용의 구현이 있다.

만약 상속받은 클래스에서 가상함수의 구현이 되어 있지 않을 경우가 있다.
이럴때 가상함수로 설정된 함수를 실행한다면 문제가 된다.
{{|
class Shape
{
...

public:
virtual void draw const = 0;

protected:
void defaultdraw() const;
}

class Rectangle : public Shape
{
...

public:
virtual void draw() const
{
defaultdraw();
}
}
|}}
이렇게 가상함수를 순수가상함수로 바꾸고 그에 따른 기본행동에 대한 정의를 default함수로 하나 만들어 두는것도 좋은 방법이다.

6.3. 항목 37. 계승된 비가상 함수를 재정의하지 않도록 한다.

가상함수와 비가상함수의 사용법에 대한 내용이다.
상속이 되는 함수에 대하여 가상함수로 설정하는게 옳고 상속이 되지 않거나 상속이 되더라도 다시 구현이 되지 않다면 비가상함수로 사용되어야 한다.

6.4. 항목 38. 계승된 부재 인자값을 재정의하지 않도록 한다.

{{|
enum ShapeColor { RED, GREEN, BLUE };

class Shape
{
...

public:
virtual void draw(ShapeColor = RED) const;
}
class Rectangle : public Shape
{
...
public:
virtual void draw(ShapeColor = BLUE) const;
};

...

Shape *pr = new Rectangle;
pr->draw();
|}}
이와 같은 경우 pr이 파란색 사각형을 그릴것이라 예상은 하지만 그렇지 않다.
pr은 Shape형의 정적 포인터로 디폴트인자와는 정적으로 결합한다.
결국 빨간색 사각형이 그려진다.

이는 성능상의 이유인데 디폴트인자와 동적으로 결합시 복잡하고 느리게 작동한다.
대신 조금 더 빠르게 실행이 된다.

6.5. 항목 39. 계층도의 아래쪽 클래스를 다운캐스트(downcast)하지 않도록 한다.

{{|
class Person { ... };
class Student : public Person { ... };

...

Student *s = new Person;
|}}
이것은 옳지 않다. 꼭 100원짜리 동전만 들어갈 수 있는 저금통에 500원짜리 동전을 우겨 넣는것과 같다.
그러므로 s를 통해 Student만의 함수를 호출시 알수 없는 결과를 나타낼 것이다.

되도록이면 이런 경우는 가상함수와 클래스의 계층구조를 통한 인터페이스의 활용으로 해결한다.
하지만 이렇게 풀리지 않을 경우 dynamic_cast와 같은 방법으로 미리 타입을 체크한후 실행여부를 결정하도록 하자.

6.6. 항목 40. 레이어링(layering)을 통해 "가지고 있는" 것과 "사용하여 구현된" 것을 모델링하도록 하자.

Composition으로도 불리는 "has-a" 관계의 이야기 이다.

만약 STL의 포함된 set이외에 자신만의 Set을 구현하고자 하는 예제가 있다.
{{|
template<class T> class Set: public list<T> { ... };
|}}
이것은 옳다고 보이나 그렇지 않다.
왜냐하면 Derived가 Base이면 (isa), Base가 참일때 항상 Derived도 참이 된다.
Set은 중복된 내용에 대해서 한번만 입력을 받는 자료형이다. 하지만 이렇게 "isa" 관계로 나타낼때 옳지 못하게 된다.

{{|
template<class T> class Set
{
...

public:
void insert(const T &항목);
private:
list<T> rep;
}

...

template<class T> void Set<T>::insert(const T& 항목);
{
// 중복된 내용이 있는지 검사후 추가
}
|}}
이렇게 구현한다면 해결이 된다.

"isa" 관계와 "has-a"관계중 어느것인지 생각하고 구현하자.

6.7. 항목 41. 계승과 템플릿과의 차이점을 이해한다.

템플릿은 객체의 타입이 클래스의 정의된 함수들의 동작 원리에 영향을 미치지 않는 경우에, 클래스의 모음을 생성하는 데 사용되어야 한다.
계승은 객체의 타입이 클래스에 정의된 함수들의 동작 원리에 영향을 미치는 경우에, 클래스의 모음을 위해 사용되어야 한다.

만약 스택을 템플릿이 아닌 계승을 통해 구현하고자 한다면 어떻게 할것인가를 생각해 보고
생물->동물->인간->학생등의 형식을 템플릿으로 구현하고자 한다고 생각해보자.

템플릿이 아닌 계승을 통한 스택의 구현이라면 void형 포인터등을 통해 여러 자료형을 동적으로 입력받도록 노력할 것이며
계층구조를 템플릿으로 묶고자 한다면 템플릿형에 따라 if문을 돌리는등 C, PASCAL의 형식을 따를지도 모른다.

6.8. 항목 42. private 계승을 바르게 사용하라.

private 계승은 layering과 매우 유사하다. public 계승과는 다르게 Derived isa Base이지도 않다.
단지 layering과 차이점은 Base클래스에서 protected과 private의 사용유무의 차이이다.
만약 만들고자 하는 클래스가 다른 클래스의 protected나 private멤버를 사용해야 한다면 layering만으로는 처리할수가 없다.
이럴때 private 계승을 사용해야한다.

{{|
class GenericStack
{
...

public:
void push(const void *Object);
void *pop();
}
|}}
이렇게 객체가 아닌 포인터에 대한 스택이 있다고 할때. 이 클래스만으로는 타입이 안정하지 못하다.

{{|
class GenericStack
{
protected:
... // 생성자, 소멸자 및 생략된 내용

void push(const void *Object);
void *pop();
}

class IntStack : private GenericStack
{
...

public:
void push(int *intPtr)
{
GenericStack::push(intPtr);
}
int *pop()
{
return static_cast<int*>(GenericStack::pop());
}
}

...

GenericStack gs; // 에러 : 생성자가 protected이다.
IntStack is; // Int 포인터형에 대한 안정한 스택이다.
|}}
타입안정하고 GenericStack만으로는 생성할 수 없도록 막아놓았다.

대부분의 경우 layering이 쓰고 이와 같이 특별한 경우만 private 계승을 쓰도록 하자.

6.9. 항목 43. 다중 계승을 바르게 사용하도록 하라.

다중 계승은 애매모호한 부분이 많다.

{{|
class A { ... };
class B : public A { ... };
class C : public A { ... };
class D : public B, public C { ... };
|}}
이와 같은 경우 매우 중복적이고 매우 위험한 코드이다.
클래스 A부분에 있는 가상함수를 B와 C에서 구현하였다면 클래스 D에서 호출되는 가상함수는 무엇을 뜻하는지 알수 없다.
이것은 최악의 설계로 이런 방법은 피해야 한다.

최대한 위와 같은 다이아몬드형 상속은 피하자.
자바에서는 상속받을 클래스는 하나만 받을수 있고 인터페이스는 여러가지를 받을 수 있다.

6.10. 항목 44. 의미하는 바를 표현하도록 하라. 자신이 표현한 것의 의미를 이해하도록 하라.

위의 내용을 요약한 내용이다.

7. 미묘한 부분

7.1. 항목 45. C++가 은밀하게 어떤 함수를 만들어주고 호출하는지 이해하기

{{|
class Empty { };
|}}
만약 이렇게 클래스를 생성한다면

{{|
class Empty
{
public:
Empty();
Empty(const Empty& rhs);
~Empty();

Empty& operator=(const Empty& rhs);
Empty* operator();
const Empty* operator&() const;
}
|}}
이렇게 만들어준다.

7.2. 항목 46. 실행 시간 에러보다는 컴파일 시간과 링크 시간 에러가 좋다.

C++은 컴파일되는 언어로 실행시간에는 에러를 탐지하지 않는다.
그 대신 빠른 수행시간의 이익이 있고 실행 시간의 에러를 예방하기 위해서 컴파일 시간과 링크 시간에 위험을 제거하도록 해야 된다.

날짜를 나타내기 위한 클래스 이다.
{{|
class Date
{
...

public:

Date(int day, int month, int year);
}
|}}
여기서 Month의 값의 유효성 체크를 위해서 고쳐보자.
{{|
class Date
{
...

public:

Date(int day, const Month& month, int year);
}

class Month
{
public:
static const Month Jan() { return 1; };
static const Month Feb() { return 2; };
...
static const Month Dec() { return 12; };
private:
Month(int number) : monthNumber(number) { }
Month(const Month& rhs);
const int monthNumber;
};

...

Date d(30,Month::Sep(),2003);
|}}
이와 같은 방법으로 Month는 실행시간에 검사를 해야 할 내용을 컴파일 시간으로 옮길수 있다.

7.3. 항목 47. 비지역 정적(Non-local static) 객체는 사용되기 전에 초기화되도록 해야 한다.

만약 파일시스템이 초기화된후 디렉토리 시스템의 부분으로 초기화 되어야 한다면
반드시 그렇게 하도록 해야한다.

그러나 비지역 객체로 선언이 되어 있다면 초기화 순서를 정하기가 매우 힘들 것이다.
(비지역 객체는 전역이나 네임스페이스 영역이거나 클래스내 static, 파일 영역의 static에 정의된 경우이다.)

이럴 때는
{{|
FileSystem& theFileSystem()
{
static FileSystem tfs;
return tfs;
}

...

class Directory
{
...
public:
Directory()
{
// theFileSystem 함수를 호출하여 디렉토리 부분을 초기화 시킨다.
}
}
|}}
이렇게 함으로 반드시 Directory클래스가 초기화 되기전에 FileSystem을 초기화 시킬수 있다.

7.4. 항목 48. 컴파일러의 경고(Warning)에 주의를 기울여라.

C++ 언어는 유연하다.
{{|
class B
{
public:
virtual void f() const;
}

class D
{
public:
virtual void f();
}
|}}
이와 같이 재정의를 하였을 때, Error가 아닌 Warning을 만들어 낼수도 있다.

Error뿐만 아니라 Warning에도 주의를 기울이자. 잠재적인 문제 요소가 될수도 있다.

7.5. 항목 49. 표준 라이브러리를 잘 알아두자.

표준 라이브러리는 유용하다!

7.6. 항목 50. C++에 대한 이해를 넓혀라.

그냥 읽어 볼것.


8. Thread

그냥 보면 술술술 넘어 가는데.. 쓰려니 막막하구나.. guts__

Effective C++ 내용을 보면서 이미 항목 21 까지는 정리가 되어 있더군요.
그래서 그 이하 내용을 정리해 올리고 싶습니다. -- 다른학교컴퓨터공학부
이곳에는 특별히 허락을 하거나, 허락을 받을만한 사람이 없습니다. 언제든 환영입니다 :) --sun

안혁준형이 읽으라고 추천해 준 책.. 은 위키에 멋지게도 정리가 되어있군요. 전 빅뱅을 나가지 못하니 혼자서라도 열심히 해야겠습니다. - 권영기

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:10
Processing time 0.1579 sec