작성: 2021-01-16 23:59
수정: 2021-02-06 23:06
난이도: Bronze Ⅴ
나머지
두 수 A와 B의 나머지 연산은 $B=Aq+r(A, B, q\in\mathbb{Z},0\le r\lt A)$의 관계식으로 정의되는데, 이 때 r이 나머지가 된다. 관계식에서 조건으로 주어졌듯이, 나머지 연산은 두 수가 정수일 때만 정의한다. 따라서 실수일 때에는 작동하지 않는다. C/C++에서는 이에 대해 컴파일 에러를 띄운다.
다만 Java와 Python은 위 관계식을 확장시켜서 실수일 때도 돌아가게끔 해놓았다. 정확히는 관계식을 $B-Aq=r$로 고친 후, r이 원하는 범위로 들어올 때까지 B에서 A를 계속 뺀다. 그러다가 r이 적당한 값이 되면 뺄셈을 멈추고 남은 값을 출력한다.
정수형 타입
정수형의 범위와 오버플로우
C/C++
타입 | 범위 | 크기 |
---|---|---|
char |
-128~127 / -27~27-1 | 1 byte |
unsigned char |
0~255 / 0~28-1 | 1 byte |
short |
-32768~32767 / -215~215-1 | 2 bytes |
unsigned short |
0~65535 / 0~216-1 | 2 bytes |
int |
-2147483648~2147483647 / -231~231-1 | 4 bytes |
unsigned int |
0~4294967295 / 0~232-1 | 4 bytes |
long |
int 또는 long long과 같은 범위1 | 4 or 8 bytes |
unsigned long |
unsigned int 또는 unsigned long long과 같은 범위1 | 4 or 8 bytes |
long long |
-9223372036854775808~9223372036854775807 / -263~263-1 | 8 bytes |
unsigned long long |
0~18846744073709551613 / 0~264-1 | 8 bytes |
Java
타입 | 범위 | 크기 |
---|---|---|
boolean |
false ~true |
1 byte |
byte |
-128~127 / -27~27-1 | 1 byte |
short |
-32768~32767 / -215~215-1 | 2 bytes |
char |
0~65535 / 0~216-1 | 2 bytes |
int |
-2147483648~2147483647 / -231~231-1 | 4 bytes |
long |
-9223372036854775808~9223372036854775807 / -263~263-1 | 8 bytes |
위의 표에서 int
를 예시로 들면, int
는 32비트(4바이트) 중에서 가장 왼쪽에 있는 1비트를 부호를 나타내는 데에 사용한다.2 이 비트값이 0이면 0을 포함한 양수, 1이면 음수를 나타내는 것이다. n번째 비트를 bn라고 하면 int
형의 비트 배열 {bi}은 $-2^{31}b_{31}+2^{30}b_{30}+…+2^{1}b_{1}$을 나타낸다. bi는 0 또는 1이기 때문에 int
의 범위가 위와 같이 정해진다.
이제 두 int
변수의 덧셈을 생각해보자. CPU 위에서 돌아가는 add
명령(어셈블리)은 부호를 신경쓰지 않고 32비트 전체에 대해 가산 연산을 진행한다. 이제 2000000000을 비트로 나타내보면 01110111 00110101 10010100 00000000
이 되고, 여기에 2000000000을 또 더하면 11101110 01101011 00101000 00000000
이 될 것이다. 그런데 가장 왼쪽 비트를 보면 1이다. 따라서 이 수는 음수임을 알 수 있고, 2의 보수법을 이용하여 부호를 +로 바꿔보면 00010001 10010100 11011000 00000000
, 294967296이다. 따라서 int
의 범위 안에서는 2000000000 + 2000000000
가 -294967296이 되는 것이다. 이런 식으로 부호가 아닌 데이터가 부호 비트에 영향을 주거나, 연산의 결과가 변수 자체의 크기보다 커서 잘려나가는 것을 오버플로우라고 한다.
정수형 상수의 타입 표기
C/C++/Java에서 그냥 숫자만 입력해서 정수형 변수에 값을 대입하는 경우가 있다. 이 때 이 숫자는 int
형으로 컴파일러가 자동으로 인식한다. 그러면 int
형 밖의 정수는 어떻게 해야할까? 이를 위해서 주어진 상수를 어떤 타입으로 접미사가 존재한다. 그 종류는 아래와 같다.
접미사 | 타입 |
---|---|
(없음) | int |
U , u |
unsigned int |
L , l |
long |
UL , ul |
unsigned long |
LL , ll |
long long |
ULL , ull |
unsigned long long |
위의 접미사 중 L
과 l
만 Java에 존재한다.
예시
unsigned long long n = 1000000000000000ULL;
고정적인 정수형(C/C++)
int
나 long
같은 타입들은 환경에 따라 그 크기가 변할 수 있다. 예를 들어, 성능은 썩 아니지만 충분히 작은 CPU가 필요한 시스템은 더 작은 크기를 할당하여 사용해야할 수도 있다. 하지만 이와 같은 환경에 상관없이 언제 어디서나 일정한 크기를 가져야 하는 값이 있을 수 있다. 이 때 사용할 수 있는 타입이 있다. 그 종류는 아래와 같다.
타입 | 의미 | 범위 | 크기 |
---|---|---|---|
int8_t |
8비트 크기의 부호있는 정수 | -27~27-1 | 1 byte |
int16_t |
16비트 크기의 부호있는 정수 | -215~215-1 | 2 bytes |
int32_t |
32비트 크기의 부호있는 정수 | -231~231-1 | 4 bytes |
int64_t |
64비트 크기의 부호있는 정수 | -263~263-1 | 8 bytes |
uint8_t |
8비트 크기의 부호없는 정수 | 0~28-1 | 1 byte |
uint16_t |
16비트 크기의 부호없는 정수 | 0~216-1 | 2 bytes |
uint32_t |
32비트 크기의 부호없는 정수 | 0~232-1 | 4 bytes |
uint64_t |
64비트 크기의 부호없는 정수 | 0~264-1 | 8 bytes |
C는 #include <stdint.h>
, C++은 #include <cstdint>
를 통해 사용할 수 있다.
실수형 타입
실수형의 범위와 특수값
C/C++/Java 기준으로 실수형은 float
와 double
이 있다. 여기에 C/C++은 long double
이라는 타입을 하나 더 가진다. Python은 실수 또한 미친듯이 넓은 범위를 자랑한다.
타입 | 범위 | 크기 |
---|---|---|
float |
±3.4028235×1038 / ±(2-2-23)×2127 | 4 bytes |
double |
±1.7976931×10308 / ±(2-2-52)×21023 | 8 bytes |
long double |
±1.1897314×104932 / ±(2-2-63)×216383 | 80 bits3 |
실수형 변수는 부호, 지수부, 가수부로 나뉜다. 부호 비트를 m, 지수부를 {ei}(n개 비트), 가수부를 {si}(k개 비트)라고 하는데, 타입 별 비트 수는 아래와 같다.
타입 | 부호 | 지수 | 가수 |
---|---|---|---|
Single (float ) |
1 | 8 | 23 |
Double (double ) |
1 | 11 | 52 |
(long double ) |
1 | 17 | 64(1/63) |
이 비트의 값에 따라 실수형 변수가 나타내는 값은 아래와 같다.
경우 | 값 |
---|---|
normalized value(지수부가 0 초과 2n-1 미만) | $(-1)^{m}\times 2^{e-2^{n-1}+1}\times (1.s_{k-1}s_{k-2}…s_{0})$ |
denormalized value(지수부가 0) | $(-1)^{m}\times 2^{2-2^{n-1}}\times (0.s_{k-1}s_{k-2}…s_{0})$ |
지수부가 2n-1이고 가수부가 0 | 부호 비트가 0일 경우 +∞, 1일 경우 -∞ |
지수부가 2n-1이고 가수부가 0이 아님 | NaN |
일반적인 경우는 normalized value와 같이 값을 인식한다. 하지만 특수한 경우가 있는데, denormalized, infinity, 그리고 NaN이다. denormalized value는 지수부가 0인 경우인데, 계산법이 약간 다르다. 이는 normalized value의 최소값(float
의 경우 $2^{2-2^{n-1}}$)과 0 사이의 간격을 일정하게 메꾸기 위해 사용하는 범위이다. Infinity는 말 그대로 무한대를 의미하며, 실제로는 이 타입이 표시할 수 있는 최대의 실수값을 넘어갔다는 의미로 쓰인다. NaN은 연산의 결과가 정의되지 않는다는 의미로 주로 쓰인다. $\sqrt{-1}$이나 $\frac{0}{0}$같은 경우 그 결과가 NaN
으로 나온다.
실수형 변수의 정확성
정수형 변수와는 다르게 실수형 변수는 나타낼 수 있는 값을 순서대로 나열했을 때 그 거리가 일정하지 않다. 가장 거리가 짧은 부분은 denormalized에서의 $2^{2-2^{n-1}}\times 2^{-k}$이고, 가장 거리가 긴 부분은 지수부가 특수값이 아닌 최대값(2n-2)일 때 $2^{2^{n-1}-2}\times 2^{-k}$이다. 값이 커질수록 그 간격도 커지는 경향성을 가진다. 이를 수직선에 나타낸 그림은 아래와 같다.
By Joeleoj123 - Own work, CC BY-SA 4.0, Link
실수형 연산은 정수형 연산과는 다르게 숨겨진 비트 2개를 사용하는데, 연산의 결과값에 가장 가까운 나타낼 수 있는 실수값을 찾는 데에 사용된다. 이 과정에서 값의 정확성이 떨어지게 된다.
정확성 때문에 발생하는 오차가 하나 더 있다. 바로 연산의 순서에 따라 결과값이 달라진다는 것이다. 아래의 예시를 보면 알 수 있다.
#include <stdio.h>
int main() {
printf("%.15lf\n", 20000000000+0.000001+0.000001);
printf("%.15lf\n", 20000000000+(0.000001+0.000001));
}
이것을 실행시키면 값이 각각 20000000000.000000000000000
, 20000000000.000003814697266
이 나온다. 첫 번째 것은 더했는데도 심지어 값이 그대로다! 일반적인 수학이라면 덧셈의 순서가 어떻든 값이 같아야하지만(결합법칙) 여기서는 성립하지 않을 수가 있다.
정리
- 사칙연산
- 나머지
- C/C++: 정수와 정수끼리만 연산
- Java/Python: 실수도 연산 가능
- 연산 시 발생할 수 있는 오류
- 오버플로우
- 정확도
- 나머지
- 수를 저장하는 타입
- 정수
char
(1),short
(2),int
(4),long
(4 or 8),long long
(8), …
- 실수
float
(4),double
(8),long double
(10), …
- 경우에 따라 적절한 타입을 선택해야 함
- 정수