본문 바로가기
Programming/C Programming Modern Approach (K.N.K)

[KNK 정리] 4장: Expressions

by hyeyoo 2021. 8. 17.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

요약/정리

C언어는 다른 언어와 달리 expression을 강조한다. 기본적으로 변수와 상수도 expression에 해당하며, 연산자를 통해서 새로운 expression을 만들 수도 있다. (x + y) * z에서 x, y, z, x + y, (x + y) * z는 모두 expression이라고 할 수 있다.

 

산술 연산자 (Arithmetic Operators)

단항 연산자 (Unary Operators)

산술 연산자 중 단항 연산자는 +, -가 있다. 이는 expression의 부호를 나타낸다.

예시): +1, -1, +x, -x

이항 연산자 (Binary Operators)

+ : 두 operand의 합을 구한다.

-  : 좌측 operand에서 우측 operand를 뺀 값을 구한다.

* : 두 operand의 곱을 구한다.

/ : 좌측 operand를 우측 operand로 나눈 몫을 구한다.

%: 좌측 operand를 우측 operand로 나눈 나머지를 구한다.

 

위에 나열한 5개의 이항 연산자 중, %연산자를 제외하고는 operand의 타입이 정수 타입이던 부동소수점이던 상관이 없다. 단, % 연산자는 정수 타입만 허용한다. (그렇지 않으면 컴파일 오류)

 

그리고 / 연산자에서 실수를 많이하는게, (1/2)의 값은 0.5가 아니라 0이다. 다른 챕터에서 expression의 타입을 정하는 규칙을 배우는데, 정수와 정수를 나누면 그 결과가 되는 expression의 타입이 정수가 되기 때문이다.

 

/, %연산자는 우측 operand가 0이면 undefined behavior이다.

 

/ 연산자에서 좌측 또는 우측 operand가 음수일 경우 

C89: 소숫점 이하 부분을 올리거나 버린다.

C99: 항상 0에 가까운 쪽으로 올리거나 버린다.

 

% 연산자에서 좌측 또는 우측 operand가 음수일 경우 

C89: i % j의 부호는 i의 부호, j의 부호 중 아무거나가 된다.

C99: i % j의 부호는 항상 i의 부호와 같다.

 

C89는 /, %연산자의 operand 둘 중 하나가 음수일 때 구현에 따라 결과가 다르게 나오는 것을 허용하는데, 이를 Implementation-Defined Behavior라고 한다.

연산자의 우선순위와 (Precedence) 결합 방향 (Associativity)

모든 연산자는 우선순위(Precedence)와 결합 방향(Associavity)을 갖는다. 우선순위는 우리가 초등학교 때 배운 수학에서의 우선순위와 같다. 1 + 2 * 3의 우선순위가 없다면, 그 값이 9가 될지 7이 될는 모르는 것이다. 이러한 상황을 피하기 위해선 연산자 간의 우선순위를 정해서, 우선순위가 높은 순서대로 계산을 해야한다.

 

하지만 우선순위만으로는 부족하다. 우선순위가 같은 연산자들 사이에는 어떤 순서로 계산을 할 것인가? 이것을 정하기 위해서 결합 방향이라는 개념이 존재한다. 결합 방향이 오른쪽(right associative)인 경우에는 왼쪽에서부터 오른쪽으로 계산하고, 결합 방향이 왼쪽(left associative)인 경우에는 오른쪽부터 왼쪽으로 계산한다.

 

대입 연산자 (Assignment Operators)

프로그램을 작성하다 보면, expression의 값을 변수에 저장할 필요가 있다. 이럴 때 대입 연산자를 사용해 저장한다. 대입 연산은 v = e의 형태로 이루어지며, e의 값이 v에 저장된다. 이때, v와 e의 타입이 다르면, v의 타입에 맞게 형변환되어 저장된다.

int x;
float y;

x = 3.14; // x의 값은 3이다.
y = 3;  // x의 값은 3.0이다.

많은 언어에서, 대입(assignment)는 하나의 구문(statement)이다. 하지만 C언어는 대입이 expression이기 때문에 (v = e)자체가 하나의 expression으로서 값을 갖는다. 이때 값은 할당 후의 v의 값, 즉 e가 expression의 값이 된다.

Side Effect란

어떤 expression이 operand의 값을 바꾸면 side effect가 있다고 표현한다. x = 10, x++, x += 10과 같은 expression은 x의 값을 바꾸기 때문에 side effect가 존재한다고 말할 수 있다.

lvalue, rvalue란

대부분의 연산자의 operand에는 변수, 상수, 그리고 expression이 올 수가 있다. 하지만 대입 연산자의 경우, 좌측 operand에는 modifiable lvalue만 올 수 있다. 이때 lvalue란 컴퓨터 메모리에 저장된 객체(An object stored in computer-memory)를 의미한다. 예를 들어 int x;로 변수를 선언하면, 일반적인 x는 메모리 상에 존재하므로 lvalue이다. 하지만 x + 1이라는 expression은, 메모리 상에 존재하지 않으므로 expression이 evaluated된 후에는 값이 사라진다. 따라서 x + 1은 lvalue가 아니다.

 

그리고 왜 대입 연산자의 좌측 operand에 그냥 lvalue가 아니라 'modifiable' lvalue냐면, 예를 들어 배열같은 경우에는 메모리 상에는 존재하지만 그 이름을 바꿀 수 없기 때문에, "메모리 상에 존재하면서 그 값을 바꿀 수 있는" modifiable lvalue만 올 수 있다. 그 외에, lvalue를 포함한 모든 expression를 rvalue라고 한다.

 

복합 할당 연산자 (Compound Assignment Operator)

복합 할당 연산자는 기존의 값을 사용하여 대입할 때 사용된다. v += e, v -= e, v *= e, v /= e, v %= e과 같은 경우이다. 이때 v += e는 v = v + e을 축약한 것과 비슷하다.

i *= j => i = i * j

같은 게 아니라 왜 '비슷'한가? (1) v라는 expression 자체가 side effect가 존재하거나, (2) 연산자 우선순위에 따라서 달라질 수 있다. i *= j + k는 i = i * j + k와 다르다. 근데 이 부분은 책 예제가 좀 이상하거나 내가 잘못 이해한 것 같다. i *= j + k는 i = i * (j + k)인게 맞는 것 아닌가?

 

증감연산자 (Increment and Decrement Operators)

프로그램을 작성하다 보면 변수를 1씩 증가시키거나 감소시킬 일이 있다. 이러한 연산자를 증감연산자라고 한다. 증감 연산자는 전위 증감 연산자(++x, --x)와 후위 증감 연산자(x++, x--)로 나눌 수 있다. 이때, 전위 증감 연산자는 즉시 해당 operand의 값을 증가 또는 감소 시킨다.

 

int x;

x = 1;
printf("%d\n", ++x); // 2
printf("%d\n", x); // 2

반면 후위 증감 연산자는 operand의 값을 나중에 증가시킨다.

int x;

x = 1;
printf("%d\n",x++); // 1
printf("%d\n", x); // 2

그런데 operand의 값을 '나중에' 증가시킨다고 했는데, 얼마나 나중에인가? 그건 표준에 나와있지 않다. 하지만 다음 라인이 실행될 때 증가된다고 가정하는게 안전하다.

 

Expression 계산하기

우리는 연산자 우선순위와 결합 방향만 알면 모든 expression의 값을 계산할 수 있다.

출처 : https://en.cppreference.com/w/c/language/operator_precedence

 

Subexpression의 계산 순서

하나의 expression은 여러 개의 expression으로 구성될 수 있다. 예를 들어서 (a + b) * (c + d)에서 (a + b)와 (c + d)는 각각 subexpression이라고 할 수 있다. 그럼 컴파일러는 (a + b)와 (c + d)의 값 중 무엇을 먼저 계산할까? 이는 표준에 명시되어있지 않다. 따라서 Undefined Behavior에 해당한다. 아니 우선순위가 있는데 왜 UB지? 라는 생각이 든다면 이 링크를 2~3번 읽어보자.

 

따라서 subexpression 자체가 side effect가 존재할 때는 계산 결과가 이상해질 수 있다. 예를 들어 a = 1일 때 x = (a = 2) + (a + 1) 이라는 expression이 존재한다면, (a = 2)가 먼저 계산되는 경우에는 x의 값은 5가 되며, (a + 1)가 먼저 계산되는 경우에는 x의 값은 4가 된다. 따라서 side effect가 존재하는 subexpression을 구성하는 건 컴파일러에 따라 다르게 동작할 수 있으며, 매우 좋지 않은 행동이다.

 

연습 문제

1. Show the output produced by each of the following program fragments. Assume that i, j, k are int variables.

 

(a) i = 5; j = 3;

printf("%d %d", i / j, i % j);

답: 1 2

 

(b) i = 2; j = 3;

printf("%d", (i + 10) % j);

답: 0

 

(c) i = 7, j = 8, k = 9;

printf("%d", (i + 10) % k / j);

답: 1

 

(d) i = 1, j = 2; k = 3;

printf("%d", (i + 5) % (j + 2) / k);

답: 1

 

2. If i and j are positive integers, does (-i)/j always have the same values as -(i/j)? justify your answer.

 

답은 항상 같은 것은 아니다. 왜냐하면 C89의 경우, (-i)/j의 올림이 될 수도 있고 반올림이 될 수도 있기 때문이다.

C99인 경우에는 항상 0에 가까운 쪽으로 올림이나 버림이 되는데, ??

 

3. What is the value of each of the following expressions in C89? (Give all possible values if an expression may have more than one value.)

 

(a) 8 / 5

답: 1

(b) -8 / 5

답: -2 또는 -1

(c) 8 / -5

답: -2 또는 -1

(d) -8 / -5

답: 2 또는 1

 

4. Repeat Exercise 3 for C99.

 

(a) 8 / 5

답: 1

(b) -8 / 5

답: -1

(c) 8 / -5

답: 1

(d) -8 / -5

답: 1

 

5. What is the value of each of the following expressions in C89? (Give all possible values if an expression may have more than one value.)

 

(a) 8 % 5

답: 3

(b) -8 % 5

답: -3 또는 3

(c) 8 % -5

답: -3 또는 3

(d) -8 % -5

답: -3

 

6. Repeat Exercise 5 for C99.

(a) 8 % 5

답: 3

(b) -8 % 5

답: -3

(c) 8 % -5

답: 3

(d) -8 % -5

답: -3

 

7. The algorithm for computing the UPC check digit ends with the following steps:

Subtract 1 from the total.

Compute the remainder when the adjust total is divided by 10.

Subtract the remainder from 9.

 

=> 9 - (total - 1) % 10

 

It's tempting to try to simplify the algorithm by using these steps instead:

Compute the remainder when the total is divided by 10.

Subtract the remainder from 10.

 

=> 10 - (total % 10)

 

Why doesn't this technique work?

 

total이 10으로 나누어 떨어지는 경우에 결과가 이상해진다.

total이 10의 배수이면, 각각의 expression의 값은 0, 10이 된다.

 

8. Would the upc.c program still work if the expression 9 - ((total - 1) % 10) were replaced by (10 - (total % 10)) % 10?

 

total > 0이라면 가능은 한데 굳이...? 그렇게..해야할까? 문제의 의도가 뭘까.

 

9. Show the output prroduced by eache of the following program fragments. Assume that i, j, k are int variables.

(a) i = 7, j = 8;

i *= j + 1;

printf("%d %d", i, j);

 

i = i * (j + 1) = 7 * (8 + 1) = 63

 

답: 63

 

(b) i = j = k = 1;

i += j += k;

printf("%d %d %d", i, j, k);

 

i += j += k;

( i += (j += k) );

( i += (j += 1) );

( i += 2 );

 

답은 3 2 1

 

(c) i = 1; j = 2; k = 3;

i -= j -= k;

printf("%d %d %d", i, j, k);

 

i -= j -= k;

( i -= (j -= k) );

( i -= (j -= 3) );

( i -= (-1) );

 

답은 2 -1 3

 

(d) i = 2; j = 1; k = 0;

i *= j *= k;

printf("%d %d %d", i, j, k);

 

i *= j *= k;

( i *= ( j *= k ) );

( i *= ( j *= 0 ) );

( i *= 0 );

답은 0 0 0

 

10. Show output produced by each of the following program fragments. Assume that i and j are int variables.

(a) i = 6;

j = i += i;

printf("%d %d", i, j);

답: 12 12

 

(b) i = 5;

j = (i -= 2) + 1;

printf("%d %d", i, j);

답: 3 4

 

(c) i = 7;

j = 6 + (i = 2.5);

printf("%d %d", i, j);

답: 2 8

 

(d) i = 2; j = 8;

j = (i = 6) + (j = 3);

printf("%d %d", i, j);

답: 6 9

 

11. Show output produced by each of the following program fragments. Assume that i, j, and k are int variables.

 

(a) i = 1;

printf("%d ", i++ - 1);

printf("%d", i);

답: 0 1

 

(b) i = 10; j = 5;

printf("%d ", i++ - ++j);

printf("%d %d", i, j);

답: 4 11 6

 

(c) i = 7; j = 8;

printf('%d ", i++ - --j);

printf("%d %d", i, j);

답: 0 8 7

 

(d) i = 3;  j  = 4; k = 5;

pritnf("%d ", i++ - j++ + --k);

printf("%d %d %d", i, j, k);

답: 3 4 5 4

 

12. Show output produced by each of the following program fragments. Assume that i and j are int variables.

(a) i = 5;

j = ++i * 3 - 2;

printf("%d %d", i, j);

답: 4 10

 

(b) i = 5;

j = 3 - 2 * i++;

printf("%d %d", i, j);

답: 6 -7

 

(c) i = 7;

i = 3 * i-- + 2;

printf("%d %d", i, j);

답: 6 23

 

(d) i = 7;

j = 3 + --i * 2;

printf("%d %d", i, j);

답: 6 15

 

13. Only one of the expression ++i and i++ is exactly the same as (i += 1); which is it? Justify your answer.

++i는 (i += 1)와 값, 값이 증가되는 시점이 정확하게 동일하다. 하지만 i++의 값은 (i += 1)의 값과 다르다.

 

14.  Supply parentheses to show how a C compiler would interpret each of the following expressions.

(a) a * b - c * d + e

답: (((a * b) - (c * d)) + e)

(b) a / b % c / d

답: (((a / b) % c) / d)

(c) - a - b + c - + d

답: (((-a) - b ) + c) - (+d))

(d) a * - b / c - d

답: (((a * (-b)) /c) - d)

 

15. Give the values of i and j after each of the following expression statements has been executed. (Assume that i has the value 1 initially and j has the value 2.)

 

(a) i += j;

i = 3, j = 2

(b) i--;

i = 2, j = 2

(c) i * j / i;

i = 2, j = 2

(d) i % ++j;

i = 2, j = 3

 

Programming Projects

1. Write a program that asks the user to enter a two-digit number, then prints the number with its digits reversed. A session with the program should have the following appearance:

Enter a two-digit number: 28

The reversal is: 82

 

Read the number using %d, then break it into two digits. Hint: If n is an interger, then n % 10 is the last digit in n and n / 10 is n with the last digit removed.

 

#include <stdio.h>

int main()
{
    int two_digit_num;
    
    printf("Enter a two-digit number: ");
    scanf("%d", &two_digit_num);
    printf("The reversal is: %d%d", two_digit_num % 10, two_digit_num / 10);

    return 0;
}

2. Extend the program in Programming Project 1 to handle three-digit numbers.

#include <stdio.h>

int main()
{
    int three_digit_num;
    
    printf("Enter a three-digit number: ");
    scanf("%d", &three_digit_num);
    printf("The reversal is: %d%d%d",
        three_digit_num % 10,
        three_digit_num % 100 / 10,
        three_digit_num / 100);

    return 0;
}

3. Rewrite the program in Programming Project 2 so that it prints the reversal of a three digit number without using arithmetic to split the number into digits. Hint: See the upc.c program of Section 4.1

#include <stdio.h>

int main()
{
    int one, two, three;
    
    printf("Enter a three-digit number: ");
    /* width를 1로 명시해서 숫자 하나만 읽어온다. */
    scanf("%1d%1d%1d", &one, &two, &three);
    printf("The reversal is: %d%d%d", three, two, one);
    return 0;
}

4. Write a program that reads an integer entered by the user and displays it in octal (base 8):

Enter a number between 0 and 32767: 1953

In octal, your number is: 03641

The output should be displayed using five digits, even if fewer digits are sufficient. Hint: To convert the number to octal number (1, in this case). Then divide the original number by 8 and repeat the process to arrive at the next-to last digit. (printf is capable of displaying numbers in base 8, as we'll see in Chapter 7, so ther's actually an easier way to write this program)

#include <stdio.h>

/* 아 더럽다 */

int main()
{
    int num, reversed[5];
    
    printf("Enter a number between 0 and 32767: ");
    scanf("%d", &num);
    
    for (int i = 4; i >= 0; i--) {
        reversed[i] = num % 8;
        num /= 8;
    }
    
    printf("In octal, your number is: ");
    for (int i = 0; i < 5; i++) {
        printf("%d", reversed[i]);
    }
    printf("\n");
    
    return 0;
}

5. Rewrite the upc.c program of Section 4.1 so that the user enters 11 digits at one time, instread of entering one digit, then five digits, and then another five digits.

 

upc.c:

/* upc.c (Chapter 4, page 57) */
/* Computes a Universal Product Code check digit */

#include <stdio.h>

int main(void)
{
  int d, i1, i2, i3, i4, i5, j1, j2, j3, j4, j5,
      first_sum, second_sum, total;

  printf("Enter the first (single) digit: ");
  scanf("%1d", &d);
  printf("Enter first group of five digits: ");
  scanf("%1d%1d%1d%1d%1d", &i1, &i2, &i3, &i4, &i5);
  printf("Enter second group of five digits: ");
  scanf("%1d%1d%1d%1d%1d", &j1, &j2, &j3, &j4, &j5);

  first_sum = d + i2 + i4 + j1 + j3 + j5;
  second_sum = i1 + i3 + i5 + j2 + j4;
  total = 3 * first_sum + second_sum;

  printf("Check digit: %d\n", 9 - ((total - 1) % 10));

  return 0;
}

답:

/* upc.c (Chapter 4, page 57) */
/* Computes a Universal Product Code check digit */

#include <stdio.h>

int main(void)
{
  int d, i1, i2, i3, i4, i5, j1, j2, j3, j4, j5,
      first_sum, second_sum, total;

  printf("Enter the first 11 digits of a UPC: ");
  scanf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d",
      &d,
      &i1, &i2, &i3, &i4, &i5,
      &j1, &j2, &j3, &j4, &j5);

  first_sum = d + i2 + i4 + j1 + j3 + j5;
  second_sum = i1 + i3 + i5 + j2 + j4;
  total = 3 * first_sum + second_sum;

  printf("Check digit: %d\n", 9 - ((total - 1) % 10));

  return 0;
}

6. European countries use a 13-digit code, known as a European Article Number (EAN) instead of the 12-digit Universal Product Code (UPC) found in North America. Each EAN ends with a check digit, just as a UPC does. The technique for calculating the check digit is also similiar: 

- Add the second, fourth, sixth, eighth,tenth, and twelfth digits.

- Add the first, third, fifth, seventh, ninth, and eleventh digits.

- Multiply the first sum by 3 and add it to the second sum.

- Subtract 1 from the total.

- Compute the remainder when the adjusted total is divided by 10.

Subract the remainder from 9.

 

.... 예시 생략 ...

/* upc.c (Chapter 4, page 57) */
/* Computes a Universal Product Code check digit */

#include <stdio.h>

int main(void)
{
  int i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12,
      first_sum, second_sum, total;

  printf("Enter the first 12 digits of a EAN: ");
  scanf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%d",
      &i1, &i2, &i3, &i4, &i5, &i6, &i7, &i8, &i9, &i10, &i11, &i12);
      
  first_sum = i2 + i4 + i6 + i8 + i10 + i12;
  second_sum = i1 + i3 + i5 + i7 + i9 + i11;
  
  total = 3 * first_sum + second_sum;

  printf("Check digit: %d\n", 9 - ((total - 1) % 10));

  return 0;
}

 

참고자료

 

Order of evaluation - cppreference.com

Order of evaluation of the operands of any C operator, including the order of evaluation of function arguments in a function-call expression, and the order of evaluation of the subexpressions within any expression is unspecified (except where noted below).

en.cppreference.com

 

What are lvalue and rvalue in C?

TL;DR: “lvalue” either means “expression which can be placed on the left-hand side of the assignment operator”, or means “expression which has a memory address”. “rvalue” is defined as “all other expressions”.

jameshfisher.com

C99: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

댓글