1.1. Item 1: Distinguish between pointers and references. ¶
- Item 1: Pointer와 Reference구별해라.
Pointers use the "*" and "->" operators, references use "."
~cpp char *pc = 0; char& rc = *pc;프로그래머 초급자면 알수 있는 바보 짓이다. 하지만 범하기 쉬운게 문제다.
~cpp string& rs; // Reference(참조)가 초기화가 되지 않아서 에러 string s("xyzzy"); // 이건 된다 신기하네 string& rs = s;아직 string써본적 한번도 없다. 반성..
~cpp void printDouble(const double& rd) { cout << rd; // rd에 관한 특별한 검사 필요 없다. reference니까. } //////////////////////////////////////////////////////////////////// void printDouble (const double* pd) { if (pd){ cout << *pd // pd가 null인지 검사 해야 한다. pointer니까. } }pointer의 유의 사항인 null에 관한 내용을 다시 알려준다.
~cpp string s1("Nancy"); // 이제는 무슨 퀴즈 같다. string s2("Clancy"); string& rs = s1; string* ps = &s1; rs = s2; // s1의 인자가 "Clancy" 로 바뀐다는 의미다. ps = &s2; // 그냥 포인터만 세팅하는 거다. // 이걸로 ps는 s2를 가리키고 s1에는 아무런 영향을 끼치지 않는다.사견: Call by Value 보다 Call by Reference와 Const의 조합을 선호하자. 저자의 Effective C++에 전반적으로 언급되어 있고, 프로그래밍을 해보니 괜찮은 편이었다. 단 return에서 말썽이 생기는데, 현재 내 생각은 return에 대해서 회의적이다. 그래서 나는 COM식 표현인 in, out 접두어를 사용해서 아예 인자를 넘겨서 관리한다. C++의 경우 return에 의해 객체를 Call by Reference하면 {} 를 벗어나는 셈이 되는데 어디서 파괴되는 것인가. 다 공부가 부족해서야 쩝 --;
지역함수 안에서 지역 객체를 생성하여 Reference로 리턴할 경우 지역 함수가 반한되면 지역 객체는 없어질 것이고, 리턴되는 Reference는 존재하지 않는 객체에 대한 다른 이름이 될 것이며, 경우에 따라서 컴파일은 될지모르나 나쁜 코드라고 하네요.-차섭-
오해의 소지가 있도록 글을 적어 놨군요. in, out 접두어를 이용해서 reference로 넘길 인자들에서는 in에 한하여 reference, out은 pointer로 new, delete로 동적으로 관리하는것을 의도한 말이었습니다. 전에 프로젝트에 이런식의 프로그래밍을 적용 시켰는데, 함수 내부에서 포인터로 사용하는 것보다 in에 해당하는 객체 사용 코딩이 편하더군요. 그리고 말씀하신대로, MEC++ 전반에 지역객체로 생성한 Refernece문제에 관한 언급이 있는데, 이것의 관리가 C++의 가장 큰 벽으로 작용하는 것이 아닐까 생각이 됩니다. OOP 적이려면 반환을 객체로 해야 하는데, 이를 포인터로 넘기는 것은 원칙적으로 객체를 넘긴다고 볼수 없고, 해제 문제가 발생하며, reference로 넘기면 말씀하신데로, 해당 scope가 벗어나면 언어상의 lifetime이 끝난 것이므로 영역에 대한 메모리 접근을 OS에서 막을지도 모릅니다. 단, inline에 한하여는 이야기가 달라집니다. (inline의 코드 교체가 compiler에 의하여 결정되므로 이것도 역시 모호해 집니다.) 아예 COM에서는 OOP에서 벗어 나더라도, 범용적으로 쓰일수 있도록 C스펙의 함수와 같이 in, out 의 접두어와 해당 접두어는 pointer로 하는 규칙을 세워놓았지요. 이 설계가 C#에서 buil-in type의 scalar형에 해당하는 것까지 반영된 것이 인상적이 었습니다.(MS가 초기 .net세미나에서 이 때문에 String 연산 차이가 10~20배 정도 난다고 광고하고 다녔었는데, 지금 생각해 보면 다 부질없는 이야기 같습니다.) -상민
1.2. Item 2 : Prefer C++ style casts. ¶
- Item 2 : C++ 스타일의 형변환을 권장한다.
~cpp (type) expressionC++ style cast
~cpp static_cast<type>(expression) const_cast<type>(expression) dynamic_cast<type>(expression) reinterpret_cast<type>(expression)
- static_cast<type>(expression)는 기존의 C style에서 사용하는 (type)expression 와 동일하다.
- const_cast<type>(expression)예제
~cpp class Widget{ ...} class SpecialWidget:public Widget{...} void update( SpecialWidget *psw); SpecialWidget sw; const SpecialWidget& csw = sw; update(&csw); // 당연히 안됨 const인자이므로 변환(풀어주는 일) 시켜 주어야 한다. update(const_cast<SpecialWidget*>(&csw)); // 옳타쿠나 update((SpecialWidget*)&csw); // C style인데 잘 돌아간다. // C++는 C와의 호환성 고려를 위한 것이므로 위와 같이 // 동작 하지만 명시적이지 못한면이 지적된다. Widget *pw = new SpecialWidget; update(pw); update(const_cast<SpecialWidget*>(pw)); // error! // const_cast<type>(expression) 는 // 오직 상수(constness)나 변수(volatileness)에 영향을 미친다. // 이런말 하면 다 가능한 듯 싶고 static_cast 와 차이가 없는 것 // 같은데 옆의 소스 처럼 down cast가 불가능하다.
위에 예제 이어서 생각
- dynamic_cast<type>(expression) 예제
~cpp Widget *pw ... update( dynamic_cast<SpecialWidget*>(pw)); // 옳다. // const_cast 가 down cast가 불가능 한 대신에 dynamic_cast 는 말그대로 // 해당 부모 객체의 포인터에 자식 객체가 가르켜 있으면 형변환이 가능하다. // 불가능 하면 null 을 반환해주는 기특한 역할이 핵심이다. void updateViaRef(SpecialWidget& rsw); updateViaRef(dynamic_cast<SpecialWidget&>(*pw)); // 옳다. // reference에서도 사용 가능하다? 그럼 불가능 할시의 처리는? // 이럴때는 예외(exception)을 발생시켜 준다.
- reinterpret_cast<type>(expression) 은 차후 다시 읽은뒤 정리한다.
- C 에서 지원하지 않을 경우 다음 매크로를 사용하여
~cpp #define static_cast(TYPE, TEXPR) ((TYPE) (EXPR)) #define const_cast(TYPE, TEXPR) ((TYPE) (EXPR)) #define reinterpret_cast(TYPE, TEXPR) ((TYPE) (EXPR))
이렇게 구현 해서 대비하는걸 추천한다. (dynamic_cast 는 C의 절차적 프로그래밍에서 등장 불가)
~cpp double result = static_cast(double, firstNumber)/ secondNumber; update(const_cast(SpecialWidget*, &sw)); funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);
1.3. Item 3: Never treat arrays polymorphically ¶
- Item 3: 절대로! 클래스 간의 다형성을 통한 배열 취급을 하지 말라
~cpp class BST { ... }; class BalancedBST : public BST { ... };이런 클래스를 선언했다. 그리고 다음과 같은 함수로 해당 클래스의 배열을 사용한다고 가정하자
~cpp void printBSTArray( ostream& s, const BST array[], int numElements) { for ( int i = 0; i < numElements; ++i ){ s << array[i]; } }그리고 다음과 같이 사용한다.
~cpp BST BSTArray[10]; ... printBSTArray(cout, BSTArray, 10); // 올바르게 작동한다. 아무 문제 없다. ////////////////////////////////////// BalancedBST bBSTArray[10]; ... printBSTArray(cout, bBSTArray, 10); // 과연 올바르게 작동하는가?
위의 두번째 호출의 클래스 상속의 다형적 성질을 이용한 함수 이용 즉
~cpp printBSTArray( cout, bBSTArray, 10 );은 정말 최악의 결과를 불러 들인다.
C++에서의 배열은 arrayi 의 의미는 *(array+i) 인데 이것의 주소 추적을 위해서 엄밀히 따지면
*(array+ ( i *sizeof(an object in the array) )
로 사용한다. 느낌이 오겠지! 당연히 상속시 child는 parent보다 큰 경우가 다반사이고 배열의 위치 추적이 엉망 진창이 되어 버린다.
*(array+ ( i *sizeof(an object in the array) )
로 사용한다. 느낌이 오겠지! 당연히 상속시 child는 parent보다 큰 경우가 다반사이고 배열의 위치 추적이 엉망 진창이 되어 버린다.
* 객체의 일관적인 삭제에 관해서도 알아 보면
~cpp void deleteArray( ostream& logStream, BST array[]) { logStream << "Deleting array at address " << static_cast<void*>(array) << '\n'; delete [] array; } BalanceBST *balTreeArray = new BalancedBST[50]; ... deleteArray(cout, balTreeArray); // 이것 역시 제대로 지워지리가 없다.자 위와 같이 객체의 지움을 담당하는 함수를 작성했을때 역시 문제가 있다.
~cpp delete [] array는 다음과 같은 코드로 교체되는것과 같다.
~cpp for( int i = the number of elements in the array -1; i>0; --i) { array[i].BST::~BST(); }부모 객체의 파괴자만 부르므로 역시 memory leak 현상을 일으킨다.
1.4. Item 4: Avoid gratuitous default constructors. ¶
- Item 4: 암시적으로 제공되는 기본 생성자를 피하라. 혹은 기본 생성자의 모호성을 파악하라.
예제에서 시작하자
~cpp class EquipmentPiece { public: EquipmentPiece(int IDNumber); ... }
해당 EquipmentPiece 는 기본 생성자가 부실(?) 하다. 이건 크게 3가지의 주제로 설명할수 있다.
- 첫번째 문제는 해당 클래스를 이용하여 배열을 생성 할때이다. . ( The first is the creation of arrays )
~cpp EquipmentPiece bestPieces[10]; EquipmentPiece bestPieces = new EquipmentPiece[10]; // 두경우 초기화 해줄 방도가 없어서 에러이다.이걸 이렇게 풀어 보자
~cpp int ID1, ID2, ID3, ... ID10; ... EquipmentPiece bestPiece[] = { EquipmentPiece(ID1), EquipmentPiece(ID2), EquipmentPiece(ID3), ..., EquipmentPiece(ID10), }하지만 이럴 경우에는 array를 heap arrays(heap영역 사용 array라는 의미로 받아들임)로의 확장이 불가능한 고정된 방법이다.
조금 더 일반적인 방법은 다음과 같이 pointer를 이용한 접근을 제시한다.
~cpp typedef EquipmentPiece* PEP; PEP bestPieces[10]; PEP *bestPieces = new PEP[10]; for ( int i = 0; i< 10; ++i) bestPiece[1] = new EquipmentPiece( ID Number );하지만 이러한 방법은 한눈에 봐도 문제가 예상된다. 바로 delete문제
두가지를 구체적으로 이야기 해보면, 첫번째로 for 문시에서 할당되는 객체의 숫자를 기억하고 있어야 한다. 잃어버리면 차후 resouce leak이 발생 할것이다. 두번째로는 pointer를 위한 공간 할당으로 실제 필요한 memory 용량보다 더 쓴다.
그럼 더 웃기는 방법을 제시해 보자. (이 책을 보고 있노라면, 이렇게도 쓸수 있다란걸 보여주는거 같다.)
일단 확보한 영역을 지우기는 쉽다. 이렇게
~cpp void *rawMemory = operator new[](10*sizeof(EquipmentPiece)); EquipmentPiece *bestPieces = static_cast<EquipmentPiece*>(rawMemory); for ( int i = 0; i< 10; ++i) new (bestPieces+1) EquipmentPiece ( ID Number ); // 이건 placement new 라고 하여 Item 8 에서 언급한다. // 처음 보고 놀랐다. 어서 사기야 하면서 --;; // 이 책 주특기다. 뒤에 설명하니까 그냥 넘어가 하는거 // 암튼 의미는 이해 갈것이다.참 거지 같은 짓 잘해 놓는다.
일단 확보한 영역을 지우기는 쉽다. 이렇게
~cpp delete [] rawMemory;역시나 이것도 delete에 관한 모호성을 제공한다. 문제시 되는 점은 rawMemory상에 배치된 EquipmentPiece의 destructor 호출 유무이다. 예상대로 destructor를 강제 호출해 줘야 한다. 그래서 위가 아니라, 이런식의 삭제가 된다.
~cpp for ( int i = 9; i>= 0; --i) bestPieces[i].~EquipmentPiece(); // 언제나 느끼는 거지만 C++을 방종을 가져다 줄수 있다. operator delete[](rawMemory);참고로
~cpp delete [] bestPieces; // 이렇게 지우는건 new operator가 건들지 않아서 불가능하다.
- 두번째 문제는 많은 template class(특히 STL에서) 들에게 아픔을 안겨준다.
~cpp template<class T> class Array{ public: Array(int size); ... private: T *data; }; template<class T> Array<T>::Array(int size) { data = new T[size]; .... }첫번째에서 제기된 문제가 이번에는 template class 내부에서 일어 나고 있는 셈이다. 거참 암담한 결과를 초례한다. 문제는 이러한 template class 가 이제는 아예 STL같은 library로 구축되었단 사실. 알아서 잘 기본 생성자 만들어 적용하시라. 다른 방도 없다.
DeleteMe 이 부분이 부실하다 차후 보강 필요
- 세번째(마지막) 문제는 virtual base class와 같이 기본 생성자를 가져야 하나 말아야 하나 미묘한 딜레마의 연출이다.