[C언어 50강] 06강. 연산자 1: 산술/대입/증감 연산자

[C언어 50강] 06강. 연산자 1: 산술/대입/증감 연산자

C언어에서 연산자는 “값을 어떻게 읽고, 바꾸고, 저장할지”를 결정하는 핵심 문법입니다. 특히 산술/대입/증감 연산자는 거의 모든 코드의 바닥을 구성합니다. 오늘은 문법을 외우는 대신, 연산이 메모리의 값을 어떻게 바꾸는지까지 연결해서 이해해보겠습니다.


핵심 개념

  • 산술 연산자는 값을 계산하고, 대입 연산자는 계산 결과를 저장한다.
  • 증감 연산자(++, --)는 “값 변경”과 “식의 결과값”이 동시에 얽혀 있어 문맥을 반드시 구분해야 한다.
  • 정수 연산에서는 나눗셈/나머지의 규칙과 타입 승격(정수 승격)을 이해해야 버그를 줄일 수 있다.

개념 먼저 이해하기

많은 초보자가 연산자를 “수학 기호”로만 이해하다가 C 코드에서 혼란을 겪습니다. C에서 +, -, *, /는 분명 계산 기호이지만, 결국 CPU가 레지스터와 메모리의 비트를 읽고 쓰는 절차로 바뀝니다. 즉 C의 연산은 수학식이 아니라 상태(state) 변화입니다. 예를 들어 a = a + 3;는 “a에 3을 더하라”가 아니라, 메모리에서 a 값을 읽고, 3과 계산한 뒤, 결과를 다시 a의 메모리 위치에 저장하는 3단계 동작입니다. 그래서 C에서는 계산 자체보다 “언제 읽고, 언제 쓰는가”가 중요합니다.

대입 연산자도 같은 관점에서 봐야 합니다. =는 수학의 등호가 아니라 오른쪽 값을 왼쪽 저장소에 복사 저장하는 연산자입니다. 그래서 x = y = 10; 같은 체인 대입이 가능하고, if (x = 0) 같은 실수가 치명적입니다. 이 코드는 비교가 아니라 실제로 x를 0으로 바꾸고, 그 결과값(0)을 조건식으로 사용하기 때문에 항상 거짓이 됩니다. 즉 대입은 언제나 “부작용(side effect)”을 만들 수 있습니다.

증감 연산자는 더 주의가 필요합니다. ++i(전위 증가)는 먼저 증가시키고 그 값을 식의 결과로 내놓고, i++(후위 증가)는 현재 값을 먼저 결과로 내놓고 나중에 증가시킵니다. 둘 다 최종적으로 i가 1 증가한다는 점은 같지만, 표현식 안에서의 결과값이 다릅니다. 그래서 printf("%d", i++);printf("%d", ++i);는 출력이 달라집니다. 실무에서는 이런 미묘한 차이를 이용한 “영리한 한 줄 코드”보다, 읽기 쉬운 두 줄 코드가 유지보수에 훨씬 안전합니다.

또 하나 자주 놓치는 지점이 정수 나눗셈입니다. C에서 7 / 2는 3.5가 아니라 3입니다. 피연산자가 둘 다 정수면 소수점 이하는 버려집니다(0 방향 절단). 반대로 실수 결과가 필요하면 피연산자 중 하나를 실수 타입으로 바꿔야 합니다. 예: (double)7 / 2는 3.5입니다. 이 규칙을 모르고 평균, 비율, 퍼센트 코드를 짜면 테스트 데이터에서는 그럴듯해 보이는데 실제 데이터에서 오차가 크게 납니다.

결론적으로 산술/대입/증감 연산자 학습의 핵심은 “기호 암기”가 아니라 다음 3가지입니다. 첫째, 계산과 저장을 분리해서 사고하기. 둘째, 표현식의 결과값과 변수의 최종 상태를 구분하기. 셋째, 정수 연산 규칙(나눗셈, 나머지, 타입)을 명확히 인지하기. 이 3가지를 잡아두면 이후 조건문/반복문/포인터 단계에서도 코드 해석력이 크게 올라갑니다.

기본 사용

예제 1) 산술 + 대입의 기본 흐름

#include <stdio.h>

int main(void) {
    int price = 12000;
    int discount = 1500;

    int final_price = price - discount;
    price = final_price;      // 계산 결과를 다시 저장

    printf("final: %d\n", price);
    return 0;
}

설명:

  • price - discount는 계산식이고, price = final_price는 저장식입니다.
  • 계산과 저장을 분리하면 디버깅 시 중간값을 확인하기 쉽습니다.
  • 유지보수 시 “어디서 값이 변했는지”를 추적하기 쉬워집니다.

예제 2) 복합 대입 연산자

#include <stdio.h>

int main(void) {
    int score = 80;

    score += 10;   // score = score + 10;
    score -= 5;    // score = score - 5;
    score *= 2;    // score = score * 2;
    score /= 5;    // score = score / 5;

    printf("score: %d\n", score);
    return 0;
}

설명:

  • 복합 대입은 코드 길이를 줄이지만, 본질은 “읽고-계산하고-다시 저장”입니다.
  • 연산자 우선순위를 과도하게 섞기보다 한 줄에 한 의도를 두는 편이 안전합니다.
  • 특히 /=는 정수 나눗셈 규칙을 그대로 따르므로 소수점이 필요하면 타입을 바꿔야 합니다.

예제 3) 전위/후위 증가의 차이

#include <stdio.h>

int main(void) {
    int a = 5;
    int b = 5;

    int x = ++a;  // a 먼저 6이 되고, x도 6
    int y = b++;  // y는 먼저 5를 받고, 그 다음 b가 6

    printf("a=%d, x=%d\n", a, x);
    printf("b=%d, y=%d\n", b, y);
    return 0;
}

설명:

  • ++a는 증가 후 값을 사용, b++는 기존 값을 사용 후 증가합니다.
  • 루프 카운터처럼 단독 문장에서 쓰면 차이가 거의 없지만, 식 내부에서는 결과가 달라집니다.
  • 팀 코드에서는 오해를 줄이기 위해 복잡한 식에 ++를 섞지 않는 규칙을 두기도 합니다.

예제 4) 정수 나눗셈과 명시적 형 변환

#include <stdio.h>

int main(void) {
    int total = 7;
    int count = 2;

    int avg_int = total / count;              // 3
    double avg_double = (double)total / count; // 3.5

    printf("avg_int=%d\n", avg_int);
    printf("avg_double=%.1f\n", avg_double);
    return 0;
}

설명:

  • 정수/정수는 정수 결과만 남습니다.
  • 정확한 비율 계산이 필요하면 계산 시작 전에 타입을 올려야 합니다.
  • 결과 변수만 double이어도, 계산 자체가 정수로 끝났다면 이미 늦습니다.

자주 하는 실수

실수 1) 비교(==)와 대입(=) 혼동

  • 원인: 수학의 등호 습관 때문에 if (x = 10)를 비교로 착각함.
  • 해결: 조건식에서 “비교는 ==”를 습관화하고, 컴파일 경고(-Wall -Wextra)를 항상 켠다.

실수 2) 후위 증가를 복잡한 식에 남발

  • 원인: arr[i++] = i++ + 3;처럼 한 줄에 상태 변경을 여러 번 넣어 가독성과 예측 가능성이 낮아짐.
  • 해결: 증감과 계산을 분리해 2~3줄로 작성한다. 디버깅 시간보다 코드 줄 수가 더 중요하지 않다.

실수 3) 정수 나눗셈을 실수 계산으로 오해

  • 원인: double avg = a / b;면 자동으로 실수 계산될 거라 착각함.
  • 해결: 피연산자에 명시적 캐스팅 적용((double)a / b)하고 테스트 케이스에 홀수/짝수 조합을 포함한다.

실수 4) 나머지 연산 %의 의미 오해

  • 원인: %를 “퍼센트”로 이해해 비율 계산에 사용함.
  • 해결: %는 나눗셈의 나머지다. 홀짝 판별, 주기 처리, 버킷 인덱싱에 사용한다.

실무 패턴

  • 상태 변경 연산(=, +=, ++)은 가능한 한 독립된 문장으로 분리한다.
  • 복합 수식보다 “중간 변수 이름”으로 의도를 드러낸다. (taxed_price, discounted_total 등)
  • 빌드 옵션 -Wall -Wextra -Werror를 기본으로 두어 연산자 관련 실수를 조기 차단한다.
  • 리뷰 시 체크리스트에 다음을 넣는다:
    • 정수/실수 나눗셈 의도 일치 여부
    • 증감 연산자 식 내부 사용 여부
    • 조건문에서 = 오타 가능성
  • 성능 최적화는 “읽기 쉬운 정답 코드”를 만든 뒤에 한다. 연산자 트릭은 마지막 수단이다.

오늘의 결론

한 줄 요약: 연산자는 계산 기호가 아니라 값의 흐름과 상태 변화를 설계하는 도구이며, 특히 대입/증감은 식의 결과와 부작용을 분리해서 읽어야 안전하다.

연습문제

  1. int a=10, b=3;일 때 a/b, a%b, (double)a/b 결과를 각각 예측하고, 이유를 설명해보세요.
  2. 다음 코드를 두 줄 이상으로 리팩터링해 가독성과 안정성을 높여보세요: result = base + bonus * rate - penalty++;
  3. 점수 누적 로직을 작성하세요. 시작 점수 50에서 보너스 3번(+7), 패널티 2번(-4)을 적용하고 최종 점수를 출력하세요. 단, 복합 대입 연산자를 반드시 사용하세요.

이전 강의 정답

지난 5강(기본 자료형) 연습문제 기준 예시 정답:

  • sizeof(char)는 보통 1바이트, sizeof(int)는 환경에 따라 보통 4바이트입니다.
  • float는 대략 소수점 6~7자리 정밀도, double은 대략 15자리 정밀도를 제공합니다.
  • int 범위를 넘는 값을 넣으면 오버플로가 발생하며, 결과는 의도와 다른 값으로 보일 수 있으므로 long long 또는 범위 체크가 필요합니다.

실습 환경/재현 정보

  • 컴파일러: Apple clang version 17.x (macOS)
  • 컴파일 옵션: -std=c11 -Wall -Wextra -O0
  • 실행 환경: macOS (arm64), 터미널 zsh
  • 재현 체크:
    • clang -std=c11 -Wall -Wextra -O0 lesson06.c -o lesson06
    • ./lesson06
    • 정수/실수 나눗셈, 전위/후위 증가 결과를 출력으로 확인