Header

  1. View current page

    kelly님의 노트

Profile_img_60x60_01
23

Chapter 1. C++에 왔으면 C++의 법을 따릅시다.

 항목 1. C++를 언어들의 연합체로 바라보는 안목은 필수

  • C++ 

    • 클래스를 쓰는 C( C with Classes )가 처음 이름 
    • 오늘날의 C++은 다중패러다임 프로그래밍 언어
    • 절차적 프로그래밍, 객체지향, 함수식, 일반화 프로그래밍을 포함해서 메타 프로그래밍 개념까지 지원
    • C++을 단일 언어로 바라보지 말고 여러 언어들의 연합체로 봐야함
  • C++의 하위 언어 

    • C : C++은 C를 기본으로 제공 
    • 객체 지향 개념의 C++ : 클래스를 쓰는 C에 관한것이 모두 해당 됨(객체 지향 설계의 대부분) 
    • 템플릿 C++ : C++의 일반화 프로그래밍 부분 
    • STL : 템플릿 라이브러리, container, iterator, algorithm, function object가 얽혀 돌아감 

 

항목 2. #define을 쓰려거든 const, enum, inline을 떠올리자.

  • #define AAA 1.653

    • 컴파일러에게 넘어가기 전에 선행 처리자가 AAA는 밀어버리고 1.653만을 채택
    • 컴파일 에러가 발생시 에러 메세지는 1.653로 표시됨 => 디버깅이 어려워짐
  •  #define을 const 로 교체하자.

    • #define을 사용하면 AAA를 사용한 수 만큼 사본이 생기지만, 상수로 쓰면 사본은 하나만 생기고 참조함. 
    • 교체할 때의 주의점 

      • 상수 포인터 정의 

        1. const char * const authorName = "Scott Meyers"; 
        2. const std::string authorName("Scott Meyers"); 
      • 클래스 멤버로 상수를 정의 

        1. class GamePlayer{
        2. private:
        3. static const int NumTurns = 5;
        4. int scores[NumTurns];
        5. ... 
        6. }; 
      • 나열자 둔갑술 

        1. class GamePlayer{
        2. private:
        3. enum { NumTurns = 5 };
        4. int scores[NumTurns];
        5. ... 
        6. }; 
        • 장점 

          • enum의 주소를 취하는 것이 불법임 
          • #define처럼 어떤 형태의 쓸데없는 메모리 할당도 저지르지 않음 
    • #define의 오용사례 

      1. // a와 b 중에 큰 것을 f에 넘겨 호출합니다. 
      2. #define CALL_WITH_MAX( a,b) f((a)> ? (b) ? (a) : (b) ) 
      3. int a = 5, b = 0; 
      4. CALL_WITH_MAX(++a, b);            // a가 두 번 증가합니다.
      5. CALL_WITH_MAX(++a, b + 10);      // a가 한 번 증가합니다.
      • 해결 방법 : 인라인 함수에 대한 템플릿 

        1. Template<typename T > 
        2. inline void callWithMax( const T& a, const T& b ) 
        3. {
        4. F( a > b : a : b ); 
      • 동일 계열 함수군 : 동일한 타입의 두가지 인자를 받고 둘중에 큰 것을 F로 넘겨서 호출하는 구조

 

항목 3. 낌새만 보이면 const를 들이대 보자!

  • const 장점

    • 외부 변경 불가능 
    • static으로 선언한 객체에도 const 가능 
    • 포인터 자체를 상수로, 포인터가 가리키는 데이터를 상수로 지정 가능, 둘다 지정, 둘자 미지정 가능 

      1. char greeting[] = "Hello"; 
      2. char *p = greeting;                 // 비상수 포인터, 비상수 데이터
      3. const char *p = greeting;           // 비상수 포인터, 상수 데이터
      4. char * const p = greeting;          // 상수 포인터, 비상수 데이터
      5. const char * const p = greeting;    // 상수 포인터, 상수 데이터
      • *의 왼쪽에 const가 존재 : 포인터가 가리키는 대상이 상수 
      • *의 오른쪽에 const가 존재 : 포인터 자체가 상수
      • 받아들이는 매개변수 타입은 모두 똑같음 

        1. void f1(const Widget *pw); // f1은 상수 Widget 객체에 대한 포인터를 매개변수로 취함 
        2. void f2(Widget const *pw); // f2도 동일 
      • STL에서 iterator가 가리키는 대상 자체의 변경을 허용하지 않으려면 const_iterator 사용

        1. std::vector<int> vec; 
        2. const std::vector<int>::iterator iter = vec.begin(); 
        3. *iter = 10;          // OK
        4. ++iter;              // 에러, iter는 상수
        5. std::vector<int>::const_iterator cIter = vec.begin(); 
        6. *cIter = 10;         // 에러, *cIter는 상수
        7. ++cIter;             // OK
      • 함수의 반환값을 const로 

        1. class Rational {...}; 
        2. const Rational operator* (const Rational& lhs, const Rational& rhs); 
        3. Rational a,b,c;
        4. (a * b) = c; // a*b의 결과(const)에 대고 operator=을 호출하다니;;; 
      • 상수 멤버 함수 

        • const 키워드의 있고 없고 차이만으로 오버로딩이 가능 
        1. class TextBlock { 
        2. public:
        3. const char& operator[] (std::size_t position) const // 상수 객체에 대한 operator[]
        4. { return text[position]; }
        5. char & operator[] (std::size_t position) // 비상수 객체에 대한 operator[]
        6. { return text[position]; } 
        7. private:
        8. std::string text; 
        9. }; 
        10.  
        11. TextBlock tb("Hello"); 
        12. std::cout << tb[0];             // TextBlock::operator[]의 비상수 멤버를 호출합니다. 
        13. const TextBlock ctb("World"); 
        14. std::cout << ctb[0];            // TextBlock::operator[]의 상수 멤버를 호출합니다. 
        15.  
        16. void print(const TextBlock& ctb)      // 이 함수에서 ctb는 상수 객체로 쓰입니다. 
        17. {
        18. std::cout << ctb[0];            // TextBlock::operator[]의 상수 멤버를 호출합니다.
        19. }
        20.  
        21. std::cout << tb[0]; 
        22. tb[0] = 'x'; 
        23. std::cout << ctb[0]; 
        24. ctb[0] = 'x';
        25.  
        • operator[]가 char &를 반환하는 이유는 값에 의한 반환이 되면 사본이 리턴되며 값에 대입연산자를 수행하면 컴파일 에러가 나기 때문
        • 비트수준 상수성(물리적 상수성)

          • 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버는 제외) 그 멤버 함수가 'const'임을 인정
          1. class CTextBlock { 
          2. public:
          3. char& operator[] (std::size_t position) const // 부적절한(그러나 비트수준 상수성이 있어서 허용되는) operator[]의 선언
          4. { return pText[position]; } 
          5. private:
          6. char *pText; 
          7. };
          8. const CTextBlock cctb("Hello"); // 상수 객체를 선언합니다. 
          9. char *pc = &cctb[0]; // 상수 버전의 operator[]를 호출하여 cctb의 내부 데이터에 대한 포인터를 얻습니다. 
          10. *pc = 'J'; // cctb는 이제 "Jello"라는 값을 갖습니다. 
        • 논리적 상수성

          • 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다. 
          1. class CTextBlock { 
          2. public:
          3. std::size_t length() const;
          4. private:
          5. char *pText;
          6. std::size_t textlength;             // 바로 직전에 계산한 텍스트 길이
          7. bool lengthIsValid;               // 이 길이가 현재 유효한가?
          8. }; 
          9. std::size_t CTextBlock::length() const 
          10. {
          11. if (!lengthIsValid) {

            1. textLength = std::strlen(pText); // 에러! 상수 멤버 함수 안에서는 textLength
            2. // 및 lengthIsValid에 대입할 수 없음
            3. lengthIsValid = true;
            4. }
            5. return textLength;
          12. }
          13. class CTextBlock {
          14. public:
          15. std::size_t length() const;
          16. private:
          17. char *pText;
          18. mutable std::size_t textlength;      // 이 데이터 멤버들은 어떤 순간에도 수정가능
          19. mutable bool lengthIsValid;            // 심지어 상수 멤버 함수 안에서도 수정가능
          20. };
          21. std::size_t CTextBlock::length() const
          22. {

            1. if (!lengthIsValid) {
            2. textLength = std::strlen(pText);    //  문제없음
            3. lengthIsValid = true;               // 문제없음
            4. }
            5. return textlength;
          23. }
  • 상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

    • 코드 판박이 괴물 

      1. class TextBlock { 
      2. public: 
      3. const char& operator[](std::size_t position) const
      4. return text[position]; // 경계검사, 접근 데이터 로깅, 자료 무결성 검증
      5. char& operator[](std::size_t position)
      6. return text[position]; // 경계검사, 접근 데이터 로깅, 자료 무결성 검증
      7. private: 
      8. std::string text; 
      9. };
      • 우리가 원하는 것 : operator[]의 핵심 기능을 한 번만 구현해 두고 이것을 두 번 사용하고 싶음 
      • 결론 : 캐스팅이 필요하긴 하지만, 안전성도 유지하면서 코드 중복을 피하는 방법 

        • 비상수 operator[]가 상수 버전을 호출하도록 구현하는 것 

          1. class TextBlock { 
          2. public: 
          3. const char& operator[] ( std::size_t position) const // 이전과 동일
          4. return text[position];
          5. char& operator[] (std::size_t position) // 상수 버전 op[]를 호출하고 끝
          6. {
          7. // op[]의 반환 타입에 캐스팅을 적용, const를 떼어냅니다.
          8. // *this의 타입에 const를 붙입니다. 
          9. // op[]의 상수 버전을 호출합니다. 
          10. return const_cast<char&>( static_cast<const TextBlock&>(*this)[position] ); 
          11. };
        • 상수 멤버와 비상수 멤버가 기능적으로 똑같게 구현되어 있을 경우, 코드 중복을 피하기 위해 비상수 버전이 상수 버전을 호출하게 만든다.

 

항목 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자 

  • C++ 의 객체 초기화 규칙

    • C 부분만 쓰면 값이 초기화된다는 보장이 없음. 
    • STL의 vector는 초기화가 보장됨 
  • best한 방법 : 모든 객체를 사용하기 전에 항상 초기화 

    1. int x = 0;          // int의 직접 초기화
    2. const char * text = "A C-style string";       // 포인터의 직접 초기화
    3. double d;       .. 입력 스트림에서 읽음으로써  "초기화" 수행
    4. std::cin >> d; 
  •  C++ 생성자에서 지킬 규칙 : 그 객체의 모든 것을 초기화하자!
  • 대입과 초기화의 차이 

    1. class PhoneNumber {...}; 
    2. class ABEntry {         // ABEntry = "Address Book Entry" 
    3. public: 

      1. ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); 
    4. private: 

      1. std::string theName; 
      2. std::string theAddress; 
      3. std::list<PhoneNumber> thePhones; 
      4. int numTimesConsulted; 
    5. }; 
    6. ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
    7. {  

      1. theName = name;               // 지금은 모두 대입을 하고 있음, 초기화가 아님
      2. theAddress = address; 
      3. thePhones = phones; 
      4. numTimesConsulted = 0; 
    • theName, theAddress, thePhone은 디폴트 생성자에서 이미 초기화됨
    • 기본 제공 타입인 numTimesConsulted은 초기화 되었다는 보장이 없음
    1. ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
    2. :

      1. theName(name),               // 이제 이들은 모두 초기화되고 있음
      2. theAddress(address),
      3. thePhones(phones), 
      4. numTimesConsulted(0) 
    • { }  // 생성자 본문엔 이제 아무것도 들어가 있지 않음
    • 기본 생성자 호출 후에 복사 대입 연산자를 연달아 호출하는 것보다 복사 생성자를 한 번 호출하는 쪽이 더 효율적임 
    • 데이터 멤버를 기본 생성자로 초기화 

      1.  ABEntry::ABEntry()
      2. : theName(), 
      3. theAddress(), 
      4. thePhones(), 
      5. numTimesConsulted(0) 
      6. {}
    • 상수와 참조자는 대입이 불가능 하기 때문에 반드시 멤버 초기화 리스트를 사용해야함 
    • 객체를 구성하는 데이터의 초기화 순서 

      • 기본 클래스는 파생 클래스보다 먼저 초기화
      • 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화 
    •  비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

      • 정적 객체 : 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체

        • 지역 정적 객체 : 함수 안에서 static으로 선언된 객체 
        • 비지역 정적 객체 : 전역 객체, 네임스페이스 유효범위에서 정의된 객체, 클래스 안에서 static으로 선언된 객체, 파일 유효범위에서 static으로 정의된 객체 
      • 번역 단위 : 그 파일이 #include 하는 파일들까지 합친 소스파일 하나 
      1. class FileSystem { 
      2. public: 

        1. std::size_t numDisks() const; 
      3. }; 
      4. extern FileSystem tfs; 
      5. class Directory { 
      6. public: 

        1. Directory(params); 
      7. }; 
      8. Directory::Directory(params) 
        1. std::size_t disks = tfs.numDisks(); 
      9. Directory tempDir(params); 
    • 해결 방법 : 비지역 정적 객체를 맡는 함수를 준비해서 이 안에 각 개체를 넣음. 함수는 정적객체의 참조자를 반환 
    • 비지역 정적 객체가 지역 정적 객체가 됨(Singleton 패턴) 

      1. class FileSystem {...}; 
      2. FileSystem& tfs() 
        1. static FileSystem fs;
        2. return fs; 
      3. } 
      4. class Directory {...}; 
      5. Directory::Directory(params)
        1. std::size_t disks = tfs().numDisks(); 
      6. Directory& tempDir() 
        1. static Directory td; 
        2. return td; 
      7. } 
      8. Directory tempDir(params);  
      • 참조자 반환 함수는 정적 객체를 쓰기 때문에 다중 스레드 시스템에서는 문제가 생길 수 있음 
  • 정리 

    • 기본 제공 타입은 직접 손으로 초기화 
    • 생성자에서는 본문에 넣지 않고 멤버 초기화 리스트 사용 
    • 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제를 피해서 설계

 

History

Last edited on 10/25/2007 18:20 by 켈리

Comments (0)

You must log in to leave a comment. Please sign in.