연산자는 값을 움직이는 작은 엔진이다
프로그램은 결국 값을 다루는 일입니다. 값을 더하고, 비교하고, 바꾸고, 조건에 맞는지 판단합니다. 이때 값과 값 사이에서 실제 일을 하는 기호가 바로 연산자입니다.
int price = 1000;
int count = 3;
int total = price * count;위 코드에서 *는 곱셈 연산자입니다. 사람 눈에는 단순히 1000 * 3처럼 보이지만, 컴퓨터 안에서는 조금 더 바쁜 일이 일어납니다. 메모리에 저장된 price 값을 꺼내고, count 값을 꺼낸 다음, CPU가 두 값을 곱합니다. 그리고 그 결과를 다시 total이라는 변수의 메모리 공간에 저장합니다.
연산자는 작지만 강력합니다. + 하나로 계산이 시작되고, == 하나로 판단이 만들어지며, & 하나로 비트 단위의 세계가 열립니다. C언어를 배우는 동안 연산자를 제대로 이해하면 이후에 조건문, 반복문, 배열, 포인터, 메모리까지 훨씬 자연스럽게 연결됩니다.

산술 연산자: 숫자를 계산하는 기본 도구
가장 먼저 만나는 연산자는 산술 연산자입니다. 이름 그대로 수학 계산을 담당합니다.
| 연산자 | 의미 | 예 |
|---|---|---|
+ | 더하기 | a + b |
- | 빼기 | a - b |
* | 곱하기 | a * b |
/ | 나누기 | a / b |
% | 나머지 | a % b |
간단한 예제를 보겠습니다.
#include <stdio.h>
int main(void)
{
int a = 10;
int b = 3;
printf("a + b = %dn", a + b);
printf("a - b = %dn", a - b);
printf("a * b = %dn", a * b);
printf("a / b = %dn", a / b);
printf("a %% b = %dn", a % b);
return 0;
}실행 결과는 다음과 같습니다.
a + b = 13
a - b = 7
a * b = 30
a / b = 3
a % b = 1여기서 눈여겨볼 부분은 a / b입니다. 10 / 3이면 수학적으로는 3.333...입니다. 그런데 C에서 두 값이 모두 정수이면 결과도 정수로 계산됩니다. 따라서 소수 부분이 버려지고 3만 남습니다. 이것을 정수 나눗셈이라고 부릅니다.
%는 나머지를 구합니다. 10을 3으로 나누면 몫은 3, 나머지는 1입니다. 그래서 10 % 3은 1입니다. 나머지 연산자는 짝수와 홀수를 구분할 때 자주 사용합니다.
int number = 17;
printf("%dn", number % 2); // 1어떤 정수를 2로 나누었을 때 나머지가 0이면 짝수이고, 1이면 홀수입니다. 아직 조건문을 본격적으로 배우지는 않았지만, 앞으로 number % 2 == 0 같은 식을 자주 만나게 됩니다.
정수 나눗셈과 형 변환
C의 나눗셈은 자료형의 영향을 강하게 받습니다. 다음 코드를 보겠습니다.
#include <stdio.h>
int main(void)
{
int total = 5;
int count = 2;
double average1 = total / count;
double average2 = (double)total / count;
printf("average1 = %fn", average1);
printf("average2 = %fn", average2);
return 0;
}실행 결과는 다음과 비슷합니다.
average1 = 2.000000
average2 = 2.500000average1은 double 변수인데 왜 2.5가 아니라 2.0일까요? 이유는 계산 순서에 있습니다.
double average1 = total / count;오른쪽의 total / count가 먼저 계산됩니다. total도 int, count도 int이므로 정수 나눗셈이 일어납니다. 그래서 5 / 2의 결과는 2입니다. 그 후 2가 double 변수에 저장되면서 2.000000이 됩니다. 이미 소수 부분은 사라진 뒤입니다.
반면 다음 코드는 다릅니다.
double average2 = (double)total / count;(double)total은 total 값을 잠시 double로 바꾸어 계산하라는 뜻입니다. 한쪽이 double이 되면 다른 쪽도 실수 계산에 맞춰 변환되고, 결과도 실수로 계산됩니다. 그래서 5.0 / 2가 되어 2.5가 나옵니다.
주의할 점도 있습니다.
double wrong = (double)(total / count);이 코드는 얼핏 맞아 보이지만 결과는 여전히 2.0입니다. 괄호 안의 total / count가 먼저 정수 나눗셈으로 계산되어 2가 되고, 그 2를 나중에 double로 바꾸기 때문입니다. 형 변환은 소수점이 사라지기 전에 해야 합니다.
또 하나 중요한 규칙이 있습니다. 어떤 자료형이든 0으로 나누면 안 됩니다. 특히 정수에서 a / 0이나 a % 0은 C에서 결과가 정해져 있지 않은 위험한 동작입니다. 프로그램이 바로 종료될 수도 있고, 이상한 결과가 나올 수도 있습니다. 나눗셈을 할 때는 나누는 값이 0인지 확인하는 습관이 필요합니다.
대입 연산자: 계산 결과를 메모리에 저장하기
=는 수학에서 "같다"라는 뜻으로 쓰입니다. 하지만 C에서는 다릅니다. C에서 =는 오른쪽 값을 왼쪽 변수에 저장하라는 뜻입니다.
int level;
level = 1;이 코드는 level이라는 메모리 공간에 1을 넣습니다. 왼쪽에는 값을 담을 수 있는 공간이 와야 합니다. 오른쪽에는 계산 가능한 식이 올 수 있습니다.
int a = 10;
int b = 20;
int result;
result = a + b;마지막 줄은 다음 순서로 이해하면 좋습니다.
메모리에서
a의 값10을 읽는다.메모리에서
b의 값20을 읽는다.CPU가 두 값을 더해
30을 만든다.결과
30을result의 메모리 공간에 저장한다.
즉, 대입 연산자는 단순한 기호가 아니라 "계산된 값을 변수의 공간에 덮어쓰기" 하는 동작입니다.
다음 코드도 처음에는 조금 이상해 보일 수 있습니다.
int score = 80;
score = score + 10;
printf("%dn", score); // 90수학식으로 보면 score = score + 10은 말이 안 됩니다. 어떤 값이 자기 자신보다 10 큰 값과 같을 수는 없기 때문입니다. 하지만 C에서는 의미가 다릅니다. 오른쪽의 기존 score 값을 먼저 읽어 10을 더하고, 그 결과를 다시 score에 저장합니다. 그래서 score의 값은 80에서 90으로 바뀝니다.
복합 대입 연산자: 기존 값을 갱신하는 짧은 표현
변수의 기존 값을 이용해 다시 그 변수에 저장하는 일은 매우 자주 일어납니다. 그래서 C는 더 짧은 표현을 제공합니다.
| 표현 | 같은 의미 |
|---|---|
x += 3 | x = x + 3 |
x -= 3 | x = x - 3 |
x *= 3 | x = x * 3 |
x /= 3 | x = x / 3 |
x %= 3 | x = x % 3 |
예제를 보겠습니다.
#include <stdio.h>
int main(void)
{
int hp = 100;
hp -= 20; // 공격을 받아 체력 20 감소
printf("hp = %dn", hp);
hp += 10; // 회복 아이템으로 체력 10 증가
printf("hp = %dn", hp);
hp *= 2; // 특별 효과로 체력 2배
printf("hp = %dn", hp);
return 0;
}실행 결과는 다음과 같습니다.
hp = 80
hp = 90
hp = 180복합 대입 연산자는 코드를 짧게 만드는 장점이 있습니다. 하지만 더 중요한 점은 의도가 분명해진다는 것입니다. hp -= 20을 보면 "기존 체력에서 20을 뺀다"는 뜻이 바로 보입니다. 메모리 관점으로 보면 hp 공간에 들어 있던 값을 읽어서 새 값으로 계산한 뒤, 같은 공간에 다시 저장하는 과정입니다.
비트 연산에서도 복합 대입 연산자를 많이 사용합니다.
flags |= 1;
flags &= 3;
flags ^= 2;지금은 낯설어도 괜찮습니다. 뒤에서 플래그와 마스크를 다룰 때 이 표현들이 다시 등장합니다.
비교 연산자: 두 값을 비교해 0 또는 1 만들기
비교 연산자는 두 값을 비교합니다. 결과는 참이면 1, 거짓이면 0입니다. C에서는 0을 거짓으로, 0이 아닌 값을 참으로 다룹니다.
| 연산자 | 의미 | 예 |
|---|---|---|
== | 같다 | a == b |
!= | 같지 않다 | a != b |
> | 크다 | a > b |
< | 작다 | a < b |
>= | 크거나 같다 | a >= b |
<= | 작거나 같다 | a <= b |
예제를 보겠습니다.
#include <stdio.h>
int main(void)
{
int age = 20;
printf("age == 20: %dn", age == 20);
printf("age != 20: %dn", age != 20);
printf("age >= 19: %dn", age >= 19);
printf("age < 10: %dn", age < 10);
return 0;
}실행 결과는 다음과 같습니다.
age == 20: 1
age != 20: 0
age >= 19: 1
age < 10: 0여기서 가장 조심해야 할 연산자는 ==입니다. C에서 ==는 "같은가?"를 묻는 비교 연산자이고, =는 값을 저장하는 대입 연산자입니다.
int x = 10;
printf("%dn", x == 10); // 비교: x가 10인가?
printf("%dn", x = 20); // 대입: x에 20을 저장한 뒤 그 값을 사용두 번째 줄은 x에 20을 저장합니다. 그리고 대입식의 결과값인 20이 출력됩니다. 나중에 조건문에서 if (x = 20)처럼 쓰면 아주 찾기 어려운 버그가 됩니다. "같은지 비교할 때는 등호 두 개"라는 사실을 손에 익혀야 합니다.
실수형 값을 비교할 때도 주의가 필요합니다. double 같은 실수는 내부적으로 근삿값으로 저장될 수 있습니다. 그래서 복잡한 실수 계산의 결과를 ==로 정확히 비교하면 예상과 다를 수 있습니다. 지금 단계에서는 "정수 비교는 ==를 자주 쓰지만, 실수 비교는 조심해야 한다" 정도로 기억하면 충분합니다.
논리 연산자: 조건을 조립하는 접착제
비교 연산자가 하나의 판단을 만든다면, 논리 연산자는 여러 판단을 연결합니다.
| 연산자 | 의미 | 예 |
|---|---|---|
&& | 그리고 | a > 0 && b > 0 |
| ` | ` | |
! | 아니다 | !finished |
예를 들어 점수가 유효하려면 0 이상이고 100 이하여야 합니다. 이 조건은 다음처럼 표현할 수 있습니다.
int score = 85;
int valid = score >= 0 && score <= 100;
printf("valid = %dn", valid); // 1score >= 0이 참이고, score <= 100도 참이므로 전체 결과는 참입니다. &&는 양쪽이 모두 참일 때만 참입니다.
이번에는 무료 입장 조건을 생각해 보겠습니다. 나이가 7세 미만이거나 65세 이상이면 무료라고 해 봅시다.
int age = 70;
int free_ticket = age < 7 || age >= 65;
printf("free_ticket = %dn", free_ticket); // 1||는 둘 중 하나라도 참이면 참입니다. age >= 65가 참이므로 전체 결과는 참입니다.
!는 참과 거짓을 뒤집습니다.
int logged_in = 0;
int need_login = !logged_in;
printf("need_login = %dn", need_login); // 1logged_in이 0, 즉 거짓이므로 !logged_in은 참이 됩니다.
논리 연산자는 읽기 쉬운 조건식을 만드는 데 매우 중요합니다. 다음 두 표현을 비교해 보겠습니다.
int ok1 = score >= 0 && score <= 100;
int ok2 = !(score < 0 || score > 100);둘은 같은 의미입니다. 하지만 보통 첫 번째가 더 읽기 쉽습니다. "점수가 0 이상이고 100 이하인가?"라고 자연스럽게 읽히기 때문입니다. 코드는 컴퓨터만 읽는 글이 아닙니다. 미래의 나와 동료도 읽습니다. 조건식은 가능한 한 사람의 문장처럼 읽히게 만드는 것이 좋습니다.
짧은 평가: 필요 없으면 계산하지 않는다
&&와 ||에는 중요한 특징이 있습니다. 왼쪽만 보고 결과가 결정되면 오른쪽은 계산하지 않습니다. 이것을 짧은 평가 또는 단락 평가라고 합니다.
int count = 0;
int total = 100;
int result = count != 0 && total / count > 10;
printf("result = %dn", result);이 코드에서 count는 0입니다. 만약 total / count가 실행되면 0으로 나누게 되어 위험합니다. 하지만 &&는 왼쪽이 거짓이면 전체가 반드시 거짓이라는 사실을 압니다. 그래서 오른쪽의 total / count > 10은 계산하지 않습니다.
이 성질은 안전한 조건식을 만들 때 자주 사용됩니다.
int safe = count != 0 && total / count > 10;이 식은 "나눌 수 있는 상황인지 먼저 확인하고, 그다음 나눗셈을 하라"는 뜻입니다. 순서가 바뀌면 안 됩니다.
int unsafe = total / count > 10 && count != 0; // 위험위 코드는 왼쪽의 나눗셈이 먼저 실행될 수 있으므로 count가 0이면 문제가 생깁니다. 논리 연산자는 단순히 참과 거짓을 연결하는 기호가 아니라, 안전한 실행 순서를 만드는 도구이기도 합니다.
||도 비슷합니다. 왼쪽이 참이면 전체가 이미 참이므로 오른쪽을 계산하지 않습니다.
int cached = 1;
int loaded = cached || load_from_file;이 예시는 개념을 보여 주기 위한 단순한 식입니다. 핵심은 ||에서 왼쪽이 참이면 오른쪽은 볼 필요가 없다는 점입니다.
증감 연산자: 1 증가와 1 감소를 짧게 쓰기
프로그래밍에서는 값을 1씩 증가시키거나 감소시키는 일이 아주 많습니다. 반복문에서 횟수를 셀 때 특히 자주 등장합니다. C는 이를 위해 증감 연산자를 제공합니다.
| 연산자 | 의미 |
|---|---|
++ | 값을 1 증가 |
-- | 값을 1 감소 |
다음 세 코드는 모두 비슷한 일을 합니다.
count = count + 1;
count += 1;
count++;count++는 count의 값을 1 증가시킵니다. count--는 1 감소시킵니다.
#include <stdio.h>
int main(void)
{
int count = 10;
count++;
printf("%dn", count); // 11
count--;
printf("%dn", count); // 10
return 0;
}여기까지는 쉽습니다. 그런데 ++와 --는 변수 앞에 붙을 수도 있고 뒤에 붙을 수도 있습니다.
++count;
count++;둘 다 최종적으로 count를 1 증가시킵니다. 차이는 그 표현식의 값이 무엇이냐에 있습니다.
전위 증감과 후위 증감
변수 앞에 붙는 ++count를 전위 증감이라고 부릅니다. 먼저 증가시키고, 증가된 값을 표현식의 결과로 사용합니다.
#include <stdio.h>
int main(void)
{
int x = 3;
int y = ++x;
printf("x = %dn", x);
printf("y = %dn", y);
return 0;
}실행 결과는 다음과 같습니다.
x = 4
y = 4++x는 먼저 x를 4로 만든 뒤, 그 값 4를 y에 저장합니다.
이번에는 후위 증감을 보겠습니다.
#include <stdio.h>
int main(void)
{
int x = 3;
int y = x++;
printf("x = %dn", x);
printf("y = %dn", y);
return 0;
}실행 결과는 다음과 같습니다.
x = 4
y = 3x++는 현재 값 3을 표현식의 결과로 먼저 사용하고, 그다음 x를 4로 증가시킵니다. 그래서 y에는 3이 저장되고, x는 나중에 4가 됩니다.
처음 배울 때는 이 차이가 꽤 헷갈립니다. 그래서 좋은 습관이 하나 있습니다. 증감 연산자는 가능하면 혼자 한 줄에 쓰는 것입니다.
x++;
y = x;이렇게 쓰면 읽는 사람도 헷갈리지 않습니다. 다음과 같은 코드는 피하는 편이 좋습니다.
int result = x++ + ++x;이런 식은 읽기도 어렵고, C에서는 같은 변수를 한 식 안에서 여러 번 바꾸면 결과가 보장되지 않는 경우가 생깁니다. 컴파일러나 상황에 따라 예상과 다르게 동작할 수 있습니다. C를 잘하는 사람일수록 이런 코드를 멋있게 쓰지 않습니다. 오히려 명확하게 나누어 씁니다.
x++;
++x;
int result = x + x;코드는 수수께끼가 아니라 설명이어야 합니다.
비트 연산자: 값을 0과 1의 스위치로 다루기
지금까지의 연산자는 숫자 자체를 계산하거나 비교했습니다. 이제 조금 더 컴퓨터 내부에 가까운 연산자를 보겠습니다. 바로 비트 연산자입니다.
컴퓨터의 값은 결국 0과 1로 저장됩니다. 예를 들어 십진수 12는 이진수로 1100입니다. 십진수 10은 이진수로 1010입니다. 비트 연산자는 이런 0과 1을 자리별로 계산합니다.
| 연산자 | 이름 | 의미 |
|---|---|---|
& | 비트 AND | 둘 다 1이면 1 |
| ` | ` | 비트 OR |
^ | 비트 XOR | 서로 다르면 1 |
~ | 비트 NOT | 0은 1로, 1은 0으로 뒤집기 |
<< | 왼쪽 시프트 | 비트를 왼쪽으로 이동 |
>> | 오른쪽 시프트 | 비트를 오른쪽으로 이동 |
12와 10을 4비트로 놓고 보면 다음과 같습니다.
1100 (12)
& 1010 (10)
------
1000 (8)둘 다 1인 자리만 1이 되므로 결과는 1000, 즉 8입니다.
1100 (12)
| 1010 (10)
------
1110 (14)하나라도 1이면 1이 되므로 결과는 1110, 즉 14입니다.
1100 (12)
^ 1010 (10)
------
0110 (6)서로 다른 자리만 1이 되므로 결과는 0110, 즉 6입니다.
코드로 확인해 보겠습니다.
#include <stdio.h>
int main(void)
{
unsigned int a = 12;
unsigned int b = 10;
printf("a & b = %un", a & b);
printf("a | b = %un", a | b);
printf("a ^ b = %un", a ^ b);
return 0;
}실행 결과는 다음과 같습니다.
a & b = 8
a | b = 14
a ^ b = 6비트 연산을 연습할 때는 unsigned int처럼 부호 없는 정수형을 사용하는 편이 좋습니다. 부호 있는 정수는 맨 앞 비트를 음수 표현에 사용하는 등 생각할 것이 많기 때문입니다. 처음에는 "비트 연산은 부호 없는 정수로 연습한다"고 기억해도 좋습니다.
비트 NOT과 시프트
~는 모든 비트를 뒤집습니다. 그런데 여기서 "모든"이라는 말이 중요합니다.
unsigned int a = 12;
unsigned int result = ~a;12를 4비트로 보면 1100이고, 뒤집으면 0011이므로 3이 나올 것 같기도 합니다. 하지만 실제 unsigned int는 보통 32비트입니다. 그러면 00000000 00000000 00000000 00001100 전체가 뒤집힙니다. 그래서 아주 큰 값이 나올 수 있습니다.
아래쪽 4비트만 보고 싶다면 마스크를 함께 사용합니다.
unsigned int a = 12;
unsigned int lower4 = (~a) & 15;
printf("%un", lower4); // 315는 이진수로 1111입니다. & 15를 하면 아래쪽 4비트만 남기고 나머지는 버립니다. 이런 값을 마스크라고 부릅니다. 마스크는 특정 비트만 골라내는 가림막 같은 역할을 합니다.
시프트 연산자는 비트를 왼쪽이나 오른쪽으로 이동합니다.
#include <stdio.h>
int main(void)
{
unsigned int x = 3; // 이진수로 0011
printf("%un", x << 1); // 6
printf("%un", x << 2); // 12
printf("%un", x >> 1); // 1
return 0;
}3은 이진수로 0011입니다. 왼쪽으로 한 칸 밀면 0110이 되어 6입니다. 왼쪽으로 두 칸 밀면 1100이 되어 12입니다. 반대로 오른쪽으로 한 칸 밀면 0001이 되어 1입니다.
부호 없는 정수에서 왼쪽 시프트는 대체로 2를 곱하는 느낌, 오른쪽 시프트는 2로 나누는 느낌으로 이해할 수 있습니다. 다만 시프트는 진짜 목적이 "비트 위치를 이동하는 것"입니다. 단순 곱셈과 완전히 같은 도구라고 생각하면 나중에 실수할 수 있습니다. 특히 부호 있는 정수나 너무 큰 시프트 값은 조심해야 합니다.
1u << 0 // 1
1u << 1 // 2
1u << 2 // 4
1u << 3 // 8이 패턴은 플래그를 만들 때 매우 자주 쓰입니다.
플래그와 마스크: 비트로 여러 상태 저장하기
게임 캐릭터에게 권한이나 상태가 있다고 생각해 봅시다. 읽기 가능, 쓰기 가능, 실행 가능이라는 세 가지 상태를 저장하고 싶습니다. 변수를 세 개 만들 수도 있습니다.
int can_read = 1;
int can_write = 0;
int can_execute = 0;하지만 비트를 사용하면 하나의 정수 안에 여러 상태를 넣을 수 있습니다.
const unsigned int CAN_READ = 1u << 0; // 0001
const unsigned int CAN_WRITE = 1u << 1; // 0010
const unsigned int CAN_EXECUTE = 1u << 2; // 0100
unsigned int permission = 0;CAN_READ는 첫 번째 비트, CAN_WRITE는 두 번째 비트, CAN_EXECUTE는 세 번째 비트를 의미합니다. 각 비트가 하나의 스위치입니다.
권한을 켤 때는 OR를 사용합니다.
permission |= CAN_READ;
permission |= CAN_WRITE;|=는 해당 비트를 1로 켭니다. 이미 켜져 있는 다른 비트는 그대로 둡니다.
권한이 있는지 확인할 때는 AND를 사용합니다.
int readable = (permission & CAN_READ) != 0;
int writable = (permission & CAN_WRITE) != 0;
printf("readable = %dn", readable);
printf("writable = %dn", writable);permission & CAN_READ는 CAN_READ에 해당하는 비트만 남깁니다. 그 결과가 0이 아니면 해당 비트가 켜져 있다는 뜻입니다.
권한을 끌 때는 AND와 NOT을 함께 사용합니다.
permission &= ~CAN_WRITE;~CAN_WRITE는 CAN_WRITE 비트만 0이고 나머지는 1인 마스크를 만듭니다. 여기에 &=를 적용하면 CAN_WRITE 비트만 꺼지고 다른 비트는 유지됩니다.
권한을 반전할 때는 XOR를 사용합니다.
permission ^= CAN_EXECUTE;^=는 해당 비트가 꺼져 있으면 켜고, 켜져 있으면 끕니다. 스위치를 딸깍 누르는 것과 비슷합니다.
전체 예제를 하나로 모아 보겠습니다.
#include <stdio.h>
int main(void)
{
const unsigned int CAN_READ = 1u << 0;
const unsigned int CAN_WRITE = 1u << 1;
const unsigned int CAN_EXECUTE = 1u << 2;
unsigned int permission = 0;
permission |= CAN_READ;
permission |= CAN_WRITE;
printf("read: %dn", (permission & CAN_READ) != 0);
printf("write: %dn", (permission & CAN_WRITE) != 0);
printf("execute: %dn", (permission & CAN_EXECUTE) != 0);
permission &= ~CAN_WRITE;
permission ^= CAN_EXECUTE;
printf("after changen");
printf("read: %dn", (permission & CAN_READ) != 0);
printf("write: %dn", (permission & CAN_WRITE) != 0);
printf("execute: %dn", (permission & CAN_EXECUTE) != 0);
return 0;
}비트 플래그는 운영체제, 파일 권한, 네트워크 옵션, 그래픽스 설정, 임베디드 장비 제어 같은 곳에서 자주 등장합니다. 아직은 낯설 수 있지만, C가 컴퓨터 내부와 가까운 언어라는 사실을 가장 잘 보여 주는 부분이기도 합니다.
연산자 우선순위: 컴퓨터는 어떤 연산을 먼저 할까?
수학에서 곱셈을 덧셈보다 먼저 계산하듯, C에도 연산자 우선순위가 있습니다.
int result = 2 + 3 * 4;이 식의 결과는 20이 아니라 14입니다. *가 +보다 먼저 계산되기 때문입니다.
int result = (2 + 3) * 4; // 20괄호를 쓰면 원하는 순서를 분명하게 만들 수 있습니다.
자주 만나는 연산자를 우선순위가 높은 것부터 간단히 정리하면 다음과 같습니다.
| 우선순위 | 연산자 | 예 |
|---|---|---|
| 높음 | () | (a + b) |
++, --, !, ~, 형 변환 | ++x, !ok, (double)n | |
*, /, % | a * b | |
+, - | a + b | |
<<, >> | x << 1 | |
<, <=, >, >= | score >= 60 | |
==, != | x == 0 | |
& | flags & mask | |
^ | a ^ b | |
| ` | ` | |
&& | a > 0 && b > 0 | |
| ` | ||
| 낮음 | =, +=, -=, *=, /= | x += 1 |
이 표를 전부 외울 필요는 없습니다. 다만 몇 가지는 꼭 기억해야 합니다. 산술 연산에서 *, /, %가 +, -보다 먼저입니다. 비교 연산은 논리 연산보다 먼저입니다. 대입 연산자는 매우 낮은 우선순위를 가집니다.
그리고 비트 연산을 쓸 때는 특히 괄호를 적극적으로 사용하는 것이 좋습니다.
int has_read = permission & CAN_READ != 0; // 헷갈리고 위험한 표현
int has_read2 = (permission & CAN_READ) != 0; // 의도가 분명한 표현첫 번째 코드는 사람이 기대한 순서와 다르게 해석될 수 있습니다. 두 번째처럼 괄호를 넣으면 "먼저 비트를 골라낸 다음, 0이 아닌지 비교한다"는 뜻이 분명해집니다.
우선순위 표를 외워서 괄호를 줄이는 것보다, 괄호를 써서 의도를 드러내는 편이 훨씬 좋은 습관입니다.
실수하기 쉬운 표현식
연산자는 짧은 기호이기 때문에 실수도 짧고 강력하게 터집니다. 초보자가 자주 만나는 실수를 정리해 보겠습니다.
첫 번째는 정수 나눗셈입니다.
int sum = 5;
int count = 2;
double avg = sum / count; // 2.000000평균을 구하려는 코드라면 다음처럼 써야 합니다.
double avg = (double)sum / count; // 2.500000두 번째는 =와 ==를 헷갈리는 실수입니다.
x = 10; // 저장
x == 10; // 비교한 글자 차이지만 의미는 완전히 다릅니다.
세 번째는 논리 연산자와 비트 연산자를 헷갈리는 실수입니다.
int ok = a > 0 && b > 0; // 논리 AND
int masked = flags & mask; // 비트 AND&&는 참과 거짓을 연결합니다. &는 비트 하나하나를 계산합니다. 비슷하게 ||는 논리 OR이고, |는 비트 OR입니다.
네 번째는 증감 연산자를 복잡한 식 안에 넣는 실수입니다.
int y = x++ + x;이런 코드는 읽는 사람을 멈춰 세웁니다. "현재 x가 쓰였나? 증가한 뒤의 x가 쓰였나?"를 생각하게 만듭니다. 더 심한 경우 C에서 결과가 보장되지 않을 수 있습니다. 증감은 되도록 한 줄에 따로 쓰는 것이 안전합니다.
다섯 번째는 나눗셈에서 0을 확인하지 않는 실수입니다.
int result = total / count; // count가 0이면 위험나누기 전에 count가 0이 아닌지 확인해야 합니다. 나중에 조건문을 배우면 이런 검사를 자연스럽게 코드로 작성할 수 있습니다.
연산자를 읽는 감각
C 코드를 읽을 때는 연산자를 단순히 기호로 보지 말고, 값의 흐름으로 읽는 것이 좋습니다.
score += bonus;이 코드는 "score에 bonus를 더한다"입니다. 메모리 관점에서는 score의 기존 값을 읽고, bonus를 더한 뒤, 다시 score 공간에 저장합니다.
score >= 0 && score <= 100이 코드는 "점수가 0 이상이고 100 이하인가?"입니다. 비교 연산자가 두 개의 작은 판단을 만들고, 논리 연산자 &&가 그 판단을 하나로 묶습니다.
permission |= CAN_READ;이 코드는 "읽기 권한 비트를 켠다"입니다. 숫자 하나를 통째로 바꾸는 것처럼 보이지만, 실제 의도는 특정 비트 스위치 하나를 켜는 것입니다.
연산자는 프로그램의 판단과 계산을 만드는 가장 작은 엔진입니다. 이 작은 엔진들이 모여 조건문을 만들고, 반복문을 움직이며, 함수의 결과를 계산하고, 나중에는 포인터와 메모리 주소까지 다루게 됩니다. 이제 값은 단순히 저장되는 것에서 그치지 않습니다. 연산자를 만나는 순간, 값은 움직이기 시작합니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
