2008년 4월 24일 목요일

C++ 프로그래머를 위한 규칙들

요셉 베르긴(Joseph Bergin)
페이스 대학(Pace University), 1997


(!) 표시가 된 규칙은 당신이 위험을 감수하고자 할 때만 무시되어야 한다.
(?) 표시가 된 규칙은 일반적으로 준수되어야 한다. 그러나 이것을 무시할 이유는 충분히 있을 수 있다.


1. (!) 만약 어떤 클래스가 메모리를 동적으로 관리한다면, (예를 들어 멤버 변수를 생성하기 위해 new를 호출하는 경우), 이 때는 다음 함수들이 모두 필요하다.
  • 디폴트 생성자 -- 파라미터가 없이 호출될 수 있는 것
  • 복제 생성자
  • 소멸자
  • 재정의된 = 연산자
실제로, 어떤 클래스가 위에 언급된 것들 중 하나나, 생성자 둘(디폴트 생성자, 복제 생성자) 중 하나를 정의한다면 나머지도 아마 모두 필요할 것이다. 비슷한 경우로 클래스가 포인터나 배열 멤버 변수를 가지고 있어도 마찬가지이다.

복제 생성자 외에 하나의 인자를 가지는 생성자를 명시적으로 가져야 한다. 그렇게 하지 말아야 할 충분한 이유가 없는 한 이렇게 하는 것이 좋다. 이렇게 하면 가끔씩 당신을 놀라게 하는 자동 타입 변환을 피할 수 있다.

2. (!) 어떤 클래스가 가상 메소드를 가지고 있다면 가상 소멸자가 필요할 것이다. 가상 소멸자가 아무 것도 하는 것이 없더라도 필요하다. 그렇지 않으면 어떤 문맥에서는 잘못된 소멸자가 호출될지도 모른다.

3. 어떤 함수에 클래스 타입의 파라미터가 값에 의해(by value) 전달되는 경우에는 차라리 const 참조로 전달되는 것이 아마 더 나을 것이다. 이렇게 하면 임시 객체의 생성과 소멸을 피할 수 있다. 함수가 인자를 어떤 식으로든 "소비(consume)"를 해야 하는 경우는 예외이다.

특히, 스트림에 출력을 하는 연산자 >>를 정의할 때는 두번째 인자를 const 참조로 전달해야 한다.

4. private 상속을 피하고 대신에 멤버 변수를 사용하라.

5. (?) 가급적 다중 상속을 피하라.

6. (!) 구식 C 기반 i/o와 iostream i/o를 혼용하지 마라. 둘 다 버퍼를 사용하지만 서로 다른 버퍼를 사용한다. 특히 cout과 stdout을 같이 사용하면 예측 불가능한 방법으로 출력이 혼합될 수 있다.

7. switch 구문의 마지막 case는 break로 끝나야 한다. defaut case를 처음에 놓는 것을 고려하라. 일반인의 감각과는 달리 default를 마지막에 꼭 놓을 이유는 없다. 만약 어떤 switch 문의 case가 break로 끝나지 않으면 주의 깊에 코멘트를 집어 넣어라.

8. 헤더가 아닌 이 헤더의 구현부에 정의된 전역 데이터나 함수는 static으로 표시되어야 한다. 이렇게 하면 다른 컴파일 유닛에 나타나는 동일한 이름을 가진 다른 변수나 함수와의 링크 문제를 피할 수 있다.

9. C preprocessor의 사용을 피하라. 상수 값을 정의하기 위해서 #define을 사용하는 대신 const를 사용하라. 매크로를 사용하지 마라. 매크로는 언어의 타입 제약 사항을 피해 버린다. #define이 허용되는 용도는 조건부 컴파일을 위한 #include나 #define이다. 또한 일련의 코드 블럭을 효과적으로 comment out 하기 위해서 다음과 같이 사용할 수 있다.

#if 0
...
#endif

위의 경우는 내부에 코멘트가 있어도 가능하다.

10. (!) foo.h라는 이름의 헤더 파일은 다음과 같이 시작해야 한다.

#ifndef FOO_H
#define FOO_H

그리고 다음과 같이 끝나야 한다.

#endif // FOO_H

11. 헤더에 정의된 변수는 extern으로 표시되어야 한다. 그러면 해당 구현 파일에서 extern 없이 정의될 수 있다. 이렇게 하면 링커 문제를 피할 수 있다.

12. 헤더 하나당 클래스 하나를 정의하도록 하라. 만약 클래스 몇 개가 밀접하게 관련되어 있다면 하나의 헤더에 정의될 수도 있을 것이다. 그러나, 다른 클래스 안에 이런 클래스를 중첩하여 정의하라. 예를 들면, List 클래스와 연관된 Node 클래스는 List 클래스 안에 중첩된 클래스로 정의하는 것이 가장 좋다.

13. (?) 파생 타입이 베이스 타입을 특화한 경우에만 상속을 사용하라. 그 외 다른 관계는 멤버 변수를 사용하여 구현하라.

14. public 멤버 변수를 피하라.

15. (!) 절대로 함수 내의 지역 변수에 대한 포인터나 참조를 리턴하지 마라. 대신에 복사본을 리턴해라.

16. (!) 새로 생성된 힙 변수에 대한 참조를 리턴하지 마라. 후에 이 공간을 해제할 방법이 없다. 대신에 포인터를 리턴하라.

특별한 테크닉

17. 클래스나 struct 내에 정의된 enumeration은 전역 이름 공간에 상수의 이름을 남기지 않는다. 따라서 이름 충돌을 피할 수 있다. struct는 이름들을 묶는 역할 외에는 다른 목적이 없이 정의될 수 있다.

struct color
{
    enum {red, green, blue}
}

이렇게 하면 color::red, color::green...와 같이 사용할 수 있다. enum 자체는 이름이 필요하지 않음에 주의하시오. 특히, C++은 상수를 클래스 내에 정의하는 것을 허용하지 않을 수 있다. 그러나 클래스 내에 enum을 정의하는 것은 허용이 된다. 따라서 enum 내에 상수를 정의하라.

class stack
{
    public:
        const int max = 100; // ILLEGAL
        ...
}

class stack
{
    public:
        enum {max = 100}; // OK
}

스타일

18. 공개 인터페이스는 클래스 정의의 앞 부분에 놓여야 한다. 사적인 필드는 클래스 정의의 끝 부분에 놓여야 한다.

19. 이름이 전역적일수록 더 서술적인 이름을 사용하여야 한다.

20. {}, (), 등과 같은 모든 그룹핑 기호는 수평이나 수직으로 배치되어야 한다.

21. 만약 긴 구문을 여러 줄로 쪼개야 한다면 각 줄은 연산자 심볼로 시작하도록 하라.

22. 불필요하게 많은 레벨의 들여쓰기를 피하라. 그러나 구조가 잘 드러나도록 들여쓰기를 하라.
너무 깊은 들여쓰기(8 글자)는 피하라. 수평 공간 자산을 아껴 써라.

class base
{
  public:
    base () { . . .}
    base& operator=(const base& b)
    {
      if(...)
      {
        b...
      }
      else
      {
        ...
      }
    }
    ...
  private:
    int temp;
    ...
};

23. 당신이 개발한 스타일을 고수하라.

24. dynamic_cast, const_cast, reinterpret_cast나 static_cast 등의 사용법을 익혀라. 그리고 구식 C 캐스팅 대신에 이것들을 사용하라. --> Explanation of the C++ cast operators by G. Bowden Wise

24. 5 가지 const 사용법을 익혀서 사용하라.

(1) 상수 값 정의

const int i = 10; // 상수를 초기화 해야 함

(2) 변경될 수 없는 것들에 대한 포인터나 참조 만들기 (const에 대한 포인터나 참조)

const int * p;
const int &r = i; // 참조 변수를 초기화 해야 함

(3) 포인터를 변경 불가하게 만들기 (const 포인터. 단지 한 곳만 가르킴)

int * const cp = &i; // 초기화 해야 함
// *cp는 변경될 수 있으나 cp 자체는 변경 불가함에 주의

(4) 멤버 변수를 바꾸지 않는 메버 함수 만들기

int get() const;
// "this" 필드들은 수정될 수 없다 (mutable로 표시되지 않는 한)

(5) 참조 인자는 수정될 수 없게 만들기

void(const foo &x);
// x는 수정될 수 없다.

25. const에 대한 const 포인터를 얻기 위해서는 위 2번째와 3번째를 조합해서 사용할 수 있다:

const int *const cpc = &i;

기억해야 할 것들

26. 템플릿은 컴파일되지 않음을 기억하라. 인스턴스화된 것만 컴파일된다. 당신이 템플릿을 인스턴스화 하기 전까지는 템플릿 정의의 구문(syntax)을 검사할 수 없다. 좋은 컴파일러는 실제로 사용되지 않는 템플릿의 일부분을 컴파일하지 않을 것이다. 따라서 템플릿 인스턴스의 완벽한 테스트를 하지 않으면 구문 에러 체크를 완전히 했다고 할 수 없을 것이다.

27. 구현부가 독립적으로 컴파일 되게 하기 위한 목적이나 헤더가 필요한 곳에만 포함되도록 하기 위한 목적으로 템플릿 정의를 헤더와 구현 파일에 분할 할 수 없음을 기억하라. 이유는 위에서 말한 것 때문이다. 템플릿을 정의하는 모든 코드는 include 파일에 있어야 한다.

28. 함수 템플릿에 있는 템플릿 인자는 함수의 인자로서 나타나야 함을 기억하라. 반환 타입은 고려하지 마라.

29. 중복 연산자를 정의할 때는 연산자의 결합법과 우선순위가 유지됨을 기억하라. 때로는 연산자를 정의하는 기호들에 기반을 둔 바람직한 연산자라도 부적합하거나 사용 상에 혼란을 초래할 수 있다는 것을 의미한다.

30. operator>>와 operator<<는 중간 정도의 우선순위(산술 연산자와 비교 연산자 사이)를 가짐을 특히 기억하라. 할당 연산자나 operator?:는 operator>>와 operator<< 보다 낮은 우선순위를 가진다. 따라서 operator>>와 operator<<는 때때로 입출력을 할 때 혼란을 초래한다. 또한 할당 연산자는 우측 결합법에 따른다.

31. 포인터는 소멸자를 가지지 않음을 기억하라.

32. 부정확한 프로그램을 캐스팅을 통해서 정확하게 만들 수 없음을 기억하라.

33. 캐스팅을 사용할 때는 컴파일러는 당신을 믿는다는 것을 기억하라. 따라서 당신이 올바로 캐스팅을 해야 한다.

34. 배열의 첨자를 사용할 때는 컴파일러는 당신을 믿는다는 것을 기억하라.

출처 : 스타일 규칙에 대해서는 "Mastering Object-Oriented Design in C++ by Cay Horstman, Wiley, 1995."를 참조하라.

- 강가딘

댓글 없음:

댓글 쓰기