본문 바로가기
모던 C

레벨 1 친숙 - 04 계산 표현하기

by 왕초보 독학 코딩 2024. 2. 23.

이 포스트는 '모던 C'를 요약한 내용입니다.

 

이 장에서 다루는 내용

  • 산술 연산 수행하기
  • 오브젝트 수정하기
  • 불 타입 다루기
  • 삼항 연산자로 조건부 컴파일하기
  • 평가 순서 지정하기

이 장에서 연산을 수행하는 데 사용하는 값이나 오브젝트의 타입은 대부분 size_t로 지정한다. 이 타입으로 선언한 값은 '크기(size)'를 나타내며 음수가 될 수 없고 0부터 시작한다. 즉, 수학에서 흔히 자연수라고 부르는 음이 아닌 정수를 의미한다. 그런데 컴퓨터는 유한하기 때문에 자연수 전체를 직접 표현할 수 없고, 무한에 가까운 값을 적절히 근사한다. size_t로 표현할 수 있는 값의 상한은 SIZE_MAX로 표현한다.

SIZE_MAX는 꽤 큰 값을 나타낸다. 플랫폼에 따라 다음 값 중 하나로 정해진다.

2^16 -1 = 65535
2^32 -1 = 4294967295
2^64 -1 = 18446744073709551615

 

임베디드 플랫폼이 아닌 이상 SIZE_MAX를 주로 두 번째와 세 번째 값을 사용한다. 특히 세 번째 값을 다수로 사용한다. 굉장히 복잡한 계산이 아니면 이 정도 값으로도 충분하다. SIZE_MAX는 stdint.h에서 제공하기 때문에 이 값을 따로 정의하는 등 프로그램 작성에 특별히 신경 쓸 일은 없다.

 

C 언어에서는 size_t처럼 '음이 될 수 없는 숫자'를 부호 없는 정수 타입(unsigned interger type)이라 부른다. +나 !=와 같은 기호를 연산자(operator)라 하고, 이런 연산자에 적용되는 대상을 피연산자(operand)라 한다. 예를 들어 a + b에서 +는 연산고, a와 b는 피연산자다.

 

4.1 산술

산술 연산자(arithmetic operator)는 값에 적용되는 연산자이다.

 

4.1.1 +, -, *

+, -, * 산술 연산자는 각각 두 값을 더하고, 빼고 곱한다.

+, -는 단항 연산자로도 사용할 수 있다. -b는 b의 음수값을 나타낸다. 

 

size_t의 장점 중 하나는 +, -, * 연산을 얼마든지 적용할 수 있다는 것이다. 최종 계산 결과가 0 ~ SIZE_MAX 구간을 벗어나지 않는 한 정식 결괏값이 된다. 만일 계산 결과가 범위는 벗어나 표현 가능(representable) 하지 않을 때, 오버플로(overflow)가 발생했다고 말한다. C 언어에서 오버플로를 처리하는 방법에 대해서는 다음 장에서 설명한다.

 

 

 

4.1.2 나눗셈과 나머지

/와 % 연산자는 정수의 나눗셈과 나머지 연산을 한다. a/b란 표현식은 a를 b로 나눈 값으로 평가되고, a%b는 a를 b로 나눈 나머지 값으로 평가된다. 

% 연산자를 사용하는 대표적인 예로 시간 계산이다. 가령 12시간 기준 시계에서 8시부터 6시간 후는 2시이다. 이때 % 연산자를 사용해서 (8 + 6) %12는 2가 된다. 

/와 %연산에서 사용할 수 없는 숫자는 0이 된다. 0으로 나눌 수 없기 때문이다.

 

% 연산자는 부호 없는 타입에 대한 덧셈과 곱셉 연산을 보완하는 용도로도 사용한다. 부호 없는 타입에 대한 값이 허용 범위를 벗어나면 오버플로가 발생한다. 이럴 때는 마치 % 연산자가 적용된 것처럼 계산 결과가 축소된다. 예를 들어 size_t에 대한 산술 연산을 수행할 때는 암묵적으로 % (SIZE_MAX+1)이 적용된다. 따라서 size_t 값이 SIZE_MAX + 1이면 0이 되고, 0 - 1는 SIZE_MAX가 된다.

- 연산자가 부호 없는 타입에 적용될 때는 이렇게 시작점으로 되돌아가는 현상이 발생한다. 예를 들어 size_t 타입에서 -1이란 SIZE_MAX와 같다. 따라서 a에 -1을 더한 결과는 a + SIZE_MAX가 되고, 이 값은 구간의 시작점으로 되돌아가게 된다. 

따라서  부호 없는 /와 % 연산에서는 오버플로가 발생하지 않게 된다.

 

 

 

4.2 오브젝트를 수정하는 연산자

a = 42 문장처럼 대입 연산자(=)를 사용해서 왼쪽 값으로 오른쪽 값으로 수정할 수 있다.

C 언어네는 +=, -=, *=-, /= %= 연산자도 추가로 제공한다. 이 문법을 적용하면 산술 연산자와 대입 연산을 하나로 간결하게 표현할 수 있게 된다.

a += b; // a = a + b와 동일하다.

이 연산자를 슬 때는 두 연산자 사이에 공백을 넣으면 안 된다. 예를 들어 a + = b와 같이 쓰면 구문 오류가 발생한다.

 

오브젝트를 수정하는 또 다른 연산자는 ++ 증가 연산자(increment operator)와 -- 감소 연산자가 있다. 증가 연산자와 감소 연산자는 선행 방식과 후행 방식이 있다. 다음은 선행 증가 연산자와 선행 감소 연산자이다. 

++i;  // 선행 증가 연산자 : i = i + 1; 과 같다.
--i;  // 후행 증가 연산자 : i = i - 1; 과 같다.

 

선행 증가와 선행 감소는 연산을 먼저 수행하고 나서 결과를 리턴한다. 

int a = 1;
int b = ++a;  // a == 2, b == 2

 

후행 방식은 결과를 전달하는 방식이 조금 다르다.  후행 방식 연산은 값을 먼저 리턴한 뒤에 연산을 수행하여 값을 변경한다. 

int a = 1;
int b = a++;  // a == 2, b == 1

 

 

 

4.3 불 연산

특정한 조건의 만족 여부에 따라 0이나 1의 값을 내는 연산자가 있다. 크게 비교 연산과 논리 연산으로 구분할 수 있다.

 

 

 

4.3.1 비교 연산

비교 연산자는 ==, !=, <, >, <=, <=. >=가 있다. 비교 연산자는 false와 true값을 리턴한다. true와 false는 1과 0을 다르게 표현한 단어일 뿐이다.

따라서 산술 연산이나 배열 인덱스로도 사용할 수 있다. 다음 예제에서 sign[1]에는 largeA에 있는 원소 중 1.0보다 작은 개수를 기록하고, sign[0]는 1.0과 같거나 큰 개수를 기록한다.

size_t sign[2] = { 0, 0 };
for (size_t i = 0; i < N; ++i) {
    sign[largeA[i] < 1.0] += 1;
}

 

not_eq라는 식별자도 있는데, != 대신 쓸 수 있지만 실제로 사용하는 경우는 거의 없다. 이 구문은 !=를 제대로 표현하지 못하는 컴퓨터가 있던 시절에 나온 것이며, 이를 사용하려면 iso646.h 헤더 파일을 인클루드해야 한다.

 

 

 

4.3.2 논리 연산

논리 연산자는 false나 true를 표현하는 값에 대한 연산을 수행한다. !, &&, || 연산자가 있다.

&&와 || 연산자는 단락 평가가 적용된다. 단락 평가는 두 번째 피연산자에 대한 평가는 상황에 따라 생략할 수 있다는 뜻이다.

 

 

 

4.4 삼항 연산자와 조건 연산자

삼항 연산자(ternary operator)는 if문처럼 두 가지 갈래 중 조건에 맞는 것을 골라서 리턴하는 표현식이다.

size_t size_min(size_t a, size_t b) {
    return (a < b) ? a : b;
}

 

 

 

4.5 평가 순서

지금까지 살펴본 연산자 중 &&, ||, ?:는 조건에 따라 피연산자의 평가 방식이 달라진다. 특히 피연산자의 평가 차이가 있다. 첫 번째 피연산자는 나머지 피연산자에 대한 조건에 해당하므로 가장 먼저 평가된다.

 

콤마 연산자(,)는 피연산자를 순서대로 평가해서 오른쪽 피연산자에 결괏값을 담는 연산자이다. 예를 들어 (f(a), f(b))는 f(a)를 평가한 뒤에 f(b)를 평가하고, 그 결과는 f(b)의 값이 된다. 여기서 주의할 점은 콤마를 나타내는 문자는 C언어에서 다른 역할도 담당하고 있는데, 그때는 지금과 다른 평가 규칙이 적용된다. 예를 들어 초기화 구문을 구분할 때 사용하는 콤마는 함수 인수를 구분하는 콤마와 속성이 다르다.

콤마 연산자는 클린 코드 작성에 도움이 안 되며, 초보자를 헷갈리게만 한다. 가령 a[i, j]는 행렬 A에 대한 이차원 인덱스가 아니라 a[j]다.

 

다른 연산자는 평가에 대한 제약이 없다. 예를 들어 f(a)+g(b)라는 표현식에서 f(a)와 g(b) 중 어느 것을 먼저 계산해야 하는지 정해진 바가 없다. 이때 순서는 컴파일러마다 다를 수 있고, 같은 컴파일러라도 버전에 따라 컴파일 옵션에 따라 다를 수 있으며, 표현식 주변의 코드에 따라 달라질 수도 있다. 

 

함수 인수도 마찬가지고 두 인수 중에 어느 것이 먼저 평가될지 알 수 없다.

printf("%g and %g\n", f(a), f(b));

 

'모던 C' 카테고리의 다른 글

레벨 1 친숙 - 03 결국은 제어  (0) 2024.02.03
레벨1 친숙 - 들어가며  (0) 2024.02.03
레벨0 만남 - 02 프로그램의 핵심 구조  (0) 2024.02.03
레벨0 만남 - 01 들어가며  (0) 2024.02.02
들어가며  (0) 2024.02.02