2장-2 변수의 크기와 오버플로우, 언더플로우

크흠.... 기존 학교에서 배운 것들을 티스토리에 요약하고, 컴퓨터에서 삭제하려는 목적으로 '컴퓨터공학' 카테고리를 만들었습니다.

그런데 이렇게 3년이 흘러버리니 1학년 때 적어둔 것들이 어떤 순서로 필기된건지 모르겠더군요. 순서가 뒤죽박죽이어서 후에 배울 것들을 앞 장에서 응용하고 있다 보니 게시글의 순서가 뒤죽박죽이 되어 버렸습니다.

티스토리의 경우 게시된 날짜를 기준으로 정렬되는 듯하여 모든 정리가 끝나면 다시 한번 순서를 조절하도록 하겠습니다.

변수의 크기 알아보기

우선 이전 장([컴퓨터공학/C] : 2장-1 자료형)에서 각 자료형이 몇 Bytes의 크기인지 배웠습니다. 실제로 프로그래밍을 할 때 변수들의 크기가 중요합니다. 뒤에 배우게 될 구조체도 크기를 아는 것이 중요합니다.

간단한 정수형이나 실수형 변수의 크기는 외울 수 있다고 하더라도 프로그래머가 직접 구현하는 구조체의 크기까지 외울 수 있을까요? 아마 불가능할겁니다.

그래서 sizeof();연산자가 있습니다.

이 연산자는 함수가 아닙니다. 연산자입니다. 왜냐하면 프로그램이 실행될 때 sizeof();를 실행해서 크기를 계산하는 게 아니고, 프로그램을 컴파일 할 때 컴파일러가 size를 계산해서 상수로 바꿉니다. 그러니까 크기를 외워서 크기를 입력하는 것과 sizeof(); 연산자를 사용하는 것은 성능상에 차이가 없다는 의미입니다. 상수가 뭔지도 추후에 배울 예정입니다. 상수는 코드를 읽기 쉬우면서 프로그램은 가볍게 만들기에 유리합니다.

#include <stdio.h>
void main(){
    char x;
    float y;

    printf("변수 x의 크기  %d\n", sizeof(x));                //<- printf("변수 x의 크기 %d\n", 1); 과 성능 차이 전혀 없음
    printf("변수 y의 크기 %d\n", sizeof(y));
    printf("double의 크기%d\n", sizeof(double));
}

이런 식으로 활용하게 됩니다. sizeof();는 연산자가 아니기 때문에 파라미터의 형식도 상관 없고, 그게 심지어는 자료형이라도 작동합니다.

오버플로우(OverFlow)와 언더플로우(UnderFlow)

변수의 크기를 알 필요가 있는 가장 큰 이유입니다. 제대로 말하자면 조금 다릅니다. 정확한 표현으로는 오버플로우와 sizeof()를 자주 사용해야 하는 이유가 같습니다.

Overflow란, 직역 의미 그대로 넘쳐 흐른다는 의미입니다. 변수의 크기가 너무 커서 변수의 공간 안에 저장할 수 없는 경우이죠. Underflow는 반대의 의미입니다. 너무 작아서 변수의 공간 안에 담을 수 없는 경우입니다.
2장-1에서 배우기로 char형은 1 Byte 크기를 가지고 있고 -128~127의 범위를 갖고 있습니다. 그리고 1 Byte는 8 bits로 구성되어 있습니다.
만약 127을 char형 변수에 저장한다면 실제로는 0111 1111로 저장됩니다. 그리고 여기에 1을 더하면 1000 0000이 되죠. 그 값은 - 128입니다. 이런 것을 Overflow라 합니다. 반대로 -128에서 1을 빼면 127이 되는 현상을 Underflow라고 합니다.

그런데 궁금한게 있습니다! 이전 장에서도 변수의 범위까지는 언급을 했지만 왜 1000 0000이 -127이 되는지는 말하지 않았습니다. 상식적으로 맨 앞이 부호를 표시한다면 1111 1111이 -128이 되어야 하지 않을까요?
컴퓨터는 구조상 뺄셈 연산을 하지 못합니다. 구조를 단순화 하기 위해, 뺄셈은 음수를 더하는 연산을 하죠. 그래서 음수의 표현보다는 음수의 연산에 초점을 맞춰서 개발한 표현방식입니다. 이런 음수의 표현을 '2의 보수'라고 합니다.

127과 -127를 더하면 그 값은 0이 됩니다. 사람이 생각하는 대로 표현했다면 0111 1111 + 1111 1111은 1 0111 1110이라는 이상한 숫자가 나옵니다. 8 Bits 공간을 가지고 있기 때문에 Overflow는 버리고 0111 1110이 됩니다.

하지만 2의 보수를 만들어주면 덧셈이 쉬워집니다. 2의 보수는 양수에서 각 자릿수마다 ~(NOT)연산을 하고 1을 더한다고 보면 됩니다. 그러니까 0111 1111의 2의 보수를 만들면 1000 0001이 -127이 됩니다. 두 수를 덧셈 연산하면 1 0000 0000이 되죠. 위에서와 같이 Overflow를 버리면 0이 됩니다!

변수의 최댓값과 최솟값

변수의 크기는 알았는데, 그럼에도 최댓값과 최솟값은 알아야 Overflow를 방지할 수 있는것 아닌가요? 그래서 몇몇 변수에 대해서 최댓값과 최솟값을 미리 정의해둔 헤더가 있습니다. limits.h인데요, 여기서 CHAR_MIN, CHAR_MAX, UINT_MIN, UINT_MAX 등의 표현으로 정의되어 있습니다. 바로 사용해볼까요?

#include<stdio.h>
#include<limits.h>

void main(){
    char x = CHAR_MAX;
    printf("MAX of CHAR : %d \n", x);
    char y= x+ 1;
    printf("Overflow : %d \n", y);
    printf("127+(-128)  = %d\n", x+y);                 //출력은 -1이 됩니다.
}

이런 식으로 사용할 수 있는겁니다. char의 최댓값을 저장하면 127이 됩니다. 그리고 Overflow된 값(-128)을 y에 저장합니다. 그리고 이 두 수를 더하면 -1이 됩니다.

요약

  • 변수의 크기를 알 수 있다.
  • 오버플로와 언더플로를 이해할 수 있다.
  • 2의 보수를 이해하고, 음수와의 연산에서 오버플로와 언더플로를 활용하는 것을 알 수 있다.
  • 변수의 최댓값과 최솟값이 정의된 헤더를 불러올 수 있다.

변수의 크기를 알아야 하는 이유?

마지막으로 한 가지 더!

오버플로를 설명하면서 변수의 크기를 알아야 하는 이유라고 언급했습니다. 하지만 아무리 생각해봐도 관련성이 없어보입니다. 나중에 배열, 포인터나 주소의 개념을 갖고 나면 이해가 될 부분입니다. C언어는 LowLevel언어이기 때문에 메모리 주소에 직접 접근할 수 있습니다. 그만큼 프로그램이 빠르지만 프로그래머가 메모리를 고려해야 하는 단점이 있습니다.

예를 들어서 char형의 변수가 5개로 구성된 배열 name이 있습니다. 이 변수는 1 Byte 변수가 5개가 있는 것이고, 총 5 Bytes로 구성됩니다. name의 주소가 0x0020이라고 하면, 첫번째 칸은 0x0020이고, 다섯번째 칸은 0x0025가 됩니다.
그런데 프로그래머가 6번째 배열에 접근하려고 하면 C언어는 0x0026에 접근합니다. name을 통해 접근할 수 있는 주소의 범위는 0x0020~0x0025까지인데, 이에 상관없이 0x0026에 접근하는 겁니다. 물론 이런 비정상적인 접근은 OS에서 차단하고 프로그램을 강제 종료합니다. 계속 실행되면 다른 프로그램의 정보를 탈취할 수 있기 때문에 아예 종료해버리는 겁니다.

출처 : reddit.com
그 결과가 이겁니다. 옛날에는 시도 때도 없이 보던 겁니다. 수 년이 지나면서도, Windows가 업데이트 되고 업그레이드 되면서도 꾸준히 보던 경고입니다. 지금 보니 메모리 접근을 고려하는게 얼마나 어려운지 아시겠죠?

물론 다른 고급 언어들도 비슷합니다. 객체 지향 언어들도 배열은 같거든요. 하지만 대부분은 객체를 사용합니다. 그리고 그 객체는 리스트(배열 개념을 객체로 표현한 것이다. C언어는 Linked List(연결리스트)로 구현할 수 있다.)와 리스트의 갯수까지 갖고 있기 때문에 프로그래머는 마지막 변수가 필요할 때, 리스트의 크기를 가져오면 됩니다.

리스트의 개념은 C언어에서도 구조체를 이용해 연결리스트로 표현이 가능합니다. 단지 구현 방법이 좀 다를 뿐이죠. 이는 후에 배열, 포인터 부분에서 학습하게 됩니다.

다음에는 진수의 표현에 대해서 올리도록 하겠습니다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다