본문 바로가기

프로그래밍 언어/C

[K.N.K C Programming 정리] Chap 4. Expressions

K.N.KING C Programming

안녕하세요 여러분! 바로 4단원 정리 시작하겠습니다.

제가 해석한 표현이 조금 애매모호할때는 소괄호로 원문을 가져다 놓았습니다!

.

.

여담이지만 공부할때마다 제가 잘못 알고 있었던 개념들이 참 많다고 느낍니다..ㅎㅎ

원서의 표현이 참 좋은것같아요! 


Chap4. Expressions.

One does not learn computing by using a hand calculator, but one can forget arithmetic.


Intro

 

C언어의 큰 특징 중 하나는 표현식(Expressions)의 강조에 있다.

(여기서 expressions"formulas that show how to compute a value" 라고 하는데 값을 계산해내는 식 정도로 받아들이면 될 것 같다.)

가장 간단한 표현식은 변수와 상수들이다. 변수는 프로그램이 실행되며 계산되는 값을 나타내고, 상수는 변하지 않는 값을 나타낸다. 또한 표현식은 연산자와 피연산자에 의해 나타낼 수 있다.  

가령, a + (b * c) 에서 +연산자는 피연산자 a와 피연산자 (b * c) 에 적용된다. 각각의 피연산자는 표현식(expressions) 이다. 연산자는 표현식을 만드는데 가장 기본이 되는 도구들이다. 

 


4.1 Arithmetic Operators(산술연산자)

산술연산자는 addition(+), subtraction(-), multiplication(*), division(/) 이다.

Unary(단항 연산자) Binary(이항 연산자)
+ unary plus
- unary minus
Additive Multiplicative
+ addition(더하기)
- subtraction(뺴기)
* multiplication (곱하기)
/ division (나누기)
% remainder (나머지)

여기서 binary 는, 2개의 피연산자(operands)를 요구한다. Unary(단항) 연산자는 한개의 피연산자를 요구한다.

여기서 단항 plus 연산자 + 는 아무것도 하지 않는다. 주로 어떤 상수가 양수임을 강조하기 위해 사용된다.

% 연산자를 제외한 나머지 이항 연산자들은 실수와 정수 끼리의 연산도 허용한다. 이때 int와 float 타입을 연산했다고 치면 결과의 type은 float 이다. 

따라서 9 + 2.5f 의 경우 11.5, 6.7f / 2 의 경우 3.35 가 나온다. 

/ 와 % 연산자는 주의해야할 점이 몇 가지 있는데, 

  • / 연산자는 정수 / 정수 연산에서 나머지를 버린다. 가령, 1 / 2 는 0.5가 아니라 0 이된다.
  • % 연산자는 피연산자가 정수가 아니면 컴파일 되지 않는다.
  • / 나 % 연산자 오른쪽에 0을 위치시키면 undefined behavior(표준에서 정의하지 않은 행동들 - 무슨 결과를 가져다 줄 지 모름)를 불러온다.
  • / 나 % 연산자를 음수 피연산자와 같이 쓰는건 표준마다 다른데, C89 같은 경우 피연산자가 음수인 경우 나누기 연산자의 결과는 올림 일 수 도 있고 버림일 수 도 있다. C99 같은 경우 0에 가까운 방향으로 버림을 한다. 가령, -9 / 7 을 하는 경우 C89는 -1 일수도 있고 -2 일수도 있다. C99 같은 경우 -1이다.

여기서 C89의 경우, -9 / 7 을 evaluate했을 때 -1 일수도 있고 -2 일수도 있다는게 무슨 말인지 궁금하시죠?

여기서 Implementation Defined Behavior 를 짚고 넘어갑시다.

 

Implementaion defined behavior  : C 표준은 의도적으로 C언어의 일부를 구체화(unspecified)하지 않았는데, "implementation", 즉 특정 플랫폼에서 다른 방식으로 구현되게끔 해놓았습니다. 따라서 같은 코드의 프로그램이 어떤 플랫폼(implemenation) 에서는 다른 결과를 도출해내는 일이 발생합니다. 언뜻 보면 이상하고 위험해보일수도 있는데 왜 이렇게 해놓았냐 라고 물어볼 수 있습니다. 이는 C언어의 철학에서 비롯되었습니다. 프로그래밍 언어의 목표 중 하나는 효율성인데, 이는 하드웨어가 동작하는 방식대로 프로그래밍 언어가 실행되게 한다는것입니다. 즉, 어떤 CPU는 -9/7 을 계산한 결과 -1 을 계산해내고, 다른 CPU는 -2를 계산해냅니다. C89는 이런 점을 반영한것입니다.

물론 Implementaion defined behavior에 의존하는 프로그램을 작성하지 않는 것이 최선이지만 그게 불가능하다면 최소한 메뉴얼을 읽어봅시다. 

 

operator Precedence and Associativiy( 연산자 우선순위와 결합법칙)

가령 i + j * k 인 표현식이 있을때, 어느 연산(*, +)이 먼저 실행될지 헷갈릴 수 있다. 

가장 먼저 생각해볼법한것은 소괄호(parentheses)로 묶는것이다. 그런데 만약 소괄호를 못쓰는 상황이 온다면?

다른 많은 프로그래밍 언어들처럼 C언어는 이런 애매모한 상황을 방지하기 위해 연산자 우선순위를 정해놓았다.

Highest + -(단항 연산자)
  * / %
Lowest + -(이항 연산자)

위쪽에 위치할수록 연산자 우선순위가 높다.

그런데 만약 같은 우선순위의 두 개 이상의 연산자가 나타난다면? 이럴 때 associativiy, 즉 결합순서가 작용한다. 

연산자가 왼쪽에서 오른쪽으로 그룹을 짓는다면(순서로 연산을 수행한다면) left associative 라고 한다.

오른쪽에서 왼쪽일 경우 right associative 라고 한다.

*,/,%,+,- 처럼 이항 연산자는 모두 left associative이고, 단항연산자+,- 는 right associative 이다.

그 외 많은 연산자들의 우선순위는 그때그때 찾아보도록 하자. 사실 헷갈릴거같으면 소괄호로 묶어주는게 이해하기 제일 편하다.


4.2 Assignment Operators(대입 연산자)

표현식(expression)이 평가(evaluate)되고 나서 그 결과값(value)이 도출된다면, 우리는 보통 이를 어딘가에 저장해놓고 싶다. C는 단순 대입(simple assignment)연산자를 통해 이를 지원한다.

만약 이미 저장된 변수의 값을 업데이트 하고 싶다면 C는 이럴 경우 복합 대입(compound assigenment) 연산자를 지원한다.

 

Simple Assignment 단순 대입 연산자

'v = e' 의 동작은 다음과 같다.

1, 표현식 e를 평가한다. 

2, e의 값을 복사해 v에 저장한다. 

여기서 e는 상수, 변수, 복잡한 표현식들이 될 수 있다. 만약 v와 e가 같은 타입을 갖고있지 않다면, e의 타입이 v의 타입으로 바뀐다. 아래와 같은 예를 볼 수 있다.

int i;
float f;

i = 72.99f; //i는 72가 된다.
f = 136; //f는 136.0 가 된다.

자세한 형 변환에 관해서는 이후의 챕터에서 다루도록 하자. 

C에서 = 는 더하기 연산자 + 처럼 '연산자' 이다. 다시 말해 연산을 수행하는 과정에서, 즉 할당하는 과정에서 '결과값(result)'이 나오게 된다. 

v = e의 결과는 대입 후 v의 값이다. 

따라서 i = 72.99f 의 결과값은 72 이다. 이를 C언어에서는 side effect, 즉 부작용 이라고 부르는데 (사실 제가 봤을때는 부작용이라기 보다 by-product(부산물) 정도가 더 나은 표현이라고 생각하는데 여러분들은 어떻게 생각하시는지 궁금합니다!)

다시 말해,  'i = 0' 이라는 표현식을 평가하면 결과(result)가 0 이고, side effect는 i에 0을 대입(assign)하는 것이다.

(추가 - 수학에서 + 를 쓴다면 두 수를 더하고 말지만,(물론 C언어에서 + 도 같음. 더하고 끝.) C언어에서 = 을 쓰면 할당이 일어난뒤 side effect 때문에 왼쪽 피연산자의 값을 내놓는다. )

'=' 이 연산자이고 결과값을 가진다는 점을 상기하면,

i = j = k = 0 ; 이런식으로 쓸 수 있다. 물론 '='연산자는 right associative이기 때문에 

i = (j = (k = 0)); 이것과 동일하다. 

 

Lvalues

많은 C의 연산자들은 피연산자로 변수, 상수, 표현식들을 허용한다. 그러나 대입연산자는 왼쪽 피연산자로 lvalue를 필요로 한다.   lvalue는 상수 혹은 연산의 결과가 아니라 컴퓨터에 메모리에 저장된 object(c언어에서 object는 다른 언어의 그것과 다릅니다!!) 라는 것을 나타낸다. 

즉, 변수는 lvalue 이다. -1 이나 2 * i 같은 표현식들은 lvalue 가 아니다.

12 = i;

i + j = 0;

-i = j; 

이런 표현들은 전부 틀린 표현들이다. 대입연산자의 왼쪽피연산자는 lvalue가 와야되기 때문이다.

 

Compound Assignmet(복합 대입 연산자)

어떤 변수의 새로운 값을 얻어내기 위해 낡은 값을 사용하는 대입연산은 C언어에서 흔하게 일어난다.

즉, 변수 i에 저장된 값에 2를 더한다고 하면,

i = i + 2;

이렇게 쓸 수 있다. C의 compound assignment 연산자는 i +=2 와 같이 이 문장을 줄여준다. 

+= 연산자는 오른쪽 피연산자의 값을 왼쪽 피연산자에 더해준다.

이것말고도 -=, *=, /=, %=을 포함해 9개의 복합대입 연산자가 존재하는데 차차 다루기로 하자.

 

v += e 는 e를 v에 더하고 결과를 v에 저장한다.

v -= e 는 e를 v에서 빼고 결과를 v에 저장한다.

v *= e 는 v를 e로 곱하고 결과를 v에 저장한다.

v /= e 는 v를 e로 나누고 결과를 v에 저장한다.

v %= e 는 v를 e로 나눈 나머지를 v에 저장한다.

 

이 때, 정말 유의해야할 점은 'v = v + e' 와 v += e' 는 다르다는 점이다.

첫 번째 발생 요인은 연산자 우선순위이다. 가령, i *= j + k 와 i = i * j + k 는 다르다는것을 알 수 있다.

또한 두 번째 발생요인은 대입연산자의 부작용(side effect)에서 발생되는데, 조금 복잡한데 잘 따라오길 바란다.

v += e 가 평가(evaluate)되는 순간 v는 단 한번 평가(evaluate)된다. 그러나 v = v + e 를 평가하는 순간 v는 두 번 평가된다. v가 두번 평가된다는 것에서 문제가 발생하는데, 가령 

 

a[i++] += 2;

 

에서 = 대신 += 을 사용하게 된다면,

 

a[i++] = a[i++] + 2; 

 

이렇게 쓸 수 있다. 

여기서 i의 값을 정할 수 있는가? 즉 왼쪽의 a[i++]와 오른쪽의 a[i++]가 evaluate 될때 어느쪽이 먼저 evaluate 될 지 아는가?

우리는 i 가 두 번 증가(증분은 1) 한다는 것까지는 알지만, 어떤일이 일어날지 모른다.

 

4.3 Increment and Decrement Operators(증감연산자)

이제 i = i + 1; 을 i+= 1; 로 바꿀 수있는 것은 알것이다.

C는 여기서 그치지 않고 더 줄일수 있는 방법을 제시해주는데, 바로 ++와 -- 를 쓰는것이다.

그런데 증감연산자는 사용하기 까다로울수 있는데, 이는 증감연산자가 전위연산자(prefix) 와 후위연산자(postfix)로 나뉘기 때문이다. 증감연산자는 대입연산자와 마찬가지로 부작용(side effects)를 가진다.

바로 증감연산자는 피연산자의 값을 바꿔버린다 라는 것이다.

 

++i 는 실행될때 i를 즉시 1 증가 시키고, i++ 는 우선 저장되어있는 i값을 쓰고, 나중에 i값을 1 증가시킨다. 

그런데 이때, "나중에 1이 증가되는건 알겠는데, 얼마나 나중에 증가되는거지? 즉 언제 1증가하는 거냐?" 라는 질문을 할 수 있다. 

C표준은 언제 증가되는지 정확한 타이밍은 구체화(specify) 하지 않았다. 그러나 어쨌든 다음 문장이 실행되기 전에 증가된다는 것은 알 수 있다.

i가 언제 증가되는지 정확한 타이밍을 알고싶으면, C표준에서 소개하는 "sequence point" 의 개념을 알아야 할 필요가 있다. 또한 C표준에서는 다음과 같이 말하고 있다. "피연산자에 저장된 값을 업데이트 하는 것은 이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 발생할 것이다.(updating the stored value of the operand shall occur between the previous and the next sequence point.)"

 

이 때 sequence point에는 문장의 끝인 세미콜론도 있는데, 이때 해당 문장의 모든 증감연산자들은 반드시 이미 실행된 상태여야 한다. 다음 문장은 이런 조건이 만족되기 전에는 실행될 수 없다.

C에는 여러종류의 sequence point가 있다. 논리 & 나 | 연산자 심지어 콤마(,) 까지도 sequence point가 될 수 있다. 

함수호출도 그 중 하나이다. 함수의 인자는 함수가 실행되기 전에 반드시 평가되어야 한다. 가령 함수의 인자에 a++ 이런 표현식이 들어가면 함수가 실행되기전에 반드시 증가연산이 먼저 일어나야 한다. 

 

4.4 Expression Evaluation 

order of Subexpression Evalution

 

연산자 우선순위와 결합법칙은 C언어의 표현식들을 괄호를 사용해 더 잘게 쪼갤 수 있게 해준다.

그런데 이게 항상 모든 표현식에 적용될 수 있는건 아니다. 즉 C는 subexpression(괄호로 묶은 표현식) 이 평가될때의 순서를 정의하지 않았다.( 논리 and, or 혹은 콤마는 예외. 가령 논리and 연산자인 && 에서 short - circuit 일어나는 경우) 이게 무슨 말이냐면, 다음 예시를 보자.

 

(a + b) * (c + d) 

 

라는 expression 에서 우리는 (a + b)가 먼저 실행될지, 아니면 (c+d)가 먼저 실행될지 모른다. 

사실 이 예시와 마찬가지로 대부분의 경우 어떤게 먼저 실행되든 같은 값을 갖지만, subexpression 내의 피연산자의 값이 변하는 경우 같은 값을 갖는다고 확신할 수 없다.

예를들면, 

a = 5;

c = (b = a + 2) - (a = 1); 

인 경우 

두 번째 문장이 어떻게 실행될지는 정의되지 않았다.(The effect of executing the second statement is undefined)

즉, C 표준은 두번째 문장이 실행될 경우 어떤 일이 일어날지 정의해놓지 않았다.

실행시켜보면 c 컴파일러들은 c값으로 6또는 2를 도출해낼것이다.

만약 (b = a + 2) 가 먼저 평가된다면, b는 7이라는 값을 할당받고 c는 6을 할당 받는다. 그러나 만약

(a = 1) 이 먼저 평가된다면 b는 3을 할당받고, c는 2를 할당받는다.

이러한 문제를 피하려면, 대입연산자를 subexpression내에서 사용하는것을 피해야한다.

좀 더 구체적으로 말하면, 한 표현식 내에 어떤 변수를 접근하는 표현식과 같은 표현식 내에서 그 변수의 값을 바꾸는것을 피해야 한다는것이다.

 

이럴 경우 해결책은 여러 문장으로 쪼개서 쓰는것이다. 무슨말이냐면,

a = 5;
b = a + 2;
a = 1;
c = b - a;

이런식으로 쪼개서 쓰도록 하자.

 

피연산자의 값을 바꾸는 연산자는 대입연산자 이외에 증감연산자가 유일하다.

따라서 증감연산자도 쓸 때 주의해야 하는데, 다음 코드를 보자.

 

i = 2;
j = i * i++;

자연스레 j값이 4가 되리라고 추측해볼 수 있다. 그러나 이 경우도 정의되지않은(undefined) 케이스로,

우리는 이러한 시나리오를 상상해볼 수 있다. 

1, 곱하기 연산자의 두번째 연산자에서 i 를 가져온다(fetch). 그 후 1 더한다.

2, i 는 이제 새로운 값인 3을 가진다.  i를 가져온다.(fetch)

3, 3 * 2 가 수행되므로 j는 6이라는 값을 가진다.

 

여기서 "Fetching" 은 제가 가져오다 라고 번역했는데요, 책에는 이렇게 나와있습니다.

"Fetching a variable" means to retrieve the vlaue of the variable from memory

메모리로부터 다시 가져온다 이렇게 생각하시면 될 듯 합니다.

또한 이미 가져온 값을 바뀌지 않는데, (두번째 연산자가 2인데 3으로 안바뀌듯이) 이는 레지스터에 저장되기때문에 가능한 일입니다. 모호하실까봐 원문을 들고왔습니다!

A later change to the variable won't affect the fetched value, which is typically stored in a special location(known as a register) inside the CPU

 

여기서 살펴본 두 예시는 모두 undefined behavior 라는 것인데요, 

C 표준에 의하면 저 두 예시는 모두 undefined behavior 를 불러일으킨다. 이는 아까 살펴본 implementation defined behavior 와는 다른것으로, 만약 프로그램이 undefined behavior의 영역에 들어가는 순간 무슨 일이 일어날지는 아무도 모릅니다. 컴파일러가 다르다면 프로그램은 다르게 실행될것입니다. 뿐만 아니라 컴파일이 안될 수 도있고, 프로그램이 엉망이 될 수도 있습니다. 이상한 결과를 도출하거나 아무짝에 쓸모없는 결과를 도출해낼수도 있습니다. 

다시 말해 undefined behavior는 전염병같은 것으로, 반드시 피해야 하는것입니다.

 

 

4.5 Expression Statements(표현식 문장들)

C언어에서는 어떤 표현식들도 문장이 될 수 있다. 다시 말해, i++; 처럼어떤 표현식이라도 뒤에 세미콜론을 붙임으로써 문장이 될 수 있다는겁니다.

이 문장이 실행되면 i가 1 증가하고 이를 메모리로부터 가져오는(fetch) 일이 일어난다. 그런데 ++i 는 단독으로 쓰이기 때문에 1 증가된 i의 새로운 값은 그냥 버려진다. 

가령,

i = 1;
i * j - 1;

첫번째 문장에서, 1은 i 에 저장되고 새로운 i의 값이 메모리로부터 가져와지지만(fetched) 사용되지 않는다.

두번째 문장에서 표현식 i * j - 1의 값은 계산되지만, 바로 버려진다. 아무 효과가 없는것이다.(do - nothing expression)

 

Q&A section

Q : 연산자중에 지수 연산자는 없나요? 2의 3제곱 이런건 어떻게 계산하죠?

A : 3제곱 같이 작은수의 power를 구하는 정도라면 i * i * i 이렇게 쓰는걸 추천합니다. 수가 너무 크다면 뒤에 배울 비트연산자(<<)를 이용하거나 pow 라이브러리 함수를 알아보세요. 

 

Q : % 연산자를 실수 피연산자에 쓰고싶은데 어떻게 해야 하나요?

A : % 연산자는 정수 피연산자를 요구합니다. fmod 라이브러리 함수를 알아보세요.

 

Q : / 와 % 연산자에 음수를 사용하는건 왜이렇게 쓰기 갑갑할까요? C89와 C99 에서 결과가 달라지는 이유가 뭔가요?

A : 당시만해도 문제 될게 없었다. C99가 등장할 무렵, 그 당시 거의 모든 CPU가 설계될때 0쪽으로 버림을 행하는 방식으로 설계되었기 때문에 C99에서는 통일되었어.

 

Q : lvalue가 있다면 rvalue도 있나요?

A : 그럼 당연하지. lvalue는 대입연산자의 왼쪽에 올 수 있는 표현식이다. rvalue는 오른쪽에 올 수 있는 표현식이고.

따라서 rvalue는 변수, 상수, 복잡한 표현식 등등 다 될 수 있어. 

(원서 표현이 좋아서 lvalue의 정의를 원서표현 그대로 써볼게요!!

An lvalue is an expression that can appear on the left side of an assignment; an rvalue is an expression that can appear on the right side. Thus an rvalue could be a variable, constant, or more complex expression.)

 


휴 ㅎㅎ 원래는 책에서 내가 새롭게 안 내용들만 넣으려 했는데 주옥같은 문장들이 너무 많아서 이것 저것 다 넣다보니 내용이 길어졌네요 ㅎㅎ 재밌게 읽으셨나요??

그럼 모두 안녕히~

 

p.s. 아 그리고 2단원하고 3단원은 조금 나중에 올릴 예정입니다! 조금만 기다려주세용 ㅎㅎ