반응형
반응형

0. 요약

  1. operator= 에서 자기대입에 대한 처리를 반드시 하자
    • 일치성 검사를 통해 자기대입일 경우 처리하지 않도록 하는 방법
    • 본래의 사본을 만들어 할당받는 방법
    • 사본을 swap하는 방법

1. 자기 대입의 예시

  1. 자기 대입이란?
    • 어떤 객체가 자기 자신에 대해 대입연산자를 적용하는 것
class widget{...};
Widget w;
w = w;
  1. 배열 또는 컨테이너 순환 중 자기 대입

    • i와 j가 같다면 자기 대입
a[i] = a[j];
  1. 중복 참조로 인해 자기 대입 발생
    • 중복 참조 : 여러 곳에서 하나의 객체를 참조하는 상태
    • px와 py가 가리키는 대상이 같으면 자기 대입
*px = *py;

2. 자기 대입 문제

  1. 정상적인 경우
    • 대입 연산 시 기존의 pb 포인터 삭제
    • rhs의 pb포인터 값을 재할당 및 복사함
  2. 자기 대입일 경우
    • 대입 연산시 기존의 pb포인터 삭제
    • rhs의 pb포인터도 삭제되어 버림
    • 대입 후에 비어 있는 Bitmap이 할당됨
class Bitmap{...};
class Widget{
public:
   Widget& operator=(const Widget& rhs);
private:
   Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs){
   delete pb;
   pb = new Bitmap(*rhs.pb);
   return *this;
}

3. 해결 방법

  1. 대입 연산자에서 일치성 검사 수행
    • 자기 대입이 일어나는 경우는 극히 적음
    • 하지만 대입할 때 마다 일치성 검사를 하므로 효율 떨어짐
    • 또한 new 단계에서 예외 발생 시 삭제된 pb만 남음
Widget& Widget::operator=(const Widget& rhs){
   if (this == &rhs) return *this; // 일치성 검사 수행
   // 이후 과정 동일
   delete pb;
   pb = new Bitmap(*rhs.pb);
   return *this;
}
  1. 삭제 전에 본래의 pb를 복제
    • new 단계에서 예외 발생해도 pb 유지 가능
    • 복제를 통해 자기 대입에 대한 방어도 가능함
Widget& Widget::operator=(const Widget& rhs){
   Bitmap *pOrig = pb; // 원래 pb를 복제
   pb = new Bitmap(*rhs.pb);  // rhs의 pb를 대입
   delete pOrig;  // 원래 pb를 삭제
   return *this;
}
  1. 복사 후 바꾸기 방법(copy and swap)
    • 예외 안정성과 자기대입 안정성을 동시에 가진 operator= 구현 방법
    • 29 항목에서 확인
    • 방법 1과 같이 rhs 사본을 만들어 swap하는 방법
    • 방법 2와 같이 값에 의한 전달 시 사본이 만들어지는 특징을 살려 바로 swap하는 방법
class Widget{
   void swap(Widget& rhs);
};

// 방법 1.
Widget& Widget::operator=(const Widget& rhs){
   Widget temp(rhs); // rhs 사본 만듬
   swap(temp); // *this를 사본과 맞바꿈
   return *this;
}

// 방법 2. 항목 20 참고
Widget& Widget::operator=(Widget rhs){
   swap(rhs);
   return *this;
}

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 대입 연산자는 *this 참조자를 반환!

1. C++ 기본 자료형의 대입 예시

  1. x = y = z = 15
    • (z = 15) => z에 15가 대입
    • y = z => y에 z(15)가 대입
    • x = y => x에 y(15)가 대입
    • 결론적으로 x,y,z에 모두 15가 대입된다.

2. 사용자 정의 타입도 위의 예시가 가능하게 하자!

  1. 대입 관련 연산자(=, +=, -=, *=)
    • *this를 return
    • 객체의 참조자를 반환
class Widget{
public:
   Widget& operator=(const Widget& rhs){
      ...
      return *this;
   }
}

Widget a,b,c;
a = b = c; // (O) 가능함

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 생성 소멸 과정 중에는 가상 함수를 호출 하지 말자!

1. 생성/소멸 중 가상 함수를 호출하면 안되는 이유

  1. BuyTransaction 객체 생성 시 생성자 호출 순서
    • 기본 클래스 생성자 호출 => 파생 클래스 생성자 호출
  2. 기본 클래스 생성자 호출 시점에 객체 타입은 기본 클래스!
    • 호출 되는 가상 함수는 모두 기본 클래스 타입으로 결정됨
    • 런타임 타입 정보 사용 요소(dynamic_cast, typeid 등) 사용 시에도 기본 클래스 타입으로 취급
  3. 마찬가지로 소멸자에 대해
    • 파생 클래스 소멸자 호출 => 기본 클래스 소멸자 호출
    • 기본 클래스 소멸자 호출 시점에 객체 타입은 기본 클래스
  4. 기본 클래스 타입인 이유
    • 기본 클래스가 생성되는 시점에 파생 클래스 데이터는 미초기화 된 상태
    • 만약 기본 클래스에서 파생 클래스 함수를 호출 할 수 있다면, 미초기화된 데이터 사용 가능함
    • 따라서 C++에서 이런 실수를 하지 못하도록 막음
  5. 결론적으로
    • Transaction 생성자에서 호출되는 logTransaction 함수는 BuyTransaction 것이 아니라, Transaction의 것이 호출됨
// 주식 거래 기본 클래스
class Transaction{
public:
   Transaction();
   // 로그 타입에 따라 달라지는 로그 기록을 만들기 위해 virtual 함수 정의
   virtual void logTransaction() const = 0;
}

Transaction::Transaction(){
   logTransaction(); // 로깅하기 시작함
}

// 주식 거래 매수 클래스
class BuyTransaction : public Transaction{
public:
   virtual void logTransaction() const;
}

// 주식 거래 매도 클래스
class SellTransaction : public Transaction{
public:
   virtual void logTransaction() const;
}

// 사용
BuyTransaction b;
  1. 위의 예시는 상대적으로 발견 쉬움

    • 소멸자에 가상함수가 있는 경우 경고 메시지를 주는 컴파일러도 있음
    • Transaction의 logtTransaction함수가 순수가상 함수이기 때문에 링킹 에러 발생
  2. 아래와 같은 형태면 발견 어려움

    • 만약 여러 생성자로 인해 공통 작업을 별도 함수로 정의
    • 해당 함수에서 가상함수 호출
    • 컴파일러에서 오류를 알 수 없음
class Transaction {
public:
   Transaction(){
      init();
   }
   virtual void logTransaction() const = 0;
private:
   void init(){
      logTransaction();
   }
}

2. 해결 방법

  1. 가상 함수를 비가상함수로 변환
    • 가상 함수이면, 각 객체에 맞게 정보를 알 수 있다.
    • 하지만 비가상함수로 변경되면서 필요한 정보를 전달 받아야함
  2. 파생 클래스 생성자로부터 필요한 정보를 기본 클래스 생성자가 전달받아야 함
    • logInfo 정보를 전달 받아 각 타입에 맞게 logTransaction 함수를 수행함
  3. logInfo를 만들어 주는 createLogString static 함수 생성
    • 기본 클래스 생성자 쪽으로 넘길 값을 생성하는 용도
    • 멤버 초기화 리스트가 많은 경우 편리함
    • static 멤버이기 때문에 미초기화된 BuyTransaction 데이터를 건드릴 위험이 없다.
class Transaction {
public:
   explicit Transaction(const std::string& logInfo);
   void logTransaction(const std::string& logInfo) const;
}

Transaction::Transaction(const std::string& logInfo){
   logTransaction(logInfo);
};

class BuyTransaction: public Transaction{
public:
   BuyTransaction( params )
      : Transaction(CreateLogString(params)) {}
private:
   static std::string createLogString(parmas);
};

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 소멸자에서 예외가 발생하지 않도록 하자!
    • 예외는 소멸자가 아닌 함수에서 처리하도록 하자!
  2. 소멸자에서 호출되는 함수가 예외 가능성이 있다면..
    • 소멸자에서 삼켜버리던지, 프로그램을 종료하던지 처리 필요

1. 소멸자에 예외가 있으면 안되는 이유

  1. Widget 객체를 담는 10 크기를 갖는 벡터 v 예시
    • 함수 사용 후 10개 만큼 메모리가 해제됨
    • 만약 1번째 Widget에서 예외가 발생했다면??
    • 2~10번째 Widget의 메모리가 누수됨!
void doSomething(){
   std::vector<Widget> v;
} // 여기서 v는 메모리가 삭제됨
  1. DBConnection 객체 예시
    • DBConnection은 create 함수와 close 함수를 제공
    • DBConn은 소멸자에서 DBConnection의 close 함수 호출
    • 만약 close하면서 예외가 발생했다면???
    • 예외가 전판되어 소멸자에서 예외가 나가도록 내버려 두게됨!
// DB 연결을 담당하는 클래스
class DBConnection{
public:
   static DBConnection create();
   void close();
}

// DBConnection 객체를 관리하는 클래스
class DBConn{
public:
   ~DBConn(){
      db.close(); // DBConnection 객체 db의 close함수 호출
   }
private:
   DBConnection db;
}

void doSomeThing{
   vector<DBConn> vecDbc;
   vecDbc.push(DBConnection::create());
   vecDbc.push(DBConnection::create());
}// vecDbc 메모리가 해제됨

2. 해결 방법

  1. close에서 예외가 발생하면 프로그램을 바로 끝내라
    • 에러 발생 후에 프로그램 지속이 어려운 경우 괜찮은 선택
DBConn::~DBConn(){
   try{
      db.close();
   } catch(...) {
      //close 호출 실패 로그 작성
      std::abort();
   }
}
  1. close를 호출한 곳에서 일어난 예외를 삼켜라!(무시하라)
    • 예외를 무시한 뒤라도 프로그램이 신뢰성 있게 실행 될 수 있어야 함
DBConn::~DBConn(){
   try{
      db.close();
   } catch(...){
      //close 호출 실패 로그만 작성
   }
}
  1. close 호출 책임을 소멸자에서 사용자로 넘겨라!
    • DBConn에서 close 함수를 제공하여 예외 발생 시 사용자가 예외에 대해 대응!
    • 만약 닫히지 않았으면 소멸자에서 한번 더 닫을 수 있음
    • 소멸자에서 호출하는 close 마저 실패하면 '끝내거나, 삼키거나'를 선택해야함
class DBConn {
public:
   void close(){
      db.close();
      closed = true;
   }
   ~DBConn(){
      if(!closed){
         try{
            db.close();
         } catch(...){
            //close 호출 실패 로그 작성
         }
      }
   }
private:
   DBConnection db;
   bool closed;
}

3. 해결 포인트

  1. 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다!
    • 소멸자에 있다면 사용자는 예외에 대처할 기회가 없다.

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 가상 함수를 하나라도 가진 클래스는 가상 소멸자를 가져야한다.!

  2. STL 컨테이너, string 타입은 다형성을 갖도록 설계 되어 있지 않다.

    • 상속해서 사용시 주의가 필요하다!
  3. 추상 클래스를 만들 때 마땅한 순수 가상함수가 없으면, 소멸자를 순수 가상함수로 만들자!

  4. 기본 클래스로 설계되지 않거나, 다형성을 갖지 않도록 하려면 가상 소멸자를 만들지 말라!

1. 가상 소멸자의 필요성

  1. 예시 상황
    • TimerKeeper라는 인터페이스(기본 클래스)를 만듦
    • 각 시계를 TimerKeeper를 상속받아 구현
    • 팩토리 함수(getTimeKeeper)를 만들어 다양한 시계를 기본 클래스로 반환(TimeKeeper)
    • 모두 사용 후 메모리 반환(delete)
//시계 인터페이스
class TimeKeeper {
public:
   TimeKeeper();
   ~TimeKeeper();
}

// 시계 구현
class AtomicClock: public TimeKeeper {...};
class WaterClock: public TimeKeeper {...};
class WristClock: public TimeKeeper {...};

// 팩토리 함수
TimeKeeper* getTimeKeeper();


//사용 부
TimeKeeper *ptk = getTimeKeeper();
...(사용)
delete ptk; // 메모리 삭제
  1. 문제 사항(부분 소멸 문제 발생)
    • 메모리 해제 시 각 시계(Atomic, Water, Wrist 등)의 소멸자 호출되지 않음
    • 메모리 해제시 기본 클래스 부분(TimeKeeper) 만 삭제됨
  2. 해결 방법
    • 기본 클래스에 가상 소멸자를 붙여라!
class TimeKeeper {
public:
   TimeKeeper();
   virtual ~TimeKeeper();
};
  1. 원리

    • 아래 가상 함수 테이블 참고
    • 소멸자를 virtual 선언하면, 가상함수 테이블 생성됨
    • 메모리가 해제 될 때 가상함수 테이블을 참고하여 알맞은 크기 만큼 메모리가 해제됨
  2. 간단한 적용 방법

    • 가상 함수를 하나라도 가진 클래스의 소멸자는 가상 소멸자로 정의!
    • 가상 소멸자가 없는 클래스는 기본 클래스 의지 없음!

2. 가상 함수 테이블

  1. 가상 함수 테이블(vtbl, virtual table)
    • 가상 함수를 가리키는 함수 포인터를 저장하는 공간
    • 각 클래스가 가지고 있음
  2. 가상 함수 테이블 포인터(vptr, virtual table pointer)
    • 객체가 생성 될 때 가상 함수 테이블을 가리킴
  3. 가상 함수를 가지고 있는 클래스는 가상 함수 테이블을 가짐
    • 아키텍처(32,64bit)에 따라 클래스 타입의 크기가 커진다.
    • 아래 예에서 Point의 크기는 원래 8byte(int가 4byte라면)
    • 하지만 가상 함수를 가졌다면 16byte가 된다(64bit 운영체제에서 포인터는 8bytef)
class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};
  1. 예시
    • Base는 __vptr 포인터를 가지고, D1,D2는 상속받는다.
    • 각 클래스는 아래 그림과 같이 각각 vTable을 가진다.
    • 객체가 생성 될 때 각자에 맞는 vTable을 가리킨다.

3. 가상 소멸자가 없는 클래스를 조심하자

  1. STL 컨테이너 타입, string 타입은 가상 소멸자가 없다.
    • 아래와 같이 구현 시 메모리 누수 발생
    • string쪽은 메모리 해제 되지만, SpecialString 자원 누수됨
    • SpecialString의 소멸자가 호출되지 않음!
class SpecialString: public std::string{...};

SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
ps = pss;
delete ps;
  1. STL 컨테이너 타입, string 타입은 다형성을 갖도록 설계되지 않았다!
  2. 나만의 컨테이너를 만들 때 자제하자!

4. 순수 가상 소멸자의 용도

  1. 추상 클래스 만드는데 마땅한 순수 가상 함수가 없을 때
    • 어차피 추상 클래스는 기본 클래스로 사용하기 위한 목적
    • 기본 클래스는 가상 소멸자를 가져야함
    • 그러므로 가상 소멸자를 순수 가상 함수로 만들자!
class AWOV{
public:
   virtual ~AWOV() = 0;
}

// 순수 가상 소멸자 정의 반드시 필요!
AWOV::~AWOV() {}
  1. 주의 할 점

    • 순수 가상 소멸자의 정의가 필요함
    • 없으면 링킹 에러 발생!
    • 이유는 아래 소멸자 호출 순서 참고!
  2. 소멸자 호출 순서

    • 파생 클래스 소멸자 호출
    • 기본 클래스쪽으로 올라오면서 각 소멸자가 호출됨
    • 컴파일러는 ~AWOV 호출 코드를 만들기 위해 파생 클래스의 소멸자를 사용

참고

  1. Effective C++
  2. https://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
반응형
반응형

0. 요약

  1. 복사 생성자가 필요없으면 아래 방법을 사용하자
  2. private로 복사 생성자 / 대입 연산자를 정의
  3. 정의를 고의로 빼서 링킹 에러 발생
  4. boost 의 noncopyable 클래스를 상속받아 구현

1. 소개

  1. 부동산 클래스가 있다.
class HomeForSale{...};
  1. 모든 자산은 하나 밖에 없으니 객체 복사가 안되도록 하자
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // 이게 안되도록 하자!
h1 = h2; // 이것도 안되게 하자.

2. 객체 복사 막는 방법1(외부 호출 금지)

  1. private로 복사 생성자/대입 연산자를 정의
    • 컴파일러가 자동생성을 하지 않는다.
    • 외부로부터 호출을 차단할 수 있다.
    • 하지만 그 클래스의 멤버 함수와 friend 함수가 호출 가능하다!

3. 객체 복사 막는 방법2(링킹 에러)

  1. 정의를 안해버려서 링킹 에러를 발생 시키자
    • 사용자가 실수로 복사 생성자를 호출하면 링킹에러가 발생되어 방지 가능
    • c++의 iostream(ios_base, basic_ios, sentry)가 이렇게 구현됨
class HomeForSale{
private:
   HomeForSale(const HomeForSale&)  // 선언만 있음
   HomeForSale& operator=(const HomeForSale&);
}

4. 객체 복사 막는 방법3(컴파일 에러)

  1. 링킹에러도 싫다. 컴파일 에러를 발생하자!
    • 복사 생성자, 대입 연산자를 private로 하는 기본 클래스를 생성!
    • 기본 클래스를 private상속 받아 구현!
    • 복사를 시도하면 기본 클래스의 복사를 호출
    • 하지만 기본 클래스가 private이기 때문에 컴파일 에러 발생!
    • 부스트 라이브러리 noncopyable 클래스가 동일 내용!
class Uncopyable{
protected:
   Uncopyable() {}
   ~Uncopyable(){}
private:
   Uncopyable(const Uncopyable&);
   Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopable{...}

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 컴파일러는 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들 수 있다.

1. 컴파일러가 알아서 선언하는 멤버 함수

  1. 빈클래스가 선언되어 있으면 컴파일러는 아래 멤버 함수를 자동으로 생성한다.
    • 복사 생성자(copy constructor)
    • 복사 대입 연산자(copy assignment operator)
    • 소멸자(destructor)
    • 기본 생성자 : 생성자가 없을 때 생성됨
  2. 자동 생성된 함수는 public, inline함수이다.
class Empty{};
// 위와 같이 선언하면 아래와 같다.
class Empty{
public:
   Empty() {...}                            // 기본 생성자
   Empty(const Empty& rhs) {...}            // 복사 생성자
   ~Empty() {...}                           // 소멸자
   Empty& operator=(const Empty& rhs) {...} // 복사 대입 연산자
}
  1. 위 함수가 만들어지는 조건

    • Empty e1; => 기본 생성자, 소멸자 호출
    • Empty e2(e1); => 복사 생성자 호출
    • e2 = e1 => 복사 대입 연산자 호출
  2. 소멸자

    • 상속한 기본 클래스의 소멸자가 가상이 아니면 비가상으로 생성됨
  3. 복사 생성자 자동 생성

    • 아래 예는 복사 생성자가 자동 생성됨
    • string은 자체 복사생성자가 있으므로, no2의 nameValue는 no1의 nameValue값을 가짐
    • int형 no2의 objectValue는 no1의 비트를 그대로 복사해옴
template<typename T>
class NamedObject{
public:
   NamedObject(const char* name, const T& value);
   NamedObject(const std::string& name, const T& value);
private:
   std::string nameValue;
   T objectValue;
};

// 사용 부
NamedObject<int> no1("Smallest Prime Number", 2);
NmaedObject<int> no2(no1); // 복사 생성자 자동 생성됨
  1. 복사 대입 연산자가 생성되지 않는 예
    • 최종 결과 코드가 적법하지 않거나 resonable하지 않으면 자동 생성하지 않음
    • 아래 예시에서 자동생성되지 않고 컴파일 거부됨.
    • 참조자를 데이터 멤버로 가지고 있으면, 직접 복사 대입 연산자 정의 필요!
template<class T>
class NamedObject{
public:
   NamedObject(std::string& name, const T& value);
private:
   std::string& nameValue; // 참조자
   const T objectValue;    // 상수 멤버
};

// 사용 부
std::string newDog("Persephone");
std::string oldDog("Satch");

NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);

p = s;   // 가능한가?

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 기본 제공 타입의 객체는 직접 초기화.
    • 경우에 따라 자동으로 되기도하고, 안되기도 함
  2. 멤버 초기화리스트를 이용해 초기화하자!
    • 초기화리스트는 선언순서대로 하자!
  3. 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서문제는 피해서 설계하자!
    • 비지역 정적객체를 지역 정적객체로 바꾸면 됨!

1. 대입과 초기화의 차이

  1. 아래는 초기화가 아니라 대입!!
    • 생성자 전에 초기화 되었고, 대입된다.
ABEntry::ABEntry(const std::string& _name, const std::string& _address){
   this.theName = name;
   this.theAddress = address;
   this.numTimes = 0;
}
  1. 아래가 초기화
    • 멤버 초기화 리스트 사용
ABEntry::ABEntry(const std::string& _name, const std::string& _address)
   :theName(_name),
   theAddress(_address),
   numTimes(0)
{}
  1. 초기화가 대입보다 더 효율적

    • 대입의 경우 초기화 후 바로 대입하므로, 앞의 초기화는 쓸모없는 행위가 됨

2. 초기화 규칙

  1. 기본 제공 타입은 멤버 초기화 리스트에 넣는 쪽으로 습관을 들이자!
    • 매개 변수가 없더라도 0으로 초기화 하는 습관!
    • 특히 상수, 참조자 형태의 데이터 멤버는 반드시 초기화 필요!
ABEntry::ABEntry()
   : theName(),
   theAddress(),
   numTimes(0)

3. 생성자가 많을 때 팁

  1. 여러 생성자에서 초기화 리스트를 정의하면 불편함
  2. private 멤버 함수 하나를 만들어 초기화 하고, 모든 생성자에서 이 함수를 호출
    • 데이터 초기값을 파일에서 읽거나,
    • 데이터베이스에서 찾아오는 경우에 유용하게 사용 가능
  3. 하지만 이런 경우가 아니면 초기화 리스트가 효율적임

4. 초기화 순서

  1. 기본 클래스는 파생 클래스보다 먼저 초기화
  2. 클래스 데이터 멤버는 선언된 순서대로 초기화
    • 초기화 리스트에서 순서가 달라져도 선언 순서대로 초기화
    • 헷갈림 방지를 위해 초기화 리스트는 선언 순서와 맞추자!

5. 비지역 정적 객체의 초기화 순서

  1. 비지역 정적 객체의 초기화 순서

    • 별개의 번역 단위에 정의된 비지역 정적 객체들의 초기화 순서는 '정해져 있지 않다'
  2. 정적 객체란?

    • 생성된 시점부터 프로그램 끝날 때 까지 살아 있는 객체
    • 정적 객체는 프로그램이 끝날 때 자동으로 소멸됨
    • 아래 에서 4)를 지역 정적 객체, 4)를 제외한 나버지를 비지역 정적 객체라고 함
  3. 정적 객체 종류

    • 1)전역 객체
    • 2)네임스페이스 유효범위에서 정의된 객체
    • 3)클래스 안에서 static으로 선언된 객체
    • 4)함수 안에서 static으로 선언된 객체
    • 5)파일 유효점위에서 static으로 정의된 객체
  4. 번역 단위란?

    • 컴파일을 통해 하나의 목적파일을 만드는 바탕이 되는 소스 코드
    • 기본적으로는 소스파일 하나며, 그 파일이 #include하는 파일까지 합쳐서 하나의 번역 단위가 됨
  5. 아래의 경우에 문제 발생

    • 별도로 컴파일된 소스 파일이 두 개 이상 있고,
    • 각 소스 파일에 비지역 정적 객체가 한 개 이상 있을 경우
  6. 문제 발생 이유

    • 한쪽 번역 단위에 있는 비정적 객체가 초기화 되면서, 다른 쪽 번역 단위에 잇는 비지역 정적 객체를 사용하는데, 객체가 초기화 되어 있지 않을 수 있다!
  7. 예제

    • tfs가 tempDir보다 먼저 초기화되어 있지 않으면, 에러 발생
// 객체 선언부
class FileSystem{
public:
   std::size_t numDisks() const;
};

extern FileSystem tfs;

// 객체 사용부(다른 파일)
class Directory{
public:
   Directory(params);
};
Directory::Directory(params){
   std::size_t disks = tfs.numDisks();
}

// 실 사용부
Directory tempDir(params);
  1. 해결 방법
    • tfs가 tempDir보다 먼저 초기화 되게는 할 수 없다.
    • 설계적인 방법(Singleton)으로 해결!!
    • 비지역 정적 객체를 하나씩 맡는 함수 선언
    • 그 함수에서 객체 참조자를 반환
    • 사용자 쪽에서는 함수호출을 하여 사용
    • 비지역 정적 객체가 지역 정적 객체로 변경됨!
class FileSystem{...};
FileSystem& tfs(){
   static FileSystem fs;
   return fs;
}

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

Directory& tempDir(){
   static Directory td;
   return td;
}

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. const는 무조건 쓰고 보자.
    • const 함수, 함수의 매개변수(인자,리턴) 등
  2. 포인터에서 * 기호 기준 const의 위치로 상수 대상 판단
  3. mutable 키워드
    • 상수 멤버 함수에서 멤버 변수 변경 필요 시 사용
    • 가급적이면 사용하지 말자
  4. 상수 멤버 & 비상수 멤버 코드 중복 피하는 방법
    • 비상수 멤버함수에서 Casting 두 번 하여 상수 멤버를 호출하자

1. const 기초

  1. const의 장점
    • 어떤 값이 불가능 해야 한다는 개발자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있다.
  2. 포인터에서 const 이해
    • 포인터가 가리키는 대상이 상수 : const가 * 왼쪽에 위치
    • 포인터가 상수 : const가 * 오른쪽에 위치
char greeting[] = "Hello";

char *p = greeting;              // 비상수 포인터, 비상수 데이터
const char *p = greeting;         // 비상수 포인터,    상수 데이터
char * const p = greeting;        //    상수 포인터, 비상수 데이터
const char * const p = greeting;   //   상수 포인터,     상수 데이터
  1. 포인터가 가리키는 대상을 상수로 정의하는 스타일
// 아래 둘 다 pw가 가리키는 대상을 상수로 정의
void f1(const Widget *pw);
void f2(Widget const *pw);

2. STL iterator에서 const

// iter 는 T* const 처럼 동작
const std::vector<int>::iterator iter = vec.begin();
*iter= 10;  //(O) => 가리키는 대상 변경 가능
++iter;     //(X) => 포인터 위치 변경 불가

// cIter는 const T* 처럼 동작
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;   //(X) => 가리키는 대상 변경 불가
++cIter;       //(O) => 포인터 위치 변경 가능

3. 함수 선언에서 const

  1. 함수 반환값을 const로 정의하는 경우
class Rational {...};
const Rational operator*(const Rational& lhs, const Rational& rhs);

// const로 정의하지 않으면,
Rational a,b,c;
// 사용자가 오타가 났을 뿐인데,
if ((a * b) = c) // a*b의 결과에 operator= 호출 가능해짐.
  1. 매개변수에 가능한한 const를 항상 사용!

4. 상수 멤버 함수

  1. 멤버 함수에 붙는 const의 역할?
    • 해당 멤버 함수가 상수 객체에 대해 호출될 함수임을 알려줌
  2. 상수 멤버 함수가 중요한 이유
    • 객체를 변경 가능한 함수와 아닌 함수를 알려줄 수 있음
    • 상수 객체를 사용할 수 있게 하자
    • C++ 성능 향상 핵심 기법 중 하나가 '객체 전달을 reference-to-const로 진행하자' 이기 때문에 상수 객체를 많이 써야함
  3. 상수 멤버 함수 사용 예제
    • operator[]의 반환값은 char& 임을 주의
    • 만약 char 라면 tb[0] = 'x' 는 컴파일 에러 발생함
class TextBlock {
public:
   //const 멤버 함수
   const char& operator[](std::size_t position) const{
      return text[position];
   }
   //비const 멤버 함수
   char& operator[](std::size_t position){
      return text[position];
   }
private:
   std::string text;
}

// 상수 객체 생성
void print(const TextBlock& ctb){
   std::cout << ctb[0]; // 상수 멤버 함수 operator[] 호출
   }


// 비상수 vs 상수 함수
TextBlock tb("Hello");
TextBlock ctb("Hello");

std::cout << tb[0];   // (O) => 비상수 멤버 함수 호출
std::cout << ctb[0]; // (O) => 상수 멤버 함수 호출
tb[0] = 'x'; // (O) => 비상수 멤버 함수 호출
ctb[0] = 'x'; // (X) => 비상수 멤버 함수 호출
// 반환값이 const char& 이기 때문에 값 할당 안됨
  1. 상수 멤버 함수의 의미
    • 비트 수준의 상수성(물리적 상수성)
    • 논리적 상수성

5. 비트 수준의 상수성(bitwise constness), 물리적 상수성(physical constness)

  1. const 멤버 함수는 객체의 멤버 변수를 건드리지 않는다.
    • 객체를 구성하는 비트들 중 어떤 것도 변경하지 않는다.
  2. 멤버 변수가 포인터인 경우 const의 역할을 잘 하지 못하는 경우 존재
    • 실제 비트 수준의 상수성은 통과함
    • 하지만 포인터를 통해 멤버 변수가 변경될 수 있음
    • 예제
class CTextBlock {
public:
   char& operator[](std::size_t position) const{
      return pText[position];
   }
private:
   char* pText;
};
// 개발자는 변수를 허락하지 않는 의도로 상수 객체 생성
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
// 하지만 변경됨
*pc = 'J';

6. 논리적 상수성(logical constness)

  1. const 멤버 함수는 멤버 변수 일부는 변경 가능하지만, 사용자 측에서만 모르게 하자.
  2. mutable 키워드를 이용
class CTextBlock{
public:
   std::sizt_t length() const;
private:
   char* pText;
   mutable std::size_t textlength;
   mutable bool lengthIsVaild;
};
std::size_t CTextBlock::length() const{
   if(!lengthIsValid){
      textLength = std::strlen(pText); // 변경 가능함
      lengthIsVaild = true;   // 변경 가능함
   }

   return textlength;
}

7. 상수 멤버 & 비상수 멤버 함수에서 코드 중복 피하는 방법

  1. 비상수 멤버 함수가 상수 버전을 호출하도록 구현
    • 두 번 캐스팅 수행
    • this에 const를 붙이는 casting(static_cast)
    • const를 제거하는 casting(const_cast)
  2. static_cast<const TextBlock&>(*this)[position]
    • *this 를 const 객체로 변환
    • 변한 후 const 멤버 함수 호출(operator[] const)
  3. const_cast<char&>()
    • 위에서 반환된 const char& 를 char&로 변환
class TextBlock{
public:
   const char& operator[](std::size_t position) const{
      return text[position];
   }

   char& operator[](std::size_t position) {
      return const_cast<char&>(
         static_cast<const TextBlock&>(*this)[position]
      );
   }
}
  1. 상수 버전에서 비상수 멤버를 호출하게는 안되나요?
    • 상수 멤버 함수는 멤버 변수를 변경하지 않겠다고 약속됨
    • 따라서 비상수 멤버를 호출하면 안됨

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. #define 보다는 const, enum, inline을 사용하자!

    • 가급적 선행 처리자보다 컴파일러를 더 가까이 하자!
  2. 상수 포인터 정의시 const + const(포인터 및 데이터) 사용하자!

  3. 클래스 멤버로 상수를 정의하면 정적 멤버로 정의하자!

    • static const~~

1. #difine을 쓰면 안되는 이유

#define ASPECT_RATIO 1.653
  1. ASPECT_RATIO는 컴파일 되기 전에 1.653이라는 숫자로 변경됨
  2. 따라서 컴파일러는 ASPECT_RATIO를 모름
    • 컴파일러가 쓰는 기호 테이블에 포함안됨
  3. 매크로 변경 지점에서 컴파일 에러 발생 시 디버깅 어려움
  4. 클래스 상수를 #define으로 만들 수 없다.
    • 캡슐화가 안되고, private 성격의 #define은 없다.

2. 상수를 써서 해결 하라!

const double AspectRatio = 1.653;
  1. AspectRatio는 컴파일러가 알고 있음
  2. 컴파일을 거친 최종 코드 크기가 #define 썻을 때 보다 작음
    • 상수가 부동소수점 실수 타입일 경우
    • 매크로를 쓰면 ASPECT_RATIO가 모두 1.653으로 변경되지만
    • 상수 AspectRatio는 사본이 한개만 존재

3. #define을 상수로 교체할 때 주의할 점

  1. 상수 포인터를 정의하는 경우
    • 상수 정의는 헤더 파일에 넣는 것이 상례
    • 포인터와 가리키는 대상 모두 const로 선언
// 문자열 포인터로 정의하는 경우
const char* const authorName = "Scott Meyers";
// string으로 정의하는 경우
const std::string authorName("Scott Meyers");
  1. 클래스 멤버로 상수(클래스 상수)를 정의하는 경우
    • 정적 멤버(static)로 생성
class GamePlayer {
private:
    static const int NumTurns = 5;
    int scores[NumTurns];
};

//1. 여기서 NumTurns는 '선언' 된것이다. '정의'가 아님에 주의
//2. 만약 별도의 정의가 필요할 경우 정의 제공 필요
const int GamePlayer::Numturns;
//3. 클래스 상수 정의는 헤더파일에 두지 않고 구현파일에 둔다.
//    - 클래스 상수의 초기값은 선언 시점에서 주어지기 때문
  1. 컴파일이 위의 문법을 허용하지 않을 때
    • 오래된 컴파일러에서 발생
    • 정적 클래스 멤버가 선언될 때 초기값 설정 허용 안함
    • 이 경우 초기값을 상수 정의 시점에 준다.
//헤더 파일에 둔다.
class CostEstimate {
private:
    static const double FudgeFactor; // 정의
};

//구현 파일에 둔다.
const double CostEstimate::FudgeFactor = 1.35; // 선언
  1. 클래스를 컴파일 시 클래스 상수값이 필요한 경우
    • 예)GamePlayer::scores 배열 멤버 선언 필요 시
    • enum hack 기법 사용
class gamePlayer{
private:
   enum { NumTurns = 5};
   int scores[NumTurns];
}
  1. enum hack(나열자 둔갑술)
    • const보다는 #define에 가까움
    • const의 주소는 알 수 있지만, enum의 주소는 취할 수 없다.
    • 선언한 정수 상수의 주소를 다른 사람이 알 수 없게 하려면 enum을 써라!

4. 매크로 함수

  1. 매크로 함수의 단점
    • 매크로 인자 마다 반드시 괄호를 씌워야함
    • 괄호가 있어도 예상 불가한 현상 발생 가능(아래 예시 참고)
#define CALL_WITH_MAX(a,b) f((a) > (B) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
// a가 두번 증가함
// 비교할 때 (a)에서 한번, 참인 경우 (a)에서 한번
CALL_WITH_MAX(++a, b+10);
// a가 한번 증가함
  1. inline 함수를 이용해 해결
    • 괄호를 매번 쓰지 않아도 되고, 인자가 여러번 평가되지 않음
    • callWithMax는 진짜 함수이기 때문에 유효 범위 및 접근 규칙을 그대로 따라감
template<typename T>
inline void callWithMax(const T& a, const T& b){
   f(a > b ? a : b);
}

참고

  1. Effective C++
반응형

+ Recent posts

반응형