본문으로 바로가기

03. 코딩과 디버깅에 관하여

 

3.1 코딩의 중요성을 간과하지 말라

 

 프로그래밍 대회에서 좋은 성적을 올리기 위한 비결은 읽기 쉬운 코드를 작성하는 것이다. 간결하고 효율적인 프로그램을 작성하는 능력은 가장 큰 소득이다.


3.2 좋은 코드를 짜기 위한 원칙

 

 

1. 간결한 코드를 작성하기

간결한 코드를 작성할 수록 오타, 버그가 줄고 디버깅도 쉬워진다. 프로그래밍 대회를 위해 작성한 코드에서는 전역 변수가 광범위하게 사용 되곤 한다. 또한, C/C++ 매크로를 사용하여 간결한 코드를 작성하는 것이다.

// 일반적인 코드

for(inr i = 0; i < array.size(); i++)
	for(int j = 0; j < i; ++j)
    	if(array[i] == array[j])
        	return true;
            
// C++ 매크로 사용

#define FOR(i, n) for(int i = 0; i < (n); ++i)

FOR(i, array.size())
	FOR(j, i)
   		if(array[i] == array[j])
        	return true;

보기엔 상당히 거슬린다고 할 수 있지만 코드 길이를 간결하게 해주며, 어이없는 실수를 피하게 해준다.

어디까지나 '흑마법' 이므로 신중하게 사용하고 남에게 자랑은 하지말자

2. 적극적으로 코드 재사용하기

 코드를 모듈화 하는 것이다. 같은 코드가 반복된다면 함수나 클래스로 분리해 재사용하는 것이다. 시간이 없다고 해서 이렇게 코드를 간결하게 고치는데 주저해서는 안된다. 나중에 버그가 발생하면 위의 방법이 훨씬 디버깅이 편하다.

3. 표준 라이브러리 공부하기

 학교에서 처럼 스택 큐 같은 자료 구조를 구현할 수는 없다. 표준 라이브러리 ( C++ 의 STL ) 을 편하게 사용하자.

4. 항상 같은 형태로 프로그래밍 작성하기

 자주 작성하는 알고리즘이나 코드에 대해서는 한 번 검증된 코드를 작성하고 이것만을 꾸준히 사용해야 한다. 그래야 실수도 줄이며, 도구가 아니라 문제에 집중할 수 있다.

5. 일관적이고 명료한 명명법 사용하기

// 명료하지 않은 명명
bool judge(int y, int x, int cy, int cx, int cr);

// 명료한 명명
bool isInsideCircle(int y, int x, int cy, int cx, int cr);

위의 명료하지 않은 함수의 이름은 return 값이 헷갈릴 수 있다. 함수 이름 자체에 함수가 어떠한 일을 하며 어떠한 값을 return 할 것인지를 알 수 있게 해야 한다. 변수명도 마찬가지이다. (필자는 Camle-case 를 사용하겠다.)

6. 모든 자료를 정규화해서 저장하기

 같은 자료를 두 가지 형태로 저장하지 않는 것이다. 같은 유리수를 기약 분수로 표현하여 통일 한다든가, 각도를 -30 으로 할지 330 이라 할지를 정하는 등 통일하는 것이다. 정규화는 프로그램이 자료를 입력받거나 계산하자마자 곧장 이루어져야 한다.

7. 코드와 데이터를 분리

 사용할 데이터를 미리 배열화, 벡터화 시켜두고 정해진 데이터 내에서만 움직이게 만들거나 정해진 데이터만을 사용하는 것이 좋다. 코드의 양을 줄여서 실수를 없앨 수 있으며 명료하다. 하지만, 상수 오타 는 컴파일러가 잡아주지 못하므로 매우 유의해야 한다.

 

 


3.3 자주하는 실수

 1. 산술 오버플로

가장 자주 등장하는 실수이다. 3.5장에서 자세히 다룬다.

2. 배열 범위 밖 원소에 접근

 c++ 에서는 배열의 원소에 접근할 때 해당 인덱스가 배열 범위 안인지 밖인지 별도로 확인해 주지 않는다. 그러므로, 이러한 오류를 찾기는 쉽지 않다. 배열의 크기 설정을 신중히 하자. 이 과정에서 런타임 스택 등을 건드려서 런타임 오류를 종류하는 경우엔 이러한 오류를 쉽게 찾아낼 수 있겠다.

3. 일관되지 않은 표현 방식 사용하기

2번 오류가 발생하게 되는 가장 큰 원인이다. 범위를 열린 구간 (1, 10) 으로 하느냐 닫힌 구간 [1, 10] 으로 하느냐를 혼용 한다면 결국 오류가 발생할 것이다. 대부분의 프로그래밍 언어는 절충안인 반 열린 구간을 사용한다. (1, 10] 으로 표현한다.

 C++ STL 에서는 $$begin()$$ 과 $$end()$$ 로 표현하는데 begin() 은 첫 번째 원소를 가리키기만 end() 는 마지막 원소가 아니라 마지막 원소 다음 있는 가상의 원소를 가리킨다.

한 가지 방법만 사용하자!

4. Off-by-one 오류

 코드의 큰 줄기는 맞지만 하나가 모자라거나 하나가 많아서 틀리는 오류들이다. 반복문에서 흔히 $$> < >= <=$$ 를 혼동하여 사용했기 때문에 발생한다. 

5. 컴파일러가 잡지 못하는 상수 오타

데이터를 직접 입력해야하는 경우 오타가 발생한다면 컴파일러는 "weird" 이든 "wierd" 이든 신경 쓰지 않는다. 프로그래머 스스로 점검해야하며 오타에 유의해야 한다. 프로그램을 잘 구현해놓고 이런 것을 잡아내지 못해서 틀리지 말자.

6. 스택 오버플로

대개 재귀 호출의 깊이가 깊어져서 발생한다. 대회에서 사용하는 환경의 스택 허용량에 대해 알아두자. 배열 등의 메모리 소비가 큰 지역 변수를 스택에 잡으면 재귀 호출이 적어도 곧장 스택 오버플로가 발생할 수 있다. 그래서, 대부분의 참가자들은 자동으로 힙에 메모리를 할당하는 STL 컨테이너를 사용하거나 전역 변수를 사용하곤 한다.

 

7. 다차원 배열 인덱스 순서 바꿔 쓰기

8장에서 DP 를 위한 메모이제이션 패턴을 사용할 때 잦다. 8장에서는 C++ 의 참조 변수로 이를 해결한다.

8. 잘못된 비교 함수 작성

 어떤 프로그램이 vector<IntegerSet> 에 담긴 집합들을 순서대로 처리하는데, 집합 A 가 B 의 진부분집합이라면 A는 항상 B보다 먼저 처리되어야 한다고 한다. 이때 정렬 함수에 비교 함수를 전달해야 하는데, 명확히 정의하지 않으면 버그를 찾을 수 없는, 의도와 다른 프로그램이 구현될 것이다.

// a가 b의 진부분집합이면 true, 아니면 false를 반환한다.
bool isPropersubset(const IntegerSet& a, const IntegerSet& b);

bool operator < (const IntegerSet& a, const IntegerSet& b) {
	//a 가 b의 진부분집합이면 a가 앞에 와야 한다. true
    if(isProperSubset(a,b)) return true;
    
    if(isProperSubset(b,a)) return false;
    
    return false;

 이렇게 한다면 절대로 정렬되지 않을 것이다. C++ 표준 라이브러리가 예상하는 일관된 답을 코드가 반환하지 않기 때문이다. C++ 에서는 < 연산자가 아래와 같이 동작할 것이라고 예상한다.

  1. a<a 는 항상 거짓이다.
  2. a<b가 참이면 b<a는 거짓이다.
  3. a<b가 참이고 b<c가 참이면 a<c 이다.
  4. a<b 와 b<a 가 모두 거짓이면 a와 b는 같은 값으로 간주한다. a와 b가 같고 b와 c가 같다면 a와 c도 같아야 한다.

위의 코드가 4번 성질을 만족시키지 못한다는 것을 알 수 있다. 

이렇듯, 어떠한 비교함수를 요구하는지 잘 파악하여야 하고 언어마다 요구하는 사항이 다르니 유의하여야 한다. 한 예로, C++ 에서는 표준 라이브러리가 < 연산을 요구하지만 JAVA 에서는 <= 연산을 요구한다.

 

9. 최소, 최대 예외 잘못 다루기

코드를 짤 때, 가장 작은 입력과 가장 큰 입력에 대해 제대로 동작할지를 생각해 보면 오류를 잡을 수 있는 경우가 꽤 많다.

bool isPrime(int n){
	if(n == 2) return true;
    if(n %2 == 0) return flase;
    for(int i = 2; i < n; ++i)
    	if(n % i == 0)
    		return false;
    return true;
    }

예외인 2를 처리했다고 해서 모든 오류가 처리된 것은 아니다. 입력 받는 범위는 자연수이다. 그렇다면 자연수의 최소는 무엇인가? 1을 대입했을 때 true 가 나오는 오류가 발생하는 것이다.

10. 연산자 우선순위 잘못 쓰기

 

11. 너무 느린 입출력 방식 선택

cin  gets() 의 속도 차이

 대개의 경우 고수준 입출력 방식 ( cin ) 을 이용하면 코드가 간단해지지만, 속도 저하 또한 클 수 있다. 저수준 방식 ( gets 

12. 변수 초기화 문제

흔한 실수 중 하나는 이전 입력에서 사용한 전역 변수 값을 초기화하지 않고 그대로 사용하는 것이다. 이를 예방하기 위한 팁은 예제 파일을 두 번 반복해 쓰는 것이다. 물론 새 테스트 케이스를 처리할 때마다 변수들이 초기화되도록 신경 쓰는 것이 더 좋다.

 

반응형