First Chapter

1.1 자주하는 실수

산술 오버플로
변수의 표현 범위를 벗어남

배열 범위 밖 원소에 접근
배열 범위 밖을 잘못 접근하는 오류. c/c++에서는 배열의 원소에 접근할 때 해당 인덱스가 배열 범위 안에 있는지를 별도로 확인해 주지 않는다. 이럴 때 발생하는 버그가 최악. 차라리 런타임 스택 등을 건드려서 프로그램이 런타임 오류를 내고 종료하는 경우에는 배열 범위 밖에 접근했다는 사실을 깨달을 수 있다.

일관되지 않은 범위 표현
폐구간, 개구간의 사용에서 발생할 수 있음. 차라리 stl함수에서처럼 첫 원소~마지막원소+1로 범위는 잡는게 더 편할지도 모름.(여러 연산들 때문에)

Off-by-one오류
로직은 맞지만 대상이 되는 자료들의 수를 하나 더 하거나 모자르게 계산하는 것.

컴파일러가 잡아주지 못하는 상수 오타
ex)자료형의 비트 수

스택 오버플로
대개 재귀 호출을 사용하다가 발생하는 오류. 지역 변수로 배열을 너무 많이 잡다가 발생하는 오류이기도 한다.

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

잘못된 비교 함수 작성
비교 연산자의 헛점. a < b와 a > b가 모두 거짓이면 둘은 같은 것이라고 컴파일러가 판단한다. 그러므로 로직을 자세히 작성해야한다.

최소, 최대 예외 잘못 다루기
예외에 대한 범위를 잘못 이해하면 발생하기 쉬운 문제이다. 그러므로 코드를 짤 때 가장 작은 입력과 가장 큰 입력에 대해 제대로 동작할지를 생각해 보면서 오류를 잡는다.

연산자 우선순위 잘못 쓰기
얼마전에도 경험했듯이 이 문제가 얼마나 큰 파장을 일으킬 수 있는지 잘 생각... 재밋는 점은 컴파일러에 따라 다르기도 한다는 점이다. 괄호를 잘 이용해서 벗어나도록 한다.

너무 느린 입출력 방식 선택
입출력에 대해서 고수준 입출력을 사용하면 코드가 간단해지겠지만 이에 따른 속도 저하 또한 클 수 있다. 입력으로 받거나 출력해야 할 변수의 수가 1만개를 넘어가면 긴장하는 편이 좋음.

변수 초기화 문제
알고리즘 문제에서는 한번 실행에 여러번 입출력이 대부분이다. 만약 소스코드에서 전역 변수를 사용했을 경우에 초기화를 신경쓰지 못한다면 이후 큰 문제로 발생할 가능성이 있다.


1.5 변수 범위의 이해

자료형의 프로모션
사칙연산이나 대소 비교 등의 이항 연산자들은 두 개의 피연산자를 받는다. 만약 피연산자의 자료형이 다르거나 자료형의 범위가 너무 작은 경우 컴파일러들은 대개 이들을 같은 자료형으로 변환해서 사용하는데, 이를 프로모션이라고 한다. 이는 파악하기 어려운 버그를 만들기도 한다.
이 프로모션 과정은 언어에 따라 조금씩 다르지만 c++의 경우 다음과 같은 규칙이 적용된다.

  • 한쪽은 정수형이고 한쪽은 실수형일 경우 : 정수형이 실수형으로 변환.
  • 양쪽 다 정수형이거나 양쪽 다 실수형일 경우 : 보다 넓은 범위를 갖는 자료형으로 변환.
  • 양쪽 다 int형보다 작은 정수형인 경우 : 양쪽 다 int형으로 변환.
  • 부호 없는 정수형(unsigned)과 부호 있는 정수형(signed)이 섞여 있을 경우 : 부호 없는 정수형으로 변환.

1.6 실수 자료형의 이해

실수연산의 어려움
실수를 다루는 코딩을 하다보면 이해하지 못하는 결과를 볼 때가 있다. 이 의문을 해결하려면 컴퓨터가 사용하는 실수 표현 방식에 대해 알아야 한다. (예를 들면 float와 double을 같이 쓴다던가하는...)

실수와 근사 값
우리가 일상적으로 다루는 정수들은 컴퓨터가 모두 정확하게 표현할 수 있다. 무한 소수같은 실수들을 다루게 되면 무한의 세계로 날아가 버리게 된다. 하지만 컴퓨터의 메모리는 항상 유한하고, 이 모든 값들을 모두 정확하게 담을 수는 없으니 어쩔수 없이 적절히 비슷한 값을 사용하는 것으로 만족해야 한다. 컴퓨터의 모든 실수 변수는 정확도가 제한된 근사 값을 저장한다. 근사 값으로 연산한 결과는 수학적으로 정확하지 않을 수 있기 때문에 실수는 훨씬 다루기 까다롭다. 정수 계산의 경우 언제나 동일한 값을 리턴하지만 실수의 경우, 중간에 어떤 순서로 동작했는지, 컴파일러 최적화는 되어있는 것인지 등등에 따라 그 답이 달라질 수 있다.
이런 현상을 극복하기 위해서는 컴퓨터가 사용하는 실수 표현 방식과 그 장단점을 이해해야 한다.

IEEE 754 표준
가장 많이 사용하는 표기 방식이다. 다음과 같은 특징이 있다.

  • 이진수로 실수를 표기
  • 부동 소수점 표기법
  • 무한대, 비정규 수 NaN등의 특수한 값이 존재

실수의 이진법 표기
실수를 이진법으로 쓰는 방법은 사실 굉장히 간단하다. 12.34라는 실수의 십진법 표기를 보자. 마지막 자리의 4가 5로 변한다면 전체 수는 0.01만큼 커진다. 그 다음 자리는 0.001만큼 작아질 것이다. 이것을 이진수일 경우에도 똑같이 표기할 수 있는 것이다.

부동 소수점 표기
32비트의 공간으로 실수를 표현할 때 가장 간단하게 생각할 수 있는 방법은 16비트씩 정수부와 소수부가 사용하는 것이다. 하지만 이 방식의 문제점은 수에 따라서 비트의 낭비가 커질 수 있다는 점이다. 그래서 IEEE 754를 포함한 대부분의 실수 표준에서는 소수점을 옮길 수 있도록 만들어졌다. 한칸만 남겨두고 모두 소수영역으로 만든 다음 자리수를 저장하고 최상위 비트를 제외한 나머지만 저장하는 것이다. 이렇게 만들면 변수는 다음과 같은 3가지 정보를 저장하게 된다.

  • 부호 비트(sign bit)
  • 지수 : 소수점을 몇 칸 옮겼나
  • 가수 : 소수점을 옮긴 실수의 최상위 X비트

이렇게 만들면 꽤나 많은 용량을 저장할 수 있지만 그 비트들이 지수를 표현하는데 사용된다면, 소수점을 거의 4503조 자리나 움직일 수 있게 되는 것이므로 정확도가 바닥을 치게 된다. 그래서 이 표준을 만든 사람들이 지수보다는 가수에 훨씬 많은 비트수를 할당하기로 했다. 그렇기 때문에 사실 32비트가 아닌 64비트의 변수를 사용하기를 권장하는 것이다.

실수 비교하기
제일 처음부분에서 언급했던 이해하기 어려운 에러가 바로 이런 종류이다. 논리는 맞지만 근사값으로 바뀌는 과정에서 에러가 발생하는 것이다. 이 문제를 해결하는 단편적인 방법 중 하나는 아래의 코드에서 볼 수 있다.

bool absoluteEqual(double a, double b)
{
    return fabs(a - b) < 1e-10; // 소수점 10자리까지의 오차한계
}

물론 이 방법은 더 작은 자리의 소수가 나올 경우 무너지는 논리이다. 그렇게 되었을 떄 해결할 수 있는 방법은 크게 두 가지이다.

  • 비교할 실수의 크기들에 비례한 오차 한도를 정한다.
  • 상대 오차를 이용한다.

주의해야 할 점은 이 정도로 실수에 대한 모든 예외처리를 했다고 생각하면 안된다는 것이다. 위의 것들은 그저 빙산의 일각에 불과하다.

results matching ""

    No results matching ""