Skip to content

Latest commit

 

History

History
582 lines (483 loc) · 32.6 KB

README.md

File metadata and controls

582 lines (483 loc) · 32.6 KB

기본 연산자

C의 기본적인 연산자에 대해 배웁니다.

연산자(operator)

컴퓨터(computer)라는 단어는 사실 계산(compute)하는 기계라는 말입니다. 2장에서 말씀드렸듯이, 컴퓨터는 메모리와 연산장치로 이루어져있습니다. 이번 장에선 그 연산장치가 어떤 기본 연산을 할 수 있는지 소개하고자 합니다.

연산자는 연산장치에게 수학에서 연산자를 쓰는 것과 비슷한 방법으로 연산에 관련된 명령을 할 수 있는 방법입니다. 연산을 수행할 값이나 식을 피연산자(operand) 라고 부릅니다. 이 피연산자의 개수에 따라 연산자를 단항 연산자(unary operator), 이항 연산자(binary operator), 삼항 연산자(ternary operator)로 분리할 수 있습니다. 예를 들어, 1 + 2에서 +는 피연산자가 12 두 개이기 때문에, 이항 연산자가 됩니다.

대입 연산자(assignment operator)

대입 연산자는 메모리에 값을 저장하는 연산자입니다. 2장의 의사 코드를 다시 상기해보도록 합시다.

load 2
load 3
add
store 1

여기서 store의 역할을 하는 것이 바로 대입 연산자입니다. 실제 C 코드를 통해 알아보도록 합시다.

int main()
{
    int result;
    result = 6;
    result = 5;
}

이 코드가 어떤 일을 하는지 감이 오시나요? int는 4바이트를 차지하는 자료형이니, result가 메모리 공간 20번부터 23번을 차지하고 있다고 해봅시다. 초기식이 없기 때문에, int result; 까지는 20~23번에 무엇이 들어있는지 알 수 없습니다. 그 다음 줄 result = 6;이 지나면 20~23번엔 6이 들어있게 됩니다. 그 다음 줄 result = 5;가 지나면 20~23번엔 5가 들어있게 됩니다. 그러니까 이 코드를 사람들이 사용하는 말에 가깝게 쓰면

프로그램이 시작했을 때,
    적당한 공간을 result라고 이름 짓습니다.
    result라는 이름의 공간에 6을 넣습니다.
    result라는 이름의 공간에 5를 넣습니다.

가 됩니다.

20,21,22,23번 각각에 6이나 5가 들어가는 것이 아니라는 것에 주의해주세요. 4장에서 설명했듯, int라는 자료형은 더 큰 숫자를 표현하기 위해, 4바이트의 공간을 묶어서 최대 32자리 2진수를 표현하기 위해 사용하는 자료형입니다. 즉, 20~23번엔 00000006(16)(2진수로 32자리)이 들어있기 때문에, 네 공간 중 세 공간엔 0만 들어있고, 나머지 한 공간에 6이 들어있게 됩니다. 여러 공간을 묶어 한 숫자를 표현하는 방식에 대해서도 나중에 자세히 다룰 것입니다. 지금은 어떤 자료형은 여러 공간이 하나의 숫자를 표현하도록 한다는 사실만 기억해주세요.

앞으로 "변수가 가리키는 메모리 공간에 무엇을 대입한다"는 말을 줄여서 "변수에 무엇을 대입한다"라고 설명하겠습니다. 실제로 프로그래머들이 사용하는 표현도 "변수에 무엇을 대입하다"입니다. 변수와 메모리 공간이라는 단어를 함께 쓰는 건 제가 여러분들이 메모리라는 개념을 이해할 수 있도록 만들어낸 표현입니다.

다시 연산자로 돌아옵시다. 수학에서 = 라는 기호는 좌항과 우항의 값이 같다라는 것을 표현하는 기호였습니다. 하지만 C에선 우항의 표현식(expression)의 값을 좌항의 변수가 가리키고 있는 메모리의 공간에 저장한다는 의미입니다. 이 =라는 기호를 대입 연산자라고 부릅니다.

산술 연산자(arithmetic operator)

산술 연산자는 쉽게 말해 사칙연산을 수행하는 연산자입니다. C에선 다섯가지 산술 연산자를 지원하는데요, 덧셈 연산자(+), 뺄셈 연산자(-), 곱셈 연산자(*), 나머지 연산자(/), 나머지를 구하는 연산자(%) 다섯가지입니다. /가 단순히 나눗셈을 수행하는 것이 아니라, 몫을 구하는 연산자임에 주의할 필요가 있습니다. 예시를 보도록 합시다.

#include <stdio.h>

int main()
{
    int a = 17, b = 6;
    int c;
    c = a + b;
    printf("a + b = %d    ", c);
    printf("a - b = %d    ", a - b);
    int d = a * b;
    printf("a * b = %d    ", d);
    printf("a / b = %d    ", a / b);
    printf("a % b = %d    ", a % b);
    int e = a * 2;
    printf("a * 2 = %d", e);
}
a + b = 23    a - b = 11    a * b = 102    a / b = 2    a % b = 5    a * 2 = 12

이 예제는 많은 것을 보여주는데요, 여기서 알 수 있는 것을 정리하면,

  • C에서 사칙연산을 수행하는 방법에 대해 알 수 있습니다.
  • c = a + b를 통해 대입 연산자의 우항에 단순한 값이 아니라 식을 넣을 수 있다는 것을 알 수 있습니다. 제가 지금까지 우항에 들어올 수 있는 것을 값(value)이 아니라 표현식이라고 했는데요, 바로 이런 특징을 고려한 것이었습니다.
  • int d = a * b를 통해 변수를 선언할 때도 초기식에 값이 아니라 식을 넣을 수 있다는 것을 알 수 있습니다.
  • printf("a - b = %d", a - b)를 통해 printf에도 식을 넣을 수 있다는 것을 알 수 있습니다. 이렇게 C를 비롯한 다양한 프로그래밍 언어에선 값이 들어갈 자리에 식도 집어넣을 수 있습니다.
  • 자료형이라는 건 변수 뿐만 아니라 값이나 식에도 있다는 것을 알 수 있습니다.
  • int를 사칙연산한 결과는 항상 int임을 알 수 있습니다.
  • int e = a * 2를 통해 반대로 변수가 들어갈 자리에 숫자를 사용할 수 있는 경우가 있다는 것을 알 수 있습니다.

위 예제는 정수 자료형에 대해 산술 연산자를 사용한 것이었는데요, 부동소수점 자료형(float, double)에 대해선 나눗셈 연산자의 행동이 달라집니다.

#include <stdio.h>

int main()
{
    double a = 17, b = 6;
    printf("a / b = %lf", a / b);
}
a / b = 2.833333

이렇게 몫을 구하는 것이 아니라 우리가 일반적으로 생각하는 나눗셈을 수행하는 것을 알 수 있습니다. 17/6은 무한소수로 표현되기 때문에, 17/6에 가까운 소수를 사용한 것도 알 수 있습니다. 그럼 나머지 연산자는 어떨까요? 다음 예제를 Visual Studio에 입력해보세요.

#include <stdio.h>

int main()
{
    double a = 17, b = 6;
    printf("a % b = %lf", a % b);
}

printf안의 ab에 빨간 줄이 뜨는 것을 알 수 있습니다. 거기에 마우스 포인터를 올리면,

Your first error

이렇게 "expression must have integral type", 즉 "표현식의 자료형이 정수형이어야 합니다"라는 오류 메시지를 보여줍니다. 이렇게 문법적으로 맞지 않은 C 코드를 사용하면 에디터가 에러 메시지와 함께 빨간 줄을 표시해주는 것을 알 수 있습니다. 주체가 에디터인 것에 주목해주세요. 여기서 Ctrl + F5를 누르면 어떻게 될까요?

Error output

이렇게 컴파일러도 에러 메시지와 함께 소스 코드의 어디가 문제인지 알려주는 것을 알 수 있습니다. 이번엔 주체가 컴파일러인 것에 주목해주세요. 여기서 밑에 "Error List"를 누르시면 이렇게 에디터가 컴파일러의 메시지를 잘 정리해서 표시해주는 것을 알 수 있습니다.

Message adapter result

여기서 컴파일러와 에디터를 철저하게 구분하는 이유가 있는데요, 소스 코드를 컴파일하는 것은 의외로 시간이 많이 걸리는 작업입니다. 소스 코드의 오류를 찾아내기 위해선 컴파일을 일부 수행해야하기 때문에, 소스 코드를 입력하고 나서 에디터가 오류를 표시해 줄 때까지 시간이 많이 걸릴 수 있습니다. 중간에 에디터에서 오류가 발생하게 되면 에디터가 오류를 표시해주지 않을 수도 있습니다. 프로그래밍을 배우기 시작하는 많은 분들이 "빨간 줄은 안 뜨는데 왜 오류가 나요?"라는 질문을 하시는데, 빨간 줄을 표시해주는 주체와 실제 오류인지 아닌지 확인해줄 수 있는 주체가 다르다는 것을 항상 기억해주시길 바랍니다. 빨간 줄이 없어도 컴파일러에서 오류가 나면 소스 코드에 오류가 있는 것이고, 빨간 줄이 있어도 컴파일러에서 오류가 안 나면 소스 코드에 문제가 없는 것입니다.

컴파일러의 역할을 자세히 알고 계시는 분을 위해 부가 설명을 하자면, 컴파일까지는 괜찮은데 링크 과정에서 오류가 발생하는 경우도 있습니다. 컴파일러와 링커 구분은 함수를 다루고 나서야 할 예정입니다.

대입 연산자에서 자주 나오는 실수들

대입 연산자와 수학에서의 등호가 모양이 같은 탓에, 많은 분들이 종종 다음과 같은 실수를 저지르십니다.

int main()
{
    int a = 5;
    a + 1 = 6;
}

a + 1은 메모리에 공간을 갖고 있는 변수가 아닙니다. 따라서 우항의 값을 좌항에 대입할 수 없습니다.

대입 연산자는 메모리에 값을 집어넣는다는 개념이기 때문에, 다음과 같은 코드도 생각할 수 있습니다.

#include <stdio.h>

int main()
{
    int a = 5;
    a = a + 1;
    a = 2 + a;
    printf("%d", a);
}
8

먼저 a5가 대입됩니다. a = a + 1에 의해, 연산장치가 a에서 5를 꺼내 1을 더한 결과인 6을 다시 a에 집어넣습니다. a = 2 + a도 마찬가지로, 2a에서 꺼낸 6을 더한 결과인 8a에 집어넣습니다. 결과적으로 a8이 들어있기 때문에 프로그램의 결과는 8이 됩니다. 항상 =가 값을 메모리에 저장한다는 개념임을 명심할 필요가 있습니다.

연산자 우선순위(operator precedence)

수학에서 한 식에 여러 연산자를 섞어쓸 수 있듯이, C에서도 한 식에 여러 연산자를 사용할 수 있습니다.

#include <stdio.h>

int main()
{
    printf("%d", 1 + 3 * 5);
}
16

수학에서 그런 것처럼, C에서도 연산자 우선순위가 적용됩니다. */, %+- 보다 우선순위가 높습니다. 그래서 1 + 3 * 5는 실제로 1 + (3 * 5)로 번역됩니다. 우선순위에 따르지 않고 싶으면 수학에서 하는 것처럼 괄호를 사용하면 됩니다.

#include <stdio.h>

int main()
{
    printf("%d", (1 + 3) * 5);
}
20

수학에서와 다르게, C에선 연산의 우선순위를 정하기 위해 괄호를 여러 개 사용하더라도 항상 소괄호( )만을 사용하여야 합니다. 예를 들면 원래 수학에서

36 / {(1 + 2) * 5}

로 표기하던 것도 36 / ((1 + 2) * 5) 로 써야 합니다.

#include <stdio.h>

int main()
{
    printf("%d", 36 / ((1 + 2) * 5));
}
2

대입 연산자 =도 연산자 우선순위가 있는데요, 산술 연산자보다 낮은 순위를 가집니다. 아까 변수에 덧셈의 결과를 대입할 때, c = a + b; 처럼 썼습니다. 여기에 우선순위에 따라 괄호를 표시하면 어떻게 될까요? c = (a + b);가 되어 우리가 생각하는대로 작동하게 될 것임을 알 수 있습니다. =+보다 높은 순위를 가지게 된다면 (c = a) + b가 될텐데, 프로그램이 우리가 의도한대로 작동하지 않을 것 같습니다. 앞으로 다양한 연산자를 공부하게 될텐데요, C의 전체 연산자 우선순위는 여기, C++은 여기를 참고해주세요.

연산자 결합방향(operator associativity)

1 + 2 + 3(1 + 2) + 3으로 해석하는게 맞을까요, 1 + (2 + 3)으로 해석하는게 맞을까요? 만약 이런 부분을 표준에서 다루지 않았다면 컴파일러를 제작하는 회사들이 이 방향을 원하는대로 정했을 것입니다. 그럼 어디서는 돌아가던 코드가 어디서는 돌아가지 않게 될 것입니다. 그래서 C 표준에선 이런 경우 괄호를 어떻게 표시해야하는지도 정해놓았는데요, 그걸 연산자 결합방향이라고 합니다. 연산자 결합방향도 위의 두 참고 자료에 적혀있는데요, 지금까지 배운 연산자들로 이루어진 예시로 결합방향에 대해 설명하겠습니다.

#include <stdio.h>

int main()
{
    int a = 5, b = 6, c = 7;
    a = b = c + 1;
    printf("%d %d %d", a, b, c);
}
8 8 7

먼저 덧셈 연산자가 대입 연산자보다 우선순위가 높습니다. 그럼 a = b = c + 1a = b = (c + 1)로 해석됩니다. 대입 연산자는 결합방향이 오른쪽에서 왼쪽입니다. 오른쪽에서 왼쪽이라는 것은 오른쪽에 있는 피연산자부터 괄호를 친다는 것입니다. 즉 a = b = (c + 1)a = (b = (c + 1))으로 해석되는 것입니다. 그럼 일단 bc + 1의 결과인 8이 대입되는 것은 알았습니다. 그럼 왜 a에도 8이 대입되는 것일까요? 그건 대입연산자의 결과가 대입한 값이라고 명시되어있기 때문입니다. 즉, b = (c + 1)의 결과는 8이 되고, 결과적으로 a = 8이 되어 a에도 같은 값이 대입되게 됩니다.

반대로 산술 연산자들은 결합방향이 왼쪽에서 오른쪽입니다. 즉 왼쪽의 피연산자부터 괄호를 친다는 것입니다. 예를 들어 a + b + c(a + b) + c가 됩니다. 만약 산술 연산자의 결합방향이 오른쪽에서 왼쪽이였다면 어떻게 될까요? 8 - 4 - 2의 결과는 수학적으로 2입니다. 그런데 결합방향이 오른쪽에서 왼쪽이면 8 - 4 - 28 - (4 - 2)가 되어 결과가 6이 되어버립니다. 이건 사람들이 일반적으로 생각하는 것과 다릅니다. 이렇게 연산자의 우선 순위와 연산자의 결합방향은 사람들이 일반적으로 생각하는대로 작동하도록 설정되어있습니다.

리터럴(literal)

위에서 변수 대신에 값을 사용해도 된다는 것을 배웠습니다. 아까 부동소수점 자료형 변수 두 개와 나눗셈을 하는 예제를 봅시다.

#include <stdio.h>

int main()
{
    double a = 17, b = 6;
    printf("a / b = %lf", a / b);
}

그럼 a / b 대신에 17 / 6을 사용해도 같은 결과가 나오겠네요.

#include <stdio.h>

int main()
{
    printf("a / b = %lf", 17 / 6);
}

하지만 이 예제를 실제로 실행하면 같은 결과가 나오지 않습니다. 왜 그럴까요? 17과 6 위에 마우스 포인터를 올려놓아주세요.

int literal

이렇게 176의 자료형이 int인 것을 알려주고 있습니다. 즉, 여기서 17 / 6int끼리의 나눗셈을 했기 때문에, 결과도 int가 되어, 잘못된 형식 지정자를 사용하게 된 것이 됩니다.

4장에서 변수에 대해 다룰 때 초기식이 없으면 변수에 어떤 값이 들어있는지 알 수 없다는 이야기를 한 적이 있는데요, 마찬가지로 이 경우에도 무엇이 출력될지 알 수 없습니다. 이건 C 표준에서, 잘못된 형식 지정자를 사용할 경우, printf가 어떻게 작동해야하는지 정해놓지 않았기 때문에, 컴파일러에 따라 잘 작동할 수도 있고, 그렇지 않을 수도 있기 때문입니다. 이렇게 특정 경우에 어떤 기능이 어떻게 돌아갈지 정해놓지 않은 것을 정의되지 않은 행동(undefined behavior) 라고 합니다. 초기식을 넣지 않았거나 값을 대입하지 않은 변수의 값을 출력하는 것도 undefined behavior의 일종입니다.

그래서 자료형이 int가 아니라 double인 숫자를 적기 위해선 소수점을 함께 적어야 합니다.

#include <stdio.h>

int main()
{
    printf("a / b = %lf", 17.0 / 6.0);
}
a / b = 2.833333

아까 했던 것 처럼 17.06.0 위에 마우스를 올리면 자료형이 double이 되는 것을 알 수 있습니다.

이렇게 intdouble 뿐만 아니라 다른 자료형을 가진 숫자도 만들 수 있습니다.

자료형 예시 설명
float 1.0f 소수 뒤에 f를 붙입니다.
long 17l 정수 뒤에 l을 붙입니다.
long long 23ll 정수 뒤에 ll을 붙입니다.
unsigned 39u 정수 뒤에 u를 붙입니다.
unsigned long 28ul 정수 뒤에 ul을 붙입니다.
unsigned long long 28ull 정수 뒤에 ull을 붙입니다.
#include <stdio.h>

int main()
{
    printf("%d %f %llu %lf", 20, 1.0f, 28ull, 2.5);
}
20 1.000000 28 2.500000

이렇게 변수에 저장되지는 않았지만 식이 들어가는 자리에 바로 넣은 값을 리터럴이라고 부릅니다. 위의 예시는 모두 10진수 리터럴인데요, 2진수, 8진수, 16진수 리터럴도 있습니다.

#include <stdio.h>

int main()
{
    short s = 0b10010;
    printf("%hd %u %lld", s, 024u, 0x234ll);
}
18 20 564

이렇게 0b로 시작하는 숫자는 2진수, 0으로 시작하는 숫자는 8진수, 0x으로 시작하는 숫자는 16진수가 됩니다. 여기에 위에서 다뤘던 것 처럼 ul 같은 것을 붙여 자료형을 지정할 수 있습니다.

지금은 숫자의 리터럴만을 다뤘지만, 나중에 다른 종류의 리터럴도 나올 예정입니다.

literal말 그대로의라는 뜻의 형용사입니다.

형변환 연산자(casting operator)

아까 176 위에 마우스를 올렸을 때, (int)17 이라고 뜬 것을 보실 수 있었을 겁니다. 이 (int)라는 표시는 단순히 에디터에서 보여주는 것이 아니라, 유효한 C의 연산자 중 하나입니다. 형변환(型變換) 연산자라는 이름에서 알아차린 분도 계시겠지만, 형변환 연산자는 어떤 식의 자료형을 다른 자료형으로 바꿔주는 기능을 합니다. 형변환 연산자를 사용하는 방법은 어렵지 않습니다.

(<자료형>)<식>

사용 예시는 다음과 같습니다.

#include <stdio.h>

int main()
{
    printf("%lf, %d", (double)17, (int)(17.0 + 8.0));
}
17.000000, 25

이렇게 결과가 int인 식은 double로, double인 식은 int로 변환할 수 있습니다. 형변환 연산자는 산술 연산자보다 우선 순위가 높고, 오른쪽에서 왼쪽으로 결합합니다. 즉, (int)17.0 + 8.0((int)17.0) + 8.0으로 해석되고, (double)(int)7.5(double)((int)7.5)로 해석됩니다.

암시적인 형변환(implicit type conversion)

연산자 양 옆의 두 항의 자료형이 서로 다르면 어떻게 될까요? 예를 들어서, 5.0 / 2는 연산자 / 양 옆에 doubleint가 있습니다. 이 때 우리가 원하는대로 2.5가 될까요, 아니면 정수의 나눗셈처럼 처리되어 2가 될까요, 아니면 아까 전 처럼 오류가 발생할까요? 만약 연산자의 두 피연산자의 자료형이 다르다고 해서 항상 오류가 발생하면 프로그래밍 언어를 사용하기 힘들것입니다. 그래서 5.0 / 2의 경우, int2double로 형변환되어 결과가 double이 됩니다.

#include <stdio.h>

int main()
{
    double d = 5.0;
    int i = 2;
    printf("%lf", d / i);
}
2.5

그럼 실수와 정수 말고 정수와 정수 자료형인 경우는 어떻게 될까요? unsigned short s = 65535;unsigned i = 1;가 있을 때, s + i의 결과는 어떻게 될까요? 일단 수학적으로 덧셈의 결과가 65536인 건 확실합니다. 만약 i가 암시적으로 unsigned short로 변환되었다면, 그럼 값이 65536인 unsigned short가 되는데, unsigned short는 0부터 65535(2¹⁶ - 1)까지만 표현할 수 있는 자료형입니다. 그러니까 표현할 수 없는 숫자가 결과가 됩니다. 그래서 이 경우엔 sunsigned로 형변환되는 것이 더 맞아보입니다. 그래서 이렇게 크기가 다르지만 둘 다 unsigned이거나 둘 다 signed일 경우 둘 중에 크기가 더 큰 자료형으로 형변환되게 됩니다.

#include <stdio.h>

int main()
{
    unsigned short s = 65535;
    unsigned i = 1;
    printf("%u", s + i);
}
65536

이렇게 형변환 연산자를 통해 명시적으로 형변환을 하지는 않았지만 컴파일러가 자동으로 형변환을 해주는 것을 암시적 형변환(implicit conversion)이라고 합니다.

이외에도

  • 둘 중에 하나가 unsigned이고 나머지 하나가 signed일 때, unsigned인 쪽의 크기가 더 크거나 같으면 signed쪽이 나머지 하나의 자료형으로 암시적 형변환
  • 둘 중에 하나가 unsigned이고 나머지 하나가 signed일 때, unsigned인 쪽의 크기가 더 작으면 unsigned쪽이 나머지 하나의 자료형으로 암시적 형변환

된다는 규칙이 있습니다. 규칙이 많이 복잡하다고 느끼실 것이라고 생각합니다. 그래서 Java같은 언어엔 unsigned 정수형이 존재하지 않습니다. 어떤 프로그래머들은 unsigned가 있어도 쓰지 않는게 좋다고 말하고요. 그래서 웬만하면 unsigned를 쓰지 말고, 정말 음수가 될 일이 없을 때에만 쓰는 것이 현명할 것 같습니다.

축소 변환(narrowing conversion)

7.5int로 형변환하게 되면 어떻게 될까요? int는 정수 자료형이기 때문에, 소수점을 표현할 수 없습니다. 그래서 (int)7.5를 출력해보면, 7로 변하는 것을 확인할 수 있습니다.

#include <stdio.h>

int main()
{
    printf("%lf %d", (double)(int)7.5, (int)7.5);
}
7.000000 7

이렇게 일부 자료가 손실될지도 모르는 형변환을 축소 변환(narrowing conversion)이라고 합니다. 축소 변환은 저렇게 명시적으로 할 수도 있지만, 다음과 같이 암시적으로도 할 수 있습니다.

#include <stdio.h>

int main()
{
    int i = 7.5;
    printf("%d", i);
}
7

이런 암시적 축소 변환은 버그의 원인이 되는데요, 예를 들어 어떤 프로그래머가 원래 double로 써야하는 걸 int로 썼을 때 그 프로그램에 심각한 오류를 초래할 수 있기 때문입니다. 그래서 요즘 나오는 컴파일러들은 이렇게 암시적 형변환이 발생한 경우,

"Narrowing conversion warning"

이렇게 오류는 아니지만 경고를 띄워줍니다.

경고(warning)라는 건 코드를 컴파일할 수는 있지만 코드가 좋지 않음을 알려주는 기능입니다. 오류(error)가 발생하면 항상 코드를 컴파일할 수 없는 것과는 차이가 있습니다. 컴파일러의 기능 중에는 모든 경고를 오류로 취급하는 기능이 있습니다. 이 기능을 사용하면 축소 변환이 필요할 때, 프로그래머가 항상 명시적으로 표시하도록 강제할 수 있습니다.

#include <stdio.h>

int main()
{
    int i = (int)7.5;
    printf("%d", i);
}
7

산술 오버플로 및 언더플로(arithmetic overflow and underflow)

다음 예제를 잘 봐주세요.

#include <stdio.h>

int main()
{
    unsigned short i = 65535, j = 1, k = 0;
    printf("%hu %hu", i + j, k - j);
}

이 예제의 결과는 놀랍게도

0 65535

입니다. 65535+1의 결과가 65536이고, 0-1의 결과가 -1임을 우리는 잘 알고 있습니다. 그런데 컴퓨터는 왜 065535라고 할까요? 65536-1unsigned short가 표현할 수 있는 범위가 아니라는 것에 주목할 필요가 있습니다. 이렇게 결과가 표현할 수 있는 최댓값을 넘는 경우를 산술 오버플로(arithmetic overflow)라고 하고, 최솟값을 못 넘는 경우를 산술 언더플로(arithmetic underflow)라고 합니다. 이 예제의 결과가 왜 저렇게 나오는지는 4장에서 언급한 2의 보수를 다루면서 함께 알아보도록 하겠습니다.

토큰화(tokenization)

지금까지의 코드에 이상한 점이 사실 하나 더 있습니다. 제가 띄어쓰기를 과도하게 사용한다는 생각은 들지 않으셨나요? 예를 들어, 1+2라고 표현할 수 있는 것을 굳이 1 + 2 처럼 굳이 사이에 띄어쓰기를 넣어서 썼습니다. 사실 맨 처음 변수 선언에 대해 설명했을 때 int i = 5 처럼 = 사이를 띄어버린 바람에 이후 예제에서 띄어쓰기를 했다가 안 하면 여러분을 혼동시킬 수 있다는 생각이 들어 계속 띄어쓰기를 쓴 건데요, 사실 띄어쓰기를 하지 않아도 괜찮습니다. 그래서 다음과 같이 코드를 써도 상관이 없습니다.

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

이렇게 스페이스도 안 넣고, 줄바꿈을 하지 않아도 됩니다. #include가 들어가있는 줄은 예외입니다. 이건 나중에 컴파일에 대해 자세히 배울 때 이유를 설명하겠습니다. 왜 이렇게 코드를 써도 되는지에 대해 설명하기 위해선 토큰화라는 과정에 대해 설명할 필요가 있습니다. 먼저, 한국어의 문장을 해석하는 방법에 대해서부터 생각해보겠습니다.

나는 오늘 음식점에 가서 맛있는 고기를 먹었다.

이 문장을 해석하기 위해선 가장 먼저 단어 단위로 분리해야합니다.

나/는/오늘/음식점/에/가서/맛있는/고기/를/먹었다

이 다음에 단어를 형태소나 접사로 분리하는 과정이 있겠습니다.

C도 마찬가지입니다. 컴파일러가 C 코드를 분석할 땐 먼저 코드를 토큰(token)이라는 단위로 나누는 작업을 먼저 합니다. 이 과정을 토큰화(tokenization)이라고도 하고 lexical analysis라고도 합니다. 그래서 위의 C 코드를 토큰화 하면

int, main, (, ), {, int, i, =, 5, ;, printf, (, "%d", ,, i, ), ;, }

이렇게 분리될 수 있습니다. 여기서 알 수 있는 것은 띄어쓰기나 줄바꿈은 이 토큰들을 구분하기 위한 용도로만 쓰인다는 것입니다. 즉 몇 칸을 띄든 전혀 문제가 없다는 것입니다. 이 토큰들을 어떻게 정렬하느냐에 따라 그 사람의 코딩 스타일이 달라집니다. 전 지금까지 중괄호{int main()의 아래에 붙였지만, 옆에 붙이는 것이 더 일반적입니다.

int main() {
    int i = 0;
}

또 전 들여쓰기를 4칸으로 썼지만, 구글에선 2칸으로 씁니다.

#include <stdio.h>
int main() {
  printf("Hello, world!");
}

중괄호를 아래에 붙이는 것이나, 들여쓰기를 4칸으로 하는 것 모두 마이크로소프트에서 만든 C#이라는 언어의 스타일입니다. 제가 C# 스타일을 따라가는 경향이 있지만, 여러분은 여러분에게 맞는 스타일을 찾으시면 됩니다. 여러 코딩 스타일들을 보고 싶으시다면, 이 위키피디아 문서를 참고해주세요. 어떤 스타일이든 다른 사람들이 읽기 편하게 정렬하는 것이 가장 좋고, 여러분이 어떤 회사나 기관에서 일하게 된다면, 그 곳의 코딩 스타일을 따라야 합니다.

세미콜론의 역할

세미콜론은 하나의 문장(statement)을 완성하는 역할을 합니다. 예를 들어, a = 11a에 대입한 후 결과가 1이 되는 식(expression)입니다. 이 식은 다른 곳에 넣어 printf("%d", a = 1)같이 새로운 식을 구성할 수 있습니다. 하지만 int main() { ... } 안에는 항상 식이 아니라 문장이 들어가야 합니다. 세미콜론은 하나의 식을 문장으로 바꿔주는 역할을 합니다. 즉, <식>;은 하나의 문장이 됩니다. 예를 들어, "Hello, "를 출력한 후 "world!"를 출력하는 프로그램을 만들고 싶습니다. 출력을 할 것이기 때문에 printf("Hello, ")printf("world!")를 사용합니다. 이 둘을 세미콜론 없이 붙이면 다음과 같이 됩니다.

#include <stdio.h>

int main()
{
    printf("Hello, ")  printf("world!")
}

그런데, 식 printf("Hello, ")와 식 printf("world!")를 붙여줄 연산자가 없고, int main() { ... } 안에선 식을 쓸 수 없습니다. 그래서 이 코드는 유효한 코드가 아닙니다. 이 둘을 문장으로 만들어주기 위해 세미콜론을 붙여줍니다.

#include <stdio.h>

int main()
{
    printf("Hello, ");  printf("world!");
}
Hello, world!

세미콜론 혼자서 문장을 하나 완성할 수 있습니다. 예를 들어, int i = 5;;는 문장이 int i = 5;, ;로 두 개 입니다. 이런 문장을 빈 문장(null statement)이라고 합니다.

복합 대입 연산자(compound assignment operator)

C 코드를 작성하다 보면 a = a + 2나, b = b / 3 처럼 어떤 한 변수로 계산한 결과를 다시 그 변수에 집어넣게 될 일이 생깁니다. 그런데, 변수의 이름이 my_special_variable 처럼 길게 되면 저런 식을 쓰기 힘들어집니다. 그래서 C언어에선 이런 경우, 코드를 더 짧게 쓰는 방법이 있는데요, 이 때 복합 대입 연산자가 사용됩니다. 사용방법은 어렵지 않습니다. 이항 연산자 @에 대해, a = a @ ba @= b로 바뀌는 겁니다. 예시는 다음과 같습니다.

#include <stdio.h>

int main()
{
    int a = 5;
    printf("%d ", a);
    a += 2;
    printf("%d ", a);
    a -= 1;
    printf("%d ", a);
    a *= 5;
    printf("%d ", a);
    a /= 6;
    printf("%d ", a);
    a %= 2;
    printf("%d", a);
}
5 7 6 30 5 1

원래 a는 가지고 있던 값이 5였는데, 거기에 2가 더해져 7이 되고, 1이 빠져 6이 되었습니다. 그리고 5를 곱해져 30이 되고, 6으로 나뉘어져 5가 되었네요. 마지막으로 2로 나눈 나머지인 1a에 들어가게 됩니다. 복합 대입 연산자가 없었다면 위의 예제는 아래처럼 쓰게 될 것입니다. (printf는 제외했습니다.)

int main()
{
    int a = 5;
    a = a + 2;
    a = a - 1;
    a = a * 5;
    a = a / 6;
    a = a % 2;
}

위와 아래를 비교했을 때, 위가 훨씬 간단한 것을 알 수 있습니다.

증가 및 감소 연산자(increment and decrement operator)

C에서 어떤 변수에 무언가 더하거나 뺄 일이 있다면, 1을 더하거나 뺄 일이 가장 많습니다. 그래서 C에선 1을 더하고 빼는 연산자도 따로 있습니다. 예시는 다음과 같습니다.

#include <stdio.h>

int main()
{
    int a = 5;
    printf("%d ", a);
    ++a;
    printf("%d ", a);
    a++;
    printf("%d ", a);
    --a;
    printf("%d ", a);
    a--;
    printf("%d", a);
}
5 6 7 6 5

++을 사용하면 1 증가하고, --을 사용하면 1 감소하는 것을 알 수 있습니다. 이 때 ++증가 연산자(increment operator), --감소 연산자(decrement operator)라고 부르고, 둘을 합쳐서 한국어로 증감연산자라고 부릅니다. 특히 변수의 앞에 온 증감연산자를 전위 증감 연산자(prefix increment/decrement operator)라고 하고, 뒤에 온 것을 후위 증감 연산자(postfix increment/decrement operator)라고 부릅니다. 전위 증감 연산자와 후위 증감 연산자의 차이는 1이 증가하거나 감소하는 시점에 있습니다.

#include <stdio.h>

int main()
{
    int a = 5;
    printf("%d ", a);
    printf("%d ", ++a);
    printf("%d ", a++);
    printf("%d", a);
}
5 6 6 7

세번째 printf 안에서도 a에 증가 연산자를 취해줬음에도 불구하고, 증가되지 않은 값이 나온 것을 알 수 있습니다. 이것은 후위 증감 연산자는 연산자를 취한 변수의 값이 그 문장이 끝난 직후에 증가하기 때문입니다. 다시 말해, 다음과 같은 차이가 있습니다.

<a에 전위 증가 연산자를 사용한 식>;
->
a += 1;
<a를 사용한 식>;

<a에 후위 증가 연산자를 사용한 식>;
->
<a를 사용한 식>;
a += 1;

증감연산자는 사용하기 간편하다는 장점이 있지만, 한 문장 내에서 여러 번 사용하게 되면 읽기 힘들어진다는 단점이 있습니다. 그래서 증감연산자는 적절한 횟수로 사용하는 것이 좋습니다. 또, 한 문장 내에서 같은 변수에 대해 증감연산자를 여러번 사용하는 것은 정의되지 않은 행동입니다.

다음: 포인터와 배열