[EffectiveC++ 요약] 1장. C++에 왔으면 C++의 법을 따릅시다.







항목1. C++은 여러 언어의 연합체
= C
= 객체 지향 개념의 C++
= 템플릿
= STL
= 위 4가지로 구성


항목2. #define을 쓰려거든 const, enum inline을 떠올리자
Ex] #define TEST_Ratio 1.653 사용 시, 기호 테이블에 들어가지 않음 (Symbol Table)
= 소스 -> 전처리기 -> 컴파일러 순으로 진행 시,
= TEST -> 1.653으로 바꿔버려 어디에 오류가 있는지 찾기 힘듬
(사족) 컴파일 에러를 말하는지, Logic 에러를 말하는지 모르겠네

= TEST_Ratio가 사용된 만큼 메모리가 사용
(TEST_Ratio -> 1.653으로 바꾸기에, TEST_Ratio 등장 횟수 만큼 리소스 사용

= 매크로 대신 상수를 쓰는 방법으로 대체
(const double TestRatio = 1.653)

= 상수 포인터 정의하는 경우 주의 사항
= char* 형식 문자열 상수 정의시 , ptr과 value까지 const로 선언하는 것이 보통
Ex]
const char* const AuthorName = "Test Author";
-> const char* (Ptr에 대한 Const)
-> const AuthorName (Value에 대한 Const)

char* 보단 std::string을 쓰는것이 좋다
const std::string AuthorName("Test Author");


항목2-1. 클래스 상수 정의
Ex]
class Player{
private:
 static const int NumTurns = 5; //상수 선언
 ...
};
(사족) numTurns는 객체가 사용되기 이전에 PreProcess 단계에서 만들어지기에 static이 필요할것으로 보인다. (미리 메모리에 올라가 있어야하니...)

= NumTurns는 선언 된 것, 정의가 아니다. (값이 5로 선언)
= 컴파일 시, NumTurns에대해 정의를 달라고 하는데, 이 경우 구현 파일 쪽에 정의를 선언한다.
(컴파일러 버전에 따라 틀림, 정의 시점에 해당 값을 선언하는 경우가 낫다)

Ex]
Player.h
class Test Player{
private:
   static const double NumTurns; // 정적 클래스 상수 선언
};

Player.cpp
const double Player::NumTurns = 1.35 // 정적 클래스 상수의 정의



항목2-2. enum hack 기법 (나열자 둔갑술) 
= 만약 컴파일 시점 배열을 선언할 일을 만든다면? enum으로 작업을 하자
class Player{
 private:
   enum { NumTurns = 5 };

   int scores[NumTurns];
};

= 해당 기법은 const 보다 #define에 가깝다.
Ex]
1. const int a = 5, <- 주소 획득 가능
2. 위 enum NumTurns <- 주소 획득 불가
3. #define <- 주소 획득 불가
= 정수 상수의 주소를 얻는 것이나 참조자를 쓰는것이 싫다면, enum은 좋은 선택지
(#define처럼 쓸데없는 리소스 할당을 하지 않는다)

= Enum Hack기법은 템플릿 메타프로그래밍의 핵심 기법

항목2-3. #define의 오용 사례
= 매크로 함수, 호출 오버헤드를 일으키지 않는 매크로 구현
Ex]
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
= 인자마다 괄호를 씌워 주어야한다. (표현식을 넘길 때 문제)


= 괄호가 있어도 문제가 발생
Ex]
int a = 5, b = 0
1. CALL_WITH_MAX(++a, b) // A가 두번 증가
2. CALL_WITH_MAX(++a, b+10)  // A가 한번 증가
(사족) 왜 1번은 두번 증가일까.  초기 인자로 넘어가기 전 한번 증가, 비교 후, (a)리턴 시 한번 더 증가,

1. (a,b)
2. f((a) > (b) ?
3. (a) : (b)

2에서 ++a 연산 진행
3에서 ++a 연산 진행
= 값으로 넘어오지않고 연산식으로 넘어오기에 계속 증가가되버리는 케이스

(사족) 2번의 경우 b가 더 크기에, 비교식에서만 증가

= 좀더 나은 방안은?
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
  f ( a > b ? a : b);
}
= T, 즉 자료형이 무엇인지 모르기에, Paramterter 선언 시, 상수 객체 참조자를 쓴다.
(사족) Object가 넘어 올 수 있기 때문에, 상수 객체 참조자를 쓰는듯 하다. (복사생성으로 인함) 

= 소스 코드 내 괄호 필수 아님
= 인자 여러번 평가 위험 없음
= 함수의 유효범위 및 접근 규칙을 그대로 따라간다.

= const, enum inline을 활용하면 #define 경우가 많이 줄어듬.
= 단순 상수 쓸 때, #define보다 const 객체 or enum 을 우선 생각
= 함수처럼 쓰이는 매크로 생성 시, #define 보다 인라인 함수


항목3. 낌새만 보이면 const를 들이대자 
= '의미적인 제약(const 키워드 붙엇을 경우, 외부 변경 불가)'을 소스코드 수준에서 붙이는 점과, 컴파일러가 이를 지켜준다는 점을 활용

= 클래스 바깥에서 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는데 사용

= static 선언 객체에도 const 가능
(클래스 내부에서 정적 멤버, 비정적 데이터 멤버 모두 상수 가능)

= 포인터 자체를 상수, 포인터가 가르키는 데이터를 상수로 지정 가능
Ex]
char greeting[] = "Hello";
1) char* ptr = greeting; // 비 상수 데이터, 비상수 포인터
2) const char* p = greeting; // 상수 데이터, 비상수 포인터
(사족) ptr 변경은 가능, 데이터는 불가? 테스트 필요....

3) char* const p = greeting; // 비상수 데이터, 상수 포인터
(사족) ptr변경 불가, 데이터는 가능?

4) const char* const p = greeting // 상수 포인터, 상수 데이터

= const가 * 왼쪽이면, 가르키는 대상이 상수
오른쪽이면, 포인터 자체가 상수

= 아래는 동일한 내용
void f1(const Widget *pw)
void f2(Widget const *pw)
= 두 매개변수는 동일하다, (가르키는 대상이 상수, *왼쪽에 const선언되었기에)

= STL 반복자는 기본적인 T* 포인터와 흡사

= Iterator, Ptr에 대한 상수화
const std::vector<int>::iterator iter = vec.begin()
(위 내용은 T* const iter와 같다고 생각하면된다.)

*iter = 10; // value를 수정한다.
++iter; // ptr을 한단계 이동 (불가, ptr이 상수)
= value에 대한 const를 지정위해서는 const_iterator를 써야한다.


Iterator, Value에 대한 상수화
std::vector<int>::const_iterator cIter = vec.begin();
(*cIter) = 10; // value에 대한 const지정으로 에러
++cIter  // 가능, ptr는 상수가아니다.



= *Return Object에 대한 Const
Ex]
const Rational operator*(const Relational& lhs, const Rational& rhs)

 Relational a, b, c;
...
(a * b ) = c 일때,

1. a * b 연산자 오버로딩 발생
2. 두 연산결과에 대한 대입 연산 발생
(사족) 이런 상황이...?

const로 오버로딩 설정이 되어있으면, 이런 상황이 방지된다.
(사족) 2. 대입 연산 과정에서, const로 선언되었기에 값에 대한 수정이 일어나면 안되기 때문일거같다.



= 상수 멤버 함수
해당 멤버 함수가 상수 Object에서만 호출되는 함수

선언 이유.
1. 클래스 인터페이스 이해하기 좋게 하기 위함
= 내부에서 값이 변경되는 함수는 무엇이고 안되는 함수는 무엇인지 명시적 정의

2. 키워드를 통해 상수 객체를 사용할 수 있게 하자
= 핵심적

= C++ 의 성능을 높이는 기법
상수 객체에 대한 참조자로 진행

위 기법을 쓸려면 상수 멤버함수가 있어야함
Ex]
class TextBlock{
public:
  ...

(1) const char& operator[](std::size_t position) const // 상수객체에 대한 operator[]
{ return text[position]; }

(2) char& operator[](std::size_t position)
{ return text[position]; }

private:
  std::string text;
};

TextBlock tb("Hello");
std::cout << tb[0];  // TextBlock::operator[]의 비 상수 멤버 호출
(2) Function 실행


const TextBlock ctb("World")
std::cout << ctb[0];  // TextBlock::Operator[]의 상수 멤버 호출
(1) Function 실행

(사족) Object가 Const이면, 강제하는 것이기에 충분한 상황판단 후 써야할듯 하다.

실제 프로그램에서 상수 객체가 생기는 경우
1. 상수 객체에 대한 포인터
2. 상수 객체에 대한 참조자로 객체 전달될 때

(아래형식으로 상수객체 전달사용하는듯)
void print(const TextBlock& ctb) // ctb는 상수 객체 사용
{
  std::cout << ctb[0];   // TextBlock::operator[] 의 상수멤버 호출
  ....
}

위 형식으로 상수 객체와 비상수 객체의 쓰임을 달리하면 좋다

Case
Ex]
std::cout << tb[0]  // 문제 없음, 비상수 버전의 TextBlock 객체를 읽습니다.
tb[0] = 'x' // 문제 없음, 비상수 버전의 TextBlock 객체를 쓴다.

std::cout << ctb[0] // 문제 없음 상수 버전의 TextBlock 객체를 읽음
ctb[0] = 'x' //컴파일 에러, 상수 버전의 TextBlock 객체 쓰기 안됨


ctb[0] = 'x'의 경우, operator[]의 반환 타입(return type) 때문에 생긴 것
const char& 타입에 대한 연산을 시도했기 때문에 생긴 것

= 상수 멤버로 되어있는 operator[]의 반환 타입이 const char& 이기 때문!


항목3-1. 상수성의 개념
비트 수준 상수성(bitwise constness)
= 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 const임을 인정하는 개념
(사족) 비트 수준에서 바뀌지 않아야 한다는 의미 일까?

= 비트수준 상수성 검사를 통과하는 멤버함수들이 적지 않다
-> 가르키는 대상을 수정하는 멤버 함수들 중 상당수가 이 경우 속함
Ex]
class CTextBlock{
public:
  ...
  char& operator[](std::size_t position) const // 부적절한 operator[] 선언, 비트수준 상수성이 있어 허용됨
  { return pText[position]; }

private:
  char* pText
};


위 operator는 상수 멤버함수로 선언되어있음.
그런데, 해당 객체 내부 데이터 참조자로 반환

아래에 대해 컴파일러 단계에서는 잡아낼 수 없다.
const CTextBlock cctb("Hello") // 상수 객체 선언
char* pc = &cctb[0] // 값 확인

*pc = 'J'; // 상수 객체인데, 값이 수정되는 문제




논리적 상수성(logical constness)
위를 보완하는 대체 개념
=> 상수 멤버라고 해서 객체의 한 비트도 수정할 수 없는 것이 아닌, 몇 비트는 바꿀수 있게... 
=> 사용자 측에서 알아채지만 못하게하자
=> 그렇다면 상수 멤버 자격이 있는 것

class CTextBlock{
public:
 ... 
 std::size_t length() const;

private:
 char *pText;
 mutable std::size_t textlength; // 직전 계산한 텍스트 길이
 mutable bool lengthIsValid; // 이게 유효함?
};

std::size_t CTextBlock::length() const
{
  if ( !lengthIsValid )
  {
    textLength = std::strlen(pText); // 상수 멤버 함수 안에서는 textLength, lengthIsValid에 대입 불가
    lengthIsValid = true;
  }
  return textLength;
}



textLength = std::strlen(pText)
lengthIsValid
두 멤버는 mutable이 선언되지 않으면 const객체 선언 불가
(const하지 않으니...)

(사족)이처럼 부분 정도는.. 바꿀 수 있게 하는거 같다..



항목3-2. 상수 멤버 및 비상수 멤버 함수에서 코드 중복을 피하는 방법
= mutable은 나쁘지 않은 방법이지만, 코드 중복이 생김...

const char& operator[] ...
{
  // 경계 검사
  // 접근 데이터 로깅
  // 자료 무결성 검증

  return text[position];
}

char& operator[] ...
{
  // 경계 검사
  // 접근 데이터 로깅
  // 자료 무결성 검증

  return text[position];
}


각 메소드 마다 이런 코드들이 필요해지는 상황...
별도의 멤버로 빼두면?

그래도 중복은 여전함. (함수 호출이 두번...)
솔직히
한번만 구현하고 두번 사용하는것이 제일 깔끔하다...

캐스팅 개념은?
=> 좋지않은 아이디어 추후 얘기.

결론
= 캐스팅이 필요하긴 하지만 안정성도 유지하면서 코드 중복을 피하는 방법은....


비 상수 operator[]가 상수 버전을 호출하도록 구현
class TextBlock
{
public:
 const char& operator&[] ( std::size_t position ) const
 {
   위와 동일
   return text[position];
 }

 char& operator&[] ( std::size_t position )
 {
    return const_char<char&> // operator[] 반환 타입에 캐스팅 적용, const 붙임
   (static_cast<const TextBlock&> // *this의 타입에 const 붙임
     (*this)[position] // op[]의 상수버전 호출
   );
 }
};

위 작업은
= 비 상수 operator가 상수 operator가 불려야 하는 것
= 내부적으로 operator[]로 호출하면 재귀적으로 돌기에 안됨.
= *this를 타입캐스팅 해버린다.

첫번째 캐스팅, static_cast<const TextBlock&>는 *this에 const를 붙이는 캐스팅
두번째 캐스팅, const_char<char&>는 operator[]의 반환값에서 const를 떼어내는 캐스팅
(사족) 굳이 이렇게까지 Rule을 지켜야하는지는 의문...


상수 멤버 함수는 객체의 논리적인 상태를 바꾸지 않겠다고 컴파일러와 논의된 함수
비상수 멤버 함수는 약속을 굳이 하지 않음

상수멤버에서 비 상수 멤버 호출 시, 이 룰이 지켜지지 않게되어 위험할 수 있음


항목3-3. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
= 초기화 되지 않은 값을 읽도록 내버려 두면, 정의되지 않은 동작이 그대로 흘러나옴
( 쓰레기값이 채워지는 케이스가 있기 때문... 일거같다 )

= 대입(assignment)와 초기화(initialization)을 구분지어서 진행하자
(사족) 대입의 경우에는 값의 Setter와 동일한 일을 할거 같다.
(사족) 초기화의 경우에는 값의 초기화를 담당한다.

= 생성자에서 일을 진행하자...
생성자에서 값, 객체를 초기화 할때, 멤버 initializer를 사용하면 더 깔끔

TestEntry::TestEntry(.... const std::list<PNumber>& phones)
: ...,
 thePhonse(phones)
{
}

기존 본문에서 할당 시,
1. Parameters phones에 복사생성
2. Paramteres Phones -> thePhonse에 복사 생성


두번의 복사생성이 일어나는데,
위 방식은 한번의 복사 생성이 한번만 일어남

멤버 initializer의 경우에는 파라미터를 바로 할당하는 형식으로 진행된다.
기본 생성자도 아래의 방식으로 진행하는것이 좋다

TestEntry::TestEntry()
: theName(),
  theAddress(),
  numTimesConsulted(0)
  thePhonse(phones)
{}

멤버 초기화 리스트에 있냐 없냐에 따라 의도하는 동작이 될수 있음

멤버 초기화 리스트의 의무사항
= 상수 or 참조자 로 되어있는 멤버의 경우 초기화 리스트로 해야함
Ex]
class TestEntry{
public:
 const char* test;
 TestEntry(const char* psTest)
 : test(psTest)
 {}
};
(사족) 테스트는 안해봣지만, 위 형식일듯하다...



항목 3-4. non-static, static 객체 초기화 순서
= static 객체는 생성된 시점부터 끝날 때 까지 살아있는 객체가 됨.
(스택, 힙, 객체는 애초에 정적 객체가 될 수 없다)

static 객체
1. 전역 객체
2. 네임스페이스 유혀범위 정의 객체
3. 클래스 안 static 선언 객체
4. 함수 안에서 static 선언 객체
5. 파일 유효범위에서 static 정의 객체

함수 안 정적 객체는 지역 정적 객체(함수의 지역성을 가지기에...)

나머지는 비지역 정적 객체 라고한다.

다섯 종류의 객체, 즉 정적 객체는 프로그램 끝날때 자동 소멸된다.
main()함수의 실행 끝날 때, 소멸자 호출


번역 단위(translation unit)는 objectfile 만드는 방탕이 되는 소스코드 (.cpp, .c의미하는듯..)
= 번역단위에서는 정의된 비지역 정적 객체들의 초기화 순서는 정해져있지않음.

즉 Compile 하는 순서대로 초기화가 될수도, 안될수도 있다는 것...
= 위를 강제할려면... 비지역 정적 객체를 하나씩 맡는 함수를 준비, 이 안에 객체를 넣는 것
비지역 정적 객체 -> 지역 정적 객체로 변환하는 것

class FilsSystem{ ... } ;

FileSystem& tfs()
{
 static FileSystem fs;
 return fs;
}

Directory::Directtory( params )
{
 ...
 std::size_t disks = tfs().numDisks();
 ...
}

함수로 쓰면, 함수에서 초기화를 하기에, 순서에 보장을 가져올수 있다.

다중 스레드의 경우에는 문제가 될 수 있음.
(비상수 정적 객체는 시한폭탄... )

항목 3-4의 정리
1. 멤버가 아닌 기본 제공 타입 객체는 직접 초기화하자
2. 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트 사용
3. 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서 염두






































































































































댓글

이 블로그의 인기 게시물

윤석열 계엄령 선포! 방산주 대폭발? 관련주 투자 전략 완벽 분석

대통령 퇴진운동 관련주: 방송·통신·촛불수혜주 완벽 분석

키움 OPEN API MFC 개발 (1)