[CodeEater와 제로부터 시작하는 C언어] 6.1장 - 자료형끼리의 연산


C언어를 사용하면서 자료형이 무조건 같은 경우만 있지 않다.
다른 형끼리의 연산도 충분히 자주 사용한다는 것이다.
이에 대해서 큰 이해없이 사용을... 해도 상관없긴한데 알면 그래도 좋지 않을까?
그래서 부록으로 다른 자료형 끼리의 연산은 도대체 어떻게 이루어지는지
그리고 여기서 우리가 뭘 조심해야할지에 대해서 부록으로 한번 다루어 보도록 하자.
노파심에서 하는 이야기이지만 부록 부분은 초심자는 안읽어도 된다.
중요한 부분이긴하지만 지금 알 필요는 없는 부분이기 때문이다.
그리고 좀 어렵기도 하고.

이번에 반복문이라는 키워드를 반드시 알아야한다.
아직 반복문에 대해서 이야기한적은 없기 때문에 모른다면 뛰어넘어도 되고,
아니면 반복문만 잠시 알아보고 와도 좋다.

그리고 추가적으로 정수 승격(Integer Promotion)이라는 개념도 배우게 될 것이다.
이 개념을 알려주는 경우도 있고 건너 뛰는 경우도 있지만 기왕이면 알고가는게 좋을 것이다.

우리는 먼저 타입군을 3가지를 정하자, 정수와 양의 정수와 실수로 타입군을 나눈다.
이 때의 경우 다른 타입군끼리의 연산이 어떠할지, 혹은 같은 타입군 끼리의 연산이 어떠할지를 알아보자.
먼저 다른 타입군끼리의 연산에 대해서 한번 알아보도록 하자.

다른 타입끼리의 연산이 어떻게 돌아가게 될지를 아는건 아주 중요하다.
사실 알고 넘어가는게 좋다.
왜냐하면 어떻게 서로 다른 녀석들을 통합시킬지에 대한 논의가 필요하기 떄문이다.

경우의 수는 3종류가 존재한다.
각각의 종류가 어떻게 진행될지 알아보도록 하자.

여기서 여러분들이 알아야할 놀라운 사실이 하나 있다...
바로 C언어는 사실은 양의 정수와 정수를 데이터적으로는 구별하지 않는다는 사실이다.

이 이야기를 하면 놀래는 사람들이 꽤 있다.
엄밀히 말하면 아예 구별을 하지 않는다는 것은 아니고 데이터는 동일하게 저장된다는 것이다.
이를 한번 우리가 확인해보도록 하자.
#include <stdio.h>
int main() {
int a = 10;
unsigned int b = 2147483647;
int c = 2147483647;
printf("%d\n", b);
printf("%d\n", c);
printf("%d\n", b + a);
printf("%d\n", c + a);
printf("%d\n", b - a);
printf("%d\n", c - a);
return 0;
}
위와 같은 코드를 보도록하자.
b와 c는 int가 표현할수 있는 최대의 수를 가지고 있다.
(2147483647은 int가 표현할 수 있는 최대의 수이다.)
여기서 1만 더해도 b의 경우는 표현범위 안이라서 상관없지만
c는 표현 범위를 넘어가므로 음수가 될 것이고 예측 못하는 수...가 된다고 아는 사람들이 있다.
하지만 실상은 이와 다르게 명백하게 예측 할 수 있다.
일단 위의 코드를 실행해보자.

그러나 왠걸?
둘은 완전하게 동일한 값을 출력하는걸 알 수 있다.
이는 %d때문 아니냐?라고 반문할 수 있다.
뭐 맞는말이긴한데 문제는 저장된 데이터자체가 같다는 뜻이다.
#include <stdio.h>
int main() {
int a = 10;
unsigned int b = 2147483647;
int c = 2147483647;
printf("%u\n", b);
printf("%u\n", c);
printf("%u\n", b + a);
printf("%u\n", c + a);
printf("%u\n", b - a);
printf("%u\n", c - a);
return 0;
}
그러면 이러한 코드로 바꿔보자.
크게 바뀐건 없고 출력을 양의 정수형인 %u로 바꾸어 보았다.

이제는 int형을 넘은 범위를 출력을 하는 것을 확인할 수 있다.
그런데 여기서도 둘의 데이터는 완전하게 동일한걸 알 수 있다.
이를통해서 우리가 알 수 있는것은 int와 unsigned int는 사실 내부적으로 unsigned int형의 연산으로 동작하게 된다는 것이다.
그리고 출력 형식에 맞춰서 거기에 맞는 값을 보여주게 된다.
출력형식을 정수형(%d)로 하면 해당 타입을 int의 범위인 -2147483648에서 2147483647로 표현하고
출력형식을 양의 정수형(%u)로 하면 해당 타입을 unsigned int의 범위인 0에서 4294967295로 표현하게 된다.
사실 여러분은 이게 좀 신기할 것이다. 어떻게 이런게 가능할까? 음수가 있는데도?
이는 음수는 사실 보수화 연산을 통해서 저장된다고 했는데 이게 절묘하게 맞아 떨어지게 된다.
이 사실을 잠깐 이해하고 넘어가자.
컴퓨터에서 왜 음수 표시를 2의 보수화를 채택을 해서 했는지를 알 수 있고
태초에 프로그래밍 언어를 설계한 사람들의 천재성을 여기서 엿볼 수 있다.
unsigned int a = -10; //4294967286
C언어에서는 위 같은 코드가 가능하다.
정말로 -10이 저장되는건 아니고 4294967286이 저장되는데
실질적으로 -10과 4294967286는 4바이트 정수(int, unsigned int 등)끼리의 처리에서는
내부에서는 같은 수로 취급하게된다.
따라서 말장난 같지만 -10이 저장된다고 생각해도 무방하고 4294967286이 저장된다고 생각해도 무방하다.
#include <stdio.h>
int main() {
int a = 5;
int b = -10;
unsigned int c = -10;
unsigned int d = 4294967286;
printf("%u\n", a + b);
printf("%u\n", a + c);
printf("%u\n", a + d);
printf("%d\n", a + b);
printf("%d\n", a + c);
printf("%d\n", a + d);
return 0;
}
그럼 위와 같은 코드를 예상해보자.
b, c, d는 이론상으로는 셋다 같은 숫자가 저장되어 있다.
심지어 b는 int형이다. 이 경우 셋의 결과가 같을까?

셋다 결과가 같다.
이로서 프로그래밍에서 정수끼리의 연산에서 unsigned와 signed는 연산에서
차이가 없이 똑같은 로직이 적용된다는걸 알 수 있다.
물론 그렇다고 해서 unsigned와 signed가 동일하다는 이야기는 아니다.
둘은 사칙연산에서는 동일하게 작동하지만 그 외의 상황에서는 다르게 작동하는 경우도 있다.
가령 아래의 예를 들어보자.
#include <stdio.h>
int main() {
for (int a = 10; a >= 0; a--) {
printf("%d\n", a);
}
return 0;
}
정상적인 for문이다. 이러한 코드는 아무문제 없는 코드다.
굳이 해석하자면 a는 10부터 0이 될때까지 a를 출력하는 코드다.

우리 생각대로 0에서 정지하는걸 확인할 수 있다.
#include <stdio.h>
int main() {
for (unsigned int a = 10; a >= 0; a--) {
printf("%d\n", a);
}
return 0;
}
하지만 이 코드는 어떻게 될까?
unsigned int는 음수가 되지 않는다.
따라서 위 코드는 영원히 무한루프를 돌리게된다.

따라서 unsigned int와 int는 연산때는 동일하게 작용하지만
내부적으로 unsigned int는 뚜렷하게 양수로 인식되고 있음을 알 수 있다.
사실 이문제는 아주 중요한 문제이다.
for문에 unsigned형을 쓰게 되는건 아주 위험하다.
그렇기 때문에 만약 for문에 unsigned형을 써야한다면 int나 long long형으로 치환한 변수를 쓰는게 좋다.

정수와 실수의 경우 정수를 실수로 바꾼다.
해당 정수를 다른 피연산 타입의 실수로 변환한다.
그리고 실수와 실수 연산으로 바뀌게된다.
가령 int와 float의 연산이면 int를 먼저 float으로 변환하고
그 다음 float과 float의 연산을 수행하게 된다.
물론 int와 double의 연산이면 int를 먼저 double로 변환한다.
사실 크게 특이한건 없으므로 이건 넘어가겠다.

이 부분역시 양의 정수와 정수가 구별된은 부분이다.
양의 정수는 음수가 없기 때문에 음수 타입으로 지정해봤자 무조건 양수로 인식한다.
#include <stdio.h>
int main() {
int a = -10;
unsigned int b = -10;
float fa = 1.5f + a;
float fb = 1.5f + b;
printf("%f\n", fa);
printf("%f\n", fb);
return 0;
}
위의 코드가 주어졌을 때 우리가 생각해보자.
내부적으로 a와 b는 같은 값이다.
그러나 a는 음수고 b는 음수의 탈을 쓴 양수이다.(unsigned형은 음수가 없다.)
여기서 실수형의 연산의 결과는 어떻게 될까?

정상적으로 동작하는걸 확인할 수 있다.
int형은 -10이 존재하므로 이를 -10으로 전환시킨다.
반대로 unsigned int는 -10이 없기에 거기에 매칭된 양수로 치환되며
이를 바탕으로 계산하는걸 알 수 있다.
사실 직관적으로 이는 당연한 일이므로
(양수가 양수로 전환되는게 원래는 당연하잖아)
그리 깊게 생각할 필요는 없다.

다른 타입군 끼리의 연산은 충분히 이야기 했으므로
같은 타입군 끼리의 연산을 한번 보도록 하자.
사실 이는 직관에 의존하면 대부분 문제가 안일어나기에 중요도는 떨어진다.
그러나 여러분이 C언어를 깊게 알고싶다면 이 부분을 알아두는것도 좋다.

#include <stdio.h>
int main() {
char c1 = 1;
printf("%d\n", sizeof(c1));
return 0;
}
sizeof 연산자는 해당 변수가 몇 바이트인지를 알려준다.
이게 정확히 char인지 short인지는 알 수 없지만 바이트 출력으로 무슨 변수인지
우회적으로는 알 수 있다.
c1의 크기는 char이니까 1이 출력될 것이라는 것은 쉽게 예상할 수 있다.

그럼 이건 예상할 수 있을까?
#include <stdio.h>
int main() {
char c1 = 1;
char c2 = 2;
printf("%d\n", sizeof(c1 - c2));
return 0;
}
char와 char의 연산결과는 과연 몇바이트일까?
우리가 일반적으로 char와 char의 연산이니까 1바이트일거라는 생각을 할 수 있다.

하지만 실제로 출력해보면 4바이트가 나온다.
놀라울 수 있는데 다른 타입이 아니라
같은 타입인 char끼리의 연산의 결과가 int로 바뀐다는것을 알 수 있다.
#include <stdio.h>
int main() {
char c = 1;
short s = 2;
printf("%d\n", sizeof(c - s));
return 0;
}
그럼 이제 이 코드를 예상할 수 있는가?
short와 char의 연산은 더 큰타입은 short에 맞춰질까?
그럼 출력은 2일까? 아니면 앞에서 char끼리의 연산이 int로 바뀌었으니까
이 연산도 int로 나올까?

이 경우에도 int가 되는걸 확인할 수 있다.
여기서 우리가 알 수 있는 사실은 정수끼리의 연산은 int형이 나오게 된다는 것이다.

정수 승격(Integer Promotion) - int이하의 타입의 경우 연산결과 int로 맞춰진다.
즉 int 이하의 타입(4바이트 이하의 모든 정수)끼리의 연산은 바드시 int타입으로 나오게된다.
정수 승격은 int와 char 같은 정수형 타입 뿐만이 아니라 진리값(bool)에도 적용된다.
그래서 bool + bool의 연산을 시행할 경우 이는 int로 결과가 나오게된다.
이는 경우에 따라서 문제를 일으킬 소지를 볼 수도 있지만 사실 일상적인 상황에서 큰 문제는 없다.
이걸 평생 모르는 사람들도 있다.
하지만 비트단위로 사용하다보면 문제가 생길 수 있는데
딱히 지금은 중요한 내용은 아니므로 여기에서 설명하지는 않겠다.
왜 일반적일 때 문제가 없을까?
#include <stdio.h>
int main() {
char c1 = 1;
char c2 = 2;
short s = c1 - c2;
printf("%d\n", s);
printf("%d\n", sizeof(s));
return 0;
}
가령 이런 코드가 존재한다고 해보자.
c1 - c2는 정수 승격이 일어나므로 short에는 int가 대입될 것이다.
그러나 좌변이 short이면 우변은 short크기로 잘라버린다고 했다.
더 정확하게 말하면 우변은 강제적으로 타입 캐스팅이 일어나게 된다.
우변을 short로 강제로 타입캐스팅을 해버린다.
따라서 우리는 일상생활에서 특정한 코드들을 제외하고는
정수 승격의 존재를 느끼기가 힘들다.
그러면 int 보다 더 큰타입들은 어떻게 될까?
#include <stdio.h>
int main() {
char c = 1;
long long ll = 2L;
printf("%d\n", sizeof(c - ll));
return 0;
}
char와 long long int형을 비교해보자.
이는 정수승격은 일어날 수 없다. 정수승격은 int형 이하에서만 일어나니까.
일반적으로 생각해보면 char와 long long int형의 연산은
long long int형으로 수렴하는게 합리적이라는 생각을 해볼 수 있다.

결과를 보면 알 겠지만 long long int로 상승한걸 알 수 있다.
우리가 일반적으로 쉽게 생각할 수 있듯이 작은 수와 큰 수의 경우에는 큰수로 맞춰진다.
int와 long, long long의 경우 int형이 아니라 더 큰 타입으로 맞춰진다.
결론 - int 이하는 int로 고정, long 이상의 경우 더 큰 타입으로 맞춰진다.

#include <stdio.h>
int main() {
float f = 3.14f;
double d = 3.14;
long double ld = 3.14L;
printf("%d\n", sizeof(f - f));
printf("%d\n", sizeof(f - d));
printf("%d\n", sizeof(d - ld));
printf("%d\n", sizeof(ld - f));
return 0;
}
float과 double, long double로 3가지의 자료형이 있다.
그럼 각각을 비교해보도록하고 결과를 보자.

우리가 흔히 예측하는게 float < double <= long double이니 더 큰쪽으로 맞춰질것이라고 생각한다.
그리고 그 결과는 정확하게 일치하게 된다.
실수형끼리의 연산에서는 더 큰타입으로 작은타입이 타입캐스팅을 하게된다.
즉 float과 float은 float이 되지만
float과 double은 double로, dobule과 long double은 long double이 된다.
사실 실수에 대해서 할 이야기가 아주 많기는 하지만 이는 다른 포스팅에서 뵙도록 하겠다.
'제로부터 시작하는 프로그래밍 > C' 카테고리의 다른 글
[CodeEater와 제로부터 시작하는 C언어] 7.1장 - 부울대수와 단축평가 (0) | 2020.04.14 |
---|---|
[CodeEater와 제로부터 시작하는 C언어] 7장 - 진리값과 부울대수 (0) | 2020.04.13 |
[CodeEater와 제로부터 시작하는 C언어] 6장 - 연산자 (0) | 2019.12.08 |
[CodeEater와 제로부터 시작하는 C언어] 5.1장 - scanf (0) | 2019.12.03 |
[CodeEater와 제로부터 시작하는 C언어] 5장 - 입력과 출력 (0) | 2019.12.01 |