데이터 타입의 범위
C/C++은 변수나 상수에 저장할 수 있는 데이터의 크기와 형식을 정의하기 위해 데이터 타입을 사용하여 정의해준다.
컴파일러는 데이터 타입을 통해 메모리 할당 크기, 데이터의 해석 방식, 수행할 수 있는 연산 등을 결정한다.
이처럼, 각각의 자료형은 메모리 공간을 사용하여 변수를 할당하고, 표현 가능한 범위를 통해 값을 나타낸다.
하지만, 만약 표현 가능한 범위를 넘어간다면 어떻게 될까 ? 예를 들어 int 타입으로 변수를 지정했는데 할당된 변수가 $2,147483647$을 넘어버리거나, unsigned int 타입으로 변수를 지정했는데, 계산된 값이 음수값을 갖는다던가, ,
이렇게 할당된 데이터가 메모리 공간을 초과할 때 생기는 현상을 오버플로우와 언더플로우라고 한다.
오버플로우(OverFlow)
다시 정리하자면 오버플로우의 정의는 다음과 같다.
변수에 저장하려는 값이 해당 데이터 타입이 표현할 수 있는 최대값을 초과하는 경우 발생하는 현상
위 표에서 signed가 붙은 자료형의 경우, 양수와 음수를 모두 가질 수 있다. 여기서 컴퓨터는 양수와 음수를 구분하기 위해 최상위 비트(MSB) 를 사용한다.
그렇다면 만약 최상위비트를 제외한 모든 비트가 1로 할당된 상태에서 1을 더해준다면, 부호 비트에 영향을 주게 될 것이다.
예를 들어, char 자료형의 최댓값인 127을 8bit로 표현하면 0111 1111이다. 여기서 1을 더해준다면, 예상하는 값은 128이 될 것이고, 1000 0000로 표현이 될 것이다. 하지만, 실제로 결과를 출력해보면 128이 아니라 -128이 출력된다.
이렇게, 지정해준 데이터 타입의 메모리 공간을 넘어가는 상황에 대해서 오버플로우라고 한다.
#include <stdio.h>
int main() {
char data = 127;
printf("%d\n", data);
data++;
printf("%d\n", data);
return 0;
}
언더플로우(UnderFlow)
언더플로우는 오버플로우와 반대로 생각하면 된다.
변수에 저장하려는 값이 해당 데이터타입이 표현할 수 있는 최소값보다 더 작을 때 발생 하는 현상
오버플로우와 마찬가지로 char 자료형을 예시로 들어보자.
-128을 8bit로 나타내면 1000 0000으로 표현할 수 있다. 여기서 1을 빼준다면, 부호 비트에 영향을 주게 되고, 다시 표현하면, 0111 1111로 비트가 변경되며 값이 -129가 아닌 127로 바뀌는 것을 알 수 있다.
#include <stdio.h>
int main() {
char data = -128;
printf("%d\n", data);
data--;
printf("%d\n", data);
return 0;
}
Unsinged가 붙은 자료형
Unsigned가 붙은 자료형의 경우, 음수 부분에 대해 생각해주지 않는다. 이 말인 즉슨, 부호 비트를 사용하지 않는다. 만약, 이런 경우에 최대값을 초과하거나 최소값보다 작아진다면 어떻게될까?
우선 오버플로우의 경우, unsigned char의 최대값인 255를 8bit로 나타내면 1111 1111이 된다. 여기서 1을 더하게 된다면, 1 0000 0000 과 같이 9bit로 값이 증가하게 된다. 하지만, unsigned char 타입의 경우, 메모리의 크기가 1byte = 8bit이기 때문에, 8bit를 초과하는 비트에 대해서는 무시하게 된다. 따라서, 값은 0000 0000만 남게되어 0이 저장된다.
#include <stdio.h>
int main() {
unsigned char data = 255;
printf("%d\n", data);
data++;
printf("%d\n", data);
return 0;
}
언더플로우의 경우, 오버플로우와 반대로 생각해주면 된다. 최소값인 0을 8bit로 나타내면 0000 0000이다. 여기서 1을 빼주게 된다면, 이 연산은 감산이 아니라 비트 순환으로 처리된다.따라서, 모듈로 연산을 사용해 계산되기 때문에 값이 255인 1111 1111로 돌아가게 된다.
#include <stdio.h>
int main() {
unsigned char data = 0;
printf("%d\n", data);
data--;
printf("%d\n", data);
return 0;
}
이러한 순환 규칙은 C/C++ 표준에서 정해진 부호 없는 자료형의 오버플로우 처리 규칙에 따른 것이 된다.
추가 : Problem Solving에서 모듈러 연산 사용 시 주의사항
코딩테스트 대비 문제를 풀 경우, 가끔 모듈로 연산을 사용해야 할 경우가 존재한다.
파이썬의 경우에는 데이터 타입을 지정해주지 않고, 음수에 대한 모듈러 연산을 제공하기 때문에, 음수에 대한 모듈러 연산도 쉽게 값을 구할 수 있다.
a = 0
a -= 1
print(a % 15)
# 결과 : 14
하지만 C/C++의 경우, 어떻게 데이터타입을 지정해주었는지에 따라서 값이 달라질 뿐더러 unsigned 타입을 사용하더라도 위 예제와 같이 마지막 값을 15로 보장해주지 않고 타입의 최댓값으로 저장되기 때문에 주의가 필요하다.
#include <iostream>
using namespace std;
int main() {
unsigned char char_data = 0;
cout << (char_data - 1) % 15 << '\n';
unsigned int int_data = 0;
cout << (int_data - 1) % 15 << '\n';
return 0;
}
// char형 결과 : 255
// int형 결과 : 0
여기서, char형의 경우, 모듈러 연산을 통해 값이 255가 되고, int형의 경우, 최대값인 $4294967295 % 15$의 나머지인 0이 저장된다.
그렇다면, 문제를 풀 때, 파이썬과 같은 결과를 얻고자 한다면 어떻게 해야할까 ? 답은 간단하다. `data - 1`을 하기 이전에 나누고자 하는 값을 더해주면 된다. 즉, 15로 나눈 나머지를 얻고자 한다면, `(15 - data - 1) % 15`를 수행하면 된다.
#include <iostream>
using namespace std;
int main() {
unsigned char char_data = 0;
char_data = (15 - char_data - 1) % 15;
unsigned int int_data = 0;
unsigned int new_int_data = (15 - int_data - 1) % 15;
return 0;
}
// char형 결과 : 14
// int형 결과 : 14