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++ 에서는 < 연산자가 아래와 같이 동작할 것이라고 예상한다.
- a<a 는 항상 거짓이다.
- a<b가 참이면 b<a는 거짓이다.
- a<b가 참이고 b<c가 참이면 a<c 이다.
- 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 )
12. 변수 초기화 문제
흔한 실수 중 하나는 이전 입력에서 사용한 전역 변수 값을 초기화하지 않고 그대로 사용하는 것이다. 이를 예방하기 위한 팁은 예제 파일을 두 번 반복해 쓰는 것이다. 물론 새 테스트 케이스를 처리할 때마다 변수들이 초기화되도록 신경 쓰는 것이 더 좋다.