C - 1.2. 두번째 예제

목차

1. 두번째 프로그램

K&R에서는 바로 C언어의 문법을 가르치지 않는다. 간단한 프로그램을 짜 보면서 먼저 C언어에 대한 감부터 잡게 하는 방식을 사용하고 있다. 따라서 우리는 본격적으로 C언어의 문법들을 세세하게 접하기 전에 복잡하지는 않지만 감을 잡는 데에 충분히 도움이 될 만한 프로그램을 몇 가지 짜 보게 된다.

이번 글에서는 그런 접근 방식을 따라서, 간단한 두 번째 프로그램을 작성하면서 C언어에 대한 감을 잡는다. 두번째 프로그램은 바로 화씨를 섭씨로 환산한 값을 인쇄해 주는 프로그램이다.

코드는 다음과 같다.

#include <stdio.h>
 
/* 화씨 0, 20, ... , 300도에 대해
	화씨 - 섭씨 표를 출력한다 */
 
main() {
	int fahr, celsius;
	int lower, upper, step;
 
	lower = 0;
	upper = 300;
	step = 20;
	fahr = lower;
 
	while (fahr <= upper) {
		celsius = 5 * (fahr - 32) / 9;
		printf("%d\t%d\n", fahr, celsius);
		fahr = fahr + step;
	}
}

2. 두번째 프로그램 설명

코드를 구성하는 요소를 하나씩 설명해 보자.

#include <stdio.h>

이는 지난 글에서 다루었으므로 생략한다. 입출력 함수를 다루게 해주는 라이브러리를 쓰게 해주는 부분이다.

/* 화씨 0, 20, ... , 300도에 대해 화씨 - 섭씨 표를 출력한다 */

/* */ 로 감싸인 부분은 주석이다. 이걸로 감싸인 부분은 코드를 컴파일할 때 무시된다. 이런 주석은 프로그램의 수월한 이해를 위해서 쓰일 수 있다. 공백, 탭, 개행이 들어올 수 있는 어느 위치에나 주석은 들어갈 수 있는데, 이는 거의 코드의 어느 부분에서나 쓰일 수 있다는 것을 의미한다.

주석을 쓰는 방법 중 하나는 충분히 작동하지만 지금은 쓰지 않는 코드를 통째로 주석 처리해 놓는 것이다. 그렇게 하면 그 코드가 필요해질 때 언제든 주석을 해제하여 사용할 수 있다.

int fahr, celsius; int lower, upper, step;

다음 줄은 변수를 정의하는 부분이다. C언어에서 변수는 사용되기 전에 무조건 정의되어야 한다. 변수는 간단히 생각하면 프로그램에서 쓸 어떤 값을 담아둘 공간이라고 생각하면 된다.

그런데 우리는 변수를 쓰기 전에 그 변수에 어떤 종류의 값을 저장할 것인지 얼만큼의 메모리를 할당할지 또 어떤 이름을 붙일지를 알려줘야 한다. 변수의 정의는 우리가 알려줘야 할 정보들을 담은, 변수의 종류(타입)와 변수명들로 이루어진다.

변수의 타입이란 것은 변수에 어떤 종류의 값을 저장할 것이며 그걸 위해 얼만큼의 메모리가 필요한지를 컴파일러에 지시한다. 변수의 이름은 말 그대로 값을 저장할 공간의 이름이다.

여기서는 int는 정수형이며 문자를 나타내는 char, 실수형을 나타내는 float, double등의 다른 여러가지 타입들이 있다는 것만 짚고 넘어간다.


NOTE

뒤에 다시 나오겠지만 변수의 타입은 변수가 저장될 메모리의 크기도 결정한다. 그러나 표준에는 변수 타입별 메모리 크기의 대소관계만 정의할 뿐 정확한 크기는 정해주지 않는다. 물론 현재의 실질적으로 쓰이는 대부분의 컴파일러는 int를 4바이트로 정의한다. 그런데 The C programming language 의 저자 커니핸이 책을 쓸 당시에는 그렇지 않았다. 따라서 K&R C에서는 16비트(즉, 2바이트)의 정수형도 흔하다고 설명한다...

 


또한 이러한 타입들로 이루어진 배열(array), 구조체(structure), 공용체(union) 그리고 관련된 포인터와 함수들도 존재한다. 이 시리즈를 진행하면서 하나하나 배워나갈 것이다.

따라서 위의 코드는 int타입의 변수를 fahr, celcius 등의 이름으로 5개 정의한다고 설명할 수 있겠다.

lower = 0; upper = 300; step = 20; fahr = lower;

각 변수에 값을 할당하는 코드이다. 몇몇 변수에 0,300,20 등의 값을 대입하였으며 fahr=lower 에서는 변수끼리 대입하기도 한다.

이때 많은 온도들을 한번에 환산해야 하므로 우리는 반복문을 사용하기로 한다. 반복문은 지금으로서는 그냥 비슷한 작업을 여러 번 반복하게 하는 의미라고 생각하자.

c while (fahr <= upper) { ... }

while 은 반복에 쓰이는 문법이다. while 뒤의 소괄호 안의 문(여기서는 fahr <= upper)이 참인지 테스트하여 참이면 중괄호 안의 문장을 실행한다. 중괄호 안의 코드가 끝나면 다시 소괄호 안의 문이 참인지 테스트한다. 이러한 동작을 반복하다가 소괄호 안의 문장이 거짓이 되면 반복문을 종료한다.

이를 보여주는 예시를 들어보자. 다음 코드는 1 2 3 4 5 를 출력한다.

#include <stdio.h>
 
main() {
    int i;
    i = 1;
    while (i <= 5) {
        printf("%d ", i);
        i = i + 1;
    }
}

이때 우리는 while 뒤의 중괄호 안의 코드를 모두 들여쓰기해 주었다. 어떤 부분이 while 반복문 안에 들어 있는지 한눈에 알 수 있게 해주려는 목적이다. 이러한 들여쓰기는 코드의 동작과는 연관성이 없지만 가독성을 매우 높여주기 때문에 대부분의 에디터에서 자동으로 해준다.

만약 그렇지 않다면 들여쓰기는 가독성에 매우 큰 영향을 미치므로 신경써 주자. 이렇게 코드를 가독성 좋게 짜는 것은 남과 같이 협업할 때도, 또 심지어 자신의 코드를 읽을 때도 도움이 된다.

중괄호를 어떤 스타일로 쓰느냐도 사람들 간에 많은 논쟁이 있지만 그건 적절한 스타일을 택하기만 하면 된다. 가독성을 높이는 이러한 조치들은 대부분의 에디터에서 이미 자동으로 해주므로, 알고만 있자.

celsius = 5 * (fahr - 32) / 9; printf("%d\t%d\n", fahr, celsius); fahr = fahr + step;

화씨 온도에 적절한 환산식을 적용해서 섭씨 온도 변수에 대입해 주는 부분이다. 그리고 화씨 온도와 그에 대응되는 섭씨 온도를 출력해 준다.

이때 (fahr - 32)5/95/9 를 곱해주는 것이 아니라 5를 곱한 후 9로 나누는 것에 주목하라. 이는 C에서 정수 나눗셈의 몫은 무조건 정수가 되고 소수점은 버리므로 5/9의 값은 0이 되기 때문이다. 따라서 의미있는 값을 얻기 위해서는 위와 같이 해야 한다.

또한 위의 printf 가 들어간 문장은 printf 가 어떻게 동작하는지를 보여준다. printf의 첫번째 인자는 출력될 문자열이다. 이때 문자열에 %가 들어간 포맷 문자열이 있을 경우 이는 다른 인수들의 값으로 치환된다. 이에 대해서는 후에 더 자세히 다룬다.

그러나 printf는 C언어의 일부는 아니다. C언어 자체에는 입출력이 없다. 그저 표준 라이브러리에 동작이 정의되어 있는 함수일 뿐이며 그 내부적인 구현은 구현체에 따라 조금씩 다르다. 단 printf의 동작은 표준에 정의되어 있으므로 어느 컴파일러에서나 똑같이 동작한다.

이러한 입출력 관련 문제들은 C언어 자체의 문법과는 큰 관련이 없으므로 마지막에 가서 자세히 다루기로 한다.

3. 몇 가지 개선

3.1 출력 포맷

현재 코드의 문제는 출력이 오른쪽 정렬되지 않아서 못생겼다는 점이다. 이는 출력 포맷을 적절히 조절하는 것으로 쉽게 해결할 수 있다. %6d 와 같이 출력 포맷의 알파벳 앞에 숫자를 붙이면 그 출력의 너비가 된다. 그리고 출력은 그 너비 내에서 오른쪽 정렬된다.

따라서 위의 출력을 다음과 같이 바꿀 수 있다.

printf("%3d %6d\n", fahr, celsius);

이러면 첫번째 숫자는 3의 너비 내에서 오른쪽 정렬되어 출력되고, 두번째 숫자는 6의 너비 내에서 오른쪽 정렬되어 출력된다. 한번 각자의 환경에서 실행해 보자.

그러나 만약 %2d와 같이, 출력할 숫자의 길이보다 너비가 더 작게 설정되었을 시 설정된 너비는 무시되고 그대로 출력된다.

3.2 실수형 값

현재는 출력이 모두 정수로만 이루어진다. 환산된 온도 값들의 소수점을 버렸기 때문이다. 그러나 더 섭씨-화씨의 더 정확한 환산값을 구하고 싶을 수 있다.

그러기 위해서는 환산식의 나눗셈에서 나오는 소수점을 버리지 않아야 한다. 이를 위해서 실수 타입을 사용해 코드를 작성한다.

#include <stdio.h>
 
/* 화씨 0, 20, ... , 300도에 대해
	화씨 - 섭씨 표를 출력한다 - 실수형 버전 */
 
main() {
	float fahr, celsius;
	int lower, upper, step;
 
	lower = 0;
	upper = 300;
	step = 20;
	fahr = lower;
 
	while (fahr <= upper) {
		celsius = (5.0 / 9.0) * (fahr - 32.0);
		printf("%3.0f %6.2f\n", fahr, celsius);
		fahr = fahr + step;
	}
}

화씨와 섭씨 온도 변수가 실수 타입인 float 가 되었고 환산식이 조금 달라졌다. 소수점을 버리는 정수형 나눗셈과 달리 실수형 숫자들 간의 나눗셈은 소숫점을 버리지 않으므로 더 정확한 값을 구해서 출력하는 게 가능하다.

환산식의 (5.0 / 9.0)도 비슷한 맥락이다. 아까는 정수형 나눗셈의 소수점 버리기 때문에 5/9를 그대로 곱할 수 없었지만 (5.0 / 9.0) 은 실수형 나눗셈이므로 결과가 그대로 나온다. (실수를 이진수 저장할 때의 오차 때문에 완벽한 그대로는 아니지만, 정수형 나눗셈보다는 훨씬 정확한 결과다)

이때 유의할 점은 덧셈이나 나눗셈 등의 2항 계산에서 두 항 다 같은 타입이면 그대로 연산되지만 한쪽이 정수형, 한쪽이 실수형이면 나머지 한쪽이 묵시적으로 실수형으로 변환되어 연산된다는 점이다. 그러나 가독성을 위해 보통 실수형 상수에는 소수점을 붙여 써 준다.

이는 fahr = lower; 와 같은 대입연산이나 다른 비교연산에서도 마찬가지다. 정수형인 lower가 실수형 변수에 대입될 때 그 값은 실수형으로 묵시적 형변환되어 대입된다. 이에 대해서는 후에 더 자세히 다룬다.

또한 아까의 너비 포매팅과 같이 소수점을 몇째 자리까지 출력할 것인지에 대한 포매팅도 존재한다. 가령 %.2f라 하면 그 포맷에 해당되는 출력을 소수점 둘째 자리까지만 한다는 의미이다.

위 코드에 있는 것처럼, 너비 포매팅과 같이 쓰는 것도 가능하다. %6.2f는 실수형의 포맷 인수를 너비 6으로, 소수점 둘째 자리까지 출력하겠다는 의미이다.